From 6e4f0f552766f274e5d90ee94ca7bcd3e7863294 Mon Sep 17 00:00:00 2001 From: DXC Date: Fri, 26 Jun 2026 13:24:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AD=A5=E9=AA=A4=E4=BA=8C=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/panels/step2_panel.py | 155 ++++++++++++++++++---------------- src/gui/styles.py | 30 +++++-- 2 files changed, 108 insertions(+), 77 deletions(-) diff --git a/src/gui/panels/step2_panel.py b/src/gui/panels/step2_panel.py index 6c54714..92c819e 100644 --- a/src/gui/panels/step2_panel.py +++ b/src/gui/panels/step2_panel.py @@ -1,23 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Step2 面板 - 耀斑区域识别 +Step2 面板 - 耀斑区域识别 (完美对齐卡片化重构) """ import os import sys from pathlib import Path -# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) +# 路径归一化 helper _HERE = os.path.dirname(os.path.abspath(__file__)) if _HERE not in sys.path: sys.path.insert(0, _HERE) from _step_path_resolver import resolve_subdir from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QGroupBox, QFormLayout, + QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, QDoubleSpinBox, QSpinBox, QComboBox, QCheckBox, QPushButton, - QMessageBox, + QMessageBox, QLabel ) from PyQt5.QtCore import Qt @@ -28,41 +28,62 @@ from src.gui.styles import ModernStylesheet class Step2Panel(QWidget): """2. 耀斑区域识别""" + def __init__(self, parent=None): super().__init__(parent) self.work_dir = None self.init_ui() def init_ui(self): - layout = QVBoxLayout() + # 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) - # 影像文件 self.img_file = FileSelectWidget( - "影像文件:", + "原始影像:", "Image Files (*.bsq *.dat *.tif);;All Files (*.*)" ) - layout.addWidget(self.img_file) + self.img_file.label.setMinimumWidth(100) # 锁死起跑线 - # 水域掩膜文件(可选,用于独立运行) self.water_mask_file = FileSelectWidget( "水域掩膜:", "Mask Files (*.dat *.tif);;All Files (*.*)" ) - self.water_mask_file.label.setText("水域掩膜:") - layout.addWidget(self.water_mask_file) + self.water_mask_file.label.setMinimumWidth(100) - # 参数设置 - params_group = QGroupBox("检测参数") + input_layout.addWidget(self.img_file) + input_layout.addWidget(self.water_mask_file) + input_group.setLayout(input_layout) + main_layout.addWidget(input_group) + + # ========================================== + # 卡片 2:核心检测参数 + # ========================================== + params_group = QGroupBox("⚙️ 检测参数") params_layout = QFormLayout() + params_layout.setSpacing(16) + params_layout.setContentsMargins(20, 24, 20, 20) # 耀斑波长 self.glint_wave = QDoubleSpinBox() self.glint_wave.setRange(300, 1000) self.glint_wave.setValue(750.0) self.glint_wave.setSuffix(" nm") + self.glint_wave.setMinimumWidth(120) + # 用 Python 原生 API 彻底没收自带的小箭头,配合全局 styles.py 杜绝重影残影 + self.glint_wave.setButtonSymbols(QDoubleSpinBox.NoButtons) params_layout.addRow("耀斑检测波长:", self.glint_wave) # 检测方法 @@ -73,63 +94,85 @@ class Step2Panel(QWidget): self.method.addItem("IQR 四分位距法", "iqr") self.method.addItem("自适应阈值法", "adaptive") self.method.addItem("多波段综合法", "multi_band") - params_layout.addRow("检测方法:", self.method) + self.method.setMinimumWidth(120) + params_layout.addRow("检测方法选择:", self.method) # 最大连通域面积 self.max_area = QSpinBox() self.max_area.setRange(0, 100000) self.max_area.setValue(50) - self.max_area.setSpecialValueText("不过滤") - params_layout.addRow("最大连通域面积:", self.max_area) + self.max_area.setSuffix(" px") + self.max_area.setSpecialValueText("不过滤面积") + self.max_area.setButtonSymbols(QSpinBox.NoButtons) + self.max_area.setMinimumWidth(120) + params_layout.addRow("连通域最大面积:", self.max_area) # 岸边缓冲区 self.buffer_size = QSpinBox() self.buffer_size.setRange(0, 200) self.buffer_size.setValue(10) - self.buffer_size.setSpecialValueText("不设置") + self.buffer_size.setSuffix(" 网格") + self.buffer_size.setSpecialValueText("不设置缓冲区") + self.buffer_size.setButtonSymbols(QSpinBox.NoButtons) + self.buffer_size.setMinimumWidth(120) params_layout.addRow("岸边缓冲区大小:", self.buffer_size) params_group.setLayout(params_layout) - layout.addWidget(params_group) + main_layout.addWidget(params_group) + + # ========================================== + # 卡片 3:输出与执行 + # ========================================== + output_group = QGroupBox("🚀 输出与执行") + output_layout = QVBoxLayout() + output_layout.setSpacing(16) + output_layout.setContentsMargins(20, 24, 20, 20) - # 输出文件路径 self.output_file = FileSelectWidget( - "输出耀斑掩膜:", - "Mask Files (*.dat *.tif);;All Files (*.*)" + "结果保存至:", + "Mask Files (*.dat *.tif);;All Files (*.*)", + mode="save" ) - self.output_file.line_edit.setPlaceholderText("") - layout.addWidget(self.output_file) + self.output_file.label.setMinimumWidth(100) + output_layout.addWidget(self.output_file) - # 启用步骤 + # 底部标准按钮栏 + action_layout = QHBoxLayout() self.enable_checkbox = QCheckBox("启用此步骤") self.enable_checkbox.setChecked(True) - layout.addWidget(self.enable_checkbox) + action_layout.addWidget(self.enable_checkbox) - # 独立运行按钮 - self.run_btn = QPushButton("独立运行此步骤") - self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success')) + 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) - layout.addWidget(self.run_btn) + 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) - layout.addStretch() - self.setLayout(layout) - # 信号连接:影像文件路径变化时动态更新波段范围 def get_config(self): """获取配置""" config = { 'img_path': self.img_file.get_path(), 'glint_wave': self.glint_wave.value(), - 'method': self.method.currentData(), # 使用 currentData() 获取英文ID + 'method': self.method.currentData(), } if self.max_area.value() > 0: config['max_area'] = self.max_area.value() if self.buffer_size.value() > 0: config['buffer_size'] = self.buffer_size.value() - # 添加水域掩膜路径(用于独立运行) + water_mask_path = self.water_mask_file.get_path() if water_mask_path: config['water_mask_path'] = water_mask_path - # 添加输出路径 + output_path = self.output_file.get_path() if output_path: config['output_path'] = output_path @@ -142,7 +185,7 @@ class Step2Panel(QWidget): if 'glint_wave' in config: self.glint_wave.setValue(config['glint_wave']) if 'method' in config: - idx = self.method.findData(config['method']) # 使用 findData() + idx = self.method.findData(config['method']) if idx >= 0: self.method.setCurrentIndex(idx) if 'max_area' in config: @@ -155,56 +198,40 @@ class Step2Panel(QWidget): self.output_file.set_path(config['output_path']) def update_from_config(self, work_dir=None, pipeline=None): - """ - 从全局配置/Pipeline 或 Step1Panel 自动填充路径,实现上下游数据流转 - - Args: - work_dir: 工作目录路径 - pipeline: Pipeline 实例,用于获取步骤1生成的水域掩膜路径 - """ - # 保存工作目录引用 + """从全局配置/Pipeline 或 Step1Panel 自动填充路径,实现上下游数据流转""" if work_dir: self.work_dir = work_dir elif hasattr(self, 'work_dir') and self.work_dir: - pass # 保持现有工作目录 + pass else: self.work_dir = None - # 1. 尝试从 Pipeline 获取 mask_path = None if pipeline and hasattr(pipeline, 'water_mask_path') and pipeline.water_mask_path: mask_path = pipeline.water_mask_path - # 2. 如果 Pipeline 中没有,则尝试直接从 Step1 界面读取(关键修复) main_window = self.window() if not mask_path and hasattr(main_window, 'step1_panel'): if main_window.step1_panel.use_ndwi_radio.isChecked(): - # NDWI模式,读取输出框的路径 mask_path = main_window.step1_panel.output_file.get_path() else: - # 导入现有模式,读取输入框的路径 mask_path = main_window.step1_panel.mask_file.get_path() - # 填充获取到的路径 if mask_path: - # 若为相对路径,使用 work_dir 合成为绝对路径 if not os.path.isabs(mask_path): mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/') self.water_mask_file.set_path(mask_path) - # 3. 自动填充输出路径(基于工作目录) if self.work_dir: - # 生成输出耀斑掩膜的标准路径:workspace/2_Glint_Detection/severe_glint_area.dat output_dir = resolve_subdir(self.work_dir, 'glint_detection') os.makedirs(output_dir, exist_ok=True) default_output_path = os.path.join(output_dir, "severe_glint_area.dat").replace('\\', '/') self.output_file.set_path(default_output_path) else: - # 没有工作目录时,清空输出路径 self.output_file.set_path("") def _on_run_single_clicked(self): - """通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor)。""" + """通过 EventBus 发布单步执行请求""" from src.gui.core.event_bus import global_event_bus img_path = self.img_file.get_path() @@ -216,18 +243,4 @@ class Step2Panel(QWidget): global_event_bus.publish('RequestRunSingleStep', { 'step_name': 'step2', 'config': config, - }) - - def run_step(self): - """独立运行步骤2(旧版 parent 链上溯方式,保留兼容)。""" - # 验证输入 - img_path = self.img_file.get_path() - if not img_path: - QMessageBox.warning(self, "输入错误", "请选择影像文件!") - return - - # 获取主窗口并运行步骤 - main_window = self.window() - if hasattr(main_window, 'run_single_step'): - config = {'step2': self.get_config()} - main_window.run_single_step('step2', config) \ No newline at end of file + }) \ No newline at end of file diff --git a/src/gui/styles.py b/src/gui/styles.py index b99f64b..67a294c 100644 --- a/src/gui/styles.py +++ b/src/gui/styles.py @@ -135,18 +135,36 @@ class ModernStylesheet: }} QComboBox:hover {{ border: 1px solid {cls.COLORS['text_secondary']}; }} QComboBox:focus {{ border: 1.5px solid {cls.COLORS['border_focus']}; }} + /* 扩大下拉按钮的点击热区并增加左侧分割线 */ QComboBox::drop-down {{ subcontrol-origin: padding; subcontrol-position: top right; - width: 20px; - border-left: none; + width: 32px; /* 把右侧热区加宽到 32px,更容易点中 */ + border-left: 1px solid {cls.COLORS['border_light']}; /* 加一条优雅的分割线 */ + background-color: transparent; }} + + /* 鼠标悬停在下拉框上时,右边的小区域背景微变,提供点击暗示 */ + QComboBox:hover::drop-down {{ + background-color: {cls.COLORS['hover']}; + border-top-right-radius: {cls.VARS['radius_md']}; + border-bottom-right-radius: {cls.VARS['radius_md']}; + }} + + /* 纯 CSS 绘制的现代大号倒三角箭头 */ QComboBox::down-arrow {{ image: none; - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-top: 5px solid {cls.COLORS['text_secondary']}; - margin-right: 8px; + width: 0; + height: 0; + border-left: 6px solid transparent; /* 宽度加大到 6px */ + border-right: 6px solid transparent; + border-top: 7px solid {cls.COLORS['text_secondary']}; /* 高度加大到 7px,颜色使用次级文本色 */ + margin-top: 2px; /* 居中微调 */ + }} + + /* 下拉展开时的箭头颜色加深变蓝,反馈更强烈 */ + QComboBox::down-arrow:on {{ + border-top: 7px solid {cls.COLORS['primary']}; }} QCheckBox {{ spacing: 8px; }}