Update GUI 2.2.0: Settings tab

wenchun
ken910606 1 week ago
parent edd15df3fc
commit 1426a618f4

@ -1,10 +1,78 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QLineEdit, QComboBox) QPushButton, QLineEdit, QComboBox, QApplication)
from PyQt6.QtGui import QFont
from PyQt6.QtCore import pyqtSignal from PyQt6.QtCore import pyqtSignal
import glob import glob
import os import os
def _get_font_scale():
app = QApplication.instance()
if app is None:
return 1.0
scale = app.property("font_scale")
try:
return float(scale) if scale is not None else 1.0
except (TypeError, ValueError):
return 1.0
def _scale_stylesheet_font_sizes(stylesheet, scale):
if not stylesheet or 'font-size' not in stylesheet:
return stylesheet
import re
def repl(match):
size = float(match.group(1))
unit = match.group(2)
scaled = max(1.0, size * scale)
text = f"{scaled:.2f}".rstrip('0').rstrip('.')
return f"font-size: {text}{unit}"
return re.sub(r'font-size\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*(px|pt)', repl, stylesheet)
def _set_scaled_stylesheet(widget, stylesheet):
widget._base_stylesheet = stylesheet
scaled = _scale_stylesheet_font_sizes(stylesheet, _get_font_scale())
widget._applied_stylesheet = scaled
widget.setStyleSheet(scaled)
def _reapply_scaled_stylesheet(widget):
current_stylesheet = widget.styleSheet()
base_stylesheet = getattr(widget, '_base_stylesheet', None)
applied_stylesheet = getattr(widget, '_applied_stylesheet', None)
if current_stylesheet != applied_stylesheet:
base_stylesheet = current_stylesheet
widget._base_stylesheet = base_stylesheet
if base_stylesheet is not None:
scaled = _scale_stylesheet_font_sizes(base_stylesheet, _get_font_scale())
widget._applied_stylesheet = scaled
if current_stylesheet != scaled:
widget.setStyleSheet(scaled)
def _apply_scaled_font(widget):
base_font = getattr(widget, '_base_font_for_scale', None)
if base_font is None:
app = QApplication.instance()
app_base_font = app.property("base_app_font") if app else None
base_font = QFont(app_base_font) if app_base_font is not None else QFont(widget.font())
widget._base_font_for_scale = QFont(base_font)
scaled_font = QFont(base_font)
scale = _get_font_scale()
if base_font.pointSizeF() > 0:
scaled_font.setPointSizeF(max(1.0, base_font.pointSizeF() * scale))
elif base_font.pointSize() > 0:
scaled_font.setPointSize(max(1, int(round(base_font.pointSize() * scale))))
widget.setFont(scaled_font)
class CommPanel(QWidget): class CommPanel(QWidget):
"""通讯设置面板类""" """通讯设置面板类"""
@ -26,6 +94,7 @@ class CommPanel(QWidget):
self.ws_connections = [] self.ws_connections = []
self.serial_connections = [] self.serial_connections = []
self._init_ui() self._init_ui()
self.apply_font_scale()
def _init_ui(self): def _init_ui(self):
"""初始化UI""" """初始化UI"""
@ -35,7 +104,7 @@ class CommPanel(QWidget):
# ========== UDP MAVLink 區域 ========== # ========== UDP MAVLink 區域 ==========
udp_title = QLabel("UDP") udp_title = QLabel("UDP")
udp_title.setStyleSheet(""" _set_scaled_stylesheet(udp_title, """
color: #DDD; color: #DDD;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
@ -60,7 +129,7 @@ class CommPanel(QWidget):
self.udp_ip_input = QLineEdit() self.udp_ip_input = QLineEdit()
self.udp_ip_input.setText("127.0.0.1") self.udp_ip_input.setText("127.0.0.1")
self.udp_ip_input.setPlaceholderText("IP") self.udp_ip_input.setPlaceholderText("IP")
self.udp_ip_input.setStyleSheet(""" _set_scaled_stylesheet(self.udp_ip_input, """
QLineEdit { QLineEdit {
background-color: #333; background-color: #333;
color: #DDD; color: #DDD;
@ -74,7 +143,7 @@ class CommPanel(QWidget):
self.udp_port_input.setText("14550") self.udp_port_input.setText("14550")
self.udp_port_input.setPlaceholderText("Port") self.udp_port_input.setPlaceholderText("Port")
self.udp_port_input.setFixedWidth(80) self.udp_port_input.setFixedWidth(80)
self.udp_port_input.setStyleSheet(""" _set_scaled_stylesheet(self.udp_port_input, """
QLineEdit { QLineEdit {
background-color: #333; background-color: #333;
color: #DDD; color: #DDD;
@ -86,7 +155,7 @@ class CommPanel(QWidget):
add_udp_btn = QPushButton("添加") add_udp_btn = QPushButton("添加")
add_udp_btn.clicked.connect(self._handle_add_udp) add_udp_btn.clicked.connect(self._handle_add_udp)
add_udp_btn.setStyleSheet(""" _set_scaled_stylesheet(add_udp_btn, """
QPushButton { QPushButton {
background-color: #4CAF50; background-color: #4CAF50;
color: white; color: white;
@ -98,9 +167,13 @@ class CommPanel(QWidget):
QPushButton:hover { background-color: #45a049; } QPushButton:hover { background-color: #45a049; }
""") """)
add_udp_layout.addWidget(QLabel("IP:", styleSheet="color: #DDD;")) ip_label = QLabel("IP:")
_set_scaled_stylesheet(ip_label, "color: #DDD;")
add_udp_layout.addWidget(ip_label)
add_udp_layout.addWidget(self.udp_ip_input) add_udp_layout.addWidget(self.udp_ip_input)
add_udp_layout.addWidget(QLabel("Port:", styleSheet="color: #DDD;")) port_label = QLabel("Port:")
_set_scaled_stylesheet(port_label, "color: #DDD;")
add_udp_layout.addWidget(port_label)
add_udp_layout.addWidget(self.udp_port_input) add_udp_layout.addWidget(self.udp_port_input)
add_udp_layout.addWidget(add_udp_btn) add_udp_layout.addWidget(add_udp_btn)
@ -113,7 +186,7 @@ class CommPanel(QWidget):
# ========== Serial 區域 ========== # ========== Serial 區域 ==========
serial_title = QLabel("Serial") serial_title = QLabel("Serial")
serial_title.setStyleSheet(""" _set_scaled_stylesheet(serial_title, """
color: #DDD; color: #DDD;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
@ -136,7 +209,7 @@ class CommPanel(QWidget):
add_serial_layout.setContentsMargins(0, 0, 0, 0) add_serial_layout.setContentsMargins(0, 0, 0, 0)
self.serial_port_combo = QComboBox() self.serial_port_combo = QComboBox()
self.serial_port_combo.setStyleSheet(""" _set_scaled_stylesheet(self.serial_port_combo, """
QComboBox { QComboBox {
background-color: #333; background-color: #333;
color: #DDD; color: #DDD;
@ -160,7 +233,7 @@ class CommPanel(QWidget):
refresh_ports_btn.setFixedWidth(35) refresh_ports_btn.setFixedWidth(35)
refresh_ports_btn.clicked.connect(self._refresh_serial_ports) refresh_ports_btn.clicked.connect(self._refresh_serial_ports)
refresh_ports_btn.setToolTip("重新掃描串口") refresh_ports_btn.setToolTip("重新掃描串口")
refresh_ports_btn.setStyleSheet(""" _set_scaled_stylesheet(refresh_ports_btn, """
QPushButton { QPushButton {
background-color: #444; background-color: #444;
color: #DDD; color: #DDD;
@ -176,7 +249,7 @@ class CommPanel(QWidget):
self.serial_baudrate_combo.addItems(['9600', '19200', '38400', '57600', '115200']) self.serial_baudrate_combo.addItems(['9600', '19200', '38400', '57600', '115200'])
self.serial_baudrate_combo.setCurrentText('57600') self.serial_baudrate_combo.setCurrentText('57600')
self.serial_baudrate_combo.setFixedWidth(100) self.serial_baudrate_combo.setFixedWidth(100)
self.serial_baudrate_combo.setStyleSheet(""" _set_scaled_stylesheet(self.serial_baudrate_combo, """
QComboBox { QComboBox {
background-color: #333; background-color: #333;
color: #DDD; color: #DDD;
@ -197,7 +270,7 @@ class CommPanel(QWidget):
add_serial_btn = QPushButton("添加") add_serial_btn = QPushButton("添加")
add_serial_btn.clicked.connect(self._handle_add_serial) add_serial_btn.clicked.connect(self._handle_add_serial)
add_serial_btn.setStyleSheet(""" _set_scaled_stylesheet(add_serial_btn, """
QPushButton { QPushButton {
background-color: #4CAF50; background-color: #4CAF50;
color: white; color: white;
@ -209,10 +282,14 @@ class CommPanel(QWidget):
QPushButton:hover { background-color: #45a049; } QPushButton:hover { background-color: #45a049; }
""") """)
add_serial_layout.addWidget(QLabel("Port:", styleSheet="color: #DDD;")) serial_port_label = QLabel("Port:")
_set_scaled_stylesheet(serial_port_label, "color: #DDD;")
add_serial_layout.addWidget(serial_port_label)
add_serial_layout.addWidget(self.serial_port_combo) add_serial_layout.addWidget(self.serial_port_combo)
add_serial_layout.addWidget(refresh_ports_btn) add_serial_layout.addWidget(refresh_ports_btn)
add_serial_layout.addWidget(QLabel("Baud:", styleSheet="color: #DDD;")) baud_label = QLabel("Baud:")
_set_scaled_stylesheet(baud_label, "color: #DDD;")
add_serial_layout.addWidget(baud_label)
add_serial_layout.addWidget(self.serial_baudrate_combo) add_serial_layout.addWidget(self.serial_baudrate_combo)
add_serial_layout.addWidget(add_serial_btn) add_serial_layout.addWidget(add_serial_btn)
@ -225,7 +302,7 @@ class CommPanel(QWidget):
# ========== WebSocket 區域 ========== # ========== WebSocket 區域 ==========
ws_title = QLabel("WebSocket") ws_title = QLabel("WebSocket")
ws_title.setStyleSheet(""" _set_scaled_stylesheet(ws_title, """
color: #DDD; color: #DDD;
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
@ -249,7 +326,7 @@ class CommPanel(QWidget):
self.ws_url_input = QLineEdit() self.ws_url_input = QLineEdit()
self.ws_url_input.setPlaceholderText("host") self.ws_url_input.setPlaceholderText("host")
self.ws_url_input.setStyleSheet(""" _set_scaled_stylesheet(self.ws_url_input, """
QLineEdit { QLineEdit {
background-color: #333; background-color: #333;
color: #DDD; color: #DDD;
@ -261,7 +338,7 @@ class CommPanel(QWidget):
add_ws_btn = QPushButton("添加") add_ws_btn = QPushButton("添加")
add_ws_btn.clicked.connect(self._handle_add_ws) add_ws_btn.clicked.connect(self._handle_add_ws)
add_ws_btn.setStyleSheet(""" _set_scaled_stylesheet(add_ws_btn, """
QPushButton { QPushButton {
background-color: #4CAF50; background-color: #4CAF50;
color: white; color: white;
@ -273,7 +350,9 @@ class CommPanel(QWidget):
QPushButton:hover { background-color: #45a049; } QPushButton:hover { background-color: #45a049; }
""") """)
add_ws_layout.addWidget(QLabel("URL:", styleSheet="color: #DDD;")) url_label = QLabel("URL:")
_set_scaled_stylesheet(url_label, "color: #DDD;")
add_ws_layout.addWidget(url_label)
add_ws_layout.addWidget(self.ws_url_input) add_ws_layout.addWidget(self.ws_url_input)
add_ws_layout.addWidget(add_ws_btn) add_ws_layout.addWidget(add_ws_btn)
@ -437,7 +516,7 @@ class CommPanel(QWidget):
def create_udp_connection_panel(self, conn): def create_udp_connection_panel(self, conn):
"""創建 UDP 連接面板""" """創建 UDP 連接面板"""
panel = QWidget() panel = QWidget()
panel.setStyleSheet(""" _set_scaled_stylesheet(panel, """
QWidget { QWidget {
background-color: #2A2A2A; background-color: #2A2A2A;
border-radius: 6px; border-radius: 6px;
@ -451,22 +530,22 @@ class CommPanel(QWidget):
# 連接資訊 # 連接資訊
info_label = QLabel(f"{conn['name']} - {conn['ip']}:{conn['port']}") info_label = QLabel(f"{conn['name']} - {conn['ip']}:{conn['port']}")
info_label.setStyleSheet("color: #DDD; font-size: 12px;") _set_scaled_stylesheet(info_label, "color: #DDD; font-size: 12px;")
# 狀態指示器 # 狀態指示器
status_label = QLabel("") status_label = QLabel("")
if conn.get('enabled', False): if conn.get('enabled', False):
status_label.setStyleSheet("color: #4CAF50; font-size: 16px;") _set_scaled_stylesheet(status_label, "color: #4CAF50; font-size: 16px;")
status_label.setToolTip("運行中") status_label.setToolTip("運行中")
else: else:
status_label.setStyleSheet("color: #888; font-size: 16px;") _set_scaled_stylesheet(status_label, "color: #888; font-size: 16px;")
status_label.setToolTip("已停止") status_label.setToolTip("已停止")
# 控制按鈕 # 控制按鈕
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動") toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
toggle_btn.setFixedWidth(60) toggle_btn.setFixedWidth(60)
toggle_btn.clicked.connect(lambda: self.udp_connection_toggled.emit(conn, toggle_btn, status_label)) toggle_btn.clicked.connect(lambda: self.udp_connection_toggled.emit(conn, toggle_btn, status_label))
toggle_btn.setStyleSheet(""" _set_scaled_stylesheet(toggle_btn, """
QPushButton { QPushButton {
background-color: #444; background-color: #444;
color: #DDD; color: #DDD;
@ -481,7 +560,7 @@ class CommPanel(QWidget):
remove_btn = QPushButton("移除") remove_btn = QPushButton("移除")
remove_btn.setFixedWidth(60) remove_btn.setFixedWidth(60)
remove_btn.clicked.connect(lambda: self.udp_connection_removed.emit(conn, panel)) remove_btn.clicked.connect(lambda: self.udp_connection_removed.emit(conn, panel))
remove_btn.setStyleSheet(""" _set_scaled_stylesheet(remove_btn, """
QPushButton { QPushButton {
background-color: #d32f2f; background-color: #d32f2f;
color: white; color: white;
@ -509,7 +588,7 @@ class CommPanel(QWidget):
def create_ws_connection_panel(self, conn): def create_ws_connection_panel(self, conn):
"""創建 WebSocket 連接面板""" """創建 WebSocket 連接面板"""
panel = QWidget() panel = QWidget()
panel.setStyleSheet(""" _set_scaled_stylesheet(panel, """
QWidget { QWidget {
background-color: #2A2A2A; background-color: #2A2A2A;
border-radius: 6px; border-radius: 6px;
@ -523,22 +602,22 @@ class CommPanel(QWidget):
# 連接資訊 # 連接資訊
info_label = QLabel(f"{conn['name']} - {conn['url']}") info_label = QLabel(f"{conn['name']} - {conn['url']}")
info_label.setStyleSheet("color: #DDD; font-size: 12px;") _set_scaled_stylesheet(info_label, "color: #DDD; font-size: 12px;")
# 狀態指示器 # 狀態指示器
status_label = QLabel("") status_label = QLabel("")
if conn.get('enabled', False): if conn.get('enabled', False):
status_label.setStyleSheet("color: #4CAF50; font-size: 16px;") _set_scaled_stylesheet(status_label, "color: #4CAF50; font-size: 16px;")
status_label.setToolTip("運行中") status_label.setToolTip("運行中")
else: else:
status_label.setStyleSheet("color: #888; font-size: 16px;") _set_scaled_stylesheet(status_label, "color: #888; font-size: 16px;")
status_label.setToolTip("已停止") status_label.setToolTip("已停止")
# 控制按鈕 # 控制按鈕
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動") toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
toggle_btn.setFixedWidth(60) toggle_btn.setFixedWidth(60)
toggle_btn.clicked.connect(lambda: self.ws_connection_toggled.emit(conn, toggle_btn, status_label)) toggle_btn.clicked.connect(lambda: self.ws_connection_toggled.emit(conn, toggle_btn, status_label))
toggle_btn.setStyleSheet(""" _set_scaled_stylesheet(toggle_btn, """
QPushButton { QPushButton {
background-color: #444; background-color: #444;
color: #DDD; color: #DDD;
@ -553,7 +632,7 @@ class CommPanel(QWidget):
remove_btn = QPushButton("移除") remove_btn = QPushButton("移除")
remove_btn.setFixedWidth(60) remove_btn.setFixedWidth(60)
remove_btn.clicked.connect(lambda: self.ws_connection_removed.emit(conn, panel)) remove_btn.clicked.connect(lambda: self.ws_connection_removed.emit(conn, panel))
remove_btn.setStyleSheet(""" _set_scaled_stylesheet(remove_btn, """
QPushButton { QPushButton {
background-color: #d32f2f; background-color: #d32f2f;
color: white; color: white;
@ -605,7 +684,7 @@ class CommPanel(QWidget):
def create_serial_connection_panel(self, conn): def create_serial_connection_panel(self, conn):
"""創建 Serial 連接面板""" """創建 Serial 連接面板"""
panel = QWidget() panel = QWidget()
panel.setStyleSheet(""" _set_scaled_stylesheet(panel, """
QWidget { QWidget {
background-color: #2A2A2A; background-color: #2A2A2A;
border-radius: 6px; border-radius: 6px;
@ -619,22 +698,22 @@ class CommPanel(QWidget):
# 連接資訊 # 連接資訊
info_label = QLabel(f"{conn['name']} - {conn['port']} @ {conn['baudrate']}") info_label = QLabel(f"{conn['name']} - {conn['port']} @ {conn['baudrate']}")
info_label.setStyleSheet("color: #DDD; font-size: 12px;") _set_scaled_stylesheet(info_label, "color: #DDD; font-size: 12px;")
# 狀態指示器 # 狀態指示器
status_label = QLabel("") status_label = QLabel("")
if conn.get('enabled', False): if conn.get('enabled', False):
status_label.setStyleSheet("color: #4CAF50; font-size: 16px;") _set_scaled_stylesheet(status_label, "color: #4CAF50; font-size: 16px;")
status_label.setToolTip("運行中") status_label.setToolTip("運行中")
else: else:
status_label.setStyleSheet("color: #888; font-size: 16px;") _set_scaled_stylesheet(status_label, "color: #888; font-size: 16px;")
status_label.setToolTip("已停止") status_label.setToolTip("已停止")
# 控制按鈕 # 控制按鈕
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動") toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
toggle_btn.setFixedWidth(60) toggle_btn.setFixedWidth(60)
toggle_btn.clicked.connect(lambda: self.serial_connection_toggled.emit(conn, toggle_btn, status_label)) toggle_btn.clicked.connect(lambda: self.serial_connection_toggled.emit(conn, toggle_btn, status_label))
toggle_btn.setStyleSheet(""" _set_scaled_stylesheet(toggle_btn, """
QPushButton { QPushButton {
background-color: #444; background-color: #444;
color: #DDD; color: #DDD;
@ -649,7 +728,7 @@ class CommPanel(QWidget):
remove_btn = QPushButton("移除") remove_btn = QPushButton("移除")
remove_btn.setFixedWidth(60) remove_btn.setFixedWidth(60)
remove_btn.clicked.connect(lambda: self.serial_connection_removed.emit(conn, panel)) remove_btn.clicked.connect(lambda: self.serial_connection_removed.emit(conn, panel))
remove_btn.setStyleSheet(""" _set_scaled_stylesheet(remove_btn, """
QPushButton { QPushButton {
background-color: #d32f2f; background-color: #d32f2f;
color: white; color: white;
@ -684,4 +763,12 @@ class CommPanel(QWidget):
def remove_serial_connection_from_list(self, conn): def remove_serial_connection_from_list(self, conn):
"""從列表中移除 Serial 連接""" """從列表中移除 Serial 連接"""
if conn in self.serial_connections: if conn in self.serial_connections:
self.serial_connections.remove(conn) self.serial_connections.remove(conn)
def apply_font_scale(self):
"""重新套用目前字體倍率到通訊面板。"""
_apply_scaled_font(self)
_reapply_scaled_stylesheet(self)
for child in self.findChildren(QWidget):
_apply_scaled_font(child)
_reapply_scaled_stylesheet(child)

@ -1,10 +1,77 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox) from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QCheckBox, QApplication)
from PyQt6.QtCore import pyqtSignal from PyQt6.QtCore import pyqtSignal
from PyQt6.QtGui import QPainter, QPen, QColor, QFont, QPolygonF from PyQt6.QtGui import QPainter, QPen, QColor, QFont, QPolygonF
from PyQt6.QtCore import QPointF, Qt from PyQt6.QtCore import QPointF, Qt
import math import math
def _get_font_scale():
app = QApplication.instance()
if app is None:
return 1.0
scale = app.property("font_scale")
try:
return float(scale) if scale is not None else 1.0
except (TypeError, ValueError):
return 1.0
def _scale_stylesheet_font_sizes(stylesheet, scale):
if not stylesheet or 'font-size' not in stylesheet:
return stylesheet
import re
def repl(match):
size = float(match.group(1))
unit = match.group(2)
scaled = max(1.0, size * scale)
text = f"{scaled:.2f}".rstrip('0').rstrip('.')
return f"font-size: {text}{unit}"
return re.sub(r'font-size\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*(px|pt)', repl, stylesheet)
def _set_scaled_stylesheet(widget, stylesheet):
widget._base_stylesheet = stylesheet
scaled = _scale_stylesheet_font_sizes(stylesheet, _get_font_scale())
widget._applied_stylesheet = scaled
widget.setStyleSheet(scaled)
def _reapply_scaled_stylesheet(widget):
current_stylesheet = widget.styleSheet()
base_stylesheet = getattr(widget, '_base_stylesheet', None)
applied_stylesheet = getattr(widget, '_applied_stylesheet', None)
if current_stylesheet != applied_stylesheet:
base_stylesheet = current_stylesheet
widget._base_stylesheet = base_stylesheet
if base_stylesheet is not None:
scaled = _scale_stylesheet_font_sizes(base_stylesheet, _get_font_scale())
widget._applied_stylesheet = scaled
if current_stylesheet != scaled:
widget.setStyleSheet(scaled)
def _apply_scaled_font(widget):
base_font = getattr(widget, '_base_font_for_scale', None)
if base_font is None:
app = QApplication.instance()
app_base_font = app.property("base_app_font") if app else None
base_font = QFont(app_base_font) if app_base_font is not None else QFont(widget.font())
widget._base_font_for_scale = QFont(base_font)
scaled_font = QFont(base_font)
scale = _get_font_scale()
if base_font.pointSizeF() > 0:
scaled_font.setPointSizeF(max(1.0, base_font.pointSizeF() * scale))
elif base_font.pointSize() > 0:
scaled_font.setPointSize(max(1, int(round(base_font.pointSize() * scale))))
widget.setFont(scaled_font)
class DronePanel(QWidget): class DronePanel(QWidget):
"""單個無人機面板類別""" """單個無人機面板類別"""
@ -32,12 +99,13 @@ class DronePanel(QWidget):
self.attitude_indicator = None self.attitude_indicator = None
self._init_ui() self._init_ui()
self.apply_font_scale()
def _init_ui(self): def _init_ui(self):
"""初始化UI""" """初始化UI"""
self.setObjectName(f"panel_{self.drone_id}") self.setObjectName(f"panel_{self.drone_id}")
self.setFixedHeight(140) self.setFixedHeight(140)
self.setStyleSheet(""" _set_scaled_stylesheet(self, """
background-color: #2A2A2A; background-color: #2A2A2A;
border-radius: 8px; border-radius: 8px;
""") """)
@ -49,7 +117,7 @@ class DronePanel(QWidget):
# 創建內容容器(包含 info 和 control # 創建內容容器(包含 info 和 control
content_widget = QWidget() content_widget = QWidget()
content_widget.setStyleSheet("background-color: #333; border-radius: 6px;") _set_scaled_stylesheet(content_widget, "background-color: #333; border-radius: 6px;")
content_layout = QHBoxLayout(content_widget) content_layout = QHBoxLayout(content_widget)
content_layout.setContentsMargins(8, 8, 8, 8) content_layout.setContentsMargins(8, 8, 8, 8)
content_layout.setSpacing(8) content_layout.setSpacing(8)
@ -82,7 +150,7 @@ class DronePanel(QWidget):
# 勾選框 # 勾選框
self.checkbox = QCheckBox() self.checkbox = QCheckBox()
self.checkbox.setObjectName(f"{self.drone_id}_checkbox") self.checkbox.setObjectName(f"{self.drone_id}_checkbox")
self.checkbox.setStyleSheet(""" _set_scaled_stylesheet(self.checkbox, """
QCheckBox { QCheckBox {
color: #DDD; color: #DDD;
} }
@ -103,8 +171,8 @@ class DronePanel(QWidget):
) )
# ID 顯示 # ID 顯示
id_label = QLabel(self.display_id) self.id_label = QLabel(self.display_id)
id_label.setStyleSheet(""" _set_scaled_stylesheet(self.id_label, """
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 14px;
color: #7FFFD4; color: #7FFFD4;
@ -112,7 +180,7 @@ class DronePanel(QWidget):
""") """)
header_layout.addWidget(self.checkbox) header_layout.addWidget(self.checkbox)
header_layout.addWidget(id_label) header_layout.addWidget(self.id_label)
header_layout.addStretch() header_layout.addStretch()
info_layout.addWidget(header) info_layout.addWidget(header)
@ -148,15 +216,15 @@ class DronePanel(QWidget):
status_layout.setContentsMargins(0, 0, 0, 0) status_layout.setContentsMargins(0, 0, 0, 0)
status_title = QLabel("狀態:") status_title = QLabel("狀態:")
status_title.setStyleSheet("color: #888; min-width: 50px;") _set_scaled_stylesheet(status_title, "color: #888; min-width: 50px;")
self.mode_label = QLabel("--") self.mode_label = QLabel("--")
self.mode_label.setObjectName(f"{self.drone_id}_mode") self.mode_label.setObjectName(f"{self.drone_id}_mode")
self.mode_label.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(self.mode_label, "color: #DDD;")
self.armed_label = QLabel("--") self.armed_label = QLabel("--")
self.armed_label.setObjectName(f"{self.drone_id}_armed") self.armed_label.setObjectName(f"{self.drone_id}_armed")
self.armed_label.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(self.armed_label, "color: #DDD;")
status_layout.addWidget(status_title) status_layout.addWidget(status_title)
status_layout.addWidget(self.mode_label) status_layout.addWidget(self.mode_label)
@ -172,15 +240,15 @@ class DronePanel(QWidget):
connection_layout.setContentsMargins(0, 0, 0, 0) connection_layout.setContentsMargins(0, 0, 0, 0)
connection_title = QLabel("Socket") connection_title = QLabel("Socket")
connection_title.setStyleSheet("color: #888; min-width: 50px;") _set_scaled_stylesheet(connection_title, "color: #888; min-width: 50px;")
# 根據解析的 drone_id 資訊設定初始值 # 根據解析的 drone_id 資訊設定初始值
self.socket_seq_label = QLabel(self.socket_seq) self.socket_seq_label = QLabel(self.socket_seq)
self.socket_seq_label.setObjectName(f"{self.drone_id}_socket_seq") self.socket_seq_label.setObjectName(f"{self.drone_id}_socket_seq")
self.socket_seq_label.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(self.socket_seq_label, "color: #DDD;")
connection_sep = QLabel(" - ") connection_sep = QLabel(" - ")
connection_sep.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(connection_sep, "color: #DDD;")
# 設定連接方式顯示 # 設定連接方式顯示
connection_type_map = { connection_type_map = {
@ -193,7 +261,7 @@ class DronePanel(QWidget):
self.connection_type_label = QLabel(connection_type) self.connection_type_label = QLabel(connection_type)
self.connection_type_label.setObjectName(f"{self.drone_id}_connection_type") self.connection_type_label.setObjectName(f"{self.drone_id}_connection_type")
self.connection_type_label.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(self.connection_type_label, "color: #DDD;")
connection_layout.addWidget(connection_title) connection_layout.addWidget(connection_title)
connection_layout.addWidget(self.socket_seq_label) connection_layout.addWidget(self.socket_seq_label)
@ -210,29 +278,29 @@ class DronePanel(QWidget):
battery_layout.setContentsMargins(0, 0, 0, 0) battery_layout.setContentsMargins(0, 0, 0, 0)
# 顯示百分比 # 顯示百分比
battery_title = QLabel("電池:") battery_title = QLabel("電池:")
battery_title.setStyleSheet("color: #888; min-width: 50px;") _set_scaled_stylesheet(battery_title, "color: #888; min-width: 50px;")
self.battery_pct_label = QLabel("--") self.battery_pct_label = QLabel("--")
self.battery_pct_label.setObjectName(f"{self.drone_id}_battery_pct") self.battery_pct_label.setObjectName(f"{self.drone_id}_battery_pct")
self.battery_pct_label.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(self.battery_pct_label, "color: #DDD;")
# 分隔符 # 分隔符
separator1 = QLabel(" - ") separator1 = QLabel(" - ")
separator1.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(separator1, "color: #DDD;")
# 顯示電壓 # 顯示電壓
self.battery_vol_label = QLabel("--") self.battery_vol_label = QLabel("--")
self.battery_vol_label.setObjectName(f"{self.drone_id}_battery_vol") self.battery_vol_label.setObjectName(f"{self.drone_id}_battery_vol")
self.battery_vol_label.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(self.battery_vol_label, "color: #DDD;")
# 分隔符 # 分隔符
separator2 = QLabel(" - ") separator2 = QLabel(" - ")
separator2.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(separator2, "color: #DDD;")
# 顯示電池節數 (S count) # 顯示電池節數 (S count)
self.battery_cells_label = QLabel("--") self.battery_cells_label = QLabel("--")
self.battery_cells_label.setObjectName(f"{self.drone_id}_battery_cells") self.battery_cells_label.setObjectName(f"{self.drone_id}_battery_cells")
self.battery_cells_label.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(self.battery_cells_label, "color: #DDD;")
battery_layout.addWidget(battery_title) battery_layout.addWidget(battery_title)
battery_layout.addWidget(self.battery_pct_label) battery_layout.addWidget(self.battery_pct_label)
@ -251,18 +319,18 @@ class DronePanel(QWidget):
altitude_layout.setContentsMargins(0, 0, 0, 0) altitude_layout.setContentsMargins(0, 0, 0, 0)
altitude_title = QLabel("高度:") altitude_title = QLabel("高度:")
altitude_title.setStyleSheet("color: #888; min-width: 50px;") _set_scaled_stylesheet(altitude_title, "color: #888; min-width: 50px;")
self.altitude_label = QLabel("--") self.altitude_label = QLabel("--")
self.altitude_label.setObjectName(f"{self.drone_id}_altitude") self.altitude_label.setObjectName(f"{self.drone_id}_altitude")
self.altitude_label.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(self.altitude_label, "color: #DDD;")
speed_title = QLabel("速度:") speed_title = QLabel("速度:")
speed_title.setStyleSheet("color: #888; margin-left: 10px;") _set_scaled_stylesheet(speed_title, "color: #888; margin-left: 10px;")
self.speed_label = QLabel("--") self.speed_label = QLabel("--")
self.speed_label.setObjectName(f"{self.drone_id}_speed") self.speed_label.setObjectName(f"{self.drone_id}_speed")
self.speed_label.setStyleSheet("color: #DDD;") _set_scaled_stylesheet(self.speed_label, "color: #DDD;")
altitude_layout.addWidget(altitude_title) altitude_layout.addWidget(altitude_title)
altitude_layout.addWidget(self.altitude_label) altitude_layout.addWidget(self.altitude_label)
@ -320,6 +388,16 @@ class DronePanel(QWidget):
def is_checked(self): def is_checked(self):
"""獲取勾選狀態""" """獲取勾選狀態"""
return self.checkbox.isChecked() return self.checkbox.isChecked()
def apply_font_scale(self):
"""重新套用目前字體倍率到 panel 內所有元件。"""
_apply_scaled_font(self)
_reapply_scaled_stylesheet(self)
for child in self.findChildren(QWidget):
_apply_scaled_font(child)
_reapply_scaled_stylesheet(child)
if self.attitude_indicator:
self.attitude_indicator.update()
class SocketGroupPanel(QWidget): class SocketGroupPanel(QWidget):
# 定義信號 # 定義信號
@ -331,11 +409,12 @@ class SocketGroupPanel(QWidget):
self.color = color self.color = color
self.socket_type = socket_type self.socket_type = socket_type
self._init_ui() self._init_ui()
self.apply_font_scale()
def _init_ui(self): def _init_ui(self):
"""初始化UI""" """初始化UI"""
self.setObjectName(f"socket_group_{self.socket_id}") self.setObjectName(f"socket_group_{self.socket_id}")
self.setStyleSheet(""" _set_scaled_stylesheet(self, """
background-color: #1E1E1E; background-color: #1E1E1E;
border-radius: 12px; border-radius: 12px;
""") """)
@ -352,7 +431,7 @@ class SocketGroupPanel(QWidget):
# 分組勾選框 # 分組勾選框
self.group_checkbox = QCheckBox() self.group_checkbox = QCheckBox()
self.group_checkbox.setObjectName(f"socket_{self.socket_id}_checkbox") self.group_checkbox.setObjectName(f"socket_{self.socket_id}_checkbox")
self.group_checkbox.setStyleSheet(f""" _set_scaled_stylesheet(self.group_checkbox, f"""
QCheckBox {{ color: #DDD; }} QCheckBox {{ color: #DDD; }}
QCheckBox::indicator {{ QCheckBox::indicator {{
width: 14px; width: 14px;
@ -380,7 +459,7 @@ class SocketGroupPanel(QWidget):
else: else:
title_text = f"Socket {self.socket_id}" title_text = f"Socket {self.socket_id}"
self.title_label = QLabel(title_text) self.title_label = QLabel(title_text)
self.title_label.setStyleSheet(f""" _set_scaled_stylesheet(self.title_label, f"""
font-weight: bold; font-weight: bold;
font-size: 16px; font-size: 16px;
color: {self.color}; color: {self.color};
@ -430,6 +509,14 @@ class SocketGroupPanel(QWidget):
"""設置分組勾選狀態(支持半選)""" """設置分組勾選狀態(支持半選)"""
self.group_checkbox.setCheckState(state) self.group_checkbox.setCheckState(state)
def apply_font_scale(self):
"""重新套用目前字體倍率到 socket 分組面板。"""
_apply_scaled_font(self)
_reapply_scaled_stylesheet(self)
for child in self.findChildren(QWidget):
_apply_scaled_font(child)
_reapply_scaled_stylesheet(child)
class AttitudeIndicator(QWidget): class AttitudeIndicator(QWidget):
""" """
@ -503,7 +590,9 @@ class AttitudeIndicator(QWidget):
# pitch ladder (every 10°, ±30°) # pitch ladder (every 10°, ±30°)
p.setPen(QPen(QColor(255, 255, 255, 180), 1)) p.setPen(QPen(QColor(255, 255, 255, 180), 1))
p.setFont(QFont("Arial", 6)) ladder_font = QFont("Arial")
ladder_font.setPointSizeF(max(1.0, 6 * _get_font_scale()))
p.setFont(ladder_font)
for deg in range(-30, 31, 10): for deg in range(-30, 31, 10):
if deg == 0: if deg == 0:
continue continue
@ -581,6 +670,8 @@ class AttitudeIndicator(QWidget):
# heading text centred (bigger) # heading text centred (bigger)
p.setPen(QPen(QColor("#FFFFFF"))) p.setPen(QPen(QColor("#FFFFFF")))
p.setFont(QFont("Arial", 10, QFont.Weight.Bold)) heading_font = QFont("Arial", weight=QFont.Weight.Bold)
heading_font.setPointSizeF(max(1.0, 10 * _get_font_scale()))
p.setFont(heading_font)
hdg_str = f"{int(self.heading)}°" hdg_str = f"{int(self.heading)}°"
p.drawText(0, int(strip_y), w, strip_h, Qt.AlignmentFlag.AlignCenter, hdg_str) p.drawText(0, int(strip_y), w, strip_h, Qt.AlignmentFlag.AlignCenter, hdg_str)

@ -4,15 +4,16 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout
QWidget, QLabel, QSplitter, QScrollArea, QWidget, QLabel, QSplitter, QScrollArea,
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem, QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
QHeaderView, QPushButton, QCheckBox, QLineEdit, QHeaderView, QPushButton, QCheckBox, QLineEdit,
QComboBox, QDialog, QPlainTextEdit) QComboBox, QDialog, QPlainTextEdit, QSlider)
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent
from PyQt6.QtGui import QColor from PyQt6.QtGui import QColor, QFont
import sys import sys
import asyncio import asyncio
import json import json
import subprocess import subprocess
import time import time
import traceback import traceback
import re
def _log(level, message): def _log(level, message):
@ -33,7 +34,8 @@ from mission_planner import FormationPlanner, MissionType
from command_sender import MavlinkSender, Ros2CommandSender from command_sender import MavlinkSender, Ros2CommandSender
from mission_executor import MissionExecutor, MissionState from mission_executor import MissionExecutor, MissionState
from mission_group import ( from mission_group import (
MissionGroup, GroupPanel, DroneAssignDialog, GROUP_COLORS MissionGroup, GroupPanel, DroneAssignDialog, GROUP_COLORS,
DEFAULT_MISSION_PARAM_VALUES
) )
# ================================================================================ # ================================================================================
@ -71,7 +73,10 @@ class StreamRedirector(QObject):
self._buffer = "" self._buffer = ""
class ControlStationUI(QMainWindow): class ControlStationUI(QMainWindow):
VERSION = '2.1.0' VERSION = '2.2.0'
FONT_SCALE_MIN = 70
FONT_SCALE_MAX = 180
FONT_SCALE_DEFAULT = 100
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -175,12 +180,22 @@ class ControlStationUI(QMainWindow):
self._group_counter = 0 # 用來產生 group_id self._group_counter = 0 # 用來產生 group_id
self._pending_box_assign = None # 框選後直接分配到的 group_id self._pending_box_assign = None # 框選後直接分配到的 group_id
# ================================================================================ # ================================================================================
self.global_mission_defaults = dict(DEFAULT_MISSION_PARAM_VALUES)
self.font_scale = self.FONT_SCALE_DEFAULT / 100.0
self.pending_font_scale = self.font_scale
self._font_scale_applying = False
self._base_app_font = QFont(QApplication.instance().font())
self.init_ui() self.init_ui()
self._setup_stream_redirector() self._setup_stream_redirector()
app = QApplication.instance()
app.setProperty("font_scale", self.font_scale)
app.setProperty("base_app_font", self._base_app_font)
app.installEventFilter(self)
def init_ui(self): def init_ui(self):
main_splitter = QSplitter(Qt.Orientation.Horizontal) main_splitter = QSplitter(Qt.Orientation.Horizontal)
self.main_splitter = main_splitter
# 左側 TabWidget # 左側 TabWidget
self.left_tab = QTabWidget() self.left_tab = QTabWidget()
@ -220,6 +235,10 @@ class ControlStationUI(QMainWindow):
self.left_tab.addTab(self.comm_panel, "通訊") self.left_tab.addTab(self.comm_panel, "通訊")
# — 分頁 5全域設定
self.settings_tab = self._create_settings_tab()
self.left_tab.addTab(self.settings_tab, "設定")
# 右侧容器 # 右侧容器
right_container = QWidget() right_container = QWidget()
right_layout = QVBoxLayout(right_container) right_layout = QVBoxLayout(right_container)
@ -227,6 +246,11 @@ class ControlStationUI(QMainWindow):
right_layout.setSpacing(10) right_layout.setSpacing(10)
# ========== 任務群組 Tab ========== # ========== 任務群組 Tab ==========
self.group_container = QWidget()
group_container_layout = QVBoxLayout(self.group_container)
group_container_layout.setContentsMargins(0, 0, 0, 0)
group_container_layout.setSpacing(6)
group_header = QHBoxLayout() group_header = QHBoxLayout()
# 標題 + 收起/展開按鈕 # 標題 + 收起/展開按鈕
@ -261,7 +285,7 @@ class ControlStationUI(QMainWindow):
clear_traj_btn.clicked.connect(self.drone_map.clear_trajectories) clear_traj_btn.clicked.connect(self.drone_map.clear_trajectories)
group_header.addWidget(clear_traj_btn) group_header.addWidget(clear_traj_btn)
right_layout.addLayout(group_header) group_container_layout.addLayout(group_header)
self.group_tab_widget = QTabWidget() self.group_tab_widget = QTabWidget()
self.group_tab_widget.setStyleSheet(""" self.group_tab_widget.setStyleSheet("""
@ -274,9 +298,9 @@ class ControlStationUI(QMainWindow):
QTabBar::tab:selected { background-color: #2B2B2B; color: #FFF; border-bottom-color: #2B2B2B; } QTabBar::tab:selected { background-color: #2B2B2B; color: #FFF; border-bottom-color: #2B2B2B; }
QTabBar::tab:hover { background-color: #3A3A3A; } QTabBar::tab:hover { background-color: #3A3A3A; }
""") """)
self.group_tab_widget.setFixedHeight(150) self.group_tab_widget.setMinimumHeight(80)
self.group_tab_widget.currentChanged.connect(self._on_group_tab_changed) self.group_tab_widget.currentChanged.connect(self._on_group_tab_changed)
right_layout.addWidget(self.group_tab_widget) group_container_layout.addWidget(self.group_tab_widget)
# 🌟 新增:保存群組面板的展開狀態 # 🌟 新增:保存群組面板的展開狀態
self.group_panel_expanded = True self.group_panel_expanded = True
@ -284,8 +308,17 @@ class ControlStationUI(QMainWindow):
# 預設建立 Group A # 預設建立 Group A
self._add_mission_group() self._add_mission_group()
# 任務群組與地圖之間使用垂直 splitter可上下拖曳調整高度
self.right_vertical_splitter = QSplitter(Qt.Orientation.Vertical)
self.right_vertical_splitter.addWidget(self.group_container)
self.right_vertical_splitter.addWidget(self.drone_map.get_widget())
self.right_vertical_splitter.setChildrenCollapsible(False)
self.right_vertical_splitter.setStretchFactor(0, 0)
self.right_vertical_splitter.setStretchFactor(1, 1)
self.right_vertical_splitter.setSizes([170, 700])
right_layout.addWidget(self.right_vertical_splitter)
# 添加地圖 # 添加地圖
right_layout.addWidget(self.drone_map.get_widget())
self.drone_map.get_gps_signal().connect(self.handle_map_click) self.drone_map.get_gps_signal().connect(self.handle_map_click)
self.drone_map.get_drone_clicked_signal().connect(self.handle_drone_clicked) self.drone_map.get_drone_clicked_signal().connect(self.handle_drone_clicked)
self.drone_map.get_clear_all_drone_selection_signal().connect(self.handle_clear_all_drone_selection) self.drone_map.get_clear_all_drone_selection_signal().connect(self.handle_clear_all_drone_selection)
@ -347,6 +380,259 @@ class ControlStationUI(QMainWindow):
return widget return widget
def _create_settings_tab(self):
"""建立字體設定分頁。"""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(8)
scale_panel = QWidget()
scale_panel.setStyleSheet("""
QWidget {
background-color: #2A2A2A;
border: 1px solid #444;
border-radius: 6px;
}
""")
scale_layout = QVBoxLayout(scale_panel)
scale_layout.setContentsMargins(12, 12, 12, 12)
scale_layout.setSpacing(8)
self.font_scale_label = QLabel()
self.font_scale_label.setStyleSheet("color: #DDD; font-size: 13px; font-weight: bold;")
scale_layout.addWidget(self.font_scale_label)
self.font_scale_slider = QSlider(Qt.Orientation.Horizontal)
self.font_scale_slider.setRange(self.FONT_SCALE_MIN, self.FONT_SCALE_MAX)
self.font_scale_slider.setValue(self.FONT_SCALE_DEFAULT)
self.font_scale_slider.setTickInterval(10)
self.font_scale_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
self.font_scale_slider.setStyleSheet("""
QSlider::groove:horizontal {
border: 1px solid #444;
height: 8px;
background: #333;
border-radius: 4px;
}
QSlider::handle:horizontal {
background: #4A9EFF;
border: 1px solid #3A8EEF;
width: 18px;
margin: -6px 0;
border-radius: 9px;
}
QSlider::sub-page:horizontal {
background: #4A9EFF;
border-radius: 4px;
}
QSlider::add-page:horizontal {
background: #2A2A2A;
border-radius: 4px;
}
""")
self.font_scale_slider.valueChanged.connect(self._handle_font_scale_slider_changed)
scale_layout.addWidget(self.font_scale_slider)
layout.addWidget(scale_panel)
layout.addStretch()
settings_button_row = QHBoxLayout()
settings_button_row.addStretch()
apply_btn = QPushButton("套用")
apply_btn.setStyleSheet("""
QPushButton {
background-color: #4A9EFF;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
QPushButton:hover { background-color: #3A8EEF; }
""")
apply_btn.clicked.connect(self._apply_pending_font_scale)
settings_button_row.addWidget(apply_btn)
layout.addLayout(settings_button_row)
self._update_font_scale_label()
return widget
def _update_font_scale_label(self):
percent = int(round(self.pending_font_scale * 100))
if hasattr(self, 'font_scale_label'):
self.font_scale_label.setText(f"字體倍率:{percent}%")
def _handle_font_scale_slider_changed(self, value):
self.pending_font_scale = value / 100.0
self._update_font_scale_label()
def _apply_pending_font_scale(self):
self._apply_font_scale(self.pending_font_scale)
def _scale_stylesheet_font_sizes(self, stylesheet, scale):
if not stylesheet or 'font-size' not in stylesheet:
return stylesheet
def repl(match):
size = float(match.group(1))
unit = match.group(2)
scaled = max(1.0, size * scale)
if unit == 'px':
text = f"{scaled:.2f}".rstrip('0').rstrip('.')
else:
text = f"{scaled:.3f}".rstrip('0').rstrip('.')
return f"font-size: {text}{unit}"
return re.sub(r'font-size\s*:\s*([0-9]+(?:\.[0-9]+)?)\s*(px|pt)', repl, stylesheet)
def _apply_font_scale_to_widget(self, widget):
if hasattr(widget, 'apply_font_scale'):
widget.apply_font_scale()
return
if hasattr(widget, '_base_stylesheet') and hasattr(widget, '_applied_stylesheet'):
current_stylesheet = widget.styleSheet()
base_stylesheet = widget._base_stylesheet
if current_stylesheet != widget._applied_stylesheet:
base_stylesheet = current_stylesheet
widget._base_stylesheet = base_stylesheet
scaled_stylesheet = self._scale_stylesheet_font_sizes(base_stylesheet, self.font_scale)
widget._applied_stylesheet = scaled_stylesheet
if current_stylesheet != scaled_stylesheet:
widget.setStyleSheet(scaled_stylesheet)
return
current_stylesheet = widget.styleSheet()
source_stylesheet = getattr(widget, '_font_scale_source_stylesheet', current_stylesheet)
applied_stylesheet = getattr(widget, '_font_scale_applied_stylesheet', None)
if current_stylesheet != applied_stylesheet:
source_stylesheet = current_stylesheet
scaled_stylesheet = self._scale_stylesheet_font_sizes(source_stylesheet, self.font_scale)
widget._font_scale_source_stylesheet = source_stylesheet
widget._font_scale_applied_stylesheet = scaled_stylesheet
if current_stylesheet != scaled_stylesheet:
widget.setStyleSheet(scaled_stylesheet)
def _apply_font_scale_to_widget_tree(self, root):
if root is None:
return
if isinstance(root, QWidget):
self._apply_font_scale_to_widget(root)
for child in root.findChildren(QWidget):
self._apply_font_scale_to_widget(child)
def _apply_font_scale(self, scale):
app = QApplication.instance()
splitter_sizes = None
left_width = None
old_left_min = None
old_left_max = None
old_window_size = self.size()
old_window_min = self.minimumSize()
old_window_max = self.maximumSize()
lock_window_size = not self.isMaximized() and not self.isFullScreen()
if hasattr(self, 'main_splitter') and self.main_splitter:
splitter_sizes = self.main_splitter.sizes()
if hasattr(self, 'left_tab') and self.left_tab:
left_width = self.left_tab.width()
old_left_min = self.left_tab.minimumWidth()
old_left_max = self.left_tab.maximumWidth()
if left_width > 0:
self.left_tab.setMinimumWidth(left_width)
self.left_tab.setMaximumWidth(left_width)
self._font_scale_applying = True
try:
self.font_scale = scale
app.setProperty("font_scale", scale)
if hasattr(self, '_base_app_font'):
scaled_font = QFont(self._base_app_font)
if self._base_app_font.pointSizeF() > 0:
scaled_font.setPointSizeF(max(1.0, self._base_app_font.pointSizeF() * scale))
elif self._base_app_font.pixelSize() > 0:
scaled_font.setPixelSize(max(1, int(round(self._base_app_font.pixelSize() * scale))))
app.setFont(scaled_font)
if lock_window_size:
self.setMinimumSize(old_window_size)
self.setMaximumSize(old_window_size)
self._update_font_scale_label()
self._apply_font_scale_to_widget_tree(self)
if hasattr(self, 'comm_panel') and self.comm_panel:
self.comm_panel.apply_font_scale()
for panel in getattr(self, 'drones', {}).values():
if hasattr(panel, 'apply_font_scale'):
panel.apply_font_scale()
for panel in getattr(self, 'socket_groups', {}).values():
if hasattr(panel, 'apply_font_scale'):
panel.apply_font_scale()
for panel in getattr(self, 'group_panels', {}).values():
if hasattr(panel, 'apply_font_scale'):
panel.apply_font_scale()
if hasattr(self, 'drone_map') and self.drone_map:
self.drone_map.set_font_scale(scale)
if splitter_sizes:
self.main_splitter.setSizes(splitter_sizes)
if lock_window_size:
self.resize(old_window_size)
self.left_tab.updateGeometry()
self.update()
finally:
self._font_scale_applying = False
def _restore_locked_sizes():
if lock_window_size:
self.setMinimumSize(old_window_min)
self.setMaximumSize(old_window_max)
self.resize(old_window_size)
if self.left_tab and left_width and left_width > 0:
self.left_tab.setMinimumWidth(left_width)
self.left_tab.setMaximumWidth(left_width)
if splitter_sizes and self.main_splitter:
self.main_splitter.setSizes(splitter_sizes)
def _release_size_locks():
if self.left_tab and old_left_min is not None and old_left_max is not None:
self.left_tab.setMinimumWidth(old_left_min)
self.left_tab.setMaximumWidth(old_left_max)
QTimer.singleShot(0, _restore_locked_sizes)
QTimer.singleShot(30, _restore_locked_sizes)
QTimer.singleShot(100, _restore_locked_sizes)
QTimer.singleShot(150, _release_size_locks)
def eventFilter(self, obj, event):
if self._font_scale_applying:
return super().eventFilter(obj, event)
if isinstance(obj, QWidget) and event.type() in {
QEvent.Type.StyleChange,
QEvent.Type.Polish,
QEvent.Type.Show,
}:
self._font_scale_applying = True
try:
self._apply_font_scale_to_widget(obj)
finally:
self._font_scale_applying = False
return super().eventFilter(obj, event)
def _clear_message_history(self): def _clear_message_history(self):
"""清空訊息歷史。""" """清空訊息歷史。"""
self.message_history.clear() self.message_history.clear()
@ -668,15 +954,28 @@ class ControlStationUI(QMainWindow):
"""🌟 收起/展開任務群組面板""" """🌟 收起/展開任務群組面板"""
if self.group_panel_expanded: if self.group_panel_expanded:
# 收起 # 收起
self.group_tab_widget.setFixedHeight(0) if hasattr(self, 'right_vertical_splitter'):
sizes = self.right_vertical_splitter.sizes()
if sizes and sizes[0] > 0:
self._last_group_panel_height = sizes[0]
self.group_tab_widget.hide() self.group_tab_widget.hide()
header_height = self.group_container.sizeHint().height()
self.group_container.setMaximumHeight(header_height)
if hasattr(self, 'right_vertical_splitter'):
total = sum(self.right_vertical_splitter.sizes()) or self.height()
self.right_vertical_splitter.setSizes([header_height, max(1, total - header_height)])
self.toggle_group_btn.setText("") self.toggle_group_btn.setText("")
self.toggle_group_btn.setToolTip("展開任務群組") self.toggle_group_btn.setToolTip("展開任務群組")
self.group_panel_expanded = False self.group_panel_expanded = False
else: else:
# 展開 # 展開
self.group_tab_widget.setFixedHeight(150) self.group_container.setMaximumHeight(16777215)
self.group_tab_widget.show() self.group_tab_widget.show()
if hasattr(self, 'right_vertical_splitter'):
total = sum(self.right_vertical_splitter.sizes()) or self.height()
group_height = getattr(self, '_last_group_panel_height', 170)
group_height = min(max(group_height, 120), max(120, total - 120))
self.right_vertical_splitter.setSizes([group_height, max(1, total - group_height)])
self.toggle_group_btn.setText("") self.toggle_group_btn.setText("")
self.toggle_group_btn.setToolTip("收起任務群組") self.toggle_group_btn.setToolTip("收起任務群組")
self.group_panel_expanded = True self.group_panel_expanded = True
@ -688,7 +987,7 @@ class ControlStationUI(QMainWindow):
group = MissionGroup(gid, color) group = MissionGroup(gid, color)
self.mission_groups[gid] = group self.mission_groups[gid] = group
panel = GroupPanel(group) panel = GroupPanel(group, default_params=self.global_mission_defaults)
panel.assign_drones_requested.connect(self._handle_assign_drones) panel.assign_drones_requested.connect(self._handle_assign_drones)
panel.mission_type_changed.connect(self._handle_mission_type_changed) panel.mission_type_changed.connect(self._handle_mission_type_changed)
panel.start_requested.connect(self._handle_group_start) panel.start_requested.connect(self._handle_group_start)

@ -16,6 +16,7 @@ class DroneMap:
self.map_view = QWebEngineView() self.map_view = QWebEngineView()
self.map_loaded = False self.map_loaded = False
self.pending_map_updates = {} self.pending_map_updates = {}
self.font_scale = 1.0
# 創建橋接對象 # 創建橋接對象
self.bridge = MapBridge() self.bridge = MapBridge()
@ -36,6 +37,7 @@ class DroneMap:
<script src="https://unpkg.com/leaflet-rotatedmarker/leaflet.rotatedMarker.js"></script> <script src="https://unpkg.com/leaflet-rotatedmarker/leaflet.rotatedMarker.js"></script>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script> <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<style> <style>
:root { --ui-font-scale: 1; }
html, body, #map { height: 100%; margin: 0; } html, body, #map { height: 100%; margin: 0; }
#map { #map {
user-select: none; user-select: none;
@ -70,7 +72,7 @@ class DroneMap:
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: calc(13px * var(--ui-font-scale));
font-weight: bold; font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.2); box-shadow: 0 2px 5px rgba(0,0,0,0.2);
} }
@ -90,7 +92,7 @@ class DroneMap:
} }
.mission-info-row { .mission-info-row {
margin-bottom: 8px; margin-bottom: 8px;
font-size: 12px; font-size: calc(12px * var(--ui-font-scale));
color: #333; color: #333;
} }
.mission-info-label { .mission-info-label {
@ -109,7 +111,7 @@ class DroneMap:
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: calc(13px * var(--ui-font-scale));
font-weight: bold; font-weight: bold;
margin-top: 8px; margin-top: 8px;
} }
@ -136,7 +138,7 @@ class DroneMap:
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: calc(13px * var(--ui-font-scale));
font-weight: bold; font-weight: bold;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
@ -154,7 +156,7 @@ class DroneMap:
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: calc(13px * var(--ui-font-scale));
font-weight: bold; font-weight: bold;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
@ -172,7 +174,7 @@ class DroneMap:
border: none; border: none;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: calc(13px * var(--ui-font-scale));
font-weight: bold; font-weight: bold;
transition: background-color 0.2s; transition: background-color 0.2s;
} }
@ -327,7 +329,7 @@ class DroneMap:
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-weight: bold; font-weight: bold;
font-size: 12px; font-size: calc(12px * var(--ui-font-scale));
text-shadow: 1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8); text-shadow: 1px 1px 2px rgba(255,255,255,0.8), -1px -1px 2px rgba(255,255,255,0.8);
">${sysid}</div>`, ">${sysid}</div>`,
iconSize: [16, 16], iconSize: [16, 16],
@ -569,7 +571,7 @@ class DroneMap:
'align-items: center;' + 'align-items: center;' +
'justify-content: center;' + 'justify-content: center;' +
'font-weight: bold;' + 'font-weight: bold;' +
'font-size: 11px;' + 'font-size: calc(11px * var(--ui-font-scale));' +
'border: 2px solid white;' + 'border: 2px solid white;' +
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' + 'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
'">' + idx + '</div>', '">' + idx + '</div>',
@ -622,6 +624,10 @@ class DroneMap:
} }
} }
// ================================================================================ // ================================================================================
function setFontScale(scale) {
document.documentElement.style.setProperty('--ui-font-scale', scale);
}
// 開始任務 // 開始任務
function startMission() { function startMission() {
@ -807,7 +813,7 @@ class DroneMap:
'width: 22px; height: 22px;' + 'width: 22px; height: 22px;' +
'border-radius: 50%;' + 'border-radius: 50%;' +
'display: flex; align-items: center; justify-content: center;' + 'display: flex; align-items: center; justify-content: center;' +
'font-weight: bold; font-size: 10px;' + 'font-weight: bold; font-size: calc(10px * var(--ui-font-scale));' +
'border: 2px solid white;' + 'border: 2px solid white;' +
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' + 'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
'">' + groupId + '</div>', '">' + groupId + '</div>',
@ -827,7 +833,7 @@ class DroneMap:
'width: 22px; height: 22px;' + 'width: 22px; height: 22px;' +
'border-radius: 50%;' + 'border-radius: 50%;' +
'display: flex; align-items: center; justify-content: center;' + 'display: flex; align-items: center; justify-content: center;' +
'font-weight: bold; font-size: 14px;' + 'font-weight: bold; font-size: calc(14px * var(--ui-font-scale));' +
'border: 2px solid white;' + 'border: 2px solid white;' +
'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' + 'box-shadow: 0 2px 5px rgba(0,0,0,0.3);' +
'">★</div>', '">★</div>',
@ -878,6 +884,7 @@ class DroneMap:
"""地圖加載完成回調""" """地圖加載完成回調"""
if ok: if ok:
self.map_loaded = True self.map_loaded = True
self.set_font_scale(self.font_scale)
else: else:
_log("ERROR", "地圖載入失敗") _log("ERROR", "地圖載入失敗")
@ -909,6 +916,12 @@ class DroneMap:
"""聚焦到指定無人機""" """聚焦到指定無人機"""
if self.map_loaded: if self.map_loaded:
self.map_view.page().runJavaScript(f"focusOn('{drone_id}');") self.map_view.page().runJavaScript(f"focusOn('{drone_id}');")
def set_font_scale(self, scale):
"""設定地圖 HTML 控制項的字體倍率。"""
self.font_scale = scale
if self.map_loaded:
self.map_view.page().runJavaScript(f"setFontScale({scale:.3f});")
# ================================================================================ # ================================================================================
# 任務規劃視覺化方法 # 任務規劃視覺化方法

@ -23,6 +23,18 @@ GROUP_COLORS = [
'#FF6B9D', # 粉 '#FF6B9D', # 粉
] ]
DEFAULT_MISSION_PARAM_VALUES = {
'spacing': '5.0',
'base_altitude': '10.0',
'altitude_diff': '2.0',
'radius': '10.0',
'altitude': '10.0',
'start_angle': '0',
'lateral_offset': '3.0',
'longitudinal_spacing': '5.0',
'line_spacing': '5.0',
}
class MissionGroup: class MissionGroup:
"""單一任務群組的資料""" """單一任務群組的資料"""
@ -157,10 +169,13 @@ class GroupPanel(QWidget):
QPushButton:disabled {{ background-color: #444; color: #666; }} QPushButton:disabled {{ background-color: #444; color: #666; }}
""" """
def __init__(self, group: MissionGroup, parent=None): def __init__(self, group: MissionGroup, parent=None, default_params=None):
super().__init__(parent) super().__init__(parent)
self.group = group self.group = group
self.all_btn_ref = None # 保存全選按鈕的參考 self.all_btn_ref = None # 保存全選按鈕的參考
self.default_params = dict(DEFAULT_MISSION_PARAM_VALUES)
if default_params:
self.default_params.update(default_params)
self._build_ui() self._build_ui()
def _make_sep(self): def _make_sep(self):
@ -372,23 +387,23 @@ class GroupPanel(QWidget):
# 每種任務類型的參數定義: (key, label, default_value) # 每種任務類型的參數定義: (key, label, default_value)
self._param_defs = { self._param_defs = {
'M_FORMATION': [ 'M_FORMATION': [
('spacing', '間距 (m)', '5.0'), ('spacing', '間距 (m)', self.default_params['spacing']),
('base_altitude', '基準高度 (m)', '10.0'), ('base_altitude', '基準高度 (m)', self.default_params['base_altitude']),
('altitude_diff', '高低差 (m)', '2.0'), ('altitude_diff', '高低差 (m)', self.default_params['altitude_diff']),
], ],
'CIRCLE_FORMATION': [ 'CIRCLE_FORMATION': [
('radius', '半徑 (m)', '10.0'), ('radius', '半徑 (m)', self.default_params['radius']),
('altitude', '高度 (m)', '10.0'), ('altitude', '高度 (m)', self.default_params['altitude']),
('start_angle', '起始角 (°)', '0'), ('start_angle', '起始角 (°)', self.default_params['start_angle']),
], ],
'LEADER_FOLLOWER': [ 'LEADER_FOLLOWER': [
('lateral_offset', '橫向偏移 (m)', '3.0'), ('lateral_offset', '橫向偏移 (m)', self.default_params['lateral_offset']),
('longitudinal_spacing', '縱向間距 (m)', '5.0'), ('longitudinal_spacing', '縱向間距 (m)', self.default_params['longitudinal_spacing']),
('altitude', '高度 (m)', '10.0'), ('altitude', '高度 (m)', self.default_params['altitude']),
], ],
'GRID_SWEEP': [ 'GRID_SWEEP': [
('line_spacing', '掃描線距 (m)', '5.0'), ('line_spacing', '掃描線距 (m)', self.default_params['line_spacing']),
('altitude', '高度 (m)', '10.0'), ('altitude', '高度 (m)', self.default_params['altitude']),
], ],
} }
@ -532,6 +547,13 @@ class GroupPanel(QWidget):
params[key] = float(default) params[key] = float(default)
return params return params
def set_param_value(self, key, value):
"""更新指定參數欄位的文字值。"""
if key not in self._param_widgets:
return
_row_w, inp = self._param_widgets[key]
inp.setText(str(value))
def update_mission_info(self, center_lat, center_lon, target_lat, target_lon): def update_mission_info(self, center_lat, center_lon, target_lat, target_lon):
"""更新中心點 / 目標點顯示""" """更新中心點 / 目標點顯示"""
info_style = f"color: {self.group.color}; font-size: 11px; font-weight: bold;" info_style = f"color: {self.group.color}; font-size: 11px; font-weight: bold;"
@ -553,4 +575,3 @@ class GroupPanel(QWidget):
except ValueError: except ValueError:
alt = 10.0 alt = 10.0
self.takeoff_requested.emit(self.group.group_id, alt) self.takeoff_requested.emit(self.group.group_id, alt)

Loading…
Cancel
Save