步骤四页面修改

This commit is contained in:
DXC
2026-06-26 13:51:10 +08:00
parent 6e4f0f5527
commit 7293ddd5ee
5 changed files with 350 additions and 425 deletions

View File

@ -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("独立运行步骤")

View File

@ -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()

View File

@ -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)
})

View File

@ -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(),

View File

@ -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']}; }}"