步骤十页面修改

This commit is contained in:
DXC
2026-06-29 10:16:12 +08:00
parent 14ea72a4e7
commit ef24398df9

View File

@ -39,6 +39,23 @@ from PyQt5.QtCore import Qt, QThread, pyqtSignal
from src.gui.components.custom_widgets import FileSelectWidget from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet from src.gui.styles import ModernStylesheet
# ==========================================
# 必须放在所有 import 的下方,class 的上方!
# ==========================================
CATEGORY_CHINESE_MAP = {
'Total_Suspended_Matter': '总悬浮物 (TSM)',
'Phycocyanin (BGA_PC)': '藻蓝蛋白 (PC)',
'Turbidity': '浊度 (Turbidity)',
'chlorophyll_a': '叶绿素a (Chl-a)',
'Colored_Dissolved_Organic_Matter': '有色可溶性有机物 (CDOM)',
'Secchi_Disk_Depth': '透明度 (SDD)',
'Total_Nitrogen': '总氮 (TN)',
'Total_Phosphorus': '总磷 (TP)',
'Chemical_Oxygen_Demand': '化学需氧量 (COD)',
'Ammonia_Nitrogen': '氨氮 (NH3-N)',
'Dissolved_Oxygen': '溶解氧 (DO)'
}
class WaterIndexWorker(QThread): class WaterIndexWorker(QThread):
"""后台线程:散点 CSV → 逐行公式计算 → 多 CSV 输出 """后台线程:散点 CSV → 逐行公式计算 → 多 CSV 输出
@ -99,6 +116,16 @@ class WaterIndexWorker(QThread):
self.error.emit(f"{e}\n{traceback.format_exc()}") self.error.emit(f"{e}\n{traceback.format_exc()}")
class NoScrollPassListWidget(QListWidget):
"""一个绝对不会把滚轮事件传给外层父组件的列表控件"""
def wheelEvent(self, event):
# 先执行原本正常的列表内部滚动逻辑
super().wheelEvent(event)
# 核心黑科技:强制接收该事件。
# 告诉 PyQt:"这个滚轮操作到我这里就结束了,无论如何不要传给外层的 ScrollArea"
event.accept()
class Step10WatercolorPanel(QWidget): class Step10WatercolorPanel(QWidget):
"""步骤10:水色指数反演(散点 CSV 模式)""" """步骤10:水色指数反演(散点 CSV 模式)"""
@ -109,89 +136,141 @@ class Step10WatercolorPanel(QWidget):
self._categories: List[str] = [] self._categories: List[str] = []
self._all_formulas: List[Dict] = [] self._all_formulas: List[Dict] = []
self._formula_list_widgets: Dict[str, QListWidgetItem] = {} self._formula_list_widgets: Dict[str, QListWidgetItem] = {}
self._current_type_filter = "all" # 'all', 'ratio', 'concentration'
self.init_ui() self.init_ui()
self._load_formulas() self._load_formulas()
def init_ui(self): def init_ui(self):
layout = QVBoxLayout() layout = QVBoxLayout()
# 注意:这里彻底删除了顶部的 title 和 hint 变量,保持全局页面清爽
# ---- 标题 ---- # ==========================================
title = QLabel("步骤10:水色指数反演(散点 CSV 模式)") # 卡片 1:输入数据配置
title.setFont(QFont("Arial", 12, QFont.Bold)) # ==========================================
layout.addWidget(title) input_group = QGroupBox("📁 输入数据")
# 【关键修正1】必须用 QVBoxLayout,如果用 QFormLayout 会导致标签文字重复!
input_layout = QVBoxLayout()
input_layout.setSpacing(16)
input_layout.setContentsMargins(20, 24, 20, 20)
# ---- 说明 ---- self.formula_file = FileSelectWidget(
hint = QLabel( "内置公式库源:",
"读取 Step 4 生成的 sampling_spectra.csv 散点光谱," "CSV Files (*.csv);;All Files (*.*)"
"对每个采样点逐行套用 waterindex.csv 中勾选的公式,"
"输出每公式一个 CSV(列:longitude, latitude, 公式值)。"
"结果可被 Step 11 直接以 ContentMapper 模式消费。"
) )
hint.setWordWrap(True) self.formula_file.line_edit.setReadOnly(True)
hint.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};") self.formula_file.label.setMinimumWidth(100)
layout.addWidget(hint) builtin_csv = self._find_waterindex_csv()
if builtin_csv:
# ---- 输入采样点数据 ---- self.formula_file.set_path(builtin_csv)
input_group = QGroupBox("输入采样点数据") input_layout.addWidget(self.formula_file)
input_layout = QFormLayout()
self.sampling_csv_file = FileSelectWidget( self.sampling_csv_file = FileSelectWidget(
"采样点 CSV:", "采样点 CSV:",
"CSV Files (*.csv);;All Files (*.*)" "CSV Files (*.csv);;All Files (*.*)"
) )
self.sampling_csv_file.line_edit.setPlaceholderText( self.sampling_csv_file.label.setMinimumWidth(100)
"选择 Step 4 输出的 sampling_spectra.csv" input_layout.addWidget(self.sampling_csv_file)
)
input_layout.addRow("采样点 CSV:", self.sampling_csv_file)
# 数据规模提示(运行后回填,避免启动时强制 read_csv) # 注意:彻底去掉了 self.meta_label 及其相关的布局代码
self.meta_label = QLabel("未加载采样点数据")
self.meta_label.setStyleSheet(
"background: #f0f0f0; padding: 4px 8px; border-radius: 4px; "
"font-size: 12px; color: #333;"
)
input_layout.addRow("数据信息:", self.meta_label)
input_group.setLayout(input_layout) input_group.setLayout(input_layout)
layout.addWidget(input_group) layout.addWidget(input_group)
# ---- 公式选择 ---- # ---- 公式选择 ----
formula_group = QGroupBox("公式选择") formula_group = QGroupBox("公式选择")
formula_layout = QGridLayout() formula_layout = QVBoxLayout()
formula_layout.setSpacing(16)
formula_layout.setContentsMargins(20, 24, 20, 20)
# 类别过滤 # 顶部工具栏 (筛选 + 按钮)
formula_layout.addWidget(QLabel("按类别筛选:"), 0, 0) toolbar_layout = QHBoxLayout()
toolbar_layout.setContentsMargins(0, 0, 0, 0)
toolbar_layout.setSpacing(10)
toolbar_layout.addWidget(QLabel("按类别筛选:"))
self.category_combo = QComboBox() self.category_combo = QComboBox()
self.category_combo.currentTextChanged.connect(self._on_category_changed) self.category_combo.setStyleSheet("""
formula_layout.addWidget(self.category_combo, 0, 1, 1, 2) QComboBox { padding: 4px 8px; border: 1px solid #C0C0C0; border-radius: 4px; min-height: 24px; }
""")
self.category_combo.currentIndexChanged.connect(self._on_category_changed)
toolbar_layout.addWidget(self.category_combo)
toolbar_layout.addSpacing(20)
# 全选/取消全选
select_btn_layout = QHBoxLayout()
self.select_all_btn = QPushButton("全选") self.select_all_btn = QPushButton("全选")
self.select_all_btn.setMaximumWidth(80)
self.select_all_btn.clicked.connect(self._select_all)
select_btn_layout.addWidget(self.select_all_btn)
self.deselect_all_btn = QPushButton("取消全选") self.deselect_all_btn = QPushButton("取消全选")
self.deselect_all_btn.setMaximumWidth(80) self.select_ratio_btn = QPushButton("仅比值型")
self.deselect_all_btn.clicked.connect(self._deselect_all) self.select_conc_btn = QPushButton("仅浓度型")
select_btn_layout.addWidget(self.deselect_all_btn)
select_btn_layout.addStretch()
formula_layout.addLayout(select_btn_layout, 0, 3)
# 公式列表 # 强制指定标准的按钮样式,彻底解决没有边框的问题
self.formula_list = QListWidget() btn_style = """
self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection) QPushButton { background-color: #FFFFFF; border: 1px solid #C0C0C0; border-radius: 4px; padding: 4px 16px; color: #333333; }
self.formula_list.setMinimumHeight(200) QPushButton:hover { background-color: #F0F0F0; border: 1px solid #0078D7; }
self.formula_list.itemChanged.connect(self._on_item_changed) QPushButton:pressed { background-color: #E0E0E0; }
formula_layout.addWidget(self.formula_list, 1, 0, 1, 4) """
# 把新按钮也加进循环里应用样式
for btn in [self.select_all_btn, self.deselect_all_btn, self.select_ratio_btn, self.select_conc_btn]:
btn.setStyleSheet(btn_style)
btn.setFixedHeight(28)
btn.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
self.select_all_btn.clicked.connect(self._select_all)
self.deselect_all_btn.clicked.connect(self._deselect_all)
# 绑定新按钮的事件
self.select_ratio_btn.clicked.connect(self._select_ratio)
self.select_conc_btn.clicked.connect(self._select_conc)
toolbar_layout.addWidget(self.select_all_btn)
toolbar_layout.addWidget(self.deselect_all_btn)
toolbar_layout.addWidget(self.select_ratio_btn)
toolbar_layout.addWidget(self.select_conc_btn)
toolbar_layout.addStretch()
# 新增统计标签,直观显示公式数量
self.formula_count_label = QLabel("共 0 个公式")
self.formula_count_label.setStyleSheet("color: #666666; font-size: 13px; font-weight: bold; padding-right: 10px;")
toolbar_layout.addWidget(self.formula_count_label)
formula_layout.addLayout(toolbar_layout)
# 列表框 (使用刚刚定义的防穿透自定义类)
self.formula_list = NoScrollPassListWidget()
self.formula_list.setMinimumHeight(240)
self.formula_list.setWordWrap(True) # 开启自动换行,防止公式过长被截断
# 关键优化:取消原生选中的蓝色渐变框,放大复选框
self.formula_list.setStyleSheet("""
QListWidget {
border: 1px solid #D1D5DB; border-radius: 6px; outline: none; background-color: #FFFFFF;
}
QListWidget::item {
padding: 10px 8px; border-bottom: 1px solid #F3F4F6;
}
QListWidget::item:hover {
background-color: #F9FAFB;
}
/* 重点:彻底屏蔽原生的深蓝色选中渐变 */
QListWidget::item:selected {
background-color: transparent; color: #333333;
}
QListWidget::indicator {
width: 18px; height: 18px; margin-right: 8px;
}
""")
# 绑定新方法:实现整行点击打勾
self.formula_list.itemSelectionChanged.connect(self._on_selection_changed)
formula_layout.addWidget(self.formula_list)
formula_group.setLayout(formula_layout) formula_group.setLayout(formula_layout)
layout.addWidget(formula_group) layout.addWidget(formula_group)
# ---- 输出设置 ---- # ==========================================
# 卡片 3:输出设置
# ==========================================
output_group = QGroupBox("输出设置") output_group = QGroupBox("输出设置")
output_layout = QFormLayout() # 【关键修正2】改成 QVBoxLayout,消除重复的"输出目录:"
output_layout = QVBoxLayout()
output_layout.setSpacing(16)
output_layout.setContentsMargins(20, 24, 20, 20)
self.output_dir = FileSelectWidget( self.output_dir = FileSelectWidget(
"输出目录:", "输出目录:",
@ -200,7 +279,7 @@ class Step10WatercolorPanel(QWidget):
self.output_dir.line_edit.setPlaceholderText( self.output_dir.line_edit.setPlaceholderText(
"留空 → 工作目录/10_WaterIndex_CSV" "留空 → 工作目录/10_WaterIndex_CSV"
) )
output_layout.addRow("输出目录:", self.output_dir) output_layout.addWidget(self.output_dir)
output_group.setLayout(output_layout) output_group.setLayout(output_layout)
layout.addWidget(output_group) layout.addWidget(output_group)
@ -217,15 +296,33 @@ class Step10WatercolorPanel(QWidget):
self.progress_label.setStyleSheet("font-size: 11px; color: #666;") self.progress_label.setStyleSheet("font-size: 11px; color: #666;")
layout.addWidget(self.progress_label) layout.addWidget(self.progress_label)
# ---- 启用 & 运行 ---- # ==========================================
self.enable_checkbox = QCheckBox("启用此步骤") # 卡片 4:输出与执行 (与进度显示合并)
self.enable_checkbox.setChecked(True) # ==========================================
layout.addWidget(self.enable_checkbox) execute_group = QGroupBox("🚀 输出与执行")
execute_layout = QVBoxLayout()
execute_layout.setSpacing(16)
execute_layout.setContentsMargins(20, 24, 20, 20)
self.run_btn = QPushButton("▶ 执行水色指数反演") # 把原本悬空的进度条放进这个卡片里,看起来更整洁
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success')) execute_layout.addWidget(self.progress_bar)
self.run_btn.clicked.connect(self.run_step) execute_layout.addWidget(self.progress_label)
layout.addWidget(self.run_btn)
# 完美对齐的底部按钮栏(彻底移除了 enable_checkbox)
action_layout = QHBoxLayout()
action_layout.addStretch()
self.run_btn = QPushButton("独立运行步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('primary'))
self.run_btn.setMinimumWidth(140)
# 统一改成连接到标准的 EventBus 方法
self.run_btn.clicked.connect(self._on_run_single_clicked)
action_layout.addWidget(self.run_btn)
execute_layout.addLayout(action_layout)
execute_group.setLayout(execute_layout)
layout.addWidget(execute_group)
layout.addStretch() layout.addStretch()
self.setLayout(layout) self.setLayout(layout)
@ -244,7 +341,6 @@ class Step10WatercolorPanel(QWidget):
def _load_formulas(self): def _load_formulas(self):
"""加载 waterindex.csv 中的公式""" """加载 waterindex.csv 中的公式"""
if not self._waterindex_csv or not Path(self._waterindex_csv).exists(): if not self._waterindex_csv or not Path(self._waterindex_csv).exists():
self.meta_label.setText("⚠️ waterindex.csv 未找到")
return return
import csv import csv
@ -252,37 +348,44 @@ class Step10WatercolorPanel(QWidget):
try: try:
with open(self._waterindex_csv, 'r', encoding='utf-8-sig') as f: with open(self._waterindex_csv, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f) reader = csv.DictReader(f)
self._all_formulas = list(reader) for row in reader:
# 关键修复1:强制清理 CSV 键值中的所有前后空格,防止匹配失败导致丢失
cleaned_row = {str(k).strip(): str(v).strip() for k, v in row.items() if k}
self._all_formulas.append(cleaned_row)
except Exception as e: except Exception as e:
self.meta_label.setText(f"⚠️ 加载公式失败: {e}") QMessageBox.critical(self, "读取失败", f"加载公式失败: {e}")
return return
# 提取所有类别 # 提取所有类别
cats = set() cats = set()
for f in self._all_formulas: for f in self._all_formulas:
c = f.get('Category', '').strip() c = f.get('Category', '')
if c: if c:
cats.add(c) cats.add(c)
self._categories = sorted(cats) self._categories = sorted(cats)
self.category_combo.blockSignals(True)
self.category_combo.clear() self.category_combo.clear()
self.category_combo.addItem("全部") self.category_combo.addItem("全部", "全部")
self.category_combo.addItems(self._categories) 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)
self._populate_list("全部") # 关键修复2:不再动态渲染,而是一次性初始化所有公式
self._init_list_items()
def _populate_list(self, category: str): def _init_list_items(self):
"""根据类别填充公式列表""" """一次性加载所有公式项,按照类别进行排序"""
from PyQt5.QtGui import QColor
self.formula_list.clear() self.formula_list.clear()
self._formula_list_widgets.clear() self._formula_list_widgets.clear()
formulas_to_show = ( # 核心优化:对 self._all_formulas 进行自定义排序
[f for f in self._all_formulas if f.get('Category', '') == category] # 排序规则:主要按 Category (类别/物质) 排,同类别下按 Formula_Name 排
if category != "全部" sorted_formulas = sorted(self._all_formulas, key=lambda x: (x.get('Category', ''), x.get('Formula_Name', '')))
else self._all_formulas
)
for f in formulas_to_show: for f in sorted_formulas:
name = f.get('Formula_Name', '') name = f.get('Formula_Name', '')
formula_str = f.get('Formula', '') formula_str = f.get('Formula', '')
cat = f.get('Category', '') cat = f.get('Category', '')
@ -291,52 +394,117 @@ class Step10WatercolorPanel(QWidget):
item = QListWidgetItem() item = QListWidgetItem()
item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
item.setCheckState(Qt.Checked) item.setCheckState(Qt.Checked)
# 存入隐式数据供后续逻辑使用
item.setData(Qt.UserRole, name) item.setData(Qt.UserRole, name)
item.setText( item.setData(Qt.UserRole + 1, cat)
f"☑ {name} [{cat}] ({ftype})\n {formula_str}" item.setData(Qt.UserRole + 2, ftype) # 存入类型,供"仅比值型/仅浓度型"按钮区分
)
item.setToolTip(f"{name}\n{category}\n{formula_str}") cat_cn = CATEGORY_CHINESE_MAP.get(cat, cat)
ftype_cn = "比值型" if ftype == 'ratio' else "浓度型"
item.setText(f"【{cat_cn}】 {name} ({ftype_cn})\n公式: {formula_str}")
item.setToolTip(f"{name}\n{cat_cn}\n{formula_str}")
bg_color = QColor("#FFFFFF") if ftype == 'ratio' else QColor("#E8F4F8")
item.setBackground(bg_color)
self.formula_list.addItem(item) self.formula_list.addItem(item)
self._formula_list_widgets[name] = item self._formula_list_widgets[name] = item
def _on_category_changed(self, category: str): self.formula_count_label.setText(f"共加载 {self.formula_list.count()} 个公式")
self._populate_list(category)
def _refresh_visibility(self):
"""统一管理公式列表的可见性(综合物质类别 + 公式类型)"""
# 获取当前选中的物质类别
current_category = self.category_combo.currentData()
if not current_category:
current_category = "全部"
visible_count = 0
for i in range(self.formula_list.count()):
item = self.formula_list.item(i)
item_cat = item.data(Qt.UserRole + 1)
item_type = item.data(Qt.UserRole + 2)
# 条件1:物质类别匹配
cat_match = (current_category == "全部" or item_cat == current_category)
# 条件2:公式类型匹配
type_match = (self._current_type_filter == "all" or item_type == self._current_type_filter)
if cat_match and type_match:
item.setHidden(False)
visible_count += 1
else:
item.setHidden(True)
# 更新统计标签(如果选了特定类型,标签上体现出来)
type_str = ""
if self._current_type_filter == 'ratio':
type_str = " (仅比值型)"
elif self._current_type_filter == 'concentration':
type_str = " (仅浓度型)"
self.formula_count_label.setText(f"当前显示: {visible_count} 个{type_str}")
def _on_category_changed(self, index: int):
# 切换物质类别时,重置类型过滤器(可选:如果不重置,就能叠加筛选。这里我们选择重置,体验更好)
self._current_type_filter = "all"
self._refresh_visibility()
def _select_all(self): def _select_all(self):
for item in self.formula_list.selectedItems(): """恢复显示所有当前类别的公式,并全部勾选"""
item.setCheckState(Qt.Checked) self._current_type_filter = "all"
# 也全选当前显示的 self._refresh_visibility()
for i in range(self.formula_list.count()): for i in range(self.formula_list.count()):
it = self.formula_list.item(i) item = self.formula_list.item(i)
it.setCheckState(Qt.Checked) if not item.isHidden():
item.setCheckState(Qt.Checked)
def _deselect_all(self): def _deselect_all(self):
"""保留当前显示状态,但全部取消勾选"""
for i in range(self.formula_list.count()): for i in range(self.formula_list.count()):
it = self.formula_list.item(i) item = self.formula_list.item(i)
it.setCheckState(Qt.Unchecked) if not item.isHidden():
item.setCheckState(Qt.Unchecked)
def _select_ratio(self):
"""过滤出比值型并全部勾选"""
self._current_type_filter = "ratio"
self._refresh_visibility()
for i in range(self.formula_list.count()):
item = self.formula_list.item(i)
if not item.isHidden():
item.setCheckState(Qt.Checked)
def _select_conc(self):
"""过滤出浓度型并全部勾选"""
self._current_type_filter = "concentration"
self._refresh_visibility()
for i in range(self.formula_list.count()):
item = self.formula_list.item(i)
if not item.isHidden():
item.setCheckState(Qt.Checked)
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)
def _on_item_changed(self, item: QListWidgetItem): def _on_item_changed(self, item: QListWidgetItem):
pass # 可扩展:实时统计选中数量 pass # 可扩展:实时统计选中数量
def _refresh_sampling_meta(self):
"""从 sampling_csv 路径快速 peek 数据规模(不触发公式计算)"""
path = self.sampling_csv_file.get_path().strip()
if not path:
self.meta_label.setText("未加载采样点数据")
return
if not Path(path).exists():
self.meta_label.setText("⚠️ 采样点 CSV 不存在")
return
try:
import pandas as pd
df = pd.read_csv(path, encoding="utf-8-sig", nrows=0)
n_cols = len(df.columns)
self.meta_label.setText(
f"✅ 已选采样点 CSV({n_cols} 列,完整列数将在运行时打印)"
)
except Exception as e:
self.meta_label.setText(f"⚠️ 读取失败: {e}")
def _get_selected_formula_names(self) -> List[str]: def _get_selected_formula_names(self) -> List[str]:
names = [] names = []
for i in range(self.formula_list.count()): for i in range(self.formula_list.count()):
@ -369,7 +537,6 @@ class Step10WatercolorPanel(QWidget):
def set_config(self, config: dict): def set_config(self, config: dict):
if config.get('sampling_csv_path'): if config.get('sampling_csv_path'):
self.sampling_csv_file.set_path(config['sampling_csv_path']) self.sampling_csv_file.set_path(config['sampling_csv_path'])
self._refresh_sampling_meta()
if config.get('output_dir'): if config.get('output_dir'):
self.output_dir.set_path(config['output_dir']) self.output_dir.set_path(config['output_dir'])
if 'selected_formulas' in config: if 'selected_formulas' in config:
@ -425,7 +592,6 @@ class Step10WatercolorPanel(QWidget):
self.work_dir or '', sampling_path self.work_dir or '', sampling_path
).replace('\\', '/') ).replace('\\', '/')
self.sampling_csv_file.set_path(sampling_path) self.sampling_csv_file.set_path(sampling_path)
self._refresh_sampling_meta()
# 自动填入输出目录(默认 work_dir/10_WaterIndex_CSV/) # 自动填入输出目录(默认 work_dir/10_WaterIndex_CSV/)
if self.work_dir: if self.work_dir: