feat(gui): 全流程面板合并 + 一键式运行 GUI 入口集成

This commit is contained in:
DXC
2026-06-09 11:30:42 +08:00
parent aefc9d5aac
commit 28394f2eda
20 changed files with 2843 additions and 2432 deletions

View File

@ -0,0 +1,237 @@
# -*- coding: utf-8 -*-
"""
PipelineModeDialog:全流程运行前的模式选择弹窗。
用户点击"运行完整流程"后,首先弹出此弹窗选择执行模式:
- 选项 A(训练新模型并预测):执行完整建模与预测流程,需要实测水质 CSV
- 选项 B(使用已有模型直接预测):跳过训练步骤,直接使用外部模型目录进行预测
弹窗结果:
- QDialog.Accepted + self.selected_mode = "training" 或 "prediction_only"
- QDialog.Rejected → 调用方中止 run_full_pipeline
"""
import os
from typing import Optional
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QRadioButton, QGroupBox, QButtonGroup, QMessageBox, QSizePolicy,
)
def _is_valid_model_dir(path: str) -> bool:
"""深层递归检测模型目录:只要任意层级存在文件即返回 True。"""
if not path or not os.path.isdir(path):
return False
for _root, _dirs, files in os.walk(path):
if files:
return True
return False
class PipelineModeDialog(QDialog):
"""全流程模式选择对话框。
两个单选按钮覆盖两种业务场景:
- A:训练新模型(完整流程,需要 step4 CSV)
- B:仅预测(跳过 step4/5/7/8,直接用外部模型目录)
属性:
selected_mode: "training" | "prediction_only"
"""
def __init__(self, main_window=None, parent=None):
super().__init__(parent)
self.main_window = main_window
self.selected_mode: Optional[str] = None
self.setWindowTitle("选择运行模式")
self.setMinimumSize(560, 340)
self.setModal(True)
self._setup_ui()
# ------------------------------------------------------------------
# UI 构建
# ------------------------------------------------------------------
def _setup_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(28, 24, 28, 20)
layout.setSpacing(14)
# ── 标题 ──
title = QLabel("请选择全流程运行模式")
title_font = QFont()
title_font.setPointSize(13)
title_font.setBold(True)
title.setFont(title_font)
title.setAlignment(Qt.AlignCenter)
layout.addWidget(title)
layout.addSpacing(4)
# ── 选项 A:训练新模型 ──
group_a = QGroupBox()
group_a.setObjectName("groupA")
group_a.setMinimumHeight(100)
layout.addWidget(group_a)
self.radio_a = QRadioButton("【训练新模型并预测】")
self.radio_a.setChecked(True) # 默认选项 A
self.radio_a.setObjectName("radioTraining")
desc_a = QLabel(
"需要提供实测水质数据 (CSV),将执行完整建模与预测流程。\n"
"包括:水域掩膜 → 耀斑去除 → 光谱特征提取 → 模型训练 → 密集采样 → 预测 → 专题图"
)
desc_a.setWordWrap(True)
desc_a.setStyleSheet("color: #555555; background: transparent;")
desc_a.setObjectName("descA")
vbox_a = QVBoxLayout(group_a)
vbox_a.setContentsMargins(16, 20, 16, 14)
vbox_a.setSpacing(8)
vbox_a.addWidget(self.radio_a)
vbox_a.addWidget(desc_a)
# ── 选项 B:仅预测 ──
group_b = QGroupBox()
group_b.setObjectName("groupB")
group_b.setMinimumHeight(100)
layout.addWidget(group_b)
self.radio_b = QRadioButton("【使用已有模型直接预测】")
self.radio_b.setObjectName("radioPrediction")
desc_b = QLabel(
"跳过模型训练步骤,直接使用导入的外部模型目录进行预测。\n"
"前提条件:请在「监督预测」或「回归预测」面板中指定模型目录。\n"
"适用范围:已有预训练模型、或其他来源模型目录。"
)
desc_b.setWordWrap(True)
desc_b.setStyleSheet("color: #555555; background: transparent;")
desc_b.setObjectName("descB")
vbox_b = QVBoxLayout(group_b)
vbox_b.setContentsMargins(16, 20, 16, 14)
vbox_b.setSpacing(8)
vbox_b.addWidget(self.radio_b)
vbox_b.addWidget(desc_b)
# ── 强制互斥:QButtonGroup ──
self.mode_group = QButtonGroup(self)
self.mode_group.addButton(self.radio_a)
self.mode_group.addButton(self.radio_b)
# ── 提示栏(动态显示 models_dir 状态) ──
self.models_hint = QLabel()
self.models_hint.setObjectName("modelsHint")
self.models_hint.setWordWrap(True)
self.models_hint.setStyleSheet("color: #888888; font-size: 11px; padding: 4px 0;")
layout.addWidget(self.models_hint)
# ── 强制 QRadioButton 指示器为实心圆点 ──
self.setStyleSheet("""
QRadioButton::indicator {
width: 14px;
height: 14px;
}
QRadioButton::indicator:checked {
background-color: #0078D7;
border: 2px solid #0078D7;
border-radius: 7px;
}
QRadioButton::indicator:unchecked {
background-color: white;
border: 2px solid #A0A0A0;
border-radius: 7px;
}
""")
# ── 按钮 ──
btn_layout = QHBoxLayout()
btn_layout.addStretch()
cancel_btn = QPushButton("取消")
cancel_btn.setObjectName("cancelBtn")
cancel_btn.setMinimumWidth(90)
cancel_btn.clicked.connect(self.reject)
self.btn_confirm = QPushButton("确认")
self.btn_confirm.setObjectName("confirmBtn")
self.btn_confirm.setMinimumWidth(90)
self.btn_confirm.setDefault(True)
self.btn_confirm.clicked.connect(self._on_confirm)
btn_layout.addWidget(self.btn_confirm)
btn_layout.addWidget(cancel_btn)
layout.addLayout(btn_layout)
# 信号连接:任一 radio 切换时重新渲染提示 + 按钮状态
self.radio_a.toggled.connect(self._update_models_hint)
self.radio_b.toggled.connect(self._update_models_hint)
# 初始状态渲染
self._update_models_hint()
def _update_models_hint(self, checked=False, *args) -> None:
"""根据当前选中模式和 models_dir 状态更新提示文字及确认按钮可用性。"""
training_checked = self.radio_a.isChecked()
# 从主窗口 config 读取 models_dir(优先 ml,其次 reg)
models_dir = ""
if self.main_window:
config = self.main_window.get_current_config()
models_dir = config.get("step11_ml", {}).get("models_dir", "")
if not models_dir:
models_dir = config.get("step11", {}).get("models_dir", "")
has_files = bool(models_dir and _is_valid_model_dir(models_dir))
dir_exists = bool(models_dir and os.path.isdir(models_dir))
if training_checked:
if hasattr(self, 'btn_confirm') and self.btn_confirm is not None:
self.btn_confirm.setEnabled(True)
if has_files:
self.models_hint.setText(
f"⚠ 注意:当前模型目录已包含文件,继续训练将会【覆盖】原有模型!\n路径:{models_dir}"
)
self.models_hint.setStyleSheet("color: #e65100; font-size: 11px; padding: 4px 0;")
else:
label = f"✓ 模型将保存至该目录(当前为空,安全)。\n路径:{models_dir}" if dir_exists else "✓ 尚未指定模型目录,将使用默认路径创建新模型。"
self.models_hint.setText(label)
self.models_hint.setStyleSheet("color: #2e7d32; font-size: 11px; padding: 4px 0;")
else:
if has_files:
self.models_hint.setText(
f"✓ 已检测到有效模型目录,可以直接预测。\n路径:{models_dir}"
)
self.models_hint.setStyleSheet("color: #2e7d32; font-size: 11px; padding: 4px 0;")
if hasattr(self, 'btn_confirm') and self.btn_confirm is not None:
self.btn_confirm.setEnabled(True)
else:
if dir_exists:
self.models_hint.setText(
f"❌ 错误:模型目录为空(未找到任何文件),无法进行预测!\n路径:{models_dir}"
)
else:
self.models_hint.setText(
"❌ 错误:模型目录为空或不存在!请先返回对应面板配置有效路径。"
)
self.models_hint.setStyleSheet("color: #c62828; font-size: 11px; padding: 4px 0;")
if hasattr(self, 'btn_confirm') and self.btn_confirm is not None:
self.btn_confirm.setEnabled(False)
def _on_confirm(self) -> None:
"""确认按钮回调:直接存储模式并关闭。
注意:按钮禁用状态已在 _update_models_hint 中处理,
此处仅负责结果存储,不再做二次弹窗拦截。
"""
if self.radio_a.isChecked():
self.selected_mode = "training"
else:
self.selected_mode = "prediction_only"
self.accept()