From c21089e8ea73d42ce651e2542d9c5195894a9df2 Mon Sep 17 00:00:00 2001 From: risingLee <871066422@qq.com> Date: Wed, 25 Feb 2026 15:05:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=20+=20=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A1=86=E5=8A=A0=E8=BD=BDjson?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard.json | 46 ++--- main.py | 190 +++++++++++++++++---- resources/down_arrow.svg | 3 + resources/up_arrow.svg | 3 + styles.py | 354 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 539 insertions(+), 57 deletions(-) create mode 100644 resources/down_arrow.svg create mode 100644 resources/up_arrow.svg create mode 100644 styles.py diff --git a/dashboard.json b/dashboard.json index cebc1c6..6ec4c8a 100644 --- a/dashboard.json +++ b/dashboard.json @@ -6,21 +6,6 @@ "h": 1080.0 }, "widgets": [ - { - "widget_type": "label", - "x": 46.0, - "y": 378.0, - "w": 162.0, - "h": 62.0, - "z": 1.0, - "config": { - "fieldName": "减速箱小轴承2", - "prefix": "减速箱小轴承温度", - "suffix": "℃", - "fontSize": 16, - "color": "#FFFFFF" - } - }, { "widget_type": "label", "x": 173.0, @@ -37,15 +22,18 @@ } }, { - "widget_type": "web", - "x": 649.0, - "y": 0.0, - "w": 886.0, - "h": 863.0, - "z": 0.0, + "widget_type": "label", + "x": 46.0, + "y": 378.0, + "w": 162.0, + "h": 62.0, + "z": 1.0, "config": { - "url": "http://127.0.0.1:8086/orgs/b2542eeb72a3e614/dashboards/0f8ab8a328fe9000?lower=now%28%29+-+1h", - "locked": true + "fieldName": "减速箱小轴承2", + "prefix": "减速箱小轴承温度", + "suffix": "℃", + "fontSize": 16, + "color": "#FFFFFF" } }, { @@ -58,6 +46,18 @@ "config": { "imagePath": "D:/1-2.JPG" } + }, + { + "widget_type": "web", + "x": 649.0, + "y": 0.0, + "w": 886.0, + "h": 863.0, + "z": 0.0, + "config": { + "url": "http://127.0.0.1:8086/orgs/b2542eeb72a3e614/dashboards/0f8ab8a328fe9000?lower=now%28%29+-+1h", + "locked": true + } } ] } \ No newline at end of file diff --git a/main.py b/main.py index 1b92c39..237d930 100644 --- a/main.py +++ b/main.py @@ -16,7 +16,14 @@ from dataclasses import dataclass, asdict, field from typing import Optional, Dict, Any import math -from PyQt6.QtCore import Qt, QRectF, pyqtSignal, QObject, QTimer, QPointF +# 忽略 Windows 显示缩放设置,使用物理像素 +# 这确保画布 1920x1080 能正确对应屏幕像素,避免 Web 组件被缩放 +os.environ["QT_SCALE_FACTOR"] = "1" +os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "0" + +from PyQt6.QtCore import Qt, QRectF, pyqtSignal, QObject, QTimer, QPointF, QSizeF +from PyQt6.QtWidgets import QSizePolicy +from PyQt6.QtGui import QFontMetrics from PyQt6.QtGui import QBrush, QColor, QPen, QPixmap, QFont, QCursor, QShortcut, QKeySequence from PyQt6.QtWidgets import ( QApplication, @@ -57,6 +64,14 @@ from PyQt6.QtCore import QUrl # 延迟导入:InfluxDB 客户端只在需要时导入(减少启动时间) # from influxdb_wrapper import InfluxDBClient +# 导入样式模块 +from styles import MAIN_STYLE, Colors, CANVAS_STYLE, ITEM_STYLE + +# 应用全局样式 +def apply_styles(app: QApplication): + """应用全局样式表""" + app.setStyleSheet(MAIN_STYLE) + # ----------------------------- # 数据结构(用于保存/加载布局) @@ -598,12 +613,12 @@ class LabelItem(DashboardItem): # 直接返回,不调用父类的 paint,避免绘制任何边框 return - # 编辑模式:选中时高亮蓝色,否则浅灰色;背景依然透明 + # 编辑模式:选中时高亮强调色,否则使用边框色;背景依然透明 # 先设置 pen,确保父类 paint 使用正确的边框颜色 if self.isSelected(): - self.setPen(QPen(QColor(80, 160, 255), 2)) + self.setPen(QPen(QColor(Colors.ACCENT), 2)) else: - self.setPen(QPen(QColor(170, 170, 170), 1)) + self.setPen(QPen(QColor(Colors.BORDER), 1)) # 确保背景透明 self.setBrush(QBrush(Qt.GlobalColor.transparent)) super().paint(painter, option, widget) @@ -821,7 +836,7 @@ class DashboardScene(QGraphicsScene): def set_canvas_edit_mode(self, edit_mode: bool): # 编辑时显示边界;展示时隐藏边界 if edit_mode: - self._canvas_item.setPen(QPen(QColor(140, 140, 140), 1, Qt.PenStyle.DashLine)) + self._canvas_item.setPen(QPen(QColor(Colors.BORDER), 1, Qt.PenStyle.DashLine)) else: self._canvas_item.setPen(QPen(Qt.GlobalColor.transparent)) @@ -831,7 +846,7 @@ class DashboardScene(QGraphicsScene): # 画布边界(用于定义“屏幕/展示区域”的坐标系与尺寸) self._canvas_item = QGraphicsRectItem() self._canvas_item.setZValue(-1e9) - # 画布背景色:纯白色 + # 画布使用白色配色 self._canvas_item.setPen(QPen(QColor(200, 200, 200), 1, Qt.PenStyle.DashLine)) # 浅灰色边框,在白色背景下可见 self._canvas_item.setBrush(QBrush(QColor(255, 255, 255))) # 纯白色背景 self._canvas_item.setFlag(QGraphicsRectItem.GraphicsItemFlag.ItemIsSelectable, False) @@ -849,11 +864,20 @@ class DashboardView(QGraphicsView): self._scene = DashboardScene(self) self.setScene(self._scene) self.setRenderHints(self.renderHints()) - self.setBackgroundBrush(QColor(255, 255, 255)) # 纯白色背景 + # 使用深色主题背景 + self.setBackgroundBrush(QColor(Colors.BG_PRIMARY)) # 取消自身的边框,避免全屏时出现白边 self.setFrameShape(QFrame.Shape.NoFrame) self.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop) self._edit_mode = True + + # 设置视口样式 + self.setStyleSheet(f""" + DashboardView {{ + background-color: {Colors.BG_PRIMARY}; + border: none; + }} + """) @property def scene_obj(self) -> DashboardScene: @@ -873,10 +897,21 @@ class DashboardView(QGraphicsView): v.setValue(v.minimum()) def fit_canvas_to_view(self): - """让画布按1:1显示(用于全屏模式)""" - # 直接使用1:1显示,不进行任何缩放 + """让画布完全适应视图(用于全屏展示模式) + + 关键:保持 1:1 比例,不做任何缩放! + 只是隐藏滚动条,让画布完整显示。 + 如果视图大小与画布匹配,自然就能全屏显示。 + """ + # 重置变换为 1:1,绝对不能缩放,否则 Web 组件会失真 self.resetTransform() + + # 滚动条复位到左上角 self.reset_scroll() + + # 隐藏滚动条 + self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) def reset_view_transform(self): """重置视图变换(用于退出全屏时恢复)""" @@ -967,7 +1002,7 @@ class DashboardView(QGraphicsView): json.dump(payload, f, indent=2, ensure_ascii=False) def load_layout(self, path: str): - if not os.path.exists(path): + if path is None or not os.path.exists(path): return with open(path, "r", encoding="utf-8") as f: data = json.load(f) @@ -1047,6 +1082,9 @@ class PropertyPanel(QWidget): self._current_item: Optional[DashboardItem] = None # mode: "none" | "item" | "canvas" self._mode: str = "none" + + # 注意:不要在这里设置样式表,否则会覆盖全局样式 + # 背景色通过父窗口或全局样式控制 self.x_spin = QSpinBox() self.y_spin = QSpinBox() @@ -1084,13 +1122,15 @@ class PropertyPanel(QWidget): self._arrow_color = "#00FF00" form = QFormLayout() - form.addRow("X:", self.x_spin) - form.addRow("Y:", self.y_spin) - form.addRow("W:", self.w_spin) - form.addRow("H:", self.h_spin) - form.addRow("Z:", self.z_spin) + form.setSpacing(10) + form.setLabelAlignment(Qt.AlignmentFlag.AlignLeft) + form.addRow("X 坐标:", self.x_spin) + form.addRow("Y 坐标:", self.y_spin) + form.addRow("宽度:", self.w_spin) + form.addRow("高度:", self.h_spin) + form.addRow("层级:", self.z_spin) - img_group = QGroupBox("图片组件") + img_group = QGroupBox("🖼 图片组件") img_layout = QVBoxLayout(img_group) img_layout.addWidget(self.image_path_edit) img_layout.addWidget(self.image_browse_btn) @@ -1119,15 +1159,14 @@ class PropertyPanel(QWidget): self.arrow_group = arrow_group layout = QVBoxLayout(self) - layout.addWidget(QLabel("当前选中组件")) layout.addLayout(form) - layout.addSpacing(10) + layout.addSpacing(8) layout.addWidget(img_group) - layout.addSpacing(10) + layout.addSpacing(8) layout.addWidget(label_group) - layout.addSpacing(10) + layout.addSpacing(8) layout.addWidget(web_group) - layout.addSpacing(10) + layout.addSpacing(8) layout.addWidget(arrow_group) layout.addStretch(1) @@ -1454,11 +1493,19 @@ class MainWindow(QMainWindow): # 右侧:属性面板 + 组件列表 self.right_panel = QWidget() + self.right_panel.setObjectName("right_panel") + self.right_panel.setStyleSheet(f""" + QWidget#right_panel {{ + background-color: {Colors.BG_PRIMARY}; + }} + """) right_layout = QVBoxLayout(self.right_panel) - right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setContentsMargins(12, 12, 12, 12) + right_layout.setSpacing(12) right_layout.addWidget(self.prop) - group_list = QGroupBox("当前组件列表") + group_list = QGroupBox("📋 当前组件列表") glay = QVBoxLayout(group_list) + glay.setContentsMargins(12, 16, 12, 12) glay.addWidget(self.item_list) right_layout.addWidget(group_list) @@ -1478,20 +1525,47 @@ class MainWindow(QMainWindow): layout.setSpacing(0) self.toolbar_widget = QWidget() - self.toolbar_widget.setFixedHeight(40) # 固定工具栏高度,避免与画布之间出现多余间距 + self.toolbar_widget.setFixedHeight(52) # 增加工具栏高度 + self.toolbar_widget.setStyleSheet(f""" + QWidget#toolbar_widget {{ + background-color: {Colors.BG_SECONDARY}; + border-bottom: 1px solid {Colors.BORDER}; + }} + """) + self.toolbar_widget.setObjectName("toolbar_widget") toolbar_layout = QHBoxLayout(self.toolbar_widget) + toolbar_layout.setContentsMargins(12, 8, 12, 8) + toolbar_layout.setSpacing(8) # 保存按钮到成员,方便在其它方法(如键盘事件)中访问 / 控制可用性 + # 所有按钮使用统一样式(默认灰色样式,与influx配置按钮一致) self.btn_mode = QPushButton("展示模式") + self.btn_influx = QPushButton("Influx配置") - self.btn_add_img = QPushButton("新增图片组件") - self.btn_add_label = QPushButton("新增标签组件") - self.btn_add_web = QPushButton("新增曲线组件") - self.btn_add_arrow = QPushButton("新增箭头组件") + + self.btn_add_img = QPushButton("+ 图片") + + self.btn_add_label = QPushButton("+ 标签") + + self.btn_add_web = QPushButton("+ 曲线") + + self.btn_add_arrow = QPushButton("+ 箭头") + self.btn_full_edit = QPushButton("全屏编辑") - self.btn_delete = QPushButton("删除组件") - self.btn_clone = QPushButton("复制组件") - self.btn_save = QPushButton("保存布局") - self.btn_load = QPushButton("加载布局") + + self.btn_delete = QPushButton("删除") + + self.btn_clone = QPushButton("复制") + + self.btn_save = QPushButton("保存") + + self.btn_load = QPushButton("加载") + + # 当前文件路径显示标签 + self.lbl_current_file = QLabel("未选择文件") + self.lbl_current_file.setStyleSheet(f"color: {Colors.TEXT_SECONDARY}; font-size: 11px;") + self.lbl_current_file.setMinimumWidth(200) + self.lbl_current_file.setMaximumWidth(400) + toolbar_layout.addWidget(self.btn_mode) toolbar_layout.addWidget(self.btn_influx) toolbar_layout.addWidget(self.btn_add_img) @@ -1500,6 +1574,8 @@ class MainWindow(QMainWindow): toolbar_layout.addWidget(self.btn_add_arrow) toolbar_layout.addWidget(self.btn_full_edit) toolbar_layout.addStretch(1) + toolbar_layout.addWidget(self.lbl_current_file) + toolbar_layout.addStretch(1) toolbar_layout.addWidget(self.btn_delete) toolbar_layout.addWidget(self.btn_clone) toolbar_layout.addWidget(self.btn_save) @@ -1530,7 +1606,7 @@ class MainWindow(QMainWindow): self._shortcut_clone = QShortcut(QKeySequence("Ctrl+D"), self) self._shortcut_clone.activated.connect(self._on_clone) - # 布局 / Influx 配置文件路径 + # Influx 配置文件路径(固定位置) # 支持打包后的单文件模式:配置文件保存在 exe 同目录 if getattr(sys, 'frozen', False): # 打包后的可执行文件模式 @@ -1538,7 +1614,7 @@ class MainWindow(QMainWindow): else: # 开发模式:使用脚本所在目录 base_dir = os.path.dirname(__file__) - self._layout_path = os.path.join(base_dir, "dashboard.json") + self._layout_path = None # 初始为 None,等待用户选择文件 self._settings_path = os.path.join(base_dir, "influx_settings.json") # 先加载全局 Influx 配置,再加载布局 @@ -1794,14 +1870,44 @@ class MainWindow(QMainWindow): self._refresh_item_list() def _on_save(self): + """保存布局到当前打开的文件,如果没有指定文件则弹出保存对话框""" try: + # 如果没有指定文件路径,弹出保存对话框 + if not self._layout_path or self._layout_path == os.path.join(os.path.dirname(__file__), "dashboard.json"): + file_path, _ = QFileDialog.getSaveFileName( + self, + "保存布局文件", + "", + "JSON Files (*.json);;All Files (*.*)" + ) + if not file_path: + return # 用户取消了对话框 + if not file_path.endswith('.json'): + file_path += '.json' + self._layout_path = file_path + self._update_current_file_label() + self.view.save_layout(self._layout_path) QMessageBox.information(self, "保存布局", f"已保存到 {self._layout_path}") except Exception as e: QMessageBox.critical(self, "保存失败", str(e)) def _on_load(self): + """通过文件对话框选择并加载 JSON 文件""" try: + # 弹出文件选择对话框 + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择布局文件", + "", + "JSON Files (*.json);;All Files (*.*)" + ) + if not file_path: + return # 用户取消了对话框 + + self._layout_path = file_path + self._update_current_file_label() + self.view.load_layout(self._layout_path) self._refresh_item_list() self.view.reset_scroll() @@ -1810,6 +1916,18 @@ class MainWindow(QMainWindow): except Exception as e: QMessageBox.critical(self, "加载失败", str(e)) + def _update_current_file_label(self): + """更新工具栏上显示当前文件路径的标签""" + if self._layout_path: + # 显示完整路径,如果太长会用省略号截断 + self.lbl_current_file.setText(self._layout_path) + # 更新窗口标题显示文件名 + file_name = os.path.basename(self._layout_path) + self.setWindowTitle(f"PCM Viewer - {file_name}") + else: + self.lbl_current_file.setText("未选择文件") + self.setWindowTitle("PCM Viewer - Widgets Dashboard") + def _on_delete(self): # 删除当前选中的组件(支持多选) scene = self.view.scene_obj @@ -1955,6 +2073,10 @@ def main(): from PyQt6.QtWebEngineWidgets import QWebEngineView # 确保已导入 app = QApplication(sys.argv) + + # 应用全局样式 + apply_styles(app) + win = MainWindow() win.show() sys.exit(app.exec()) diff --git a/resources/down_arrow.svg b/resources/down_arrow.svg new file mode 100644 index 0000000..6f7c332 --- /dev/null +++ b/resources/down_arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/resources/up_arrow.svg b/resources/up_arrow.svg new file mode 100644 index 0000000..467fd17 --- /dev/null +++ b/resources/up_arrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/styles.py b/styles.py new file mode 100644 index 0000000..c626a70 --- /dev/null +++ b/styles.py @@ -0,0 +1,354 @@ +""" +PCM Viewer - 现代化UI样式定义 +主题:深色专业主题 +""" + +# ==================== 颜色定义 ==================== +class Colors: + """颜色常量定义""" + # 主背景色 + BG_PRIMARY = "#1e1e2e" # 主背景 + BG_SECONDARY = "#252537" # 次级背景(面板) + BG_TERTIARY = "#313244" # 第三级背景(输入框、按钮) + BG_HOVER = "#3a3a52" # 悬停背景 + BG_PRESSED = "#45475a" # 按下背景 + + # 强调色 + ACCENT = "#89b4fa" # 主强调色(蓝) + ACCENT_HOVER = "#b4befe" # 强调色悬停 + ACCENT_PRESSED = "#74c7ec" # 强调色按下 + + # 功能色 + SUCCESS = "#a6e3a1" # 成功(绿) + WARNING = "#f9e2af" # 警告(黄) + ERROR = "#f38ba8" # 错误(红) + INFO = "#89b4fa" # 信息(蓝) + + # 文字颜色 + TEXT_PRIMARY = "#cdd6f4" # 主文字 + TEXT_SECONDARY = "#a6adc8" # 次级文字 + TEXT_MUTED = "#6c7086" # 弱化文字 + TEXT_ON_ACCENT = "#1e1e2e" # 强调色上的文字 + + # 边框 + BORDER = "#45475a" # 普通边框 + BORDER_FOCUS = "#89b4fa" # 焦点边框 + BORDER_SUBTLE = "#313244" # 细微边框 + + +# ==================== 样式表 ==================== +MAIN_STYLE = f""" +/* ==================== 全局样式 ==================== */ +QMainWindow {{ + background-color: {Colors.BG_PRIMARY}; +}} + +QWidget {{ + color: {Colors.TEXT_PRIMARY}; + font-family: "Microsoft YaHei", "Segoe UI", "PingFang SC", sans-serif; + font-size: 13px; +}} + +/* 特定容器背景 */ +QMainWindow > QWidget {{ + background-color: {Colors.BG_PRIMARY}; +}} + +/* ==================== 按钮样式 ==================== */ +QPushButton {{ + background-color: {Colors.BG_TERTIARY}; + color: {Colors.TEXT_PRIMARY}; + border: 1px solid {Colors.BORDER}; + border-radius: 6px; + padding: 8px 16px; + font-weight: 500; + min-height: 20px; +}} + +QPushButton:hover {{ + background-color: {Colors.BG_HOVER}; + border-color: {Colors.ACCENT}; +}} + +QPushButton:pressed {{ + background-color: {Colors.BG_PRESSED}; +}} + +QPushButton:disabled {{ + background-color: {Colors.BG_SECONDARY}; + color: {Colors.TEXT_MUTED}; + border-color: {Colors.BORDER_SUBTLE}; +}} + +/* 主要操作按钮 */ +QPushButton#primary {{ + background-color: {Colors.ACCENT}; + color: {Colors.TEXT_ON_ACCENT}; + border: none; + font-weight: 600; +}} + +QPushButton#primary:hover {{ + background-color: {Colors.ACCENT_HOVER}; +}} + +QPushButton#primary:pressed {{ + background-color: {Colors.ACCENT_PRESSED}; +}} + +/* 危险操作按钮 */ +QPushButton#danger {{ + background-color: {Colors.ERROR}; + color: {Colors.TEXT_ON_ACCENT}; + border: none; +}} + +QPushButton#danger:hover {{ + background-color: #f5a0b3; +}} + +/* ==================== 输入框样式 ==================== */ +QLineEdit {{ + background-color: {Colors.BG_TERTIARY}; + color: {Colors.TEXT_PRIMARY}; + border: 1px solid {Colors.BORDER}; + border-radius: 6px; + padding: 6px 10px; + selection-background-color: {Colors.ACCENT}; +}} + +QLineEdit:focus {{ + border-color: {Colors.ACCENT}; +}} + +QLineEdit:disabled {{ + background-color: {Colors.BG_SECONDARY}; + color: {Colors.TEXT_MUTED}; +}} + +/* ==================== 数字输入框样式 ==================== */ +QSpinBox {{ + background-color: {Colors.BG_TERTIARY}; + color: {Colors.TEXT_PRIMARY}; + border: 1px solid {Colors.BORDER}; + border-radius: 6px; + padding-right: 20px; +}} + +QSpinBox:focus {{ + border-color: {Colors.ACCENT}; +}} + +/* ==================== 标签样式 ==================== */ +QLabel {{ + color: {Colors.TEXT_PRIMARY}; + background: transparent; +}} + +QLabel#title {{ + font-size: 16px; + font-weight: 600; + color: {Colors.TEXT_PRIMARY}; + padding: 4px 0; +}} + +QLabel#subtitle {{ + font-size: 12px; + color: {Colors.TEXT_SECONDARY}; +}} + +/* ==================== 分组框样式 ==================== */ +QGroupBox {{ + background-color: {Colors.BG_SECONDARY}; + border: 1px solid {Colors.BORDER}; + border-radius: 8px; + margin-top: 12px; + padding-top: 12px; + padding: 12px; + font-weight: 500; +}} + +QGroupBox::title {{ + subcontrol-origin: margin; + left: 12px; + padding: 0 8px; + color: {Colors.ACCENT}; +}} + +/* ==================== 列表样式 ==================== */ +QListWidget {{ + background-color: {Colors.BG_TERTIARY}; + color: {Colors.TEXT_PRIMARY}; + border: 1px solid {Colors.BORDER}; + border-radius: 6px; + padding: 4px; + outline: none; +}} + +QListWidget::item {{ + padding: 8px 12px; + border-radius: 4px; + margin: 2px 0; +}} + +QListWidget::item:hover {{ + background-color: {Colors.BG_HOVER}; +}} + +QListWidget::item:selected {{ + background-color: {Colors.ACCENT}; + color: {Colors.TEXT_ON_ACCENT}; +}} + +/* ==================== 复选框样式 ==================== */ +QCheckBox {{ + color: {Colors.TEXT_PRIMARY}; + spacing: 8px; +}} + +QCheckBox::indicator {{ + width: 18px; + height: 18px; + border-radius: 4px; + border: 2px solid {Colors.BORDER}; + background-color: {Colors.BG_TERTIARY}; +}} + +QCheckBox::indicator:hover {{ + border-color: {Colors.ACCENT}; +}} + +QCheckBox::indicator:checked {{ + background-color: {Colors.ACCENT}; + border-color: {Colors.ACCENT}; + image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTIiIGhlaWdodD0iMTIiIHZpZXdCb3g9IjAgMCAxMiAxMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTAgM0w0LjUgOC41TDIgNiIgc3Ryb2tlPSIjMWUxZTJlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg==); +}} + +/* ==================== 滚动条样式 ==================== */ +QScrollBar:vertical {{ + background-color: {Colors.BG_SECONDARY}; + width: 12px; + border-radius: 6px; +}} + +QScrollBar::handle:vertical {{ + background-color: {Colors.BG_TERTIARY}; + border-radius: 6px; + min-height: 30px; +}} + +QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ + height: 0px; +}} + +QScrollBar:horizontal {{ + background-color: {Colors.BG_SECONDARY}; + height: 12px; + border-radius: 6px; +}} + +QScrollBar::handle:horizontal {{ + background-color: {Colors.BG_TERTIARY}; + border-radius: 6px; + min-width: 30px; +}} + +QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ + width: 0px; +}} + +/* ==================== 分割器样式 ==================== */ +QSplitter::handle {{ + background-color: {Colors.BORDER}; +}} + +QSplitter::handle:horizontal {{ + width: 2px; +}} + +QSplitter::handle:vertical {{ + height: 2px; +}} + +QSplitter::handle:hover {{ + background-color: {Colors.ACCENT}; +}} + +/* ==================== 对话框样式 ==================== */ +QDialog {{ + background-color: {Colors.BG_PRIMARY}; +}} + +QDialogButtonBox QPushButton {{ + min-width: 80px; +}} + +/* ==================== 消息框样式 ==================== */ +QMessageBox {{ + background-color: {Colors.BG_PRIMARY}; +}} + +QMessageBox QLabel {{ + color: {Colors.TEXT_PRIMARY}; +}} + +/* ==================== 文件对话框样式 ==================== */ +QFileDialog {{ + background-color: {Colors.BG_PRIMARY}; +}} + +/* ==================== 颜色选择对话框样式 ==================== */ +QColorDialog {{ + background-color: {Colors.BG_PRIMARY}; +}} + +/* ==================== 纯文本编辑框样式 ==================== */ +QPlainTextEdit {{ + background-color: {Colors.BG_TERTIARY}; + color: {Colors.TEXT_PRIMARY}; + border: 1px solid {Colors.BORDER}; + border-radius: 6px; + padding: 8px; + font-family: "Consolas", "Monaco", "Courier New", monospace; + selection-background-color: {Colors.ACCENT}; +}} + +QPlainTextEdit:focus {{ + border-color: {Colors.ACCENT}; +}} + +/* ==================== 表单布局样式 ==================== */ +QFormLayout QLabel {{ + color: {Colors.TEXT_SECONDARY}; + padding-right: 12px; +}} + +/* ==================== 工具提示样式 ==================== */ +QToolTip {{ + background-color: {Colors.BG_SECONDARY}; + color: {Colors.TEXT_PRIMARY}; + border: 1px solid {Colors.BORDER}; + border-radius: 4px; + padding: 6px 10px; +}} +""" + + +# ==================== 画布样式 ==================== +CANVAS_STYLE = { + 'background': Colors.BG_SECONDARY, + 'grid_color': Colors.BORDER_SUBTLE, + 'selection_border': Colors.ACCENT, + 'selection_fill': f"{Colors.ACCENT}33", # 20%透明度 + 'canvas_border': Colors.BORDER, +} + + +# ==================== 组件默认样式 ==================== +ITEM_STYLE = { + 'default_border': Colors.BORDER, + 'selected_border': Colors.ACCENT, + 'hover_border': Colors.ACCENT_HOVER, + 'resize_handle': Colors.ACCENT, + 'resize_handle_size': 8, +}