# -*- 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())