From 730c3e2420667a1bec858e514aa0135ac4958c04 Mon Sep 17 00:00:00 2001 From: wenchun Date: Thu, 30 Apr 2026 10:56:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(GUI):=20=E6=96=B0=E5=A2=9E=E6=A8=A1?= =?UTF-8?q?=E6=93=AC=E9=A9=97=E8=AD=89=E6=BB=91=E5=8B=95=E9=96=8B=E9=97=9C?= =?UTF-8?q?=EF=BC=88ToggleSwitch=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在清除軌跡按鈕左邊加入滑動開關,可控制執行任務時是否開啟 matplotlib 驗證視窗。 預設開啟,OFF 時跳過驗證。 Co-Authored-By: Claude Opus 4.6 --- src/GUI/gui.py | 83 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/src/GUI/gui.py b/src/GUI/gui.py index 9b1e34f..72d5af4 100644 --- a/src/GUI/gui.py +++ b/src/GUI/gui.py @@ -5,8 +5,8 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem, QHeaderView, QPushButton, QCheckBox, QLineEdit, QComboBox, QDialog, QPlainTextEdit, QSlider) -from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent -from PyQt6.QtGui import QColor, QFont +from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent, QPropertyAnimation, pyqtProperty, QEasingCurve +from PyQt6.QtGui import QColor, QFont, QPainter, QPen import sys import asyncio import json @@ -72,6 +72,76 @@ class StreamRedirector(QObject): self.text_written.emit(line) self._buffer = "" +class ToggleSwitch(QWidget): + """自訂滑動開關 — ON 綠色 / OFF 紅色""" + toggled = pyqtSignal(bool) + + def __init__(self, checked=True, parent=None): + super().__init__(parent) + self._checked = checked + self._knob_x = 1.0 if checked else 0.0 + self.setFixedSize(44, 24) + self.setCursor(Qt.CursorShape.PointingHandCursor) + + self._animation = QPropertyAnimation(self, b"knob_position", self) + self._animation.setDuration(150) + self._animation.setEasingCurve(QEasingCurve.Type.InOutCubic) + + def _get_knob_position(self): + return self._knob_x + + def _set_knob_position(self, val): + self._knob_x = val + self.update() + + knob_position = pyqtProperty(float, _get_knob_position, _set_knob_position) + + def isChecked(self): + return self._checked + + def setChecked(self, checked): + if self._checked != checked: + self._checked = checked + self._animate(checked) + self.toggled.emit(checked) + + def mousePressEvent(self, event): + self.setChecked(not self._checked) + + def _animate(self, checked): + self._animation.stop() + self._animation.setStartValue(self._knob_x) + self._animation.setEndValue(1.0 if checked else 0.0) + self._animation.start() + + def paintEvent(self, event): + p = QPainter(self) + p.setRenderHint(QPainter.RenderHint.Antialiasing) + w, h = self.width(), self.height() + r = h / 2 + + # 背景軌道:依 knob_x 插值 灰→綠 + red = QColor(0x88, 0x88, 0x88) + green = QColor(0x4C, 0xAF, 0x50) + t = self._knob_x + bg = QColor( + int(red.red() + (green.red() - red.red()) * t), + int(red.green() + (green.green() - red.green()) * t), + int(red.blue() + (green.blue() - red.blue()) * t), + ) + p.setPen(Qt.PenStyle.NoPen) + p.setBrush(bg) + p.drawRoundedRect(0, 0, w, h, r, r) + + # 滑塊圓形 + knob_d = h - 4 + knob_range = w - knob_d - 4 + knob_cx = 2 + self._knob_x * knob_range + knob_d / 2 + p.setBrush(QColor(255, 255, 255)) + p.drawEllipse(int(knob_cx - knob_d / 2), 2, int(knob_d), int(knob_d)) + p.end() + + class ControlStationUI(QMainWindow): VERSION = '2.2.0' FONT_SCALE_MIN = 70 @@ -276,6 +346,12 @@ class ControlStationUI(QMainWindow): group_header.addLayout(title_layout) group_header.addStretch() + verify_label = QLabel("模擬驗證") + verify_label.setStyleSheet("color: #CCC; font-size: 12px;") + self.verify_toggle = ToggleSwitch(checked=True) + group_header.addWidget(verify_label) + group_header.addWidget(self.verify_toggle) + clear_traj_btn = QPushButton("清除軌跡") clear_traj_btn.setStyleSheet(""" QPushButton { background-color: #EF5350; color: white; border: none; @@ -1935,6 +2011,9 @@ class ControlStationUI(QMainWindow): selected_drones, waypoints_per_drone, origin, target_gps=None, rect_corners=None, route_waypoints=None): """存 JSON + 啟動 matplotlib 視覺化驗證 (獨立 process)""" + if not self.verify_toggle.isChecked(): + _log("INFO", "模擬驗證已關閉,跳過驗證視窗") + return import os data = { 'mission_type': mission_type,