# control_tab.py """页面2:系统状态栏(三卡片) + 控制参数(Section 卡片) 重构要点: - 状态栏:三张横向并排卡片,每张含圆形图标 + 大字数值 + 右上角色点 - 控制参数区:Section 卡片(蓝竖线装饰),QGridLayout 双列布局,输入列拉伸占满约 2/3 页宽 """ import os from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit, QComboBox, QPushButton, QRadioButton, QCheckBox, QFrame, QButtonGroup, QSizePolicy, QGraphicsDropShadowEffect, ) from PySide6.QtCore import Qt, Signal, QSize from PySide6.QtGui import QColor, QIcon from PySide6.QtSvgWidgets import QSvgWidget from ui.connection_tab import _make_section_card _SRC_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "src")) # ========================================== # 工具函数:透明容器 # ========================================== def _transparent_widget() -> QWidget: """创建一个透明的空容器(用于包裹多个控件)。""" w = QWidget() w.setProperty("cssClass", "transparentBg") w.style().unpolish(w) w.style().polish(w) return w # ========================================== # 工具函数:三点状态栏卡片 # ========================================== def _make_status_card(parent, title: str, value: str, unit: str, value_color: str, circle_bg: str, icon_path: str, dot_color: str): """创建单张状态卡片(圆形图标 + 大字数值 + 右上角圆点)。 返回 (card, value_label)。 """ card = QFrame(parent) card.setProperty("cssClass", "sectionCard") card.style().unpolish(card) card.style().polish(card) card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) shadow = QGraphicsDropShadowEffect(card) shadow.setColor(QColor(0, 0, 0, 10)) shadow.setBlurRadius(14) shadow.setOffset(0, 2) card.setGraphicsEffect(shadow) inner = QVBoxLayout(card) inner.setContentsMargins(16, 12, 16, 14) inner.setSpacing(0) # ---- 右上角圆点 ---- dot_row = QHBoxLayout() dot_row.setContentsMargins(0, 0, 0, 6) dot_row.addStretch() dot = QWidget() dot.setFixedSize(8, 8) dot.setStyleSheet(f"background: {dot_color}; border-radius: 4px;") dot_row.addWidget(dot) inner.addLayout(dot_row) # ---- 主体:圆形图标 + 文本 ---- body = QHBoxLayout() body.setSpacing(30) # 圆形图标容器 icon_circle = QWidget() icon_circle.setFixedSize(82, 82) icon_circle.setStyleSheet( f"background: {circle_bg}; border-radius: 41px;" ) icon_inner = QVBoxLayout(icon_circle) icon_inner.setContentsMargins(0, 0, 0, 0) icon_inner.setAlignment(Qt.AlignCenter) svg = QSvgWidget(icon_path) svg.setFixedSize(48, 48) icon_inner.addWidget(svg, alignment=Qt.AlignCenter) body.addWidget(icon_circle) # 文本列 text_col = QVBoxLayout() text_col.setSpacing(4) title_lbl = QLabel(title) title_lbl.setStyleSheet( "color: #555555; font-size: 15px; background: transparent; border: none;" ) text_col.addWidget(title_lbl) value_row = QHBoxLayout() value_row.setSpacing(4) val_lbl = QLabel(value) val_lbl.setStyleSheet( f"color: {value_color}; font-size: 56px; font-weight: bold;" "background: transparent; border: none;" ) value_row.addWidget(val_lbl) unit_lbl = QLabel(unit) unit_lbl.setStyleSheet( f"color: {value_color}; font-size: 24px; background: transparent;" "border: none; padding-top: 14px;" ) value_row.addWidget(unit_lbl) value_row.addStretch() text_col.addLayout(value_row) body.addLayout(text_col, 1) inner.addLayout(body, 1) return card, val_lbl # ========================================== # 工具函数:行级标签 # ========================================== def _label(text: str, parent=None) -> QLabel: """紧凑表单标签。""" lbl = QLabel(text, parent) lbl.setStyleSheet( "color: #333333; font-size: 14px; font-weight: bold; background: transparent;" ) lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) return lbl def _unit_label(unit: str, parent=None) -> QLabel: """单位标签(灰色小字)。""" lbl = QLabel(unit, parent) lbl.setStyleSheet( "color: #94A3B8; font-size: 12px; background: transparent;" ) return lbl # ========================================== # 主类 # ========================================== class ControlTab(QWidget): """控制设置页面""" # ---- 信号 ---- target_set_requested = Signal(float) mode_changed = Signal(str) pid_update_requested = Signal(float, float, float) model_load_requested = Signal(str) models_refresh_requested = Signal() control_toggle_requested = Signal() plot_requested = Signal() manual_valve_set_requested = Signal(float) log_message_requested = Signal(str) def __init__(self, colors: dict, parent=None): super().__init__(parent) self.setProperty("cssClass", "tabPage") self.colors = colors main_layout = QVBoxLayout(self) main_layout.setContentsMargins(20, 16, 20, 16) main_layout.setSpacing(14) # ========================================== # A. 系统状态栏(三卡片) # ========================================== status_bar = QHBoxLayout() status_bar.setSpacing(14) self._pressure_card, self.current_pressure_lbl = _make_status_card( self, title="当前系统压力", value="0.0", unit="kPa", value_color="#0F955D", circle_bg="#E2F5ED", icon_path=os.path.join(_SRC_DIR, "pressure.svg"), dot_color="#0F955D", ) self._target_card, self.target_pressure_lbl = _make_status_card( self, title="设置目标压力", value="0.0", unit="kPa", value_color="#0960D1", circle_bg="#EBF3FE", icon_path=os.path.join(_SRC_DIR, "target.svg"), dot_color="#0960D1", ) self._valve_card, self.valve_opening_lbl = _make_status_card( self, title="控制阀门开度", value="0.0", unit="%", value_color="#E67E22", circle_bg="#FFF2E8", icon_path=os.path.join(_SRC_DIR, "valve.svg"), dot_color="#E67E22", ) status_bar.addWidget(self._pressure_card) status_bar.addWidget(self._target_card) status_bar.addWidget(self._valve_card) main_layout.addLayout(status_bar) # ========================================== # B. 控制参数设置区(Section 卡片) # ========================================== ctrl_card, ctrl_grid = _make_section_card(self, "控制设置", colors) self._build_control_section(ctrl_grid) main_layout.addWidget(ctrl_card) main_layout.addStretch() # 信号连接 self.mode_group.buttonClicked.connect(self._on_mode_changed_internal) # ========================================== # 控制设置 — QGridLayout 双列布局,输入列拉伸占满 2/3 页宽 # ========================================== def _build_control_section(self, grid: QGridLayout): # 沿用 _make_section_card 的列配置:col 0 标签固定 148px,col 1 输入区拉伸 grid.setVerticalSpacing(16) # --- B1: 物理工况 (容积 + 流量) --- row = 0 grid.addWidget(_label("物理工况:"), row, 0) b1 = _transparent_widget() b1h = QHBoxLayout(b1) b1h.setContentsMargins(0, 0, 0, 0) b1h.setSpacing(6) b1h.addWidget(_label("容积")) self.volume_entry = QLineEdit() self.volume_entry.setFixedWidth(120) b1h.addWidget(self.volume_entry) b1h.addWidget(_unit_label("L")) b1h.addSpacing(32) b1h.addWidget(_label("流量")) self.flow_entry = QLineEdit("100") self.flow_entry.setFixedWidth(120) b1h.addWidget(self.flow_entry) b1h.addWidget(_unit_label("L/min")) b1h.addStretch() grid.addWidget(b1, row, 1) # --- B2: 目标压力 + 按钮 --- row = 1 grid.addWidget(_label("目标压力:"), row, 0) b2 = _transparent_widget() b2h = QHBoxLayout(b2) b2h.setContentsMargins(0, 0, 0, 0) b2h.setSpacing(6) self.target_entry = QLineEdit("80.0") self.target_entry.setFixedWidth(200) b2h.addWidget(self.target_entry) b2h.addWidget(_unit_label("kPa")) b2h.addSpacing(10) self.set_target_btn = QPushButton("设置目标") self.set_target_btn.setIcon(QIcon(os.path.join(_SRC_DIR, "target.svg"))) self.set_target_btn.setIconSize(QSize(18, 18)) self.set_target_btn.setStyleSheet( "QPushButton { background: white; color: #0960D1; border: 1.5px solid #0960D1;" "border-radius: 6px; padding: 9px 20px; font-weight: bold; font-size: 14px; }" "QPushButton:hover { background: #EBF3FE; }" ) self.set_target_btn.setCursor(Qt.PointingHandCursor) self.set_target_btn.clicked.connect(self._on_set_target) b2h.addWidget(self.set_target_btn) b2h.addStretch() grid.addWidget(b2, row, 1) # --- B3: 控制模式单选 --- row = 2 grid.addWidget(_label("控制方式:"), row, 0) b3 = _transparent_widget() b3h = QHBoxLayout(b3) b3h.setContentsMargins(0, 0, 0, 0) b3h.setSpacing(24) self.mode_group = QButtonGroup(self) self.radio_rl = QRadioButton("智能自动") self.radio_pid = QRadioButton("手动PID") self.radio_manual = QRadioButton("设置开度") self.mode_group.addButton(self.radio_rl, 0) self.mode_group.addButton(self.radio_pid, 1) self.mode_group.addButton(self.radio_manual, 2) self.radio_rl.setChecked(True) b3h.addWidget(self.radio_rl) b3h.addWidget(self.radio_pid) b3h.addWidget(self.radio_manual) b3h.addStretch() grid.addWidget(b3, row, 1) # --- B4: 模型面板(跨两列,内部标签固定148px与外层col0对齐) --- row = 3 self.rl_panel = QWidget() self.rl_panel.setStyleSheet("background: transparent;") rl_layout = QHBoxLayout(self.rl_panel) rl_layout.setContentsMargins(0, 0, 0, 0) rl_layout.setSpacing(8) rl_lbl = _label("决策模型:") rl_lbl.setFixedWidth(148) rl_layout.addWidget(rl_lbl) self.model_combobox = QComboBox() self.model_combobox.setFixedWidth(280) self.model_combobox.setFixedHeight(36) rl_layout.addWidget(self.model_combobox) self.load_model_btn = QPushButton("加载模型") self.load_model_btn.setStyleSheet( "QPushButton { background: #0960D1; color: white; border: none;" "border-radius: 6px; padding: 7px 16px; font-weight: bold; font-size: 14px; }" "QPushButton:hover { background: #0856B8; }" ) self.load_model_btn.setCursor(Qt.PointingHandCursor) self.load_model_btn.clicked.connect(self._on_load_model) rl_layout.addWidget(self.load_model_btn) self.refresh_models_btn = QPushButton("🔄 刷新") self.refresh_models_btn.setProperty("cssClass", "refresh") self.refresh_models_btn.setCursor(Qt.PointingHandCursor) self.refresh_models_btn.clicked.connect(self._on_refresh_models) rl_layout.addWidget(self.refresh_models_btn) rl_layout.addStretch() grid.addWidget(self.rl_panel, row, 0, 1, 2) # --- B5: PID 面板(跨两列,内部标签固定148px) --- row = 4 self.pid_panel = QWidget() self.pid_panel.setStyleSheet("background: transparent;") self.pid_panel.hide() pid_layout = QHBoxLayout(self.pid_panel) pid_layout.setContentsMargins(0, 0, 0, 0) pid_layout.setSpacing(6) pid_lbl = _label("PID 调节:") pid_lbl.setFixedWidth(148) pid_layout.addWidget(pid_lbl) pid_layout.addWidget(_label("Kp:")) self.Kp_entry = QLineEdit("1.0") self.Kp_entry.setFixedWidth(80) pid_layout.addWidget(self.Kp_entry) pid_layout.addWidget(_label("Ki:")) self.Ki_entry = QLineEdit("0.4") self.Ki_entry.setFixedWidth(80) pid_layout.addWidget(self.Ki_entry) pid_layout.addWidget(_label("Kd:")) self.Kd_entry = QLineEdit("0") self.Kd_entry.setFixedWidth(80) pid_layout.addWidget(self.Kd_entry) self.update_pid_btn = QPushButton("更新PID参数") self.update_pid_btn.setStyleSheet( "QPushButton { background: #0960D1; color: white; border: none;" "border-radius: 6px; padding: 7px 16px; font-weight: bold; font-size: 14px; }" "QPushButton:hover { background: #0856B8; }" ) self.update_pid_btn.setCursor(Qt.PointingHandCursor) self.update_pid_btn.clicked.connect(self._on_update_pid) pid_layout.addWidget(self.update_pid_btn) pid_layout.addStretch() grid.addWidget(self.pid_panel, row, 0, 1, 2) # --- B6: 手动开度面板(跨两列,内部标签固定148px) --- row = 5 self.manual_panel = QWidget() self.manual_panel.setStyleSheet("background: transparent;") self.manual_panel.hide() man_layout = QHBoxLayout(self.manual_panel) man_layout.setContentsMargins(0, 0, 0, 0) man_layout.setSpacing(6) man_lbl = _label("设置开度:") man_lbl.setFixedWidth(148) man_layout.addWidget(man_lbl) self.valve_entry = QLineEdit() self.valve_entry.setFixedWidth(160) man_layout.addWidget(self.valve_entry) man_layout.addWidget(_unit_label("%")) self.set_valve_btn = QPushButton("设置") self.set_valve_btn.setStyleSheet( "QPushButton { background: #0960D1; color: white; border: none;" "border-radius: 6px; padding: 7px 16px; font-weight: bold; font-size: 14px; }" "QPushButton:hover { background: #0856B8; }" ) self.set_valve_btn.setCursor(Qt.PointingHandCursor) self.set_valve_btn.clicked.connect(self._on_set_valve) man_layout.addWidget(self.set_valve_btn) man_layout.addStretch() grid.addWidget(self.manual_panel, row, 0, 1, 2) # --- B7: 控制启停行(跨两列) --- row = 6 b7 = _transparent_widget() b7h = QHBoxLayout(b7) b7h.setContentsMargins(0, 0, 0, 0) b7h.setSpacing(12) self.start_btn = QPushButton("开始控制") self.start_btn.setIcon(QIcon(os.path.join(_SRC_DIR, "start_control.svg"))) self.start_btn.setIconSize(QSize(18, 18)) self.start_btn.setProperty("cssClass", "action") self.start_btn.setStyleSheet( "QPushButton { background-color: #0F955D; color: white; border: none;" "border-radius: 6px; padding: 9px 24px; font-weight: bold; font-size: 14px; }" "QPushButton:hover { background-color: #0D8250; }" ) self.start_btn.setCursor(Qt.PointingHandCursor) self.start_btn.clicked.connect(self._on_toggle_control) self.plot_btn = QPushButton("绘制图线") self.plot_btn.setIcon(QIcon(os.path.join(_SRC_DIR, "plot.svg"))) self.plot_btn.setIconSize(QSize(18, 18)) self.plot_btn.setStyleSheet( "QPushButton { background: white; color: #0960D1; border: 1.5px solid #0960D1;" "border-radius: 6px; padding: 9px 20px; font-weight: bold; font-size: 14px; }" "QPushButton:hover { background: #EBF3FE; }" ) self.plot_btn.setCursor(Qt.PointingHandCursor) self.plot_btn.clicked.connect(self._on_plot) self.collect_data_cb = QCheckBox("同步收集数据集") b7h.addWidget(self.start_btn) b7h.addWidget(self.plot_btn) b7h.addWidget(self.collect_data_cb) grid.addWidget(b7, row, 0, 1, 2) # ============================================================ # 以下方法完全兼容旧版 API,main_window.py 无需变动 # ============================================================ # ---- 模式切换 ---- def _on_mode_changed_internal(self, btn): if btn == self.radio_pid: mode = "PID" self.rl_panel.hide() self.manual_panel.hide() self.pid_panel.show() self.collect_data_cb.setEnabled(True) self.model_combobox.setEnabled(False) self.load_model_btn.setEnabled(False) self.refresh_models_btn.setEnabled(False) elif btn == self.radio_rl: mode = "RL" self.pid_panel.hide() self.manual_panel.hide() self.rl_panel.show() self.collect_data_cb.setEnabled(True) self.model_combobox.setEnabled(True) self.load_model_btn.setEnabled(True) self.refresh_models_btn.setEnabled(True) elif btn == self.radio_manual: mode = "MANUAL" self.rl_panel.hide() self.pid_panel.hide() self.manual_panel.show() self.collect_data_cb.setChecked(False) self.collect_data_cb.setEnabled(False) else: mode = "RL" self.mode_changed.emit(mode) def init_mode_ui(self): self.rl_panel.show() self.pid_panel.hide() self.manual_panel.hide() def set_mode_switch_enabled(self, enabled: bool): self.radio_pid.setEnabled(enabled) self.radio_rl.setEnabled(enabled) self.radio_manual.setEnabled(enabled) # ---- 信号处理 ---- def _on_set_target(self): try: target = float(self.target_entry.text()) if 0 <= target <= 3000: self.target_set_requested.emit(target) else: self.target_set_requested.emit(-1) except ValueError: self.target_set_requested.emit(-1) def _on_load_model(self): selected = self.model_combobox.currentText() self.model_load_requested.emit(selected) def _on_refresh_models(self): self.models_refresh_requested.emit() def _on_update_pid(self): try: kp = float(self.Kp_entry.text()) ki = float(self.Ki_entry.text()) kd = float(self.Kd_entry.text()) self.pid_update_requested.emit(kp, ki, kd) except ValueError: self.log_message_requested.emit("错误: PID参数输入无效,请输入有效数字") def _on_set_valve(self): try: valve = float(self.valve_entry.text()) if 0 <= valve <= 120: self.manual_valve_set_requested.emit(valve) else: self.manual_valve_set_requested.emit(-1) except ValueError: self.manual_valve_set_requested.emit(-2) def _on_toggle_control(self): self.control_toggle_requested.emit() def _on_plot(self): self.plot_requested.emit() # ---- 公开方法 (由 main_window 调用) ---- def set_control_running(self, running: bool): if running: self.start_btn.setText("停止控制") self.start_btn.setIcon(QIcon()) self.start_btn.setProperty("cssClass", "danger") self.start_btn.setStyleSheet( "QPushButton { background-color: #EF4444; color: white; border: none;" "border-radius: 6px; padding: 9px 24px; font-weight: bold; font-size: 14px; }" "QPushButton:hover { background-color: #DC2626; }" ) else: self.start_btn.setText("开始控制") self.start_btn.setIcon(QIcon(os.path.join(_SRC_DIR, "start_control.svg"))) self.start_btn.setIconSize(QSize(18, 18)) self.start_btn.setProperty("cssClass", "action") self.start_btn.setStyleSheet( "QPushButton { background-color: #0F955D; color: white; border: none;" "border-radius: 6px; padding: 9px 24px; font-weight: bold; font-size: 14px; }" "QPushButton:hover { background-color: #0D8250; }" ) self.start_btn.style().unpolish(self.start_btn) self.start_btn.style().polish(self.start_btn) def update_display(self, pressure: float, target: float, valve: float): self.current_pressure_lbl.setText(f"{pressure:.1f}") self.target_pressure_lbl.setText(f"{target:.1f}") self.valve_opening_lbl.setText(f"{valve:.1f}") def update_pid_entries(self, kp: float, ki: float, kd: float): self.Kp_entry.setText(f"{kp:.3f}") self.Ki_entry.setText(f"{ki:.3f}") self.Kd_entry.setText(f"{kd:.3f}") def update_model_list(self, files: list): self.model_combobox.clear() if files: self.model_combobox.addItems(files) else: self.model_combobox.addItem("无模型文件") def get_mode(self) -> str: if self.radio_pid.isChecked(): return "PID" elif self.radio_manual.isChecked(): return "MANUAL" return "RL" def get_collect_data(self) -> bool: return self.collect_data_cb.isChecked() def get_control_params(self) -> dict: return { "volume": float(self.volume_entry.text() or "0"), "flow": float(self.flow_entry.text() or "100"), } def get_pid_params(self) -> tuple: return ( float(self.Kp_entry.text() or "1.0"), float(self.Ki_entry.text() or "0.4"), float(self.Kd_entry.text() or "0"), ) def get_manual_valve(self) -> float: return float(self.valve_entry.text() or "0") def enable_plot_button(self, enable: bool): self.plot_btn.setEnabled(enable) def set_pid_entries_text(self, kp, ki, kd): self.Kp_entry.setText(str(kp)) self.Ki_entry.setText(str(ki)) self.Kd_entry.setText(str(kd))