Files
WQ_GUI/src/new/views/step1_view.py

284 lines
11 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.

# -*- coding: utf-8 -*-
"""
Step1View —— Step 1水域掩膜生成的前端视图端到端模块化
设计原则
========
1. **UI 完全移植**:控件、布局、单选样式、信号槽、文件过滤器、阈值参数
全部从 ``src/gui/panels/step1_panel.py`` 原样搬迁,业务行为保持一致。
2. **零业务逻辑**:本文件 **绝不** import 任何 ``src/core/``、``src/services/``
算法模块;**绝不** 调用 Pipeline唯一跨层出口是底部按钮的
``self.dispatch_execute("step1", self.get_config())``。
3. **契约对齐**:继承 ``BaseView``;实现 ``init_ui / get_config / set_config``
通过 ``update_work_directory`` 在 NDWI 模式下自动填充输出路径。
调用链(自顶向下)::
Step1View._on_run_clicked
→ BaseView.dispatch_execute("step1", config)
→ MainView.run_single_step(step_id, config)
→ TaskWorker → services.step1_service.execute_step1(config)
→ 返回结果 dict → MainView 日志区
"""
import os
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QCheckBox, QDoubleSpinBox, QGroupBox, QHBoxLayout,
QLabel, QPushButton, QRadioButton, QVBoxLayout,
)
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
from src.new.core.base_view import BaseView
# QRadioButton 统一样式(实心选中点)——与旧 panel 完全一致
_RADIO_STYLE = """
QRadioButton::indicator {
width: 16px;
height: 16px;
border-radius: 8px;
border: 2px solid #999;
}
QRadioButton::indicator:checked {
background-color: #0078D7;
border: 2px solid #0078D7;
}
QRadioButton::indicator:unchecked {
background-color: white;
border: 2px solid #999;
}
QRadioButton::indicator:hover {
border: 2px solid #0078D7;
}
"""
class Step1View(BaseView):
"""Step 1: 水域掩膜生成 —— 纯 View所有跨层通讯走 dispatch_execute"""
# ------------------------------------------------------------------
# UI 构建(与旧 step1_panel.init_ui 字节级一致,仅按钮文案与回调不同)
# ------------------------------------------------------------------
def init_ui(self):
layout = QVBoxLayout()
# ---------- 掩膜生成方式 ----------
method_group = QGroupBox("掩膜生成方式")
method_layout = QVBoxLayout()
self.use_existing_radio = QRadioButton("使用现有掩膜文件")
self.use_existing_radio.setChecked(True)
method_layout.addWidget(self.use_existing_radio)
self.use_ndwi_radio = QRadioButton("使用NDWI自动生成")
method_layout.addWidget(self.use_ndwi_radio)
self.use_existing_radio.setStyleSheet(_RADIO_STYLE)
self.use_ndwi_radio.setStyleSheet(_RADIO_STYLE)
method_group.setLayout(method_layout)
layout.addWidget(method_group)
# ---------- 掩膜文件 ----------
self.mask_file = FileSelectWidget(
"掩膜文件:",
"Shapefiles (*.shp);;Raster Files (*.dat *.tif);;All Files (*.*)"
)
layout.addWidget(self.mask_file)
# ---------- 参考影像 ----------
self.img_file = FileSelectWidget(
"参考影像:",
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
)
layout.addWidget(self.img_file)
# ---------- NDWI 参数 ----------
self.ndwi_group = QGroupBox("NDWI参数设置")
ndwi_layout = QVBoxLayout()
threshold_layout = QHBoxLayout()
threshold_layout.addWidget(QLabel("NDWI阈值:"))
self.ndwi_threshold = QDoubleSpinBox()
self.ndwi_threshold.setRange(0.0, 1.0)
self.ndwi_threshold.setSingleStep(0.05)
self.ndwi_threshold.setValue(0.4)
self.ndwi_threshold.setDecimals(2)
threshold_layout.addWidget(self.ndwi_threshold)
threshold_layout.addStretch()
ndwi_layout.addLayout(threshold_layout)
self.ndwi_group.setLayout(ndwi_layout)
layout.addWidget(self.ndwi_group)
# ---------- 输出掩膜路径save 模式) ----------
self.output_file = FileSelectWidget(
"输出掩膜:",
"Mask Files (*.dat *.tif);;All Files (*.*)",
mode="save",
)
self.output_file.line_edit.setPlaceholderText("water_mask.dat")
layout.addWidget(self.output_file)
# ---------- 提示信息Info Alert 样式) ----------
hint = QLabel(
"💡 提示: 如果掩膜文件是Shapefile(.shp),需要提供参考影像用于栅格化;"
"如果使用NDWI自动生成只需要提供参考影像"
)
hint.setWordWrap(True)
hint.setStyleSheet("""
QLabel {
color: #0055D4;
font-size: 13px;
font-weight: bold;
background-color: #E8F4FF;
border: 2px solid #0055D4;
border-radius: 8px;
padding: 12px 16px;
margin: 8px 0px;
}
""")
layout.addWidget(hint)
# ---------- 启用步骤 ----------
self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(True)
layout.addWidget(self.enable_checkbox)
# ---------- 执行按钮(绿色 success 样式,仅作视图层入口) ----------
self.run_btn = QPushButton("执行 Step 1: 水域掩膜")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
self.run_btn.clicked.connect(self._on_run_clicked)
layout.addWidget(self.run_btn)
# ---------- 信号联动 ----------
self.use_existing_radio.toggled.connect(self.update_ui_state)
self.use_ndwi_radio.toggled.connect(self.update_ui_state)
layout.addStretch()
self.setLayout(layout)
# 初始 UI 状态
self.update_ui_state()
# ------------------------------------------------------------------
# UI 状态联动(与旧 panel.update_ui_state 同语义)
# ------------------------------------------------------------------
def update_ui_state(self):
"""根据单选状态显示/隐藏 NDWI 参数与输出路径"""
use_ndwi = self.use_ndwi_radio.isChecked()
if use_ndwi:
self.mask_file.setVisible(False)
self.ndwi_group.setVisible(True)
self.output_file.setVisible(True)
# 主窗口已推送过 work_dir 才填,避免 init_ui 阶段空指针
if self.work_dir:
self._auto_fill_output_path()
else:
self.mask_file.setVisible(True)
self.ndwi_group.setVisible(False)
self.output_file.setVisible(False)
# 参考影像在两种模式下都显示
self.img_file.setVisible(True)
# ------------------------------------------------------------------
# 工作目录推送——主窗口变更 work_dir 时调用
# ------------------------------------------------------------------
def update_work_directory(self, work_dir):
"""主窗口推送工作目录变更NDWI 模式下自动填充输出路径
重写基类以叠加"自动填路径"行为,再 super 缓存保持一致。
"""
super().update_work_directory(work_dir)
if work_dir and self.use_ndwi_radio.isChecked():
self._auto_fill_output_path()
def _auto_fill_output_path(self):
"""NDWI 模式下根据 work_dir 推导默认输出路径(统一正斜杠)
view 层内联的轻量推导——不依赖旧 ``_step_path_resolver``
output_dir = work_dir/1_water_mask/
default_output = output_dir/water_mask_out.dat
"""
if not self.work_dir:
return
output_dir = os.path.join(self.work_dir, "1_water_mask")
os.makedirs(output_dir, exist_ok=True)
default_output_path = os.path.join(output_dir, "water_mask_out.dat").replace("\\", "/")
self.output_file.set_path(default_output_path)
# ------------------------------------------------------------------
# BaseView 契约get_config / set_config
# ------------------------------------------------------------------
def get_config(self) -> dict:
"""读取当前 UI 状态——返回干净字典供 dispatch_execute 携带
字段:
mask_path : str | None 现有掩膜文件路径NDWI 模式下为 None
use_ndwi : bool 是否走 NDWI 自动生成
ndwi_threshold : float NDWI 阈值
img_path : str 参考影像路径(非空才包含)
output_path : str | None NDWI 模式下的输出路径;现有掩膜模式下为 None
"""
use_ndwi = self.use_ndwi_radio.isChecked()
config = {
"mask_path": None if use_ndwi else self.mask_file.get_path(),
"use_ndwi": use_ndwi,
"ndwi_threshold": self.ndwi_threshold.value(),
}
# 参考影像:非空才写入,避免下游合并时被空字符串覆盖
img_path = self.img_file.get_path()
if img_path:
config["img_path"] = img_path
if use_ndwi:
output_path = self.output_file.get_path()
if output_path:
config["output_path"] = output_path
else:
# 现有掩膜模式下不传递 output_path避免底层错误尝试保存文件
config["output_path"] = None
return config
def set_config(self, config: dict):
"""根据 config 恢复 UI 状态——None / 空值字段一律跳过"""
if config.get("mask_path"):
self.mask_file.set_path(config["mask_path"])
if config.get("img_path"):
self.img_file.set_path(config["img_path"])
if config.get("output_path"):
self.output_file.set_path(config["output_path"])
if "use_ndwi" in config:
if config["use_ndwi"]:
self.use_ndwi_radio.setChecked(True)
else:
self.use_existing_radio.setChecked(True)
if "ndwi_threshold" in config:
self.ndwi_threshold.setValue(config["ndwi_threshold"])
self.update_ui_state()
# ------------------------------------------------------------------
# 执行入口:仅触发 dispatch_execute——无任何业务校验/算法调用
# ------------------------------------------------------------------
def _on_run_clicked(self):
"""绿色按钮:纯 View 层入口,把当前 UI 配置打包交给主窗口
所有跨层通讯一律走 ``dispatch_execute``
Step1View → 沿父链上溯 → MainView.run_single_step(step_id, config)
"""
self.dispatch_execute("step1", self.get_config())