# debug_tab.py """页面3:系统辨识 + 高级设置(Section 卡片 + 蓝竖线装饰) 参考 connection_tab 的页面设计,使用 _make_section_card 创建带蓝色左侧竖线的 纯白卡片,内部以 QGridLayout 双列排列表单项。 """ from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit, QPushButton, ) from PySide6.QtCore import Qt, QSettings, Signal from ui.connection_tab import _make_section_card # ---- 按钮默认样式(品牌蓝底白字,保证不被父级 inline stylesheet 覆盖) ---- _BTN_STYLE = """ QPushButton { background-color: #0960D1; color: white; border: none; border-radius: 6px; padding: 9px 20px; font-weight: bold; font-size: 14px; } QPushButton:hover { background-color: #0856B8; } QPushButton:pressed { background-color: #0960D1; } """ _BTN_STYLE_DANGER = """ QPushButton { background-color: #EF4444; color: white; border: none; border-radius: 6px; padding: 9px 20px; font-weight: bold; font-size: 14px; } QPushButton:hover { background-color: #DC2626; } QPushButton:pressed { background-color: #B91C1C; } """ def _compact_label(text: str, parent=None) -> QLabel: """紧凑表单标签(无 140px min-width,自然适应文字宽度)。""" 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 _wrap_widget(child: QWidget) -> QWidget: """将子控件放入透明容器(使用 cssClass 而非 inline stylesheet, 避免覆盖子控件的 QSS 样式)。""" w = QWidget() w.setProperty("cssClass", "transparentBg") w.style().unpolish(w) w.style().polish(w) lay = QHBoxLayout(w) lay.setContentsMargins(0, 0, 0, 0) lay.setSpacing(0) lay.addWidget(child, 1) return w def _transparent_widget() -> QWidget: """创建一个透明的空容器(用于包裹多个控件)。""" w = QWidget() w.setProperty("cssClass", "transparentBg") w.style().unpolish(w) w.style().polish(w) return w class DebugTab(QWidget): """模型调试页面""" # ---- 信号 ---- identify_start_requested = Signal() identify_stop_requested = Signal() volume_measure_requested = Signal() volume_stop_requested = Signal() def __init__(self, colors: dict, parent=None): super().__init__(parent) self.setProperty("cssClass", "tabPage") self.colors = colors # 记录按钮当前是否处于"运行中"状态 self._identifying_running = False self._volume_running = False main_layout = QVBoxLayout(self) main_layout.setContentsMargins(20, 16, 20, 16) main_layout.setSpacing(14) # ========================================== # Card 1: 系统辨识 # ========================================== ident_card, ident_grid = _make_section_card(self, "系统辨识", colors) self._build_ident_section(ident_grid) main_layout.addWidget(ident_card) # ========================================== # Card 2: 高级设置 # ========================================== adv_card, adv_grid = _make_section_card(self, "高级设置", colors) self._build_advanced_section(adv_grid) main_layout.addWidget(adv_card) adv_card.hide() main_layout.addStretch() # 恢复上次保存的设置 self._load_settings() # ========================================== # Card 1: 系统辨识 — 4 行双列 + 1 行通栏 # ========================================== def _build_ident_section(self, grid: QGridLayout): # 重置 _make_section_card 预设的单列表单列宽配置 for c in range(10): grid.setColumnMinimumWidth(c, 0) grid.setColumnStretch(c, 0) # 紧凑双列布局: 左标签 | 左输入区 | 间距 | 右标签 | 右输入区 grid.setColumnMinimumWidth(0, 60) grid.setColumnStretch(1, 1) grid.setColumnMinimumWidth(2, 100) grid.setColumnMinimumWidth(3, 60) grid.setColumnStretch(4, 1) grid.setVerticalSpacing(8) # ---- 第 1 行:压力上限(kPa) | 过程升温(°C) ---- row = 0 grid.addWidget(_compact_label("压力上限:", self), row, 0) self.p_max_entry = QLineEdit("200") grid.addWidget(self._with_unit(self.p_max_entry, "kPa"), row, 1) grid.addWidget(_compact_label("过程升温:", self), row, 3) self.T_delta_entry = QLineEdit("30") grid.addWidget(self._with_unit(self.T_delta_entry, "°C"), row, 4) # ---- 第 2 行:约束上界 | 下界 ---- row = 1 grid.addWidget(_compact_label("约束上界:", self), row, 0) self.fit_high_entry = QLineEdit("200") grid.addWidget(_wrap_widget(self.fit_high_entry), row, 1) grid.addWidget(_compact_label("下界:", self), row, 3) self.fit_low_entry = QLineEdit("50") grid.addWidget(_wrap_widget(self.fit_low_entry), row, 4) # ---- 第 3 行:容积(L) | 测试按钮 ---- row = 2 grid.addWidget(_compact_label("容积:", self), row, 0) self.volume_entry = QLineEdit() grid.addWidget(self._with_unit(self.volume_entry, "L"), row, 1) self.test_btn = QPushButton("测试") self.test_btn.setStyleSheet(_BTN_STYLE) self.test_btn.setCursor(Qt.PointingHandCursor) self.test_btn.clicked.connect(self._on_measure_volume) btn_wrap = _transparent_widget() btn_h = QHBoxLayout(btn_wrap) btn_h.setContentsMargins(0, 0, 0, 0) btn_h.addWidget(self.test_btn) btn_h.addStretch() grid.addWidget(btn_wrap, row, 4) # ---- 第 4 行:周期(s) | 阶数 ---- row = 3 grid.addWidget(_compact_label("周期:", self), row, 0) self.period_entry = QLineEdit("2.5") grid.addWidget(self._with_unit(self.period_entry, "s"), row, 1) grid.addWidget(_compact_label("阶数:", self), row, 3) self.order_entry = QLineEdit("6") grid.addWidget(_wrap_widget(self.order_entry), row, 4) # ---- 第 5 行(通栏):序列 + 开始辨识按钮 ---- row = 4 grid.addWidget(_compact_label("序列:", self), row, 0) seq_wrap = _transparent_widget() seq_h = QHBoxLayout(seq_wrap) seq_h.setContentsMargins(0, 0, 0, 0) seq_h.setSpacing(8) self.levels_entry = QLineEdit() seq_h.addWidget(self.levels_entry, 1) self.ident_result_label = QLabel("等待开始") self.ident_result_label.setStyleSheet( "color: #64748B; font-size: 13px; font-weight: 600;" ) seq_h.addWidget(self.ident_result_label) self.identify_btn = QPushButton(" ▶ 开始辨识") self.identify_btn.setStyleSheet(_BTN_STYLE) self.identify_btn.setCursor(Qt.PointingHandCursor) self.identify_btn.clicked.connect(self._on_start_identify) seq_h.addWidget(self.identify_btn) grid.addWidget(seq_wrap, row, 1, 1, 4) # 跨越列 1-4 # Keep the legacy widgets for internal compatibility, but do not # expose confidential measurement parameters in the customer UI. # Volume-test values come only from volume_measurement.json. for index in range(grid.count()): widget = grid.itemAt(index).widget() if widget is not None and widget not in (btn_wrap, seq_wrap): widget.hide() self.levels_entry.hide() # ========================================== # Card 2: 高级设置 — 2 行双列 # ========================================== def _build_advanced_section(self, grid: QGridLayout): # 重置 _make_section_card 预设的单列表单列宽配置 for c in range(10): grid.setColumnMinimumWidth(c, 0) grid.setColumnStretch(c, 0) # 同样采用紧凑双列布局 grid.setColumnMinimumWidth(0, 60) grid.setColumnStretch(1, 1) grid.setColumnMinimumWidth(2, 100) grid.setColumnMinimumWidth(3, 60) grid.setColumnStretch(4, 1) grid.setVerticalSpacing(8) # ---- 第 1 行:死区 ---- row = 0 grid.addWidget(_compact_label("死区:", self), row, 0) self.dz_entry = QLineEdit() self.dz_entry.setPlaceholderText("默认2...") grid.addWidget(_wrap_widget(self.dz_entry), row, 1) # ---- 第 2 行:单步限幅 | 总限幅 ---- row = 1 grid.addWidget(_compact_label("单步限幅:", self), row, 0) self.motor_max_entry = QLineEdit() grid.addWidget(_wrap_widget(self.motor_max_entry), row, 1) grid.addWidget(_compact_label("总限幅:", self), row, 3) self.xa_full_entry = QLineEdit() grid.addWidget(_wrap_widget(self.xa_full_entry), row, 4) # ---- 第 3 行:模拟量映射最小值 | 最大值 ---- row = 2 grid.addWidget(_compact_label("模拟量映射最小值:", self), row, 0) self.volthege_min_entry = QLineEdit("819") grid.addWidget(_wrap_widget(self.volthege_min_entry), row, 1) grid.addWidget(_compact_label("最大值:", self), row, 3) self.volthege_max_entry = QLineEdit("4095") grid.addWidget(_wrap_widget(self.volthege_max_entry), row, 4) # ---- 第 4 行:确认设置按钮 ---- row = 3 self.confirm_settings_btn = QPushButton("确认设置") self.confirm_settings_btn.setStyleSheet(_BTN_STYLE) self.confirm_settings_btn.setCursor(Qt.PointingHandCursor) self.confirm_settings_btn.clicked.connect(self._save_settings) btn_wrap = _transparent_widget() btn_h = QHBoxLayout(btn_wrap) btn_h.setContentsMargins(0, 0, 0, 0) btn_h.addWidget(self.confirm_settings_btn) btn_h.addStretch() grid.addWidget(btn_wrap, row, 0, 1, 5) # ========================================== # 辅助方法 # ========================================== def _with_unit(self, line_edit: QLineEdit, unit: str) -> QWidget: """将输入框与单位标签组合为一个 widget,单位以灰色显示在输入框右侧。""" w = _transparent_widget() h = QHBoxLayout(w) h.setContentsMargins(0, 0, 0, 0) h.setSpacing(0) h.addWidget(line_edit, 1) unit_lbl = QLabel(unit) unit_lbl.setStyleSheet( "color: #94A3B8; font-size: 12px; background: transparent;" "padding: 0 10px 0 6px;" ) h.addWidget(unit_lbl) return w # ---- 信号处理 ---- def _on_start_identify(self): if self._identifying_running: self.identify_stop_requested.emit() else: self._identifying_running = True self.identify_btn.setText(" ■ 结束辨识") self.identify_btn.setStyleSheet(_BTN_STYLE_DANGER) self.identify_start_requested.emit() def _on_measure_volume(self): if self._volume_running: self.volume_stop_requested.emit() else: self._volume_running = True self.test_btn.setText("停止") self.test_btn.setStyleSheet(_BTN_STYLE_DANGER) self.volume_measure_requested.emit() # ---- 公开方法:任务完成后由 main_window 调用恢复按钮 ---- def set_identify_finished(self): self._identifying_running = False self.identify_btn.setText(" ▶ 开始辨识") self.identify_btn.setStyleSheet(_BTN_STYLE) def set_identification_feedback(self, text: str, state="neutral"): """显示当前辨识审核状态。""" colors = { "neutral": "#64748B", "pending": "#2563EB", "passed": "#15803D", "failed": "#B91C1C", } color = colors.get(state, colors["neutral"]) self.ident_result_label.setText(text) self.ident_result_label.setStyleSheet( f"color: {color}; font-size: 13px; font-weight: 600;" ) def set_volume_finished(self): self._volume_running = False self.test_btn.setText("测试") self.test_btn.setStyleSheet(_BTN_STYLE) # ---- 公开数据获取方法(接口与旧版完全兼容) ---- def get_identify_params(self) -> dict: """获取辨识参数""" return { "p_max": float(self.p_max_entry.text() or "200"), "T_delta": float(self.T_delta_entry.text() or "30"), "fit_high": float(self.fit_high_entry.text() or "200"), "fit_low": float(self.fit_low_entry.text() or "50"), "volume": float(self.volume_entry.text() or "0"), "period": float(self.period_entry.text() or "2.5"), "order": int(self.order_entry.text() or "6"), "levels": self._parse_levels(), } def get_advanced_params(self) -> dict: """获取高级设置参数""" dz = self.dz_entry.text().strip() mm = self.motor_max_entry.text().strip() xa = self.xa_full_entry.text().strip() return { "dz": float(dz) if dz else None, "motor_max": float(mm) if mm else None, "xa_full": float(xa) if xa else None, "volthege_min": int(self.volthege_min_entry.text() or "0"), "volthege_max": int(self.volthege_max_entry.text() or "4095"), } def _parse_levels(self) -> list: """解析序列输入""" levels_str = self.levels_entry.text().strip() if not levels_str: print("未输入序列,使用默认值: 10,20,30,40,50,60,70,80") return [10, 20, 30, 40, 50, 60, 70, 80] try: levels = [int(x.strip()) for x in levels_str.split(',')] if len(levels) < 2: print("序列至少需要两个值,使用默认值: 10,20,30,40,50,60,70,80") return [10, 20, 30, 40, 50, 60, 70, 80] print(f"使用自定义序列: {levels}") return levels except ValueError: print("序列格式错误,使用默认值: 10,20,30,40,50,60,70,80") return [10, 20, 30, 40, 50, 60, 70, 80] def set_volume_text(self, vol: float): """设置容积输入框(测量完成后回填)""" self.volume_entry.setText(f"{vol:.2f}") # ---- 设置持久化 ---- def _save_settings(self): """将高级设置和容积保存到 QSettings,下次启动自动恢复""" settings = QSettings("ReinLoop", "ReinLoop") settings.setValue("advanced/dz", self.dz_entry.text()) settings.setValue("advanced/xa_full", self.xa_full_entry.text()) settings.setValue("advanced/volthege_min", self.volthege_min_entry.text()) settings.setValue("advanced/volthege_max", self.volthege_max_entry.text()) settings.setValue("identify/volume", self.volume_entry.text()) print("设置已保存") def _load_settings(self): """从 QSettings 恢复上次保存的设置(contains 确保空值也能覆盖默认值)""" settings = QSettings("ReinLoop", "ReinLoop") if settings.contains("advanced/dz"): self.dz_entry.setText(settings.value("advanced/dz")) if settings.contains("advanced/xa_full"): self.xa_full_entry.setText(settings.value("advanced/xa_full")) if settings.contains("advanced/volthege_min"): self.volthege_min_entry.setText(settings.value("advanced/volthege_min")) if settings.contains("advanced/volthege_max"): self.volthege_max_entry.setText(settings.value("advanced/volthege_max")) if settings.contains("identify/volume"): self.volume_entry.setText(settings.value("identify/volume"))