diff --git a/.gitignore b/.gitignore
index f2310ce4cb3fb3b9c55f237baccb8793456990ed..7a85b3c7f0677b9a48bb6ecb33a228a73f10a6c3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -89,3 +89,8 @@ md转docx.py
生成源代码文档_v3.py
生成源代码文档_v4.py
生成源代码文档_v5.py
+文档/使用说明.txt
+文档/color_card_20260313_142803.json
+文档/color_card_20260313_161405.json
+/测试
+文档/代码审查报告-26.03.md
diff --git a/README.md b/README.md
index 93466baf473b343d557918893b2603ada45db4a6..2f3aa080c93813e55a02227691e5cfc8402134c6 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,8 @@
## 使用说明
+📺 **视频教程**:[取色卡(Color Card)使用说明](https://www.bilibili.com/video/BV1vpckzhEH8/)
+
### 基本操作
1. **导入图片**:点击「色彩提取」或「明度提取」面板中的图片显示区域即可导入图片,也支持拖拽导入
@@ -362,6 +364,8 @@ Unlike common color tools or websites that only provide a single function, Color
## Usage
+📺 **Video Tutorial**: [Color Card Tutorial (in Chinese)](https://www.bilibili.com/video/BV1vpckzhEH8/)
+
### Basic Operations
1. **Import Image**: Click "Open Image" button, supports drag-and-drop
diff --git a/dialogs/about_dialog.py b/dialogs/about_dialog.py
index 49cfd3f8fd7607b0e547fc57365514daba204abe..1df6dcd24c7e8fe8da9b3e9707eb82abc3f03ce4 100644
--- a/dialogs/about_dialog.py
+++ b/dialogs/about_dialog.py
@@ -9,7 +9,7 @@ from PySide6.QtGui import QDesktopServices
from PySide6.QtWidgets import (
QDialog, QFrame, QHBoxLayout, QVBoxLayout, QWidget
)
-from qfluentwidgets import CaptionLabel, PlainTextEdit, PrimaryPushButton, PushButton, isDarkTheme, qconfig
+from qfluentwidgets import CaptionLabel, PlainTextEdit, PushButton, isDarkTheme, qconfig
# 项目模块导入
from utils import tr, fix_windows_taskbar_icon_for_window, load_icon_universal, set_window_title_bar_theme
@@ -44,7 +44,7 @@ class AboutDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle(tr('dialogs.about.title'))
- self.setFixedSize(600, 550)
+ self.setFixedSize(700, 550)
# 设置窗口图标
self.setWindowIcon(load_icon_universal())
@@ -142,8 +142,8 @@ class AboutDialog(QDialog):
)
buttons_layout.addWidget(self.homepage_button)
- # 项目地址按钮(主题色)
- self.project_button = PrimaryPushButton(tr('dialogs.about.project'))
+ # 项目地址按钮
+ self.project_button = PushButton(tr('dialogs.about.project'))
self.project_button.setMinimumWidth(90)
self.project_button.clicked.connect(
lambda: self._open_url("https://gitee.com/qingshangongzai/color_card")
@@ -161,7 +161,15 @@ class AboutDialog(QDialog):
self.agreement_button.setMinimumWidth(90)
self.agreement_button.clicked.connect(self._open_agreement_file)
buttons_layout.addWidget(self.agreement_button)
-
+
+ # 使用说明按钮
+ self.tutorial_button = PushButton(tr('dialogs.about.tutorial'))
+ self.tutorial_button.setMinimumWidth(90)
+ self.tutorial_button.clicked.connect(
+ lambda: self._open_url("https://www.bilibili.com/video/BV1vpckzhEH8/")
+ )
+ buttons_layout.addWidget(self.tutorial_button)
+
buttons_layout.addStretch()
parent_layout.addWidget(buttons_container)
diff --git a/dialogs/edit_palette.py b/dialogs/edit_palette.py
index f32d065e2c701e918453d939c65644e54b61f6cb..be6239e8e3c85785cfbe34ed7c0a3f68ac6ceb19 100644
--- a/dialogs/edit_palette.py
+++ b/dialogs/edit_palette.py
@@ -951,17 +951,19 @@ class ColorInputRow(QWidget):
class EditPaletteDialog(QDialog):
"""编辑配色对话框"""
- def __init__(self, default_name="", palette_data=None, parent=None):
+ def __init__(self, default_name="", palette_data=None, parent=None, show_name_input=True):
"""初始化添加/编辑配色对话框
Args:
default_name: 默认配色名称
palette_data: 已有配色数据(编辑模式),None表示添加模式
parent: 父窗口
+ show_name_input: 是否显示名称输入区域(默认True)
"""
super().__init__(parent)
self._palette_data = palette_data
self._is_edit_mode = palette_data is not None
+ self._show_name_input = show_name_input
self.setWindowTitle("编辑配色" if self._is_edit_mode else "添加配色")
self.setFixedSize(300, 400)
self._default_name = default_name
@@ -1010,25 +1012,26 @@ class EditPaletteDialog(QDialog):
layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(15)
- # 名称输入区域
- name_layout = QHBoxLayout()
- name_label = QLabel("配色名称:")
- name_label.setStyleSheet(f"color: {get_text_color().name()}; font-size: 13px;")
- name_layout.addWidget(name_label)
-
- self.name_input = LineEdit()
- self.name_input.setText(self._default_name)
- self.name_input.setPlaceholderText("输入配色名称...")
- self.name_input.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
- self.name_input.setClearButtonEnabled(True)
- name_layout.addWidget(self.name_input)
- layout.addLayout(name_layout)
-
- # 分隔线
- separator = QLabel()
- separator.setFixedHeight(1)
- separator.setStyleSheet(f"background-color: {get_border_color().name()};")
- layout.addWidget(separator)
+ # 名称输入区域(根据参数决定是否显示)
+ if self._show_name_input:
+ name_layout = QHBoxLayout()
+ name_label = QLabel("配色名称:")
+ name_label.setStyleSheet(f"color: {get_text_color().name()}; font-size: 13px;")
+ name_layout.addWidget(name_label)
+
+ self.name_input = LineEdit()
+ self.name_input.setText(self._default_name)
+ self.name_input.setPlaceholderText("输入配色名称...")
+ self.name_input.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
+ self.name_input.setClearButtonEnabled(True)
+ name_layout.addWidget(self.name_input)
+ layout.addLayout(name_layout)
+
+ # 分隔线
+ separator = QLabel()
+ separator.setFixedHeight(1)
+ separator.setStyleSheet(f"background-color: {get_border_color().name()};")
+ layout.addWidget(separator)
# 颜色列表标题
colors_title = QLabel(tr('dialogs.edit_palette.colors_title'))
@@ -1080,9 +1083,10 @@ class EditPaletteDialog(QDialog):
layout.addLayout(buttons_layout)
- # 设置焦点到名称输入框
- self.name_input.setFocus()
- self.name_input.selectAll()
+ # 设置焦点到名称输入框(如果存在)
+ if self._show_name_input:
+ self.name_input.setFocus()
+ self.name_input.selectAll()
# 添加第一个颜色输入行(不滚动)
self._on_add_color(scroll_to_bottom=False)
@@ -1126,7 +1130,7 @@ class EditPaletteDialog(QDialog):
# 设置名称
name = self._palette_data.get('name', '')
- if name:
+ if name and self._show_name_input:
self.name_input.setText(name)
# 获取已有颜色
@@ -1168,7 +1172,7 @@ class EditPaletteDialog(QDialog):
return
# 获取名称
- name = self.name_input.text().strip()
+ name = self.name_input.text().strip() if self._show_name_input else ""
if not name:
name = self._default_name
diff --git a/docs/changelog.json b/docs/changelog.json
index 7cccaad092e988bac763cf71bf6088f3d455640d..aa413241b212e93d94497f8d33b62ef91e16b93e 100644
--- a/docs/changelog.json
+++ b/docs/changelog.json
@@ -1,5 +1,43 @@
{
"versions": [
+ {
+ "version": "v1.5.1",
+ "date": "2026-03-15",
+ "changes": [
+ {
+ "category": "新增功能",
+ "items": [
+ "新增使用说明视频教程入口"
+ ]
+ },
+ {
+ "category": "问题修复",
+ "items": [
+ "修复渐变提取HEX输入框可输入非法字符的问题"
+ ]
+ },
+ {
+ "category": "界面优化",
+ "items": [
+ "预览配色编辑对话框隐藏名称输入区域",
+ "配色管理卡片添加序号显示"
+ ]
+ },
+ {
+ "category": "内容调整",
+ "items": [
+ "重命名设置分组「色卡显示设置」为「颜色信息」",
+ "修正简体中文配置文件中的错误标题名称"
+ ]
+ },
+ {
+ "category": "性能提升",
+ "items": [
+ "优化配色组删除性能,避免重新加载导致的卡顿"
+ ]
+ }
+ ]
+ },
{
"version": "v1.5.0",
"date": "2026-03-08",
diff --git a/docs/index.html b/docs/index.html
index 1b728d0a2a3fd6e073d2f73cacc86691b165ca79..e5ad4931d9ac4767634c145d4567ac81ce3b2e3f 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -484,6 +484,7 @@
项目起源
下载
更新日志
+ 使用说明
反馈
关于我们
@@ -675,7 +676,7 @@
立即开始使用
无需联网、无需注册、隐私安全、一步即达
-
+
@@ -701,6 +702,17 @@
Gitee
+
+
+
使用说明
+
视频教程,快速上手
+
+
+ 观看教程
+
+
@@ -1029,6 +1041,7 @@
diff --git a/locales/FR_FR.json b/locales/FR_FR.json
index ebfd30201d7c08db7d75b0285ebe81bd767230d8..9d0ac79b984504b0d4983ed6e1ca6a9d0beae8a3 100644
--- a/locales/FR_FR.json
+++ b/locales/FR_FR.json
@@ -193,7 +193,8 @@
},
"settings": {
"title": "Paramètres",
- "card_display": "Paramètres d'affichage des cartes",
+ "card_display": "Paramètres d'infos couleur",
+ "card_display_desc": "Configurer l'affichage des valeurs et modes de couleur",
"hex_display": "Afficher la valeur HEX",
"hex_display_desc": "Afficher la valeur HEX et le bouton de copie dans les cartes de couleurs",
"color_mode": "Affichage du mode couleur",
@@ -251,7 +252,8 @@
"homepage": "Page d'accueil",
"project": "Projet",
"license": "Licence",
- "agreement": "Contrat utilisateur",
+ "agreement": "Accord utilisateur",
+ "tutorial": "Tutoriel",
"copyright": "Droits d'auteur",
"license_note": "Sous licence GPL v3, uniquement à des fins éducatives"
},
diff --git a/locales/JA_JP.json b/locales/JA_JP.json
index fdae388bb205274b4d7ccbdbf154a1bab0cd8def..b20d7b62ac7749af5c4e4fea0449694a743099ff 100644
--- a/locales/JA_JP.json
+++ b/locales/JA_JP.json
@@ -193,7 +193,8 @@
},
"settings": {
"title": "設定",
- "card_display": "カード表示設定",
+ "card_display": "カラー情報設定",
+ "card_display_desc": "カラー値とカラーモードの表示方法を設定",
"hex_display": "HEXカラー値を表示",
"hex_display_desc": "カラーカードにHEXカラー値とコピーボタンを表示",
"color_mode": "カラーモード表示",
@@ -252,6 +253,7 @@
"project": "プロジェクト",
"license": "ライセンス",
"agreement": "利用規約",
+ "tutorial": "使い方",
"copyright": "著作権",
"license_note": "GPL v3の下でライセンスされています。教育目的のみです"
},
diff --git a/locales/RU_RU.json b/locales/RU_RU.json
index 21dc775ae4ee3646539fa5d42cbbad80e5c154a6..58af60fc789445cfeea4754adf1b07e3eee2fb41 100644
--- a/locales/RU_RU.json
+++ b/locales/RU_RU.json
@@ -193,7 +193,8 @@
},
"settings": {
"title": "Настройки",
- "card_display": "Настройки отображения карточек",
+ "card_display": "Настройки цветовой информации",
+ "card_display_desc": "Настройка отображения цветовых значений и цветовых режимов",
"hex_display": "Показывать HEX значение",
"hex_display_desc": "Отображать HEX значение цвета и кнопку копирования в цветовых карточках",
"color_mode": "Отображение цветового режима",
@@ -252,6 +253,7 @@
"project": "Проект",
"license": "Лицензия",
"agreement": "Пользовательское соглашение",
+ "tutorial": "Руководство",
"copyright": "Авторские права",
"license_note": "Лицензировано под GPL v3, только для образовательных целей"
},
diff --git a/locales/ZW_FT.json b/locales/ZW_FT.json
index 418d3eba7ab641d30728444d63a6fc5908ef0f65..53d0cff2feadc4381c2304446d04930388e49a87 100644
--- a/locales/ZW_FT.json
+++ b/locales/ZW_FT.json
@@ -193,7 +193,8 @@
},
"settings": {
"title": "設置",
- "card_display": "色卡顯示設置",
+ "card_display": "顏色信息設置",
+ "card_display_desc": "配置顏色值和色彩模式的顯示方式",
"hex_display": "顯示16進制顏色值",
"hex_display_desc": "在色彩提取面板的色卡中顯示16進制顏色值和複製按鈕",
"color_mode": "色彩模式顯示",
@@ -252,6 +253,7 @@
"project": "項目地址",
"license": "開源許可",
"agreement": "用戶協議",
+ "tutorial": "使用說明",
"copyright": "版權所有",
"license_note": "基於 GPL v3 開源,僅供學習交流使用"
},
diff --git a/locales/ZW_JT.json b/locales/ZW_JT.json
index 0da52d8fc67e193407da6425cbbae1425f22716d..16672d745048efd60dbb53c0ac4a5a0f0e346718 100644
--- a/locales/ZW_JT.json
+++ b/locales/ZW_JT.json
@@ -176,7 +176,7 @@
"hue_distribution_title": "色相分布"
},
"preset_color": {
- "title": "预设颜色",
+ "title": "内置色彩",
"random_palette": "随机配色",
"random": "随机",
"desc_random": "从所有配色方案中随机筛选",
@@ -193,7 +193,8 @@
},
"settings": {
"title": "设置",
- "card_display": "色卡显示设置",
+ "card_display": "颜色信息设置",
+ "card_display_desc": "配置颜色值和色彩模式的显示方式",
"hex_display": "显示16进制颜色值",
"hex_display_desc": "在色彩提取面板的色卡中显示16进制颜色值和复制按钮",
"color_mode": "色彩模式显示",
@@ -252,6 +253,7 @@
"project": "项目地址",
"license": "开源许可",
"agreement": "用户协议",
+ "tutorial": "使用说明",
"copyright": "版权所有",
"license_note": "基于 GPL v3 开源,仅供学习交流使用"
},
diff --git a/locales/en_US.json b/locales/en_US.json
index ca1c89415f4e8f65110fbf8bcbf7a8f5f0078daa..1ecb3cb99449fa785622c7295bce9bdef127eede 100644
--- a/locales/en_US.json
+++ b/locales/en_US.json
@@ -193,7 +193,8 @@
},
"settings": {
"title": "Settings",
- "card_display": "Card Display Settings",
+ "card_display": "Color Info Settings",
+ "card_display_desc": "Configure how color values and color modes are displayed",
"hex_display": "Show HEX Color Value",
"hex_display_desc": "Display HEX color value and copy button in color cards",
"color_mode": "Color Mode Display",
@@ -252,6 +253,7 @@
"project": "Project",
"license": "License",
"agreement": "User Agreement",
+ "tutorial": "Tutorial",
"copyright": "Copyright",
"license_note": "Licensed under GPL v3, for educational purposes only"
},
diff --git a/ui/color_preview.py b/ui/color_preview.py
index 3e2976e09ea8839440079b7a40cac5217b033592..b0b5b3709f407996dc411081390182ddc90487bc 100644
--- a/ui/color_preview.py
+++ b/ui/color_preview.py
@@ -177,8 +177,19 @@ class DraggableColorDot(QWidget):
def _copy_hex_to_clipboard(self):
"""复制HEX值到剪贴板"""
clipboard = QApplication.clipboard()
- clipboard.setText(self._color.upper())
- log_user_action("copy_hex_to_clipboard", {"color": self._color.upper()})
+ hex_value = self._color.upper()
+ clipboard.setText(hex_value)
+ log_user_action("copy_hex_to_clipboard", {"color": hex_value})
+
+ InfoBar.success(
+ title=tr('messages.copy_success.title'),
+ content=tr('messages.copy_success.content', value=hex_value),
+ orient=Qt.Orientation.Horizontal,
+ isClosable=True,
+ position=InfoBarPosition.TOP,
+ duration=2000,
+ parent=self.window()
+ )
# ============================================================================
@@ -294,8 +305,8 @@ class ColorDotBar(QWidget):
'colors': colors_info
}
- # 打开编辑对话框
- dialog = EditPaletteDialog(palette_data=palette_data, parent=self)
+ # 打开编辑对话框(预览配色场景不显示名称输入)
+ dialog = EditPaletteDialog(palette_data=palette_data, parent=self, show_name_input=False)
if dialog.exec() == EditPaletteDialog.DialogCode.Accepted:
new_palette_data = dialog.get_palette_data()
if new_palette_data and 'colors' in new_palette_data:
diff --git a/ui/gradient_extract.py b/ui/gradient_extract.py
index 24ed6a8873f1c2da787bef4a492ffe35f92b6005..271da410c4192e4c51c8582b3e47c8fdcc43be01 100644
--- a/ui/gradient_extract.py
+++ b/ui/gradient_extract.py
@@ -308,7 +308,10 @@ class GradientExtractInterface(QWidget):
self.start_color_input.setFixedHeight(28)
self.start_color_input.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.start_color_input.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
- self.start_color_input.textChanged.connect(self._on_start_color_input_changed)
+ self.start_color_input.setMaxLength(7) # #RRGGBB 格式
+ self.start_color_input.setPlaceholderText("#RRGGBB")
+ self.start_color_input.textChanged.connect(self._on_start_color_text_changed)
+ self.start_color_input.editingFinished.connect(self._on_start_color_editing_finished)
self.start_color_dot.clicked.connect(self._open_start_color_picker)
start_color_layout.addStretch()
start_color_layout.addWidget(self.start_color_dot)
@@ -328,7 +331,10 @@ class GradientExtractInterface(QWidget):
self.end_color_input.setFixedHeight(28)
self.end_color_input.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.end_color_input.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
- self.end_color_input.textChanged.connect(self._on_end_color_input_changed)
+ self.end_color_input.setMaxLength(7) # #RRGGBB 格式
+ self.end_color_input.setPlaceholderText("#RRGGBB")
+ self.end_color_input.textChanged.connect(self._on_end_color_text_changed)
+ self.end_color_input.editingFinished.connect(self._on_end_color_editing_finished)
self.end_color_dot.clicked.connect(self._open_end_color_picker)
end_color_layout.addStretch()
end_color_layout.addWidget(self.end_color_dot)
@@ -447,21 +453,115 @@ class GradientExtractInterface(QWidget):
self.random_button.setText(tr('gradient_extract.random'))
self.favorite_button.setText(tr('gradient_extract.favorite'))
- def _on_start_color_input_changed(self, text: str):
- """起始颜色输入框改变"""
- if self._is_valid_hex(text):
- self._start_color = text.upper()
- self.start_color_dot.set_color(self._start_color)
- log_user_action("change_start_color", {"color": self._start_color})
- self._generate_gradient()
-
- def _on_end_color_input_changed(self, text: str):
- """结束颜色输入框改变"""
- if self._is_valid_hex(text):
- self._end_color = text.upper()
- self.end_color_dot.set_color(self._end_color)
- log_user_action("change_end_color", {"color": self._end_color})
- self._generate_gradient()
+ def _on_start_color_text_changed(self, text: str):
+ """起始颜色文本变化处理(自动格式化为大写并确保#前缀)"""
+ if not text:
+ return
+
+ # 自动转大写
+ upper_text = text.upper()
+
+ # 只允许有效的十六进制字符和#
+ valid_chars = '#0123456789ABCDEF'
+ filtered_text = ''.join(c for c in upper_text if c in valid_chars)
+
+ # 确保以#开头
+ if not filtered_text.startswith('#'):
+ filtered_text = '#' + filtered_text
+
+ # 限制长度(# + 6位十六进制)
+ if len(filtered_text) > 7:
+ filtered_text = filtered_text[:7]
+
+ # 更新文本(如果发生变化)
+ if text != filtered_text:
+ cursor_pos = self.start_color_input.cursorPosition()
+ self.start_color_input.setText(filtered_text)
+ # 保持光标位置
+ new_pos = min(len(filtered_text), cursor_pos + (1 if not text.startswith('#') else 0))
+ self.start_color_input.setCursorPosition(new_pos)
+
+ def _on_start_color_editing_finished(self):
+ """起始颜色编辑完成处理(验证并更新颜色)"""
+ text = self.start_color_input.text().strip().upper()
+
+ if not text:
+ return
+
+ # 添加 # 前缀
+ if not text.startswith('#'):
+ text = '#' + text
+
+ # 验证HEX格式
+ if not self._is_valid_hex(text):
+ # 无效则恢复原值
+ self.start_color_input.setText(self._start_color)
+ return
+
+ # 如果颜色没有变化,不进行处理
+ if text == self._start_color:
+ return
+
+ # 更新颜色
+ self._start_color = text
+ self.start_color_dot.set_color(self._start_color)
+ log_user_action("change_start_color", {"color": self._start_color})
+ self._generate_gradient()
+
+ def _on_end_color_text_changed(self, text: str):
+ """结束颜色文本变化处理(自动格式化为大写并确保#前缀)"""
+ if not text:
+ return
+
+ # 自动转大写
+ upper_text = text.upper()
+
+ # 只允许有效的十六进制字符和#
+ valid_chars = '#0123456789ABCDEF'
+ filtered_text = ''.join(c for c in upper_text if c in valid_chars)
+
+ # 确保以#开头
+ if not filtered_text.startswith('#'):
+ filtered_text = '#' + filtered_text
+
+ # 限制长度(# + 6位十六进制)
+ if len(filtered_text) > 7:
+ filtered_text = filtered_text[:7]
+
+ # 更新文本(如果发生变化)
+ if text != filtered_text:
+ cursor_pos = self.end_color_input.cursorPosition()
+ self.end_color_input.setText(filtered_text)
+ # 保持光标位置
+ new_pos = min(len(filtered_text), cursor_pos + (1 if not text.startswith('#') else 0))
+ self.end_color_input.setCursorPosition(new_pos)
+
+ def _on_end_color_editing_finished(self):
+ """结束颜色编辑完成处理(验证并更新颜色)"""
+ text = self.end_color_input.text().strip().upper()
+
+ if not text:
+ return
+
+ # 添加 # 前缀
+ if not text.startswith('#'):
+ text = '#' + text
+
+ # 验证HEX格式
+ if not self._is_valid_hex(text):
+ # 无效则恢复原值
+ self.end_color_input.setText(self._end_color)
+ return
+
+ # 如果颜色没有变化,不进行处理
+ if text == self._end_color:
+ return
+
+ # 更新颜色
+ self._end_color = text
+ self.end_color_dot.set_color(self._end_color)
+ log_user_action("change_end_color", {"color": self._end_color})
+ self._generate_gradient()
def _open_start_color_picker(self):
"""打开起始颜色选择器"""
diff --git a/ui/palette_management.py b/ui/palette_management.py
index 624e74309038bf84cc0906ade1d4e2adc996cead..8565a1e49e73157277c08a4fe8a061a89674598f 100644
--- a/ui/palette_management.py
+++ b/ui/palette_management.py
@@ -468,8 +468,9 @@ class PaletteManagementCard(CardWidget):
export_ase_requested = Signal(dict)
MAX_COLORS_PER_ROW = 6
- def __init__(self, favorite_data: Dict[str, Any], parent=None):
+ def __init__(self, favorite_data: Dict[str, Any], card_index: int = 0, parent=None):
self._favorite_data = favorite_data
+ self._card_index = card_index
self._hex_visible = True
self._color_modes = ['HSB', 'LAB']
self._color_cards = []
@@ -635,7 +636,8 @@ class PaletteManagementCard(CardWidget):
def _load_favorite_data(self):
"""加载收藏数据"""
- self.name_label.setText(self._favorite_data.get('name', tr('palette_management.unnamed')))
+ name = self._favorite_data.get('name', tr('palette_management.unnamed'))
+ self.name_label.setText(f"{self._card_index + 1}.{name}")
created_at = self._favorite_data.get('created_at', '')
if created_at:
@@ -739,6 +741,7 @@ class PaletteManagementList(QWidget):
self._groups = []
self._current_group_index = 0
self._loader = None
+ self._current_group_indices = [] # 当前分组的索引列表
super().__init__(parent)
self.setup_ui()
qconfig.themeChangedFinished.connect(self._update_styles)
@@ -839,47 +842,52 @@ class PaletteManagementList(QWidget):
"""加载当前分组的数据"""
self._cancel_loader()
self._clear_cards()
-
+
if not self._groups or self._current_group_index >= len(self._groups):
return
-
+
current_group = self._groups[self._current_group_index]
- group_indices = current_group.get("indices", [])
-
- if len(group_indices) > self.BATCH_THRESHOLD:
- self._start_batch_loading(group_indices)
+ self._current_group_indices = current_group.get("indices", [])
+
+ if len(self._current_group_indices) > self.BATCH_THRESHOLD:
+ self._start_batch_loading(self._current_group_indices)
else:
- self._load_group_directly(group_indices)
+ self._load_group_directly(self._current_group_indices)
- def _load_group_directly(self, group_indices: list):
- """直接加载分组数据(小数据量)
+ def _create_palette_card(self, favorite: Dict[str, Any], card_index: int) -> PaletteManagementCard:
+ """创建配色卡片并设置连接
Args:
- group_indices: 分组索引列表
+ favorite: 收藏数据
+ card_index: 卡片索引(用于显示序号)
+
+ Returns:
+ PaletteManagementCard: 创建的卡片
"""
- # 禁用UI更新,批量处理
- self.content_widget.setUpdatesEnabled(False)
+ card = PaletteManagementCard(favorite, card_index=card_index)
+ card.set_hex_visible(self._hex_visible)
+ card.set_color_modes(self._color_modes)
+ card.delete_requested.connect(self.favorite_deleted)
+ card.preview_requested.connect(self._on_preview_requested)
+ card.contrast_requested.connect(self._on_contrast_requested)
+ card.color_changed.connect(self._on_color_changed)
+ card.preview_in_panel_requested.connect(self._on_preview_in_panel_requested)
+ card.edit_requested.connect(self._on_edit_requested)
+ card.export_ase_requested.connect(self._on_export_ase_requested)
+ return card
+ def _load_group_directly(self, group_indices: list):
+ """直接加载分组数据(小数据量)"""
+ self.content_widget.setUpdatesEnabled(False)
try:
for idx in group_indices:
if 0 <= idx < len(self._favorites):
favorite = self._favorites[idx]
- card = PaletteManagementCard(favorite)
- card.set_hex_visible(self._hex_visible)
- card.set_color_modes(self._color_modes)
- card.delete_requested.connect(self.favorite_deleted)
- card.preview_requested.connect(self._on_preview_requested)
- card.contrast_requested.connect(self._on_contrast_requested)
- card.color_changed.connect(self._on_color_changed)
- card.preview_in_panel_requested.connect(self._on_preview_in_panel_requested)
- card.edit_requested.connect(self._on_edit_requested)
- card.export_ase_requested.connect(self._on_export_ase_requested)
+ card = self._create_palette_card(favorite, idx)
self.content_layout.addWidget(card)
self._favorite_cards[favorite.get('id', '')] = card
-
self.content_layout.addStretch()
finally:
- # 恢复UI更新
self.content_widget.setUpdatesEnabled(True)
def _start_batch_loading(self, group_indices: List[int]):
@@ -896,26 +904,13 @@ class PaletteManagementList(QWidget):
self._loader.start()
def _on_batch_data_ready(self, batch_idx: int, batch_data: List[Dict[str, Any]]):
- """批次数据就绪回调
-
- Args:
- batch_idx: 批次索引
- batch_data: 批次数据列表
- """
- for favorite in batch_data:
- card = PaletteManagementCard(favorite)
- card.set_hex_visible(self._hex_visible)
- card.set_color_modes(self._color_modes)
- card.delete_requested.connect(self.favorite_deleted)
- card.preview_requested.connect(self._on_preview_requested)
- card.contrast_requested.connect(self._on_contrast_requested)
- card.color_changed.connect(self._on_color_changed)
- card.preview_in_panel_requested.connect(self._on_preview_in_panel_requested)
- card.edit_requested.connect(self._on_edit_requested)
- card.export_ase_requested.connect(self._on_export_ase_requested)
+ """批次数据就绪回调"""
+ start_index = batch_idx * self.BATCH_SIZE
+ for i, favorite in enumerate(batch_data):
+ global_index = self._current_group_indices[start_index + i]
+ card = self._create_palette_card(favorite, global_index)
self.content_layout.addWidget(card)
self._favorite_cards[favorite.get('id', '')] = card
-
QApplication.processEvents()
def _on_loading_finished(self):
@@ -1058,6 +1053,38 @@ class PaletteManagementList(QWidget):
except RuntimeError as e:
logger.debug(f"Hint label already deleted: {e}")
+ def remove_favorite_card(self, favorite_id: str) -> bool:
+ """局部删除指定收藏卡片,其他卡片序号保持不变"""
+ card = self._favorite_cards.get(favorite_id)
+ if not card:
+ return False
+
+ deleted_idx = next((i for i, fav in enumerate(self._favorites)
+ if fav.get('id', '') == favorite_id), -1)
+ if deleted_idx < 0:
+ return False
+
+ self.content_layout.removeWidget(card)
+ card.deleteLater()
+ del self._favorite_cards[favorite_id]
+ self._favorites.pop(deleted_idx)
+
+ if deleted_idx in self._current_group_indices:
+ self._current_group_indices.remove(deleted_idx)
+
+ if not self._current_group_indices:
+ if self._current_group_index > 0:
+ self._current_group_index -= 1
+ elif self._current_group_index < len(self._groups) - 1:
+ self._current_group_index += 1
+ else:
+ self._show_empty_state()
+ return True
+ self.group_changed.emit(self._current_group_index)
+ self._load_current_group()
+
+ return True
+
# =============================================================================
# 配色管理界面
@@ -1225,7 +1252,10 @@ class PaletteManagementInterface(QWidget):
log_user_action("clear_all_favorites", {"count": favorites_count}, "success")
def _on_favorite_deleted(self, favorite_id):
- """收藏删除回调"""
+ """收藏删除回调(优化版)
+
+ 使用局部刷新替代完整重新加载,避免卡顿。
+ """
favorite = self._config_manager.get_favorite(favorite_id)
palette_name = favorite.get('name', tr('palette_management.unnamed')) if favorite else tr('palette_management.unnamed')
@@ -1240,7 +1270,14 @@ class PaletteManagementInterface(QWidget):
if msg_box.exec():
self._config_manager.delete_favorite(favorite_id)
self._config_manager.save()
- self._load_favorites()
+
+ # 局部刷新:仅移除对应卡片,不重新加载
+ success = self.palette_management_list.remove_favorite_card(favorite_id)
+
+ if not success:
+ # 局部删除失败时回退到完整刷新
+ self._load_favorites()
+
log_user_action("delete_favorite", {"id": favorite_id, "name": palette_name}, "success")
def _on_import_clicked(self):
diff --git a/version.py b/version.py
index b042171aa66526bc940b3b04727038b6bf3120d3..a22ae180cb398401c6e642542a7e676931d2bcff 100644
--- a/version.py
+++ b/version.py
@@ -9,7 +9,7 @@ class VersionManager:
# 版本号组件
self.major: int = 1
self.minor: int = 5
- self.patch: int = 0
+ self.patch: int = 1
self.build: int = 0
self.prerelease: str = ""
diff --git a/version.txt b/version.txt
index 9e9b5a71216568e2b830ca48b9c6e1b987329d80..ab7dd437beb6065f1915ee9fdc567fca700d6930 100644
--- a/version.txt
+++ b/version.txt
@@ -1,6 +1,6 @@
-1.5.0
-2026.3.8.1
-1.5.0.0
+1.5.1
+2026.3.15.1
+1.5.1.0
浮晓 HXiao Studio
© 2026 浮晓 HXiao Studio
取色卡 - Color Card
\ No newline at end of file
diff --git a/version_info.txt b/version_info.txt
index 92cc9a49fcb05af36b4d5f91ad0a2d93517a1fac..691c4f15d21f4e7952ef9e5c9022e7ca673a85e4 100644
--- a/version_info.txt
+++ b/version_info.txt
@@ -1,7 +1,7 @@
VSVersionInfo(
ffi=FixedFileInfo(
- filevers=(2026,3,8,1),
- prodvers=(1,5,0,0),
+ filevers=(2026,3,15,1),
+ prodvers=(1,5,1,0),
mask=0x3f,
flags=0x0,
OS=0x4,
@@ -17,12 +17,12 @@ VSVersionInfo(
[
StringStruct(u'CompanyName', u'浮晓 HXiao Studio'),
StringStruct(u'FileDescription', u'取色卡 - Color Card'),
- StringStruct(u'FileVersion', u'1.5.0'),
+ StringStruct(u'FileVersion', u'1.5.1'),
StringStruct(u'InternalName', u'Color_Card'),
StringStruct(u'LegalCopyright', u'© 2026 浮晓 HXiao Studio'),
StringStruct(u'OriginalFilename', u'Color_Card.exe'),
StringStruct(u'ProductName', u'取色卡'),
- StringStruct(u'ProductVersion', u'1.5.0'),
+ StringStruct(u'ProductVersion', u'1.5.1'),
StringStruct(u'Comments', u'一站式的图片的图片分析和配色工具')
]
)
diff --git a/website/index.html b/website/index.html
index 1b728d0a2a3fd6e073d2f73cacc86691b165ca79..e5ad4931d9ac4767634c145d4567ac81ce3b2e3f 100644
--- a/website/index.html
+++ b/website/index.html
@@ -484,6 +484,7 @@
项目起源
下载
更新日志
+
使用说明
反馈
关于我们
@@ -675,7 +676,7 @@
立即开始使用
无需联网、无需注册、隐私安全、一步即达
-
+
@@ -701,6 +702,17 @@
Gitee
+
+
+
使用说明
+
视频教程,快速上手
+
+
+ 观看教程
+
+
@@ -1029,6 +1041,7 @@
diff --git a/website/public/changelog.json b/website/public/changelog.json
index 6fa6753b1bad23aa5e2c7ffacbfff6488568aac7..65bb19fa4b2f94ece25776f4050235aa6a07c6d0 100644
--- a/website/public/changelog.json
+++ b/website/public/changelog.json
@@ -1,5 +1,43 @@
{
"versions": [
+ {
+ "version": "v1.5.1",
+ "date": "2026-03-15",
+ "changes": [
+ {
+ "category": "新增功能",
+ "items": [
+ "新增使用说明视频教程入口"
+ ]
+ },
+ {
+ "category": "问题修复",
+ "items": [
+ "修复渐变提取HEX输入框可输入非法字符的问题"
+ ]
+ },
+ {
+ "category": "界面优化",
+ "items": [
+ "预览配色编辑对话框隐藏名称输入区域",
+ "配色管理卡片添加序号显示"
+ ]
+ },
+ {
+ "category": "内容调整",
+ "items": [
+ "重命名设置分组「色卡显示设置」为「颜色信息」",
+ "修正简体中文配置文件中的错误标题名称"
+ ]
+ },
+ {
+ "category": "性能提升",
+ "items": [
+ "优化配色组删除性能,避免重新加载导致的卡顿"
+ ]
+ }
+ ]
+ },
{
"version": "v1.5.0",
"date": "2026-03-08",
diff --git "a/\346\226\207\346\241\243/\345\274\200\345\217\221\350\247\204\350\214\203.md" "b/\346\226\207\346\241\243/\345\274\200\345\217\221\350\247\204\350\214\203.md"
index 8b68ea512dfc0d6c331f24ed1f074f569d180639..107b29e7ccdcdc437f93eadb6d022d1ccd41e0de 100644
--- "a/\346\226\207\346\241\243/\345\274\200\345\217\221\350\247\204\350\214\203.md"
+++ "b/\346\226\207\346\241\243/\345\274\200\345\217\221\350\247\204\350\214\203.md"
@@ -9,7 +9,7 @@ Color Card(取色卡)是一个基于 PySide6 开发的图片颜色分析工
**核心功能:**
- 图片导入(JPG、PNG、BMP、GIF)
-- 色彩提取:5个可拖动取色点,实时显示色彩值
+- 色彩提取:6个可拖动取色点,实时显示色彩值
- 多色彩模式:HSB、LAB、HSL、CMYK、RGB
- 明度提取与直方图可视化
- 双面板数据同步
@@ -109,8 +109,7 @@ color_card/
└── .json 颜色库
```
-
----
+***
## 2. 代码组织规范
@@ -122,13 +121,13 @@ color_card/
### 2.2 模块划分
-|模块类型 |职责 |示例文件 |
-|:---:|:---:|:---:|
-|入口模块 |应用程序入口 |`main.py` |
-|核心模块 |颜色处理、配置管理 |`core/color.py`, `core/config.py` |
-|UI 模块 |界面组件和面板 |`ui/canvases.py`, `ui/cards.py` |
-|对话框模块 |弹出对话框 |`dialogs/about_dialog.py` |
-|工具模块 |通用功能 |`utils/icon.py`, `utils/platform.py` |
+| 模块类型 | 职责 | 示例文件 |
+| :---: | :-------: | :----------------------------------: |
+| 入口模块 | 应用程序入口 | `main.py` |
+| 核心模块 | 颜色处理、配置管理 | `core/color.py`, `core/config.py` |
+| UI 模块 | 界面组件和面板 | `ui/canvases.py`, `ui/cards.py` |
+| 对话框模块 | 弹出对话框 | `dialogs/about_dialog.py` |
+| 工具模块 | 通用功能 | `utils/icon.py`, `utils/platform.py` |
### 2.3 目录结构原则
@@ -156,95 +155,45 @@ color_card/ ← 第1级
项目使用 PyInstaller 打包为独立可执行文件,资源路径获取必须兼容开发环境和打包后的环境。
-**路径获取函数:**
-
-```python
-import os
-import sys
-
-def get_base_path() -> str:
- """获取应用程序基础路径
-
- 支持开发环境和 PyInstaller 打包后的环境
-
- Returns:
- str: 应用程序基础路径
- """
- if getattr(sys, 'frozen', False):
- # PyInstaller 打包后的环境
- if hasattr(sys, '_MEIPASS'):
- return sys._MEIPASS
- return os.path.dirname(sys.executable)
- # 开发环境 - 返回当前文件所在目录的父目录(项目根目录)
- return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
-```
+**路径获取函数:** 使用 `get_base_path()` 获取应用程序基础路径,支持开发环境和 PyInstaller 打包后的环境。
**使用规范:**
-- 所有资源文件(语言包、数据文件、配置文件)路径必须通过 `get_base_path()` 获取
+- 所有资源文件路径必须通过 `get_base_path()` 获取
- 禁止直接使用 `__file__` 获取项目根目录(打包后会失效)
-- 资源目录应使用相对路径拼接
```python
-# 正确示例
-from utils.icon import get_base_path
-
+# 正确用法
base_path = get_base_path()
locales_dir = os.path.join(base_path, 'locales')
-config_path = os.path.join(base_path, 'color_data', 'config.json')
-# 错误示例 - 打包后会找不到资源
-current_dir = os.path.dirname(os.path.abspath(__file__))
-project_root = os.path.dirname(current_dir) # 开发环境有效,打包后失效
+# 错误用法 - 打包后会找不到资源
+project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
```
-**已适配的模块:**
-- `utils/icon.py` - 图标资源路径
-- `utils/locale.py` - 语言包路径
-- `core/config.py` - 场景配置路径
-- `core/color_data.py` - 配色数据路径
### 2.5 导入规范
**导入顺序:** 标准库 → 第三方库 → 项目模块
-**规范要求:**
-
-- 按模块类型分组导入,添加清晰的分组注释
-- 定期清理未使用的导入
-- 合并同一模块的多次导入
-- 优先使用绝对导入
-
```python
# 标准库导入
import sys
-import math
from pathlib import Path
# 第三方库导入
-from PySide6.QtWidgets import QApplication, QMainWindow
-from PySide6.QtCore import Qt, Signal
+from PySide6.QtWidgets import QApplication
from qfluentwidgets import FluentWindow
# 项目模块导入
-from core import get_color_info, get_config_manager
+from core import get_color_info
from ui import MainWindow
```
-**导入清理示例:**
-
-```python
-# 修改前
-from styles import UnifiedStyleHelper
-from styles import get_global_font_manager
-
-# 修改后
-from styles import UnifiedStyleHelper, get_global_font_manager
-```
-
+**规范要求:** 按模块类型分组导入,定期清理未使用的导入,合并同一模块的多次导入,优先使用绝对导入。
----
+***
## 3. 代码编写规范
@@ -257,82 +206,37 @@ from styles import UnifiedStyleHelper, get_global_font_manager
### 3.2 代码清理原则
-**修改代码时应同步进行以下清理:**
-
-- 删除未使用的变量、函数、导入和注释
-- 合并重复的逻辑和相似的功能
-- 检查并清除相关的重复冗余代码
-- 保持代码整洁,提高代码复用性
-
-**清理示例:**
-
-```python
-# 修改前
-import json
-import re
-
-def calculate():
- temp = 0 # 未使用的变量
- return result
-
-# 修改后(删除未使用的导入和变量)
-def calculate():
- return result
-```
+**修改代码时应同步进行以下清理:** 删除未使用的变量、函数、导入和注释;合并重复的逻辑和相似的功能;保持代码整洁,提高代码复用性。
### 3.3 命名规范
**详细规范请参阅**
-|类型 |规范 |示例 |
-|:---:|:---:|:---:|
-|类名 |驼峰命名法 |`ColorPicker`, `ImageCanvas` |
-|函数/方法 |小写+下划线 |`extract_color()` |
-|变量 |小写+下划线 |`picker_positions` |
-|常量 |大写+下划线 |`PICKER_RADIUS = 12` |
-|私有属性 |单下划线前缀 |`_dragging` |
-|信号 |小写+下划线 |`color_picked = Signal(int, tuple)` |
+| 类型 | 规范 | 示例 |
+| :---: | :----: | :---------------------------------: |
+| 类名 | 驼峰命名法 | `ColorPicker`, `ImageCanvas` |
+| 函数/方法 | 小写+下划线 | `extract_color()` |
+| 变量 | 小写+下划线 | `picker_positions` |
+| 常量 | 大写+下划线 | `PICKER_RADIUS = 12` |
+| 私有属性 | 单下划线前缀 | `_dragging` |
+| 信号 | 小写+下划线 | `color_picked = Signal(int, tuple)` |
### 3.3.1 服务类命名规范
业务服务类统一命名为 `XxxService`,放在 `core/` 目录下:
-|服务类 |职责 |涉及文件 |
-|:---:|:---|:---|
-|`ColorService` |颜色提取服务 |`core/color_service.py` |
-|`PaletteService` |配色导入导出服务 |`core/palette_service.py` |
-|`ImageService` |图片加载服务 |`core/image_service.py` |
-|`LuminanceService` |明度计算服务 |`core/luminance_service.py` |
-|`PreviewService` |预览场景管理服务 |`core/preview_service.py` |
-|`HistogramService` |直方图计算服务 |`core/histogram_service.py` |
-
-**使用示例:**
-
-```python
-from core import PreviewService
-
-class ColorPreviewInterface(QWidget):
- def __init__(self, parent=None):
- super().__init__(parent)
- self._preview_service = PreviewService(self)
-
- def _on_import_config(self):
- # UI层只负责界面交互,业务逻辑下沉到服务层
- success, message = self._preview_service.add_user_template(
- self._current_scene, file_path
- )
- # 根据结果显示UI反馈
-```
+| 服务类 | 职责 | 涉及文件 |
+| :----------------: | :------- | :-------------------------- |
+| `ColorService` | 颜色提取服务 | `core/color_service.py` |
+| `PaletteService` | 配色导入导出服务 | `core/palette_service.py` |
+| `ImageService` | 图片加载服务 | `core/image_service.py` |
+| `LuminanceService` | 明度计算服务 | `core/luminance_service.py` |
+| `PreviewService` | 预览场景管理服务 | `core/preview_service.py` |
+| `HistogramService` | 直方图计算服务 | `core/histogram_service.py` |
### 3.4 异常处理规范
-**基本原则:**
-
-- 避免使用裸 `except:` 或 `except Exception:`
-- 应指定具体异常类型
-- 提供详细的错误信息,便于调试
-
-**示例:**
+**基本原则:** 避免使用裸 `except:` 或 `except Exception:`,应指定具体异常类型,提供详细的错误信息,便于调试。
```python
# 错误示例
@@ -342,48 +246,32 @@ except Exception:
# 正确示例
except (OSError, ValueError) as e:
error_msg = f"文件读取失败: {str(e)}"
- print(error_msg)
```
### 3.5 文档字符串规范
-**基本原则:**
-
-- 所有公共类和方法必须添加文档字符串
-- 使用简洁的中文描述,避免冗余
-- 类文档字符串保持简洁(单行或简短段落)
-- 方法文档字符串包含 Args、Returns 说明
-
-**精简示例:**
+**基本原则:** 所有公共类和方法必须添加文档字符串;使用简洁的中文描述,避免冗余;类文档字符串保持简洁(单行或简短段落);方法文档字符串包含 Args、Returns 说明。
```python
-# 类文档字符串(简洁)
class ImageCanvas(QWidget):
"""图片显示画布,支持取色点拖动"""
- pass
-# 方法文档字符串(完整但简洁)
def set_image(self, image_path):
"""加载并显示图片
Args:
image_path: 图片文件的完整路径
"""
- pass
-# 带返回值的文档字符串
def get_color_info(self, r, g, b):
"""获取颜色信息
Args:
- r: 红色通道值 (0-255)
- g: 绿色通道值 (0-255)
- b: 蓝色通道值 (0-255)
+ r, g, b: RGB通道值 (0-255)
Returns:
dict: 包含RGB、HSB、LAB、HEX颜色信息的字典
"""
- pass
```
### 3.6 信号命名规范
@@ -393,100 +281,48 @@ def get_color_info(self, r, g, b):
```python
class ImageCanvas(QWidget):
- color_picked = Signal(int, tuple) # 信号:索引, RGB颜色
- image_loaded = Signal(str) # 信号:图片路径
- image_cleared = Signal() # 信号:图片已清空
+ color_picked = Signal(int, tuple) # 信号:索引, RGB颜色
+ image_loaded = Signal(str) # 信号:图片路径
```
### 3.7 类型注解规范
-**基本原则:**
-
-- 使用 `typing` 模块的类型,而非内置小写类型
-- 避免使用 Python 3.9+ 内置泛型语法,统一使用 `typing` 模块
-- 公共方法应添加返回类型注解
+**基本原则:** 使用 `typing` 模块的类型,而非内置小写类型;避免使用 Python 3.9+ 内置泛型语法;公共方法应添加返回类型注解。
**类型对照表:**
-|内置类型(避免) |typing 类型(推荐) |说明 |
-|:---:|:---:|:---|
-|`list` |`List[元素类型]` |列表类型 |
-|`dict` |`Dict[键类型, 值类型]` |字典类型 |
-|`tuple` |`Tuple[元素类型, ...]` |元组类型 |
-|`type` |`Type[类名]` |类型对象 |
-
-**正确示例:**
+| 内置类型(避免) | typing 类型(推荐) | 说明 |
+| :------: | :----------------: | :--- |
+| `list` | `List[元素类型]` | 列表类型 |
+| `dict` | `Dict[键类型, 值类型]` | 字典类型 |
+| `tuple` | `Tuple[元素类型, ...]` | 元组类型 |
+| `type` | `Type[类名]` | 类型对象 |
```python
-from typing import List, Dict, Any, Tuple, Type, Optional
+from typing import List, Dict, Any, Tuple, Optional
-# 列表类型
def get_colors(self) -> List[str]:
return ['#FF5733', '#33FF57']
-# 字典类型(结构不确定时使用 Any)
def get_config(self) -> Dict[str, Any]:
return {'name': 'test', 'value': 123}
-# 元组类型
-def get_position(self) -> Tuple[int, int]:
- return (100, 200)
-
-# 类型对象
-def register(self, scene_class: Type['BaseScene']) -> None:
- pass
-
-# 可选类型
def find_item(self, item_id: str) -> Optional[Dict[str, Any]]:
return None # 或返回字典
```
-**错误示例:**
-
-```python
-# 错误:使用小写内置类型
-def get_colors(self) -> list:
- pass
-
-# 错误:使用 Python 3.9+ 内置泛型语法
-def get_colors(self) -> list[str]:
- pass
-
-# 错误:字典类型未参数化
-def get_config(self) -> dict:
- pass
-```
-
-**导入规范:**
-
-```python
-# 标准库导入
-from typing import List, Dict, Any, Tuple, Type, Optional
-
-# 按需导入,避免导入未使用的类型
-```
-
### 3.8 主题与样式规范
#### 3.8.1 主题切换实现规范
-**使用 qfluentwidgets 的主题系统:**
-
-- 使用 `setTheme(Theme.LIGHT/DARK)` 切换主题
-- 使用 `isDarkTheme()` 检测当前主题
-- 使用 `qconfig.themeChangedFinished` 信号监听主题变化
-
-**示例代码:**
+**使用 qfluentwidgets 的主题系统:** 使用 `setTheme(Theme.LIGHT/DARK)` 切换主题;使用 `isDarkTheme()` 检测当前主题;使用 `qconfig.themeChangedFinished` 信号监听主题变化。
```python
from qfluentwidgets import setTheme, Theme, isDarkTheme, qconfig
# 切换主题
def toggle_theme():
- if isDarkTheme():
- setTheme(Theme.LIGHT)
- else:
- setTheme(Theme.DARK)
+ setTheme(Theme.DARK if isDarkTheme() else Theme.LIGHT)
# 监听主题变化
qconfig.themeChangedFinished.connect(self._update_styles)
@@ -494,22 +330,12 @@ qconfig.themeChangedFinished.connect(self._update_styles)
#### 3.8.2 颜色管理规范
-**集中管理颜色值:**
-
-- 所有颜色值应定义在 `theme_colors.py` 中
-- 使用函数返回 QColor,支持主题感知
-- 避免在组件中硬编码颜色值
-
-**颜色函数命名规范:**
+**集中管理颜色值:** 所有颜色值应定义在 `theme_colors.py` 中;使用函数返回 QColor,支持主题感知;避免在组件中硬编码颜色值。
```python
-# 背景颜色
-
def get_card_background_color():
return QColor(42, 42, 42) if isDarkTheme() else QColor(255, 255, 255)
-# 文本颜色
-
def get_text_color(secondary=False):
if isDarkTheme():
return QColor(160, 160, 160) if secondary else QColor(255, 255, 255)
@@ -519,13 +345,7 @@ def get_text_color(secondary=False):
#### 3.7.3 主题自适应组件实现
-**必须实现** `_update_styles()` 方法:
-
-- 在 `__init__` 中调用 `_update_styles()` 初始化样式
-- 连接 `qconfig.themeChangedFinished` 信号
-- 根据 `isDarkTheme()` 返回不同颜色值
-
-**示例:**
+**必须实现** `_update_styles()` 方法:在 `__init__` 中调用 `_update_styles()` 初始化样式;连接 `qconfig.themeChangedFinished` 信号;根据 `isDarkTheme()` 返回不同颜色值。
```python
class MyWidget(QWidget):
@@ -537,52 +357,21 @@ class MyWidget(QWidget):
def _update_styles(self):
"""更新样式以适配主题"""
- if isDarkTheme():
- label_color = "#ffffff"
- value_color = "#ffffff"
- else:
- label_color = "#333333"
- value_color = "#333333"
-
+ label_color = "#ffffff" if isDarkTheme() else "#333333"
self.label.setStyleSheet(f"color: {label_color};")
```
-**注意事项:**
-
-- `_update_styles()` 中只更新必要的文字颜色,避免调用 `setStyleSheet` 设置容器样式
-- 让 qfluentwidgets 原生机制处理主题切换,避免双重样式更新导致性能问题
-- qfluentwidgets 组件(如 CardWidget、ScrollArea)会自动适配主题
+**注意事项:** `_update_styles()` 中只更新必要的文字颜色,避免调用 `setStyleSheet` 设置容器样式;让 qfluentwidgets 原生机制处理主题切换。
#### 3.7.4 样式表使用规范
-**避免使用** `!important`:
-
-- 优先使用组件特定的选择器
-- 如必须使用,确保在主题切换后重新应用
-
-**自定义标题栏按钮样式:**
-
-```python
-# 在主题切换后重新应用样式
-
-def _toggle_theme(self):
- if isDarkTheme():
- setTheme(Theme.LIGHT)
- else:
- setTheme(Theme.DARK)
- # 重新应用自定义样式
- self._apply_custom_style()
-```
+**避免使用** `!important`:优先使用组件特定的选择器;如必须使用,确保在主题切换后重新应用。
#### 3.8.5 Windows 原生标题栏深色模式
-**使用 Windows DWM API 设置原生标题栏主题:**
-
-对于继承自 `QDialog` 的对话框,使用 Windows DWM (Desktop Window Manager) API 设置原生标题栏的沉浸式深色模式。
-
-**实现步骤:**
+对于继承自 `QDialog` 的对话框,使用 Windows DWM API 设置原生标题栏的沉浸式深色模式。
-1. **创建工具函数**(放在 `utils/platform.py`):
+**工具函数**(放在 `utils/platform.py`):
```python
import ctypes
@@ -592,61 +381,38 @@ DWMWA_USE_IMMERSIVE_DARK_MODE = 20
def set_window_title_bar_theme(window, is_dark=False):
"""为窗口设置标题栏主题(Windows 10+)"""
+ if sys.platform != "win32":
+ return False
try:
- if sys.platform != "win32":
- return False
-
- window_handle = window.windowHandle()
- if not window_handle:
- return False
-
- hwnd = int(window_handle.winId())
+ hwnd = int(window.windowHandle().winId())
value = ctypes.c_int(1 if is_dark else 0)
-
- result = ctypes.windll.dwmapi.DwmSetWindowAttribute(
- hwnd,
- DWMWA_USE_IMMERSIVE_DARK_MODE,
- ctypes.byref(value),
- ctypes.sizeof(value)
- )
- return result == 0
+ return ctypes.windll.dwmapi.DwmSetWindowAttribute(
+ hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE,
+ ctypes.byref(value), ctypes.sizeof(value)
+ ) == 0
except Exception:
return False
```
-2. **在对话框中应用**(使用 `showEvent` 避免闪烁):
+**在对话框中应用**(使用 `showEvent` 避免闪烁):
```python
-from qfluentwidgets import isDarkTheme, qconfig
-from utils import set_window_title_bar_theme
-
class MyDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
- # ... 其他初始化代码 ...
-
- # 监听主题变化(用于更新已打开的对话框)
qconfig.themeChangedFinished.connect(self._update_title_bar_theme)
def showEvent(self, event):
- """窗口显示前设置标题栏主题,避免闪烁"""
self._update_title_bar_theme()
super().showEvent(event)
def _update_title_bar_theme(self):
- """更新标题栏主题"""
set_window_title_bar_theme(self, isDarkTheme())
```
-**关键要点:**
-
-- 使用 `showEvent` 在窗口显示前设置标题栏主题,避免闪烁
-- 连接 `qconfig.themeChangedFinished` 信号,支持已打开对话框的主题切换
-- 仅支持 Windows 10 版本 2004 (Build 19041) 及以上,Windows 11 完全支持
-- 非 Windows 平台静默跳过,不影响程序运行
+**关键要点:** 使用 `showEvent` 在窗口显示前设置标题栏主题;连接 `qconfig.themeChangedFinished` 信号支持已打开对话框的主题切换;仅支持 Windows 10 版本 2004 及以上。
-
----
+***
## 4. 基类设计规范
@@ -654,23 +420,16 @@ class MyDialog(QDialog):
**文件位置:** `ui/canvases.py`
-**职责:**
-
-- 提供图片加载、显示的基础功能
-- 实现坐标转换(画布坐标 ↔ 图片坐标)
-- 管理图片相对坐标系统
-- 提供右键菜单框架
+**职责:** 提供图片加载、显示的基础功能;实现坐标转换(画布坐标 ↔ 图片坐标);管理图片相对坐标系统;提供右键菜单框架。
**子类必须实现的方法:**
```python
def _on_image_loaded(self):
"""图片加载后的处理(子类重写)"""
- pass
def _on_image_cleared(self):
"""图片清空后的处理(子类重写)"""
- pass
def _draw_overlay(self, painter: QPainter):
"""绘制叠加内容(子类必须实现)"""
@@ -681,27 +440,15 @@ def _draw_overlay(self, painter: QPainter):
**文件位置:** `ui/cards.py`
-**职责:**
-
-- 提供统一的卡片接口
-- 管理卡片列表(BaseCardPanel)
-- 支持批量清空卡片
-
-**设计原则:**
+**职责:** 提供统一的卡片接口;管理卡片列表(BaseCardPanel);支持批量清空卡片。
-- 保持接口简洁(setup_ui, clear)
-- 灵活的卡片创建方法
-- 清晰的职责分离
+**设计原则:** 保持接口简洁(setup_ui, clear);灵活的卡片创建方法;清晰的职责分离。
### 4.3 直方图基类 (BaseHistogram)
**文件位置:** `ui/histograms.py`
-**职责:**
-
-- 提供通用的数据管理(set_data, clear)
-- 提供通用的绘制框架(paintEvent)
-- 定义抽象方法接口
+**职责:** 提供通用的数据管理(set_data, clear);提供通用的绘制框架(paintEvent);定义抽象方法接口。
**子类必须实现的方法:**
@@ -712,11 +459,9 @@ def _draw_histogram(self, painter: QPainter):
def _draw_custom_overlay(self, painter: QPainter):
"""绘制自定义叠加内容(子类重写)"""
- pass
def _draw_labels(self, painter: QPainter):
"""绘制标签(子类重写)"""
- pass
```
### 4.4 基类设计原则
@@ -726,8 +471,7 @@ def _draw_labels(self, painter: QPainter):
3. **可扩展性**:便于添加新的子类实现
4. **代码复用**:提取公共逻辑,消除重复代码
-
----
+***
## 5. PySide6 开发规范
@@ -745,11 +489,7 @@ setThemeColor('#0078d4')
class MainWindow(FluentWindow):
def setup_navigation(self):
- self.addSubInterface(
- self.interface,
- FluentIcon.PALETTE,
- "界面名称"
- )
+ self.addSubInterface(self.interface, FluentIcon.PALETTE, "界面名称")
```
### 5.2 界面组织规范
@@ -776,31 +516,17 @@ class ColorExtractInterface(QWidget):
# 信号连接
self.image_canvas.color_picked.connect(self.on_color_picked)
-# 槽函数实现
-def on_color_picked(self, index, rgb):
- """颜色提取回调"""
- pass
-
-# 防止双向同步循环的示例
+# 防止双向同步循环
def set_image_data(self, pixmap, image, emit_sync=True):
"""设置图片数据
Args:
- pixmap: QPixmap 对象
- image: QImage 对象
emit_sync: 是否发射同步信号(默认True,从其他面板同步时设为False)
"""
self._original_pixmap = pixmap
self._image = image
-
- # 只在独立导入时发射同步信号,防止双向循环
if emit_sync:
self.image_loaded.emit(file_path)
- self.image_data_loaded.emit(pixmap, image)
-
-# 从其他面板同步时禁用信号发射
-# 面板A导入图片 → 同步到面板B(emit_sync=False)
-# 面板B不会发射信号回到面板A,打破循环
```
### 5.3.1 图片状态中介者(ImageMediator)
@@ -815,7 +541,6 @@ self._image_mediator = ImageMediator(self)
# 连接信号
self._image_mediator.image_updated.connect(self._on_image_updated)
-self._image_mediator.image_cleared.connect(self._on_image_cleared)
# 设置图片(带来源标识,防止循环)
self._image_mediator.set_image(pixmap, image, 'color')
@@ -824,33 +549,7 @@ self._image_mediator.set_image(pixmap, image, 'color')
self._image_mediator.clear_image('color')
```
-**清空同步规范:**
-
-界面类应提供 `clear_all(emit_signal=True)` 方法统一处理所有清空操作:
-
-```python
-def clear_all(self, emit_signal: bool = True):
- """清空所有相关内容
-
- Args:
- emit_signal: 是否发射清空信号(默认True,从其他面板同步时设为False)
- """
- self.image_canvas.clear_image(emit_signal)
- self.color_card_panel.clear_all()
- self.hsb_color_wheel.clear_sample_points()
- self.rgb_histogram_widget.clear()
- self.hue_histogram_widget.clear()
-
-def clear_image(self):
- """清空图片(供外部调用,会发射信号同步到其他面板)"""
- self.clear_all(emit_signal=True)
-```
-
-**要点:**
-
-- 使用 `clear_all()` 统一处理所有组件的清理,避免遗漏
-- 通过 `emit_signal` 参数控制是否触发同步,防止循环
-- 从其他面板同步清空时,使用 `clear_all(emit_signal=False)`
+**清空同步规范:** 界面类应提供 `clear_all(emit_signal=True)` 方法统一处理所有清空操作,通过 `emit_signal` 参数控制是否触发同步,防止循环。
### 5.4 自定义控件规范
@@ -864,7 +563,6 @@ class ColorPicker(QWidget):
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
- # 绘制代码...
```
### 5.5 样式设置规范
@@ -876,54 +574,29 @@ class ColorPicker(QWidget):
所有颜色必须通过 `utils/theme_colors.py` 模块统一管理:
```python
-# 正确用法 - 从主题颜色模块导入
-from utils.theme_colors import get_text_color, get_canvas_background_color
-
-# 获取主题感知的文本颜色
+# 正确用法
+from utils.theme_colors import get_text_color
text_color = get_text_color()
-# 获取固定颜色(如图片显示器背景)
-bg_color = get_canvas_background_color() # 固定灰黑色 #2a2a2a
-```
-
-**禁止的做法:**
-
-```python
-# 错误 - 硬编码颜色值
+# 错误用法
painter.setPen(QColor(255, 255, 255))
-widget.setStyleSheet("background-color: #2a2a2a;")
```
#### 5.5.2 主题颜色模块 (utils/theme_colors.py)
-**设计原则:**
-
-- **集中管理**:所有颜色值集中在 theme_colors.py 中定义
-- **主题感知**:颜色函数根据当前主题(深色/浅色)自动返回对应颜色
-- **分类清晰**:按用途分类(背景色、文本色、边框色、控件颜色等)
+**设计原则:** 集中管理所有颜色值;颜色函数根据当前主题自动返回对应颜色;按用途分类(背景色、文本色、边框色、控件颜色等)。
**颜色分类:**
-|分类 |函数示例 |说明 |
-|:---:|:---|:---|
-|背景色 |`get_canvas_background_color()` |图片显示器背景(固定 #2a2a2a) |
-|背景色 |`get_card_background_color()` |卡片背景(主题感知) |
-|背景色 |`get_histogram_background_color()` |直方图背景(固定 #2a2a2a) |
-|文本色 |`get_text_color()` |主文本颜色(主题感知) |
-|文本色 |`get_secondary_text_color()` |次要文本颜色(主题感知) |
-|文本色 |`get_title_color()` |标题颜色(主题感知) |
-|边框色 |`get_border_color()` |边框颜色(主题感知) |
-|控件色 |`get_picker_border_color()` |取色点边框颜色 |
-|控件色 |`get_picker_fill_color()` |取色点填充颜色 |
-|Zone色 |`get_zone_background_color()` |Zone框背景颜色 |
-|Zone色 |`get_zone_text_color()` |Zone框文字颜色 |
-
-**添加新颜色的步骤:**
-
-1. 在 `utils/theme_colors.py` 中添加颜色函数
-2. 根据用途选择合适的分类
-3. 确定是固定颜色还是主题感知颜色
-4. 在相关组件中使用新函数
+| 分类 | 函数示例 | 说明 |
+| :---: | :--------------------------------- | :------------------ |
+| 背景色 | `get_canvas_background_color()` | 图片显示器背景(固定 #2a2a2a) |
+| 背景色 | `get_card_background_color()` | 卡片背景(主题感知) |
+| 文本色 | `get_text_color()` | 主文本颜色(主题感知) |
+| 边框色 | `get_border_color()` | 边框颜色(主题感知) |
+| 控件色 | `get_picker_border_color()` | 取色点边框颜色 |
+
+**添加新颜色的步骤:** 在 `utils/theme_colors.py` 中添加颜色函数;根据用途选择合适的分类;在相关组件中使用新函数。
#### 5.5.3 主题设置
@@ -932,45 +605,29 @@ widget.setStyleSheet("background-color: #2a2a2a;")
- 使用 `setThemeColor()` 设置主题色
```python
-from qfluentwidgets import FluentWindow, setTheme, Theme, FluentIcon, setThemeColor
+from qfluentwidgets import setTheme, Theme, setThemeColor
setTheme(Theme.AUTO)
setThemeColor('#0078d4')
-
-class MainWindow(FluentWindow):
- pass
```
#### 5.5.4 禁用Qt原生功能规范
**核心原则:新组件应禁用不符合项目需求的Qt原生功能**
-Qt控件默认提供的某些功能可能与项目设计不符,应在创建组件时主动禁用。
-
-**需要禁用的常见原生功能:**
-
-|功能 |禁用方法 |适用场景 |
-|:---:|:---|:---|
-|右键上下文菜单 |`setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)` |输入框、标签等不需要原生编辑菜单的控件 |
-|原生工具提示 |不调用 `setToolTip()` 或设置为空字符串 |使用自定义提示或不需要提示的按钮 |
-|焦点虚线框 |`setFocusPolicy(Qt.FocusPolicy.NoFocus)` 或样式表 |不需要键盘焦点的装饰性控件 |
-
-**示例 - 禁用输入框右键菜单:**
+| 功能 | 禁用方法 | 适用场景 |
+| :-----: | :--------------------------------------------------------- | :------------------ |
+| 右键上下文菜单 | `setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)` | 输入框、标签等不需要原生编辑菜单的控件 |
+| 原生工具提示 | 不调用 `setToolTip()` 或设置为空字符串 | 使用自定义提示或不需要提示的按钮 |
+| 焦点虚线框 | `setFocusPolicy(Qt.FocusPolicy.NoFocus)` 或样式表 | 不需要键盘焦点的装饰性控件 |
```python
-from PySide6.QtWidgets import QLineEdit
-from PySide6.QtCore import Qt
-
-# 创建输入框并禁用原生右键菜单
+# 禁用输入框右键菜单
self.hex_input = QLineEdit()
self.hex_input.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
```
-**注意事项:**
-
-- 仅在确实不需要原生功能时禁用
-- 如果需要自定义右键菜单,应使用 `RoundMenu` 替代
-- 禁用工具提示时,确保用户通过其他方式了解按钮功能
+**注意事项:** 仅在确实不需要原生功能时禁用;如果需要自定义右键菜单,应使用 `RoundMenu` 替代。
### 5.6 右键菜单规范
@@ -982,13 +639,11 @@ from qfluentwidgets import RoundMenu, Action, FluentIcon
def contextMenuEvent(self, event):
menu = RoundMenu("")
- change_action = Action(FluentIcon.PHOTO, "更换图片")
- menu.addAction(change_action)
+ menu.addAction(Action(FluentIcon.PHOTO, "更换图片"))
menu.exec(event.globalPos())
```
-
----
+***
## 6. 颜色处理规范
@@ -1010,66 +665,37 @@ def get_color_info(r, g, b):
### 6.2 明度计算规范
-使用 Rec. 709 标准计算亮度值,包含 sRGB Gamma 校正。
-
-```python
-def get_luminance(r: int, g: int, b: int) -> int:
- """计算像素的明度值 (0-255)
-
- 使用 Rec. 709 标准计算亮度值,包含 sRGB Gamma 校正
- 这是 Lightroom、Photoshop 等专业软件使用的标准方法
- """
- # 步骤1: 归一化到 0-1 范围
- r_norm, g_norm, b_norm = r / 255.0, g / 255.0, b / 255.0
-
- # 步骤2: sRGB Gamma 解码(转换到线性空间)
- def srgb_to_linear(c):
- if c <= 0.04045:
- return c / 12.92
- else:
- return ((c + 0.055) / 1.055) ** 2.4
-
- r_linear = srgb_to_linear(r_norm)
- g_linear = srgb_to_linear(g_norm)
- b_linear = srgb_to_linear(b_norm)
-
- # 步骤3: 在线性空间应用 Rec. 709 权重
- luminance_linear = 0.2126 * r_linear + 0.7152 * g_linear + 0.0722 * b_linear
-
- # 步骤4-5: 编码回 sRGB 空间并转换到 0-255
- # ...
-```
+使用 Rec. 709 标准计算亮度值,包含 sRGB Gamma 校正。这是 Lightroom、Photoshop 等专业软件使用的标准方法。
### 6.3 Zone 分区规范
采用 Adobe 官方标准的 5 个明度区域:
-|区域 |明度范围 |英文名 |描述 |
-|:---:|:---:|:---:|:---|
-|黑色 |0%–10% |Blacks |黑点区域,最暗的部分 |
-|阴影 |10%–30% |Shadows |阴影区域,较暗的色调 |
-|中间调 |30%–70% |Midtones |中间亮度区域,由 Exposure/Contrast 负责调整 |
-|高光 |70%–90% |Highlights |高光区域,较亮的色调 |
-|白色 |90%–100% |Whites |白点区域,最亮的部分 |
+| 区域 | 明度范围 | 英文名 | 描述 |
+| :-: | :------: | :--------: | :------------------------------ |
+| 黑色 | 0%–10% | Blacks | 黑点区域,最暗的部分 |
+| 阴影 | 10%–30% | Shadows | 阴影区域,较暗的色调 |
+| 中间调 | 30%–70% | Midtones | 中间亮度区域,由 Exposure/Contrast 负责调整 |
+| 高光 | 70%–90% | Highlights | 高光区域,较亮的色调 |
+| 白色 | 90%–100% | Whites | 白点区域,最亮的部分 |
**技术映射(0-255 值到区域):**
使用 `ZONE_WIDTH = 255 / 9 ≈ 28.33` 计算每个Zone的范围:
-|Zone |明度范围 |百分比 |区域 |
-|:---:|:---:|:---:|:---:|
-|Zone 0 |0-28 |0-11% |黑色(Blacks) |
-|Zone 1 |28-56 |11-22% |阴影(Shadows) |
-|Zone 2 |56-85 |22-33% |阴影(Shadows) |
-|Zone 3 |85-113 |33-44% |中间调(Midtones) |
-|Zone 4 |113-141 |44-55% |中间调(Midtones) |
-|Zone 5 |141-170 |55-67% |中间调(Midtones) |
-|Zone 6 |170-198 |67-78% |高光(Highlights) |
-|Zone 7 |198-226 |78-89% |高光(Highlights) |
-|Zone 8 |226-255 |89-100% |白色(Whites) |
+| Zone | 明度范围 | 百分比 | 区域 |
+| :----: | :-----: | :-----: | :------------: |
+| Zone 0 | 0-28 | 0-11% | 黑色(Blacks) |
+| Zone 1 | 28-56 | 11-22% | 阴影(Shadows) |
+| Zone 2 | 56-85 | 22-33% | 阴影(Shadows) |
+| Zone 3 | 85-113 | 33-44% | 中间调(Midtones) |
+| Zone 4 | 113-141 | 44-55% | 中间调(Midtones) |
+| Zone 5 | 141-170 | 55-67% | 中间调(Midtones) |
+| Zone 6 | 170-198 | 67-78% | 高光(Highlights) |
+| Zone 7 | 198-226 | 78-89% | 高光(Highlights) |
+| Zone 8 | 226-255 | 89-100% | 白色(Whites) |
-
----
+***
## 7. 图片处理规范
@@ -1083,10 +709,10 @@ def get_luminance(r: int, g: int, b: int) -> int:
**核心原则:** 采样点位置使用**图片相对坐标**(归一化坐标 0.0-1.0)存储。
-|坐标类型 |说明 |范围 |
-|:---:|:---|:---:|
-|相对坐标 |相对于图片的归一化坐标 |0.0-1.0 |
-|画布坐标 |相对于画布控件的像素坐标 |像素值 |
+| 坐标类型 | 说明 | 范围 |
+| :--: | :----------- | :-----: |
+| 相对坐标 | 相对于图片的归一化坐标 | 0.0-1.0 |
+| 画布坐标 | 相对于画布控件的像素坐标 | 像素值 |
```python
# 相对坐标 → 画布坐标
@@ -1098,17 +724,9 @@ rel_x = (canvas_x - disp_x) / disp_w
### 7.3 性能优化规范
-**基本策略:**
-
-- 使用 `QThread` 在子线程读取图片文件
-- 使用 `QTimer.singleShot()` 延迟执行耗时操作
-- 直方图计算使用采样优化
+**基本策略:** 使用 `QThread` 在子线程读取图片文件;使用 `QTimer.singleShot()` 延迟执行耗时操作;直方图计算使用采样优化。
-**QThread 取消机制:**
-
-- 使用标志位机制实现线程取消,避免使用 `wait()` 阻塞UI线程
-- 在 `run()` 方法的关键检查点检查取消标志
-- 不等待旧线程结束,立即启动新线程
+**QThread 取消机制:** 使用标志位机制实现线程取消,避免使用 `wait()` 阻塞UI线程;在 `run()` 方法的关键检查点检查取消标志;不等待旧线程结束,立即启动新线程。
```python
class ImageLoader(QThread):
@@ -1116,60 +734,27 @@ class ImageLoader(QThread):
def __init__(self, image_path: str) -> None:
super().__init__()
- self._image_path = image_path
- self._is_cancelled = False # 取消标志
+ self._is_cancelled = False
def cancel(self) -> None:
- """请求取消加载(线程安全)"""
self._is_cancelled = True
- def _check_cancelled(self) -> bool:
- """检查是否被取消"""
- return self._is_cancelled
-
def run(self) -> None:
- """在子线程中加载图片"""
- try:
- with Image.open(self._image_path) as pil_image:
- # 关键检查点1
- if self._check_cancelled():
- return
-
- # 耗时操作前检查
- if self._check_cancelled():
- return
-
- # 执行耗时操作...
-
- # 关键检查点2
- if self._check_cancelled():
- return
-
- except Exception as e:
- if not self._check_cancelled():
- self.error.emit(str(e))
+ if self._is_cancelled:
+ return
+ # 执行耗时操作...
# 使用示例(非阻塞切换)
def set_image(self, image_path: str) -> None:
- # 取消旧线程(非阻塞)
if self._loader is not None:
- self._loader.cancel()
- # 注意:不调用 wait(),避免阻塞UI线程
- self._loader = None
-
- # 立即启动新线程
+ self._loader.cancel() # 不调用 wait()
self._loader = ImageLoader(image_path)
self._loader.start()
```
-**UI性能优化:**
-
-- 批量更新UI时使用 `setUpdatesEnabled(False/True)` 包裹更新操作
-- 避免在循环中频繁更新UI,先收集数据再批量更新
-- 使用 `try-finally` 确保 `setUpdatesEnabled(True)` 一定会执行
+**UI性能优化:** 批量更新UI时使用 `setUpdatesEnabled(False/True)` 包裹更新操作;避免在循环中频繁更新UI;使用 `try-finally` 确保 `setUpdatesEnabled(True)` 一定会执行。
```python
-# 批量更新UI示例
def update_table_data(self):
self.table.setUpdatesEnabled(False)
try:
@@ -1179,119 +764,33 @@ def update_table_data(self):
self.table.setUpdatesEnabled(True)
```
-**数据结构优化:**
-
-- 将列表转换为集合,将查找时间复杂度从 O(n) 降到 O(1)
-- 缓存计算结果,避免重复计算和I/O操作
-- 缓存应在数据变化时自动失效,确保数据一致性
+**数据结构优化:** 将列表转换为集合,将查找时间复杂度从 O(n) 降到 O(1);缓存计算结果,避免重复计算和I/O操作。
**应用启动优化策略:**
- **界面预创建策略**:启动时创建所有界面,避免切换时的视觉闪烁
- - 优点:切换流畅,无延迟
- - 缺点:启动时间增加
-- **组件缓存延迟生成**:耗时计算(如色轮逐像素渲染)延迟到首次绘制时执行
- - 使用 `QTimer.singleShot(0, self._generate_cache)` 异步生成
- - 生成期间先显示简单背景,完成后刷新
+- **组件缓存延迟生成**:耗时计算延迟到首次绘制时执行,使用 `QTimer.singleShot(0, self._generate_cache)` 异步生成
-**示例 - 色轮缓存延迟生成:**
-
-```python
-def paintEvent(self, event):
- painter = QPainter(self)
- painter.setRenderHint(QPainter.RenderHint.Antialiasing)
-
- # 检查是否需要重新生成缓存
- if not self._cache_valid:
- # 延迟生成缓存,避免阻塞启动
- if not self._pending_generation:
- self._pending_generation = True
- QTimer.singleShot(0, self._generate_wheel_cache_async)
- # 绘制简单背景
- painter.fillRect(self.rect(), self._get_theme_colors()['bg'])
- elif self._wheel_cache:
- # 绘制缓存的色环背景
- painter.drawPixmap(0, 0, self._wheel_cache)
-
- # 绘制其他内容...
-
-def _generate_wheel_cache_async(self):
- """异步生成色轮缓存"""
- self._pending_generation = False
- self._generate_wheel_cache()
- self.update()
-```
-
-**决策原则:**
-
-- 界面结构简单 → 预创建所有界面
-- 界面结构复杂/组件初始化耗时 → 延迟创建(需确保切换无闪烁)
-- 绘制密集型组件(色轮、直方图) → 延迟生成缓存
+**决策原则:** 界面结构简单 → 预创建所有界面;界面结构复杂/组件初始化耗时 → 延迟创建;绘制密集型组件(色轮、直方图) → 延迟生成缓存。
### 7.4 内存管理规范
-**使用 ImageMemoryManager 管理图片内存:**
-
-- 使用 `ImageMemoryManager` 统一管理 QPixmap 和 QImage 的内存
-- 实现 LRU 缓存策略,自动清理最久未使用的图片
-- 限制最大内存占用(默认 500MB)
+**使用 ImageMemoryManager 管理图片内存:** 使用 `ImageMemoryManager` 统一管理 QPixmap 和 QImage 的内存;实现 LRU 缓存策略,自动清理最久未使用的图片;限制最大内存占用(默认 500MB)。
```python
from core import get_memory_manager
-# 获取全局内存管理器
memory_manager = get_memory_manager()
-
-# 添加图片到内存管理器
memory_manager.add_image(source_id, pixmap, image)
-
-# 获取图片(自动更新访问时间)
pixmap, image = memory_manager.get_image(source_id)
-
-# 移除指定图片
-memory_manager.remove_image(source_id)
-
-# 清空所有图片
memory_manager.clear_all()
-
-# 获取内存统计信息
-stats = memory_manager.get_memory_stats()
```
-**内存清理规范:**
-
-- 清空图片时直接赋值为 `None`,让 Python 垃圾回收处理
-- 清理相关缓存(高亮遮罩等)
-- 触发垃圾回收 `gc.collect()`
-
-```python
-def clear_image(self, emit_signal: bool = True) -> None:
- """清空图片"""
- # 直接赋值为 None,让 Python 垃圾回收处理
- self._original_pixmap = None
- self._image = None
-
- # 触发垃圾回收
- gc.collect()
-```
-
-**缩略图生成优化:**
-
-- 使用 QImage 进行缩放,然后转换为 QPixmap
-- 限制缩略图最大尺寸
-
-```python
-# 使用QImage进行缩放,内存效率更高
-thumbnail_image = image.scaled(
- size, size,
- aspectMode=Qt.AspectRatioMode.KeepAspectRatio,
- transformMode=Qt.TransformationMode.SmoothTransformation
-)
-return QPixmap.fromImage(thumbnail_image)
-```
+**内存清理规范:** 清空图片时直接赋值为 `None`,让 Python 垃圾回收处理;清理相关缓存;触发垃圾回收 `gc.collect()`。
+**缩略图生成优化:** 使用 QImage 进行缩放,然后转换为 QPixmap;限制缩略图最大尺寸。
----
+***
## 8. 配色数据JSON格式规范
@@ -1327,21 +826,21 @@ return QPixmap.fromImage(thumbnail_image)
### 8.3 字段说明
-|字段 |类型 |必填 |说明 |
-|:---|:---:|:---:|:---|
-|`version` |string |是 |格式版本号,当前为 "1.0" |
-|`id` |string |是 |唯一标识符 |
-|`name` |string |是 |配色方案名称(英文) |
-|`name_zh` |string |否 |配色方案中文名称 |
-|`description` |string |否 |描述信息 |
-|`author` |string |是 |作者名称 |
-|`category` |string |否 |分类:`design_system`/`theme`/`palette`/`user_palette` |
-|`palettes` |array |是 |配色组列表 |
-|`palettes[].name` |string |是 |配色组名称 |
-|`palettes[].colors` |array |是 |颜色值数组(HEX格式) |
-|`groups` |array |否 |分组定义(可选) |
-|`groups[].name` |string |是 |分组显示名称 |
-|`groups[].indices` |array |是 |配色组索引列表 |
+| 字段 | 类型 | 必填 | 说明 |
+| :------------------ | :----: | :-: | :-------------------------------------------------- |
+| `version` | string | 是 | 格式版本号,当前为 "1.0" |
+| `id` | string | 是 | 唯一标识符 |
+| `name` | string | 是 | 配色方案名称(英文) |
+| `name_zh` | string | 否 | 配色方案中文名称 |
+| `description` | string | 否 | 描述信息 |
+| `author` | string | 是 | 作者名称 |
+| `category` | string | 否 | 分类:`design_system`/`theme`/`palette`/`user_palette` |
+| `palettes` | array | 是 | 配色组列表 |
+| `palettes[].name` | string | 是 | 配色组名称 |
+| `palettes[].colors` | array | 是 | 颜色值数组(HEX格式) |
+| `groups` | array | 否 | 分组定义(可选) |
+| `groups[].name` | string | 是 | 分组显示名称 |
+| `groups[].indices` | array | 是 | 配色组索引列表 |
### 8.4 分组配置
@@ -1349,10 +848,10 @@ return QPixmap.fromImage(thumbnail_image)
**分组规则**:
-|配色组数量 |分组方式 |示例 |
-|:---:|:---|:---|
-|< 20 |单分组 |"全部 (15组)" |
-|≥ 20 |按20组分组 |"第 1-20 组"、"第 21-40 组" |
+| 配色组数量 | 分组方式 | 示例 |
+| :---: | :----- | :--------------------- |
+| < 20 | 单分组 | "全部 (15组)" |
+| ≥ 20 | 按20组分组 | "第 1-20 组"、"第 21-40 组" |
**分组示例**:
@@ -1378,8 +877,7 @@ return QPixmap.fromImage(thumbnail_image)
用户可在配色管理面板导出配色,导出的JSON文件符合标准格式,可直接作为配色源使用。
-
----
+***
## 9. 场景配置化规范
@@ -1421,7 +919,7 @@ scenes_data/ # 场景数据目录(打包后只读)
### 9.3 场景类型定义
-**scene_types.json**:
+**scene\_types.json**:
```json
{
@@ -1456,14 +954,14 @@ scenes_data/ # 场景数据目录(打包后只读)
**内置布局类型**:
-|类型ID |描述 |效果 |
-|---|---|---|
-|`single` |单图模式 |一次显示一个,左右切换 |
-|`scroll_v` |垂直滚动 |多个SVG垂直排列,可滚动 |
-|`scroll_h` |水平滚动 |多个SVG水平排列,可滚动 |
-|`grid_2x2` |2x2网格 |4个SVG网格展示 |
-|`grid_3x2` |3x2网格 |6个SVG网格展示 |
-|`mixed` |混合布局 |左侧2x2网格 + 右侧大图 |
+| 类型ID | 描述 | 效果 |
+| ---------- | ----- | -------------- |
+| `single` | 单图模式 | 一次显示一个,左右切换 |
+| `scroll_v` | 垂直滚动 | 多个SVG垂直排列,可滚动 |
+| `scroll_h` | 水平滚动 | 多个SVG水平排列,可滚动 |
+| `grid_2x2` | 2x2网格 | 4个SVG网格展示 |
+| `grid_3x2` | 3x2网格 | 6个SVG网格展示 |
+| `mixed` | 混合布局 | 左侧2x2网格 + 右侧大图 |
### 9.5 场景管理
@@ -1473,18 +971,9 @@ scenes_data/ # 场景数据目录(打包后只读)
from core import get_scene_type_manager
manager = get_scene_type_manager()
-
-# 获取所有场景类型
scene_types = manager.get_all_scene_types()
-
-# 获取场景的所有模板(内置 + 用户)
templates = manager.get_all_templates("ui")
-
-# 获取内置SVG路径
builtin_path = manager.get_builtin_svg_path("ui")
-
-# 获取布局配置
-layout_config = manager.get_layout_config("ui")
```
### 9.6 用户模板管理
@@ -1495,19 +984,9 @@ layout_config = manager.get_layout_config("ui")
from core import get_config_manager
config_manager = get_config_manager()
-
-# 获取场景的用户模板
templates = config_manager.get_scene_templates_by_type("ui")
-
-# 添加用户模板
-config_manager.add_scene_template("ui", {
- "path": "D:/设计素材/my_ui.svg",
- "name": "我的UI模板",
- "added_at": "2026-02-16"
-})
-
-# 删除用户模板
-config_manager.remove_scene_template("ui", "D:/设计素材/my_ui.svg")
+config_manager.add_scene_template("ui", {"path": "D:/my_ui.svg", "name": "我的UI模板"})
+config_manager.remove_scene_template("ui", "D:/my_ui.svg")
config_manager.save()
```
@@ -1519,22 +998,22 @@ SVG模板使用语义化 `class` 命名规范,支持智能配色映射。
#### 9.7.1 元素分类依据
-|优先级 |依据 |示例 |
-|:---:|---|---|
-|1 |class 属性关键词 |`class="background"` → 背景 |
-|2 |id 属性关键词 |`id="main-title"` → 文字 |
-|3 |标签类型 + 面积 |最大矩形 → 背景 |
+| 优先级 | 依据 | 示例 |
+| :-: | ----------- | ------------------------- |
+| 1 | class 属性关键词 | `class="background"` → 背景 |
+| 2 | id 属性关键词 | `id="main-title"` → 文字 |
+| 3 | 标签类型 + 面积 | 最大矩形 → 背景 |
#### 9.7.2 支持的关键词
-|元素类型 |class/id 关键词 |颜色映射 |
-|---|---|---|
-|背景 |`background`, `bg`, `back` |配色[0] |
-|主元素 |`primary`, `main` |配色[1] |
-|次要元素 |`secondary`, `sub` |配色[2] |
-|强调元素 |`accent`, `highlight` |配色[3] |
-|文字 |`text`, `title`, `label` |配色[4] |
-|描边 |`stroke`, `border`, `line` |配色[4] |
+| 元素类型 | class/id 关键词 | 颜色映射 |
+| ---- | -------------------------- | ------ |
+| 背景 | `background`, `bg`, `back` | 配色\[0] |
+| 主元素 | `primary`, `main` | 配色\[1] |
+| 次要元素 | `secondary`, `sub` | 配色\[2] |
+| 强调元素 | `accent`, `highlight` | 配色\[3] |
+| 文字 | `text`, `title`, `label` | 配色\[4] |
+| 描边 | `stroke`, `border`, `line` | 配色\[4] |
#### 9.7.3 双模式映射策略
@@ -1555,15 +1034,15 @@ else:
**映射规则**:
-|元素类型 |使用配色索引 |说明 |
-|---|---|---|
-|BACKGROUND |colors[0] |背景元素 |
-|PRIMARY |colors[1] |主元素 |
-|SECONDARY |colors[2] |次要元素 |
-|ACCENT |colors[3] |强调元素 |
-|TEXT |colors[4] |文字元素 |
-|STROKE |colors[4] |描边元素 |
-|UNKNOWN |不映射 |保持原色 |
+| 元素类型 | 使用配色索引 | 说明 |
+| ---------- | ---------- | ---- |
+| BACKGROUND | colors\[0] | 背景元素 |
+| PRIMARY | colors\[1] | 主元素 |
+| SECONDARY | colors\[2] | 次要元素 |
+| ACCENT | colors\[3] | 强调元素 |
+| TEXT | colors\[4] | 文字元素 |
+| STROKE | colors\[4] | 描边元素 |
+| UNKNOWN | 不映射 | 保持原色 |
**固定颜色处理**:
@@ -1597,20 +1076,11 @@ else:
#### 9.7.4 SVG模板示例
```svg
-
@@ -1618,58 +1088,31 @@ else:
#### 9.7.5 创建SVG模板规范
-1. **画布尺寸**:
- - 参考尺寸:400×300(适用于单图、滚动布局)
- - 根据实际布局需求调整尺寸
- - 网格布局可使用较小尺寸(如 200×150)
- - 混合布局可根据展示区域调整
-2. **命名规范**:
- - 使用语义化的 `class` 属性
- - 使用描述性的 `id` 属性
+1. **画布尺寸**:参考尺寸 400×300(适用于单图、滚动布局);网格布局可使用较小尺寸(如 200×150)
+2. **命名规范**:使用语义化的 `class` 属性;使用描述性的 `id` 属性
3. **颜色填充**:使用任意颜色,系统会自动替换
4. **导出格式**:使用"演示 SVG"或"内联样式"格式,避免外部CSS引用
-5. **固定颜色属性**:
- - 使用 `data-fixed-color` 属性标记不参与配色映射的元素
- - `data-fixed-color="black"`:固定使用黑色(适用于文字)
- - `data-fixed-color="original"`:保持原始颜色(适用于边框、框架等)
-
-**固定颜色示例**:
+5. **固定颜色属性**:使用 `data-fixed-color` 属性标记不参与配色映射的元素
+ - `data-fixed-color="black"`:固定使用黑色
+ - `data-fixed-color="original"`:保持原始颜色
```svg
-
-