Files
WQ_GUI/src/gui/panels/step5_clean_panel.py
2026-06-30 11:32:40 +08:00

260 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step5 面板 - 数据清洗 (完美对齐无跳动重构版)
"""
import os
from pathlib import Path
from src.gui.panels._step_path_resolver import resolve_subdir
import pandas as pd
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QHBoxLayout, QLabel,
QSpinBox, QPushButton, QTableView, QSizePolicy,
QAbstractItemView, QHeaderView, QMessageBox,
)
from PyQt5.QtCore import Qt
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step5CleanPanel(QWidget):
"""步骤5:数据清洗"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
# 1. 注入全局样式系统
self.setStyleSheet(ModernStylesheet.get_main_stylesheet())
# 主布局:增加四周留白(24px)和模块间的呼吸间距(20px)
main_layout = QVBoxLayout()
main_layout.setContentsMargins(24, 24, 24, 24)
main_layout.setSpacing(20)
# ==========================================
# 卡片 1:输入数据
# ==========================================
input_group = QGroupBox("📁 输入数据")
input_layout = QVBoxLayout()
input_layout.setSpacing(16)
input_layout.setContentsMargins(20, 24, 20, 20)
step5_hint = QLabel("💡 提示: 选择包含水质参数或光谱特征的 CSV 文件,我们将对其进行异常值剔除和清洗。")
step5_hint.setWordWrap(True)
step5_hint.setStyleSheet(f"""
QLabel {{
color: {ModernStylesheet.COLORS['primary']};
background-color: {ModernStylesheet.COLORS['selected']};
border: 1px solid {ModernStylesheet.COLORS['border_light']};
border-radius: 6px;
padding: 10px 14px;
margin-bottom: 4px;
}}
""")
input_layout.addWidget(step5_hint)
self.csv_file = FileSelectWidget(
"输入 CSV 文件:",
"CSV Files (*.csv);;All Files (*.*)"
)
self.csv_file.label.setMinimumWidth(100) # 绝对对齐
input_layout.addWidget(self.csv_file)
input_group.setLayout(input_layout)
main_layout.addWidget(input_group)
# ==========================================
# 卡片 2:数据预览
# ==========================================
preview_group = QGroupBox("🔍 数据预览")
preview_layout = QVBoxLayout()
preview_layout.setSpacing(16)
preview_layout.setContentsMargins(20, 24, 20, 20)
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, 1000) # 放宽一下预览上限
self.preview_rows_spin.setValue(10)
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)
controls_layout.addWidget(self.preview_btn)
controls_layout.addStretch()
# ─────────────────────────────────────────────────────────
self.preview_table = QTableView()
self.preview_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.preview_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.preview_table.setSelectionMode(QAbstractItemView.SingleSelection)
self.preview_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.preview_table.verticalHeader().setVisible(False)
self.preview_table.setMinimumHeight(220)
self.preview_status_label = QLabel("等待加载数据...")
self.preview_status_label.setStyleSheet(f"color: {ModernStylesheet.COLORS['text_secondary']}; font-size: 12px;")
preview_layout.addLayout(controls_layout)
preview_layout.addWidget(self.preview_table)
preview_layout.addWidget(self.preview_status_label)
preview_group.setLayout(preview_layout)
main_layout.addWidget(preview_group)
# ==========================================
# 卡片 3:输出与执行
# ==========================================
output_group = QGroupBox("🚀 输出与执行")
output_layout = QVBoxLayout()
output_layout.setSpacing(16)
output_layout.setContentsMargins(20, 24, 20, 20)
self.output_file = FileSelectWidget(
"结果保存至:",
"CSV Files (*.csv);;All Files (*.*)",
mode="save"
)
self.output_file.label.setMinimumWidth(100)
self.output_file.line_edit.setPlaceholderText("processed_data.csv")
output_layout.addWidget(self.output_file)
action_layout = QHBoxLayout()
action_layout.addStretch()
self.run_btn = QPushButton("独立运行步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('primary'))
self.run_btn.setMinimumWidth(140)
self.run_btn.clicked.connect(self._on_run_single_clicked)
action_layout.addWidget(self.run_btn)
output_layout.addLayout(action_layout)
output_group.setLayout(output_layout)
main_layout.addWidget(output_group)
main_layout.addStretch()
self.setLayout(main_layout)
self.reset_preview()
def _add_row_with_fixed_label(self, layout, label_text, widget):
"""辅助方法:创建绝对锁死宽度的对齐行 (保留原始代码结构) - 已废弃,保留仅用于兼容"""
lbl = QLabel(label_text)
lbl.setMinimumWidth(100)
layout.addWidget(lbl)
if hasattr(widget, 'setSizePolicy'):
widget.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
layout.addWidget(widget)
def get_config(self):
"""获取配置"""
config = {
'csv_path': self.csv_file.get_path(),
}
output_path = self.output_file.get_path()
if output_path:
config['output_path'] = output_path
return config
def set_config(self, config):
"""设置配置"""
if 'csv_path' in config:
self.csv_file.set_path(config['csv_path'])
self.load_csv_preview()
if 'output_path' in config:
self.output_file.set_path(config['output_path'])
def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置自动填充输出路径"""
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
pass
else:
self.work_dir = None
if self.work_dir and not self.output_file.get_path():
output_dir = resolve_subdir(self.work_dir, 'data_cleaning')
default_output_path = os.path.join(output_dir, "processed_data.csv").replace('\\', '/')
self.output_file.set_path(default_output_path)
elif not self.work_dir:
self.output_file.set_path("")
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求"""
from src.gui.core.event_bus import global_event_bus
csv_path = self.csv_file.get_path()
if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择需要清洗的 CSV 数据文件!")
return
config = {'step5_clean': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step5_clean',
'config': config,
})
def reset_preview(self, message="请选择 CSV 文件并点击刷新预览"):
"""重置预览表格"""
try:
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:
self.preview_status_label.setText("数据预览模块加载失败")
def load_csv_preview(self):
"""加载 CSV 预览数据"""
csv_path = self.csv_file.get_path()
if not csv_path:
self.reset_preview("请先选择 CSV 文件")
return
if not os.path.exists(csv_path):
self.reset_preview("文件不存在,请检查路径")
return
try:
from src.gui.components.data_models import PandasTableModel
except ImportError:
self.reset_preview("数据预览模块加载失败")
return
try:
rows_to_preview = max(1, self.preview_rows_spin.value())
df = pd.read_csv(csv_path, nrows=rows_to_preview, dtype=object)
df = df.fillna('')
if df.empty:
self.reset_preview("CSV文件为空")
return
model = PandasTableModel(df)
self.preview_table.setModel(model)
self.preview_status_label.setText(
f"预览前 {len(df)} 行,共 {len(df.columns)} 列"
)
except Exception as exc:
self.reset_preview(f"加载数据失败: {exc}")