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,
+}