fix(ui):修复 3 项 UI 交互痛点(输出路径不显示 / Step4 预览缺失 / Step5 CSV NaN 报错)

1. base_view.py:update_from_config 路由修复
   原默认实现只缓存 work_dir + pipeline,不触发 update_work_directory,
   导致所有派生 view 的输出路径自动填入从未执行。
   补一行 self.update_work_directory(work_dir) 后,13 个 view 全部受益。

2. step4_view.py:恢复采样点交互式预览
   从旧 panel 移植 preview_btn 按钮 + QTimer 2s 心跳(_check_csv_exists)
   + SamplingViewerDialog 弹窗。
   用户在执行 Step 4 后点击按钮即可点击散点查看各采样点光谱曲线。

3. step5_view.py:CSV 预览 NaN 崩溃修复
   pd.read_csv(csv_path, nrows=n) → pd.read_csv(csv_path, nrows=n).fillna("")。
   避免底层 Qt 模型在解析 float64 空值时崩溃(PandasTableModel 路径必填)。
This commit is contained in:
DXC
2026-06-17 13:28:10 +08:00
parent 48668c9e74
commit 9cb3c8ed0d
4 changed files with 98 additions and 510 deletions

View File

@ -47,9 +47,16 @@ class BaseView(QWidget):
self.work_dir = work_dir
def update_from_config(self, work_dir: str, pipeline=None):
"""主窗口推送全局状态——子类可重写以联动 pipeline 状态"""
"""主窗口推送全局状态——子类可重写以联动 pipeline 状态
默认实现:缓存 work_dir + pipeline 后,**自动调用 update_work_directory**
以触发各 view 的输出路径自动填入。这是 main_view.set_work_dir 的唯一入口,
必须让派生类的 update_work_directory 真正执行;子类如需联动 pipeline
可在 update_work_directory 里读取 self.pipeline。
"""
self.work_dir = work_dir
self.pipeline = pipeline
self.update_work_directory(work_dir)
# ------------------------------------------------------------------
# 核心通讯:沿父链向上查找 run_single_step 容器

View File

@ -2,19 +2,24 @@
"""
Step4View —— Step 4采样点布设的端到端模块化 view
UI 从 ``src/gui/panels/step4_sampling_panel.py`` 原样搬迁
注:原 panel 中的 QTimer 心跳检查(``_check_csv_exists``)和交互式预览按钮
在 view 层留接口位,运行时实际由 main_view 的 dispatch_execute 路由到 service
触发状态更新(后续 service 迁移时再补全交互逻辑)。
UI 从 ``src/gui/panels/step4_sampling_panel.py`` 原样搬迁,包括:
- 去耀斑影像 / 水域掩膜 / 采样参数 / 输出路径输入控件
- 独立运行按钮(绿色 success 样式)
- **交互式预览按钮 + QTimer 心跳**——CSV 生成后点击即弹
``SamplingViewerDialog``,可点击散点查看各采样点光谱曲线。
"""
import os
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import (
QCheckBox, QFormLayout, QGroupBox, QPushButton, QSpinBox, QVBoxLayout,
QCheckBox, QFormLayout, QGroupBox, QMessageBox, QPushButton, QSpinBox,
QVBoxLayout,
)
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.dialogs import SamplingViewerDialog
from src.gui.styles import ModernStylesheet
from src.new.core.base_view import BaseView
@ -88,9 +93,23 @@ class Step4View(BaseView):
self.run_btn.clicked.connect(self._on_run_clicked)
layout.addWidget(self.run_btn)
# 交互式预览按钮(旧 panel 移植:仅在 CSV 已生成时启用)
self.preview_btn = QPushButton("📊 交互式预览采样点与光谱")
self.preview_btn.setEnabled(False)
self.preview_btn.clicked.connect(self._open_sampling_viewer)
layout.addWidget(self.preview_btn)
layout.addStretch()
self.setLayout(layout)
# 心跳定时器:每 2 秒检查一次输出 CSV 是否已生成,刷新预览按钮
self._status_timer = QTimer(self)
self._status_timer.timeout.connect(self._check_csv_exists)
self._status_timer.start(2000)
# 输出路径输入框变化时同步刷新预览按钮
self.output_file.line_edit.textChanged.connect(self._on_output_changed)
def get_config(self) -> dict:
config = {
"interval": self.interval.value(),
@ -131,6 +150,36 @@ class Step4View(BaseView):
os.makedirs(output_dir, exist_ok=True)
default_output_path = os.path.join(output_dir, "sampling_spectra.csv").replace("\\", "/")
self.output_file.set_path(default_output_path)
# 路径刚自动填充,立即同步一次预览按钮状态
self._check_csv_exists()
def _on_run_clicked(self):
self.dispatch_execute("step4", self.get_config())
# ------------------------------------------------------------------
# 采样点预览CSV 存在才启用;点击弹窗 SamplingViewerDialog
# (从旧 src/gui/panels/step4_sampling_panel.py 原样移植)
# ------------------------------------------------------------------
def _check_csv_exists(self) -> bool:
"""检查输出 CSV 是否存在,驱动预览按钮启停"""
csv_path = self.output_file.get_path()
enabled = bool(csv_path and os.path.isabs(csv_path) and os.path.exists(csv_path))
self.preview_btn.setEnabled(enabled)
return enabled
def _on_output_changed(self, _text=None):
"""输出路径输入框内容变化时同步刷新预览按钮"""
self._check_csv_exists()
def _open_sampling_viewer(self):
"""打开交互式采样点查看器弹窗"""
csv_path = self.output_file.get_path()
if not csv_path or not os.path.exists(csv_path):
QMessageBox.warning(
self, "文件不存在",
f"采样点 CSV 文件不存在:{csv_path}\n请先执行 Step 4 生成数据。",
)
return
dialog = SamplingViewerDialog(csv_path, self)
dialog.exec_()
self._check_csv_exists()

View File

@ -3,17 +3,20 @@
Step5View —— Step 5数据清洗的端到端模块化 view
UI 从 ``src/gui/panels/step5_clean_panel.py`` 原样搬迁CSV 预览表格在
view 层保留静态占位(运行时由 main_view 控制实际刷新逻辑)。
view 层挂上旧版 ``PandasTableModel``,点击"刷新预览"即可读取前 N 行
渲染到 ``QTableView``,完全本地化,不触发任何 service。
"""
import os
import pandas as pd
from PyQt5.QtWidgets import (
QAbstractItemView, QCheckBox, QGroupBox, QHBoxLayout, QHeaderView,
QLabel, QPushButton, QSpinBox, QTableView, QVBoxLayout,
)
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.components.data_models import PandasTableModel
from src.gui.styles import ModernStylesheet
from src.new.core.base_view import BaseView
@ -39,7 +42,7 @@ class Step5View(BaseView):
hint.setStyleSheet("color: #666; font-size: 10px;")
layout.addWidget(hint)
# CSV 数据预览(静态占位
# CSV 数据预览(实时 pandas 渲染 + PandasTableModel
preview_group = QGroupBox("CSV数据预览")
preview_layout = QVBoxLayout()
controls_layout = QHBoxLayout()
@ -49,7 +52,10 @@ class Step5View(BaseView):
self.preview_rows_spin.setValue(10)
controls_layout.addWidget(self.preview_rows_spin)
self.preview_btn = QPushButton("刷新预览")
self.preview_btn.setEnabled(False) # 后续 service 迁移时再启用
self.preview_btn.setStyleSheet(
ModernStylesheet.get_button_stylesheet("primary")
)
self.preview_btn.clicked.connect(self._on_preview_clicked)
controls_layout.addWidget(self.preview_btn)
controls_layout.addStretch()
@ -118,3 +124,30 @@ class Step5View(BaseView):
def _on_run_clicked(self):
self.dispatch_execute("step5", self.get_config())
# ------------------------------------------------------------------
# CSV 预览pandas → PandasTableModel → QTableView
# ------------------------------------------------------------------
def _on_preview_clicked(self):
csv_path = self.csv_file.get_path()
if not csv_path:
self.preview_status_label.setText("⚠ 请先选择 CSV 文件")
self.preview_table.setModel(None)
return
if not os.path.isfile(csv_path):
self.preview_status_label.setText(f"⚠ 文件不存在: {csv_path}")
self.preview_table.setModel(None)
return
try:
n = int(self.preview_rows_spin.value())
# .fillna("") 把所有 NaN 替换为空字符串,避免底层 Qt 模型在
# 解析 float64 空值时崩溃pandas → PandasTableModel 路径必填)
df = pd.read_csv(csv_path, nrows=n).fillna("")
model = PandasTableModel(df)
self.preview_table.setModel(model)
self.preview_status_label.setText(
f"✓ 已加载 {len(df)} 行 / {len(df.columns)} 列(来源: {os.path.basename(csv_path)}"
)
except Exception as e: # noqa: BLE001 —— 预览失败时降级提示,不影响主流程
self.preview_status_label.setText(f"⚠ 读取失败: {e}")
self.preview_table.setModel(None)