"""
main_window
PC 側のメインウィンドウを定義するモジュール
カメラ映像のリアルタイム表示と操作 UI を提供する
"""
import numpy as np
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QImage, QPixmap
from PySide6.QtWidgets import (
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
from common import config
from pc.comm.zmq_client import PcZmqClient
# 映像更新間隔 (ms)
FRAME_INTERVAL_MS: int = 33
# 映像表示のスケール倍率
DISPLAY_SCALE: float = 2.0
class MainWindow(QMainWindow):
"""PC 側のメインウィンドウ"""
def __init__(self) -> None:
super().__init__()
self._zmq_client = PcZmqClient()
self._is_connected = False
self._setup_ui()
self._setup_timer()
def _setup_ui(self) -> None:
"""UI を構築する"""
self.setWindowTitle("RobotCar Controller")
# 中央ウィジェット
central = QWidget()
self.setCentralWidget(central)
root_layout = QHBoxLayout(central)
# 左側: 映像表示
self._video_label = QLabel("カメラ映像待機中...")
self._video_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._video_label.setMinimumSize(
int(config.FRAME_WIDTH * DISPLAY_SCALE),
int(config.FRAME_HEIGHT * DISPLAY_SCALE),
)
self._video_label.setStyleSheet(
"background-color: #222; color: #aaa; font-size: 16px;"
)
root_layout.addWidget(self._video_label, stretch=3)
# 右側: コントロールパネル
control_layout = QVBoxLayout()
root_layout.addLayout(control_layout, stretch=1)
# 接続ボタン
self._connect_btn = QPushButton("接続開始")
self._connect_btn.clicked.connect(self._toggle_connection)
control_layout.addWidget(self._connect_btn)
# ステータス表示
self._status_label = QLabel("未接続")
self._status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
control_layout.addWidget(self._status_label)
# 余白を下に詰める
control_layout.addStretch()
def _setup_timer(self) -> None:
"""映像更新用タイマーを設定する"""
self._frame_timer = QTimer(self)
self._frame_timer.timeout.connect(self._update_frame)
def _toggle_connection(self) -> None:
"""接続 / 切断を切り替える"""
if self._is_connected:
self._disconnect()
else:
self._connect()
def _connect(self) -> None:
"""ZMQ 通信を開始して映像受信を始める"""
self._zmq_client.start()
self._is_connected = True
self._connect_btn.setText("切断")
self._status_label.setText("接続中")
self._frame_timer.start(FRAME_INTERVAL_MS)
def _disconnect(self) -> None:
"""ZMQ 通信を停止する"""
self._frame_timer.stop()
self._zmq_client.stop()
self._is_connected = False
self._connect_btn.setText("接続開始")
self._status_label.setText("未接続")
self._video_label.setText("カメラ映像待機中...")
def _update_frame(self) -> None:
"""タイマーから呼び出され,最新フレームを表示する"""
frame = self._zmq_client.receive_image()
if frame is None:
return
self._display_frame(frame)
def _display_frame(self, frame: np.ndarray) -> None:
"""NumPy 配列の画像を QLabel に表示する
Args:
frame: BGR 形式の画像
"""
# BGR → RGB 変換
rgb = frame[:, :, ::-1].copy()
h, w, ch = rgb.shape
image = QImage(
rgb.data, w, h, ch * w, QImage.Format.Format_RGB888,
)
# スケーリングして表示
scaled_w = int(w * DISPLAY_SCALE)
scaled_h = int(h * DISPLAY_SCALE)
pixmap = QPixmap.fromImage(image).scaled(
scaled_w,
scaled_h,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation,
)
self._video_label.setPixmap(pixmap)
def closeEvent(self, event) -> None:
"""ウィンドウを閉じるときに通信を停止する"""
if self._is_connected:
self._disconnect()
event.accept()