diff --git a/src/gui/panels/step10_watercolor_panel.py b/src/gui/panels/step10_watercolor_panel.py index f135579..e2dd5da 100644 --- a/src/gui/panels/step10_watercolor_panel.py +++ b/src/gui/panels/step10_watercolor_panel.py @@ -15,16 +15,11 @@ Step10 面板 - 水色指数反演(散点 CSV 模式) """ import os -import sys import traceback from pathlib import Path from typing import Dict, List, Optional -# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) -from _step_path_resolver import resolve_subdir, scan_work_dir_for_input +from src.gui.panels._step_path_resolver import resolve_subdir, scan_work_dir_for_input from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QFormLayout, diff --git a/src/gui/panels/step11_map_panel.py b/src/gui/panels/step11_map_panel.py index 2ef2158..5a5c0ba 100644 --- a/src/gui/panels/step11_map_panel.py +++ b/src/gui/panels/step11_map_panel.py @@ -5,15 +5,10 @@ Step10 面板 - 专题图生成 """ import os -import sys import traceback from pathlib import Path from typing import List, Optional -# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) from src.gui.panels._step_path_resolver import resolve_subdir, get_step_output_path, scan_work_dir_for_input from PyQt5.QtCore import Qt, QThread, pyqtSignal @@ -649,13 +644,13 @@ class Step11MapPanel(QWidget): out_dir = resolve_subdir(self._get_default_work_dir(), 'visualization') os.makedirs(out_dir, exist_ok=True) tif_stem = Path(geotiff_path).stem - chinese_name = mapper._get_chinese_title(tif_stem) - output_png = os.path.join(out_dir, f"{chinese_name}_专题图.png") self.run_button.setEnabled(False) try: from src.postprocessing.map import ContentMapper mapper = ContentMapper() + chinese_name = mapper._get_chinese_title(tif_stem) + output_png = os.path.join(out_dir, f"{chinese_name}_专题图.png") result_path = mapper.visualize_raster( raster_tif_path=geotiff_path, output_file=output_png, diff --git a/src/gui/panels/step12_viz_panel.py b/src/gui/panels/step12_viz_panel.py index 394e720..52e4558 100644 --- a/src/gui/panels/step12_viz_panel.py +++ b/src/gui/panels/step12_viz_panel.py @@ -6,7 +6,6 @@ VisualizationPanel - 可视化分析面板 """ import os -import sys import traceback from pathlib import Path from typing import Optional, List, Union @@ -14,11 +13,7 @@ from typing import Optional, List, Union import numpy as np import pandas as pd -# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) -from _step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir +from src.gui.panels._step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal, QAbstractTableModel from PyQt5.QtGui import QPixmap @@ -1716,7 +1711,7 @@ class Step12VizPanel(QWidget): factory = getattr(main_window, '_panel_factory', None) if main_window else None # [新增] 直接从 Step 1 面板读取原始 .shp 的绝对路径,突破工作目录限制 - step1_panel = factory.get_panel('step1_mask') if factory else None + step1_panel = factory.get_panel('step1') if factory else None if step1_panel: s1_conf = step1_panel.get_config() s1_mask = s1_conf.get('mask_path') diff --git a/src/gui/panels/step13_report_panel.py b/src/gui/panels/step13_report_panel.py index 4a80424..687008b 100644 --- a/src/gui/panels/step13_report_panel.py +++ b/src/gui/panels/step13_report_panel.py @@ -5,16 +5,11 @@ ReportGenerationPanel - Word 分析报告生成面板 """ import os -import sys import traceback from pathlib import Path from typing import Optional -# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) -from _step_path_resolver import resolve_subdir +from src.gui.panels._step_path_resolver import resolve_subdir from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSettings from PyQt5.QtWidgets import ( diff --git a/src/gui/panels/step1_panel.py b/src/gui/panels/step1_panel.py index 900115d..619c7b6 100644 --- a/src/gui/panels/step1_panel.py +++ b/src/gui/panels/step1_panel.py @@ -5,13 +5,9 @@ Step1 面板 - 水域掩膜生成 (已移除“启用此步骤”) """ import os -import sys from pathlib import Path -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) -from _step_path_resolver import resolve_subdir +from src.gui.panels._step_path_resolver import resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, diff --git a/src/gui/panels/step2_panel.py b/src/gui/panels/step2_panel.py index 78cdc43..48ea83c 100644 --- a/src/gui/panels/step2_panel.py +++ b/src/gui/panels/step2_panel.py @@ -5,13 +5,9 @@ Step2 面板 - 耀斑区域识别 (已移除“启用此步骤”) """ import os -import sys from pathlib import Path -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) -from _step_path_resolver import resolve_subdir, scan_work_dir_for_input +from src.gui.panels._step_path_resolver import resolve_subdir, scan_work_dir_for_input from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, diff --git a/src/gui/panels/step3_panel.py b/src/gui/panels/step3_panel.py index 7cdbf4d..bf15826 100644 --- a/src/gui/panels/step3_panel.py +++ b/src/gui/panels/step3_panel.py @@ -5,13 +5,9 @@ Step3 面板 - 耀斑去除 (已移除“启用此步骤”) """ import os -import sys from pathlib import Path -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) -from _step_path_resolver import resolve_subdir, scan_work_dir_for_input +from src.gui.panels._step_path_resolver import resolve_subdir, scan_work_dir_for_input from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, @@ -268,7 +264,7 @@ class Step3Panel(QWidget): form_layout.addRow(row_layout) def open_interactive_viewer(self): - from src.gui.water_quality_gui import InteractiveViewerDialog + from src.gui.components.chart_dialogs import InteractiveViewerDialog img_path = self.img_file.get_path() if not img_path or not os.path.isfile(img_path): QMessageBox.warning(self, "警告", "请先选择影像文件!") @@ -381,7 +377,7 @@ class Step3Panel(QWidget): config['sugar_iter'] = self.sugar_iter.value() if self.sugar_iter.value() > 0 else None config['sugar_sigma'] = self._safe_float(self.sugar_sigma, 1.0) config['sugar_estimate_background'] = self.sugar_estimate_background.isChecked() - config['sugar_glint_mask_method'] = self.sugar_glint_mask_method.currentData() + config['sugar_glint_mask_method'] = self.sugar_glint_mask_method.currentText() config['sugar_termination_thresh'] = self._safe_float(self.sugar_termination_thresh, 20.0) try: import ast diff --git a/src/gui/panels/step4_sampling_panel.py b/src/gui/panels/step4_sampling_panel.py index b69f179..a278a75 100644 --- a/src/gui/panels/step4_sampling_panel.py +++ b/src/gui/panels/step4_sampling_panel.py @@ -12,7 +12,6 @@ Step4 面板 - 采样点布设(内嵌交互式光谱探针视图) """ import os -import sys from pathlib import Path import matplotlib.pyplot as plt @@ -23,10 +22,7 @@ import pandas as pd plt.rcParams['font.sans-serif'] = ['Microsoft YaHei', 'SimHei', 'sans-serif'] plt.rcParams['axes.unicode_minus'] = False -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) -from _step_path_resolver import resolve_subdir, scan_work_dir_for_input +from src.gui.panels._step_path_resolver import resolve_subdir, scan_work_dir_for_input from PyQt5.QtCore import QTimer, Qt from PyQt5.QtWidgets import ( diff --git a/src/gui/panels/step5_clean_panel.py b/src/gui/panels/step5_clean_panel.py index 8541852..1d88fce 100644 --- a/src/gui/panels/step5_clean_panel.py +++ b/src/gui/panels/step5_clean_panel.py @@ -5,14 +5,9 @@ Step5 面板 - 数据清洗 (完美对齐无跳动重构版) """ import os -import sys from pathlib import Path -# 路径归一化 helper -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) -from _step_path_resolver import resolve_subdir +from src.gui.panels._step_path_resolver import resolve_subdir import pandas as pd from PyQt5.QtWidgets import ( @@ -85,22 +80,32 @@ class Step5CleanPanel(QWidget): controls_layout = QHBoxLayout() controls_layout.setContentsMargins(0, 0, 0, 0) + # ── 【核心修复】:剥离 Suffix,恢复微调按钮,提升输入体验 ── + lbl = QLabel("加载预览行数:") + lbl.setMinimumWidth(100) + controls_layout.addWidget(lbl) + self.preview_rows_spin = QSpinBox() - self.preview_rows_spin.setRange(1, 200) + self.preview_rows_spin.setRange(1, 1000) # 放宽一下预览上限 self.preview_rows_spin.setValue(10) - self.preview_rows_spin.setSuffix(" 行") - self.preview_rows_spin.setButtonSymbols(QSpinBox.NoButtons) - self.preview_rows_spin.setMinimumWidth(80) + self.preview_rows_spin.setAlignment(Qt.AlignCenter) # 数字居中,体验更好 + self.preview_rows_spin.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.preview_rows_spin.setMinimumWidth(90) + controls_layout.addWidget(self.preview_rows_spin) + + suffix_lbl = QLabel("行") # 将单位作为独立标签,避免光标被困住 + controls_layout.addWidget(suffix_lbl) + + controls_layout.addSpacing(16) self.preview_btn = QPushButton("刷新预览") self.preview_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('normal')) self.preview_btn.setMinimumWidth(100) self.preview_btn.clicked.connect(self.load_csv_preview) - - # 使用封装的方法实现严格对齐的行 - self._add_row_with_fixed_label(controls_layout, "加载预览行数:", self.preview_rows_spin) controls_layout.addWidget(self.preview_btn) + controls_layout.addStretch() + # ───────────────────────────────────────────────────────── self.preview_table = QTableView() self.preview_table.setEditTriggers(QAbstractItemView.NoEditTriggers) @@ -155,7 +160,7 @@ class Step5CleanPanel(QWidget): self.reset_preview() def _add_row_with_fixed_label(self, layout, label_text, widget): - """辅助方法:创建绝对锁死宽度的对齐行""" + """辅助方法:创建绝对锁死宽度的对齐行 (保留原始代码结构) - 已废弃,保留仅用于兼容""" lbl = QLabel(label_text) lbl.setMinimumWidth(100) layout.addWidget(lbl) @@ -215,19 +220,12 @@ class Step5CleanPanel(QWidget): def reset_preview(self, message="请选择 CSV 文件并点击刷新预览"): """重置预览表格""" try: - from src.gui.water_quality_gui import PandasTableModel + from src.gui.components.data_models import PandasTableModel empty_model = PandasTableModel(pd.DataFrame()) self.preview_table.setModel(empty_model) self.preview_status_label.setText(message) except ImportError: - # 兼容水质软件 V2 版的重构包结构 - try: - from src.new.main_view import PandasTableModel - empty_model = PandasTableModel(pd.DataFrame()) - self.preview_table.setModel(empty_model) - self.preview_status_label.setText(message) - except ImportError: - self.preview_status_label.setText("数据预览模块加载失败,请检查 PandasTableModel 依赖") + self.preview_status_label.setText("数据预览模块加载失败") def load_csv_preview(self): """加载 CSV 预览数据""" @@ -240,13 +238,10 @@ class Step5CleanPanel(QWidget): return try: - from src.gui.water_quality_gui import PandasTableModel + from src.gui.components.data_models import PandasTableModel except ImportError: - try: - from src.new.main_view import PandasTableModel - except ImportError: - self.reset_preview("数据预览模块加载失败") - return + self.reset_preview("数据预览模块加载失败") + return try: rows_to_preview = max(1, self.preview_rows_spin.value()) diff --git a/src/gui/panels/step6_feature_panel.py b/src/gui/panels/step6_feature_panel.py index 7022da6..7e6edcf 100644 --- a/src/gui/panels/step6_feature_panel.py +++ b/src/gui/panels/step6_feature_panel.py @@ -5,13 +5,8 @@ Step6 面板 - 光谱特征提取 (完美对齐无跳动重构版) """ import os -import sys from pathlib import Path -# 路径归一化 helper -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) from src.gui.panels._step_path_resolver import resolve_subdir, scan_work_dir_for_input from PyQt5.QtWidgets import ( diff --git a/src/gui/panels/step7_inversion_panel.py b/src/gui/panels/step7_inversion_panel.py index 180ff11..01f0382 100644 --- a/src/gui/panels/step7_inversion_panel.py +++ b/src/gui/panels/step7_inversion_panel.py @@ -6,7 +6,6 @@ Step7 视图 - 水质光谱指数计算 (完美对齐卡片化重构版) import os import sys -import csv from pathlib import Path import pandas as pd @@ -25,7 +24,7 @@ from src.gui.components.custom_widgets import FileSelectWidget from src.gui.styles import ModernStylesheet # ========================================== -# 模块级字典 + 防穿透 ListWidget(与 Step10 对齐) +# 模块级字典 + 防穿透 ListWidget # ========================================== CATEGORY_CHINESE_MAP = { 'Total_Suspended_Matter': '总悬浮物 (TSM)', @@ -43,7 +42,8 @@ CATEGORY_CHINESE_MAP = { class NoScrollPassListWidget(QListWidget): - """一个绝对不会把滚轮事件传给外层父组件的列表控件(与 Step10 同源)。""" + """一个绝对不会把滚轮事件传给外层父组件的列表控件""" + def wheelEvent(self, event): super().wheelEvent(event) event.accept() @@ -52,7 +52,7 @@ class NoScrollPassListWidget(QListWidget): class Step7InversionPanel(QWidget): """步骤7:水质光谱指数计算""" - # --- 保持原有的类级别映射表不变 --- + # --- 保留原有的类级别映射表作为兜底 --- _formula_type_map = { 'NDWI': 'ratio', 'MNDWI': 'ratio', 'AWEI': 'ratio', 'AWEInsh': 'ratio', 'NDTI': 'ratio', 'WRI': 'ratio', 'NDSS': 'ratio', 'NDSSI': 'ratio', @@ -74,48 +74,10 @@ class Step7InversionPanel(QWidget): 'COD_4': 'concentration' } - _formula_color_map = { - 'ratio': ModernStylesheet.COLORS['panel_bg'], # 比值型用白色底 - 'concentration': ModernStylesheet.COLORS['primary_light'] # 浓度型用浅蓝底 - } - - _formula_coef_map = { - 'Chla_3B': [25.0456, 116.8924], - 'Chla_2B': [5.8753, 56.4025], - 'Chla_2B_sim': [38.16, 21.36], - 'Chla_4B': [27.79, 137.9], - 'Chla_NDCI': [14.039, 86.115, 194.325], - 'Chla_NDI': [1.321, 23.94], - 'Chla_Peak': [3.614, 5.097], - 'Chla_SABG': [10.2, 5.6], - 'Chla_MCI': [0.5, 0.2], - 'Chla_FAI': [0.8, 0.1], - - 'TP_1': [0.1, 0.05], - 'TP_2': [0.2, 0.1], - 'TP_3': [0.15, 0.08], - 'TP_4': [0.12, 0.06], - 'TP_5': [0.18, 0.09], - 'TP_6': [0.25, 0.12], - - 'TN_1': [0.5, 0.2], - 'TN_2': [0.6, 0.3], - 'TN_3': [0.45, 0.15], - 'TN_4': [0.55, 0.25], - - 'NH3_1': [0.05, 0.02], - 'NH3_2': [0.06, 0.03], - 'NH3_3': [0.04, 0.015], - 'NH3_4': [0.07, 0.035], - - 'COD_1': [10.0, 5.0], - 'COD_2': [12.0, 6.0], - 'COD_3': [8.5, 4.0], - 'COD_4': [11.0, 5.5], - } - def __init__(self, parent=None): self._formula_df = None + self._categories = [] + self._current_type_filter = "all" # 'all', 'ratio', 'concentration' super().__init__(parent) self.init_ui() @@ -123,7 +85,7 @@ class Step7InversionPanel(QWidget): # 注入全局样式 self.setStyleSheet(ModernStylesheet.get_main_stylesheet()) - # 主布局:增加四周留白(24px)和呼吸间距(20px) + # 主布局 main_layout = QVBoxLayout(self) main_layout.setContentsMargins(24, 24, 24, 24) main_layout.setSpacing(20) @@ -157,7 +119,6 @@ class Step7InversionPanel(QWidget): self.formula_file.line_edit.setReadOnly(True) self.formula_file.label.setMinimumWidth(120) - # 智能查找自带的公式库文件(兼容开发 + PyInstaller 打包) builtin_csv = self._resolve_builtin_csv() if builtin_csv: self.formula_file.set_path(builtin_csv) @@ -181,7 +142,6 @@ class Step7InversionPanel(QWidget): params_layout.setSpacing(16) params_layout.setContentsMargins(20, 24, 20, 20) - # 辅助布局:公式选择列表 list_row = QHBoxLayout() list_row.setContentsMargins(0, 0, 0, 0) @@ -197,7 +157,17 @@ class Step7InversionPanel(QWidget): # 按钮工具栏 btn_layout = QHBoxLayout() btn_layout.setContentsMargins(0, 0, 0, 0) - btn_layout.setSpacing(10) # 👉 [新增] 增加按钮之间的水平间距,不再挤在一起 + btn_layout.setSpacing(10) + + # 👉 【新增】: 按类别筛选下拉框 (对齐 Step 10) + btn_layout.addWidget(QLabel("按类别筛选:")) + self.category_combo = QComboBox() + self.category_combo.setStyleSheet(""" + QComboBox { padding: 4px 8px; border: 1px solid #C0C0C0; border-radius: 4px; min-height: 24px; } + """) + self.category_combo.currentIndexChanged.connect(self._on_category_changed) + btn_layout.addWidget(self.category_combo) + btn_layout.addSpacing(10) self.btn_select_all = QPushButton("全选") self.btn_select_none = QPushButton("清空") @@ -207,26 +177,28 @@ class Step7InversionPanel(QWidget): for btn in [self.btn_select_all, self.btn_select_none, self.btn_select_ratio, self.btn_select_conc, self.btn_reload]: - base_style = ModernStylesheet.get_button_stylesheet('normal') - # 👉 [修改] 追加左右内边距 (padding-left/right),保证无论文字长短都不会被截断 btn.setStyleSheet(base_style + "\nQPushButton { padding-left: 14px; padding-right: 14px; }") - - # 👉 [修改] 高度从 28 稍微调高到 30,让中文字体上下有更好的呼吸感 btn.setFixedHeight(30) - - # 👉 [新增] 设置尺寸策略:高度固定,宽度根据文字内容自动撑开 btn.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) - btn_layout.addWidget(btn) btn_layout.addStretch() + + # 👉 【升级】: 统计标签 (显示选中、当前可见、总计) + self.formula_count_label = QLabel("已选: 0 | 显示: 0 / 共 0 个") + self.formula_count_label.setStyleSheet( + "color: #666666; font-size: 13px; font-weight: bold; padding-right: 10px;") + btn_layout.addWidget(self.formula_count_label) + right_layout.addLayout(btn_layout) - # 列表框 (去掉 setSelectionMode,改为默认行为) - self.formula_list = QListWidget() - self.formula_list.setMinimumHeight(150) - # 用样式表去掉默认的边框和丑陋的焦点框,增加 hover 和复选框大小调整 + # 列表框 (使用自定义防穿透类) + self.formula_list = NoScrollPassListWidget() + self.formula_list.setMinimumHeight(220) # 稍微加高以展示复杂内容 + self.formula_list.setWordWrap(True) # 允许公式换行 + + # 禁用原生的 selected 高亮样式,并设置透明背景 self.formula_list.setStyleSheet(f""" QListWidget {{ border: 1px solid {ModernStylesheet.COLORS['border']}; @@ -235,19 +207,28 @@ class Step7InversionPanel(QWidget): outline: none; }} QListWidget::item {{ - padding: 4px 8px; /* 稍微减小上下间距,给复选框留空间 */ + padding: 8px 8px; /* 加大 padding 让多行显示更舒服 */ border-bottom: 1px solid {ModernStylesheet.COLORS['border_light']}; }} QListWidget::item:hover {{ background-color: {ModernStylesheet.COLORS['hover']}; }} - /* 放大复选框并拉开间距 */ + QListWidget::item:selected {{ + background-color: transparent; + color: #333333; + }} QListWidget::indicator {{ width: 16px; height: 16px; margin-right: 6px; }} """) + + # 绑定状态改变事件 + self.formula_list.itemChanged.connect(self._update_formula_count) + # 绑定行选中事件,实现点击整行打勾 + self.formula_list.itemSelectionChanged.connect(self._on_selection_changed) + right_layout.addWidget(self.formula_list) list_row.addWidget(list_label) @@ -300,14 +281,7 @@ class Step7InversionPanel(QWidget): self._load_formulas_from_csv() def _resolve_builtin_csv(self): - """探测内置 waterindex.csv 的实际位置。 - - 按优先级探测 4 个候选路径(兼容开发 + PyInstaller 打包): - 1. sys._MEIPASS/_internal/model/ (PyInstaller onedir 带 _MEIPASS) - 2. sys._MEIPASS/model/ (PyInstaller onefile 或 onedir) - 3. /../model/ (开发模式真实位置:src/gui/model/) - 4. /../../model/ (项目根 model/,兼容旧布局) - """ + """探测内置 waterindex.csv 的实际位置。""" panel_dir = os.path.dirname(os.path.abspath(__file__)) candidates = [ os.path.join(panel_dir, '..', 'model', 'waterindex.csv'), @@ -325,9 +299,8 @@ class Step7InversionPanel(QWidget): def get_config(self) -> dict: """收集当前界面的配置字典""" - # 只筛选出打勾状态为 Checked 的项 selected_names = [ - self.formula_list.item(i).text() + self.formula_list.item(i).data(Qt.UserRole) # 从隐藏属性中提取纯净的名称 for i in range(self.formula_list.count()) if self.formula_list.item(i).checkState() == Qt.Checked ] @@ -336,7 +309,7 @@ class Step7InversionPanel(QWidget): 'training_csv_path': self.training_data_widget.get_path(), 'formula_csv_file': self.formula_file.get_path(), 'formula_names': selected_names, - 'enabled': True # 默认启用 + 'enabled': True } output_path = self.output_file.get_path() if output_path: @@ -350,15 +323,16 @@ class Step7InversionPanel(QWidget): if 'formula_csv_file' in config: self.formula_file.set_path(config['formula_csv_file']) - # 恢复勾选状态 if 'formula_names' in config: target_names = set(config['formula_names']) self.formula_list.blockSignals(True) for i in range(self.formula_list.count()): item = self.formula_list.item(i) - state = Qt.Checked if item.text() in target_names else Qt.Unchecked + item_name = item.data(Qt.UserRole) + state = Qt.Checked if item_name in target_names else Qt.Unchecked item.setCheckState(state) self.formula_list.blockSignals(False) + self._update_formula_count() if 'output_path' in config: self.output_file.set_path(config['output_path']) @@ -383,48 +357,180 @@ class Step7InversionPanel(QWidget): break if not target_col: - QMessageBox.warning(self, "错误", f"CSV 文件格式不正确,列名应为 'Index_Name' 或 'Formula_Name'。当前列为: {list(df.columns)}") + QMessageBox.warning(self, "错误", + f"CSV 文件格式不正确,列名应为 'Index_Name' 或 'Formula_Name'。当前列为: {list(df.columns)}") self.formula_list.blockSignals(False) return - for idx_name in df[target_col]: - item = QListWidgetItem(idx_name) + # 👉 【提取所有分类并更新下拉框】 + cats = set() + for _, row in df.iterrows(): + if 'Category' in df.columns and pd.notna(row['Category']): + cats.add(str(row['Category']).strip()) + else: + cats.add("综合/未分类") - # 开启复选框功能,并设置为默认选中 + self._categories = sorted(list(cats)) + self.category_combo.blockSignals(True) + self.category_combo.clear() + self.category_combo.addItem("全部类别", "全部") + for cat in self._categories: + display_name = CATEGORY_CHINESE_MAP.get(cat, cat) + self.category_combo.addItem(display_name, cat) + self.category_combo.blockSignals(False) + + # 👉 【构建美观的列表项】 + for _, row in df.iterrows(): + idx_name = str(row[target_col]).strip() + item = QListWidgetItem() item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) - ftype = self._formula_type_map.get(idx_name, 'ratio') - # 简化底色判断,保留类型的色彩提示 - bg_color = Qt.transparent if ftype == 'ratio' else Qt.GlobalColor.cyan + # 解析公式和分类 + formula_str = str(row['Formula']).strip() if 'Formula' in df.columns else "" + cat = str(row['Category']).strip() if 'Category' in df.columns and pd.notna( + row['Category']) else "综合/未分类" + + # 解析精确的 Formula_Type + ftype = '' + if 'Formula_Type' in df.columns and pd.notna(row['Formula_Type']): + ftype = str(row['Formula_Type']).strip().lower() + if 'conc' in ftype: + ftype = 'concentration' + elif 'ratio' in ftype: + ftype = 'ratio' + else: + ftype = self._formula_type_map.get(idx_name, 'ratio') + + # 保存隐式数据 (极其重要,用于过滤和配置提取) + item.setData(Qt.UserRole, idx_name) + item.setData(Qt.UserRole + 1, cat) + item.setData(Qt.UserRole + 2, ftype) + + # 设置带分类和公式的美观文本 (对齐 Step 10) + cat_cn = CATEGORY_CHINESE_MAP.get(cat, cat) + ftype_cn = "比值型" if ftype == 'ratio' else "浓度型" + if formula_str: + item.setText(f"【{cat_cn}】 {idx_name} ({ftype_cn})\n公式: {formula_str}") + else: + item.setText(f"【{cat_cn}】 {idx_name} ({ftype_cn})") + + # 设置底色 + bg_color = QColor("#FFFFFF") if ftype == 'ratio' else QColor("#E8F4F8") item.setBackground(bg_color) self.formula_list.addItem(item) self.formula_list.blockSignals(False) + self._update_formula_count() except Exception as e: QMessageBox.critical(self, "加载错误", f"加载公式文件时发生错误:\n{str(e)}") - def _select_all_formulas(self): + def _refresh_visibility(self): + """核心过滤引擎:根据下拉框和类型按钮动态隐藏/显示公式""" + current_category = self.category_combo.currentData() + if not current_category: + current_category = "全部" + for i in range(self.formula_list.count()): - self.formula_list.item(i).setCheckState(Qt.Checked) + item = self.formula_list.item(i) + item_cat = item.data(Qt.UserRole + 1) + item_type = item.data(Qt.UserRole + 2) + + cat_match = (current_category == "全部" or item_cat == current_category) + type_match = (self._current_type_filter == "all" or item_type == self._current_type_filter) + + if cat_match and type_match: + item.setHidden(False) + else: + item.setHidden(True) + + self._update_formula_count() + + def _on_category_changed(self, index: int): + """下拉框改变分类时触发""" + self._current_type_filter = "all" # 切换分类时默认重置类型过滤 + self._refresh_visibility() + + def _update_formula_count(self): + """计算并更新多维度统计标签""" + total = self.formula_list.count() + visible = 0 + selected = 0 + for i in range(total): + item = self.formula_list.item(i) + if not item.isHidden(): + visible += 1 + if item.checkState() == Qt.Checked: + selected += 1 + + type_str = "" + if self._current_type_filter == 'ratio': + type_str = " (仅比值型)" + elif self._current_type_filter == 'concentration': + type_str = " (仅浓度型)" + + self.formula_count_label.setText(f"已选: {selected} | 显示: {visible}{type_str} / 共 {total} 个") + + def _on_selection_changed(self): + """实现点击整行任意位置即可切换勾选状态,并立刻取消高亮""" + self.formula_list.blockSignals(True) + for item in self.formula_list.selectedItems(): + new_state = Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked + item.setCheckState(new_state) + item.setSelected(False) # 立刻取消原生选中状态,避免变白 + self.formula_list.blockSignals(False) + self._update_formula_count() + + def _select_all_formulas(self): + """全选并显示所有(当前分类下的)公式""" + self._current_type_filter = "all" + self._refresh_visibility() + + self.formula_list.blockSignals(True) + for i in range(self.formula_list.count()): + item = self.formula_list.item(i) + if not item.isHidden(): + item.setCheckState(Qt.Checked) + self.formula_list.blockSignals(False) + self._update_formula_count() def _clear_formula_selection(self): + """只清空当前显示项的勾选""" + self.formula_list.blockSignals(True) for i in range(self.formula_list.count()): - self.formula_list.item(i).setCheckState(Qt.Unchecked) + item = self.formula_list.item(i) + if not item.isHidden(): + item.setCheckState(Qt.Unchecked) + self.formula_list.blockSignals(False) + self._update_formula_count() def _select_ratio_formulas(self): + """仅显示比值型,并勾选""" + self._current_type_filter = "ratio" + self._refresh_visibility() + + self.formula_list.blockSignals(True) for i in range(self.formula_list.count()): item = self.formula_list.item(i) - ftype = self._formula_type_map.get(item.text(), 'ratio') - item.setCheckState(Qt.Checked if ftype == 'ratio' else Qt.Unchecked) + if not item.isHidden(): + item.setCheckState(Qt.Checked) + self.formula_list.blockSignals(False) + self._update_formula_count() def _select_conc_formulas(self): + """仅显示浓度型,并勾选""" + self._current_type_filter = "concentration" + self._refresh_visibility() + + self.formula_list.blockSignals(True) for i in range(self.formula_list.count()): item = self.formula_list.item(i) - ftype = self._formula_type_map.get(item.text(), 'ratio') - item.setCheckState(Qt.Checked if ftype == 'concentration' else Qt.Unchecked) + if not item.isHidden(): + item.setCheckState(Qt.Checked) + self.formula_list.blockSignals(False) + self._update_formula_count() def _on_run_single_clicked(self): """点击独立运行按钮:包装参数并通过 EventBus 派发任务""" @@ -433,7 +539,6 @@ class Step7InversionPanel(QWidget): QMessageBox.warning(self, "输入错误", "请先选择输入的样本数据 (CSV)!") return - # 检查是否有任何一项处于打勾状态 has_checked = any( self.formula_list.item(i).checkState() == Qt.Checked for i in range(self.formula_list.count()) @@ -444,7 +549,6 @@ class Step7InversionPanel(QWidget): return config = self.get_config() - # 兼容旧架构的数据流,外层包装为 {'step_id': config} payload = { 'step_name': 'step7_index', 'config': {'step7_index': config} @@ -452,12 +556,6 @@ class Step7InversionPanel(QWidget): global_event_bus.publish('RequestRunSingleStep', payload) def update_from_config(self, work_dir=None, pipeline=None): - """从全局配置/Pipeline 同步工作目录。 - - step6 的训练数据已由 PANEL_REGISTRY 的 dependencies 自动通过 set_config - 注入到 self.training_data_widget;此处仅缓存 work_dir 并填入默认输出路径, - 不重复拉取,避免与 panel_factory 注入路径冲突。 - """ if work_dir: self.work_dir = work_dir elif hasattr(self, 'work_dir') and self.work_dir: @@ -465,10 +563,9 @@ class Step7InversionPanel(QWidget): else: self.work_dir = None - # 仅在输出框为空时填入默认路径(不创建目录,留给 pipeline 执行时创建) if self.work_dir and not self.output_file.get_path(): output_dir = os.path.join(self.work_dir, "7_Water_Quality_Indices") default_path = os.path.join(output_dir, "training_spectra_indices.csv").replace('\\', '/') self.output_file.set_path(default_path) elif not self.work_dir: - self.output_file.set_path("") + self.output_file.set_path("") \ No newline at end of file diff --git a/src/gui/panels/step8_ml_train_panel.py b/src/gui/panels/step8_ml_train_panel.py index 7b56ed9..d166160 100644 --- a/src/gui/panels/step8_ml_train_panel.py +++ b/src/gui/panels/step8_ml_train_panel.py @@ -5,13 +5,8 @@ Step8 面板 - 机器学习建模 """ import os -import sys from pathlib import Path -# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) from src.gui.panels._step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir, scan_work_dir_for_input import pandas as pd diff --git a/src/gui/panels/step9_ml_predict_panel.py b/src/gui/panels/step9_ml_predict_panel.py index 8c6e30a..0b5daf3 100644 --- a/src/gui/panels/step9_ml_predict_panel.py +++ b/src/gui/panels/step9_ml_predict_panel.py @@ -5,16 +5,11 @@ Step11 面板 - 机器学习预测 """ import os -import sys from pathlib import Path import pandas as pd -# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) -_HERE = os.path.dirname(os.path.abspath(__file__)) -if _HERE not in sys.path: - sys.path.insert(0, _HERE) -from _step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir, scan_work_dir_for_input +from src.gui.panels._step_path_resolver import get_step_output_path, resolve_step_widget, resolve_subdir, scan_work_dir_for_input from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QGroupBox, QFormLayout,