From 7293ddd5ee087b55649ecbbd6b01931907c0b8b0 Mon Sep 17 00:00:00 2001 From: DXC Date: Fri, 26 Jun 2026 13:51:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AD=A5=E9=AA=A4=E5=9B=9B=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/step1_panel.py | 25 +- src/gui/panels/step2_panel.py | 35 +-- src/gui/panels/step3_panel.py | 401 ++++++++++++------------- src/gui/panels/step4_sampling_panel.py | 31 +- src/gui/styles.py | 283 ++++++++--------- 5 files changed, 350 insertions(+), 425 deletions(-) diff --git a/src/gui/panels/step1_panel.py b/src/gui/panels/step1_panel.py index a342a6c..ebeaddb 100644 --- a/src/gui/panels/step1_panel.py +++ b/src/gui/panels/step1_panel.py @@ -1,14 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Step1 面板 - 水域掩膜生成 (完美对齐无跳动重构) +Step1 面板 - 水域掩膜生成 (已移除“启用此步骤”) """ import os import sys from pathlib import Path -# 路径归一化 helper _HERE = os.path.dirname(os.path.abspath(__file__)) if _HERE not in sys.path: sys.path.insert(0, _HERE) @@ -16,7 +15,7 @@ from _step_path_resolver import resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel, - QCheckBox, QPushButton, QRadioButton, QLineEdit, + QPushButton, QRadioButton, QLineEdit, QMessageBox, QStackedWidget, QSizePolicy ) from PyQt5.QtCore import Qt @@ -27,8 +26,6 @@ from src.gui.styles import ModernStylesheet class Step1Panel(QWidget): - """1. 水域掩膜生成""" - def __init__(self, parent=None): super().__init__(parent) self.init_ui() @@ -40,9 +37,6 @@ class Step1Panel(QWidget): main_layout.setContentsMargins(24, 24, 24, 24) main_layout.setSpacing(20) - # ========================================== - # 卡片 1:掩膜生成策略 - # ========================================== method_group = QGroupBox("📁 掩膜生成策略") method_layout = QHBoxLayout() method_layout.setSpacing(24) @@ -50,7 +44,6 @@ class Step1Panel(QWidget): self.use_existing_radio = QRadioButton("使用现有掩膜文件") self.use_existing_radio.setChecked(True) - # 【核心修复】:保留原生圆圈,但直接强制给予足够大(200x35)的安全盒模型,杜绝高DPI下任何形式的截断! self.use_existing_radio.setMinimumSize(200, 35) self.use_ndwi_radio = QRadioButton("使用 NDWI 自动生成") @@ -62,9 +55,6 @@ class Step1Panel(QWidget): method_group.setLayout(method_layout) main_layout.addWidget(method_group) - # ========================================== - # 卡片 2:文件与参数配置 (绝对像素级对齐) - # ========================================== config_group = QGroupBox("⚙️ 文件与参数配置") config_layout = QVBoxLayout() config_layout.setSpacing(16) @@ -87,7 +77,6 @@ class Step1Panel(QWidget): self.stack = QStackedWidget() - # 第 0 页:现有掩膜文件 self.mask_file = FileSelectWidget( "现有掩膜文件:", "Shapefiles (*.shp);;Raster Files (*.dat *.tif);;All Files (*.*)" @@ -95,7 +84,6 @@ class Step1Panel(QWidget): self.mask_file.label.setMinimumWidth(100) self.stack.addWidget(self.mask_file) - # 第 1 页:NDWI 阈值参数 self.ndwi_widget = QWidget() ndwi_layout = QHBoxLayout(self.ndwi_widget) ndwi_layout.setContentsMargins(0, 0, 0, 0) @@ -116,7 +104,6 @@ class Step1Panel(QWidget): self.stack.addWidget(self.ndwi_widget) config_layout.addWidget(self.stack) - # 参考影像 self.img_file = FileSelectWidget( "输入参考影像:", "Image Files (*.bsq *.dat *.tif);;All Files (*.*)" @@ -127,9 +114,6 @@ class Step1Panel(QWidget): config_group.setLayout(config_layout) main_layout.addWidget(config_group) - # ========================================== - # 卡片 3:输出与执行 - # ========================================== output_group = QGroupBox("🚀 输出与执行") output_layout = QVBoxLayout() output_layout.setSpacing(16) @@ -144,11 +128,8 @@ class Step1Panel(QWidget): self.output_file.line_edit.setPlaceholderText("water_mask.dat") output_layout.addWidget(self.output_file) + # 完美对齐的底部按钮栏(已移除多余的启用步骤选项) action_layout = QHBoxLayout() - self.enable_checkbox = QCheckBox("启用此步骤") - self.enable_checkbox.setChecked(True) - self.enable_checkbox.setMinimumHeight(24) - action_layout.addWidget(self.enable_checkbox) action_layout.addStretch() self.run_btn = QPushButton("独立运行步骤") diff --git a/src/gui/panels/step2_panel.py b/src/gui/panels/step2_panel.py index 92c819e..7cdd4f4 100644 --- a/src/gui/panels/step2_panel.py +++ b/src/gui/panels/step2_panel.py @@ -1,14 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Step2 面板 - 耀斑区域识别 (完美对齐卡片化重构) +Step2 面板 - 耀斑区域识别 (已移除“启用此步骤”) """ import os import sys from pathlib import Path -# 路径归一化 helper _HERE = os.path.dirname(os.path.abspath(__file__)) if _HERE not in sys.path: sys.path.insert(0, _HERE) @@ -16,36 +15,28 @@ from _step_path_resolver import resolve_subdir from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, - QDoubleSpinBox, QSpinBox, QComboBox, QCheckBox, QPushButton, + QDoubleSpinBox, QSpinBox, QComboBox, QPushButton, QMessageBox, QLabel ) from PyQt5.QtCore import Qt -# 从公共组件库导入 from src.gui.components.custom_widgets import FileSelectWidget 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): - # 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) @@ -55,7 +46,7 @@ class Step2Panel(QWidget): "原始影像:", "Image Files (*.bsq *.dat *.tif);;All Files (*.*)" ) - self.img_file.label.setMinimumWidth(100) # 锁死起跑线 + self.img_file.label.setMinimumWidth(100) self.water_mask_file = FileSelectWidget( "水域掩膜:", @@ -68,25 +59,19 @@ class Step2Panel(QWidget): 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) - # 检测方法 self.method = QComboBox() self.method.addItem("Otsu 阈值法", "otsu") self.method.addItem("Z-Score 方法", "zscore") @@ -97,7 +82,6 @@ class Step2Panel(QWidget): self.method.setMinimumWidth(120) params_layout.addRow("检测方法选择:", self.method) - # 最大连通域面积 self.max_area = QSpinBox() self.max_area.setRange(0, 100000) self.max_area.setValue(50) @@ -107,7 +91,6 @@ class Step2Panel(QWidget): 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) @@ -120,9 +103,6 @@ class Step2Panel(QWidget): params_group.setLayout(params_layout) main_layout.addWidget(params_group) - # ========================================== - # 卡片 3:输出与执行 - # ========================================== output_group = QGroupBox("🚀 输出与执行") output_layout = QVBoxLayout() output_layout.setSpacing(16) @@ -136,12 +116,7 @@ class Step2Panel(QWidget): self.output_file.label.setMinimumWidth(100) output_layout.addWidget(self.output_file) - # 底部标准按钮栏 action_layout = QHBoxLayout() - self.enable_checkbox = QCheckBox("启用此步骤") - self.enable_checkbox.setChecked(True) - action_layout.addWidget(self.enable_checkbox) - action_layout.addStretch() self.run_btn = QPushButton("独立运行步骤") @@ -158,7 +133,6 @@ class Step2Panel(QWidget): self.setLayout(main_layout) def get_config(self): - """获取配置""" config = { 'img_path': self.img_file.get_path(), 'glint_wave': self.glint_wave.value(), @@ -179,7 +153,6 @@ class Step2Panel(QWidget): return config def set_config(self, config): - """设置配置""" if 'img_path' in config: self.img_file.set_path(config['img_path']) if 'glint_wave' in config: @@ -198,7 +171,6 @@ class Step2Panel(QWidget): self.output_file.set_path(config['output_path']) def update_from_config(self, work_dir=None, pipeline=None): - """从全局配置/Pipeline 或 Step1Panel 自动填充路径,实现上下游数据流转""" if work_dir: self.work_dir = work_dir elif hasattr(self, 'work_dir') and self.work_dir: @@ -231,7 +203,6 @@ class Step2Panel(QWidget): self.output_file.set_path("") def _on_run_single_clicked(self): - """通过 EventBus 发布单步执行请求""" from src.gui.core.event_bus import global_event_bus img_path = self.img_file.get_path() diff --git a/src/gui/panels/step3_panel.py b/src/gui/panels/step3_panel.py index c7bfdd9..ea59ea4 100644 --- a/src/gui/panels/step3_panel.py +++ b/src/gui/panels/step3_panel.py @@ -1,243 +1,273 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Step3 面板 - 耀斑去除 +Step3 面板 - 耀斑去除 (已移除“启用此步骤”) """ import os import sys from pathlib import Path -# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里) _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, - QDoubleSpinBox, QSpinBox, QComboBox, QCheckBox, QPushButton, - QLabel, QLineEdit, QMessageBox, + QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, + QSpinBox, QComboBox, QCheckBox, QPushButton, + QLabel, QLineEdit, QMessageBox, QSizePolicy ) from PyQt5.QtCore import Qt +from PyQt5.QtGui import QDoubleValidator -# 从公共组件库导入 from src.gui.components.custom_widgets import FileSelectWidget from src.gui.styles import ModernStylesheet class Step3Panel(QWidget): - """步骤3:耀斑去除""" def __init__(self, parent=None): super().__init__(parent) self.init_ui() def init_ui(self): - layout = QVBoxLayout() + self.setStyleSheet(ModernStylesheet.get_main_stylesheet()) - # 标题 + main_layout = QVBoxLayout() + main_layout.setContentsMargins(24, 24, 24, 24) + main_layout.setSpacing(20) + 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(120) - # 水域掩膜/边界:完整流程可由步骤1自动生成;独立单步运行时须手动指定 self.water_mask_file = FileSelectWidget( "水域掩膜/边界:", "Mask/Boundary (*.dat *.tif *.shp);;All Files (*.*)" ) - layout.addWidget(self.water_mask_file) + self.water_mask_file.label.setMinimumWidth(120) + step3_mask_hint = QLabel( - "提示:独立运行本步骤时必须选择水域掩膜或边界(与影像同区域的 .dat/.tif 掩膜,或 .shp 矢量)。" + "💡 提示:独立运行本步骤时必须选择水域掩膜或边界(与影像同区域的 .dat/.tif 掩膜,或 .shp 矢量)。" ) step3_mask_hint.setWordWrap(True) - step3_mask_hint.setStyleSheet("color: #666; font-size: 10px;") - layout.addWidget(step3_mask_hint) + step3_mask_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; + }} + """) - # 方法选择 - method_group = QGroupBox("去耀斑方法") - method_layout = QVBoxLayout() + input_layout.addWidget(step3_mask_hint) + input_layout.addWidget(self.img_file) + input_layout.addWidget(self.water_mask_file) + input_group.setLayout(input_layout) + main_layout.addWidget(input_group) + params_group = QGroupBox("⚙️ 去耀斑算法设置") + params_layout = QVBoxLayout() + params_layout.setSpacing(16) + params_layout.setContentsMargins(20, 24, 20, 20) + + method_row = QHBoxLayout() + method_row.setContentsMargins(0, 0, 0, 0) + method_label = QLabel("去耀斑方法:") + method_label.setMinimumWidth(120) self.method = QComboBox() - for text, data in [('Goodman方法', 'goodman'), ('Kutser方法', 'kutser'), - ('Hedley方法', 'hedley'), ('SUGAR算法', 'sugar')]: + for text, data in [('Goodman 方法', 'goodman'), ('Kutser 方法', 'kutser'), + ('Hedley 方法', 'hedley'), ('SUGAR 算法', 'sugar')]: self.method.addItem(text, data) + self.method.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.method.currentIndexChanged.connect(self._on_method_changed) - method_layout.addWidget(self.method) + method_row.addWidget(method_label) + method_row.addWidget(self.method) + params_layout.addLayout(method_row) - method_group.setLayout(method_layout) - layout.addWidget(method_group) - - # Goodman参数组 - self.goodman_group = QGroupBox("Goodman方法参数") - goodman_layout = QFormLayout() + # --- Goodman --- + self.goodman_widget = QWidget() + goodman_layout = QFormLayout(self.goodman_widget) + goodman_layout.setContentsMargins(0, 0, 0, 0) self.nir_lower = QSpinBox() self.nir_lower.setRange(0, 200) self.nir_lower.setValue(65) - goodman_layout.addRow("NIR下波段索引:", self.nir_lower) + self.nir_lower.setButtonSymbols(QSpinBox.NoButtons) + self._add_row_with_fixed_label(goodman_layout, "NIR下波段索引:", self.nir_lower) self.nir_upper = QSpinBox() self.nir_upper.setRange(0, 200) self.nir_upper.setValue(91) - goodman_layout.addRow("NIR上波段索引:", self.nir_upper) + self.nir_upper.setButtonSymbols(QSpinBox.NoButtons) + self._add_row_with_fixed_label(goodman_layout, "NIR上波段索引:", self.nir_upper) - self.goodman_a = QDoubleSpinBox() - self.goodman_a.setDecimals(6) - self.goodman_a.setRange(0, 1) - self.goodman_a.setValue(0.000019) - goodman_layout.addRow("参数A:", self.goodman_a) + self.goodman_a = QLineEdit("0.000019") + self.goodman_a.setValidator(QDoubleValidator(0.0, 1.0, 6, self)) + self._add_row_with_fixed_label(goodman_layout, "参数 A:", self.goodman_a) - self.goodman_b = QDoubleSpinBox() - self.goodman_b.setDecimals(2) - self.goodman_b.setRange(0, 1) - self.goodman_b.setValue(0.1) - goodman_layout.addRow("参数B:", self.goodman_b) + self.goodman_b = QLineEdit("0.10") + self.goodman_b.setValidator(QDoubleValidator(0.0, 1.0, 2, self)) + self._add_row_with_fixed_label(goodman_layout, "参数 B:", self.goodman_b) + params_layout.addWidget(self.goodman_widget) - self.goodman_group.setLayout(goodman_layout) - layout.addWidget(self.goodman_group) - - # Kutser参数组 - self.kutser_group = QGroupBox("Kutser方法参数") - kutser_layout = QFormLayout() + # --- Kutser --- + self.kutser_widget = QWidget() + kutser_layout = QFormLayout(self.kutser_widget) + kutser_layout.setContentsMargins(0, 0, 0, 0) self.oxy_band = QSpinBox() self.oxy_band.setRange(0, 200) self.oxy_band.setValue(38) - kutser_layout.addRow("氧吸收波段索引:", self.oxy_band) + self.oxy_band.setButtonSymbols(QSpinBox.NoButtons) + self._add_row_with_fixed_label(kutser_layout, "氧吸收波段索引:", self.oxy_band) self.lower_oxy = QSpinBox() self.lower_oxy.setRange(0, 200) self.lower_oxy.setValue(36) - kutser_layout.addRow("下氧吸收波段索引:", self.lower_oxy) + self.lower_oxy.setButtonSymbols(QSpinBox.NoButtons) + self._add_row_with_fixed_label(kutser_layout, "下氧吸收波段索引:", self.lower_oxy) self.upper_oxy = QSpinBox() self.upper_oxy.setRange(0, 200) self.upper_oxy.setValue(49) - kutser_layout.addRow("上氧吸收波段索引:", self.upper_oxy) + self.upper_oxy.setButtonSymbols(QSpinBox.NoButtons) + self._add_row_with_fixed_label(kutser_layout, "上氧吸收波段索引:", self.upper_oxy) self.nir_band = QSpinBox() self.nir_band.setRange(0, 200) self.nir_band.setValue(47) - kutser_layout.addRow("NIR波段索引:", self.nir_band) + self.nir_band.setButtonSymbols(QSpinBox.NoButtons) + self._add_row_with_fixed_label(kutser_layout, "NIR波段索引:", self.nir_band) - self.kutser_group.setLayout(kutser_layout) - self.kutser_group.setVisible(False) - layout.addWidget(self.kutser_group) + self.kutser_widget.setVisible(False) + params_layout.addWidget(self.kutser_widget) - # Hedley参数组 - self.hedley_group = QGroupBox("Hedley方法参数") - hedley_layout = QFormLayout() + # --- Hedley --- + self.hedley_widget = QWidget() + hedley_layout = QFormLayout(self.hedley_widget) + hedley_layout.setContentsMargins(0, 0, 0, 0) self.hedley_nir_band = QSpinBox() self.hedley_nir_band.setRange(0, 200) self.hedley_nir_band.setValue(47) - hedley_layout.addRow("NIR波段索引:", self.hedley_nir_band) + self.hedley_nir_band.setButtonSymbols(QSpinBox.NoButtons) + self._add_row_with_fixed_label(hedley_layout, "NIR波段索引:", self.hedley_nir_band) - self.hedley_group.setLayout(hedley_layout) - self.hedley_group.setVisible(False) - layout.addWidget(self.hedley_group) + self.hedley_widget.setVisible(False) + params_layout.addWidget(self.hedley_widget) - # SUGAR参数组 - self.sugar_group = QGroupBox("SUGAR方法参数") - sugar_layout = QFormLayout() + # --- SUGAR --- + self.sugar_widget = QWidget() + sugar_layout = QFormLayout(self.sugar_widget) + sugar_layout.setContentsMargins(0, 0, 0, 0) self.sugar_iter = QSpinBox() self.sugar_iter.setRange(1, 20) self.sugar_iter.setValue(3) - self.sugar_iter.setSpecialValueText("自动") - sugar_layout.addRow("迭代次数:", self.sugar_iter) + self.sugar_iter.setButtonSymbols(QSpinBox.NoButtons) + self._add_row_with_fixed_label(sugar_layout, "迭代次数:", self.sugar_iter) - self.sugar_sigma = QDoubleSpinBox() - self.sugar_sigma.setDecimals(2) - self.sugar_sigma.setRange(0.1, 10) - self.sugar_sigma.setValue(1.0) - sugar_layout.addRow("LoG平滑σ:", self.sugar_sigma) + self.sugar_sigma = QLineEdit("1.00") + self.sugar_sigma.setValidator(QDoubleValidator(0.1, 10.0, 2, self)) + self._add_row_with_fixed_label(sugar_layout, "LoG 平滑 σ:", self.sugar_sigma) self.sugar_estimate_background = QCheckBox() self.sugar_estimate_background.setChecked(True) - sugar_layout.addRow("估计背景光谱:", self.sugar_estimate_background) + self._add_row_with_fixed_label(sugar_layout, "估计背景光谱:", self.sugar_estimate_background) self.sugar_glint_mask_method = QComboBox() self.sugar_glint_mask_method.addItems(['cdf', 'otsu']) self.sugar_glint_mask_method.setCurrentText('cdf') - sugar_layout.addRow("耀斑掩膜方法:", self.sugar_glint_mask_method) + self.sugar_glint_mask_method.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self._add_row_with_fixed_label(sugar_layout, "耀斑掩膜方法:", self.sugar_glint_mask_method) - self.sugar_termination_thresh = QDoubleSpinBox() - self.sugar_termination_thresh.setDecimals(2) - self.sugar_termination_thresh.setRange(1, 100) - self.sugar_termination_thresh.setValue(20.0) - sugar_layout.addRow("终止阈值:", self.sugar_termination_thresh) + self.sugar_termination_thresh = QLineEdit("20.00") + self.sugar_termination_thresh.setValidator(QDoubleValidator(1.0, 100.0, 2, self)) + self._add_row_with_fixed_label(sugar_layout, "终止阈值:", self.sugar_termination_thresh) - self.sugar_bounds = QLineEdit() - self.sugar_bounds.setText("[(1, 2)]") - sugar_layout.addRow("优化边界:", self.sugar_bounds) + self.sugar_bounds = QLineEdit("[(1, 2)]") + self._add_row_with_fixed_label(sugar_layout, "优化边界:", self.sugar_bounds) - self.sugar_group.setLayout(sugar_layout) - self.sugar_group.setVisible(False) - layout.addWidget(self.sugar_group) + self.sugar_widget.setVisible(False) + params_layout.addWidget(self.sugar_widget) - # 插值选项 - interp_group = QGroupBox("0值像素插值") - interp_layout = QFormLayout() - - self.interpolate_zeros = QCheckBox("启用插值") - interp_layout.addRow("", self.interpolate_zeros) + # --- 通用参数 --- + interp_row = QHBoxLayout() + interp_row.setContentsMargins(0, 8, 0, 0) + self.interpolate_zeros = QCheckBox("启用 0 值像素插值") + self.interpolate_zeros.setMinimumWidth(120) self.interp_method = QComboBox() for text, data in [('最近邻插值', 'nearest'), ('双线性插值', 'bilinear'), ('样条插值', 'spline'), ('克里金插值', 'kriging')]: self.interp_method.addItem(text, data) - self.interp_method.setCurrentIndex(1) # 默认双线性插值 - interp_layout.addRow("插值方法:", self.interp_method) + self.interp_method.setCurrentIndex(1) + self.interp_method.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - interp_group.setLayout(interp_layout) - layout.addWidget(interp_group) + interp_row.addWidget(self.interpolate_zeros) + interp_row.addWidget(self.interp_method) + params_layout.addLayout(interp_row) - # # 实测经纬度参考点 - # self.ref_csv_file = FileSelectWidget( - # "实测经纬度CSV:", - # "CSV Files (*.csv);;All Files (*.*)" - # ) - # self.ref_csv_file.line_edit.setPlaceholderText("可选:包含 Lon/Lat 列的 CSV 文件") - # layout.addWidget(self.ref_csv_file) + params_group.setLayout(params_layout) + main_layout.addWidget(params_group) - # 交互式预览按钮 - # self.preview_btn = QPushButton("👁️ 打开交互式影像预览") - # self.preview_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('info')) - # self.preview_btn.clicked.connect(self.open_interactive_viewer) - # layout.addWidget(self.preview_btn) + output_group = QGroupBox("🚀 输出与执行") + output_layout = QVBoxLayout() + output_layout.setSpacing(16) + output_layout.setContentsMargins(20, 24, 20, 20) - # 输出文件路径 self.output_file = FileSelectWidget( - "输出影像:", - "Image Files (*.bsq *.dat *.tif);;All Files (*.*)" + "结果保存至:", + "Image Files (*.bsq *.dat *.tif);;All Files (*.*)", + mode="save" ) + self.output_file.label.setMinimumWidth(120) self.output_file.line_edit.setPlaceholderText("deglint_image.bsq") - layout.addWidget(self.output_file) + output_layout.addWidget(self.output_file) - # 启用步骤 - self.enable_checkbox = QCheckBox("启用此步骤") - self.enable_checkbox.setChecked(True) - layout.addWidget(self.enable_checkbox) + action_layout = QHBoxLayout() + action_layout.addStretch() - # 独立运行按钮 - self.run_btn = QPushButton("独立运行此步骤") - self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success')) + 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) - # 信号连接:影像文件路径变化时动态更新波段范围 self.img_file.line_edit.textChanged.connect(self._update_band_ranges) + def _add_row_with_fixed_label(self, form_layout, label_text, widget): + lbl = QLabel(label_text) + lbl.setMinimumWidth(120) + + row_layout = QHBoxLayout() + row_layout.setContentsMargins(0, 0, 0, 0) + row_layout.addWidget(lbl) + + if hasattr(widget, 'setSizePolicy'): + widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + row_layout.addWidget(widget) + form_layout.addRow(row_layout) + def open_interactive_viewer(self): - """打开交互式影像预览""" from src.gui.water_quality_gui import InteractiveViewerDialog img_path = self.img_file.get_path() if not img_path or not os.path.isfile(img_path): @@ -245,19 +275,15 @@ class Step3Panel(QWidget): return water_mask = self.water_mask_file.get_path() - dialog = InteractiveViewerDialog(img_path, self) if water_mask and os.path.isfile(water_mask): dialog.load_water_mask(water_mask) dialog.exec_() def _update_band_ranges(self, file_path): - """根据选择的影像动态限制波段索引的输入范围""" from osgeo import gdal - if not file_path or not os.path.isfile(file_path): return - try: dataset = gdal.Open(file_path) if dataset is None: @@ -274,38 +300,25 @@ class Step3Panel(QWidget): pass def update_from_config(self, work_dir=None, pipeline=None): - """ - 从 Step1Panel 自动填充水域掩膜路径,实现上下游数据流转 - - Args: - work_dir: 工作目录路径 - pipeline: Pipeline 实例(未使用,保留接口兼容性) - """ - # 保存工作目录引用 if work_dir: self.work_dir = work_dir elif hasattr(self, 'work_dir') and self.work_dir: - pass # 保持现有工作目录 + pass else: self.work_dir = None - # 从 Step1 界面读取水域掩膜路径 main_window = self.window() if 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) - # 自动填充输出路径(基于工作目录) if self.work_dir: output_dir = resolve_subdir(self.work_dir, 'deglint') os.makedirs(output_dir, exist_ok=True) @@ -315,36 +328,40 @@ class Step3Panel(QWidget): self.output_file.set_path("") def _on_method_changed(self, index): - """方法改变时更新参数显示""" method_id = self.method.currentData() - self.goodman_group.setVisible(method_id == 'goodman') - self.kutser_group.setVisible(method_id == 'kutser') - self.hedley_group.setVisible(method_id == 'hedley') - self.sugar_group.setVisible(method_id == 'sugar') + self.goodman_widget.setVisible(method_id == 'goodman') + self.kutser_widget.setVisible(method_id == 'kutser') + self.hedley_widget.setVisible(method_id == 'hedley') + self.sugar_widget.setVisible(method_id == 'sugar') + + def _safe_float(self, line_edit, default_val=0.0): + try: + return float(line_edit.text().strip()) + except ValueError: + return default_val def get_config(self): - """获取配置""" config = { 'img_path': self.img_file.get_path(), - 'method': self.method.currentData(), # 使用 currentData() 获取英文ID - 'enabled': self.enable_checkbox.isChecked(), + 'method': self.method.currentData(), 'interpolate_zeros': self.interpolate_zeros.isChecked(), - 'interpolation_method': self.interp_method.currentData(), # 使用 currentData() + 'interpolation_method': self.interp_method.currentData(), } 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 - method = self.method.currentData() # 使用 currentData() + method = self.method.currentData() if method == 'goodman': config['nir_lower'] = self.nir_lower.value() config['nir_upper'] = self.nir_upper.value() - config['goodman_A'] = self.goodman_a.value() - config['goodman_B'] = self.goodman_b.value() + config['goodman_A'] = self._safe_float(self.goodman_a, 0.000019) + config['goodman_B'] = self._safe_float(self.goodman_b, 0.1) elif method == 'kutser': config['oxy_band'] = self.oxy_band.value() @@ -357,53 +374,45 @@ class Step3Panel(QWidget): elif method == 'sugar': config['sugar_iter'] = self.sugar_iter.value() if self.sugar_iter.value() > 0 else None - config['sugar_sigma'] = self.sugar_sigma.value() + config['sugar_sigma'] = self._safe_float(self.sugar_sigma, 1.0) config['sugar_estimate_background'] = self.sugar_estimate_background.isChecked() config['sugar_glint_mask_method'] = self.sugar_glint_mask_method.currentData() - config['sugar_termination_thresh'] = self.sugar_termination_thresh.value() - # 解析bounds字符串 + config['sugar_termination_thresh'] = self._safe_float(self.sugar_termination_thresh, 20.0) try: import ast config['sugar_bounds'] = ast.literal_eval(self.sugar_bounds.text()) except: - config['sugar_bounds'] = [(1, 2)] # 默认值 + config['sugar_bounds'] = [(1, 2)] return config def set_config(self, config): - """设置配置""" if 'img_path' in config: self.img_file.set_path(config['img_path']) if 'water_mask_path' in config: self.water_mask_file.set_path(config['water_mask_path']) if 'output_path' in config: self.output_file.set_path(config['output_path']) - if 'reference_csv' in config: - self.ref_csv_file.set_path(config['reference_csv']) 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 'enabled' in config: - self.enable_checkbox.setChecked(config['enabled']) if 'interpolate_zeros' in config: self.interpolate_zeros.setChecked(config['interpolate_zeros']) if 'interpolation_method' in config: - idx = self.interp_method.findData(config['interpolation_method']) # 使用 findData() + idx = self.interp_method.findData(config['interpolation_method']) if idx >= 0: self.interp_method.setCurrentIndex(idx) - # Goodman参数 if 'nir_lower' in config: self.nir_lower.setValue(config['nir_lower']) if 'nir_upper' in config: self.nir_upper.setValue(config['nir_upper']) if 'goodman_A' in config: - self.goodman_a.setValue(config['goodman_A']) + self.goodman_a.setText(f"{config['goodman_A']:.6f}") if 'goodman_B' in config: - self.goodman_b.setValue(config['goodman_B']) + self.goodman_b.setText(f"{config['goodman_B']:.2f}") - # Kutser参数 if 'oxy_band' in config: self.oxy_band.setValue(config['oxy_band']) if 'lower_oxy' in config: @@ -413,73 +422,45 @@ class Step3Panel(QWidget): if 'nir_band' in config: self.nir_band.setValue(config['nir_band']) - # Hedley参数 if 'hedley_nir_band' in config: self.hedley_nir_band.setValue(config['hedley_nir_band']) - # SUGAR参数 if 'sugar_iter' in config: self.sugar_iter.setValue(config['sugar_iter'] if config['sugar_iter'] is not None else 0) if 'sugar_sigma' in config: - self.sugar_sigma.setValue(config['sugar_sigma']) + self.sugar_sigma.setText(f"{config['sugar_sigma']:.2f}") if 'sugar_estimate_background' in config: self.sugar_estimate_background.setChecked(config['sugar_estimate_background']) if 'sugar_glint_mask_method' in config: - idx = self.sugar_glint_mask_method.findData(config['sugar_glint_mask_method']) # 使用 findData() + idx = self.sugar_glint_mask_method.findData(config['sugar_glint_mask_method']) if idx >= 0: self.sugar_glint_mask_method.setCurrentIndex(idx) if 'sugar_termination_thresh' in config: - self.sugar_termination_thresh.setValue(config['sugar_termination_thresh']) + self.sugar_termination_thresh.setText(f"{config['sugar_termination_thresh']:.2f}") if 'sugar_bounds' in config: self.sugar_bounds.setText(str(config['sugar_bounds'])) def _on_run_single_clicked(self): - """通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor)。""" from src.gui.core.event_bus import global_event_bus img_path = self.img_file.get_path() if not img_path: QMessageBox.warning(self, "输入错误", "请选择影像文件!") return - if self.enable_checkbox.isChecked(): - water_mask_path = self.water_mask_file.get_path() - if not water_mask_path: - QMessageBox.warning( - self, - "输入错误", - "独立运行耀斑去除时,必须选择水域掩膜或边界文件。\n\n" - "请提供与当前影像空间一致的水域栅格掩膜(.dat/.tif),或水域矢量边界(.shp)。\n" - "若刚跑过完整流程,可使用步骤1生成的水域掩膜文件。", - ) - return + + water_mask_path = self.water_mask_file.get_path() + if not water_mask_path: + QMessageBox.warning( + self, + "输入错误", + "独立运行耀斑去除时,必须选择水域掩膜或边界文件。\n\n" + "请提供与当前影像空间一致的水域栅格掩膜(.dat/.tif),或水域矢量边界(.shp)。\n" + "若刚跑过完整流程,可使用步骤1生成的水域掩膜文件。", + ) + return config = {'step3': self.get_config()} global_event_bus.publish('RequestRunSingleStep', { 'step_name': 'step3', 'config': config, - }) - - def run_step(self): - """独立运行步骤3(旧版 parent 链上溯方式,保留兼容)。""" - # 验证输入 - img_path = self.img_file.get_path() - if not img_path: - QMessageBox.warning(self, "输入错误", "请选择影像文件!") - return - if self.enable_checkbox.isChecked(): - water_mask_path = self.water_mask_file.get_path() - if not water_mask_path: - QMessageBox.warning( - self, - "输入错误", - "独立运行耀斑去除时,必须选择水域掩膜或边界文件。\n\n" - "请提供与当前影像空间一致的水域栅格掩膜(.dat/.tif),或水域矢量边界(.shp)。\n" - "若刚跑过完整流程,可使用步骤1生成的水域掩膜文件。", - ) - return - - # 获取主窗口并运行步骤 - main_window = self.window() - if hasattr(main_window, 'run_single_step'): - config = {'step3': self.get_config()} - main_window.run_single_step('step3', config) \ No newline at end of file + }) \ No newline at end of file diff --git a/src/gui/panels/step4_sampling_panel.py b/src/gui/panels/step4_sampling_panel.py index 5ee3b29..69427e1 100644 --- a/src/gui/panels/step4_sampling_panel.py +++ b/src/gui/panels/step4_sampling_panel.py @@ -1,14 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Step4 面板 - 采样点布设 (现代化排版重构) +Step4 面板 - 采样点布设 (已移除“启用此步骤”) """ import os import sys from pathlib import Path -# 路径归一化 helper _HERE = os.path.dirname(os.path.abspath(__file__)) if _HERE not in sys.path: sys.path.insert(0, _HERE) @@ -17,7 +16,7 @@ from _step_path_resolver import resolve_subdir from PyQt5.QtCore import QTimer, Qt from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, - QPushButton, QCheckBox, QSpinBox, QMessageBox, QLabel, QFrame + QPushButton, QSpinBox, QMessageBox, QLabel, QFrame ) from src.gui.components.custom_widgets import FileSelectWidget @@ -26,24 +25,17 @@ from src.gui.styles import ModernStylesheet class Step4SamplingPanel(QWidget): - """步骤4:采样点布设""" - def __init__(self, parent=None): super().__init__(parent) self.init_ui() def init_ui(self): - # 注入全局样式系统 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) @@ -62,9 +54,6 @@ class Step4SamplingPanel(QWidget): input_group.setLayout(input_layout) main_layout.addWidget(input_group) - # ========================================== - # 卡片 2:核心参数设置 - # ========================================== params_group = QGroupBox("⚙️ 采样参数") params_layout = QFormLayout() params_layout.setSpacing(16) @@ -91,6 +80,7 @@ class Step4SamplingPanel(QWidget): self.chunk_size.setMinimumWidth(120) params_layout.addRow("内存处理块大小:", self.chunk_size) + from PyQt5.QtWidgets import QCheckBox self.use_adaptive_sampling = QCheckBox("启用自适应边缘采样") self.use_adaptive_sampling.setChecked(True) params_layout.addRow("智能模式:", self.use_adaptive_sampling) @@ -98,15 +88,11 @@ class Step4SamplingPanel(QWidget): params_group.setLayout(params_layout) 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( "结果保存至:", "CSV Files (*.csv);;All Files (*.*)", @@ -115,14 +101,8 @@ class Step4SamplingPanel(QWidget): self.output_file.line_edit.setPlaceholderText("sampling_spectra.csv") output_layout.addWidget(self.output_file) - # 底部操作栏 (水平布局) action_layout = QHBoxLayout() - - self.enable_checkbox = QCheckBox("启用此步骤") - self.enable_checkbox.setChecked(True) - action_layout.addWidget(self.enable_checkbox) - - action_layout.addStretch() # 把按钮推到右边 + action_layout.addStretch() self.preview_btn = QPushButton("交互式预览采样点") self.preview_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('normal')) @@ -145,15 +125,12 @@ class Step4SamplingPanel(QWidget): main_layout.addStretch() self.setLayout(main_layout) - # 添加心跳定时器 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): config = { 'interval': self.interval.value(), diff --git a/src/gui/styles.py b/src/gui/styles.py index 67a294c..fc9661f 100644 --- a/src/gui/styles.py +++ b/src/gui/styles.py @@ -53,149 +53,166 @@ class ModernStylesheet: def get_main_stylesheet(cls) -> str: """全局基础样式表""" return f""" - /* 全局默认透明背景,防止嵌套组件出现“灰色残影” */ - QWidget {{ - color: {cls.COLORS['text_primary']}; - font-family: {cls.FONTS['family']}; - font-size: 13px; - background-color: transparent; - }} + /* 全局默认透明背景,防止嵌套组件出现“灰色残影” */ + QWidget {{ + color: {cls.COLORS['text_primary']}; + font-family: {cls.FONTS['family']}; + font-size: 13px; + background-color: transparent; + }} - /* 仅对最底层的窗口设置实体浅灰底色 */ - QMainWindow, QDialog {{ - background-color: {cls.COLORS['main_bg']}; - }} + /* 仅对最底层的窗口设置实体浅灰底色 */ + QMainWindow, QDialog {{ + background-color: {cls.COLORS['main_bg']}; + }} - /* 阻断继承:让卡片内的元素保持纯白 */ - QGroupBox, QFrame, QScrollArea, QListWidget, QTreeWidget, QTableWidget {{ - background-color: {cls.COLORS['panel_bg']}; - }} + /* 阻断继承:让卡片内的元素保持纯白 */ + QGroupBox, QFrame, QScrollArea, QListWidget, QTreeWidget, QTableWidget {{ + background-color: {cls.COLORS['panel_bg']}; + }} - QGroupBox {{ - background-color: {cls.COLORS['panel_bg']}; - border: 1px solid {cls.COLORS['border_light']}; - border-radius: {cls.VARS['radius_lg']}; - margin-top: 14px; - padding-top: 16px; - }} - QGroupBox::title {{ - subcontrol-origin: margin; - subcontrol-position: top left; - left: 12px; - top: 0px; - color: {cls.COLORS['primary']}; - font-weight: bold; - font-size: 13px; - background-color: {cls.COLORS['main_bg']}; - padding: 0 4px; - }} + QGroupBox {{ + background-color: {cls.COLORS['panel_bg']}; + border: 1px solid {cls.COLORS['border_light']}; + border-radius: {cls.VARS['radius_lg']}; + margin-top: 14px; + padding-top: 16px; + }} + QGroupBox::title {{ + subcontrol-origin: margin; + subcontrol-position: top left; + left: 12px; + top: 0px; + color: {cls.COLORS['primary']}; + font-weight: bold; + font-size: 13px; + background-color: {cls.COLORS['main_bg']}; + padding: 0 4px; + }} - /* 【核心修复】彻底删除了 QRadioButton 的全部 CSS 代码,让其 100% 保持 Windows 原生样式 */ + /* --- 纯净版:输入框 & 数字框 --- */ + QLineEdit, QSpinBox, QDoubleSpinBox {{ + background-color: {cls.COLORS['panel_bg']}; + border: 1px solid {cls.COLORS['border']}; + border-radius: {cls.VARS['radius_md']}; + padding: 6px 10px; + color: {cls.COLORS['text_primary']}; + selection-background-color: {cls.COLORS['primary']}; + }} + QLineEdit:hover, QSpinBox:hover, QDoubleSpinBox:hover {{ + border: 1px solid {cls.COLORS['text_secondary']}; + }} + QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus {{ + border: 1.5px solid {cls.COLORS['border_focus']}; + background-color: #FFFFFF; + }} + QLineEdit:read-only {{ + background-color: #F8FAFC; + color: {cls.COLORS['text_secondary']}; + border: 1px solid {cls.COLORS['border_light']}; + }} - /* --- 纯净版:输入框 & 数字框 --- */ - QLineEdit, QSpinBox, QDoubleSpinBox {{ - background-color: {cls.COLORS['panel_bg']}; - border: 1px solid {cls.COLORS['border']}; - border-radius: {cls.VARS['radius_md']}; - padding: 6px 10px; - color: {cls.COLORS['text_primary']}; - selection-background-color: {cls.COLORS['primary']}; - }} - QLineEdit:hover, QSpinBox:hover, QDoubleSpinBox:hover {{ - border: 1px solid {cls.COLORS['text_secondary']}; - }} - QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus {{ - border: 1.5px solid {cls.COLORS['border_focus']}; - background-color: #FFFFFF; - }} - QLineEdit:read-only {{ - background-color: #F8FAFC; - color: {cls.COLORS['text_secondary']}; - border: 1px solid {cls.COLORS['border_light']}; - }} + QSpinBox > QLineEdit, QDoubleSpinBox > QLineEdit {{ + background: transparent; + border: none; + padding: 0px; + min-height: 0px; + }} - QSpinBox > QLineEdit, QDoubleSpinBox > QLineEdit {{ - background: transparent; - border: none; - padding: 0px; - min-height: 0px; - }} + QSpinBox::up-button, QSpinBox::down-button, QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {{ + width: 0px; + border: none; + background: transparent; + }} - QSpinBox::up-button, QSpinBox::down-button, QDoubleSpinBox::up-button, QDoubleSpinBox::down-button {{ - width: 0px; - border: none; - background: transparent; - }} + QComboBox {{ + border: 1px solid {cls.COLORS['border']}; + border-radius: {cls.VARS['radius_md']}; + padding: 6px 10px; + background-color: {cls.COLORS['panel_bg']}; + }} + QComboBox:hover {{ border: 1px solid {cls.COLORS['text_secondary']}; }} + QComboBox:focus {{ border: 1.5px solid {cls.COLORS['border_focus']}; }} - QComboBox {{ - border: 1px solid {cls.COLORS['border']}; - border-radius: {cls.VARS['radius_md']}; - padding: 6px 10px; - background-color: {cls.COLORS['panel_bg']}; - }} - 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: 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']}; - }} + QComboBox::drop-down {{ + subcontrol-origin: padding; + subcontrol-position: top right; + width: 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']}; + }} + QComboBox::down-arrow {{ + image: none; + width: 0; + height: 0; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-top: 7px solid {cls.COLORS['text_secondary']}; + margin-top: 2px; + }} + QComboBox::down-arrow:on {{ + border-top: 7px solid {cls.COLORS['primary']}; + }} - /* 纯 CSS 绘制的现代大号倒三角箭头 */ - QComboBox::down-arrow {{ - image: none; - 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; }} - QCheckBox::indicator {{ - width: 16px; - height: 16px; - border-radius: 4px; - border: 1px solid {cls.COLORS['border']}; - background-color: {cls.COLORS['panel_bg']}; - }} - QCheckBox::indicator:hover {{ border: 1px solid {cls.COLORS['primary']}; }} - QCheckBox::indicator:checked {{ - background-color: {cls.COLORS['primary']}; - border: 1px solid {cls.COLORS['primary']}; - image: none; - }} + QCheckBox, QRadioButton {{ + spacing: 8px; + }} - QScrollBar:vertical {{ - border: none; - background: transparent; - width: 8px; - margin: 0px; - }} - QScrollBar::handle:vertical {{ - background: {cls.COLORS['border']}; - min-height: 20px; - border-radius: 4px; - }} - QScrollBar::handle:vertical:hover {{ background: {cls.COLORS['text_secondary']}; }} - QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0px; }} - """ + /* 未选中状态:完美的空心灰边小圆圈 */ + QCheckBox::indicator, QRadioButton::indicator {{ + width: 14px; /* 内部内容宽 14px */ + height: 14px; + /* 总宽 = 14 + 左边框1 + 右边框1 = 16px。16的一半是8,绝对的数学完美圆! */ + border-radius: 8px; + border: 1px solid {cls.COLORS['border']}; + background-color: {cls.COLORS['panel_bg']}; + }} + + /* 悬停状态:边框变蓝 */ + QCheckBox::indicator:hover, QRadioButton::indicator:hover {{ + border: 1px solid {cls.COLORS['primary']}; + }} + + /* 选中状态:外圈蓝 + 留白 + 内圈实心蓝点 (经典同心圆) */ + QCheckBox::indicator:checked, QRadioButton::indicator:checked {{ + border: 1px solid {cls.COLORS['primary']}; /* 外部蓝色细边框 */ + + /* 核心黑科技:利用 Qt 原生径向渐变画出完美的中心实心圆,彻底解决边框挤压导致的方块变形! */ + background: qradialgradient( + cx: 0.5, cy: 0.5, radius: 0.5, fx: 0.5, fy: 0.5, + stop: 0.0 {cls.COLORS['primary']}, /* 圆心向外 0~45% 为纯蓝色实心 */ + stop: 0.45 {cls.COLORS['primary']}, + stop: 0.55 {cls.COLORS['panel_bg']}, /* 55% 向外为白色(留白) */ + stop: 1.0 {cls.COLORS['panel_bg']} + ); + image: none; /* 屏蔽掉原生的对勾或点 */ + }} + + /* ======================================================= */ + + QScrollBar:vertical {{ + border: none; + background: transparent; + width: 8px; + margin: 0px; + }} + QScrollBar::handle:vertical {{ + background: {cls.COLORS['border']}; + min-height: 20px; + border-radius: 4px; + }} + QScrollBar::handle:vertical:hover {{ background: {cls.COLORS['text_secondary']}; }} + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0px; }} + """ @classmethod def get_button_stylesheet(cls, style_type: str = 'normal') -> str: @@ -224,7 +241,6 @@ class ModernStylesheet: @classmethod def get_sidebar_stylesheet(cls) -> str: - """⚠️ 历史兼容:主框架依赖此方法绘制侧边导航栏""" return f""" QWidget {{ background-color: {cls.COLORS['panel_bg']}; border-right: 1px solid {cls.COLORS['border_light']}; }} QPushButton {{ text-align: left; padding: 12px 20px; border: none; background-color: transparent; font-size: 14px; font-weight: 500; border-left: 4px solid transparent; }} @@ -235,5 +251,4 @@ class ModernStylesheet: @classmethod def get_toolbar_stylesheet(cls) -> str: - """⚠️ 历史兼容:顶部工具栏样式""" return f"QWidget {{ background-color: {cls.COLORS['panel_bg']}; border-bottom: 1px solid {cls.COLORS['border_light']}; }}" \ No newline at end of file