feat(gui): 全流程面板合并 + 一键式运行 GUI 入口集成
This commit is contained in:
237
src/gui/core/pipeline_mode_dialog.py
Normal file
237
src/gui/core/pipeline_mode_dialog.py
Normal 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()
|
||||
Reference in New Issue
Block a user