diff --git a/src/GUI/comm_panel.py b/src/GUI/comm_panel.py
index 3a41226..cf79be1 100644
--- a/src/GUI/comm_panel.py
+++ b/src/GUI/comm_panel.py
@@ -1,10 +1,78 @@
#!/usr/bin/env python3
-from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
- QPushButton, QLineEdit, QComboBox)
+from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
+ QPushButton, QLineEdit, QComboBox, QApplication)
+from PyQt6.QtGui import QFont
from PyQt6.QtCore import pyqtSignal
import glob
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):
"""通讯设置面板类"""
@@ -26,6 +94,7 @@ class CommPanel(QWidget):
self.ws_connections = []
self.serial_connections = []
self._init_ui()
+ self.apply_font_scale()
def _init_ui(self):
"""初始化UI"""
@@ -35,7 +104,7 @@ class CommPanel(QWidget):
# ========== UDP MAVLink 區域 ==========
udp_title = QLabel("UDP")
- udp_title.setStyleSheet("""
+ _set_scaled_stylesheet(udp_title, """
color: #DDD;
font-size: 14px;
font-weight: bold;
@@ -60,7 +129,7 @@ class CommPanel(QWidget):
self.udp_ip_input = QLineEdit()
self.udp_ip_input.setText("127.0.0.1")
self.udp_ip_input.setPlaceholderText("IP")
- self.udp_ip_input.setStyleSheet("""
+ _set_scaled_stylesheet(self.udp_ip_input, """
QLineEdit {
background-color: #333;
color: #DDD;
@@ -74,7 +143,7 @@ class CommPanel(QWidget):
self.udp_port_input.setText("14550")
self.udp_port_input.setPlaceholderText("Port")
self.udp_port_input.setFixedWidth(80)
- self.udp_port_input.setStyleSheet("""
+ _set_scaled_stylesheet(self.udp_port_input, """
QLineEdit {
background-color: #333;
color: #DDD;
@@ -86,7 +155,7 @@ class CommPanel(QWidget):
add_udp_btn = QPushButton("添加")
add_udp_btn.clicked.connect(self._handle_add_udp)
- add_udp_btn.setStyleSheet("""
+ _set_scaled_stylesheet(add_udp_btn, """
QPushButton {
background-color: #4CAF50;
color: white;
@@ -98,9 +167,13 @@ class CommPanel(QWidget):
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(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(add_udp_btn)
@@ -113,7 +186,7 @@ class CommPanel(QWidget):
# ========== Serial 區域 ==========
serial_title = QLabel("Serial")
- serial_title.setStyleSheet("""
+ _set_scaled_stylesheet(serial_title, """
color: #DDD;
font-size: 14px;
font-weight: bold;
@@ -136,7 +209,7 @@ class CommPanel(QWidget):
add_serial_layout.setContentsMargins(0, 0, 0, 0)
self.serial_port_combo = QComboBox()
- self.serial_port_combo.setStyleSheet("""
+ _set_scaled_stylesheet(self.serial_port_combo, """
QComboBox {
background-color: #333;
color: #DDD;
@@ -160,7 +233,7 @@ class CommPanel(QWidget):
refresh_ports_btn.setFixedWidth(35)
refresh_ports_btn.clicked.connect(self._refresh_serial_ports)
refresh_ports_btn.setToolTip("重新掃描串口")
- refresh_ports_btn.setStyleSheet("""
+ _set_scaled_stylesheet(refresh_ports_btn, """
QPushButton {
background-color: #444;
color: #DDD;
@@ -176,7 +249,7 @@ class CommPanel(QWidget):
self.serial_baudrate_combo.addItems(['9600', '19200', '38400', '57600', '115200'])
self.serial_baudrate_combo.setCurrentText('57600')
self.serial_baudrate_combo.setFixedWidth(100)
- self.serial_baudrate_combo.setStyleSheet("""
+ _set_scaled_stylesheet(self.serial_baudrate_combo, """
QComboBox {
background-color: #333;
color: #DDD;
@@ -197,7 +270,7 @@ class CommPanel(QWidget):
add_serial_btn = QPushButton("添加")
add_serial_btn.clicked.connect(self._handle_add_serial)
- add_serial_btn.setStyleSheet("""
+ _set_scaled_stylesheet(add_serial_btn, """
QPushButton {
background-color: #4CAF50;
color: white;
@@ -209,10 +282,14 @@ class CommPanel(QWidget):
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(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(add_serial_btn)
@@ -225,7 +302,7 @@ class CommPanel(QWidget):
# ========== WebSocket 區域 ==========
ws_title = QLabel("WebSocket")
- ws_title.setStyleSheet("""
+ _set_scaled_stylesheet(ws_title, """
color: #DDD;
font-size: 14px;
font-weight: bold;
@@ -249,7 +326,7 @@ class CommPanel(QWidget):
self.ws_url_input = QLineEdit()
self.ws_url_input.setPlaceholderText("host")
- self.ws_url_input.setStyleSheet("""
+ _set_scaled_stylesheet(self.ws_url_input, """
QLineEdit {
background-color: #333;
color: #DDD;
@@ -261,7 +338,7 @@ class CommPanel(QWidget):
add_ws_btn = QPushButton("添加")
add_ws_btn.clicked.connect(self._handle_add_ws)
- add_ws_btn.setStyleSheet("""
+ _set_scaled_stylesheet(add_ws_btn, """
QPushButton {
background-color: #4CAF50;
color: white;
@@ -273,7 +350,9 @@ class CommPanel(QWidget):
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(add_ws_btn)
@@ -437,7 +516,7 @@ class CommPanel(QWidget):
def create_udp_connection_panel(self, conn):
"""創建 UDP 連接面板"""
panel = QWidget()
- panel.setStyleSheet("""
+ _set_scaled_stylesheet(panel, """
QWidget {
background-color: #2A2A2A;
border-radius: 6px;
@@ -451,22 +530,22 @@ class CommPanel(QWidget):
# 連接資訊
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("●")
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("運行中")
else:
- status_label.setStyleSheet("color: #888; font-size: 16px;")
+ _set_scaled_stylesheet(status_label, "color: #888; font-size: 16px;")
status_label.setToolTip("已停止")
# 控制按鈕
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
toggle_btn.setFixedWidth(60)
toggle_btn.clicked.connect(lambda: self.udp_connection_toggled.emit(conn, toggle_btn, status_label))
- toggle_btn.setStyleSheet("""
+ _set_scaled_stylesheet(toggle_btn, """
QPushButton {
background-color: #444;
color: #DDD;
@@ -481,7 +560,7 @@ class CommPanel(QWidget):
remove_btn = QPushButton("移除")
remove_btn.setFixedWidth(60)
remove_btn.clicked.connect(lambda: self.udp_connection_removed.emit(conn, panel))
- remove_btn.setStyleSheet("""
+ _set_scaled_stylesheet(remove_btn, """
QPushButton {
background-color: #d32f2f;
color: white;
@@ -509,7 +588,7 @@ class CommPanel(QWidget):
def create_ws_connection_panel(self, conn):
"""創建 WebSocket 連接面板"""
panel = QWidget()
- panel.setStyleSheet("""
+ _set_scaled_stylesheet(panel, """
QWidget {
background-color: #2A2A2A;
border-radius: 6px;
@@ -523,22 +602,22 @@ class CommPanel(QWidget):
# 連接資訊
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("●")
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("運行中")
else:
- status_label.setStyleSheet("color: #888; font-size: 16px;")
+ _set_scaled_stylesheet(status_label, "color: #888; font-size: 16px;")
status_label.setToolTip("已停止")
# 控制按鈕
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
toggle_btn.setFixedWidth(60)
toggle_btn.clicked.connect(lambda: self.ws_connection_toggled.emit(conn, toggle_btn, status_label))
- toggle_btn.setStyleSheet("""
+ _set_scaled_stylesheet(toggle_btn, """
QPushButton {
background-color: #444;
color: #DDD;
@@ -553,7 +632,7 @@ class CommPanel(QWidget):
remove_btn = QPushButton("移除")
remove_btn.setFixedWidth(60)
remove_btn.clicked.connect(lambda: self.ws_connection_removed.emit(conn, panel))
- remove_btn.setStyleSheet("""
+ _set_scaled_stylesheet(remove_btn, """
QPushButton {
background-color: #d32f2f;
color: white;
@@ -605,7 +684,7 @@ class CommPanel(QWidget):
def create_serial_connection_panel(self, conn):
"""創建 Serial 連接面板"""
panel = QWidget()
- panel.setStyleSheet("""
+ _set_scaled_stylesheet(panel, """
QWidget {
background-color: #2A2A2A;
border-radius: 6px;
@@ -619,22 +698,22 @@ class CommPanel(QWidget):
# 連接資訊
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("●")
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("運行中")
else:
- status_label.setStyleSheet("color: #888; font-size: 16px;")
+ _set_scaled_stylesheet(status_label, "color: #888; font-size: 16px;")
status_label.setToolTip("已停止")
# 控制按鈕
toggle_btn = QPushButton("停止" if conn.get('enabled', False) else "啟動")
toggle_btn.setFixedWidth(60)
toggle_btn.clicked.connect(lambda: self.serial_connection_toggled.emit(conn, toggle_btn, status_label))
- toggle_btn.setStyleSheet("""
+ _set_scaled_stylesheet(toggle_btn, """
QPushButton {
background-color: #444;
color: #DDD;
@@ -649,7 +728,7 @@ class CommPanel(QWidget):
remove_btn = QPushButton("移除")
remove_btn.setFixedWidth(60)
remove_btn.clicked.connect(lambda: self.serial_connection_removed.emit(conn, panel))
- remove_btn.setStyleSheet("""
+ _set_scaled_stylesheet(remove_btn, """
QPushButton {
background-color: #d32f2f;
color: white;
@@ -684,4 +763,12 @@ class CommPanel(QWidget):
def remove_serial_connection_from_list(self, conn):
"""從列表中移除 Serial 連接"""
if conn in self.serial_connections:
- self.serial_connections.remove(conn)
\ No newline at end of file
+ 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)
diff --git a/src/GUI/drone_panel.py b/src/GUI/drone_panel.py
index 49e560e..3477126 100644
--- a/src/GUI/drone_panel.py
+++ b/src/GUI/drone_panel.py
@@ -1,10 +1,77 @@
#!/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.QtGui import QPainter, QPen, QColor, QFont, QPolygonF
from PyQt6.QtCore import QPointF, Qt
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):
"""單個無人機面板類別"""
@@ -32,12 +99,13 @@ class DronePanel(QWidget):
self.attitude_indicator = None
self._init_ui()
+ self.apply_font_scale()
def _init_ui(self):
"""初始化UI"""
self.setObjectName(f"panel_{self.drone_id}")
self.setFixedHeight(140)
- self.setStyleSheet("""
+ _set_scaled_stylesheet(self, """
background-color: #2A2A2A;
border-radius: 8px;
""")
@@ -49,7 +117,7 @@ class DronePanel(QWidget):
# 創建內容容器(包含 info 和 control)
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.setContentsMargins(8, 8, 8, 8)
content_layout.setSpacing(8)
@@ -82,7 +150,7 @@ class DronePanel(QWidget):
# 勾選框
self.checkbox = QCheckBox()
self.checkbox.setObjectName(f"{self.drone_id}_checkbox")
- self.checkbox.setStyleSheet("""
+ _set_scaled_stylesheet(self.checkbox, """
QCheckBox {
color: #DDD;
}
@@ -103,8 +171,8 @@ class DronePanel(QWidget):
)
# ID 顯示
- id_label = QLabel(self.display_id)
- id_label.setStyleSheet("""
+ self.id_label = QLabel(self.display_id)
+ _set_scaled_stylesheet(self.id_label, """
font-weight: bold;
font-size: 14px;
color: #7FFFD4;
@@ -112,7 +180,7 @@ class DronePanel(QWidget):
""")
header_layout.addWidget(self.checkbox)
- header_layout.addWidget(id_label)
+ header_layout.addWidget(self.id_label)
header_layout.addStretch()
info_layout.addWidget(header)
@@ -148,15 +216,15 @@ class DronePanel(QWidget):
status_layout.setContentsMargins(0, 0, 0, 0)
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.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.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(self.mode_label)
@@ -172,15 +240,15 @@ class DronePanel(QWidget):
connection_layout.setContentsMargins(0, 0, 0, 0)
connection_title = QLabel("Socket:")
- connection_title.setStyleSheet("color: #888; min-width: 50px;")
+ _set_scaled_stylesheet(connection_title, "color: #888; min-width: 50px;")
# 根據解析的 drone_id 資訊設定初始值
self.socket_seq_label = QLabel(self.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.setStyleSheet("color: #DDD;")
+ _set_scaled_stylesheet(connection_sep, "color: #DDD;")
# 設定連接方式顯示
connection_type_map = {
@@ -193,7 +261,7 @@ class DronePanel(QWidget):
self.connection_type_label = QLabel(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(self.socket_seq_label)
@@ -210,29 +278,29 @@ class DronePanel(QWidget):
battery_layout.setContentsMargins(0, 0, 0, 0)
# 顯示百分比
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.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.setStyleSheet("color: #DDD;")
+ _set_scaled_stylesheet(separator1, "color: #DDD;")
# 顯示電壓
self.battery_vol_label = QLabel("--")
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.setStyleSheet("color: #DDD;")
+ _set_scaled_stylesheet(separator2, "color: #DDD;")
# 顯示電池節數 (S count)
self.battery_cells_label = QLabel("--")
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(self.battery_pct_label)
@@ -251,18 +319,18 @@ class DronePanel(QWidget):
altitude_layout.setContentsMargins(0, 0, 0, 0)
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.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.setStyleSheet("color: #888; margin-left: 10px;")
+ _set_scaled_stylesheet(speed_title, "color: #888; margin-left: 10px;")
self.speed_label = QLabel("--")
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(self.altitude_label)
@@ -320,6 +388,16 @@ class DronePanel(QWidget):
def is_checked(self):
"""獲取勾選狀態"""
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):
# 定義信號
@@ -331,11 +409,12 @@ class SocketGroupPanel(QWidget):
self.color = color
self.socket_type = socket_type
self._init_ui()
+ self.apply_font_scale()
def _init_ui(self):
"""初始化UI"""
self.setObjectName(f"socket_group_{self.socket_id}")
- self.setStyleSheet("""
+ _set_scaled_stylesheet(self, """
background-color: #1E1E1E;
border-radius: 12px;
""")
@@ -352,7 +431,7 @@ class SocketGroupPanel(QWidget):
# 分組勾選框
self.group_checkbox = QCheckBox()
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::indicator {{
width: 14px;
@@ -380,7 +459,7 @@ class SocketGroupPanel(QWidget):
else:
title_text = f"Socket {self.socket_id}"
self.title_label = QLabel(title_text)
- self.title_label.setStyleSheet(f"""
+ _set_scaled_stylesheet(self.title_label, f"""
font-weight: bold;
font-size: 16px;
color: {self.color};
@@ -430,6 +509,14 @@ class SocketGroupPanel(QWidget):
"""設置分組勾選狀態(支持半選)"""
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):
"""
@@ -503,7 +590,9 @@ class AttitudeIndicator(QWidget):
# pitch ladder (every 10°, ±30°)
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):
if deg == 0:
continue
@@ -581,6 +670,8 @@ class AttitudeIndicator(QWidget):
# heading text centred (bigger)
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)}°"
- p.drawText(0, int(strip_y), w, strip_h, Qt.AlignmentFlag.AlignCenter, hdg_str)
\ No newline at end of file
+ p.drawText(0, int(strip_y), w, strip_h, Qt.AlignmentFlag.AlignCenter, hdg_str)
diff --git a/src/GUI/gui.py b/src/GUI/gui.py
index fc63e9d..9b1e34f 100644
--- a/src/GUI/gui.py
+++ b/src/GUI/gui.py
@@ -4,15 +4,16 @@ from PyQt6.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout
QWidget, QLabel, QSplitter, QScrollArea,
QSizePolicy, QTabWidget, QTableWidget, QTableWidgetItem,
QHeaderView, QPushButton, QCheckBox, QLineEdit,
- QComboBox, QDialog, QPlainTextEdit)
-from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal
-from PyQt6.QtGui import QColor
+ QComboBox, QDialog, QPlainTextEdit, QSlider)
+from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QEvent
+from PyQt6.QtGui import QColor, QFont
import sys
import asyncio
import json
import subprocess
import time
import traceback
+import re
def _log(level, message):
@@ -33,7 +34,8 @@ from mission_planner import FormationPlanner, MissionType
from command_sender import MavlinkSender, Ros2CommandSender
from mission_executor import MissionExecutor, MissionState
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 = ""
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):
super().__init__()
@@ -175,12 +180,22 @@ class ControlStationUI(QMainWindow):
self._group_counter = 0 # 用來產生 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._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):
main_splitter = QSplitter(Qt.Orientation.Horizontal)
+ self.main_splitter = main_splitter
# 左側 TabWidget
self.left_tab = QTabWidget()
@@ -220,6 +235,10 @@ class ControlStationUI(QMainWindow):
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_layout = QVBoxLayout(right_container)
@@ -227,6 +246,11 @@ class ControlStationUI(QMainWindow):
right_layout.setSpacing(10)
# ========== 任務群組 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()
# 標題 + 收起/展開按鈕
@@ -261,7 +285,7 @@ class ControlStationUI(QMainWindow):
clear_traj_btn.clicked.connect(self.drone_map.clear_trajectories)
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.setStyleSheet("""
@@ -274,9 +298,9 @@ class ControlStationUI(QMainWindow):
QTabBar::tab:selected { background-color: #2B2B2B; color: #FFF; border-bottom-color: #2B2B2B; }
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)
- right_layout.addWidget(self.group_tab_widget)
+ group_container_layout.addWidget(self.group_tab_widget)
# 🌟 新增:保存群組面板的展開狀態
self.group_panel_expanded = True
@@ -284,8 +308,17 @@ class ControlStationUI(QMainWindow):
# 預設建立 Group A
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_drone_clicked_signal().connect(self.handle_drone_clicked)
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
+ 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):
"""清空訊息歷史。"""
self.message_history.clear()
@@ -668,15 +954,28 @@ class ControlStationUI(QMainWindow):
"""🌟 收起/展開任務群組面板"""
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()
+ 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.setToolTip("展開任務群組")
self.group_panel_expanded = False
else:
# 展開
- self.group_tab_widget.setFixedHeight(150)
+ self.group_container.setMaximumHeight(16777215)
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.setToolTip("收起任務群組")
self.group_panel_expanded = True
@@ -688,7 +987,7 @@ class ControlStationUI(QMainWindow):
group = MissionGroup(gid, color)
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.mission_type_changed.connect(self._handle_mission_type_changed)
panel.start_requested.connect(self._handle_group_start)
diff --git a/src/GUI/map_layout.py b/src/GUI/map_layout.py
index 4f36017..c0239ff 100644
--- a/src/GUI/map_layout.py
+++ b/src/GUI/map_layout.py
@@ -16,6 +16,7 @@ class DroneMap:
self.map_view = QWebEngineView()
self.map_loaded = False
self.pending_map_updates = {}
+ self.font_scale = 1.0
# 創建橋接對象
self.bridge = MapBridge()
@@ -36,6 +37,7 @@ class DroneMap: