From bcd1ce371e32d510062c0ed6da856f0f910d384c Mon Sep 17 00:00:00 2001 From: DXC Date: Fri, 26 Jun 2026 09:45:00 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B5=E9=9D=A24=E6=A0=B7=E5=BC=8F=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/components/custom_widgets.py | 166 ++---- src/gui/panels/step4_sampling_panel.py | 181 +++--- src/gui/styles.py | 725 ++++++------------------- 3 files changed, 306 insertions(+), 766 deletions(-) diff --git a/src/gui/components/custom_widgets.py b/src/gui/components/custom_widgets.py index 6cc7557..08e2e0d 100644 --- a/src/gui/components/custom_widgets.py +++ b/src/gui/components/custom_widgets.py @@ -1,148 +1,102 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- """ -自定义组件 - 文件选择控件等公共组件 +自定义基础组件库 + +提供全站统一封装的文件选择器、目录选择器等。 +样式全权委托给全局 styles.py,杜绝在此处进行硬编码修饰。 """ import os - -from PyQt5.QtWidgets import ( - QWidget, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog, -) -from PyQt5.QtCore import Qt - +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog +from src.gui.styles import ModernStylesheet class DirSelectWidget(QWidget): - """目录选择组件""" - def __init__(self, label_text, parent=None): - """ - 初始化目录选择组件 - - Args: - label_text: 标签文本 - parent: 父控件 - """ + def __init__(self, label_text: str, parent=None): super().__init__(parent) self.init_ui(label_text) - def init_ui(self, label_text): - layout = QHBoxLayout() + def init_ui(self, label_text: str): + layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(8) self.label = QLabel(label_text) - self.label.setMinimumWidth(120) - self.line_edit = QLineEdit() - self.line_edit.setPlaceholderText("请选择目录...") - self.browse_btn = QPushButton("浏览...") - self.browse_btn.setMaximumWidth(80) - self.browse_btn.clicked.connect(self.browse_dir) - + self.label.setMinimumWidth(100) # 保底宽度,防挤压 layout.addWidget(self.label) + + self.line_edit = QLineEdit() + self.line_edit.setReadOnly(True) + self.line_edit.setPlaceholderText("请选择目录...") layout.addWidget(self.line_edit, 1) + + self.browse_btn = QPushButton("浏览...") + self.browse_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('normal')) + self.browse_btn.clicked.connect(self.browse_dir) layout.addWidget(self.browse_btn) - self.setLayout(layout) - def browse_dir(self): - """浏览目录 - 智能记忆上次选择位置""" - current_text = self.line_edit.text().strip() - initial_dir = "" + current_path = self.get_path() + start_dir = current_path if os.path.isdir(current_path) else "" + directory = QFileDialog.getExistingDirectory(self, "选择目录", start_dir) + if directory: + self.set_path(directory) - # 最高优先级:输入框已有路径存在 - if current_text: - if os.path.isdir(current_text): - initial_dir = current_text - else: - dir_path = os.path.dirname(current_text) - if dir_path and os.path.exists(dir_path): - initial_dir = dir_path + def get_path(self) -> str: + return self.line_edit.text().strip() - # 调用目录选择对话框 - dir_path = QFileDialog.getExistingDirectory( - self, "选择目录", initial_dir - ) - if dir_path: - self.line_edit.setText(dir_path) - - def get_path(self): - """获取路径""" - return self.line_edit.text() - - def set_path(self, path): - """设置路径""" - self.line_edit.setText(str(path)) + def set_path(self, path: str): + if path: + self.line_edit.setText(os.path.normpath(path).replace('\\', '/')) class FileSelectWidget(QWidget): - """文件选择组件""" - def __init__(self, label_text, file_filter="All Files (*.*)", mode="open", parent=None): - """ - 初始化文件选择组件 - - Args: - label_text: 标签文本 - file_filter: 文件过滤器 - mode: 选择模式 - "open"(打开文件) 或 "save"(保存文件) - parent: 父控件 - """ + def __init__(self, label_text: str, file_filter: str = "All Files (*.*)", mode: str = "open", parent=None): super().__init__(parent) self.file_filter = file_filter - self.mode = mode # "open" 或 "save" + self.mode = mode self.init_ui(label_text) - def init_ui(self, label_text): - layout = QHBoxLayout() + def init_ui(self, label_text: str): + layout = QHBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(8) self.label = QLabel(label_text) - self.label.setMinimumWidth(120) - self.line_edit = QLineEdit() - placeholder = "请选择保存路径..." if self.mode == "save" else "请选择文件..." - self.line_edit.setPlaceholderText(placeholder) - self.browse_btn = QPushButton("浏览...") - self.browse_btn.setMaximumWidth(80) - self.browse_btn.clicked.connect(self.browse_file) - + self.label.setMinimumWidth(100) # 保底宽度,防挤压 layout.addWidget(self.label) + + self.line_edit = QLineEdit() + self.line_edit.setReadOnly(True) + self.line_edit.setPlaceholderText("请选择文件..." if self.mode == "open" else "请指定保存位置...") layout.addWidget(self.line_edit, 1) + + self.browse_btn = QPushButton("浏览...") + self.browse_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('normal')) + self.browse_btn.clicked.connect(self.browse_file) layout.addWidget(self.browse_btn) - self.setLayout(layout) - def browse_file(self): - """浏览文件 - 智能记忆上次选择位置""" - current_text = self.line_edit.text().strip() - initial_dir = "" + current_path = self.get_path() + start_dir = "" + if current_path: + start_dir = current_path if os.path.isdir(current_path) else os.path.dirname(current_path) - # 最高优先级:输入框已有路径存在 - if current_text: - if os.path.isdir(current_text): - initial_dir = current_text - else: - dir_path = os.path.dirname(current_text) - if dir_path and os.path.exists(dir_path): - initial_dir = dir_path - - if self.mode == "save": - file_path, _ = QFileDialog.getSaveFileName( - self, "保存文件", initial_dir, self.file_filter - ) + if self.mode == "open": + file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", start_dir, self.file_filter) else: - file_path, _ = QFileDialog.getOpenFileName( - self, "选择文件", initial_dir, self.file_filter - ) + file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", start_dir, self.file_filter) + if file_path: - self.line_edit.setText(file_path) + self.set_path(file_path) - def get_path(self): - """获取路径""" - return self.line_edit.text() + def get_path(self) -> str: + return self.line_edit.text().strip() - def set_path(self, path): - """设置路径""" - self.line_edit.setText(str(path)) + def set_path(self, path: str): + if path: + self.line_edit.setText(os.path.normpath(path).replace('\\', '/')) - def set_read_only(self, read_only=True): - """设置文件选择框为只读,并禁用浏览按钮。""" + def set_read_only(self, read_only: bool = True): self.line_edit.setReadOnly(read_only) - self.browse_btn.setEnabled(not read_only) \ No newline at end of file + self.browse_btn.setEnabled(not read_only) + self.label.setEnabled(not read_only) \ 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 45fc0cf..5ee3b29 100644 --- a/src/gui/panels/step4_sampling_panel.py +++ b/src/gui/panels/step4_sampling_panel.py @@ -1,23 +1,23 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Step4 面板 - 采样点布设 +Step4 面板 - 采样点布设 (现代化排版重构) """ 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.QtCore import QTimer +from PyQt5.QtCore import QTimer, Qt from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QGroupBox, QFormLayout, - QPushButton, QCheckBox, QSpinBox, QMessageBox, + QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, + QPushButton, QCheckBox, QSpinBox, QMessageBox, QLabel, QFrame ) from src.gui.components.custom_widgets import FileSelectWidget @@ -27,92 +27,134 @@ 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): - layout = QVBoxLayout() + # 注入全局样式系统 + 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.deglint_img_file = FileSelectWidget( "去耀斑影像:", "Image Files (*.bsq *.dat *.tif);;All Files (*.*)" ) - layout.addWidget(self.deglint_img_file) - - # 水域掩膜文件(可选,用于独立运行) self.water_mask_file = FileSelectWidget( - "水域掩膜:", + "水域掩膜图:", "Mask Files (*.dat *.tif);;All Files (*.*)" ) - self.water_mask_file.label.setText("水域掩膜:") - layout.addWidget(self.water_mask_file) + input_layout.addWidget(self.deglint_img_file) + input_layout.addWidget(self.water_mask_file) + input_group.setLayout(input_layout) + main_layout.addWidget(input_group) - # 参数设置 - params_group = QGroupBox("采样参数") + # ========================================== + # 卡片 2:核心参数设置 + # ========================================== + params_group = QGroupBox("⚙️ 采样参数") params_layout = QFormLayout() + params_layout.setSpacing(16) + params_layout.setContentsMargins(20, 24, 20, 20) self.interval = QSpinBox() self.interval.setRange(10, 500) self.interval.setValue(50) - params_layout.addRow("采样点间隔(像素):", self.interval) + self.interval.setSuffix(" px") + self.interval.setMinimumWidth(120) + params_layout.addRow("采样点间隔:", self.interval) self.sample_radius = QSpinBox() self.sample_radius.setRange(1, 50) self.sample_radius.setValue(5) - params_layout.addRow("采样半径(像素):", self.sample_radius) + self.sample_radius.setSuffix(" px") + self.sample_radius.setMinimumWidth(120) + params_layout.addRow("中心采样半径:", self.sample_radius) self.chunk_size = QSpinBox() self.chunk_size.setRange(100, 10000) self.chunk_size.setValue(1000) - params_layout.addRow("处理块大小:", self.chunk_size) + self.chunk_size.setSuffix(" px") + self.chunk_size.setMinimumWidth(120) + params_layout.addRow("内存处理块大小:", self.chunk_size) - self.use_adaptive_sampling = QCheckBox("启用自适应采样") + self.use_adaptive_sampling = QCheckBox("启用自适应边缘采样") self.use_adaptive_sampling.setChecked(True) - params_layout.addRow("采样模式:", self.use_adaptive_sampling) + params_layout.addRow("智能模式:", self.use_adaptive_sampling) 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( - "输出采样点:", - "CSV Files (*.csv);;All Files (*.*)" + "结果保存至:", + "CSV Files (*.csv);;All Files (*.*)", + mode="save" ) self.output_file.line_edit.setPlaceholderText("sampling_spectra.csv") - layout.addWidget(self.output_file) + 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')) - self.run_btn.clicked.connect(self._on_run_single_clicked) - layout.addWidget(self.run_btn) + action_layout.addStretch() # 把按钮推到右边 - # 交互式预览按钮 - self.preview_btn = QPushButton("📊 交互式预览采样点与光谱") + self.preview_btn = QPushButton("交互式预览采样点") + self.preview_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('normal')) self.preview_btn.setEnabled(False) + self.preview_btn.setMinimumWidth(160) self.preview_btn.clicked.connect(self._open_sampling_viewer) - layout.addWidget(self.preview_btn) - layout.addStretch() - self.setLayout(layout) + 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) - # 添加心跳定时器,每2秒自动检查一次输出文件状态,刷新预览按钮 + action_layout.addWidget(self.preview_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) + + # 添加心跳定时器 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(), 'sample_radius': self.sample_radius.value(), @@ -128,7 +170,6 @@ class Step4SamplingPanel(QWidget): return config def set_config(self, config): - """设置配置""" if 'interval' in config: self.interval.setValue(config['interval']) if 'sample_radius' in config: @@ -141,16 +182,8 @@ class Step4SamplingPanel(QWidget): self.deglint_img_file.set_path(config['deglint_img_path']) if 'water_mask_path' in config: self.water_mask_file.set_path(config['water_mask_path']) - if 'glint_mask_path' in config: - self.glint_mask_file.set_path(config['glint_mask_path']) def update_from_config(self, work_dir=None, pipeline=None): - """从全局配置自动填充去耀斑影像和掩膜路径 - - Args: - work_dir: 工作目录路径 - pipeline: Pipeline 实例(用于从 step_outputs 获取绝对路径) - """ if work_dir: self.work_dir = work_dir elif hasattr(self, 'work_dir') and self.work_dir: @@ -159,123 +192,87 @@ class Step4SamplingPanel(QWidget): self.work_dir = None main_window = self.window() - - # 1. 填充去耀斑影像路径(优先从 pipeline.step_outputs 获取绝对路径) deglint_path = None if pipeline and hasattr(pipeline, 'step_outputs'): step3_outputs = getattr(pipeline, 'step_outputs', {}).get('step3', {}) deglint_path = ( - step3_outputs.get('deglint_image') - or step3_outputs.get('output_path') - or step3_outputs.get('output_file') - or step3_outputs.get('deglint_img_path') + step3_outputs.get('deglint_image') or step3_outputs.get('output_path') or + step3_outputs.get('output_file') or step3_outputs.get('deglint_img_path') ) - # 回退:从 step3 面板 widget 直接读取(可能是相对路径) if not deglint_path and hasattr(main_window, 'step3_panel'): deglint_path = main_window.step3_panel.output_file.get_path() if deglint_path: - # 若为相对路径,使用 work_dir 合成为绝对路径 if not os.path.isabs(deglint_path): deglint_path = os.path.join(self.work_dir or '', deglint_path).replace('\\', '/') self.deglint_img_file.set_path(deglint_path) - # 2. 填充水域掩膜路径(优先级:pipeline.step_outputs > step1_panel > 1_water_mask > input-test) water_mask_path = None if pipeline and hasattr(pipeline, 'step_outputs'): step1_outputs = getattr(pipeline, 'step_outputs', {}).get('step1', {}) water_mask_path = ( - step1_outputs.get('water_mask') - or step1_outputs.get('output_path') - or step1_outputs.get('output_file') + step1_outputs.get('water_mask') or step1_outputs.get('output_path') or step1_outputs.get( + 'output_file') ) - # 回退:从 step1 面板 widget 直接读取 if not water_mask_path and hasattr(main_window, 'step1_panel'): water_mask_path = main_window.step1_panel.output_file.get_path() - # 备选:扫描 1_water_mask 目录下的 .dat 文件 + if not water_mask_path and self.work_dir: mask_dir = resolve_subdir(self.work_dir, 'water_mask') if os.path.isdir(mask_dir): dat_files = [f for f in os.listdir(mask_dir) if f.lower().endswith('.dat')] if dat_files: water_mask_path = os.path.join(mask_dir, dat_files[0]).replace('\\', '/') - # 备选:扫描 input-test 目录(优先匹配 water_mask_from_shp.dat) + if not water_mask_path and self.work_dir: input_test_dir = os.path.join(self.work_dir, "input-test") if os.path.isdir(input_test_dir): dat_files = [f for f in os.listdir(input_test_dir) if f.lower().endswith('.dat')] - # 优先匹配 water_mask_from_shp.dat for f in dat_files: if 'water_mask_from_shp' in f.lower(): water_mask_path = os.path.join(input_test_dir, f).replace('\\', '/') break - # 否则取第一个 .dat 文件 if not water_mask_path and dat_files: water_mask_path = os.path.join(input_test_dir, dat_files[0]).replace('\\', '/') if water_mask_path: - # 若为相对路径,使用 work_dir 合成为绝对路径 if not os.path.isabs(water_mask_path): water_mask_path = os.path.join(self.work_dir or '', water_mask_path).replace('\\', '/') self.water_mask_file.set_path(water_mask_path) - # 3. 自动填充输出路径(绝对路径) if self.work_dir: output_path = resolve_subdir(self.work_dir, 'sampling_csv_path') os.makedirs(os.path.dirname(output_path), exist_ok=True) self.output_file.set_path(output_path.replace('\\', '/')) - # 4. 同步更新预览按钮状态(路径可能已自动填充) self._check_csv_exists() def _on_run_single_clicked(self): - """通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor)。""" from src.gui.core.event_bus import global_event_bus - deglint_img_path = self.deglint_img_file.get_path() if not deglint_img_path: QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!") return - config = {'step4_sampling': self.get_config()} global_event_bus.publish('RequestRunSingleStep', { 'step_name': 'step4_sampling', 'config': config, }) - def run_step(self): - """独立运行步骤4(旧版 parent 链上溯方式,保留兼容)。""" - deglint_img_path = self.deglint_img_file.get_path() - if not deglint_img_path: - QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!") - return - - main_window = self.window() - if hasattr(main_window, 'run_single_step'): - config = {'step4_sampling': self.get_config()} - main_window.run_single_step('step4_sampling', config) - def _check_csv_exists(self): - """检查 output csv 是否存在,驱动预览按钮启停""" csv_path = self.output_file.get_path() enabled = bool(csv_path and os.path.isabs(csv_path) and os.path.exists(csv_path)) self.preview_btn.setEnabled(enabled) return enabled def _on_output_changed(self, _text=None): - """输出路径输入框内容变化时调用(_text 为 line_edit.textChanged 信号参数)""" self._check_csv_exists() def _open_sampling_viewer(self): - """打开交互式采样点查看器弹窗""" csv_path = self.output_file.get_path() if not csv_path or not os.path.exists(csv_path): - QMessageBox.warning( - self, "文件不存在", - f"采样点 CSV 文件不存在:{csv_path}\n请先运行步骤4生成数据。" - ) + QMessageBox.warning(self, "文件不存在", f"采样点 CSV 文件不存在:{csv_path}\n请先运行生成数据。") return dialog = SamplingViewerDialog(csv_path, self) dialog.exec_() - # 弹窗关闭后再次检查状态(可能文件被覆盖等) self._check_csv_exists() \ No newline at end of file diff --git a/src/gui/styles.py b/src/gui/styles.py index 6725536..9381907 100644 --- a/src/gui/styles.py +++ b/src/gui/styles.py @@ -1,608 +1,197 @@ -#!/usr/bin/env python # -*- coding: utf-8 -*- """ -现代化样式表和主题管理模块 -Modern Stylesheet and Theme Management Module +全局样式与设计系统 (Design System) - 向后兼容增强版 """ class ModernStylesheet: - """现代化样式表集合""" - - # 颜色定义 + """现代化样式生成器""" + + # 1. 增强版色板 (Design Tokens) COLORS = { - 'main_bg': '#F0F0F0', # 主窗口背景:浅灰 - 'panel_bg': '#FFFFFF', # 面板/容器背景:白色 - 'text_primary': '#000000', # 主文字:黑色 - 'text_secondary': '#666666', # 辅助文字:灰色 - 'border': '#D0D0D0', # 边框:浅灰 - 'border_light': '#E8E8E8', # 浅边框 - 'accent': '#007BFF', # 强调色:蓝色 - 'success': '#28A745', # 成功绿 - 'error': '#DC3545', # 错误红 - 'warning': '#FFC107', # 警告黄 - 'hover': '#E8E8E8', # 悬停背景 - 'selected': '#0056B3', # 选中色 + 'primary': '#0078D4', + 'accent': '#0078D4', # ⚠️ 历史兼容:保留全局 30+ 处对 accent 的引用 + 'primary_hover': '#106EBE', + 'primary_pressed': '#005A9E', + 'primary_light': '#E8F4FD', + 'primary_glow': 'rgba(0, 120, 212, 0.2)', + + 'main_bg': '#F0F2F5', + 'panel_bg': '#FFFFFF', + 'hover': '#F3F4F6', + 'selected': '#E0F2FE', + + 'text_primary': '#1E293B', + 'text_secondary': '#64748B', + 'text_disabled': '#94A3B8', + 'text_on_primary': '#FFFFFF', + + 'success': '#10B981', + 'success_hover': '#059669', + 'warning': '#F59E0B', + 'error': '#EF4444', + 'info': '#3B82F6', + + 'border': '#CBD5E1', + 'border_light': '#E2E8F0', + 'border_focus': '#0078D4', } - - @staticmethod - def get_main_stylesheet(): - """获取主样式表""" + + FONTS = { + 'family': '"Microsoft YaHei", "Segoe UI", sans-serif', + } + + VARS = { + 'radius_sm': '4px', + 'radius_md': '6px', + 'radius_lg': '8px', + 'padding_input': '8px 12px', + 'padding_btn': '8px 16px', + } + + @classmethod + def get_main_stylesheet(cls) -> str: + """全局基础样式表""" return f""" - /* 主窗口 */ - QMainWindow {{ - background-color: {ModernStylesheet.COLORS['main_bg']}; - }} - - /* 中央部件和容器 */ QWidget {{ - background-color: {ModernStylesheet.COLORS['main_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; + background-color: {cls.COLORS['main_bg']}; + color: {cls.COLORS['text_primary']}; + font-family: {cls.FONTS['family']}; + font-size: 13px; }} - - /* 分组框 */ + + QGroupBox, QFrame, QScrollArea, QListWidget, QTreeWidget, QTableWidget {{ + background-color: {cls.COLORS['panel_bg']}; + }} + QGroupBox {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - font-weight: bold; - border: 0px; - margin-top: 10px; - padding-top: 15px; - padding-left: 9px; - padding-right: 9px; - padding-bottom: 9px; - border-bottom: 1px solid {ModernStylesheet.COLORS['border_light']}; + 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; - padding: 0 5px; - font-size: 12px; + left: 12px; + top: 0px; + color: {cls.COLORS['primary']}; font-weight: bold; - color: {ModernStylesheet.COLORS['text_primary']}; + font-size: 13px; + background-color: {cls.COLORS['main_bg']}; + padding: 0 4px; }} - - /* 按钮 */ - QPushButton {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-radius: 7px; - padding: 3px 5px; - min-height: 25px; - max-height: 33px; - font-size: 12px; - font-weight: normal; - outline: none; + + 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']}; }} - - QPushButton:hover {{ - background-color: {ModernStylesheet.COLORS['hover']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; + QLineEdit:hover, QSpinBox:hover, QDoubleSpinBox:hover {{ + border: 1px solid {cls.COLORS['text_secondary']}; }} - - QPushButton:pressed {{ - background-color: {ModernStylesheet.COLORS['border_light']}; + QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus {{ + border: 1.5px solid {cls.COLORS['border_focus']}; + background-color: #FFFFFF; }} - - QPushButton:disabled {{ - background-color: {ModernStylesheet.COLORS['hover']}; - color: {ModernStylesheet.COLORS['text_secondary']}; - border: 1px solid {ModernStylesheet.COLORS['border_light']}; + QLineEdit:read-only {{ + background-color: #F8FAFC; + color: {cls.COLORS['text_secondary']}; + border: 1px solid {cls.COLORS['border_light']}; }} - - QPushButton:focus {{ - outline: none; - }} - - /* 输入框 */ - QLineEdit {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-radius: 10px; - padding: 5px 8px; - min-height: 20px; - selection-background-color: {ModernStylesheet.COLORS['selected']}; - selection-color: white; - }} - - QLineEdit:focus {{ - border: 1px solid {ModernStylesheet.COLORS['accent']}; - background-color: {ModernStylesheet.COLORS['panel_bg']}; - }} - - /* 下拉框 */ + QComboBox {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-radius: 5px; - padding: 5px 8px; - min-height: 25px; - selection-background-color: {ModernStylesheet.COLORS['selected']}; + border: 1px solid {cls.COLORS['border']}; + border-radius: {cls.VARS['radius_md']}; + padding: 6px 10px; + background-color: {cls.COLORS['panel_bg']}; }} - - QComboBox:focus {{ - border: 1px solid {ModernStylesheet.COLORS['accent']}; - }} - + QComboBox:hover {{ border: 1px solid {cls.COLORS['text_secondary']}; }} + QComboBox:focus {{ border: 1.5px solid {cls.COLORS['border_focus']}; }} QComboBox::drop-down {{ - border: 0px; - padding-right: 5px; + subcontrol-origin: padding; + subcontrol-position: top right; + width: 20px; + border-left: none; }} - - QComboBox QAbstractItemView {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - selection-background-color: {ModernStylesheet.COLORS['selected']}; - selection-color: white; - border: 1px solid {ModernStylesheet.COLORS['border']}; + 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; }} - - /* 数值输入框 */ - QSpinBox, QDoubleSpinBox {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-radius: 5px; - padding: 5px 8px; - min-height: 25px; - }} - - QSpinBox:focus, QDoubleSpinBox:focus {{ - border: 1px solid {ModernStylesheet.COLORS['accent']}; - }} - - QSpinBox::up-button, QDoubleSpinBox::up-button {{ - border: 0px; - padding-right: 5px; - }} - - QSpinBox::down-button, QDoubleSpinBox::down-button {{ - border: 0px; - padding-right: 5px; - }} - - /* 复选框 */ - QCheckBox {{ - color: {ModernStylesheet.COLORS['text_primary']}; - spacing: 5px; - }} - + + QCheckBox {{ spacing: 8px; }} QCheckBox::indicator {{ width: 16px; height: 16px; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-radius: 3px; - background-color: {ModernStylesheet.COLORS['panel_bg']}; + 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: {ModernStylesheet.COLORS['accent']}; - border: 1px solid {ModernStylesheet.COLORS['accent']}; + background-color: {cls.COLORS['primary']}; + border: 1px solid {cls.COLORS['primary']}; + image: none; }} - - /* 单选框 */ - QRadioButton {{ - color: {ModernStylesheet.COLORS['text_primary']}; - spacing: 5px; - }} - - QRadioButton::indicator {{ - width: 16px; - height: 16px; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-radius: 8px; - background-color: {ModernStylesheet.COLORS['panel_bg']}; - }} - - QRadioButton::indicator:checked {{ - background: qradial(circle, {ModernStylesheet.COLORS['accent']} 0%, {ModernStylesheet.COLORS['accent']} 40%, {ModernStylesheet.COLORS['panel_bg']} 60%); - border: 1px solid {ModernStylesheet.COLORS['accent']}; - }} - - /* 文本编辑框 */ - QTextEdit {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-radius: 5px; - padding: 5px; - selection-background-color: {ModernStylesheet.COLORS['selected']}; - selection-color: white; - }} - - QTextEdit:focus {{ - border: 1px solid {ModernStylesheet.COLORS['accent']}; - }} - - /* 列表部件 */ - QListWidget {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-radius: 5px; - outline: none; - }} - - QListWidget::item {{ - padding: 6px; - border: 0px; - }} - - QListWidget::item:hover {{ - background-color: {ModernStylesheet.COLORS['hover']}; - }} - - QListWidget::item:selected {{ - background-color: {ModernStylesheet.COLORS['selected']}; - color: white; - }} - - /* 滚动区域 */ - QScrollArea {{ - background-color: {ModernStylesheet.COLORS['main_bg']}; - border: 0px; - }} - - /* 滚动条 */ - QScrollBar:vertical {{ - background-color: {ModernStylesheet.COLORS['main_bg']}; - width: 12px; - border: 0px; - }} - - QScrollBar::handle:vertical {{ - background-color: {ModernStylesheet.COLORS['border']}; - border-radius: 6px; - min-height: 20px; - }} - - QScrollBar::handle:vertical:hover {{ - background-color: {ModernStylesheet.COLORS['text_secondary']}; - }} - - QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ - border: 0px; - background-color: transparent; - }} - - QScrollBar:horizontal {{ - background-color: {ModernStylesheet.COLORS['main_bg']}; - height: 12px; - border: 0px; - }} - - QScrollBar::handle:horizontal {{ - background-color: {ModernStylesheet.COLORS['border']}; - border-radius: 6px; - min-width: 20px; - }} - - QScrollBar::handle:horizontal:hover {{ - background-color: {ModernStylesheet.COLORS['text_secondary']}; - }} - - QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ - border: 0px; - background-color: transparent; - }} - - /* 进度条 */ - QProgressBar {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-radius: 5px; - padding: 2px; - text-align: center; - height: 20px; - }} - - QProgressBar::chunk {{ - background-color: {ModernStylesheet.COLORS['success']}; - border-radius: 3px; - }} - - /* 标签 */ - QLabel {{ - color: {ModernStylesheet.COLORS['text_primary']}; - background-color: transparent; - }} - - /* 标签栏 */ - QTabBar::tab {{ - background-color: {ModernStylesheet.COLORS['main_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-bottom: 0px; - padding: 8px 12px; - margin-right: 2px; - border-radius: 5px 5px 0px 0px; - }} - - QTabBar::tab:selected {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-bottom: 2px solid {ModernStylesheet.COLORS['accent']}; - color: {ModernStylesheet.COLORS['accent']}; - }} - - QTabBar::tab:hover {{ - background-color: {ModernStylesheet.COLORS['hover']}; - }} - - QTabWidget::pane {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - border-top: 0px; - border-radius: 0px 0px 5px 5px; - }} - - /* 菜单栏 */ - QMenuBar {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border-bottom: 1px solid {ModernStylesheet.COLORS['border_light']}; - padding: 2px; - }} - - QMenuBar::item:selected {{ - background-color: {ModernStylesheet.COLORS['hover']}; - }} - - /* 菜单 */ - QMenu {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border: 1px solid {ModernStylesheet.COLORS['border']}; - padding: 4px 0px; - border-radius: 5px; - }} - - QMenu::item:selected {{ - background-color: {ModernStylesheet.COLORS['hover']}; - padding-left: 20px; - }} - - QMenu::separator {{ - height: 1px; - background-color: {ModernStylesheet.COLORS['border_light']}; - margin: 4px 0px; - }} - - /* 状态栏 */ - QStatusBar {{ - background-color: {ModernStylesheet.COLORS['panel_bg']}; - color: {ModernStylesheet.COLORS['text_primary']}; - border-top: 1px solid {ModernStylesheet.COLORS['border_light']}; - }} - - /* 框架 */ - QFrame {{ - background-color: {ModernStylesheet.COLORS['main_bg']}; - border: 0px; - }} - - /* 对话框 */ - QDialog {{ - background-color: {ModernStylesheet.COLORS['main_bg']}; - }} - - /* 消息框 */ - QMessageBox {{ - background-color: {ModernStylesheet.COLORS['main_bg']}; - }} - - QMessageBox QLabel {{ - color: {ModernStylesheet.COLORS['text_primary']}; - }} - """ - - @staticmethod - def get_button_stylesheet(style_type='normal'): - """获取特定样式的按钮样式表""" - colors = ModernStylesheet.COLORS - - if style_type == 'primary': - # 蓝色主按钮 - return f""" - QPushButton {{ - background-color: {colors['accent']}; - color: white; - border: 1px solid {colors['accent']}; - border-radius: 7px; - padding: 3px 5px; - min-height: 25px; - max-height: 33px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: #0056b3; - border: 1px solid #0056b3; - }} - QPushButton:pressed {{ - background-color: #003d82; - }} - QPushButton:disabled {{ - background-color: {colors['hover']}; - color: {colors['text_secondary']}; - border: 1px solid {colors['border_light']}; - }} - """ - - elif style_type == 'success': - # 绿色成功按钮 - return f""" - QPushButton {{ - background-color: {colors['success']}; - color: white; - border: 1px solid {colors['success']}; - border-radius: 7px; - padding: 3px 5px; - min-height: 25px; - max-height: 33px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: #218838; - border: 1px solid #218838; - }} - QPushButton:pressed {{ - background-color: #1a6c28; - }} - QPushButton:disabled {{ - background-color: {colors['hover']}; - color: {colors['text_secondary']}; - border: 1px solid {colors['border_light']}; - }} - """ - - elif style_type == 'danger': - # 红色危险按钮 - return f""" - QPushButton {{ - background-color: {colors['error']}; - color: white; - border: 1px solid {colors['error']}; - border-radius: 7px; - padding: 3px 5px; - min-height: 25px; - max-height: 33px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: #c82333; - border: 1px solid #c82333; - }} - QPushButton:pressed {{ - background-color: #9a1a24; - }} - QPushButton:disabled {{ - background-color: {colors['hover']}; - color: {colors['text_secondary']}; - border: 1px solid {colors['border_light']}; - }} - """ - - else: # normal/default - return f""" - QPushButton {{ - background-color: {colors['panel_bg']}; - color: {colors['text_primary']}; - border: 1px solid {colors['border']}; - border-radius: 7px; - padding: 3px 5px; - min-height: 25px; - max-height: 33px; - }} - QPushButton:hover {{ - background-color: {colors['hover']}; - border: 1px solid {colors['border']}; - }} - QPushButton:pressed {{ - background-color: {colors['border_light']}; - }} - QPushButton:disabled {{ - background-color: {colors['hover']}; - color: {colors['text_secondary']}; - border: 1px solid {colors['border_light']}; - }} - """ - - @staticmethod - def get_toolbar_stylesheet(): - """获取顶部工具栏样式表""" - colors = ModernStylesheet.COLORS - return f""" - QWidget {{ - background-color: {colors['panel_bg']}; - border-bottom: 1px solid {colors['border_light']}; - }} - QLabel {{ - color: {colors['text_primary']}; - }} - QPushButton {{ - background-color: {colors['panel_bg']}; - color: {colors['text_primary']}; - border: 1px solid {colors['border']}; - border-radius: 5px; - padding: 5px 10px; - min-height: 25px; - }} - QPushButton:hover {{ - background-color: {colors['hover']}; - }} - """ - - @staticmethod - def get_sidebar_stylesheet(): - """获取左侧边栏样式表 - 设计主题:扁平无框 + 蓝色高亮。 - 结构契约: - - 分类头 stage_header 项已通过 setFlags(~Qt.ItemIsEnabled) 禁用, - 因此 :!enabled 选择器可精确锁定分类头(蓝色加粗)。 - - 可点击的步骤项保持 enabled,进入 :enabled 通道 - (padding/margin 留白 + 圆角 + hover/selected 蓝色现代感)。 - """ - colors = ModernStylesheet.COLORS - # 主题色板(蓝色高亮) - stage_header_color = '#0078D4' # 分类头:亮蓝色加粗 - step_hover_bg = '#F0F4F8' # hover(极浅蓝灰) - step_selected_bg = '#0078D4' # selected(饱和蓝) - step_selected_fg = '#FFFFFF' # selected(白字) - return f""" - QWidget {{ - background-color: {colors['panel_bg']}; - border-right: 1px solid {colors['border_light']}; - }} - QLabel {{ - color: {colors['text_primary']}; - font-weight: bold; - }} - /* ── 容器:无框化、零描边、零焦点环 ── */ - QListWidget {{ + QScrollBar:vertical {{ border: none; - outline: none; - background-color: transparent; + background: transparent; + width: 8px; + margin: 0px; }} - /* ── 分类头(stage_header,禁用态):亮蓝色 + 加粗 + 上下间距 ── */ - QListWidget::item:!enabled {{ - color: {stage_header_color}; - font-weight: bold; - background-color: transparent; - border: none; - padding: 14px 8px 6px 8px; - margin-top: 4px; - }} - /* ── 步骤项(enabled):padding/margin 留白 + 圆角过渡 ── */ - QListWidget::item:enabled {{ - color: {colors['text_secondary']}; - padding: 8px 6px; - margin: 2px 8px; - border: none; + QScrollBar::handle:vertical {{ + background: {cls.COLORS['border']}; + min-height: 20px; border-radius: 4px; }} - /* ── 步骤项 hover(未选中态):极浅蓝灰悬浮 ── */ - QListWidget::item:enabled:hover:!selected {{ - background-color: {step_hover_bg}; - color: {colors['text_primary']}; + 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: + base = f""" + QPushButton {{ + font-family: {cls.FONTS['family']}; + font-size: 13px; + font-weight: 500; + padding: {cls.VARS['padding_btn']}; + border-radius: {cls.VARS['radius_md']}; + border: 1px solid transparent; }} - /* ── 步骤项 selected:饱和蓝高亮 + 白字 ── */ - QListWidget::item:enabled:selected {{ - background-color: {step_selected_bg}; - color: {step_selected_fg}; - border: none; - font-weight: bold; - }} - /* ── 分隔符占位项:完全透明,零干扰 ── */ - QListWidget::item[separator="true"] {{ - background-color: transparent; - border: none; - padding: 0px; - margin: 0px; - min-height: 4px; + QPushButton:disabled {{ + background-color: #E2E8F0; + color: {cls.COLORS['text_disabled']}; }} """ + if style_type == 'primary': + return base + f"QPushButton {{ background-color: {cls.COLORS['primary']}; color: {cls.COLORS['text_on_primary']}; }} QPushButton:hover {{ background-color: {cls.COLORS['primary_hover']}; }} QPushButton:pressed {{ background-color: {cls.COLORS['primary_pressed']}; }}" + elif style_type == 'success': + return base + f"QPushButton {{ background-color: {cls.COLORS['success']}; color: {cls.COLORS['text_on_primary']}; }} QPushButton:hover {{ background-color: {cls.COLORS['success_hover']}; }}" + elif style_type == 'danger': + return base + f"QPushButton {{ background-color: {cls.COLORS['error']}; color: {cls.COLORS['text_on_primary']}; }}" + else: + return base + f"QPushButton {{ background-color: {cls.COLORS['panel_bg']}; color: {cls.COLORS['text_primary']}; border: 1px solid {cls.COLORS['border']}; }} QPushButton:hover {{ background-color: {cls.COLORS['hover']}; border: 1px solid {cls.COLORS['primary']}; color: {cls.COLORS['primary']}; }} QPushButton:pressed {{ background-color: #E2E8F0; }}" + + @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; }} + QPushButton:hover {{ background-color: {cls.COLORS['hover']}; }} + QPushButton:checked {{ background-color: {cls.COLORS['primary_light']}; color: {cls.COLORS['primary']}; font-weight: bold; border-left: 4px solid {cls.COLORS['primary']}; }} + QLabel#stage_header {{ color: {cls.COLORS['primary']}; font-weight: bold; font-size: 15px; padding: 16px 0 8px 16px; border-bottom: 1px solid {cls.COLORS['border_light']}; }} + """ + + @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