diff --git a/.gitignore b/.gitignore index 7a85b3c7f0677b9a48bb6ecb33a228a73f10a6c3..5692f319cd61623d0e822ef33da12da360ea8beb 100644 --- a/.gitignore +++ b/.gitignore @@ -94,3 +94,6 @@ md转docx.py 文档/color_card_20260313_161405.json /测试 文档/代码审查报告-26.03.md +/创作过程 +bandit_report.html +bandit_project_report.html diff --git a/LICENSE b/LICENSE index 1a647a3363894becc3f746d49e448b99dc33e728..9a4d027b84f7a67ed279c37135c131f2c500d993 100644 --- a/LICENSE +++ b/LICENSE @@ -1028,7 +1028,20 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================================================ -6. Open Color +6. PySideSix-Frameless-Window +-------------------------------------------------------------------------------- +版权所有:zhiyiYo +项目地址:https://github.com/zhiyiYo/PyQt-Frameless-Window +许可证:GNU Lesser General Public License v3.0 + +说明: +PySideSix-Frameless-Window 使用 LGPLv3 许可证。由于本项目主许可证为 GPLv3, +而 LGPLv3 是 GPLv3 的补充版本,完整的 LGPLv3 许可证文本请参考本文档 +"PySide6" 章节中的 "GNU LESSER GENERAL PUBLIC LICENSE Version 3" 部分。 + + +================================================================================ +7. Open Color -------------------------------------------------------------------------------- 版权所有:heeyeun (Yeun) 项目地址:https://github.com/yeun/open-color @@ -1060,7 +1073,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -7. Nice Color Palettes +8. Nice Color Palettes -------------------------------------------------------------------------------- 版权所有:Jam3 项目地址:https://github.com/Experience-Monks/nice-color-palettes @@ -1090,7 +1103,7 @@ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -8. Tailwind CSS Colors +9. Tailwind CSS Colors -------------------------------------------------------------------------------- 版权所有:Tailwind Labs, Inc. 项目地址:https://github.com/tailwindlabs/tailwindcss @@ -1122,7 +1135,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -9. Material Design Colors (Apache License 2.0) +10. Material Design Colors (Apache License 2.0) -------------------------------------------------------------------------------- 版权所有:Google LLC 项目地址:https://m3.material.io/styles/color/system/overview @@ -1134,7 +1147,7 @@ Material Design Colors 使用 Apache License 2.0 许可证。完整的许可证 requests 章节中的 "Apache License Version 2.0" 部分。 ================================================================================ -10. ColorBrewer (Apache License 2.0) +11. ColorBrewer (Apache License 2.0) -------------------------------------------------------------------------------- 版权所有:Cynthia Brewer 官网:https://colorbrewer2.org/ @@ -1146,7 +1159,7 @@ ColorBrewer 使用 Apache License 2.0 许可证。完整的许可证文本请参 requests 章节中的 "Apache License Version 2.0" 部分。 ================================================================================ -11. Radix UI Colors +12. Radix UI Colors -------------------------------------------------------------------------------- 版权所有:WorkOS 项目地址:https://github.com/radix-ui/colors @@ -1179,7 +1192,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -12. Nord +13. Nord -------------------------------------------------------------------------------- 版权所有:Sven Greb 项目地址:https://github.com/arcticicestudio/nord @@ -1211,7 +1224,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -13. Dracula +14. Dracula --------------------------------------------------------------------------------- 版权所有:Dracula Theme contributors 官网:https://draculatheme.com/ @@ -1242,7 +1255,7 @@ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -14. Rosé Pine +15. Rosé Pine --------------------------------------------------------------------------------- 版权所有:Rosé Pine 团队 项目地址:https://github.com/rose-pine/rose-pine-theme @@ -1274,7 +1287,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -15. Solarized +16. Solarized -------------------------------------------------------------------------------- 版权所有:Ethan Schoonover 项目地址:https://github.com/altercation/solarized @@ -1304,7 +1317,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -16. Catppuccin +17. Catppuccin -------------------------------------------------------------------------------- 版权所有:Catppuccin 团队 项目地址:https://github.com/catppuccin/catppuccin @@ -1337,7 +1350,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -17. Gruvbox +18. Gruvbox -------------------------------------------------------------------------------- 版权所有:Pavel Pertsev 项目地址:https://github.com/morhetz/gruvbox @@ -1367,7 +1380,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -18. Tokyo Night +19. Tokyo Night -------------------------------------------------------------------------------- 版权所有:enkia 项目地址:https://github.com/enkia/tokyo-night-vscode-theme @@ -1400,7 +1413,7 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================================================ -19. 网站使用资源 +20. 网站使用资源 -------------------------------------------------------------------------------- 项目官网 (https://qingshangongzai.github.io/Color_Card/) 使用了以下资源: diff --git a/README.md b/README.md index 00ba75511fe2b55d7f0781261bf8f04ed695c88f..c2c02df644bf694e6c1629091d1b1e52b5c9f1fd 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,7 @@ 简体中文 | English

- ---- +*** @@ -29,50 +28,51 @@ **开源地址**: -- **主仓库(Gitee)**:https://gitee.com/qingshangongzai/color_card -- **镜像仓库(GitHub)**:https://github.com/qingshangongzai/Color_Card -- **官方网站**:https://qingshangongzai.github.io/Color_Card/ +- **主仓库(Gitee)**: +- **镜像仓库(GitHub)**: +- **官方网站**: ### 开发历程 自 2026-02-05 发布 v1.0.0 以来,项目保持快速稳定的迭代节奏: -| 指标 | 数据 | -| :-- | :-- | -| 发布版本 | 8 个版本(v1.0.0 → v1.5.1) | -| 开发周期 | 38 天 | -| 总更新项 | **102 项** | -| 平均每版本 | 12.75 项 | +| 指标 | 数据 | +| :---- | :--------------------- | +| 发布版本 | 9 个版本(v1.0.0 → v1.6.0) | +| 开发周期 | 45 天 | +| 总更新项 | **111 项** | +| 平均每版本 | 12.33 项 | **详细分类统计**: -| 分类 | 数量 | 说明 | -| :-- | :--: | :-- | -| ✨ 新增功能 | **32** | 包含首次发布的 9 项核心功能 | -| 🔧 问题修复 | **25** | 持续修复 Bug,提升稳定性 | -| 🎨 界面优化 | **25** | 用户体验打磨 | -| ⚡ 性能提升 | **8** | 缓存机制、启动优化等 | -| 📝 内容调整 | **5** | 文本、名称等调整 | -| 🏗️ 代码重构 | **3** | 代码结构优化 | -| 🔮 逻辑优化 | **2** | 算法逻辑改进 | -| 📜 许可证完善 | **1** | 开源合规性 | -| 🚀 功能优化 | **1** | 功能增强 | +| 分类 | 数量 | 说明 | +| :------- | :----: | :-------------- | +| ✨ 新增功能 | **33** | 包含首次发布的 9 项核心功能 | +| 🔧 问题修复 | **27** | 持续修复 Bug,提升稳定性 | +| 🎨 界面优化 | **28** | 用户体验打磨 | +| ⚡ 性能提升 | **8** | 缓存机制、启动优化等 | +| 📝 内容调整 | **5** | 文本、名称等调整 | +| 🏗️ 代码重构 | **4** | 代码结构优化 | +| 🔮 逻辑优化 | **2** | 算法逻辑改进 | +| ⚙️ 体验优化 | **2** | 交互体验改进 | +| 📜 许可证完善 | **1** | 开源合规性 | +| 🚀 功能优化 | **1** | 功能增强 | ### 核心功能特色 **一站式色彩解决方案**:从图片分析到配色应用,提供完整的色彩工作流 -| 功能 | 截图预览 | -|------|---------| -| **色彩信息提取**
通过可拖动取色点实时提取图片颜色,支持多色彩空间显示(HSB、LAB、HSL、CMYK、RGB) | ![色彩提取](docs/screenshots/color-extract.png) | -| **明度分析**
将图片按明度分为9个区域(基于Adobe标准),提供直方图可视化,可快速分析图片影调 | ![明度分析](docs/screenshots/luminance-extract.png) | -| **渐变色提取**
通过起始色和结束色生成渐变色序列,支持 RGB/HSB/LAB 三种颜色空间插值 | ![渐变色提取](docs/screenshots/Gradient%20Extract.png) | -| **配色生成**
提供5种专业配色方案(同色系、邻近色、互补色、分离补色、双补色),支持可交互色环选择 | ![配色生成](docs/screenshots/color-generation.png) | -| **配色收藏**
支持收藏、管理配色方案,支持批量导入导出为JSON文件,支持单组色卡导出为 Adobe ASE 格式 | ![配色管理](docs/screenshots/palette-management.png) | -| **内置色彩库**
集成 Open Color、Tailwind CSS、Material Design 等13大开源配色方案,总计661组色卡 | ![内置色彩库](docs/screenshots/preset-colors.png) | -| **配色预览**
支持手机UI、网页、插画、排版、品牌、海报、图案、杂志等8种场景预览,并支持导入自定义SVG | ![配色预览](docs/screenshots/color-preview.png) | -| **多语言支持**
支持简体中文、繁体中文、英语、日语、法语、俄语等6种语言 | ![多语言支持](docs/screenshots/locales.png) | -| **现代化界面**
基于 Fluent Design 设计语言,支持深色/浅色主题切换 | ![深色/浅色模式](./docs/screenshots/Dark%20mode%26light%20mode.png) | +| 功能 | 截图预览 | +| ----------------------------------------------------------------------- | -------------------------------------------------------------------- | +| **色彩信息提取**通过可拖动取色点实时提取图片颜色,支持多色彩空间显示(HSB、LAB、HSL、CMYK、RGB) | !\[色彩提取]\(docs/screenshots/color-extract.png null) | +| **明度分析**将图片按明度分为9个区域(基于Adobe标准),提供直方图可视化,可快速分析图片影调 | !\[明度分析]\(docs/screenshots/luminance-extract.png null) | +| **渐变色提取**通过起始色和结束色生成渐变色序列,支持 RGB/HSB/LAB 三种颜色空间插值 | !\[渐变色提取]\(docs/screenshots/Gradient%20Extract.png null) | +| **配色生成**提供5种专业配色方案(同色系、邻近色、互补色、分离补色、双补色),支持可交互色环选择 | !\[配色生成]\(docs/screenshots/color-generation.png null) | +| **配色收藏**支持收藏、管理配色方案,支持批量导入导出为JSON文件,支持单组色卡导出为 Adobe ASE 格式 | !\[配色管理]\(docs/screenshots/palette-management.png null) | +| **内置色彩库**集成 Open Color、Tailwind CSS、Material Design 等13大开源配色方案,总计661组色卡 | !\[内置色彩库]\(docs/screenshots/preset-colors.png null) | +| **配色预览**支持手机UI、网页、插画、排版、品牌、海报、图案、杂志等8种场景预览,并支持导入自定义SVG | !\[配色预览]\(docs/screenshots/color-preview\.png null) | +| **多语言支持**支持简体中文、繁体中文、英语、日语、法语、俄语等6种语言,支持跟随系统语言自动切换 | !\[多语言支持]\(docs/screenshots/locales.png null) | +| **现代化界面**基于 Fluent Design 设计语言,支持深色/浅色主题切换 | !\[深色/浅色模式]\(./docs/screenshots/Dark%20mode%26light%20mode.png null) | ### 适用场景 @@ -95,8 +95,7 @@ - **色彩教学**:作为色彩理论和实践的教学工具 - **快速原型**:快速生成配色并预览效果,加速设计迭代 - ---- +*** ## 安装指南 @@ -118,36 +117,31 @@ #### 依赖安装与运行 1. **克隆仓库**: - ```bash # 从 Gitee 克隆(国内推荐) git clone https://gitee.com/qingshangongzai/color_card.git - + # 或从 GitHub 克隆 git clone https://github.com/qingshangongzai/Color_Card.git - + cd color_card ``` 2. **创建虚拟环境(推荐)**: - ```bash python -m venv .venv # 激活虚拟环境 .\.venv\Scripts\activate # Windows ``` 3. **安装项目依赖**: - ```bash pip install -r requirements.txt ``` 4. **启动应用程序**: - ```bash python main.py ``` - ---- +*** ## 使用说明 @@ -163,25 +157,23 @@ ### 功能模块 -|模块 |功能 | -|:---|:---| -|色彩提取 |可拖动取色点、多色彩空间显示、一键复制颜色值 | -|明度分析 |9级明度分区(Zone 0-8)、直方图可视化、区域高亮 | -|渐变色提取 |起始色/结束色设置、RGB/HSB/LAB插值、中间色数量调节 | -|配色生成 |5种配色方案、可交互色环、明度调整 | -|配色管理 |收藏配色、自定义名称、批量导入导出为JSON文件、支持单组配色ASE格式导出(支持Adobe软件) | -|配色预览 |8种内置场景、自定义SVG、智能配色映射 | -|内置色彩库 |13大开源配色方案、661组色卡 | - +| 模块 | 功能 | +| :---- | :------------------------------------------------ | +| 色彩提取 | 可拖动取色点、多色彩空间显示、一键复制颜色值 | +| 明度分析 | 9级明度分区(Zone 0-8)、直方图可视化、区域高亮 | +| 渐变色提取 | 起始色/结束色设置、RGB/HSB/LAB插值、中间色数量调节 | +| 配色生成 | 5种配色方案、可交互色环、明度调整 | +| 配色管理 | 收藏配色、自定义名称、批量导入导出为JSON文件、支持单组配色ASE格式导出(支持Adobe软件) | +| 配色预览 | 8种内置场景、自定义SVG、智能配色映射 | +| 内置色彩库 | 13大开源配色方案、661组色卡 | ---- +*** ## 开发规范 本项目遵循 PEP 8 代码风格规范,采用模块化架构设计。详细的开发规范请参考 [开发规范.md](文档/开发规范.md)。 - ---- +*** ## 贡献指南 @@ -197,7 +189,7 @@ 2. 创建你的特性分支:`git checkout -b feature/你的功能名称` 3. 提交你的更改:`git commit -m '[类型] 添加了某个功能'` 4. 将分支推送到你的 Fork:`git push origin feature/你的功能名称` -5. **在 Gitee 主仓库上对该分支创建一个 Pull Request,并合并至 `Dev` 分支** +5. **在 Gitee 主仓库上对该分支创建一个 Pull Request,并合并至** **`Dev`** **分支** ### 协作规范 @@ -211,8 +203,7 @@ - [开发规范.md](文档/开发规范.md) - 涵盖代码组织、样式、命名等全方位规范 - ---- +*** ## 许可证信息 @@ -225,56 +216,51 @@ Color Card 采用 **GNU General Public License v3.0 (GPL 3.0)** 许可证发布 ### 许可证文件 - **项目完整许可证信息**:[LICENSE](./LICENSE) -- **GPL 3.0 官方文本**:https://www.gnu.org/licenses/gpl-3.0.html +- **GPL 3.0 官方文本**: ### 第三方库许可证 -本项目使用了以下第三方库: - -|库 |许可证 | -|:---|:---:| -|PySide6 |LGPL-3.0 | -|PySide6-Fluent-Widgets |GPL-3.0 | -|Pillow |MIT License | -|requests |Apache-2.0 | -|numpy |BSD-3-Clause | -|Open Color |MIT License | -|Tailwind CSS Colors |MIT License | -|Material Design Colors |Apache-2.0 | -|ColorBrewer |Apache-2.0 | -|Radix Colors |MIT License | -|Nord |MIT License | -|Dracula |MIT License | -|Solarized |MIT License | -|Gruvbox |MIT License | -|Catppuccin |MIT License | -|Rose Pine |MIT License | -|Tokyo Night |MIT License | -|Nice Color Palettes |MIT License | - - ---- +本项目使用了以下第三方库(部分): + +| 库 | 许可证 | +| :--------------------- | :----------: | +| PySide6 | LGPL-3.0 | +| PySide6-Fluent-Widgets | GPL-3.0 | +| Pillow | MIT License | +| requests | Apache-2.0 | +| numpy | BSD-3-Clause | +| Open Color | MIT License | +| Tailwind CSS Colors | MIT License | +| Material Design Colors | Apache-2.0 | +| ColorBrewer | Apache-2.0 | +| Radix Colors | MIT License | +| Nord | MIT License | +| Dracula | MIT License | +| Solarized | MIT License | +| Gruvbox | MIT License | +| Catppuccin | MIT License | +| Rose Pine | MIT License | +| Tokyo Night | MIT License | +| Nice Color Palettes | MIT License | + +*** ## 联系方式 -- **主仓库(Gitee)**:https://gitee.com/qingshangongzai/color_card -- **镜像仓库(GitHub)**:https://github.com/qingshangongzai/Color_Card -- **联系邮箱**:[hxiao_studio@163.com](mailto:hxiao_studio@163.com)、[qingshangongzai@163.com](mailto:qingshangongzai@163.com) +- **主仓库(Gitee)**: +- **镜像仓库(GitHub)**: +- **联系邮箱**: - ---- +*** **免责声明**:Color Card 仅供学习和研究使用。开发者不对因使用本工具导致的任何后果负责,请谨慎使用。 - ---- +*** **取色卡 (Color Card)** - 为摄影师和设计师打造的一站式配色工具和图片色彩分析工具\ Copyright © 2026 浮晓 HXiao Studio - ---- - +*** @@ -292,50 +278,51 @@ Unlike common color tools or websites that only provide a single function, Color **Repository URLs**: -- **Primary Repository (Gitee)**: https://gitee.com/qingshangongzai/color_card -- **Mirror Repository (GitHub)**: https://github.com/qingshangongzai/Color_Card -- **Official Website**: https://qingshangongzai.github.io/Color_Card/ +- **Primary Repository (Gitee)**: +- **Mirror Repository (GitHub)**: +- **Official Website**: ### Development Journey Since the release of v1.0.0 on 2026-02-05, the project has maintained a fast and stable iteration pace: -| Metric | Data | -| :-- | :-- | -| Released Versions | 8 versions (v1.0.0 → v1.5.1) | -| Development Period | 38 days | -| Total Updates | **102 items** | -| Average per Version | 12.75 items | - -**Detailed Category Statistics**: - -| Category | Count | Description | -| :-- | :--: | :-- | -| ✨ New Features | **32** | Including 9 core features from v1.0.0 launch | -| 🔧 Bug Fixes | **25** | Continuous bug fixes for stability | -| 🎨 UI Improvements | **25** | User experience refinements | -| ⚡ Performance | **8** | Cache mechanism, startup optimization | -| 📝 Content Adjustments | **5** | Text, naming adjustments | -| 🏗️ Code Refactoring | **3** | Code structure optimization | -| 🔮 Logic Optimization | **2** | Algorithm improvements | -| 📜 License Compliance | **1** | Open source compliance | -| 🚀 Feature Enhancement | **1** | Feature enhancements | +| Metric | Data | +| :------------------ | :--------------------------- | +| Released Versions | 9 versions (v1.0.0 → v1.6.0) | +| Development Period | 45 days | +| Total Updates | **111 items** | +| Average per Version | 12.33 items | + +**Detailed Category Statistics(portion)**: + +| Category | Count | Description | +| :--------------------- | :----: | :------------------------------------------- | +| ✨ New Features | **33** | Including 9 core features from v1.0.0 launch | +| 🔧 Bug Fixes | **27** | Continuous bug fixes for stability | +| 🎨 UI Improvements | **28** | User experience refinements | +| ⚡ Performance | **8** | Cache mechanism, startup optimization | +| 📝 Content Adjustments | **5** | Text, naming adjustments | +| 🏗️ Code Refactoring | **4** | Code structure optimization | +| 🔮 Logic Optimization | **2** | Algorithm improvements | +| ⚙️ Experience | **2** | Interaction improvements | +| 📜 License Compliance | **1** | Open source compliance | +| 🚀 Feature Enhancement | **1** | Feature enhancements | ### Key Features **One-stop Color Solution**: Complete color workflow from image analysis to color application -| Feature | Screenshot | -|---------|------------| -| **Visual Color Extraction**
Real-time color extraction via draggable color pickers, supporting multiple color spaces (HSB, LAB, HSL, CMYK, RGB) | ![Color Extraction](docs/screenshots/color-extract.png) | -| **Luminance Analysis**
9-zone luminance segmentation (Zone 0-8 based on Adobe standard) with histogram visualization | ![Luminance Analysis](docs/screenshots/luminance-extract.png) | -| **Gradient Extraction**
Generate gradient color sequences from start and end colors, supporting RGB/HSB/LAB color space interpolation | ![Gradient Extraction](docs/screenshots/Gradient%20Extract.png) | -| **Color Scheme Generation**
5 professional color schemes (Monochromatic, Analogous, Complementary, Split-Complementary, Double Complementary) with interactive color wheel | ![Color Generation](docs/screenshots/color-generation.png) | -| **Palette Collection**
Save and manage color schemes, support batch import/export in JSON format, support single palette export to Adobe ASE format | ![Palette Management](docs/screenshots/palette-management.png) | -| **Built-in Color Library**
13 major open-source color schemes including Open Color, Tailwind CSS, Material Design, totaling 661 color palettes | ![Preset Colors](docs/screenshots/preset-colors.png) | -| **Color Preview**
8 built-in scene previews (Mobile UI, Web, Illustration, Typography, Brand, Poster, Pattern, Magazine) with custom SVG support | ![Color Preview](docs/screenshots/color-preview.png) | -| **Multi-language Support**
6 languages including Simplified Chinese, Traditional Chinese, English, Japanese, French, and Russian | ![Multi-language](docs/screenshots/locales.png) | -| **Modern Interface**
Based on Fluent Design, supports dark/light theme switching | ![Dark/Light Mode](./docs/screenshots/Dark%20mode%26light%20mode.png) | +| Feature | Screenshot | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| **Visual Color Extraction**Real-time color extraction via draggable color pickers, supporting multiple color spaces (HSB, LAB, HSL, CMYK, RGB) | !\[Color Extraction]\(docs/screenshots/color-extract.png null) | +| **Luminance Analysis**9-zone luminance segmentation (Zone 0-8 based on Adobe standard) with histogram visualization | !\[Luminance Analysis]\(docs/screenshots/luminance-extract.png null) | +| **Gradient Extraction**Generate gradient color sequences from start and end colors, supporting RGB/HSB/LAB color space interpolation | !\[Gradient Extraction]\(docs/screenshots/Gradient%20Extract.png null) | +| **Color Scheme Generation**5 professional color schemes (Monochromatic, Analogous, Complementary, Split-Complementary, Double Complementary) with interactive color wheel | !\[Color Generation]\(docs/screenshots/color-generation.png null) | +| **Palette Collection**Save and manage color schemes, support batch import/export in JSON format, support single palette export to Adobe ASE format | !\[Palette Management]\(docs/screenshots/palette-management.png null) | +| **Built-in Color Library**13 major open-source color schemes including Open Color, Tailwind CSS, Material Design, totaling 661 color palettes | !\[Preset Colors]\(docs/screenshots/preset-colors.png null) | +| **Color Preview**8 built-in scene previews (Mobile UI, Web, Illustration, Typography, Brand, Poster, Pattern, Magazine) with custom SVG support | !\[Color Preview]\(docs/screenshots/color-preview\.png null) | +| **Multi-language Support**6 languages including Simplified Chinese, Traditional Chinese, English, Japanese, French, and Russian, with system language auto-detection | !\[Multi-language]\(docs/screenshots/locales.png null) | +| **Modern Interface**Based on Fluent Design, supports dark/light theme switching | !\[Dark/Light Mode]\(./docs/screenshots/Dark%20mode%26light%20mode.png null) | ### Use Cases @@ -358,8 +345,7 @@ Since the release of v1.0.0 on 2026-02-05, the project has maintained a fast and - **Color Education**: Serve as a teaching tool for color theory and practice - **Rapid Prototyping**: Quickly generate and preview color schemes, accelerate design iteration - ---- +*** ## Installation @@ -381,36 +367,31 @@ Since the release of v1.0.0 on 2026-02-05, the project has maintained a fast and #### Installation Steps 1. **Clone the repository**: - ```bash # Clone from Gitee (recommended for China) git clone https://gitee.com/qingshangongzai/color_card.git - + # Or clone from GitHub git clone https://github.com/qingshangongzai/Color_Card.git - + cd color_card ``` 2. **Create virtual environment (recommended)**: - ```bash python -m venv .venv # Activate virtual environment .\.venv\Scripts\activate # Windows ``` 3. **Install dependencies**: - ```bash pip install -r requirements.txt ``` 4. **Launch the application**: - ```bash python main.py ``` - ---- +*** ## Usage @@ -426,25 +407,23 @@ Since the release of v1.0.0 on 2026-02-05, the project has maintained a fast and ### Feature Modules -|Module |Features | -|:---|:---| -|Color Extraction |Draggable pickers, multiple color spaces, one-click copy | -|Gradient Extraction |Start/end color selection, RGB/HSB/LAB interpolation, adjustable middle colors | -|Luminance Analysis |9-zone segmentation (Zone 0-8), histogram visualization, zone highlighting | -|Color Generation |5 color schemes, interactive color wheel, luminance adjustment | -|Palette Management |Save palettes, custom names, batch import/export in JSON format, support single palette ASE format export (Adobe software compatible) | -|Color Preview |8 built-in scenes, custom SVG, intelligent color mapping | -|Built-in Library |13 open-source color schemes, 661 color palettes | - +| Module | Features | +| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------ | +| Color Extraction | Draggable pickers, multiple color spaces, one-click copy | +| Gradient Extraction | Start/end color selection, RGB/HSB/LAB interpolation, adjustable middle colors | +| Luminance Analysis | 9-zone segmentation (Zone 0-8), histogram visualization, zone highlighting | +| Color Generation | 5 color schemes, interactive color wheel, luminance adjustment | +| Palette Management | Save palettes, custom names, batch import/export in JSON format, support single palette ASE format export (Adobe software compatible) | +| Color Preview | 8 built-in scenes, custom SVG, intelligent color mapping | +| Built-in Library | 13 open-source color schemes, 661 color palettes | ---- +*** ## Development Standards This project follows PEP 8 code style guidelines and adopts modular architecture design. For detailed development standards, please refer to [开发规范.md](./开发规范.md). - ---- +*** ## Contributing @@ -468,8 +447,7 @@ All contributed code must strictly follow the project's existing development sta - [开发规范.md](./开发规范.md) - Covers code organization, style, naming, and more - ---- +*** ## License Information @@ -482,49 +460,46 @@ Color Card is released under the **GNU General Public License v3.0 (GPL 3.0)** l ### License Files - **Complete Project License Information**: [LICENSE](./LICENSE) -- **GPL 3.0 Official Text**: https://www.gnu.org/licenses/gpl-3.0.html +- **GPL 3.0 Official Text**: ### Third-party Library Licenses This project uses the following third-party libraries: -|Library |License | -|:---|:---:| -|PySide6 |LGPL-3.0 | -|PySide6-Fluent-Widgets |GPL-3.0 | -|Pillow |MIT License | -|requests |Apache-2.0 | -|numpy |BSD-3-Clause | -|Open Color |MIT License | -|Tailwind CSS Colors |MIT License | -|Material Design Colors |Apache-2.0 | -|ColorBrewer |Apache-2.0 | -|Radix Colors |MIT License | -|Nord |MIT License | -|Dracula |MIT License | -|Solarized |MIT License | -|Gruvbox |MIT License | -|Catppuccin |MIT License | -|Rose Pine |MIT License | -|Tokyo Night |MIT License | -|Nice Color Palettes |MIT License | - - ---- +| Library | License | +| :--------------------- | :----------: | +| PySide6 | LGPL-3.0 | +| PySide6-Fluent-Widgets | GPL-3.0 | +| Pillow | MIT License | +| requests | Apache-2.0 | +| numpy | BSD-3-Clause | +| Open Color | MIT License | +| Tailwind CSS Colors | MIT License | +| Material Design Colors | Apache-2.0 | +| ColorBrewer | Apache-2.0 | +| Radix Colors | MIT License | +| Nord | MIT License | +| Dracula | MIT License | +| Solarized | MIT License | +| Gruvbox | MIT License | +| Catppuccin | MIT License | +| Rose Pine | MIT License | +| Tokyo Night | MIT License | +| Nice Color Palettes | MIT License | + +*** ## Contact -- **Primary Repository (Gitee)**: https://gitee.com/qingshangongzai/color_card -- **Mirror Repository (GitHub)**: https://github.com/qingshangongzai/Color_Card -- **Email**: [hxiao_studio@163.com](mailto:hxiao_studio@163.com)、[qingshangongzai@163.com](mailto:qingshangongzai@163.com) +- **Primary Repository (Gitee)**: +- **Mirror Repository (GitHub)**: +- **Email**: - ---- +*** **Disclaimer**: Color Card is for learning and research purposes only. The developers are not responsible for any consequences resulting from the use of this tool. Please use with caution. - ---- +*** **Color Card** - An all-in-one color tool and image color analysis tool for photographers and designers\ -Copyright © 2026 浮晓 HXiao Studio \ No newline at end of file +Copyright © 2026 浮晓 HXiao Studio diff --git a/core/color.py b/core/color.py index 7c1f39904538170b9c5523dd5612425e70799820..a0cfed6999edc97bbf0220c79ee6211342d8ae83 100644 --- a/core/color.py +++ b/core/color.py @@ -3,11 +3,7 @@ import colorsys from typing import Any, Dict, List, Tuple # 第三方库导入 -try: - import numpy as np - NUMPY_AVAILABLE = True -except ImportError: - NUMPY_AVAILABLE = False +import numpy as np # 项目模块导入 from .color_scheme_cache import get_color_scheme_cache @@ -130,11 +126,11 @@ def rgb_to_lab(r: int, g: int, b: int) -> Tuple[float, float, float]: # L = 116*f(Y) - 16 (亮度分量) # A = 500*(f(X) - f(Y)) (红绿对立分量) # B = 200*(f(Y) - f(Z)) (黄蓝对立分量) - l = 116 * f(y) - 16 - a_val = 500 * (f(x) - f(y)) - b_val = 200 * (f(y) - f(z)) + L = 116 * f(y) - 16 + A = 500 * (f(x) - f(y)) + B = 200 * (f(y) - f(z)) - return l, a_val, b_val + return L, A, B def rgb_to_hex(r: int, g: int, b: int) -> str: @@ -194,8 +190,8 @@ def rgb_to_hsl(r: int, g: int, b: int) -> Tuple[float, float, float]: tuple: (色相 0-360, 饱和度 0-100, 亮度 0-100) """ r_norm, g_norm, b_norm = r / 255.0, g / 255.0, b / 255.0 - h, l, s = colorsys.rgb_to_hls(r_norm, g_norm, b_norm) - return h * 360, s * 100, l * 100 + H, L, S = colorsys.rgb_to_hls(r_norm, g_norm, b_norm) + return H * 360, S * 100, L * 100 def rgb_to_cmyk(r: int, g: int, b: int) -> Tuple[float, float, float, float]: @@ -233,17 +229,17 @@ def get_color_info(r: int, g: int, b: int) -> Dict[str, Any]: Returns: dict: 包含RGB、HSB、LAB、HEX、HSL、CMYK颜色信息的字典 """ - h, s, b_val = rgb_to_hsb(r, g, b) - l, a, b_lab = rgb_to_lab(r, g, b) - h_hsl, s_hsl, l_hsl = rgb_to_hsl(r, g, b) - c, m, y, k = rgb_to_cmyk(r, g, b) + H, S, B = rgb_to_hsb(r, g, b) + L, A, B_lab = rgb_to_lab(r, g, b) + H2, S2, L2 = rgb_to_hsl(r, g, b) + C, M, Y, K = rgb_to_cmyk(r, g, b) return { 'rgb': (r, g, b), - 'hsb': (round(h), round(s), round(b_val)), - 'lab': (round(l), round(a), round(b_lab)), - 'hsl': (round(h_hsl), round(s_hsl), round(l_hsl)), - 'cmyk': (round(c), round(m), round(y), round(k)), + 'hsb': (round(H), round(S), round(B)), + 'lab': (round(L), round(A), round(B_lab)), + 'hsl': (round(H2), round(S2), round(L2)), + 'cmyk': (round(C), round(M), round(Y), round(K)), 'rgb_display': (r, g, b), 'hex': rgb_to_hex(r, g, b) } @@ -353,7 +349,7 @@ def calculate_histogram(image, sample_step: int = 4, gamma: float = 2.2) -> List width = image.width() height = image.height() - if NUMPY_AVAILABLE and hasattr(image, 'bits'): + if hasattr(image, 'bits'): try: return _calculate_histogram_numpy(image, width, height, sample_step, gamma) except Exception: @@ -468,8 +464,8 @@ def calculate_rgb_histogram(image, sample_step: int = 4) -> Tuple[List[int], Lis width = image.width() height = image.height() - # 使用NumPy向量化计算(如果可用) - if NUMPY_AVAILABLE and hasattr(image, 'bits'): + # 使用NumPy向量化计算 + if hasattr(image, 'bits'): try: return _calculate_rgb_histogram_numpy(image, width, height, sample_step) except Exception: @@ -575,7 +571,7 @@ def hsb_to_rgb(h: float, s: float, b: float) -> Tuple[int, int, int]: return round(r * 255), round(g * 255), round(b_out * 255) -def lab_to_rgb(l: float, a: float, b: float) -> Tuple[int, int, int]: +def lab_to_rgb(L: float, A: float, B: float) -> Tuple[int, int, int]: """将LAB转换为RGB 转换步骤: @@ -586,9 +582,9 @@ def lab_to_rgb(l: float, a: float, b: float) -> Tuple[int, int, int]: 5. 转换到0-255范围 Args: - l: 亮度 (0-100) - a: 红绿对立通道 (-128-127) - b: 黄蓝对立通道 (-128-127) + L: 亮度 (0-100) + A: 红绿对立通道 (-128-127) + B: 黄蓝对立通道 (-128-127) Returns: tuple: (R 0-255, G 0-255, B 0-255) @@ -602,9 +598,9 @@ def lab_to_rgb(l: float, a: float, b: float) -> Tuple[int, int, int]: else: return 3 * (delta ** 2) * (t - 4 / 29) - y = (l + 16) / 116 - x = y + a / 500 - z = y - b / 200 + y = (L + 16) / 116 + x = y + A / 500 + z = y - B / 200 # 使用D65参考白点 x_ref, y_ref, z_ref = 0.95047, 1.00000, 1.08883 @@ -636,23 +632,23 @@ def lab_to_rgb(l: float, a: float, b: float) -> Tuple[int, int, int]: return round(r * 255), round(g * 255), round(b_out * 255) -def hsl_to_rgb(h: float, s: float, l: float) -> Tuple[int, int, int]: +def hsl_to_rgb(H: float, S: float, L: float) -> Tuple[int, int, int]: """将HSL转换为RGB Args: - h: 色相 (0-360) - s: 饱和度 (0-100) - l: 亮度 (0-100) + H: 色相 (0-360) + S: 饱和度 (0-100) + L: 亮度 (0-100) Returns: tuple: (R 0-255, G 0-255, B 0-255) """ - h_norm = h / 360.0 - s_norm = s / 100.0 - l_norm = l / 100.0 + H_norm = H / 360.0 + S_norm = S / 100.0 + L_norm = L / 100.0 - r, g, b_out = colorsys.hls_to_rgb(h_norm, l_norm, s_norm) - return round(r * 255), round(g * 255), round(b_out * 255) + R, G, B_out = colorsys.hls_to_rgb(H_norm, L_norm, S_norm) + return round(R * 255), round(G * 255), round(B_out * 255) def cmyk_to_rgb(c: float, m: float, y: float, k: float) -> Tuple[int, int, int]: @@ -679,15 +675,15 @@ def cmyk_to_rgb(c: float, m: float, y: float, k: float) -> Tuple[int, int, int]: return round(r * 255), round(g * 255), round(b_out * 255) -def generate_monochromatic(hue: float, count: int = 4, base_saturation: float = 100) -> List[Tuple[float, float, float]]: - """生成同色系配色方案 +# ========== 配色方案内部实现(私有函数)========== - 基于同一色相,通过调整饱和度和亮度生成和谐的颜色组合 +def _build_monochromatic_colors(rgb_hue: float, count: int, base_saturation: float) -> List[Tuple[float, float, float]]: + """构建同色系颜色(与色彩空间无关的内部实现) Args: - hue: 基准色相 (0-360) - count: 生成颜色数量 (默认4) - base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度序列 + rgb_hue: RGB色相值 (0-360) + count: 生成颜色数量 + base_saturation: 基准饱和度 (0-100) Returns: list: HSB颜色列表 [(h, s, b), ...] @@ -699,73 +695,82 @@ def generate_monochromatic(hue: float, count: int = 4, base_saturation: float = for i in range(count): s = max(MIN_SATURATION, min(100, saturations[i] if i < len(saturations) else 50)) b = max(40, min(100, brightnesses[i] if i < len(brightnesses) else 70)) - colors.append((hue % 360, s, b)) + colors.append((rgb_hue % 360, s, b)) return colors -def generate_analogous(hue: float, angle: float = 30, count: int = 4, base_saturation: float = 100) -> List[Tuple[float, float, float]]: - """生成邻近色配色方案 - - 在色相环上选择与基准色相邻的颜色,创造和谐统一的视觉效果 +def _calculate_analogous_hues(base_hue: float, angle: float, count: int) -> List[float]: + """计算邻近色的色相值列表 Args: - hue: 基准色相 (0-360) - angle: 邻近角度范围 (默认30度) - count: 生成颜色数量 (默认4) - base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度 + base_hue: 基准色相 + angle: 邻近角度范围 + count: 颜色数量 Returns: - list: HSB颜色列表 [(h, s, b), ...] + list: 色相值列表 """ - colors = [] if count == 4: - # 4个颜色:基准色两侧各1个,加上基准色和另一个过渡色 - hues = [ - (hue - angle) % 360, - (hue - angle / 2) % 360, - hue % 360, - (hue + angle / 2) % 360 + return [ + (base_hue - angle) % 360, + (base_hue - angle / 2) % 360, + base_hue % 360, + (base_hue + angle / 2) % 360 ] else: step = (2 * angle) / max(count - 1, 1) - hues = [(hue - angle + i * step) % 360 for i in range(count)] + return [(base_hue - angle + i * step) % 360 for i in range(count)] - # 基于基准饱和度生成变化的饱和度 - for i, h in enumerate(hues): - # 距离基准色越远,饱和度略有变化 - distance_from_center = abs(i - len(hues) / 2) / (len(hues) / 2) - saturation_variation = 1 - distance_from_center * 0.3 # 变化范围30% - s = max(60, min(100, base_saturation * saturation_variation)) - colors.append((h, s, 90)) - return colors +def _build_analogous_colors( + rgb_hues: List[float], + base_saturation: float +) -> List[Tuple[float, float, float]]: + """构建邻近色颜色(与色彩空间无关的内部实现) + + Args: + rgb_hues: RGB色相值列表 (已计算好的邻近色相) + base_saturation: 基准饱和度 (0-100) + Returns: + list: HSB颜色列表 [(h, s, b), ...] + """ + colors = [] + for i, h in enumerate(rgb_hues): + distance_from_center = abs(i - len(rgb_hues) / 2) / (len(rgb_hues) / 2) + saturation_variation = 1 - distance_from_center * 0.3 + s = max(60, min(100, base_saturation * saturation_variation)) + colors.append((h % 360, s, 90)) + return colors -def generate_complementary(hue: float, count: int = 5, base_saturation: float = 100) -> List[Tuple[float, float, float]]: - """生成互补色配色方案 - 选择色相环上相对位置的颜色(相差180度),创造强烈对比 - 所有采样点集中在两个区域:基准色区域和互补色区域 +def _build_complementary_colors( + base_hue: float, + comp_hue: float, + count: int, + base_saturation: float +) -> List[Tuple[float, float, float]]: + """构建互补色颜色(与色彩空间无关的内部实现) Args: - hue: 基准色相 (0-360) - count: 生成颜色数量 (默认5) - base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度 + base_hue: 基准RGB色相 + comp_hue: 互补RGB色相 + count: 生成颜色数量 + base_saturation: 基准饱和度 Returns: list: HSB颜色列表 [(h, s, b), ...] """ colors = [] - comp_hue = (hue + 180) % 360 if count == 5: base_saturations = _generate_saturation_steps(base_saturation, 3) comp_saturations = _generate_saturation_steps(base_saturation, 2) colors = [ - (hue, base_saturations[0], 100), - (hue, max(30, base_saturations[1]), 90), - (hue, max(30, base_saturations[2]), 80), + (base_hue, base_saturations[0], 100), + (base_hue, max(30, base_saturations[1]), 90), + (base_hue, max(30, base_saturations[2]), 80), (comp_hue, comp_saturations[0], 100), (comp_hue, max(30, comp_saturations[1]), 90), ] @@ -779,7 +784,7 @@ def generate_complementary(hue: float, count: int = 5, base_saturation: float = for i in range(base_count): s = max(30, base_saturations[i]) b = 100 - i * (20 / max(base_count, 1)) - colors.append((hue, s, max(80, b))) + colors.append((base_hue, s, max(80, b))) for i in range(comp_count): s = max(30, comp_saturations[i]) @@ -789,90 +794,185 @@ def generate_complementary(hue: float, count: int = 5, base_saturation: float = return colors -def generate_split_complementary(hue: float, angle: float = 30, count: int = 3, base_saturation: float = 100) -> List[Tuple[float, float, float]]: - """生成分离补色配色方案 - - 选择基准色和互补色两侧的颜色,既有对比又更柔和 +def _build_split_complementary_colors( + base_hue: float, + left_hue: float, + right_hue: float, + count: int, + base_saturation: float +) -> List[Tuple[float, float, float]]: + """构建分离补色颜色(与色彩空间无关的内部实现) Args: - hue: 基准色相 (0-360) - angle: 分离角度 (默认30度) - count: 生成颜色数量 (默认3) - base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度 + base_hue: 基准RGB色相 + left_hue: 左侧分离补色RGB色相 + right_hue: 右侧分离补色RGB色相 + count: 生成颜色数量 + base_saturation: 基准饱和度 Returns: list: HSB颜色列表 [(h, s, b), ...] """ colors = [] - comp_hue = (hue + 180) % 360 - left_comp = (comp_hue - angle) % 360 - right_comp = (comp_hue + angle) % 360 if count == 3: - # 3个颜色:基准色 + 两个分离补色 colors = [ - (hue, base_saturation, 100), - (left_comp, max(50, base_saturation * 0.9), 100), - (right_comp, max(50, base_saturation * 0.9), 100) + (base_hue, base_saturation, 100), + (left_hue, max(50, base_saturation * 0.9), 100), + (right_hue, max(50, base_saturation * 0.9), 100) ] else: - colors.append((hue, base_saturation, 100)) - colors.append((left_comp, max(50, base_saturation * 0.9), 100)) - colors.append((right_comp, max(50, base_saturation * 0.9), 100)) + colors.extend([ + (base_hue, base_saturation, 100), + (left_hue, max(50, base_saturation * 0.9), 100), + (right_hue, max(50, base_saturation * 0.9), 100) + ]) remaining = count - 3 for i in range(remaining): - blend_hue = (hue + (i + 1) * 60) % 360 + blend_hue = (base_hue + (i + 1) * 60) % 360 s = max(50, base_saturation * (0.7 - i * 0.1)) colors.append((blend_hue, s, 85)) return colors -def generate_double_complementary(hue: float, angle: float = 30, count: int = 4, base_saturation: float = 100) -> List[Tuple[float, float, float]]: - """生成双补色配色方案 - - 选择两组互补色,创造丰富而平衡的配色方案 +def _build_double_complementary_colors( + hues: List[float], + saturations: List[float], + count: int +) -> List[Tuple[float, float, float]]: + """构建双补色颜色(与色彩空间无关的内部实现) Args: - hue: 基准色相 (0-360) - angle: 分离角度 (默认30度) - count: 生成颜色数量 (默认4) - base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度 + hues: RGB色相值列表 [基准色, 互补色, 第二色, 第二互补色] + saturations: 饱和度列表 + count: 生成颜色数量 Returns: list: HSB颜色列表 [(h, s, b), ...] """ colors = [] - comp_hue = (hue + 180) % 360 - second_hue = (hue + angle) % 360 - second_comp = (second_hue + 180) % 360 if count == 4: - # 4个颜色:两组互补色,使用基准饱和度 colors = [ - (hue, base_saturation, 100), - (comp_hue, max(50, base_saturation * 0.9), 100), - (second_hue, max(50, base_saturation * 0.9), 100), - (second_comp, max(50, base_saturation * 0.8), 100) + (hues[0], saturations[0], 100), + (hues[1], max(50, saturations[1] * 0.9), 100), + (hues[2], max(50, saturations[2] * 0.9), 100), + (hues[3], max(50, saturations[3] * 0.8), 100) ] else: - hues = [hue, comp_hue, second_hue, second_comp] - saturations = [ - base_saturation, - max(50, base_saturation * 0.9), - max(50, base_saturation * 0.9), - max(50, base_saturation * 0.8) - ] for i in range(min(count, 4)): colors.append((hues[i], saturations[i], 95)) for i in range(4, count): - blend_hue = (hue + i * 45) % 360 - s = max(50, base_saturation * (0.7 - (i - 4) * 0.1)) + blend_hue = (hues[0] + i * 45) % 360 + s = max(50, saturations[0] * (0.7 - (i - 4) * 0.1)) colors.append((blend_hue, s, 85)) return colors +# ========== RGB配色方案公共API ========== + +def generate_monochromatic(hue: float, count: int = 4, base_saturation: float = 100) -> List[Tuple[float, float, float]]: + """生成同色系配色方案 + + 基于同一色相,通过调整饱和度和亮度生成和谐的颜色组合 + + Args: + hue: 基准色相 (0-360) + count: 生成颜色数量 (默认4) + base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度序列 + + Returns: + list: HSB颜色列表 [(h, s, b), ...] + """ + return _build_monochromatic_colors(hue, count, base_saturation) + + +def generate_analogous(hue: float, angle: float = 30, count: int = 4, base_saturation: float = 100) -> List[Tuple[float, float, float]]: + """生成邻近色配色方案 + + 在色相环上选择与基准色相邻的颜色,创造和谐统一的视觉效果 + + Args: + hue: 基准色相 (0-360) + angle: 邻近角度范围 (默认30度) + count: 生成颜色数量 (默认4) + base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度 + + Returns: + list: HSB颜色列表 [(h, s, b), ...] + """ + hues = _calculate_analogous_hues(hue, angle, count) + return _build_analogous_colors(hues, base_saturation) + + +def generate_complementary(hue: float, count: int = 5, base_saturation: float = 100) -> List[Tuple[float, float, float]]: + """生成互补色配色方案 + + 选择色相环上相对位置的颜色(相差180度),创造强烈对比 + 所有采样点集中在两个区域:基准色区域和互补色区域 + + Args: + hue: 基准色相 (0-360) + count: 生成颜色数量 (默认5) + base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度 + + Returns: + list: HSB颜色列表 [(h, s, b), ...] + """ + comp_hue = (hue + 180) % 360 + return _build_complementary_colors(hue, comp_hue, count, base_saturation) + + +def generate_split_complementary(hue: float, angle: float = 30, count: int = 3, base_saturation: float = 100) -> List[Tuple[float, float, float]]: + """生成分离补色配色方案 + + 选择基准色和互补色两侧的颜色,既有对比又更柔和 + + Args: + hue: 基准色相 (0-360) + angle: 分离角度 (默认30度) + count: 生成颜色数量 (默认3) + base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度 + + Returns: + list: HSB颜色列表 [(h, s, b), ...] + """ + comp_hue = (hue + 180) % 360 + left_comp = (comp_hue - angle) % 360 + right_comp = (comp_hue + angle) % 360 + return _build_split_complementary_colors(hue, left_comp, right_comp, count, base_saturation) + + +def generate_double_complementary(hue: float, angle: float = 30, count: int = 4, base_saturation: float = 100) -> List[Tuple[float, float, float]]: + """生成双补色配色方案 + + 选择两组互补色,创造丰富而平衡的配色方案 + + Args: + hue: 基准色相 (0-360) + angle: 分离角度 (默认30度) + count: 生成颜色数量 (默认4) + base_saturation: 基准饱和度 (0-100),用于生成变化的饱和度 + + Returns: + list: HSB颜色列表 [(h, s, b), ...] + """ + comp_hue = (hue + 180) % 360 + second_hue = (hue + angle) % 360 + second_comp = (second_hue + 180) % 360 + + hues = [hue, comp_hue, second_hue, second_comp] + saturations = [ + base_saturation, + max(50, base_saturation * 0.9), + max(50, base_saturation * 0.9), + max(50, base_saturation * 0.8) + ] + return _build_double_complementary_colors(hues, saturations, count) + + def adjust_brightness(hsb_colors: List[Tuple[float, float, float]], brightness_delta: float) -> List[Tuple[float, float, float]]: """调整配色方案的明度 @@ -950,7 +1050,7 @@ class _ColorCube: use_numpy: 是否使用 numpy 优化 """ self.pixels = pixels - self._use_numpy = use_numpy and NUMPY_AVAILABLE + self._use_numpy = use_numpy self._cache_volume = None self._cache_avg_color = None self._cache_ranges = None @@ -1093,7 +1193,7 @@ def _mmcq_quantize(pixels: List[Tuple[int, int, int]], count: int) -> List[_Colo return [] # 判断是否使用 numpy 优化(像素数量较多时) - use_numpy = NUMPY_AVAILABLE and len(pixels) > 1000 + use_numpy = len(pixels) > 1000 # 初始立方体包含所有像素 cubes = [_ColorCube(pixels, use_numpy)] @@ -1145,7 +1245,7 @@ def _extract_pixels_fast(image, sample_step: int = 4) -> List[Tuple[int, int, in width = image.width() height = image.height() - if NUMPY_AVAILABLE and hasattr(image, 'bits'): + if hasattr(image, 'bits'): # 使用 numpy 批量读取像素(QImage 格式) try: # 将 QImage 转换为 numpy 数组 @@ -1167,7 +1267,8 @@ def _extract_pixels_fast(image, sample_step: int = 4) -> List[Tuple[int, int, in return pixels except Exception: - pass # 失败时回退到普通方法 + # NumPy 加速失败,静默回退到普通方法 + pass # 普通方法(逐个读取) for y in range(0, height, sample_step): @@ -1188,10 +1289,9 @@ def _extract_pixels_fast(image, sample_step: int = 4) -> List[Tuple[int, int, in elif hasattr(image, 'size') and hasattr(image, 'getpixel'): width, height = image.size - if NUMPY_AVAILABLE and hasattr(image, 'convert'): + if hasattr(image, 'convert'): # 使用 numpy 批量读取像素(PIL Image 格式) try: - import numpy as np arr = np.array(image.convert('RGB')) # 采样像素 @@ -1279,7 +1379,7 @@ def _extract_pixels_with_positions_fast( width = image.width() height = image.height() - if NUMPY_AVAILABLE and hasattr(image, 'bits'): + if hasattr(image, 'bits'): # 使用 numpy 批量读取像素 try: image = image.convertToFormat(image.Format.Format_RGB888) @@ -1312,7 +1412,7 @@ def _extract_pixels_with_positions_fast( elif hasattr(image, 'size') and hasattr(image, 'getpixel'): width, height = image.size - if NUMPY_AVAILABLE and hasattr(image, 'convert'): + if hasattr(image, 'convert'): # 使用 numpy 批量读取像素 try: arr = np.array(image.convert('RGB')) @@ -1371,7 +1471,7 @@ def find_dominant_color_positions( return [(0.5, 0.5)] * len(dominant_colors) # 使用 numpy 加速聚类计算 - if NUMPY_AVAILABLE and len(pixel_data) > 100: + if len(pixel_data) > 100: try: # 转换为 numpy 数组 pixel_array = np.array(pixel_data, dtype=np.float32) # [x, y, r, g, b] @@ -1436,41 +1536,6 @@ def find_dominant_color_positions( # ==================== RYB 色彩空间支持 ==================== -# RYB 色相映射表:RGB色相角度 -> RYB色相角度 -# 基于传统美术色轮的红-黄-蓝三原色系统 -RYB_HUE_MAP_RGB_TO_RYB = { - 0: 0, # 红 - 30: 30, # 橙红 - 60: 60, # 橙 - 90: 90, # 橙黄 - 120: 120, # 黄 - 150: 150, # 黄绿 - 180: 180, # 绿(RYB中绿在180度) - 210: 210, # 青绿 - 240: 240, # 青 - 270: 270, # 蓝 - 300: 300, # 紫 - 330: 330, # 品红 - 360: 360, # 红 -} - -# RYB 色相映射表:RYB色相角度 -> RGB色相角度 -RYB_HUE_MAP_RYB_TO_RGB = { - 0: 0, # 红 - 30: 30, # 橙红 - 60: 60, # 橙 - 90: 90, # 橙黄 - 120: 120, # 黄 - 150: 150, # 黄绿 - 180: 180, # 绿(RYB中绿在180度,RGB中绿在120度) - 210: 210, # 青绿 - 240: 240, # 青 - 270: 270, # 蓝 - 300: 300, # 紫 - 330: 330, # 品红 - 360: 360, # 红 -} - def rgb_hue_to_ryb_hue(rgb_hue: float) -> float: """将 RGB 色相转换为 RYB 色相 @@ -1540,6 +1605,8 @@ def ryb_hue_to_rgb_hue(ryb_hue: float) -> float: return hue +# ========== RYB配色方案公共API ========== + def generate_ryb_monochromatic(ryb_hue: float, count: int = 4, base_saturation: float = 100) -> List[Tuple[float, float, float]]: """生成 RYB 同色系配色方案 @@ -1551,18 +1618,8 @@ def generate_ryb_monochromatic(ryb_hue: float, count: int = 4, base_saturation: Returns: list: HSB颜色列表 [(h, s, b), ...] (RGB色相) """ - colors = [] - saturations = _generate_saturation_steps(base_saturation, count) - brightnesses = _generate_brightness_steps(count) - rgb_hue = ryb_hue_to_rgb_hue(ryb_hue) - - for i in range(count): - s = max(MIN_SATURATION, min(100, saturations[i] if i < len(saturations) else 50)) - b = max(40, min(100, brightnesses[i] if i < len(brightnesses) else 70)) - colors.append((rgb_hue % 360, s, b)) - - return colors + return _build_monochromatic_colors(rgb_hue, count, base_saturation) def generate_ryb_analogous(ryb_hue: float, angle: float = 30, count: int = 4, base_saturation: float = 100) -> List[Tuple[float, float, float]]: @@ -1577,30 +1634,11 @@ def generate_ryb_analogous(ryb_hue: float, angle: float = 30, count: int = 4, ba Returns: list: HSB颜色列表 [(h, s, b), ...] (RGB色相) """ - colors = [] - if count == 4: - # 4个颜色:基准色两侧各1个,加上基准色和另一个过渡色 - ryb_hues = [ - (ryb_hue - angle) % 360, - (ryb_hue - angle / 2) % 360, - ryb_hue % 360, - (ryb_hue + angle / 2) % 360 - ] - else: - step = (2 * angle) / max(count - 1, 1) - ryb_hues = [(ryb_hue - angle + i * step) % 360 for i in range(count)] - - # 基于基准饱和度生成变化的饱和度 - for i, h in enumerate(ryb_hues): - # 距离基准色越远,饱和度略有变化 - distance_from_center = abs(i - len(ryb_hues) / 2) / (len(ryb_hues) / 2) - saturation_variation = 1 - distance_from_center * 0.3 # 变化范围30% - s = max(60, min(100, base_saturation * saturation_variation)) - # 转换 RYB 色相到 RGB 色相 - rgb_hue = ryb_hue_to_rgb_hue(h) - colors.append((rgb_hue, s, 90)) - - return colors + # 在RYB空间计算邻近色相 + ryb_hues = _calculate_analogous_hues(ryb_hue, angle, count) + # 转换为RGB色相 + rgb_hues = [ryb_hue_to_rgb_hue(h) for h in ryb_hues] + return _build_analogous_colors(rgb_hues, base_saturation) def generate_ryb_complementary(ryb_hue: float, count: int = 5, base_saturation: float = 100) -> List[Tuple[float, float, float]]: @@ -1616,40 +1654,12 @@ def generate_ryb_complementary(ryb_hue: float, count: int = 5, base_saturation: Returns: list: HSB颜色列表 [(h, s, b), ...] (RGB色相) """ - colors = [] + # 在RYB空间计算互补色 ryb_comp_hue = (ryb_hue + 180) % 360 - + # 转换为RGB色相 rgb_hue = ryb_hue_to_rgb_hue(ryb_hue) rgb_comp_hue = ryb_hue_to_rgb_hue(ryb_comp_hue) - - if count == 5: - base_saturations = _generate_saturation_steps(base_saturation, 3) - comp_saturations = _generate_saturation_steps(base_saturation, 2) - colors = [ - (rgb_hue, base_saturations[0], 100), - (rgb_hue, max(30, base_saturations[1]), 90), - (rgb_hue, max(30, base_saturations[2]), 80), - (rgb_comp_hue, comp_saturations[0], 100), - (rgb_comp_hue, max(30, comp_saturations[1]), 90), - ] - else: - base_count = (count + 1) // 2 - comp_count = count - base_count - - base_saturations = _generate_saturation_steps(base_saturation, base_count) - comp_saturations = _generate_saturation_steps(base_saturation, comp_count) - - for i in range(base_count): - s = max(30, base_saturations[i]) - b = 100 - i * (20 / max(base_count, 1)) - colors.append((rgb_hue, s, max(80, b))) - - for i in range(comp_count): - s = max(30, comp_saturations[i]) - b = 100 - i * (20 / max(comp_count, 1)) - colors.append((rgb_comp_hue, s, max(80, b))) - - return colors + return _build_complementary_colors(rgb_hue, rgb_comp_hue, count, base_saturation) def generate_ryb_split_complementary(ryb_hue: float, angle: float = 30, count: int = 3, base_saturation: float = 100) -> List[Tuple[float, float, float]]: @@ -1664,34 +1674,17 @@ def generate_ryb_split_complementary(ryb_hue: float, angle: float = 30, count: i Returns: list: HSB颜色列表 [(h, s, b), ...] (RGB色相) """ - colors = [] + # 在RYB空间计算 ryb_comp_hue = (ryb_hue + 180) % 360 - ryb_left_comp = (ryb_comp_hue - angle) % 360 - ryb_right_comp = (ryb_comp_hue + angle) % 360 + ryb_left = (ryb_comp_hue - angle) % 360 + ryb_right = (ryb_comp_hue + angle) % 360 - # 转换到 RGB 色相 + # 转换为RGB色相 rgb_hue = ryb_hue_to_rgb_hue(ryb_hue) - rgb_left = ryb_hue_to_rgb_hue(ryb_left_comp) - rgb_right = ryb_hue_to_rgb_hue(ryb_right_comp) + rgb_left = ryb_hue_to_rgb_hue(ryb_left) + rgb_right = ryb_hue_to_rgb_hue(ryb_right) - if count == 3: - colors = [ - (rgb_hue, base_saturation, 100), - (rgb_left, max(50, base_saturation * 0.9), 100), - (rgb_right, max(50, base_saturation * 0.9), 100) - ] - else: - colors.append((rgb_hue, base_saturation, 100)) - colors.append((rgb_left, max(50, base_saturation * 0.9), 100)) - colors.append((rgb_right, max(50, base_saturation * 0.9), 100)) - remaining = count - 3 - for i in range(remaining): - blend_hue = (ryb_hue + (i + 1) * 60) % 360 - rgb_blend = ryb_hue_to_rgb_hue(blend_hue) - s = max(50, base_saturation * (0.7 - i * 0.1)) - colors.append((rgb_blend, s, 85)) - - return colors + return _build_split_complementary_colors(rgb_hue, rgb_left, rgb_right, count, base_saturation) def generate_ryb_double_complementary(ryb_hue: float, angle: float = 30, count: int = 4, base_saturation: float = 100) -> List[Tuple[float, float, float]]: @@ -1706,41 +1699,26 @@ def generate_ryb_double_complementary(ryb_hue: float, angle: float = 30, count: Returns: list: HSB颜色列表 [(h, s, b), ...] (RGB色相) """ - colors = [] + # 在RYB空间计算 ryb_comp_hue = (ryb_hue + 180) % 360 ryb_second_hue = (ryb_hue + angle) % 360 ryb_second_comp = (ryb_second_hue + 180) % 360 - # 转换到 RGB 色相 - rgb_hue = ryb_hue_to_rgb_hue(ryb_hue) - rgb_comp = ryb_hue_to_rgb_hue(ryb_comp_hue) - rgb_second = ryb_hue_to_rgb_hue(ryb_second_hue) - rgb_second_comp = ryb_hue_to_rgb_hue(ryb_second_comp) - - if count == 4: - colors = [ - (rgb_hue, base_saturation, 100), - (rgb_comp, max(50, base_saturation * 0.9), 100), - (rgb_second, max(50, base_saturation * 0.9), 100), - (rgb_second_comp, max(50, base_saturation * 0.8), 100) - ] - else: - hues = [rgb_hue, rgb_comp, rgb_second, rgb_second_comp] - saturations = [ - base_saturation, - max(50, base_saturation * 0.9), - max(50, base_saturation * 0.9), - max(50, base_saturation * 0.8) - ] - for i in range(min(count, 4)): - colors.append((hues[i], saturations[i], 95)) - for i in range(4, count): - blend_ryb = (ryb_hue + i * 45) % 360 - rgb_blend = ryb_hue_to_rgb_hue(blend_ryb) - s = max(50, base_saturation * (0.7 - (i - 4) * 0.1)) - colors.append((rgb_blend, s, 85)) - - return colors + # 转换为RGB色相 + rgb_hues = [ + ryb_hue_to_rgb_hue(ryb_hue), + ryb_hue_to_rgb_hue(ryb_comp_hue), + ryb_hue_to_rgb_hue(ryb_second_hue), + ryb_hue_to_rgb_hue(ryb_second_comp) + ] + saturations = [ + base_saturation, + max(50, base_saturation * 0.9), + max(50, base_saturation * 0.9), + max(50, base_saturation * 0.8) + ] + + return _build_double_complementary_colors(rgb_hues, saturations, count) def get_scheme_preview_colors_ryb( diff --git a/core/colorblind.py b/core/colorblind.py index 591fbce958efadd831226844222de58e7f4b45c5..92fa6207db72bd575a8faaddabe8e6b87ded624c 100644 --- a/core/colorblind.py +++ b/core/colorblind.py @@ -63,95 +63,95 @@ def rgb_to_lms(r: int, g: int, b: int) -> Tuple[float, float, float]: b_linear = gamma_correction(b_norm) # RGB 到 LMS 转换矩阵 (Bradford) - l = 0.8951 * r_linear + 0.2664 * g_linear - 0.1614 * b_linear - m = -0.7502 * r_linear + 1.7135 * g_linear + 0.0367 * b_linear - s = 0.0389 * r_linear - 0.0685 * g_linear + 1.0296 * b_linear - - return (l, m, s) + L = 0.8951 * r_linear + 0.2664 * g_linear - 0.1614 * b_linear + M = -0.7502 * r_linear + 1.7135 * g_linear + 0.0367 * b_linear + S = 0.0389 * r_linear - 0.0685 * g_linear + 1.0296 * b_linear + + return (L, M, S) -def lms_to_rgb(l: float, m: float, s: float) -> Tuple[int, int, int]: +def lms_to_rgb(L: float, M: float, S: float) -> Tuple[int, int, int]: """将 LMS 转换回 RGB 色彩空间 - + Args: - l: 长波视锥响应 - m: 中波视锥响应 - s: 短波视锥响应 - + L: 长波视锥响应 + M: 中波视锥响应 + S: 短波视锥响应 + Returns: - (r, g, b) 元组,范围 0-255 + (R, G, B) 元组,范围 0-255 """ # LMS 到 RGB 转换矩阵 (Bradford 逆矩阵) - r_linear = 0.986993 * l - 0.147054 * m + 0.159963 * s - g_linear = 0.432305 * l + 0.51836 * m + 0.049291 * s - b_linear = -0.008529 * l + 0.040043 * m + 0.968487 * s - + r_linear = 0.986993 * L - 0.147054 * M + 0.159963 * S + g_linear = 0.432305 * L + 0.51836 * M + 0.049291 * S + b_linear = -0.008529 * L + 0.040043 * M + 0.968487 * S + # 线性 RGB 到 sRGB 的伽马校正 def gamma_correction_inv(c): if c <= 0.0031308: return c * 12.92 else: return 1.055 * (c ** (1.0 / 2.4)) - 0.055 - + r_norm = gamma_correction_inv(r_linear) g_norm = gamma_correction_inv(g_linear) b_norm = gamma_correction_inv(b_linear) - + # 裁剪到 0-1 范围并转换为 0-255 - r = int(max(0, min(1, r_norm)) * 255) - g = int(max(0, min(1, g_norm)) * 255) - b = int(max(0, min(1, b_norm)) * 255) - - return (r, g, b) + R = int(max(0, min(1, r_norm)) * 255) + G = int(max(0, min(1, g_norm)) * 255) + B = int(max(0, min(1, b_norm)) * 255) + + return (R, G, B) -def simulate_protanopia(l: float, m: float, s: float) -> Tuple[float, float, float]: +def simulate_protanopia(L: float, M: float, S: float) -> Tuple[float, float, float]: """模拟红色盲 (Protanopia) - + 红色视锥细胞缺失,L 通道信息丢失。 使用红色盲模拟矩阵。 """ # 红色盲转换:L 通道由 M 和 S 估算 - l_blind = 0.0 * l + 2.02344 * m - 2.52581 * s - m_blind = m - s_blind = s - return (l_blind, m_blind, s_blind) + L_blind = 0.0 * L + 2.02344 * M - 2.52581 * S + M_blind = M + S_blind = S + return (L_blind, M_blind, S_blind) -def simulate_deuteranopia(l: float, m: float, s: float) -> Tuple[float, float, float]: +def simulate_deuteranopia(L: float, M: float, S: float) -> Tuple[float, float, float]: """模拟绿色盲 (Deuteranopia) - + 绿色视锥细胞缺失,M 通道信息丢失。 使用绿色盲模拟矩阵。 """ # 绿色盲转换:M 通道由 L 和 S 估算 - l_blind = l - m_blind = 0.49421 * l + 0.0 * m + 1.24827 * s - s_blind = s - return (l_blind, m_blind, s_blind) + L_blind = L + M_blind = 0.49421 * L + 0.0 * M + 1.24827 * S + S_blind = S + return (L_blind, M_blind, S_blind) -def simulate_tritanopia(l: float, m: float, s: float) -> Tuple[float, float, float]: +def simulate_tritanopia(L: float, M: float, S: float) -> Tuple[float, float, float]: """模拟蓝色盲 (Tritanopia) - + 蓝色视锥细胞缺失,S 通道信息丢失。 使用蓝色盲模拟矩阵。 """ # 蓝色盲转换:S 通道由 L 和 M 估算 - l_blind = l - m_blind = m - s_blind = -0.395913 * l + 0.801109 * m + 0.0 * s - return (l_blind, m_blind, s_blind) + L_blind = L + M_blind = M + S_blind = -0.395913 * L + 0.801109 * M + 0.0 * S + return (L_blind, M_blind, S_blind) -def simulate_achromatopsia(l: float, m: float, s: float) -> Tuple[float, float, float]: +def simulate_achromatopsia(L: float, M: float, S: float) -> Tuple[float, float, float]: """模拟全色盲 (Achromatopsia) - + 完全无法感知颜色,转换为灰度。 使用 LMS 到灰度的转换。 """ # 转换为灰度值 (使用亮度权重) - gray = 0.299 * l + 0.587 * m + 0.114 * s + gray = 0.299 * L + 0.587 * M + 0.114 * S return (gray, gray, gray) @@ -176,25 +176,25 @@ def simulate_colorblind( if colorblind_type == 'normal': return rgb - r, g, b = rgb - + R, G, B = rgb + # 转换为 LMS 空间 - l, m, s = rgb_to_lms(r, g, b) - + L, M, S = rgb_to_lms(R, G, B) + # 应用色盲模拟 if colorblind_type == 'protanopia': - l, m, s = simulate_protanopia(l, m, s) + L, M, S = simulate_protanopia(L, M, S) elif colorblind_type == 'deuteranopia': - l, m, s = simulate_deuteranopia(l, m, s) + L, M, S = simulate_deuteranopia(L, M, S) elif colorblind_type == 'tritanopia': - l, m, s = simulate_tritanopia(l, m, s) + L, M, S = simulate_tritanopia(L, M, S) elif colorblind_type == 'achromatopsia': - l, m, s = simulate_achromatopsia(l, m, s) + L, M, S = simulate_achromatopsia(L, M, S) else: return rgb - + # 转换回 RGB - return lms_to_rgb(l, m, s) + return lms_to_rgb(L, M, S) def get_colorblind_info(colorblind_type: str) -> Dict[str, str]: diff --git a/core/config.py b/core/config.py index b27d43385979af2110c5ccd8055ddde69b29e5ab..da1f35df9d506199df3778b311545e592729e8ff 100644 --- a/core/config.py +++ b/core/config.py @@ -3,7 +3,6 @@ import json import os import shutil import sys -import uuid from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -80,7 +79,7 @@ class ConfigManager: "color_wheel_mode": "RGB", "theme": "auto", "color_wheel_labels_visible": True, - "language": "ZW_JT" + "language": "auto" }, "scheme": { "default_scheme": "monochromatic", diff --git a/core/gradient.py b/core/gradient.py index 8f0cf2f9901de91c69fbb8ab34a9465bec5743d0..3f663c786645c67207a3727b62f87d303df47c5e 100644 --- a/core/gradient.py +++ b/core/gradient.py @@ -101,36 +101,36 @@ def _interpolate_lab(start_rgb: Tuple[int, int, int], end_rgb: Tuple[int, int, i List[Tuple[int, int, int]]: RGB颜色列表,包含起始色、中间色、结束色 """ # 转换为LAB - l1, a1, b1 = rgb_to_lab(*start_rgb) - l2, a2, b2 = rgb_to_lab(*end_rgb) + L1, A1, B1 = rgb_to_lab(*start_rgb) + L2, A2, B2 = rgb_to_lab(*end_rgb) colors = [start_rgb] total_segments = steps + 1 for i in range(1, steps + 1): t = i / total_segments - l = l1 + (l2 - l1) * t - a = a1 + (a2 - a1) * t - b = b1 + (b2 - b1) * t + L = L1 + (L2 - L1) * t + A = A1 + (A2 - A1) * t + B = B1 + (B2 - B1) * t # 转换回RGB - colors.append(_lab_to_rgb(l, a, b)) + colors.append(_lab_to_rgb(L, A, B)) colors.append(end_rgb) return colors -def _lab_to_rgb(l: float, a: float, b: float) -> Tuple[int, int, int]: +def _lab_to_rgb(L: float, A: float, B: float) -> Tuple[int, int, int]: """将LAB颜色空间转换为RGB 这是rgb_to_lab的逆运算 Args: - l: 亮度分量 (0-100) - a: 红绿对立分量 (-128-127) - b: 黄蓝对立分量 (-128-127) + L: 亮度分量 (0-100) + A: 红绿对立分量 (-128-127) + B: 黄蓝对立分量 (-128-127) Returns: - Tuple[int, int, int]: RGB颜色值 (r, g, b),每个值范围0-255 + Tuple[int, int, int]: RGB颜色值 (R, G, B),每个值范围0-255 """ # 步骤1: 从LAB转换到XYZ def f_inv(t: float) -> float: @@ -144,9 +144,9 @@ def _lab_to_rgb(l: float, a: float, b: float) -> Tuple[int, int, int]: x_ref, y_ref, z_ref = 0.95047, 1.00000, 1.08883 # 计算f(Y), f(X), f(Z) - fy = (l + 16) / 116 - fx = fy + a / 500 - fz = fy - b / 200 + fy = (L + 16) / 116 + fx = fy + A / 500 + fz = fy - B / 200 # 计算XYZ x = x_ref * f_inv(fx) diff --git a/core/image_service.py b/core/image_service.py index 30430c2b3a1829282aae9111b2831cf82bb56e53..a1104458b5deac14446e93bd649c3648f78eebc2 100644 --- a/core/image_service.py +++ b/core/image_service.py @@ -12,8 +12,7 @@ from typing import Optional # 第三方库导入 from PIL import Image -from PIL.ExifTags import TAGS -from PySide6.QtCore import QObject, QThread, Signal +from PySide6.QtCore import QObject, QThread, Signal, Qt from PySide6.QtGui import QImage, QPixmap # 项目模块导入 @@ -612,6 +611,3 @@ class ImageService(QObject): dict: 内存统计信息 """ return self._get_memory_manager().get_memory_stats() - - -from PySide6.QtCore import Qt diff --git a/core/svg_color_mapper.py b/core/svg_color_mapper.py index 4691c40ae733651e46b6d7b1a11e39537a2726d6..7246e34070341abc5429e0edf90a06bc2b207abb 100644 --- a/core/svg_color_mapper.py +++ b/core/svg_color_mapper.py @@ -35,7 +35,7 @@ class SVGElementInfo: stroke_color: Optional[str] = None # 原始 stroke 颜色 area: float = 0.0 # 元素面积(用于排序) is_visible: bool = True # 是否可见 - fixed_color: Optional[str] = None # 固定颜色设置(black/original) + fixed_color: Optional[str] = None # 固定颜色设置(black/original),仅语义化映射使用 is_semantic: bool = False # 是否通过语义化标识(class/id关键词)分类 is_transparent: bool = False # 是否是透明元素(无 fill 但有 stroke) z_index: int = 0 # 绘制顺序(文档顺序,越大越在上层) @@ -104,47 +104,12 @@ class SVGElementClassifier: """ tag = element.tag.split('}')[-1] if '}' in element.tag else element.tag - # 1. 检查 class 属性(最高优先级) + # 检查 class 属性 element_class = element.get('class', '') if element_class: for keyword, elem_type in self.CLASS_KEYWORDS.items(): if keyword in element_class.lower(): - return elem_type, True # 通过语义化标识分类 - - # 2. 检查 id 属性 - element_id = element.get('id', '') - if element_id: - for keyword, elem_type in self.CLASS_KEYWORDS.items(): - if keyword in element_id.lower(): - return elem_type, True # 通过语义化标识分类 - - # 3. 根据标签类型和上下文智能判断(非语义化,是启发式规则) - if tag == 'rect': - # 矩形分类策略: - # - 如果只有一个矩形或面积最大 -> 可能是背景 - # - 如果有多个矩形且不是最大的 -> 可能是装饰/主元素 - # - 大面积矩形(>10000)-> 背景 - if is_largest_rect and area > 5000: - return ElementType.BACKGROUND, False - elif area > 10000: - return ElementType.BACKGROUND, False - elif total_rect_count > 1: - # 多个矩形时,小矩形作为装饰元素 - return (ElementType.PRIMARY if area < 5000 else ElementType.BACKGROUND), False - else: - return ElementType.BACKGROUND, False - - elif tag in ['path', 'polygon', 'polyline']: - return ElementType.PRIMARY, False - - elif tag in ['circle', 'ellipse']: - return ElementType.PRIMARY, False - - elif tag == 'line': - return ElementType.SECONDARY, False - - elif tag in ['text', 'tspan']: - return ElementType.TEXT, False + return elem_type, True return ElementType.UNKNOWN, False @@ -492,13 +457,10 @@ class SVGColorMapper: for elem in sorted_elements: if elem.bounding_box is None: continue - + if elem.is_transparent: continue - - if elem.fixed_color: - continue - + # 只检测大面积元素,避免小元素被误判 if elem.area < 10000: continue @@ -602,14 +564,8 @@ class SVGColorMapper: continue # 应用颜色 - 直接设置内联属性,这会覆盖 CSS 类样式 - # 1. 处理 fill - should_apply_fill = ( - elem_info.fill_color is not None or # 有显式 fill - elem_info.tag in ['text', 'tspan'] or # 文字元素 - elem_info.element_type in [ElementType.PRIMARY, ElementType.SECONDARY, ElementType.ACCENT, ElementType.BACKGROUND] # 图形元素 - ) - - if should_apply_fill: + # 处理 fill + if elem_info.fill_color is not None: elem.set('fill', color) # 2. 处理 stroke @@ -771,8 +727,6 @@ class SVGColorMapper: # 为所有可见元素分配颜色 print("排序后的元素:") for i, elem in enumerate(visible_elements[:10]): - x = elem.attributes.get('x', 'N/A') - y = elem.attributes.get('y', 'N/A') fill = elem.fill_color or 'N/A' print(f" {i}: fill={fill}, area={elem.area:.2f}") @@ -846,14 +800,6 @@ class SVGColorMapper: if elem is None: continue - # 处理固定颜色元素 - if elem_info.fixed_color: - if elem_info.fixed_color == 'black': - # 固定为黑色 - self._apply_color_to_element(elem, elem_info, '#000000') - # fixed_color == 'original' 时保持原色,不做任何操作 - continue - # 应用 fill 颜色映射 if elem_info.fill_color: fill_lower = elem_info.fill_color.lower() @@ -1074,7 +1020,7 @@ class SVGColorMapper: if modified_css != css_text: style_elem.text = modified_css - print(f"已更新 CSS 样式中的颜色映射") + print("已更新 CSS 样式中的颜色映射") def set_element_color(self, element_id: str, color: str, color_type: str = 'fill') -> bool: """设置单个元素的颜色 diff --git a/dialogs/__init__.py b/dialogs/__init__.py index a0bf7e5b9b6e462b8f97ea160cc38da3ec852153..5ab821944f3166aa8d72c901b0c1f63a350af045 100644 --- a/dialogs/__init__.py +++ b/dialogs/__init__.py @@ -1,6 +1,7 @@ """对话框模块""" from .about_dialog import AboutDialog +from .base_frameless_dialog import BaseFramelessDialog from .colorblind_dialog import ColorblindPreviewDialog from .contrast_dialog import ContrastCheckDialog from .edit_palette import EditPaletteDialog, ColorPickerDialog @@ -9,6 +10,7 @@ from .update_dialog import UpdateAvailableDialog __all__ = [ 'AboutDialog', + 'BaseFramelessDialog', 'ColorblindPreviewDialog', 'ColorPickerDialog', 'ContrastCheckDialog', diff --git a/dialogs/about_dialog.py b/dialogs/about_dialog.py index 1df6dcd24c7e8fe8da9b3e9707eb82abc3f03ce4..02198899dbfc27f82874db101e6cbf51034effd1 100644 --- a/dialogs/about_dialog.py +++ b/dialogs/about_dialog.py @@ -1,40 +1,19 @@ # 标准库导入 -import os -import sys from pathlib import Path # 第三方库导入 -from PySide6.QtCore import Qt, QTimer, QUrl +from PySide6.QtCore import Qt, QUrl from PySide6.QtGui import QDesktopServices -from PySide6.QtWidgets import ( - QDialog, QFrame, QHBoxLayout, QVBoxLayout, QWidget -) +from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout, QWidget 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 +from utils import tr, load_icon_universal, get_base_path from version import version_manager -from utils.theme_colors import get_dialog_bg_color, get_text_color +from dialogs.base_frameless_dialog import BaseFramelessDialog -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__))) - - -class AboutDialog(QDialog): +class AboutDialog(BaseFramelessDialog): """关于对话框 显示应用程序信息、版本信息、开发团队信息、 @@ -49,30 +28,24 @@ class AboutDialog(QDialog): # 设置窗口图标 self.setWindowIcon(load_icon_universal()) - # 设置窗口标志:只保留关闭按钮(必须在设置窗口标题之后) - self.setWindowFlags( - Qt.WindowType.Window | - Qt.WindowType.WindowTitleHint | - Qt.WindowType.WindowCloseButtonHint | - Qt.WindowType.CustomizeWindowHint - ) + # 设置自定义标题栏 + self._setup_title_bar() - # 设置窗口背景色(与 FluentWindow 一致) - bg_color = get_dialog_bg_color() - self.setStyleSheet(f"QDialog {{ background-color: {bg_color.name()}; }}") + # 初始化样式 + self._update_styles() + # 设置界面 self.setup_ui() - # 修复任务栏图标(在窗口显示后调用) - QTimer.singleShot(100, lambda: fix_windows_taskbar_icon_for_window(self)) - # 监听主题变化 - qconfig.themeChangedFinished.connect(self._update_title_bar_theme) + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) def setup_ui(self): """设置界面布局""" layout = QVBoxLayout(self) - layout.setContentsMargins(20, 20, 20, 20) + layout.setContentsMargins(20, 40, 20, 20) layout.setSpacing(15) # 内容区域 @@ -86,7 +59,7 @@ class AboutDialog(QDialog): def _create_content_area(self, parent_layout): """创建内容显示区域 - + Args: parent_layout: 父布局对象 """ @@ -94,24 +67,25 @@ class AboutDialog(QDialog): self.text_edit.setReadOnly(True) self.text_edit.setPlainText(self._get_about_text()) self.text_edit.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu) - # 禁用焦点,去除底部蓝色条 self.text_edit.setFocusPolicy(Qt.FocusPolicy.NoFocus) - - # 设置主题感知的样式 - bg_color = get_dialog_bg_color() - text_color = get_text_color() - self.text_edit.setStyleSheet( - f"PlainTextEdit {{ background-color: {bg_color.name()}; " - f"color: {text_color.name()}; border: none; }}\n" - f"PlainTextEdit:focus {{ border: none; outline: none; }}\n" - f"PlainTextEdit::focus {{ border: none; }}\n" - f"QPlainTextEdit {{ border: none; }}\n" - f"QPlainTextEdit:focus {{ border: none; outline: none; }}" - ) - - parent_layout.addWidget(self.text_edit, stretch=1) - + self.text_edit.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction) + + # 设置背景和边框透明,文字颜色根据主题变化 + text_color = "#ffffff" if isDarkTheme() else "#333333" + self.text_edit.setStyleSheet(f""" + PlainTextEdit {{ + background-color: transparent; + border: none; + color: {text_color}; + }} + QPlainTextEdit {{ + background-color: transparent; + border: none; + color: {text_color}; + }} + """) + parent_layout.addWidget(self.text_edit, stretch=1) def _create_buttons_area(self, parent_layout): """创建按钮区域 @@ -196,40 +170,31 @@ class AboutDialog(QDialog): """ QDesktopServices.openUrl(QUrl(url)) - def _open_license_file(self): - """打开开源许可文件""" - # 获取许可证文件路径(相对于项目根目录的 file/LICENSE.html) - base_path = _get_base_path() - license_path = Path(base_path) / "file" / "LICENSE.html" - - if license_path.exists(): - # 转换为文件URL并打开 - file_url = QUrl.fromLocalFile(str(license_path.absolute())) - QDesktopServices.openUrl(file_url) + def _open_file_or_url(self, filename: str, fallback_url: str) -> None: + """打开本地文件,不存在则打开URL + + Args: + filename: 文件名(位于 file/ 目录下) + fallback_url: 文件不存在时的备用URL + """ + file_path = Path(get_base_path()) / "file" / filename + + if file_path.exists(): + QDesktopServices.openUrl(QUrl.fromLocalFile(str(file_path.absolute()))) else: - # 如果文件不存在,打开项目地址 - self._open_url("https://gitee.com/qingshangongzai/color_card") + self._open_url(fallback_url) + + def _open_license_file(self) -> None: + """打开开源许可文件""" + self._open_file_or_url("LICENSE.html", "https://gitee.com/qingshangongzai/color_card") - def _open_agreement_file(self): + def _open_agreement_file(self) -> None: """打开用户协议文件""" - # 获取用户协议文件路径(相对于项目根目录的 file/UserAgreement.html) - base_path = _get_base_path() - agreement_path = Path(base_path) / "file" / "UserAgreement.html" - - if agreement_path.exists(): - # 转换为文件URL并打开 - file_url = QUrl.fromLocalFile(str(agreement_path.absolute())) - QDesktopServices.openUrl(file_url) - else: - # 如果文件不存在,打开项目地址 - self._open_url("https://gitee.com/qingshangongzai/color-card") + self._open_file_or_url("UserAgreement.html", "https://gitee.com/qingshangongzai/color-card") def _get_about_text(self): """获取关于页面的文本内容""" - app_info = version_manager.get_app_info() - version = version_manager.get_version() - - return f"""  取色卡(Color Card)是一款专为摄影师和设计师开发的图片分析及配色工具,旨在帮助摄影爱好者和专业人士快速分析图像的色彩分布、亮度信息等关键数据,并提供一站式的本地配色解决方案。 + return """  取色卡(Color Card)是一款专为摄影师和设计师开发的图片分析及配色工具,旨在帮助摄影爱好者和专业人士快速分析图像的色彩分布、亮度信息等关键数据,并提供一站式的本地配色解决方案。 项目功能设计借鉴参考了Adobe Color、色采、palettemakel等优秀的在线配色工具。 @@ -268,6 +233,11 @@ class AboutDialog(QDialog): 项目地址:https://github.com/numpy/numpy 官网:https://numpy.org/ + • 本程序使用 PySideSix-Frameless-Window 实现对话框无边框窗口 + 版权所有:zhiyiYo + 许可证:LGPLv3 + 项目地址:https://github.com/zhiyiYo/PyQt-Frameless-Window + 【开源配色方案使用说明】 • Open Color 配色方案 版权所有:heeyeun (Yeun) @@ -375,16 +345,9 @@ class AboutDialog(QDialog): • 感谢Adobe Color、色采、palettemakel等优秀产品为我们提供的灵感和参考 """ - def _update_title_bar_theme(self): - """更新标题栏主题以适配当前主题""" - set_window_title_bar_theme(self, isDarkTheme()) - - def showEvent(self, event): - """窗口显示事件 - 在显示前设置标题栏主题避免闪烁""" - # 先设置标题栏主题(在父类 showEvent 之前) - self._update_title_bar_theme() - # 调用父类的 showEvent - super().showEvent(event) + def closeEvent(self, event): + """关闭事件""" + super().closeEvent(event) # 基类处理信号断开 def contextMenuEvent(self, event): """屏蔽原生右键菜单""" diff --git a/dialogs/base_frameless_dialog.py b/dialogs/base_frameless_dialog.py new file mode 100644 index 0000000000000000000000000000000000000000..1049118ffb965926c20ad0ca8e52095d6c19aebc --- /dev/null +++ b/dialogs/base_frameless_dialog.py @@ -0,0 +1,159 @@ +# 第三方库导入 +from PySide6.QtWidgets import QLabel +from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap, QPalette +from qframelesswindow import FramelessDialog +from qfluentwidgets import qconfig + +# 项目模块导入 +from utils.icon import get_icon_path +from utils.theme_colors import ( + get_text_color, get_dialog_bg_color, + get_close_button_hover_bg_color, + get_close_button_hover_color, + get_close_button_pressed_color +) + + +class BaseFramelessDialog(FramelessDialog): + """无边框对话框基类 + + 提供统一的 Fluent Design 风格标题栏和主题适配功能。 + 子类只需调用 _setup_title_bar() 和 _update_styles() 即可。 + + 使用示例: + class MyDialog(BaseFramelessDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("我的对话框") + self.setFixedSize(400, 300) + + # 设置自定义标题栏 + self._setup_title_bar() + + # 初始化样式 + self._update_styles() + + # 设置界面 + self.setup_ui() + + # 监听主题变化 + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) + + def closeEvent(self, event): + # 断开信号连接 + try: + qconfig.themeChangedFinished.disconnect(self._theme_connection) + except (TypeError, RuntimeError): + pass + super().closeEvent(event) + """ + + def _setup_title_bar(self): + """设置自定义 Fluent Design 风格标题栏""" + # 获取 FramelessDialog 内置的标题栏 + title_bar = self.titleBar + h_layout = title_bar.layout() + if not h_layout: + return + + # 获取主题颜色 + text_color = get_text_color() + text_color_str = text_color.name() + + # 创建标题栏控件:左边距、Logo、间距、标题 + # 1. 左边距 + left_spacer = QLabel(title_bar) + left_spacer.setFixedWidth(10) + + # 2. Logo + icon_path = get_icon_path() + logo_label = None + if icon_path: + logo_label = QLabel(title_bar) + pixmap = QPixmap(icon_path) + if not pixmap.isNull(): + logo_label.setPixmap(pixmap.scaled( + 20, 20, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation + )) + + # 3. Logo和标题之间的间距 + spacer_label = QLabel(title_bar) + spacer_label.setFixedWidth(8) + + # 4. 标题 + title_label = QLabel(self.windowTitle(), title_bar) + title_label.setStyleSheet(f"color: {text_color_str}; font-size: 13px; font-weight: 500;") + + # 按顺序插入到布局开头(倒序插入) + for widget in [title_label, spacer_label, logo_label, left_spacer]: + if widget: + h_layout.insertWidget(0, widget) + h_layout.setAlignment(widget, Qt.AlignmentFlag.AlignVCenter) + + # 保存标题标签引用,以便主题切换时更新 + self._title_label = title_label + + def _update_close_button_color(self, text_color): + """更新关闭按钮颜色以适配主题 + + Args: + text_color: 文本颜色 (QColor) + """ + title_bar = self.titleBar + if hasattr(title_bar, 'closeBtn') and title_bar.closeBtn: + close_btn = title_bar.closeBtn + # 设置正常状态颜色 + close_btn.setNormalColor(text_color) + # 设置悬停状态颜色 + close_btn.setHoverColor(get_close_button_hover_color()) + close_btn.setHoverBackgroundColor(get_close_button_hover_bg_color()) + # 设置按下状态颜色 + close_btn.setPressedColor(get_close_button_pressed_color()) + + def _update_styles(self): + """更新样式以适配主题""" + text_color = get_text_color() + text_color_str = text_color.name() + bg_color = get_dialog_bg_color() + + # 使用 QPalette 设置窗口背景色 + palette = self.palette() + palette.setColor(QPalette.ColorRole.Window, bg_color) + self.setPalette(palette) + self.setAutoFillBackground(True) + + # 设置样式表 - QLabel 文字颜色 + self.setStyleSheet(f""" + QLabel {{ + color: {text_color_str}; + background-color: transparent; + }} + """) + + # 更新标题标签颜色(如果存在) + if hasattr(self, '_title_label') and self._title_label: + self._title_label.setStyleSheet( + f"color: {text_color_str}; font-size: 13px; font-weight: 500;" + ) + + # 更新关闭按钮颜色 + self._update_close_button_color(text_color) + + def closeEvent(self, event): + """关闭事件:断开主题变化信号连接 + + 子类可以重写此方法,但应调用 super().closeEvent(event) + 以确保信号正确断开。 + """ + if hasattr(self, '_theme_connection'): + try: + qconfig.themeChangedFinished.disconnect(self._theme_connection) + except (TypeError, RuntimeError): + pass + delattr(self, '_theme_connection') + super().closeEvent(event) diff --git a/dialogs/colorblind_dialog.py b/dialogs/colorblind_dialog.py index 44955fb3847fa399e9edcae4e42d083f09908945..0aa7c35455f01e4cbd54f9b47fd61b5b0787f6c5 100644 --- a/dialogs/colorblind_dialog.py +++ b/dialogs/colorblind_dialog.py @@ -7,23 +7,24 @@ from typing import List, Dict, Tuple # 第三方库导入 -from PySide6.QtCore import Qt, QTimer +from PySide6.QtCore import Qt from PySide6.QtWidgets import ( - QDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget, + QHBoxLayout, QLabel, QVBoxLayout, QWidget, QFrame ) from PySide6.QtGui import QColor from qfluentwidgets import ( - ComboBox, isDarkTheme, qconfig, ScrollArea + ComboBox, qconfig, ScrollArea ) # 项目模块导入 -from utils import tr, fix_windows_taskbar_icon_for_window, load_icon_universal, set_window_title_bar_theme +from utils import tr, load_icon_universal +from dialogs import BaseFramelessDialog from core.colorblind import ( simulate_colorblind, get_colorblind_info, get_all_colorblind_types ) from utils.theme_colors import ( - get_dialog_bg_color, get_text_color, get_border_color, + get_text_color, get_border_color, get_secondary_text_color, get_title_color ) @@ -62,13 +63,11 @@ class ColorBlock(QWidget): class ColorComparisonRow(QWidget): """颜色对比行组件 - 显示原颜色和模拟颜色""" - + def __init__(self, parent=None): super().__init__(parent) self.setup_ui() self._update_styles() - # 监听主题变化 - qconfig.themeChangedFinished.connect(self._update_styles) def setup_ui(self): """设置界面""" @@ -140,15 +139,15 @@ class ColorComparisonRow(QWidget): self.simulated_hex.setText(f"{tr('dialogs.colorblind.simulated')}: {simulated_hex}") -class ColorblindPreviewDialog(QDialog): +class ColorblindPreviewDialog(BaseFramelessDialog): """色盲模拟预览对话框 - + 显示配色方案在不同色盲类型下的视觉效果。 """ - + def __init__(self, scheme_name: str, colors: List[Dict], parent=None): """初始化色盲预览对话框 - + Args: scheme_name: 配色方案名称 colors: 颜色列表,每个颜色是一个字典,包含 'rgb' 键 @@ -158,38 +157,32 @@ class ColorblindPreviewDialog(QDialog): self._scheme_name = scheme_name self._colors = colors self._current_type = 'normal' - + self.setWindowTitle(tr('dialogs.colorblind.window_title', name=scheme_name)) self.setFixedSize(320, 420) - + # 设置窗口图标 self.setWindowIcon(load_icon_universal()) - - # 设置窗口标志 - self.setWindowFlags( - Qt.WindowType.Window | - Qt.WindowType.WindowTitleHint | - Qt.WindowType.WindowCloseButtonHint | - Qt.WindowType.CustomizeWindowHint - ) - - # 设置窗口背景色 - bg_color = get_dialog_bg_color() - self.setStyleSheet(f"QDialog {{ background-color: {bg_color.name()}; }}") - + + # 设置界面 self.setup_ui() + + # 设置标题栏和样式 + self._setup_title_bar() + self._update_styles() + + # 更新预览 self.update_preview() - - # 修复任务栏图标 - QTimer.singleShot(100, lambda: fix_windows_taskbar_icon_for_window(self)) - + # 监听主题变化 - qconfig.themeChangedFinished.connect(self._update_title_bar_theme) + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) def setup_ui(self): """设置界面布局""" main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(20, 20, 20, 20) + main_layout.setContentsMargins(20, 40, 20, 20) main_layout.setSpacing(15) # 获取主题颜色 @@ -281,9 +274,8 @@ class ColorblindPreviewDialog(QDialog): } """) scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - + self.comparison_container = QWidget() - self.comparison_container.setStyleSheet("background: transparent;") self.comparison_layout = QVBoxLayout(self.comparison_container) self.comparison_layout.setContentsMargins(0, 0, 0, 0) self.comparison_layout.setSpacing(5) @@ -328,12 +320,7 @@ class ColorblindPreviewDialog(QDialog): # 更新说明文字 info = get_colorblind_info(self._current_type) self.description_label.setText(tr('dialogs.colorblind.description', text=info['description'])) - - def _update_title_bar_theme(self): - """更新标题栏主题以适配当前主题""" - set_window_title_bar_theme(self, isDarkTheme()) - - def showEvent(self, event): - """窗口显示事件""" - self._update_title_bar_theme() - super().showEvent(event) + + def closeEvent(self, event): + """关闭事件""" + super().closeEvent(event) # 基类处理信号断开 diff --git a/dialogs/contrast_dialog.py b/dialogs/contrast_dialog.py index 53418047ee5b60fbfa7c5029b7f82079798abc0c..ae8cc89eadd58dab3f33ab6e322e401e69a5cb5b 100644 --- a/dialogs/contrast_dialog.py +++ b/dialogs/contrast_dialog.py @@ -8,26 +8,27 @@ from typing import List, Dict, Tuple # 第三方库导入 -from PySide6.QtCore import Qt, QTimer, Signal +from PySide6.QtCore import Qt, Signal, QPointF from PySide6.QtWidgets import ( - QDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget, - QFrame, QGridLayout, QScrollArea + QHBoxLayout, QLabel, QVBoxLayout, QWidget, + QFrame ) -from PySide6.QtGui import QColor, QPainter, QBrush, QPen, QFont +from PySide6.QtGui import QColor, QPainter, QBrush, QPen, QPolygonF from qfluentwidgets import ( ComboBox, PushButton, ToolButton, FluentIcon, isDarkTheme, qconfig, CardWidget ) # 项目模块导入 -from utils import tr, fix_windows_taskbar_icon_for_window, load_icon_universal, set_window_title_bar_theme +from utils import tr, load_icon_universal +from dialogs import BaseFramelessDialog from core.contrast import ( - calculate_contrast_ratio, get_contrast_info, + get_contrast_info, rgb_to_hex, get_contrast_status_color ) from utils.theme_colors import ( - get_dialog_bg_color, get_text_color, get_border_color, - get_secondary_text_color, get_title_color, get_card_background_color + get_text_color, get_border_color, + get_secondary_text_color, get_title_color ) @@ -73,9 +74,7 @@ class ColorSelector(QWidget): # 颜色下拉选择 self.color_combo = ComboBox() self.color_combo.setFixedWidth(100) - for i, color_data in enumerate(self._colors): - rgb = color_data.get('rgb', [128, 128, 128]) - hex_val = rgb_to_hex(tuple(rgb)) + for i, _ in enumerate(self._colors): self.color_combo.addItem(tr('dialogs.contrast.color_index', index=i+1)) self.color_combo.setItemData(i, i) self.color_combo.currentIndexChanged.connect(self._on_combo_changed) @@ -272,8 +271,6 @@ class GraphicWidget(QWidget): painter.drawEllipse(50, 20, 20, 20) # 绘制三角形 - from PySide6.QtGui import QPolygonF - from PySide6.QtCore import QPointF triangle = QPolygonF([ QPointF(90, 20), QPointF(80, 40), @@ -350,16 +347,16 @@ class GraphicPreviewCard(QWidget): painter.drawRoundedRect(0, 0, self.width() - 1, self.height() - 1, 8, 8) -class ContrastCheckDialog(QDialog): +class ContrastCheckDialog(BaseFramelessDialog): """对比度检查对话框 - + 允许用户选择配色方案中的两种颜色进行对比度检查, 并实时预览文字可读性效果。 """ - + def __init__(self, scheme_name: str, colors: List[Dict], parent=None): """初始化对比度检查对话框 - + Args: scheme_name: 配色方案名称 colors: 颜色列表,每个颜色是一个字典,包含 'rgb' 键 @@ -368,43 +365,34 @@ class ContrastCheckDialog(QDialog): super().__init__(parent) self._scheme_name = scheme_name self._colors = colors - + self.setWindowTitle(tr('dialogs.contrast.window_title', name=scheme_name)) - + # 设置窗口图标 self.setWindowIcon(load_icon_universal()) - - # 设置窗口标志 - 使用 MSWindowsFixedSizeDialogHint 防止 Windows 自动调整大小 - self.setWindowFlags( - Qt.WindowType.Window | - Qt.WindowType.WindowTitleHint | - Qt.WindowType.WindowCloseButtonHint | - Qt.WindowType.CustomizeWindowHint | - Qt.WindowType.MSWindowsFixedSizeDialogHint - ) - - # 设置固定大小(使用最小/最大尺寸确保不被压缩) - self.setMinimumSize(480, 420) - self.setMaximumSize(480, 420) - self.resize(480, 420) - - # 设置窗口背景色 - bg_color = get_dialog_bg_color() - self.setStyleSheet(f"QDialog {{ background-color: {bg_color.name()}; }}") - + + # 设置固定大小 + self.setFixedSize(480, 580) + + # 设置界面 self.setup_ui() + + # 设置标题栏和样式 + self._setup_title_bar() + self._update_styles() + + # 更新对比度 self._update_contrast() - - # 修复任务栏图标 - QTimer.singleShot(100, lambda: fix_windows_taskbar_icon_for_window(self)) - + # 监听主题变化 - qconfig.themeChangedFinished.connect(self._update_title_bar_theme) + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) def setup_ui(self): """设置界面布局""" main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(20, 20, 20, 20) + main_layout.setContentsMargins(20, 40, 20, 20) main_layout.setSpacing(15) # 获取主题颜色 @@ -486,7 +474,7 @@ class ContrastCheckDialog(QDialog): # 等级标签 self.level_label = QLabel("--") - self.level_label.setStyleSheet(f"font-size: 16px; font-weight: bold;") + self.level_label.setStyleSheet("font-size: 16px; font-weight: bold;") result_layout.addWidget(self.level_label) result_layout.addStretch() @@ -591,12 +579,7 @@ class ContrastCheckDialog(QDialog): self.normal_preview.set_colors(bg_rgb, text_rgb) self.large_preview.set_colors(bg_rgb, text_rgb) self.graphic_preview.set_colors(bg_rgb, text_rgb) - - def _update_title_bar_theme(self): - """更新标题栏主题以适配当前主题""" - set_window_title_bar_theme(self, isDarkTheme()) - - def showEvent(self, event): - """窗口显示事件""" - self._update_title_bar_theme() - super().showEvent(event) + + def closeEvent(self, event): + """关闭事件""" + super().closeEvent(event) # 基类处理信号断开 diff --git a/dialogs/edit_palette.py b/dialogs/edit_palette.py index be6239e8e3c85785cfbe34ed7c0a3f68ac6ceb19..b28ec19b34e6073cdcb356a3372566422248fbc3 100644 --- a/dialogs/edit_palette.py +++ b/dialogs/edit_palette.py @@ -1,19 +1,18 @@ # 标准库导入 import re from datetime import datetime -from typing import Dict, Any, List, Tuple, Optional +from typing import Dict, Any, Tuple, Optional # 第三方库导入 from PySide6.QtCore import Qt, QTimer, Signal, QPoint, QRect from PySide6.QtWidgets import ( - QDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget, QGridLayout, QApplication + QHBoxLayout, QLabel, QVBoxLayout, QWidget, QGridLayout, QApplication, QDialog ) from PySide6.QtGui import QColor, QPainter, QLinearGradient, QBrush, QPen, QMouseEvent from qfluentwidgets import ( LineEdit, PrimaryPushButton, PushButton, ToolButton, FluentIcon, - ScrollArea, isDarkTheme, qconfig + ScrollArea, qconfig ) - # 项目模块导入 from core import ( hex_to_rgb, rgb_to_hex, rgb_to_hsb, hsb_to_rgb, @@ -21,8 +20,11 @@ from core import ( rgb_to_cmyk, cmyk_to_rgb, get_color_info ) from core.config import get_config_manager -from utils import tr, fix_windows_taskbar_icon_for_window, load_icon_universal, set_window_title_bar_theme -from utils.theme_colors import get_dialog_bg_color, get_text_color, get_border_color +from utils import tr, load_icon_universal +from utils.theme_colors import get_text_color, get_border_color + +# 对话框模块导入 +from .base_frameless_dialog import BaseFramelessDialog # ==================== 颜色选择器对话框组件 ==================== @@ -242,7 +244,7 @@ class ColorModeSliders(QWidget): # 模式标题 title = QLabel(f"{self._mode}:") - title.setStyleSheet(f"color: {get_text_color().name()}; font-size: 12px;") + title.setStyleSheet(f"color: {get_text_color().name()}; font-size: 12px; background: transparent;") layout.addWidget(title) # 创建3个滑块 @@ -260,7 +262,7 @@ class ColorModeSliders(QWidget): label = QLabel(f"{min_val}{unit}") label.setFixedWidth(45) label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) - label.setStyleSheet(f"color: {get_text_color().name()}; font-size: 11px;") + label.setStyleSheet(f"color: {get_text_color().name()}; font-size: 11px; background: transparent;") self._labels.append((label, unit)) row_layout.addWidget(label) @@ -331,7 +333,7 @@ class ColorModeSliders(QWidget): self._sliders[2].set_gradient(gradient_b) elif self._mode == 'HSL': - h, s, l = rgb_to_hsl(r, g, b) + H, S, L = rgb_to_hsl(r, g, b) # H滑块:色相渐变 gradient_h = QLinearGradient(0, 0, 200, 0) for i in range(7): @@ -342,15 +344,15 @@ class ColorModeSliders(QWidget): # S滑块:从灰到纯色 gradient_s = QLinearGradient(0, 0, 200, 0) - gradient_s.setColorAt(0.0, QColor.fromHsl(int(h / 360 * 359), 0, int(l / 100 * 255))) - gradient_s.setColorAt(1.0, QColor.fromHsl(int(h / 360 * 359), 255, int(l / 100 * 255))) + gradient_s.setColorAt(0.0, QColor.fromHsl(int(H / 360 * 359), 0, int(L / 100 * 255))) + gradient_s.setColorAt(1.0, QColor.fromHsl(int(H / 360 * 359), 255, int(L / 100 * 255))) self._sliders[1].set_gradient(gradient_s) # L滑块:从黑到白 gradient_l = QLinearGradient(0, 0, 200, 0) - gradient_l.setColorAt(0.0, QColor.fromHsl(int(h / 360 * 359), int(s / 100 * 255), 0)) - gradient_l.setColorAt(0.5, QColor.fromHsl(int(h / 360 * 359), int(s / 100 * 255), 128)) - gradient_l.setColorAt(1.0, QColor.fromHsl(int(h / 360 * 359), int(s / 100 * 255), 255)) + gradient_l.setColorAt(0.0, QColor.fromHsl(int(H / 360 * 359), int(S / 100 * 255), 0)) + gradient_l.setColorAt(0.5, QColor.fromHsl(int(H / 360 * 359), int(S / 100 * 255), 128)) + gradient_l.setColorAt(1.0, QColor.fromHsl(int(H / 360 * 359), int(S / 100 * 255), 255)) self._sliders[2].set_gradient(gradient_l) elif self._mode == 'CMYK': @@ -388,7 +390,7 @@ class ColorModeSliders(QWidget): self._sliders[3].set_gradient(gradient_k) -class ColorPickerDialog(QDialog): +class ColorPickerDialog(BaseFramelessDialog): """颜色选择器对话框""" def __init__(self, initial_color: Optional[Tuple[int, int, int]] = None, parent=None): @@ -401,27 +403,18 @@ class ColorPickerDialog(QDialog): self.setWindowTitle(tr('dialogs.color_picker.title')) self.setFixedSize(520, 420) - # 设置窗口标志 - self.setWindowFlags( - Qt.WindowType.Window | - Qt.WindowType.WindowTitleHint | - Qt.WindowType.WindowCloseButtonHint | - Qt.WindowType.CustomizeWindowHint - ) + # 设置自定义标题栏 + self._setup_title_bar() - # 设置背景色 - bg_color = get_dialog_bg_color() - self.setStyleSheet(f"QDialog {{ background-color: {bg_color.name()}; }}") + # 初始化样式(包含窗口背景色和标题颜色) + self._update_styles() self.setup_ui() self._update_from_rgb() - # 修复任务栏图标 - QTimer.singleShot(100, lambda: self._fix_taskbar_icon()) - # 监听主题变化 self._theme_connection = qconfig.themeChangedFinished.connect( - self._update_title_bar_theme + self._update_styles ) def closeEvent(self, event): @@ -435,7 +428,8 @@ class ColorPickerDialog(QDialog): def setup_ui(self): """设置界面布局""" main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(20, 20, 20, 20) + # 顶部边距设置为40,为无边框窗口的标题栏留出空间 + main_layout.setContentsMargins(20, 40, 20, 20) main_layout.setSpacing(15) # 内容区域(左右分割) @@ -735,22 +729,7 @@ class ColorPickerDialog(QDialog): """获取选择的颜色信息""" return self._color_info - def _update_title_bar_theme(self): - """更新标题栏主题""" - set_window_title_bar_theme(self, isDarkTheme()) - - def _fix_taskbar_icon(self): - """修复任务栏图标""" - try: - if self and self.isVisible(): - fix_windows_taskbar_icon_for_window(self) - except RuntimeError: - pass - def showEvent(self, event): - """窗口显示事件""" - self._update_title_bar_theme() - super().showEvent(event) class ColorInputRow(QWidget): @@ -775,15 +754,7 @@ class ColorInputRow(QWidget): self._debounce_timer.setSingleShot(True) self._debounce_timer.timeout.connect(self._process_hex_input) - def closeEvent(self, event): - """关闭事件 - 断开信号连接""" - try: - if hasattr(self, '_theme_connection'): - qconfig.themeChangedFinished.disconnect(self._theme_connection) - delattr(self, '_theme_connection') - except (TypeError, RuntimeError): - pass - super().closeEvent(event) + def setup_ui(self): """设置界面""" @@ -838,7 +809,8 @@ class ColorInputRow(QWidget): else: initial = (128, 128, 128) # 默认灰色 - dialog = ColorPickerDialog(initial, self) + # 使用顶层窗口作为父窗口,避免背景色异常和两套窗口控制器 + dialog = ColorPickerDialog(initial, self.window()) if dialog.exec() == QDialog.DialogCode.Accepted: color_info = dialog.get_color_info() if color_info: @@ -847,7 +819,7 @@ class ColorInputRow(QWidget): def _update_styles(self): """更新样式以适配主题""" text_color = get_text_color() - self.index_label.setStyleSheet(f"color: {text_color.name()}; font-size: 13px;") + self.index_label.setStyleSheet(f"color: {text_color.name()}; font-size: 13px; background: transparent;") self._update_preview_style(self._color_info) def _update_preview_style(self, color_info): @@ -948,7 +920,7 @@ class ColorInputRow(QWidget): self._update_preview_style(color_info) -class EditPaletteDialog(QDialog): +class EditPaletteDialog(BaseFramelessDialog): """编辑配色对话框""" def __init__(self, default_name="", palette_data=None, parent=None, show_name_input=True): @@ -964,25 +936,21 @@ class EditPaletteDialog(QDialog): 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 self._color_rows = [] + # 设置窗口标题 + self.setWindowTitle("编辑配色" if self._is_edit_mode else "添加配色") + self.setFixedSize(300, 400) + # 设置窗口图标 self.setWindowIcon(load_icon_universal()) - # 设置窗口标志 - self.setWindowFlags( - Qt.WindowType.Window | - Qt.WindowType.WindowTitleHint | - Qt.WindowType.WindowCloseButtonHint | - Qt.WindowType.CustomizeWindowHint - ) + # 设置自定义标题栏 + self._setup_title_bar() - # 设置窗口背景色 - bg_color = get_dialog_bg_color() - self.setStyleSheet(f"QDialog {{ background-color: {bg_color.name()}; }}") + # 初始化样式(包含窗口背景色和标题颜色) + self._update_styles() self.setup_ui() @@ -990,33 +958,27 @@ class EditPaletteDialog(QDialog): if self._is_edit_mode: self._load_palette_data() - # 修复任务栏图标 - QTimer.singleShot(100, lambda: self._fix_taskbar_icon()) - # 监听主题变化 self._theme_connection = qconfig.themeChangedFinished.connect( - self._update_title_bar_theme + self._update_styles ) def closeEvent(self, event): - """关闭事件 - 断开信号连接""" - try: - qconfig.themeChangedFinished.disconnect(self._theme_connection) - except (TypeError, RuntimeError): - pass - super().closeEvent(event) + """关闭事件""" + super().closeEvent(event) # 基类处理信号断开 def setup_ui(self): """设置界面布局""" layout = QVBoxLayout(self) - layout.setContentsMargins(20, 20, 20, 20) + # 顶部边距设置为40,为无边框窗口的标题栏留出空间 + layout.setContentsMargins(20, 40, 20, 20) layout.setSpacing(15) # 名称输入区域(根据参数决定是否显示) if self._show_name_input: name_layout = QHBoxLayout() name_label = QLabel("配色名称:") - name_label.setStyleSheet(f"color: {get_text_color().name()}; font-size: 13px;") + name_label.setStyleSheet(f"color: {get_text_color().name()}; font-size: 13px; background: transparent;") name_layout.addWidget(name_label) self.name_input = LineEdit() @@ -1035,7 +997,7 @@ class EditPaletteDialog(QDialog): # 颜色列表标题 colors_title = QLabel(tr('dialogs.edit_palette.colors_title')) - colors_title.setStyleSheet(f"color: {get_text_color().name()}; font-size: 13px;") + colors_title.setStyleSheet(f"color: {get_text_color().name()}; font-size: 13px; background: transparent;") layout.addWidget(colors_title) # 颜色输入区域(可滚动) @@ -1225,20 +1187,4 @@ class EditPaletteDialog(QDialog): """ return getattr(self, '_palette_data', None) - def _update_title_bar_theme(self): - """更新标题栏主题以适配当前主题""" - set_window_title_bar_theme(self, isDarkTheme()) - - def _fix_taskbar_icon(self): - """修复任务栏图标""" - try: - if self and self.isVisible(): - fix_windows_taskbar_icon_for_window(self) - except RuntimeError: - # 对象已被销毁 - pass - def showEvent(self, event): - """窗口显示事件 - 在显示前设置标题栏主题避免闪烁""" - self._update_title_bar_theme() - super().showEvent(event) diff --git a/dialogs/export_settings_dialog.py b/dialogs/export_settings_dialog.py index dd2f3c650ee53f453b547f178b9a7c53d8e0d18f..ed98d45383089c6547ae95e71d89e585f737f4d8 100644 --- a/dialogs/export_settings_dialog.py +++ b/dialogs/export_settings_dialog.py @@ -2,23 +2,22 @@ from typing import List, Optional # 第三方库导入 -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import Qt from PySide6.QtWidgets import ( - QDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget, - QFrame, QFileDialog, QMessageBox + QHBoxLayout, QLabel, QVBoxLayout, QWidget, + QMessageBox ) -from PySide6.QtGui import QPixmap from qfluentwidgets import ( - PushButton, LineEdit, RadioButton, isDarkTheme, qconfig, + PushButton, LineEdit, RadioButton, qconfig, PrimaryPushButton, CheckBox, ScrollArea ) # 项目模块导入 -from utils import tr, set_window_title_bar_theme -from utils.theme_colors import get_text_color, get_card_background_color, get_dialog_bg_color +from dialogs import BaseFramelessDialog +from utils import tr -class ExportSettingsDialog(QDialog): +class ExportSettingsDialog(BaseFramelessDialog): """导出设置对话框 用于配置配色预览导出选项,包括选择图片、设置文件名前缀、选择导出格式等。 @@ -39,32 +38,25 @@ class ExportSettingsDialog(QDialog): self._png_radio: Optional[RadioButton] = None self.setWindowTitle(tr('dialogs.export_settings.title')) + self.setFixedSize(400, 450) - # 设置窗口标志 - 只保留关闭按钮 - self.setWindowFlags( - Qt.WindowType.Window | - Qt.WindowType.WindowTitleHint | - Qt.WindowType.WindowCloseButtonHint | - Qt.WindowType.CustomizeWindowHint - ) - - # 设置背景色 - bg_color = get_dialog_bg_color() - self.setStyleSheet(f"QDialog {{ background-color: {bg_color.name()}; }}") - + # 设置界面 self.setup_ui() + + # 设置标题栏和样式(基类提供) + self._setup_title_bar() self._update_styles() # 监听主题变化 - qconfig.themeChangedFinished.connect(self._update_styles) - - # 设置固定大小(必须在 setup_ui 之后) - self.setFixedSize(400, 450) + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) def setup_ui(self): """设置界面""" layout = QVBoxLayout(self) - layout.setContentsMargins(20, 20, 20, 20) + # 顶部边距40px为标题栏留出空间 + layout.setContentsMargins(20, 40, 20, 20) layout.setSpacing(16) # 选择图片区域 @@ -227,20 +219,6 @@ class ExportSettingsDialog(QDialog): return "png" return "svg" - 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()) - - def _update_styles(self): - """更新样式以适配主题""" - text_color = get_text_color() - text_color_hex = text_color.name() - - # 更新所有标签的文字颜色 - for widget in self.findChildren(QLabel): - widget.setStyleSheet(f"color: {text_color_hex};") + def closeEvent(self, event): + """关闭事件""" + super().closeEvent(event) # 基类处理信号断开 diff --git a/dialogs/update_dialog.py b/dialogs/update_dialog.py index 76e676b9564673a37c4c5d352cadb1a04dcac93c..6005e75f9e6c725909c4770c5998fb73eb0ba80c 100644 --- a/dialogs/update_dialog.py +++ b/dialogs/update_dialog.py @@ -3,10 +3,10 @@ import re from typing import List, Tuple # 第三方库导入 -from PySide6.QtCore import Qt, QThread, QTimer, QUrl, Signal +from PySide6.QtCore import Qt, QThread, Signal, QUrl from PySide6.QtGui import QDesktopServices -from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget -from qfluentwidgets import InfoBar, InfoBarPosition, PrimaryPushButton, PushButton, isDarkTheme, qconfig +from PySide6.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget +from qfluentwidgets import InfoBar, InfoBarPosition, PrimaryPushButton, PushButton, qconfig try: import requests @@ -14,8 +14,8 @@ except ImportError: requests = None # 项目模块导入 -from utils import tr, fix_windows_taskbar_icon_for_window, load_icon_universal, set_window_title_bar_theme -from utils.theme_colors import get_dialog_bg_color, get_text_color +from utils import tr, load_icon_universal +from dialogs import BaseFramelessDialog class UpdateCheckThread(QThread): @@ -115,10 +115,10 @@ def compare_versions(current: str, latest: str) -> int: current_parts.extend([0] * (max_len - len(current_parts))) latest_parts.extend([0] * (max_len - len(latest_parts))) - for c, l in zip(current_parts, latest_parts): - if c > l: + for c, latest_part in zip(current_parts, latest_parts): + if c > latest_part: return 1 - elif c < l: + elif c < latest_part: return -1 if current_pre > latest_pre: @@ -134,7 +134,7 @@ def compare_versions(current: str, latest: str) -> int: return 0 -class UpdateAvailableDialog(QDialog): +class UpdateAvailableDialog(BaseFramelessDialog): """新版本可用提示对话框 当检测到有新版本时弹出,提供跳转到发行页面的功能。 @@ -159,45 +159,33 @@ class UpdateAvailableDialog(QDialog): # 设置窗口图标 self.setWindowIcon(load_icon_universal()) - # 设置窗口标志 - self.setWindowFlags( - Qt.WindowType.Window - | Qt.WindowType.WindowTitleHint - | Qt.WindowType.WindowCloseButtonHint - | Qt.WindowType.CustomizeWindowHint - ) - - # 设置窗口背景色 - bg_color = get_dialog_bg_color() - self.setStyleSheet(f"QDialog {{ background-color: {bg_color.name()}; }}") - + # 设置界面 self.setup_ui() - # 修复任务栏图标 - QTimer.singleShot(100, lambda: fix_windows_taskbar_icon_for_window(self)) + # 设置标题栏和样式(基类提供) + self._setup_title_bar() + self._update_styles() # 监听主题变化 - qconfig.themeChangedFinished.connect(self._update_title_bar_theme) + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) def setup_ui(self): """设置界面布局""" layout = QVBoxLayout(self) - layout.setContentsMargins(20, 20, 20, 20) + # 顶部边距40px为标题栏留出空间 + layout.setContentsMargins(20, 40, 20, 20) layout.setSpacing(15) - # 提示文本 - text_color = get_text_color() + # 提示文本(基类统一处理文字颜色) info_label = QLabel(tr('dialogs.update.new_version')) info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - info_label.setStyleSheet( - f"QLabel {{ color: {text_color.name()}; font-size: 16px; font-weight: bold; }}" - ) layout.addWidget(info_label) - # 版本信息 + # 版本信息(基类统一处理文字颜色) version_label = QLabel(tr('dialogs.update.version_info', current=self.current_version, latest=self.latest_version)) version_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - version_label.setStyleSheet(f"QLabel {{ color: {text_color.name()}; font-size: 12px; }}") layout.addWidget(version_label) layout.addStretch() @@ -232,16 +220,9 @@ class UpdateAvailableDialog(QDialog): QDesktopServices.openUrl(QUrl(url)) self.accept() - def _update_title_bar_theme(self): - """更新标题栏主题以适配当前主题""" - set_window_title_bar_theme(self, isDarkTheme()) - - def showEvent(self, event): - """窗口显示事件 - 在显示前设置标题栏主题避免闪烁""" - # 先设置标题栏主题(在父类 showEvent 之前) - self._update_title_bar_theme() - # 调用父类的 showEvent - super().showEvent(event) + def closeEvent(self, event): + """关闭事件""" + super().closeEvent(event) # 基类处理信号断开 @staticmethod def check_update(parent, current_version): @@ -278,7 +259,9 @@ class UpdateAvailableDialog(QDialog): ) else: # 有新版本可用,显示对话框 - dialog = UpdateAvailableDialog(parent, current_version, latest_version) + # 使用 window() 获取顶层窗口,确保无边框对话框正常显示 + top_parent = parent.window() if parent else None + dialog = UpdateAvailableDialog(top_parent, current_version, latest_version) dialog.exec() else: InfoBar.warning( diff --git a/docs/changelog.html b/docs/changelog.html index 5f052429f2e3f00e4b21aeab9d82a8ff773e0fa5..e94a138e2b66bdafeae2121bf9e6a025321d786f 100644 --- a/docs/changelog.html +++ b/docs/changelog.html @@ -171,6 +171,7 @@ 项目起源 下载 更新日志 + 使用说明 反馈 关于我们 diff --git a/docs/changelog.json b/docs/changelog.json index aa413241b212e93d94497f8d33b62ef91e16b93e..c0545cb6f81aebf2f01e45750725f3a0955ecf07 100644 --- a/docs/changelog.json +++ b/docs/changelog.json @@ -1,5 +1,45 @@ { "versions": [ + { + "version": "v1.6.0", + "date": "2026-03-22", + "changes": [ + { + "category": "新增功能", + "items": [ + "新增「跟随系统」语言选项,应用语言可自动适配系统语言设置" + ] + }, + { + "category": "问题修复", + "items": [ + "修复启动动画期间操作其他窗口导致主窗口不弹出的问题", + "修复 Hex 值区域字重异常问题" + ] + }, + { + "category": "界面优化", + "items": [ + "HSB 色轮改为顺时针排列,符合常用色彩理论习惯", + "所有对话框统一采用无边框设计,视觉风格更加一致", + "禁用关于对话框文本选择高亮,避免误操作" + ] + }, + { + "category": "体验优化", + "items": [ + "优化文件导入导出目录路径,图片导入默认打开用户图片文件夹,配色文件导入导出路径更加便捷", + "统一色轮采样点在 RGB 和 RYB 模式下的行为一致性" + ] + }, + { + "category": "代码重构", + "items": [ + "优化内部代码结构,提升软件稳定性" + ] + } + ] + }, { "version": "v1.5.1", "date": "2026-03-15", diff --git a/docs/index.html b/docs/index.html index e5ad4931d9ac4767634c145d4567ac81ce3b2e3f..2b0b91a54dfb105bcd2abca5177bed2e9ec485e7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1262,7 +1262,7 @@ { icon: 'eye', title: '配色预览', desc: '支持手机UI、网页、插画、排版、品牌、海报、图案、杂志等8种场景预览,支持自定义SVG' }, { icon: 'barchart', title: '明度分析', desc: '将图片按明度分为9个区域,提供直方图可视化,辅助调色决策' }, { icon: 'gradient', title: '渐变生成', desc: '选择起始和结束颜色,生成渐变色序列,支持RGB/HSB/LAB三种颜色空间插值,可调节1-10个中间色' }, - { icon: 'globe', title: '多语言支持', desc: '支持简体中文、繁体中文、英语、日语、法语、俄语六种语言界面切换' }, + { icon: 'globe', title: '多语言支持', desc: '支持简体中文、繁体中文、英语、日语、法语、俄语六种语言界面切换,支持跟随系统语言自动切换' }, { icon: 'moon', title: '明暗主题', desc: '支持深色模式和浅色模式切换,保护视力,适应不同使用环境' }, ]; diff --git a/file/LICENSE.html b/file/LICENSE.html index eb89fc463d7f2a7e0295b47306178195df4569d5..82bd7790535c6fbb5b4833dec92893df05553ce5 100644 --- a/file/LICENSE.html +++ b/file/LICENSE.html @@ -689,9 +689,24 @@ + +
+

6. PySideSix-Frameless-Window

+
+

版权所有:zhiyiYo

+

项目地址:https://github.com/zhiyiYo/PyQt-Frameless-Window

+

许可证:GNU Lesser General Public License v3.0

+
+
+
+

PySideSix-Frameless-Window 使用 LGPLv3 许可证。由于本项目主许可证为 GPLv3,而 LGPLv3 是 GPLv3 的补充版本,完整的 LGPLv3 许可证文本请参考本文档 PySide6 章节中的 "GNU LESSER GENERAL PUBLIC LICENSE Version 3" 部分。

+
+
+
+
-

6. Open Color

+

7. Open Color

版权所有:heeyeun (Yeun)

项目地址:https://github.com/yeun/open-color

@@ -714,7 +729,7 @@
-

7. Nice Color Palettes

+

8. Nice Color Palettes

版权所有:Jam3

项目地址:https://github.com/Experience-Monks/nice-color-palettes

@@ -748,7 +763,7 @@
-

8. Tailwind CSS Colors

+

9. Tailwind CSS Colors

版权所有:Tailwind Labs, Inc.

项目地址:https://github.com/tailwindlabs/tailwindcss

@@ -771,7 +786,7 @@
-

9. Material Design Colors

+

10. Material Design Colors

版权所有:Google LLC

项目地址:https://m3.material.io/styles/color/system/overview

@@ -787,7 +802,7 @@
-

10. ColorBrewer

+

11. ColorBrewer

版权所有:Cynthia Brewer

项目地址:https://colorbrewer2.org/

@@ -804,7 +819,7 @@
-

11. Radix UI Colors

+

12. Radix UI Colors

版权所有:WorkOS

项目地址:https://github.com/radix-ui/colors

@@ -828,7 +843,7 @@
-

12. Nord

+

13. Nord

版权所有:Sven Greb

项目地址:https://github.com/arcticicestudio/nord

@@ -851,7 +866,7 @@
-

13. Dracula

+

14. Dracula

版权所有:Dracula Theme contributors

官网:https://draculatheme.com/

@@ -873,7 +888,7 @@
-

14. Rosé Pine

+

15. Rosé Pine

版权所有:Rosé Pine 团队

项目地址:https://github.com/rose-pine/rose-pine-theme

@@ -896,7 +911,7 @@
-

15. Solarized

+

16. Solarized

版权所有:Ethan Schoonover

项目地址:https://github.com/altercation/solarized

@@ -916,7 +931,7 @@
-

16. Catppuccin

+

17. Catppuccin

版权所有:Catppuccin 团队

项目地址:https://github.com/catppuccin/catppuccin

@@ -939,7 +954,7 @@
-

17. Gruvbox (MIT/X11 License)

+

18. Gruvbox (MIT/X11 License)

版权所有:Pavel Pertsev

项目地址:https://github.com/morhetz/gruvbox

@@ -961,7 +976,7 @@
-

18. Tokyo Night (MIT License)

+

19. Tokyo Night (MIT License)

版权所有:enkia

项目地址:https://github.com/enkia/tokyo-night-vscode-theme

diff --git a/locales/FR_FR.json b/locales/FR_FR.json index 9d0ac79b984504b0d4983ed6e1ca6a9d0beae8a3..df106d3c2f03468f8c218f1098a7bd48929b56c3 100644 --- a/locales/FR_FR.json +++ b/locales/FR_FR.json @@ -229,6 +229,7 @@ "language": "Paramètres de langue", "language_title": "Langue de l'interface", "language_desc": "Sélectionner la langue d'affichage de l'application", + "language_auto": "Suivre le système", "help": "Aide", "check_update": "Vérifier", "version_update": "Mise à jour", diff --git a/locales/JA_JP.json b/locales/JA_JP.json index b20d7b62ac7749af5c4e4fea0449694a743099ff..621bfe544ba2774b78e33b9a47148efe0a7a5b87 100644 --- a/locales/JA_JP.json +++ b/locales/JA_JP.json @@ -229,6 +229,7 @@ "language": "言語設定", "language_title": "インターフェース言語", "language_desc": "アプリケーションの表示言語を選択", + "language_auto": "システムに従う", "help": "ヘルプ", "check_update": "更新を確認", "version_update": "バージョン更新", diff --git a/locales/RU_RU.json b/locales/RU_RU.json index 58af60fc789445cfeea4754adf1b07e3eee2fb41..10e9633f1a913b2934d42776b343b4834345002c 100644 --- a/locales/RU_RU.json +++ b/locales/RU_RU.json @@ -229,6 +229,7 @@ "language": "Настройки языка", "language_title": "Язык интерфейса", "language_desc": "Выберите язык отображения приложения", + "language_auto": "Следовать системе", "help": "Справка", "check_update": "Проверить", "version_update": "Обновление версии", diff --git a/locales/ZW_FT.json b/locales/ZW_FT.json index 53d0cff2feadc4381c2304446d04930388e49a87..a8a86c356c1f59d77c1b55d5823f7f2f3e5dd1fa 100644 --- a/locales/ZW_FT.json +++ b/locales/ZW_FT.json @@ -229,6 +229,7 @@ "language": "語言設置", "language_title": "頁面語言", "language_desc": "選擇應用程序的顯示語言", + "language_auto": "跟隨系統", "help": "幫助", "check_update": "檢查更新", "version_update": "版本更新", diff --git a/locales/ZW_JT.json b/locales/ZW_JT.json index 16672d745048efd60dbb53c0ac4a5a0f0e346718..fa123ea19aefdbb61502f1ffa37110fe249315a4 100644 --- a/locales/ZW_JT.json +++ b/locales/ZW_JT.json @@ -229,6 +229,7 @@ "language": "语言设置", "language_title": "页面语言", "language_desc": "选择应用程序的显示语言", + "language_auto": "跟随系统", "help": "帮助", "check_update": "检查更新", "version_update": "版本更新", diff --git a/locales/en_US.json b/locales/en_US.json index 1ecb3cb99449fa785622c7295bce9bdef127eede..572ddf39d5dccc8e63a1b9cccb1ac59b588edec6 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -229,6 +229,7 @@ "language": "Language Settings", "language_title": "Interface Language", "language_desc": "Select the display language for the application", + "language_auto": "Follow System", "help": "Help", "check_update": "Check for Updates", "version_update": "Version Update", diff --git a/main.py b/main.py index d3e365042cc857c0a3bd5243762a54f202b2a29c..1ab97b94224f71152a5c31f4f1feba47f46026a7 100644 --- a/main.py +++ b/main.py @@ -16,7 +16,8 @@ def set_app_user_model_id(): app_id = 'HXiaoStudio.ColorCard.1.0.0' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id) return True - except Exception: + except Exception as e: + logger.debug(f"设置 AppUserModelID 失败: {e}") return False @@ -96,7 +97,8 @@ def _create_splash_screen(): splash.setWindowFlags( Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | - Qt.WindowType.SplashScreen + Qt.WindowType.SplashScreen | + Qt.WindowType.WindowDoesNotAcceptFocus ) # 居中显示 @@ -118,7 +120,8 @@ def _create_splash_screen(): ) return splash - except Exception: + except Exception as e: + logger.debug(f"创建启动画面失败: {e}") return None @@ -166,7 +169,7 @@ def main(): sys.stdout = _old_stdout # 安装自定义 Qt 消息处理器以过滤 QFont 警告 - def qt_message_handler(mode, context, message): + def qt_message_handler(mode, _context, message): """自定义 Qt 消息处理器,过滤掉 QFont::setPointSize 警告""" if "QFont::setPointSize: Point size <= 0" in message: return @@ -180,7 +183,7 @@ def main(): from core import get_config_manager logger.info("core 模块导入完成") - from utils import fix_windows_taskbar_icon_for_window, load_icon_universal, tr, get_locale_manager + from utils import fix_windows_taskbar_icon_for_window, load_icon_universal, get_locale_manager logger.info("utils 模块导入完成") from ui import MainWindow @@ -226,6 +229,9 @@ def main(): if splash: splash.finish(window) fix_windows_taskbar_icon_for_window(window) + # 强制激活主窗口,确保在其他窗口操作后仍能弹出 + window.activateWindow() + window.raise_() QTimer.singleShot(100, _on_window_shown) diff --git a/requirements.txt b/requirements.txt index dd0acbc8097bc6bcf3a1bbac53472e57c972cc28..2863ba367b14100188f5fa56cd77683d87b34857 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ PySide6>=6.0.0 PySide6-Fluent-Widgets>=1.0.0 +PySideSix-Frameless-Window>=0.1.0 Pillow>=9.0.0 requests>=2.32.0 numpy>=1.21.0 diff --git a/tests/test_logger.py b/tests/test_logger.py index 8b360664ba391f7a68946b4968966c65af1e4ebf..a0da9c7190f320ee178cf9926753da5a8081c120 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -7,7 +7,6 @@ import logging import time from pathlib import Path -from typing import Any, Dict from unittest.mock import patch # 第三方库导入 diff --git a/tests/test_threading.py b/tests/test_threading.py index 8a23c55b1c29bf65c321aa448a376811aa895936..1acf107fed36c03b1cb7da1775e3daaa8fec165c 100644 --- a/tests/test_threading.py +++ b/tests/test_threading.py @@ -5,19 +5,18 @@ # 标准库导入 import time -from typing import List, Tuple # 第三方库导入 import pytest -from PySide6.QtCore import Qt, QCoreApplication, QThread +from PySide6.QtCore import Qt from PySide6.QtGui import QImage # 项目模块导入 from core.histogram_service import HistogramCalculator, HistogramService from core.luminance_service import LuminanceCalculator, LuminanceService from core.color_service import DominantColorExtractor, ColorService -from core.image_service import ProgressiveImageLoader, ImageService -from core.palette_service import PaletteImporter, PaletteExporter, PaletteService +from core.image_service import ProgressiveImageLoader +from core.palette_service import PaletteImporter, PaletteExporter class TestHistogramCalculator: diff --git a/ui/canvases.py b/ui/canvases.py index 01522960f01c1ca0dea6b9f04e022588cb8061ff..6eae108f14e7c2340f81ef62c249b0a08a6cc9bc 100644 --- a/ui/canvases.py +++ b/ui/canvases.py @@ -10,7 +10,7 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel from qfluentwidgets import Action, FluentIcon, RoundMenu # 项目模块导入 -from core import get_luminance, get_zone, ServiceFactory, ZONE_WIDTH, log_user_action +from core import get_luminance, get_zone, ServiceFactory, log_user_action from utils import tr from .color_picker import ColorPicker from .zoom_viewer import ZoomViewer @@ -162,11 +162,19 @@ class BaseCanvas(QWidget): self.update() def resizeEvent(self, event) -> None: - """窗口大小改变时更新加载状态组件位置""" + """窗口大小改变时处理相关逻辑""" super().resizeEvent(event) + + # 更新加载状态组件位置 if self._is_loading: self._loading_widget.setGeometry(self.rect()) + # 重新调整图片 + if self._image and not self._image.isNull(): + self.update_picker_positions() + self.extract_all() + self.update() + def set_image(self, image_path: str) -> None: """异步加载并显示图片(使用ImageService分阶段加载,非阻塞) @@ -638,15 +646,6 @@ class BaseCanvas(QWidget): self.open_image_requested.emit() event.accept() - def resizeEvent(self, event) -> None: - """窗口大小改变时重新调整图片""" - super().resizeEvent(event) - if self._image and not self._image.isNull(): - # 窗口大小改变时,更新取色点位置并重新提取数据 - self.update_picker_positions() - self.extract_all() - self.update() - def contextMenuEvent(self, event) -> None: """右键菜单事件""" # 只有在有图片时才显示右键菜单 diff --git a/ui/cards.py b/ui/cards.py index 8597508aead142cd0bfbde37960cfe436adfce71..3871c8f596d6d4ed7793ca87bbbd040015238655 100644 --- a/ui/cards.py +++ b/ui/cards.py @@ -80,9 +80,7 @@ class BaseCardPanel(QWidget): old_count = self._card_count self._card_count = count - - layout = self.layout() - + if count > old_count: self._add_cards(old_count, count) else: @@ -285,9 +283,14 @@ class ColorCard(BaseCard): super().__init__(index, parent) # 监听主题变化 self._theme_connection = qconfig.themeChangedFinished.connect( - self._update_color_block_style + self._update_styles ) + def _update_styles(self): + """更新样式以适配主题""" + self._update_hex_button_style() + self._update_color_block_style() + def closeEvent(self, event): """关闭事件 - 断开信号连接""" try: diff --git a/ui/color_extract.py b/ui/color_extract.py index ce8f031a90b06329408f5b5912794e6ffd4d4347..1c0fc181c478c61af41b32568daf6048787f8a1c 100644 --- a/ui/color_extract.py +++ b/ui/color_extract.py @@ -7,9 +7,10 @@ # 标准库导入 import uuid from datetime import datetime +from pathlib import Path # 第三方库导入 -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import Qt from PySide6.QtWidgets import ( QFileDialog, QHBoxLayout, QSplitter, QStackedWidget, QSizePolicy, QVBoxLayout, QWidget @@ -21,7 +22,7 @@ from qfluentwidgets import ( # 项目模块导入 from core import get_color_info, get_config_manager, ServiceFactory, log_user_action -from utils import tr, get_locale_manager +from utils import tr, get_locale_manager, get_default_image_directory, get_last_directory, set_last_directory from dialogs import EditPaletteDialog from .canvases import ImageCanvas from .cards import ColorCardPanel @@ -194,7 +195,7 @@ class ColorExtractInterface(QWidget): file_path, _ = QFileDialog.getOpenFileName( self, tr('color_extract.select_image'), - "", + get_last_directory("image_import", get_default_image_directory()), tr('color_extract.image_filter') ) @@ -204,10 +205,12 @@ class ColorExtractInterface(QWidget): params={"path": file_path, "source": "color_extract"}, result="success" ) + set_last_directory("image_import", str(Path(file_path).parent)) self.image_canvas.set_image(file_path) def on_image_loaded(self, file_path): - """图片加载完成回调""" + """图片加载完成回调(由主窗口同步时调用)""" + # 图片数据处理已在 on_image_data_loaded 中完成 pass def on_image_data_loaded(self, pixmap, image): diff --git a/ui/color_generation.py b/ui/color_generation.py index 2f0ecf3da6517974d13b2078a73f7f3a2c973a00..f41d719e7564d107330ffa2b0c67a626911b39ba 100644 --- a/ui/color_generation.py +++ b/ui/color_generation.py @@ -15,14 +15,15 @@ from PySide6.QtWidgets import ( QSplitter ) from PySide6.QtCore import Qt, Signal, QTimer -from PySide6.QtGui import QColor from qfluentwidgets import ( - CardWidget, PushButton, ToolButton, FluentIcon, InfoBar, InfoBarPosition, + PushButton, ToolButton, FluentIcon, InfoBar, InfoBarPosition, qconfig, isDarkTheme, ComboBox, PrimaryPushButton, Slider ) # 项目模块导入 -from core import get_color_info, get_config_manager, hsb_to_rgb, rgb_to_hsb, adjust_brightness +from core import ( + get_color_info, get_config_manager, hsb_to_rgb, rgb_to_hsb, rgb_hue_to_ryb_hue, ryb_hue_to_rgb_hue +) from utils import tr, get_locale_manager from dialogs import EditPaletteDialog from .cards import BaseCard, BaseCardPanel, ColorModeContainer, get_text_color, get_placeholder_color, get_border_color @@ -47,7 +48,12 @@ class GenerationColorInfoCard(BaseCard): self._hex_visible = True super().__init__(index, parent) # 监听主题变化 - qconfig.themeChangedFinished.connect(self._update_color_block_style) + qconfig.themeChangedFinished.connect(self._update_styles) + + def _update_styles(self): + """更新样式以适配主题""" + self._update_hex_button_style() + self._update_color_block_style() def setup_ui(self): """设置界面""" @@ -482,8 +488,7 @@ class ColorGenerationInterface(QWidget): self.random_btn.setText(tr('color_generation.random')) self.favorite_button.setText(tr('color_generation.favorite')) self.brightness_label.setText(tr('color_generation.brightness')) - - current_index = self.scheme_combo.currentIndex() + self.scheme_combo.setItemText(0, tr('color_generation.schemes.monochromatic')) self.scheme_combo.setItemText(1, tr('color_generation.schemes.analogous')) self.scheme_combo.setItemText(2, tr('color_generation.schemes.complementary')) @@ -550,7 +555,7 @@ class ColorGenerationInterface(QWidget): def on_base_color_changed(self, h, s, b): """基准颜色改变回调 - 色相变化时,所有采样点跟随旋转; + 色相变化时,所有采样点跟随旋转,保持相对角度关系; 饱和度变化时,仅基准点变化,其他采样点保持原位。 """ # 计算色相变化量 @@ -562,17 +567,28 @@ class ColorGenerationInterface(QWidget): self._base_hue = h self._base_saturation = s - # RYB模式下,重新生成配色以保持RYB色轮上的相对角度关系 - if self._color_wheel_mode == 'RYB' and delta_h != 0: - self._generate_scheme_colors() - return - - # 色相变化:所有采样点跟着旋转(仅RGB模式) + # 色相变化:所有采样点跟着旋转 if delta_h != 0 and self._scheme_colors: - for i in range(len(self._scheme_colors)): - old_h, old_s, old_b = self._scheme_colors[i] - new_h = (old_h + delta_h) % 360 - self._scheme_colors[i] = (new_h, old_s, old_b) + if self._color_wheel_mode == 'RYB': + # RYB模式下需要在RYB色轮上进行偏移,保持RYB角度关系 + # 将RGB的delta_h转换为RYB的delta_h + old_base_ryb = rgb_hue_to_ryb_hue(self._base_hue - delta_h) + new_base_ryb = rgb_hue_to_ryb_hue(self._base_hue) + ryb_delta_h = new_base_ryb - old_base_ryb + + for i in range(len(self._scheme_colors)): + old_h, old_s, old_b = self._scheme_colors[i] + # RGB -> RYB -> 偏移 -> RGB + ryb_h = rgb_hue_to_ryb_hue(old_h) + new_ryb_h = (ryb_h + ryb_delta_h) % 360 + new_h = ryb_hue_to_rgb_hue(new_ryb_h) + self._scheme_colors[i] = (new_h, old_s, old_b) + else: + # RGB模式下直接在RGB色轮上偏移 + for i in range(len(self._scheme_colors)): + old_h, old_s, old_b = self._scheme_colors[i] + new_h = (old_h + delta_h) % 360 + self._scheme_colors[i] = (new_h, old_s, old_b) # 饱和度变化:更新 _scheme_colors[0](基准点) if delta_s != 0 and self._scheme_colors: @@ -601,7 +617,6 @@ class ColorGenerationInterface(QWidget): self._scheme_colors[index] = (h, s, b) # 转换为RGB并更新色块面板 - rgb = hsb_to_rgb(h, s, b) self.color_panel.set_colors([hsb_to_rgb(*c) for c in self._scheme_colors]) def on_brightness_changed(self, value): diff --git a/ui/color_preview.py b/ui/color_preview.py index b0b5b3709f407996dc411081390182ddc90487bc..a34c1821e332958b7ad4adceee18c6c542d3d297 100644 --- a/ui/color_preview.py +++ b/ui/color_preview.py @@ -12,6 +12,7 @@ - 配色预览界面 """ # 标准库导入 +from pathlib import Path from typing import List, Optional, Dict, Any, Type # 第三方库导入 @@ -35,7 +36,7 @@ from core.color import get_color_info from core.logger import get_logger, log_user_action from dialogs.edit_palette import EditPaletteDialog from dialogs.export_settings_dialog import ExportSettingsDialog -from utils import tr, get_locale_manager +from utils import tr, get_locale_manager, get_default_image_directory, get_last_directory, set_last_directory from utils.theme_colors import get_border_color, get_text_color logger = get_logger("color_preview") @@ -306,7 +307,7 @@ class ColorDotBar(QWidget): } # 打开编辑对话框(预览配色场景不显示名称输入) - dialog = EditPaletteDialog(palette_data=palette_data, parent=self, show_name_input=False) + dialog = EditPaletteDialog(palette_data=palette_data, parent=self.window(), 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: @@ -1882,13 +1883,15 @@ class ColorPreviewInterface(QWidget): file_path, _ = QFileDialog.getOpenFileName( self, tr('color_preview.import_svg'), - "", + get_last_directory("svg_import", get_default_image_directory()), tr('color_preview.svg_filter') ) if not file_path: return + set_last_directory("svg_import", str(Path(file_path).parent)) + svg_preview = self.preview_panel.get_svg_preview() if svg_preview is None: InfoBar.warning( @@ -1960,12 +1963,13 @@ class ColorPreviewInterface(QWidget): return # 打开文件保存对话框(获取保存目录) + last_dir = get_last_directory("svg_export", get_default_image_directory()) if len(selected_indices) == 1: # 单张图片 - default_name = f"{filename_prefix}.{export_format}" + default_name = str(Path(last_dir) / f"{filename_prefix}.{export_format}") else: # 多张图片 - default_name = filename_prefix + default_name = str(Path(last_dir) / filename_prefix) file_path, _ = QFileDialog.getSaveFileName( self, @@ -1977,6 +1981,8 @@ class ColorPreviewInterface(QWidget): if not file_path: return + set_last_directory("svg_export", str(Path(file_path).parent)) + # 执行导出 success_count = 0 failed_messages = [] @@ -2007,7 +2013,6 @@ class ColorPreviewInterface(QWidget): base_path = base_path[:-len(ext)] break # 获取保存目录 - from pathlib import Path path_obj = Path(file_path) parent_dir = path_obj.parent # 生成带序号的文件名 @@ -2081,13 +2086,15 @@ class ColorPreviewInterface(QWidget): file_path, _ = QFileDialog.getOpenFileName( self, tr('color_preview.import_template'), - "", + get_last_directory("svg_import", get_default_image_directory()), tr('color_preview.svg_filter') ) if not file_path: return + set_last_directory("svg_import", str(Path(file_path).parent)) + is_valid, error_msg = self._get_preview_service().validate_svg_file(file_path) if not is_valid: InfoBar.error( diff --git a/ui/color_wheel.py b/ui/color_wheel.py index 26fc12129d0959b5a8c277e7a64c65babbcf3d09..766b0f1309ac7b2a181b317a96aa0293b7ab2cb9 100644 --- a/ui/color_wheel.py +++ b/ui/color_wheel.py @@ -127,9 +127,10 @@ class HSBColorWheel(QWidget): """ import math - # 色相转换为角度(0°在上方12点钟方向,逆时针增加) + # 色相转换为角度(0°在上方12点钟方向,顺时针增加) # 加上90度将0°从右侧(3点钟)旋转到上方(12点钟) - angle_rad = ((h + 90) * math.pi / 180.0) + # 360-h实现水平翻转,使色相顺时针排列 + angle_rad = ((360 - h + 90) * math.pi / 180.0) # 饱和度转换为半径(0%在中心,100%在边缘) # 使用完整的色轮半径,让采样点可以到达圆周 @@ -175,6 +176,7 @@ class HSBColorWheel(QWidget): # 减90度偏移,使0°色相(红色)位于12点钟方向 angle = math.atan2(-dy, dx) - math.pi / 2 hue = (angle / (2 * math.pi)) % 1.0 + hue = (1.0 - hue) % 1.0 # 水平翻转:色相取反,实现顺时针排列 # 计算饱和度(距离中心的远近) saturation = min(distance / self._wheel_radius, 1.0) @@ -306,7 +308,8 @@ class HSBColorWheel(QWidget): for angle, label in hue_labels: # 计算标签位置(注意Y轴翻转) # 加上90度将0°从右侧(3点钟)旋转到上方(12点钟) - adjusted_angle = angle + 90 + # 360-angle实现水平翻转,使标签顺时针排列 + adjusted_angle = (360 - angle) + 90 rad = math.radians(adjusted_angle) x = self._center_x + label_radius * math.cos(rad) y = self._center_y - label_radius * math.sin(rad) @@ -457,7 +460,8 @@ class InteractiveColorWheel(QWidget): (x, y) 坐标 """ # 加上90度将0°从右侧(3点钟)旋转到上方(12点钟) - angle_rad = ((h + 90) * math.pi / 180.0) + # 360-h实现水平翻转,使色相顺时针排列 + angle_rad = ((360 - h + 90) * math.pi / 180.0) # 使用完整的色轮半径,让采样点可以到达圆周 max_radius = self._wheel_radius @@ -491,6 +495,7 @@ class InteractiveColorWheel(QWidget): angle = math.atan2(-dy, dx) # 减90度偏移,使0°色相(红色)位于12点钟方向 hue = ((angle - math.pi / 2) / (2 * math.pi)) % 1.0 * 360 + hue = (360 - hue) % 360 # 水平翻转:色相取反,实现顺时针排列 return hue, saturation @@ -596,6 +601,7 @@ class InteractiveColorWheel(QWidget): angle = math.atan2(-dy, dx) # 减90度偏移,使0°色相(红色)位于12点钟方向 hue = ((angle - math.pi / 2) / (2 * math.pi)) % 1.0 + hue = (1.0 - hue) % 1.0 # 水平翻转:色相取反,实现顺时针排列 saturation = min(distance / self._wheel_radius, 1.0) # 使用全局明度值 value = brightness_value @@ -675,7 +681,8 @@ class InteractiveColorWheel(QWidget): for angle, label in hue_labels: # 计算标签位置(注意Y轴翻转) # 加上90度将0°从右侧(3点钟)旋转到上方(12点钟) - adjusted_angle = angle + 90 + # 360-angle实现水平翻转,使标签顺时针排列 + adjusted_angle = (360 - angle) + 90 rad = math.radians(adjusted_angle) x = self._center_x + label_radius * math.cos(rad) y = self._center_y - label_radius * math.sin(rad) diff --git a/ui/gradient_extract.py b/ui/gradient_extract.py index 271da410c4192e4c51c8582b3e47c8fdcc43be01..b5b4ddc86c01a099e5dc54a2c17e9cb6216641f3 100644 --- a/ui/gradient_extract.py +++ b/ui/gradient_extract.py @@ -7,20 +7,19 @@ from typing import List, Tuple from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QColor, QPainter from PySide6.QtWidgets import ( - QApplication, QDialog, QHBoxLayout, QLabel, QLineEdit, QPushButton, - QSizePolicy, QSplitter, QVBoxLayout, QWidget + QDialog, QHBoxLayout, QLabel, QLineEdit, QSizePolicy, QSplitter, QVBoxLayout, QWidget ) from qfluentwidgets import ( - FluentIcon, InfoBar, InfoBarPosition, PushButton, Slider, ToolButton, qconfig, isDarkTheme, ScrollArea + FluentIcon, InfoBar, InfoBarPosition, PushButton, Slider, qconfig, ScrollArea ) # 项目模块导入 -from core import generate_gradient, generate_random_gradient, get_color_info, rgb_to_hex +from core import generate_gradient, generate_random_gradient, get_color_info from core import get_config_manager from core.logger import get_logger, log_user_action from ui.cards import ColorCard from utils import tr, get_locale_manager, calculate_grid_columns -from utils.theme_colors import get_border_color, get_card_background_color, get_text_color +from utils.theme_colors import get_border_color, get_text_color logger = get_logger("gradient_extract") @@ -425,7 +424,6 @@ class GradientExtractInterface(QWidget): def _update_hex_input_style(self): """更新16进制输入框样式(与配色管理一致)""" primary_color = get_text_color(secondary=False) - secondary_color = get_text_color(secondary=True) border_color = get_border_color() input_style = f""" @@ -584,7 +582,8 @@ class GradientExtractInterface(QWidget): # 将HEX转换为RGB r, g, b = self._hex_to_rgb(current_color) - dialog = ColorPickerDialog((r, g, b), self) + # 使用顶层窗口作为父窗口,避免背景色异常和两套窗口控制器 + dialog = ColorPickerDialog((r, g, b), self.window()) if dialog.exec() == QDialog.DialogCode.Accepted: color_info = dialog.get_color_info() if color_info: diff --git a/ui/histograms.py b/ui/histograms.py index d703329b79985d27265d3d1b992ea7e9fba03e8d..62507bcd2751f5b3ecb927040a88470b73cb3838 100644 --- a/ui/histograms.py +++ b/ui/histograms.py @@ -1,6 +1,6 @@ # 第三方库导入 import math -from typing import List, Optional +from typing import List from PySide6.QtCore import Qt, Signal from PySide6.QtGui import QColor, QFont, QLinearGradient, QPainter, QPen, QMouseEvent from PySide6.QtWidgets import QWidget diff --git a/ui/luminance_extract.py b/ui/luminance_extract.py index 9abaea6f439626cea1eca7de857a7b82fef22060..1b737a0d4e9528b9f23f4be02f7fdac3a0017fe4 100644 --- a/ui/luminance_extract.py +++ b/ui/luminance_extract.py @@ -4,6 +4,7 @@ """ # 标准库导入 +from pathlib import Path from typing import Dict, Any from PySide6.QtCore import Qt, QTimer, Signal @@ -12,7 +13,7 @@ from PySide6.QtWidgets import QFileDialog, QSplitter, QVBoxLayout, QWidget # 项目模块导入 from core import LuminanceService from core.logger import get_logger, log_user_action -from utils import tr, get_locale_manager +from utils import tr, get_locale_manager, get_default_image_directory, get_last_directory, set_last_directory from .canvases import LuminanceCanvas from .histograms import LuminanceHistogramWidget @@ -100,12 +101,13 @@ class LuminanceExtractInterface(QWidget): file_path, _ = QFileDialog.getOpenFileName( self, tr('luminance_extract.select_image'), - "", + get_last_directory("image_import", get_default_image_directory()), tr('luminance_extract.image_filter') ) if file_path: log_user_action("open_image", {"file_path": file_path}) + set_last_directory("image_import", str(Path(file_path).parent)) self._load_image(file_path) def change_image(self): diff --git a/ui/main_window.py b/ui/main_window.py index 6a55395841d3a42dfea7cf7fcd6c999a54475ec5..a312b156726556d25625b57a3d87f4e7fbb5bcb7 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -3,15 +3,14 @@ import sys from typing import List, Dict, Any # 第三方库导入 -from PySide6.QtCore import Qt, QTimer -from PySide6.QtGui import QIcon, QKeySequence, QScreen, QShortcut +from PySide6.QtCore import Qt +from PySide6.QtGui import QIcon, QKeySequence, QShortcut from PySide6.QtWidgets import ( - QApplication, QFileDialog, QHBoxLayout, QLabel, QSplitter, QVBoxLayout, QWidget + QApplication, QLabel ) from qfluentwidgets import FluentIcon, FluentWindow, NavigationItemPosition, qrouter, FluentTitleBar, ToolButton, setTheme, Theme, isDarkTheme # 项目模块导入 -from core import get_color_info from core import get_config_manager, ImageMediator from utils import tr, get_locale_manager from version import version_manager @@ -23,10 +22,21 @@ from .palette_management import PaletteManagementInterface from .preset_color import PresetColorInterface from .settings import SettingsInterface from .color_preview import ColorPreviewInterface -from .cards import ColorCardPanel -from .histograms import LuminanceHistogramWidget, RGBHistogramWidget from .color_wheel import HSBColorWheel, InteractiveColorWheel -from .canvases import ImageCanvas, LuminanceCanvas + +# 工具按钮统一样式 +_TOOLBUTTON_STYLE = """ + ToolButton { + background-color: transparent !important; + border: none !important; + } + ToolButton:hover { + background-color: rgba(128, 128, 128, 30) !important; + } + ToolButton:pressed { + background-color: rgba(128, 128, 128, 50) !important; + } +""" class CustomTitleBar(FluentTitleBar): @@ -39,18 +49,7 @@ class CustomTitleBar(FluentTitleBar): self.themeButton = ToolButton(self) self.themeButton.setFixedSize(40, 32) self.themeButton.setToolTip(tr('title_bar.toggle_theme')) - self.themeButton.setStyleSheet(""" - ToolButton { - background-color: transparent !important; - border: none !important; - } - ToolButton:hover { - background-color: rgba(128, 128, 128, 30) !important; - } - ToolButton:pressed { - background-color: rgba(128, 128, 128, 50) !important; - } - """) + self.themeButton.setStyleSheet(_TOOLBUTTON_STYLE) self._update_theme_icon() # 连接点击事件 @@ -60,18 +59,7 @@ class CustomTitleBar(FluentTitleBar): self.fullscreenButton = ToolButton(self) self.fullscreenButton.setFixedSize(40, 32) self.fullscreenButton.setToolTip(tr('title_bar.toggle_fullscreen')) - self.fullscreenButton.setStyleSheet(""" - ToolButton { - background-color: transparent !important; - border: none !important; - } - ToolButton:hover { - background-color: rgba(128, 128, 128, 30) !important; - } - ToolButton:pressed { - background-color: rgba(128, 128, 128, 50) !important; - } - """) + self.fullscreenButton.setStyleSheet(_TOOLBUTTON_STYLE) self._update_fullscreen_icon() # 连接点击事件 diff --git a/ui/palette_management.py b/ui/palette_management.py index 8565a1e49e73157277c08a4fe8a061a89674598f..d49a7909fa72a76c2b9e32aa0b21549b76dcd93a 100644 --- a/ui/palette_management.py +++ b/ui/palette_management.py @@ -1,6 +1,7 @@ # 标准库导入 import math from datetime import datetime +from pathlib import Path from typing import List, Dict, Any # 第三方库导入 @@ -11,18 +12,18 @@ from PySide6.QtWidgets import ( ) from qfluentwidgets import ( CardWidget, ScrollArea, ToolButton, FluentIcon, ComboBox, - InfoBar, InfoBarPosition, isDarkTheme, qconfig, + InfoBar, InfoBarPosition, qconfig, PushButton, SubtitleLabel, MessageBox ) # 项目模块导入 from core import get_color_info, hex_to_rgb, get_config_manager, ServiceFactory -from utils import tr, get_locale_manager, calculate_grid_columns +from utils import tr, get_locale_manager, calculate_grid_columns, get_default_data_directory, get_last_directory, set_last_directory from core.async_loader import BaseBatchLoader from core.grouping import generate_groups from core.logger import get_logger, log_user_action, log_performance from .cards import ColorModeContainer, get_text_color, get_border_color, get_placeholder_color -from utils.theme_colors import get_card_background_color, get_title_color, get_interface_background_color +from utils.theme_colors import get_title_color from dialogs import ColorblindPreviewDialog, ContrastCheckDialog, EditPaletteDialog logger = get_logger("palette_management") @@ -1285,13 +1286,14 @@ class PaletteManagementInterface(QWidget): file_path, _ = QFileDialog.getOpenFileName( self, tr('palette_management.import_title'), - "", + get_last_directory("palette_import", get_default_data_directory()), tr('palette_management.json_filter') ) if not file_path: return + set_last_directory("palette_import", str(Path(file_path).parent)) log_user_action("import_palette_start", {"file_path": file_path}) self._pending_import_path = file_path @@ -1365,7 +1367,8 @@ class PaletteManagementInterface(QWidget): def _on_export_clicked(self): """导出按钮点击""" - default_name = f"color_card_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + last_dir = get_last_directory("palette_export", get_default_data_directory()) + default_name = str(Path(last_dir) / f"color_card_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json") file_path, _ = QFileDialog.getSaveFileName( self, tr('palette_management.export_title'), @@ -1379,6 +1382,7 @@ class PaletteManagementInterface(QWidget): if not file_path.endswith('.json'): file_path += '.json' + set_last_directory("palette_export", str(Path(file_path).parent)) log_user_action("export_palette_start", {"file_path": file_path}) favorites = self._config_manager.get_favorites() @@ -1442,7 +1446,8 @@ class PaletteManagementInterface(QWidget): return palette_name = favorite_data.get('name', tr('palette_management.unnamed')) - default_name = f"{palette_name}.ase" + last_dir = get_last_directory("palette_export", get_default_data_directory()) + default_name = str(Path(last_dir) / f"{palette_name}.ase") file_path, _ = QFileDialog.getSaveFileName( self, @@ -1457,6 +1462,7 @@ class PaletteManagementInterface(QWidget): if not file_path.endswith('.ase'): file_path += '.ase' + set_last_directory("palette_export", str(Path(file_path).parent)) log_user_action("export_ase_start", {"file_path": file_path, "palette_name": palette_name}) with log_performance("export_ase", {"file_path": file_path, "color_count": len(colors)}): diff --git a/ui/preset_color.py b/ui/preset_color.py index ba0c493fe0ed167fd7053b5b777a4b5ba4e047b7..735ac25c179d6678444bd27b3a62adb7e17df8f0 100644 --- a/ui/preset_color.py +++ b/ui/preset_color.py @@ -2,7 +2,7 @@ import math import uuid from datetime import datetime -from typing import List, Dict, Any +from typing import Dict, Any # 第三方库导入 from PySide6.QtCore import Qt, Signal @@ -23,7 +23,7 @@ from core.color_data import ( get_color_source, get_all_color_sources, get_random_palettes, ColorSource ) from .cards import ColorModeContainer, get_text_color, get_border_color, get_placeholder_color -from utils.theme_colors import get_card_background_color, get_title_color, get_interface_background_color, get_secondary_text_color +from utils.theme_colors import get_title_color, get_secondary_text_color # ============================================================================= diff --git a/ui/settings.py b/ui/settings.py index 49ee4c49b4abf03893d0fe0ba236e519d83e6b14..35dea45d7f8fe081fe8e292adcf218bf35b230a3 100644 --- a/ui/settings.py +++ b/ui/settings.py @@ -1,23 +1,21 @@ # 标准库导入 from PySide6.QtCore import Qt, Signal from PySide6.QtWidgets import ( - QFileDialog, QHBoxLayout, QLabel, QVBoxLayout, QWidget + QHBoxLayout, QLabel, QVBoxLayout, QWidget ) from qfluentwidgets import ( - ComboBox, FluentIcon, InfoBar, InfoBarPosition, - PushButton, PushSettingCard, ScrollArea, SettingCardGroup, SubtitleLabel, SwitchButton, qconfig, isDarkTheme + ComboBox, FluentIcon, PushSettingCard, ScrollArea, SettingCardGroup, SubtitleLabel, SwitchButton, qconfig ) # 项目模块导入 from core import get_config_manager from core.logger import get_logger, log_user_action from utils import tr, get_supported_languages, set_language, get_locale_manager - +from utils.theme_colors import get_title_color from dialogs import AboutDialog, UpdateAvailableDialog +from version import version_manager logger = get_logger("settings") -from version import version_manager -from utils.theme_colors import get_title_color, get_text_color, get_interface_background_color, get_card_background_color, get_border_color AVAILABLE_COLOR_MODES = ['HSB', 'LAB', 'HSL', 'CMYK', 'RGB'] @@ -240,12 +238,17 @@ class SettingsInterface(QWidget): card.button.setVisible(False) combo_box = ComboBox(self.content_widget) + + combo_box.addItem(tr('settings.language_auto')) + combo_box.setItemData(0, 'auto') + supported_languages = get_supported_languages() for code, name in supported_languages.items(): + if code == 'auto': + continue combo_box.addItem(name) combo_box.setItemData(combo_box.count() - 1, code) - # 设置当前语言 for i in range(combo_box.count()): if combo_box.itemData(i) == self._language: combo_box.setCurrentIndex(i) @@ -297,6 +300,8 @@ class SettingsInterface(QWidget): # 更新语言卡片 self.language_card.titleLabel.setText(tr('settings.language_title')) self.language_card.contentLabel.setText(tr('settings.language_desc')) + # 更新"跟随系统"选项文本 + self.language_card.combo_box.setItemText(0, tr('settings.language_auto')) # 更新16进制显示卡片 self.hex_display_card.titleLabel.setText(tr('settings.hex_display')) diff --git a/ui/zoom_viewer.py b/ui/zoom_viewer.py index 97057470860ac34188a3b26edc7b5ceac3ea62c4..59a63eb477cf59ad998e265b2b81050e285936f5 100644 --- a/ui/zoom_viewer.py +++ b/ui/zoom_viewer.py @@ -1,6 +1,6 @@ # 第三方库导入 from PySide6.QtCore import QPoint, Qt -from PySide6.QtGui import QColor, QImage, QPainter, QPainterPath, QPen +from PySide6.QtGui import QPainter, QPainterPath, QPen from PySide6.QtWidgets import QWidget # 项目模块导入 diff --git a/utils/__init__.py b/utils/__init__.py index 622ca684af0022eeb2f8a82e086f87762233e3e4..18b19b4a63ecec1da0e5185b9e49777b7eca1baf 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -1,6 +1,13 @@ """工具函数模块""" -from .icon import load_icon_universal, get_icon_path, create_fallback_icon +# 标准库导入 +from pathlib import Path + +# 第三方库导入 +from PySide6.QtCore import QSettings + +# 项目模块导入 +from .icon import load_icon_universal, get_icon_path, create_fallback_icon, get_base_path from .platform import set_app_user_model_id, fix_windows_taskbar_icon_for_window, set_window_title_bar_theme from .layout import calculate_grid_columns from .locale import ( @@ -12,10 +19,58 @@ from .locale import ( get_supported_languages, ) + +def get_default_image_directory() -> str: + """获取默认图片导入目录 + + Returns: + str: 用户图片文件夹路径 + """ + return str(Path.home() / "Pictures") + + +def get_default_data_directory() -> str: + """获取默认数据文件目录(用于导入/导出配色数据) + + Returns: + str: 用户文档文件夹路径 + """ + return str(Path.home() / "Documents") + + +def get_last_directory(key: str, default_dir: str) -> str: + """获取用户上次选择的目录 + + Args: + key: 存储键名(区分不同功能) + default_dir: 默认目录(首次使用或记录不存在时返回) + + Returns: + str: 上次选择的目录或默认目录 + """ + settings = QSettings("ColorCard", "App") + last_dir = settings.value(f"last_directory/{key}", default_dir) + if last_dir and Path(last_dir).exists(): + return str(last_dir) + return default_dir + + +def set_last_directory(key: str, directory: str): + """记录用户选择的目录 + + Args: + key: 存储键名 + directory: 用户选择的目录路径 + """ + settings = QSettings("ColorCard", "App") + settings.setValue(f"last_directory/{key}", directory) + + __all__ = [ 'load_icon_universal', 'get_icon_path', 'create_fallback_icon', + 'get_base_path', 'set_app_user_model_id', 'fix_windows_taskbar_icon_for_window', 'set_window_title_bar_theme', @@ -26,4 +81,8 @@ __all__ = [ 'set_language', 'get_current_language', 'get_supported_languages', + 'get_default_image_directory', + 'get_default_data_directory', + 'get_last_directory', + 'set_last_directory', ] diff --git a/utils/locale.py b/utils/locale.py index 2961b548a26cde6d89abc9ee9ac7af58c25c57c1..af963efdc9b4d9441a0b3a3a7df2186b3015e0e7 100644 --- a/utils/locale.py +++ b/utils/locale.py @@ -9,7 +9,26 @@ import sys from pathlib import Path from typing import Any, Dict, Optional -from PySide6.QtCore import QObject, Signal +from PySide6.QtCore import QLocale, QObject, Signal + + +SYSTEM_LANGUAGE_MAPPING: Dict[str, str] = { + 'zh_CN': 'ZW_JT', + 'zh_Hans': 'ZW_JT', + 'zh': 'ZW_JT', + 'zh_TW': 'ZW_FT', + 'zh_HK': 'ZW_FT', + 'zh_Hant': 'ZW_FT', + 'en': 'EN_US', + 'en_US': 'EN_US', + 'en_GB': 'EN_US', + 'ja': 'JA_JP', + 'ja_JP': 'JA_JP', + 'fr': 'FR_FR', + 'fr_FR': 'FR_FR', + 'ru': 'RU_RU', + 'ru_RU': 'RU_RU', +} def _get_base_path() -> str: @@ -38,6 +57,7 @@ class LocaleManager(QObject): language_changed = Signal(str) SUPPORTED_LANGUAGES = { + 'auto': '跟随系统', 'ZW_JT': '简体中文', 'ZW_FT': '繁體中文', 'EN_US': 'English', @@ -46,7 +66,8 @@ class LocaleManager(QObject): 'RU_RU': 'Русский' } - DEFAULT_LANGUAGE = 'ZW_JT' + DEFAULT_LANGUAGE = 'auto' + FALLBACK_LANGUAGE = 'ZW_JT' def __init__(self): """初始化多语言管理器""" @@ -68,19 +89,21 @@ class LocaleManager(QObject): """加载指定语言的翻译数据 Args: - language_code: 语言代码(如 'ZW_JT', 'EN_US') + language_code: 语言代码(如 'ZW_JT', 'EN_US', 'auto') Returns: bool: 是否加载成功 """ - if language_code not in self.SUPPORTED_LANGUAGES: - language_code = self.DEFAULT_LANGUAGE + resolved_code = self.resolve_language(language_code) + + if resolved_code not in self.SUPPORTED_LANGUAGES: + resolved_code = self.FALLBACK_LANGUAGE - locale_file = self._locales_dir / f'{language_code}.json' + locale_file = self._locales_dir / f'{resolved_code}.json' if not locale_file.exists(): - if language_code != self.DEFAULT_LANGUAGE: - return self.load_language(self.DEFAULT_LANGUAGE) + if resolved_code != self.FALLBACK_LANGUAGE: + return self.load_language(self.FALLBACK_LANGUAGE) return False try: @@ -91,6 +114,36 @@ class LocaleManager(QObject): except (json.JSONDecodeError, IOError, OSError): return False + def resolve_language(self, language_code: str) -> str: + """解析语言代码,如果是 'auto' 则返回系统语言 + + Args: + language_code: 语言代码(如 'auto', 'ZW_JT', 'EN_US') + + Returns: + str: 实际的语言代码 + """ + if language_code == 'auto': + return self.get_system_language() + return language_code + + def get_system_language(self) -> str: + """获取系统语言并映射到项目支持的语言代码 + + Returns: + str: 映射后的语言代码,未匹配则返回默认语言 + """ + try: + system_locale = QLocale.system().name() + if system_locale in SYSTEM_LANGUAGE_MAPPING: + return SYSTEM_LANGUAGE_MAPPING[system_locale] + base_locale = system_locale.split('_')[0] + if base_locale in SYSTEM_LANGUAGE_MAPPING: + return SYSTEM_LANGUAGE_MAPPING[base_locale] + except (AttributeError, ValueError): + pass + return self.FALLBACK_LANGUAGE + def set_language(self, language_code: str) -> bool: """设置当前语言 diff --git a/utils/platform.py b/utils/platform.py index c95695f659f158863e20853ef1d1bca7c852741a..17d6364ade234fdd6ea2cf96042352003695546c 100644 --- a/utils/platform.py +++ b/utils/platform.py @@ -5,7 +5,7 @@ import sys from typing import Dict, Optional # 第三方库导入 -from PySide6.QtCore import QObject, Qt, QTimer, Signal +from PySide6.QtCore import QObject, QTimer, Signal # 项目模块导入 from .icon import get_icon_path @@ -145,7 +145,8 @@ def fix_windows_taskbar_icon_for_window(window) -> bool: try: # 确保窗口已经显示 - if not window.isVisible(): + # 注意:全屏窗口的 isVisible 可能返回 False,需要特殊处理 + if not window.isVisible() and not window.isFullScreen(): window.show() window.raise_() window.activateWindow() diff --git a/utils/theme_colors.py b/utils/theme_colors.py index 50d7137ac7dd2bd05830368afa8e90506a8c19c3..b468a8920fa1c0410c724225baee8b5a9a66b4d5 100644 --- a/utils/theme_colors.py +++ b/utils/theme_colors.py @@ -12,16 +12,6 @@ def get_canvas_background_color(): return QColor(42, 42, 42) -def get_card_background_color(): - """获取卡片背景颜色""" - return QColor(42, 42, 42) if isDarkTheme() else QColor(255, 255, 255) - - -def get_interface_background_color(): - """获取界面背景颜色(与FluentWindow一致)""" - return QColor(32, 32, 32) if isDarkTheme() else QColor(243, 243, 243) - - def get_histogram_background_color(): """获取直方图背景颜色 - 固定灰黑色 #2a2a2a""" return QColor(42, 42, 42) @@ -52,11 +42,6 @@ def get_border_color(): return QColor(80, 80, 80) if isDarkTheme() else QColor(221, 221, 221) -def get_border_color_secondary(): - """获取次要边框颜色""" - return QColor(120, 120, 120) if isDarkTheme() else QColor(200, 200, 200) - - # ========== 占位符/空状态颜色 ========== def get_placeholder_color(): """获取占位符颜色(空色块背景)""" @@ -201,36 +186,11 @@ def get_canvas_empty_text_color(): return QColor(150, 150, 150) -def get_picker_colors(): - """获取取色点颜色列表""" - return [ - QColor(0, 102, 255, 100), - QColor(0, 128, 255, 100), - QColor(0, 153, 255, 100), - QColor(0, 204, 102, 100), - QColor(102, 255, 102, 100), - QColor(255, 204, 0, 100), - QColor(255, 128, 0, 100), - QColor(255, 51, 102, 100), - QColor(200, 100, 255, 100), - ] - - def get_tooltip_bg_color(): """获取提示框背景颜色""" return QColor(0, 0, 0, 180) -def get_tooltip_border_color(): - """获取提示框边框颜色""" - return QColor(255, 255, 255) - - -def get_tooltip_text_color(): - """获取提示框文本颜色""" - return QColor(0, 0, 0) - - # ========== 缩放查看器颜色 ========== def get_zoom_grid_color(): """获取缩放查看器网格颜色""" @@ -248,6 +208,21 @@ def get_dialog_bg_color(): return QColor(32, 32, 32) if isDarkTheme() else QColor(255, 255, 255) +def get_close_button_hover_bg_color(): + """获取关闭按钮悬停背景颜色""" + return QColor(196, 43, 28) if isDarkTheme() else QColor(232, 17, 35) + + +def get_close_button_hover_color(): + """获取关闭按钮悬停图标颜色""" + return QColor(255, 255, 255) + + +def get_close_button_pressed_color(): + """获取关闭按钮按下图标颜色""" + return QColor(255, 255, 255) + + # ========== Zone框颜色 ========== def get_zone_background_color(): """获取Zone框背景颜色""" @@ -259,12 +234,6 @@ def get_zone_text_color(): return QColor(255, 255, 255) if isDarkTheme() else QColor(0, 0, 0) -# ========== 收藏组件颜色 ========== -def get_favorite_icon_color(): - """获取收藏界面图标颜色""" - return QColor(153, 153, 153) - - # ========== 高饱和度区域高亮颜色 ========== def get_high_saturation_highlight_color(): """获取高饱和度区域高亮颜色 - 半透明品红色""" diff --git a/version.py b/version.py index a22ae180cb398401c6e642542a7e676931d2bcff..77daee7a1529f9fb5f83eb9937f5fccca2f33eab 100644 --- a/version.py +++ b/version.py @@ -8,8 +8,8 @@ class VersionManager: """初始化版本管理器""" # 版本号组件 self.major: int = 1 - self.minor: int = 5 - self.patch: int = 1 + self.minor: int = 6 + self.patch: int = 0 self.build: int = 0 self.prerelease: str = "" diff --git a/version.txt b/version.txt index ab7dd437beb6065f1915ee9fdc567fca700d6930..5f427fb704e5e698446f99090c5113c7a1a032df 100644 --- a/version.txt +++ b/version.txt @@ -1,6 +1,6 @@ -1.5.1 -2026.3.15.1 -1.5.1.0 +1.6.0 +2026.3.22.1 +1.6.0.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 691c4f15d21f4e7952ef9e5c9022e7ca673a85e4..484e954b603070ab5596c46162da6ccf9ecf7cb1 100644 --- a/version_info.txt +++ b/version_info.txt @@ -1,7 +1,7 @@ VSVersionInfo( ffi=FixedFileInfo( - filevers=(2026,3,15,1), - prodvers=(1,5,1,0), + filevers=(2026,3,22,1), + prodvers=(1,6,0,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.1'), + StringStruct(u'FileVersion', u'1.6.0'), 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.1'), + StringStruct(u'ProductVersion', u'1.6.0'), StringStruct(u'Comments', u'一站式的图片的图片分析和配色工具') ] ) diff --git a/website/changelog.html b/website/changelog.html index 5f052429f2e3f00e4b21aeab9d82a8ff773e0fa5..e94a138e2b66bdafeae2121bf9e6a025321d786f 100644 --- a/website/changelog.html +++ b/website/changelog.html @@ -171,6 +171,7 @@ 项目起源 下载 更新日志 + 使用说明 反馈 关于我们
diff --git a/website/index.html b/website/index.html index e5ad4931d9ac4767634c145d4567ac81ce3b2e3f..2b0b91a54dfb105bcd2abca5177bed2e9ec485e7 100644 --- a/website/index.html +++ b/website/index.html @@ -1262,7 +1262,7 @@ { icon: 'eye', title: '配色预览', desc: '支持手机UI、网页、插画、排版、品牌、海报、图案、杂志等8种场景预览,支持自定义SVG' }, { icon: 'barchart', title: '明度分析', desc: '将图片按明度分为9个区域,提供直方图可视化,辅助调色决策' }, { icon: 'gradient', title: '渐变生成', desc: '选择起始和结束颜色,生成渐变色序列,支持RGB/HSB/LAB三种颜色空间插值,可调节1-10个中间色' }, - { icon: 'globe', title: '多语言支持', desc: '支持简体中文、繁体中文、英语、日语、法语、俄语六种语言界面切换' }, + { icon: 'globe', title: '多语言支持', desc: '支持简体中文、繁体中文、英语、日语、法语、俄语六种语言界面切换,支持跟随系统语言自动切换' }, { icon: 'moon', title: '明暗主题', desc: '支持深色模式和浅色模式切换,保护视力,适应不同使用环境' }, ]; diff --git a/website/public/changelog.json b/website/public/changelog.json index 65bb19fa4b2f94ece25776f4050235aa6a07c6d0..8d5c58bbb1d29a2ce7fd439df217fc4925d5b76d 100644 --- a/website/public/changelog.json +++ b/website/public/changelog.json @@ -1,5 +1,45 @@ { "versions": [ + { + "version": "v1.6.0", + "date": "2026-03-22", + "changes": [ + { + "category": "新增功能", + "items": [ + "新增「跟随系统」语言选项,应用语言可自动适配系统语言设置" + ] + }, + { + "category": "问题修复", + "items": [ + "修复启动动画期间操作其他窗口导致主窗口不弹出的问题", + "修复 Hex 值区域字重异常问题" + ] + }, + { + "category": "界面优化", + "items": [ + "HSB 色轮改为顺时针排列,符合常用色彩理论习惯", + "所有对话框统一采用无边框设计,视觉风格更加一致", + "禁用关于对话框文本选择高亮,避免误操作" + ] + }, + { + "category": "体验优化", + "items": [ + "优化文件导入导出目录路径,图片导入默认打开用户图片文件夹,配色文件导入导出路径更加便捷", + "统一色轮采样点在 RGB 和 RYB 模式下的行为一致性" + ] + }, + { + "category": "代码重构", + "items": [ + "优化内部代码结构,提升软件稳定性" + ] + } + ] + }, { "version": "v1.5.1", "date": "2026-03-15", diff --git "a/\346\226\207\346\241\243/\344\273\243\347\240\201\345\256\241\346\237\245\350\246\201\351\242\206.md" "b/\346\226\207\346\241\243/\344\273\243\347\240\201\345\256\241\346\237\245\350\246\201\351\242\206.md" new file mode 100644 index 0000000000000000000000000000000000000000..a0f647f4a2eed0d23d3ab46aba9ad2bebb0256b5 --- /dev/null +++ "b/\346\226\207\346\241\243/\344\273\243\347\240\201\345\256\241\346\237\245\350\246\201\351\242\206.md" @@ -0,0 +1,177 @@ +# 代码审查要领(非程序员版) + +> 无需编程基础,只需关注代码"整洁度" + +--- + +## 一、审查时机 + +- **AI 生成新功能后** +- **每次修改代码后** +- **每月定期审查**(运行工具检查) + +--- + +## 二、三个核心检查点 + +### 1. 导入检查(最简单) + +**问题**:AI 经常导入一堆没用的模块 + +**检查方法**: +```python +# ❌ 坏例子 - 导入了很多没用的 +from PySide6.QtWidgets import ( + QWidget, QPushButton, QLabel, QDialog, + QFileDialog, QScrollArea, QSplitter +) + +# 实际只用了 QWidget +class MyWidget(QWidget): + pass +``` + +**你的动作**:问 AI "这些导入都用到了吗?" + +**工具辅助**: +```bash +ruff check . # 会自动标记未使用的导入 +``` + +--- + +### 2. 重复检查(肉眼可见) + +**问题**:AI 喜欢复制粘贴代码 + +**检查方法**: +- 两段代码看起来**几乎一样**? +- 只是改了**几个数字或变量名**? + +**示例**: +```python +# ❌ 坏例子 - 重复代码 +self.themeButton.setStyleSheet(""" + ToolButton { background: transparent; } +""") + +self.fullscreenButton.setStyleSheet(""" + ToolButton { background: transparent; } +""") # 完全一样! +``` + +**你的动作**:问 AI "这段代码和前面那段能合并吗?" + +--- + +### 3. 长度检查(最直观) + +**问题**:AI 经常写出超长函数 + +**检查标准**: +| 指标 | 警戒线 | 危险线 | +|:---|:---:|:---:| +| 单个函数 | 50 行 | 100 行 | +| 单个文件 | 500 行 | 1000 行 | +| 类的方法数 | 10 个 | 20 个 | + +**你的动作**: +- 超过警戒线 → 问 AI "能拆短一点吗?" +- 超过危险线 → 必须拆分 + +**工具辅助**: +```bash +radon cc . -a -s # 显示复杂度高的函数 +``` + +--- + +## 三、审查问题清单 + +每次 AI 生成代码后,问这 3 个问题: + +1. [ ] **导入问题**:"这段代码有未使用的导入吗?" +2. [ ] **重复问题**:"这段代码和项目里其他代码有重复吗?" +3. [ ] **长度问题**:"这个函数能再短一点吗?" + +--- + +## 四、工具自动化检查 + +### 每月运行一次 + +```bash +# 1. 检查代码规范问题 +ruff check . + +# 2. 检查复杂度 +radon cc . -a -s + +# 3. 自动修复能修复的问题 +ruff check . --fix +``` + +### 看不懂报错怎么办? + +直接复制报错信息给 AI: +> "ruff 报了这个错误,是什么意思?怎么修?" + +--- + +## 五、常见 AI 代码问题 + +| 问题类型 | 表现 | 严重程度 | +|:---|:---|:---:| +| 未使用导入 | 导入了很多模块但没用 | 🟢 低 | +| 重复代码 | 复制粘贴后微调 | 🟡 中 | +| 函数过长 | 一个函数做太多事 | 🟡 中 | +| 过度设计 | 只有一种实现却用工厂模式 | 🟡 中 | +| 模糊变量名 | 用 `l` 做变量名 | 🟢 低 | +| 未定义名称 | 用了没导入的类 | 🔴 高 | + +--- + +## 六、审查原则 + +### 记住这三句话 + +1. **少就是多** - 导入少、代码少、功能聚焦 +2. **重复是坏味道** - 看到相似代码就问能否合并 +3. **工具是帮手** - 让 ruff、radon 帮你发现问题 + +### 不需要懂的东西 + +- ❌ 英语(AI 会翻译) +- ❌ 数学(除非做算法) +- ❌ 数据结构(AI 会处理) +- ❌ 设计模式(记住"延迟抽象"即可) + +--- + +## 七、快速决策流程 + +``` +AI 生成代码 + ↓ +看导入 → 太多?→ 问 AI "都用到了吗?" + ↓ +看重复 → 相似代码?→ 问 AI "能合并吗?" + ↓ +看长度 → 超过50行?→ 问 AI "能拆分吗?" + ↓ +运行 ruff → 有问题?→ 让 AI 解释并修复 + ↓ +完成审查 +``` + +--- + +## 八、参考文档 + +- [核心原则](核心原则.md) - 约束 AI 生成代码的规范 +- [代码审查报告](代码审查报告-26.03.md) - 当前项目已知问题 +- [开发规范](开发规范.md) - 详细的代码编写规范 + +--- + +> **核心思想**:不需要懂代码细节,只需要关注"整洁度"。让工具发现问题,让 AI 解释和修复问题。 diff --git "a/\346\226\207\346\241\243/\344\273\243\347\240\201\346\243\200\346\237\245.txt" "b/\346\226\207\346\241\243/\344\273\243\347\240\201\346\243\200\346\237\245.txt" new file mode 100644 index 0000000000000000000000000000000000000000..a5a90d922fe77bf928521f79d970e2afe5184788 --- /dev/null +++ "b/\346\226\207\346\241\243/\344\273\243\347\240\201\346\243\200\346\237\245.txt" @@ -0,0 +1,12 @@ +# 定期代码检查 + +# 安装依赖 +pip install ruff mypy radon bandit vulture + +# 日常检查 +ruff check . --fix +mypy . +radon cc . -a -s + +# 发布前检查 +vulture core ui dialogs utils main.py --min-confidence 60 diff --git "a/\346\226\207\346\241\243/\345\274\200\345\217\221\347\273\217\351\252\214\346\200\273\347\273\223.md" "b/\346\226\207\346\241\243/\345\274\200\345\217\221\347\273\217\351\252\214\346\200\273\347\273\223.md" index 796a9a42c23cd952cd312658ce2edd26784c6724..d1d98cb892b5ac73b40ed3694e497f0c264bc3b8 100644 --- "a/\346\226\207\346\241\243/\345\274\200\345\217\221\347\273\217\351\252\214\346\200\273\347\273\223.md" +++ "b/\346\226\207\346\241\243/\345\274\200\345\217\221\347\273\217\351\252\214\346\200\273\347\273\223.md" @@ -697,3 +697,714 @@ def test_queued_connection_safety(qtbot): | terminate() 崩溃 | 使用 cancel() + wait() 优雅退出 | | 线程销毁警告 | 服务类添加 __del__ 析构函数 | | 信号未断开崩溃 | closeEvent 中断开全局信号 | + +--- + +## 10. PySideSix-Frameless-Window 无边框窗口改造经验 + +### 10.1 改造背景与目标 + +将传统 `QDialog` 改造为 `FramelessDialog`,实现 Fluent Design 风格的自定义标题栏,获得更现代、统一的界面效果。 + +### 10.1.1 推荐使用 BaseFramelessDialog 基类 + +**项目已提供统一的 `BaseFramelessDialog` 基类**,封装了完整的标题栏和主题适配功能。新对话框应直接继承此基类,无需重复实现。 + +**文件位置:** `dialogs/base_frameless_dialog.py` + +**使用方法:** + +```python +from dialogs import BaseFramelessDialog +from qfluentwidgets import qconfig +from PySide6.QtWidgets import QVBoxLayout + +class MyDialog(BaseFramelessDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("我的对话框") + self.setFixedSize(400, 300) + + # 设置界面 + self.setup_ui() + + # 设置标题栏和样式(基类提供) + self._setup_title_bar() + self._update_styles() + + # 监听主题变化 + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) + + def closeEvent(self, event): + """关闭事件:断开信号连接""" + if hasattr(self, '_theme_connection'): + try: + qconfig.themeChangedFinished.disconnect(self._theme_connection) + except (TypeError, RuntimeError): + pass + delattr(self, '_theme_connection') + super().closeEvent(event) + + def setup_ui(self): + """设置界面""" + layout = QVBoxLayout(self) + # 顶部边距40px为标题栏留出空间 + layout.setContentsMargins(20, 40, 20, 20) + # ... 其他控件 +``` + +**关键要点:** +- 继承 `BaseFramelessDialog` 而非直接使用 `FramelessDialog` +- 先调用 `setup_ui()` 设置界面,再调用 `_setup_title_bar()` 和 `_update_styles()` +- 布局顶部边距设置为 40px,为标题栏留出空间 +- 父窗口使用 `self.window()` 传递顶层窗口 +- 在 `closeEvent` 中断开主题变化信号连接 + +### 10.2 核心改造步骤(旧对话框迁移参考) + +#### 10.2.1 依赖安装 + +```bash +pip install PySideSix-Frameless-Window +``` + +在 `requirements.txt` 中添加: +``` +PySideSix-Frameless-Window>=0.1.0 +``` + +#### 10.2.2 导入变更 + +```python +# 原有导入 +from PySide6.QtWidgets import QDialog + +# 新增导入 +from qframelesswindow import FramelessDialog +``` + +#### 10.2.3 类继承变更 + +```python +# 从 +class EditPaletteDialog(QDialog): + +# 改为 +class EditPaletteDialog(FramelessDialog): +``` + +#### 10.2.4 初始化方法调整 + +**移除的代码:** +- `setWindowFlags()` 调用(FramelessDialog 已处理) +- `set_window_title_bar_theme()` 相关代码(不再使用 Windows DWM API) +- `showEvent` 中的标题栏主题更新逻辑 + +**新增的代码:** +```python +def __init__(self, ...): + super().__init__(parent) + + # 设置窗口标题 + self.setWindowTitle("窗口标题") + + # 设置自定义标题栏 + self._setup_title_bar() + + # 初始化样式(包含窗口背景色和标题颜色) + self._update_styles() + + # 监听主题变化 + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) +``` + +### 10.3 自定义标题栏实现 + +#### 10.3.1 标题栏布局结构 + +``` +[左边距(10px)] [Logo] [间距(8px)] [标题] [Stretch] [最小化] [最大化] [关闭] +``` + +#### 10.3.2 核心实现代码 + +```python +def _setup_title_bar(self): + """设置自定义 Fluent Design 风格标题栏""" + from PySide6.QtWidgets import QLabel + from PySide6.QtCore import Qt + from PySide6.QtGui import QPixmap + from utils.icon import get_icon_path + from utils.theme_colors import get_text_color + + # 获取 FramelessDialog 内置的标题栏 + title_bar = self.titleBar + h_layout = title_bar.layout() + if not h_layout: + return + + # 获取主题颜色 + text_color = get_text_color() + text_color_str = text_color.name() + + # 创建标题栏控件 + # 1. 左边距 + left_spacer = QLabel(title_bar) + left_spacer.setFixedWidth(10) + + # 2. Logo + icon_path = get_icon_path() + logo_label = None + if icon_path: + logo_label = QLabel(title_bar) + pixmap = QPixmap(icon_path) + if not pixmap.isNull(): + logo_label.setPixmap(pixmap.scaled( + 20, 20, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation + )) + + # 3. Logo和标题之间的间距 + spacer_label = QLabel(title_bar) + spacer_label.setFixedWidth(8) + + # 4. 标题 + title_label = QLabel(self.windowTitle(), title_bar) + title_label.setStyleSheet( + f"color: {text_color_str}; font-size: 13px; font-weight: 500;" + ) + + # 按顺序插入到布局开头(倒序插入) + for widget in [title_label, spacer_label, logo_label, left_spacer]: + if widget: + h_layout.insertWidget(0, widget) + h_layout.setAlignment(widget, Qt.AlignmentFlag.AlignVCenter) + + # 保存标题标签引用,以便主题切换时更新 + self._title_label = title_label +``` + +### 10.4 主题适配关键经验 + +#### 10.4.1 背景色设置方式 + +**❌ 错误方式(样式表不生效):** +```python +self.setStyleSheet(f"FramelessDialog {{ background-color: {bg_color_str}; }}") +``` + +**✅ 正确方式(使用 QPalette):** +```python +from PySide6.QtGui import QPalette, QColor + +palette = self.palette() +palette.setColor(QPalette.ColorRole.Window, QColor(bg_color_str)) +self.setPalette(palette) +self.setAutoFillBackground(True) +``` + +#### 10.4.2 关闭按钮颜色更新 + +```python +def _update_close_button_color(self, text_color): + """更新关闭按钮颜色以适配主题""" + from utils.theme_colors import ( + get_close_button_hover_bg_color, + get_close_button_hover_color, + get_close_button_pressed_color + ) + + title_bar = self.titleBar + if hasattr(title_bar, 'closeBtn') and title_bar.closeBtn: + close_btn = title_bar.closeBtn + # 设置正常状态颜色 + close_btn.setNormalColor(text_color) + # 设置悬停状态颜色 + close_btn.setHoverColor(get_close_button_hover_color()) + close_btn.setHoverBackgroundColor(get_close_button_hover_bg_color()) + # 设置按下状态颜色 + close_btn.setPressedColor(get_close_button_pressed_color()) +``` + +#### 10.4.3 完整样式更新方法 + +```python +def _update_styles(self): + """更新样式以适配主题""" + text_color = get_text_color() + text_color_str = text_color.name() + bg_color = get_dialog_bg_color() + + # 使用 QPalette 设置窗口背景色 + palette = self.palette() + palette.setColor(QPalette.ColorRole.Window, bg_color) + self.setPalette(palette) + self.setAutoFillBackground(True) + + # 设置样式表 - QLabel 文字颜色 + self.setStyleSheet(f""" + QLabel {{ + color: {text_color_str}; + background-color: transparent; + }} + """) + + # 更新标题标签颜色 + if hasattr(self, '_title_label') and self._title_label: + self._title_label.setStyleSheet( + f"color: {text_color_str}; font-size: 13px; font-weight: 500;" + ) + + # 更新关闭按钮颜色 + self._update_close_button_color(text_color) +``` + +### 10.5 布局调整经验 + +#### 10.5.1 为标题栏留出空间 + +```python +def setup_ui(self): + """设置界面布局""" + layout = QVBoxLayout(self) + # 顶部边距设置为40,为无边框窗口的标题栏留出空间 + layout.setContentsMargins(20, 40, 20, 20) + layout.setSpacing(15) +``` + +#### 10.5.2 标题栏控件插入技巧 + +**关键原则:倒序插入,保证正序显示** + +```python +# 期望的最终顺序:左边距 → Logo → 间距 → 标题 +# 倒序插入:标题 → 间距 → Logo → 左边距 +for widget in [title_label, spacer_label, logo_label, left_spacer]: + if widget: + h_layout.insertWidget(0, widget) +``` + +### 10.6 常见问题与解决方案 + +| 问题 | 原因 | 解决方案 | +|------|------|----------| +| 标题栏和内容重叠 | 未为标题栏留出空间 | 设置 `layout.setContentsMargins(20, 40, 20, 20)` | +| 背景色不生效 | FramelessDialog 不使用样式表设置背景 | 使用 `QPalette` + `setAutoFillBackground(True)` | +| 标题颜色不随主题切换 | 样式表未应用到标题栏控件 | 直接为标题标签设置样式表,并在 `_update_styles` 中更新 | +| 关闭按钮颜色不随主题切换 | 未设置关闭按钮的颜色属性 | 使用 `closeBtn.setNormalColor()` 等方法设置 | +| Logo/标题/按钮挤在一起 | 清除了原有布局的所有项 | 保留原有布局,只在开头插入新控件 | +| 标题紧贴左边框 | 未设置左边距 | 添加固定宽度的占位符 QLabel | +| 从子控件打开对话框时背景色异常 | 父窗口不是顶层窗口 | 使用 `parent=self.window()` 而不是 `parent=self` | +| 出现两套窗口控制器(最小化/最大化/关闭按钮) | 父控件不是窗口,导致 Windows 显示系统按钮 | 确保 `parent` 参数传入顶层窗口(`self.window()`) | +| 深色模式下背景仍为白色/灰色 | `QPalette` 设置时机不对或父窗口问题 | 1. 确保使用 `QPalette` + `setAutoFillBackground(True)` 设置背景
2. 确保父窗口是顶层窗口
3. 在 `_update_styles` 中设置,而非 `__init__` 中 | + +### 10.7 代码清理与避免冗余 + +#### 10.7.1 不要重复实现基类已有的功能 + +**❌ 错误方式(子类重复实现样式更新):** +```python +class MyDialog(BaseFramelessDialog): + def _update_styles(self): + """更新样式以适配主题""" + # 调用父类方法 + super()._update_styles() + + # 冗余:基类已通过 setStyleSheet 统一设置 QLabel 颜色 + text_color = get_text_color() + text_color_hex = text_color.name() + for widget in self.findChildren(QLabel): + widget.setStyleSheet(f"color: {text_color_hex};") +``` + +**✅ 正确方式(完全依赖基类):** +```python +class MyDialog(BaseFramelessDialog): + # 不需要重写 _update_styles,完全使用基类实现 + pass +``` + +**关键原则:** +- `BaseFramelessDialog._update_styles()` 已通过 `self.setStyleSheet()` 为所有 QLabel 统一设置颜色 +- 子类无需重复设置 QLabel 颜色,除非需要特殊的样式覆盖 +- 保持代码简洁,遵循 DRY 原则 + +#### 10.7.2 及时清理未使用的导入 + +改造完成后应检查并删除未使用的导入: + +```python +# 改造前可能需要 +from PySide6.QtGui import QPixmap +from qfluentwidgets import isDarkTheme +from utils.theme_colors import get_text_color + +# 改造后若未使用,应删除上述导入 +``` + +**常见可删除的导入(使用 BaseFramelessDialog 后):** +- `QPixmap` - 若对话框不涉及图片显示 +- `isDarkTheme` - 基类已处理主题检测 +- `get_text_color` / `get_dialog_bg_color` - 基类已处理颜色获取 +- `set_window_title_bar_theme` - 无边框窗口不再需要 + +### 10.8 主题颜色函数(theme_colors.py) + +在 `utils/theme_colors.py` 中添加关闭按钮颜色函数: + +```python +def get_close_button_hover_bg_color(): + """获取关闭按钮悬停背景颜色""" + return QColor(196, 43, 28) if isDarkTheme() else QColor(232, 17, 35) + + +def get_close_button_hover_color(): + """获取关闭按钮悬停图标颜色""" + return QColor(255, 255, 255) + + +def get_close_button_pressed_color(): + """获取关闭按钮按下图标颜色""" + return QColor(255, 255, 255) +``` + +### 10.9 改造检查清单 + +#### 新对话框(推荐) +- [ ] 继承 `BaseFramelessDialog` 基类 +- [ ] 调用 `_setup_title_bar()` 设置标题栏 +- [ ] 调用 `_update_styles()` 初始化样式 +- [ ] 调整布局边距为标题栏留出空间(顶部40px) +- [ ] 监听主题变化信号 +- [ ] 在 `closeEvent` 中断开信号连接 +- [ ] **关键**:确保对话框父窗口是顶层窗口(`parent=self.window()`) +- [ ] **清理**:删除未使用的导入(`QPixmap`、`isDarkTheme`、`get_text_color` 等) +- [ ] **清理**:避免重写 `_update_styles`,除非需要特殊样式覆盖 + +#### 旧对话框迁移(参考) +- [ ] 安装 PySideSix-Frameless-Window 依赖 +- [ ] 修改类继承为 `FramelessDialog`(或迁移到 `BaseFramelessDialog`) +- [ ] 移除 `setWindowFlags` 调用 +- [ ] 移除 `set_window_title_bar_theme` 相关代码 +- [ ] 移除 `_fix_taskbar_icon` 调用(FramelessDialog 自动处理) +- [ ] 实现 `_setup_title_bar()` 方法 +- [ ] 实现 `_update_styles()` 方法 +- [ ] 实现 `_update_close_button_color()` 方法 +- [ ] 在 `theme_colors.py` 中添加关闭按钮颜色函数 +- [ ] 调整布局边距为标题栏留出空间 +- [ ] 监听主题变化信号 +- [ ] 在 `closeEvent` 中断开信号连接 +- [ ] **关键**:确保对话框父窗口是顶层窗口(`parent=self.window()`) +- [ ] **关键**:测试从子控件打开对话框时背景色是否正常 +- [ ] **关键**:测试深色/浅色主题切换时关闭按钮颜色是否正常 +- [ ] 测试窗口拖动和调整大小功能 +- [ ] **清理**:删除未使用的导入(`QPixmap`、`isDarkTheme`、`set_window_title_bar_theme` 等) +- [ ] **清理**:检查并删除冗余的 `_update_styles` 重写 + +### 10.10 后续对话框改造模板 + +#### 推荐模板(使用 BaseFramelessDialog 基类) + +```python +from dialogs import BaseFramelessDialog +from qfluentwidgets import qconfig +from PySide6.QtWidgets import QVBoxLayout + +class CustomDialog(BaseFramelessDialog): + """自定义对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("对话框标题") + self.setFixedSize(400, 300) + + # 设置界面 + self.setup_ui() + + # 设置标题栏和样式(基类提供) + self._setup_title_bar() + self._update_styles() + + # 监听主题变化 + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) + + def setup_ui(self): + """设置界面""" + layout = QVBoxLayout(self) + # 顶部边距40px为标题栏留出空间 + layout.setContentsMargins(20, 40, 20, 20) + # ... 其他控件 + + def closeEvent(self, event): + """关闭事件:断开信号连接""" + if hasattr(self, '_theme_connection'): + try: + qconfig.themeChangedFinished.disconnect(self._theme_connection) + except (TypeError, RuntimeError): + pass + delattr(self, '_theme_connection') + super().closeEvent(event) + + +# ==================== 使用示例 ==================== + +# ✅ 正确:从子控件打开对话框时,使用 self.window() 作为父窗口 +dialog = CustomDialog(parent=self.window()) +dialog.exec() + +# ❌ 错误:直接使用 self 作为父窗口(self 是子控件而非窗口) +# dialog = CustomDialog(parent=self) # 会导致背景色异常和两套窗口控制器 +``` + +#### 旧模板(直接使用 FramelessDialog,供参考) + +如需自定义标题栏样式或实现特殊需求,可直接继承 `FramelessDialog`,参考 10.3-10.4 节实现相关方法。 + +--- + +## 11. 静态分析工具使用经验 + +### 11.1 Ruff 使用经验 + +**E402 导入位置问题处理原则:** + +- **故意设计的延迟导入**(如启动优化)→ 维持现状 +- **代码组织混乱**导致的导入位置问题 → 修复 + +**示例:** + +```python +# ❌ 代码组织混乱 +logger = get_logger("settings") +from version import version_manager + +# ✓ 正确的导入顺序 +from version import version_manager +logger = get_logger("settings") +``` + +**处理决策表:** + +| 场景 | 示例 | 处理方式 | +|------|------|----------| +| 启动优化延迟导入 | `main.py` 中延迟导入启动画面 | 维持现状 | +| 服务延迟加载 | `ServiceFactory` 中延迟导入服务类 | 维持现状 | +| 代码组织混乱 | `settings.py` 中导入在 logger 之后 | 修复 | + +### 11.2 Bandit 安全扫描使用经验 + +**对于纯本地桌面应用:** + +| 问题类型 | 典型场景 | 评估 | 原因 | +|----------|----------|------|------| +| MD5 哈希使用 | 缓存键生成 | ✅ 无需处理 | 非安全场景,仅影响缓存命中率 | +| XML 解析安全 | SVG 文件解析 | 🟢 低优先级 | 本地应用,用户主动行为 | +| random 模块使用 | UI 颜色生成 | ✅ 无需处理 | 非加密用途 | +| try-except-pass | 降级策略 | ✅ 无需处理 | 合理用法 | + +**结论:** 纯本地桌面应用的 Bandit 告警大部分是**误报**,仅供参考,不应作为代码质量门槛。 + +### 11.3 Vulture 死代码检测使用经验 + +**60% 置信度问题的局限性:** + +| 问题类型 | Vulture 报告 | 实际情况 | 评估 | +|----------|-------------|----------|------| +| 未使用属性 | `_nav_icon` | 动态设置使用 | 误报 | +| 未使用方法 | `get_elements` | 公共 API | 误报 | +| 未使用事件 | `mouseMoveEvent` | Qt 框架调用 | 误报 | +| 未使用函数 | `cancel_loading` | 预留功能 | 保留 | + +**建议:** +- 60% 置信度的问题需要**人工确认**,不建议批量删除 +- 100% 置信度的问题(如未使用变量)可以安全修复 +- 公共 API 方法即使暂时未使用也应保留 + +--- + +## 12. 代码审查经验 + +### 12.1 重复代码处理原则 + +**不应该抽象的重复:** + +- 标准库/框架的惯用写法(如 `try/except` 信号断开) +- 只有 6 处以下且场景简单 +- 抽象后反而降低可读性 + +**应该抽象的重复:** + +- 业务逻辑重复(如 RGB/RYB 配色生成) +- 重复代码超过 10 行 +- 抽象后能显著提升可维护性 + +**决策案例:** + +```python +# ❌ 过度设计:创建 safe_disconnect 工具函数 +# 22处重复,但每处只有4行,且是标准写法 + +def safe_disconnect(signal, slot): + try: + signal.disconnect(slot) + except (TypeError, RuntimeError): + pass + +# ✅ 维持现状:标准 try/except 写法 +try: + signal.disconnect(slot) +except (TypeError, RuntimeError): + pass +``` + +### 12.2 过度设计识别 + +**信号处理:** + +- ❌ 创建 `safe_disconnect` 工具函数(过度设计) +- ✅ 维持 `try/except` 标准写法(符合惯例) + +**配置管理:** + +- ❌ 统一 `COLOR_MODE_CONFIG` 和 `MODE_PARAMS`(用途不同) +- ✅ 维持各自模块独立(内聚性原则) + +**延迟抽象原则:** + +- 只有一种实现 → 不需要工厂/策略模式 +- 标准用法重复 → 不需要提取工具函数 +- 60% 置信度的工具告警 → 需要人工确认 + +### 12.3 评估原则 + +**代码审查检查清单:** + +- [ ] 是否过度抽象?(延迟抽象原则) +- [ ] 是否影响可读性?(标准写法 vs 自定义函数) +- [ ] 工具告警是否合理?(考虑实际场景) +- [ ] 修改收益是否大于成本?(行数节省 vs 复杂度增加) + +**常见误报识别:** + +| 工具 | 误报类型 | 识别方法 | +|------|----------|----------| +| Ruff E402 | 故意延迟导入 | 检查是否是启动优化或服务延迟加载 | +| Bandit B303 | MD5 用于缓存 | 确认非安全场景 | +| Vulture 60% | 动态属性/事件方法 | 检查实际使用情况 | +| Ruff F841 | 未使用变量 | 100% 置信度可安全修复 | + +--- + +## 13. 延迟加载设计模式总结 + +### 13.1 多层延迟加载架构 + +Color Card 项目实现了完整的延迟加载体系: + +**1. 服务层**(`ServiceFactory`) + +```python +@classmethod +def get_color_service(cls): + """延迟创建服务,双检锁保证线程安全""" + if 'color' not in cls._instances: + with cls._get_lock(): + if 'color' not in cls._instances: + from .color_service import ColorService + cls._instances['color'] = ColorService(None) + return cls._instances['color'] +``` + +**2. UI 层**(`_get_xxx_service`) + +```python +def _get_color_service(self): + """方法级延迟获取""" + if self._color_service is None: + from core import get_color_service + self._color_service = get_color_service() + return self._color_service +``` + +**3. 数据层**(`_ensure_loaded`) + +```python +def _ensure_loaded(self) -> None: + """懒加载标志 + 确保加载""" + if not self._loaded: + self._load_data() + self._loaded = True +``` + +**4. 界面层**(`on_tab_selected`) + +```python +def on_tab_selected(self): + """标签页选中时延迟加载""" + if not self._data_loaded: + self._load_data() +``` + +**5. 计算任务**(`QTimer.singleShot`) + +```python +# 延迟执行耗时操作,让 UI 先响应 +QTimer.singleShot(100, self.extract_all) +``` + +### 13.2 延迟加载与代码规范的关系 + +| 场景 | 是否延迟加载 | 处理方式 | +|------|-------------|----------| +| `main.py` 启动画面导入 | ✅ 是 | 维持现状,符合规范 | +| `ServiceFactory` 服务导入 | ✅ 是 | 维持现状,符合规范 | +| `settings.py` 导入位置混乱 | ❌ 否 | 修复,代码组织问题 | +| `image_service.py` 末尾导入 | ❌ 否 | 修复,代码组织问题 | + +**关键区分:** + +- **延迟加载**:有明确的性能优化目的,导入在函数/方法内部 +- **代码组织问题**:无实际目的,只是导入位置混乱 + +### 13.3 延迟加载最佳实践 + +**导入位置规范:** + +```python +# 标准库导入 +import sys +from pathlib import Path + +# 第三方库导入 +from PySide6.QtCore import QObject +from PIL import Image + +# 项目模块导入(非延迟加载) +from core import get_config_manager +from utils import tr + +# 延迟加载(在函数内部) +def _get_service(self): + if self._service is None: + from core import get_service # 延迟导入 + self._service = get_service() + return self._service +``` + +**性能优化效果:** + +从 Changelog 可以看到延迟加载的实际收益: + +> "实现业务服务延迟加载,减少应用启动时间" +> "优化直方图计算服务,缩短延迟计算时间,大图片加载性能显著提升" + +--- + +**文档更新日期**: 2026-03-22 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 107b29e7ccdcdc437f93eadb6d022d1ccd41e0de..e55fa1a91dab166fa138bc956d52b409a7a6b07d 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" @@ -79,6 +79,7 @@ color_card/ ├── dialogs/ # 对话框模块目录 │ ├── __init__.py │ ├── about_dialog.py # 关于对话框 +│ ├── base_frameless_dialog.py # 无边框对话框基类(BaseFramelessDialog,提供统一标题栏和主题适配) │ ├── colorblind_dialog.py # 色盲模拟预览对话框 │ ├── contrast_dialog.py # 对比度检查对话框 │ ├── edit_palette.py # 配色编辑对话框(EditPaletteDialog、ColorPickerDialog、PresetGrid、ColorModeSliders、GradientSlider、ColorPreview) @@ -234,6 +235,26 @@ from ui import MainWindow | `PreviewService` | 预览场景管理服务 | `core/preview_service.py` | | `HistogramService` | 直方图计算服务 | `core/histogram_service.py` | +### 3.3.2 颜色空间命名规范 + +**颜色空间分量使用行业标准大写简写命名:** + +| 颜色空间 | 命名 | +|---------|------| +| RGB | `R`, `G`, `B` | +| HSB/HSL | `H`, `S`, `B`/`L` | +| LAB | `L`, `A`, `B` | +| LMS | `L`, `M`, `S` | +| CMYK | `C`, `M`, `Y`, `K` | + +```python +# 示例 +L, A, B = rgb_to_lab(R, G, B) +H, S, L = rgb_to_hsl(R, G, B) +``` + +**注意:** 禁止使用小写 `l`(易与数字 1 混淆,违反 E741 规范) + ### 3.4 异常处理规范 **基本原则:** 避免使用裸 `except:` 或 `except Exception:`,应指定具体异常类型,提供详细的错误信息,便于调试。 @@ -367,51 +388,6 @@ class MyWidget(QWidget): **避免使用** `!important`:优先使用组件特定的选择器;如必须使用,确保在主题切换后重新应用。 -#### 3.8.5 Windows 原生标题栏深色模式 - -对于继承自 `QDialog` 的对话框,使用 Windows DWM API 设置原生标题栏的沉浸式深色模式。 - -**工具函数**(放在 `utils/platform.py`): - -```python -import ctypes -import sys - -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: - hwnd = int(window.windowHandle().winId()) - value = ctypes.c_int(1 if is_dark else 0) - return ctypes.windll.dwmapi.DwmSetWindowAttribute( - hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, - ctypes.byref(value), ctypes.sizeof(value) - ) == 0 - except Exception: - return False -``` - -**在对话框中应用**(使用 `showEvent` 避免闪烁): - -```python -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 及以上。 - *** ## 4. 基类设计规范 @@ -492,6 +468,97 @@ class MainWindow(FluentWindow): self.addSubInterface(self.interface, FluentIcon.PALETTE, "界面名称") ``` +### 5.1.1 无边框对话框基类使用规范 + +**使用 `BaseFramelessDialog` 作为无边框对话框基类:** + +项目提供统一的 `BaseFramelessDialog` 基类,封装了 Fluent Design 风格的自定义标题栏和主题适配功能。 + +**文件位置:** `dialogs/base_frameless_dialog.py` + +**使用步骤:** + +1. **继承基类** +```python +from dialogs import BaseFramelessDialog + +class MyDialog(BaseFramelessDialog): + """我的对话框""" + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("对话框标题") + self.setFixedSize(400, 300) +``` + +2. **设置标题栏和样式** +```python +# 设置自定义标题栏(显示Logo和标题) +self._setup_title_bar() + +# 初始化样式(背景色、文字颜色等) +self._update_styles() +``` + +3. **监听主题变化** +```python +from qfluentwidgets import qconfig + +# 在 __init__ 中连接信号 +self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles +) +``` + +4. **关闭事件处理(基类已自动处理信号断开)** +```python +def closeEvent(self, event): + """关闭事件""" + super().closeEvent(event) # 基类自动断开信号连接 +``` + +**完整示例:** +```python +from dialogs import BaseFramelessDialog +from qfluentwidgets import qconfig + +class CustomDialog(BaseFramelessDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("自定义对话框") + self.setFixedSize(400, 300) + + # 设置标题栏和样式 + self._setup_title_bar() + self._update_styles() + + # 设置界面 + self.setup_ui() + + # 监听主题变化 + self._theme_connection = qconfig.themeChangedFinished.connect( + self._update_styles + ) + + def closeEvent(self, event): + """关闭事件""" + super().closeEvent(event) # 基类自动断开信号连接 + + def setup_ui(self): + """设置界面布局""" + from PySide6.QtWidgets import QVBoxLayout + layout = QVBoxLayout(self) + # 顶部边距40px为标题栏留出空间 + layout.setContentsMargins(20, 40, 20, 20) + # ... 其他控件 +``` + +**关键要点:** +- 必须调用 `_setup_title_bar()` 设置自定义标题栏 +- 必须调用 `_update_styles()` 初始化样式 +- 布局边距顶部设置为 40px,为标题栏留出空间 +- 父窗口必须是顶层窗口:`parent=self.window()` +- `closeEvent` 中调用 `super().closeEvent(event)`,基类会自动断开主题变化信号 + ### 5.2 界面组织规范 - 每个功能模块创建独立的 `QWidget` 子类 @@ -998,15 +1065,15 @@ SVG模板使用语义化 `class` 命名规范,支持智能配色映射。 #### 9.7.1 元素分类依据 -| 优先级 | 依据 | 示例 | -| :-: | ----------- | ------------------------- | -| 1 | class 属性关键词 | `class="background"` → 背景 | -| 2 | id 属性关键词 | `id="main-title"` → 文字 | -| 3 | 标签类型 + 面积 | 最大矩形 → 背景 | +语义化映射仅通过 `class` 属性识别元素类型: + +| 依据 | 示例 | +| ---- | ---- | +| class 属性关键词 | `class="background"` → 背景 | #### 9.7.2 支持的关键词 -| 元素类型 | class/id 关键词 | 颜色映射 | +| 元素类型 | class 关键词 | 颜色映射 | | ---- | -------------------------- | ------ | | 背景 | `background`, `bg`, `back` | 配色\[0] | | 主元素 | `primary`, `main` | 配色\[1] | @@ -1030,7 +1097,7 @@ else: ##### 语义化映射模式 -**适用条件**:SVG有语义化类型信息(通过class/id关键词识别) +**适用条件**:SVG有语义化类型信息(通过class关键词识别) **映射规则**: @@ -1959,6 +2026,9 @@ def _get_about_text(self): | 版本 | 日期 | 变更内容 | | :--: | :--------: | :------------------------------------------------------------------------: | +| 3.44 | 2026-03-21 | 更新SVG颜色映射规范(9.7.1节、9.7.2节、9.7.3节):简化语义化映射为仅通过class属性识别,删除id属性和标签类型回退的兼容处理 | +| 3.43 | 2026-03-16 | 更新无边框对话框基类规范(5.1.1节):`closeEvent` 信号断开逻辑移至基类;删除 Windows DWM API 相关内容(3.8.5节) | +| 3.42 | 2026-03-16 | 新增无边框对话框基类规范(5.1.1节):提取 BaseFramelessDialog 到独立文件,统一标题栏和主题适配;更新项目结构(1.3节) | | 3.41 | 2026-03-13 | 精简开发规范中的代码示例,删除冗余注释和重复内容,使文档更加简洁易读 | | 3.40 | 2026-03-07 | 更新项目结构(1.3节):新增 export\_settings\_dialog.py 到 dialogs/ 目录 | | 3.39 | 2026-03-04 | 新增资源路径规范(2.4节):说明 PyInstaller 打包后的资源路径获取方式,避免打包后资源加载失败 | diff --git "a/\346\226\207\346\241\243/\346\240\270\345\277\203\345\216\237\345\210\231.md" "b/\346\226\207\346\241\243/\346\240\270\345\277\203\345\216\237\345\210\231.md" index 3696230319794de57fb003c606f52decbaf61ab2..27fe3ec9cd51e2a07259e1656f9865f96597deda 100644 --- "a/\346\226\207\346\241\243/\346\240\270\345\277\203\345\216\237\345\210\231.md" +++ "b/\346\226\207\346\241\243/\346\240\270\345\277\203\345\216\237\345\210\231.md" @@ -1,14 +1,12 @@ # Color Card 核心原则 -> **详细规范请参阅**:`文档/开发规范.md`(包含完整的代码编写规范、布局规范、配色规范等) +> **详细规范请参阅**:`文档/开发规范.md` ## 1. 技术栈 -| 项目 | 说明 | -|:---:|:---| -| Python | 3.11+ | -| GUI 框架 | PySide6 + PySide6-Fluent-Widgets | -| 注释语言 | 中文 | +- Python 3.11+ +- GUI: PySide6 + PySide6-Fluent-Widgets +- 注释语言: 中文 --- @@ -16,277 +14,174 @@ ``` color_card/ -├── main.py # 程序入口 -├── core/ # 核心功能模块 -│ ├── color.py # 颜色处理 -│ ├── config.py # 配置管理 -│ └── ... -├── ui/ # UI 模块(扁平化) -│ ├── main_window.py # 主窗口 -│ ├── canvases.py # 画布组件 -│ ├── cards.py # 卡片组件 -│ ├── theme_colors.py # 主题颜色管理 -│ └── ... -├── dialogs/ # 对话框模块 -├── utils/ # 工具函数 -│ ├── locale.py # 多语言国际化 -│ └── ... -├── locales/ # 语言包目录 -│ -└── color_data/ # 颜色数据(JSON) +├── main.py # 程序入口 +├── core/ # 核心功能(业务层) +├── ui/ # UI模块(展示层) +├── dialogs/ # 对话框 +├── utils/ # 工具函数 +├── locales/ # 语言包 +└── color_data/ # 颜色数据 ``` -**核心理念:UI层和业务层分层,公共模块统一管理,业务模块按需调用;UI层只负责UI,功能层面全部下到业务层** +**核心理念:UI层和业务层分离,业务逻辑全部下沉到 core 模块** --- ## 3. 命名规范 | 类型 | 规范 | 示例 | -|:---:|:---:|:---| -| 类名 | 驼峰命名法 | `ColorPicker`, `ImageCanvas` | -| 函数/方法 | 小写+下划线 | `extract_color()` | -| 变量 | 小写+下划线 | `picker_positions` | -| 常量 | 大写+下划线 | `PICKER_RADIUS = 12` | +|:---:|:---|:---| +| 类名 | 驼峰 | `ColorPicker` | +| 函数/变量 | 小写+下划线 | `extract_color()` | +| 常量 | 大写+下划线 | `MAX_SIZE = 1000` | | 私有属性 | 单下划线前缀 | `_dragging` | -| 信号 | 小写+下划线 | `color_picked = Signal(int, tuple)` | + +**禁止**:`l`(易与1混淆)、`O`(易与0混淆) --- -## 4. 导入顺序 +## 4. 导入原则 ```python -# 标准库导入 +# 标准库 → 第三方 → 项目模块 import sys from pathlib import Path -# 第三方库导入 from PySide6.QtWidgets import QWidget -from qfluentwidgets import FluentWindow, setTheme, Theme +from qfluentwidgets import FluentWindow -# 项目模块导入 from core import get_color_info -from ui.theme_colors import get_text_color ``` -**导入清理原则:** -- 按模块类型分组导入,添加清晰的分组注释 -- 定期清理未使用的导入,合并同一模块的多次导入 -- 优先使用绝对导入 +- **只导入实际使用的模块** +- 禁止预导入"可能用到"的模块 +- 按分组添加注释 --- -## 5. 代码风格 - -| 规则 | 说明 | -|:---:|:---| -| 代码风格 | PEP 8 | -| 缩进 | 4 空格 | -| 行长 | ≤ 100 字符 | - ---- - -## 6. 关键约束 - -### 6.1 颜色管理 +## 5. 关键约束 +### 颜色管理 ```python -# 禁止硬编码颜色 -painter.setPen(QColor(255, 255, 255)) # ❌ 错误 +# ❌ 禁止硬编码 +painter.setPen(QColor(255, 255, 255)) -# 必须使用主题颜色模块 +# ✓ 必须使用主题颜色 from ui.theme_colors import get_text_color -painter.setPen(get_text_color()) # ✓ 正确 +painter.setPen(get_text_color()) ``` -### 6.2 信号同步防循环 - +### 信号防循环 ```python -# 双向同步时使用 emit_sync 参数 def set_image_data(self, pixmap, image, emit_sync=True): self._pixmap = pixmap - if emit_sync: # 只在独立操作时发射信号 + if emit_sync: self.image_loaded.emit() ``` -### 6.3 主题切换 - -```python -from qfluentwidgets import qconfig, isDarkTheme - -# 监听主题变化 -qconfig.themeChangedFinished.connect(self._update_styles) - -# 更新样式 -def _update_styles(self): - if isDarkTheme(): - # 深色主题样式 - pass -``` - -### 6.4 异常处理 - +### 异常处理 ```python -# 禁止裸 except -except Exception: # ❌ 错误 - pass - -# 必须指定具体类型 -except (OSError, ValueError) as e: # ✓ 正确 +# ❌ 禁止裸 except +# ✓ 必须指定具体类型 +except (OSError, ValueError) as e: print(f"错误: {e}") ``` -### 6.5 QSplitter 样式 - +### QSplitter ```python -# 所有 QSplitter 必须隐藏分隔条 -splitter = QSplitter(Qt.Orientation.Vertical) -splitter.setHandleWidth(0) # 必须设置 +splitter.setHandleWidth(0) # 必须隐藏分隔条 ``` -### 6.6 代码清理同步 - -修改代码时同步清理相关重复代码,避免遗留冗余逻辑。 - -- 删除未使用的变量、函数、导入和注释 -- 合并重复的逻辑和相似的功能 -- 检查并清除相关的重复冗余代码 -- 保持代码整洁,提高代码复用性 +### 代码清理 +- 删除未使用的变量、导入 +- 禁止复制粘贴后仅做微调 +- 重复代码提取公共方法或基类 -### 6.7 基类设计 +### 设计原则 +- **延迟抽象**:只有一种实现时不用工厂/策略模式 +- **适度防御**:边界检查要有实际意义 +- **单一职责**:一个类只负责一类功能 +- **避免不必要的防御性策略**:对于项目必需依赖(如 requirements.txt 中定义的包),无需编写防御性导入和回退代码 -- 单一职责:一个基类只负责一类功能 -- 接口清晰:抽象方法定义明确 -- 可扩展:便于添加新子类 - -### 6.8 多线程使用 +### 多线程使用 耗时操作(复杂计算等场景)应使用多线程,避免阻塞UI主线程。 --- -## 7. 文档规范 - -### 7.1 编写原则 - -禁止使用浮夸、空洞的词汇,保持务实、准确的表述风格。 +## 6. 文档字符串 -如:最佳实践这类词应禁止使用,而应使用更具体的描述。 - -### 7.2 文档字符串(重要) - -**所有公共类和方法必须添加文档字符串,使用中文。** +使用中文,避免过度详细: ```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颜色信息的字典 + dict: 颜色信息 """ pass ``` --- -## 8. 常用模块导入 +## 7. 常用导入 ```python # 主题颜色 -from ui.theme_colors import ( - get_text_color, - get_card_background_color, - get_border_color -) +from ui.theme_colors import get_text_color, get_card_background_color # 配置管理 -from core import get_config_manager, get_scene_config_manager +from core import get_config_manager -# 多语言国际化 -from utils import tr, set_language, get_locale_manager +# 国际化 +from utils import tr, set_language -# Fluent Widgets -from qfluentwidgets import ( - FluentWindow, setTheme, Theme, FluentIcon, - RoundMenu, Action, MessageBox, qconfig, isDarkTheme -) +# Fluent +from qfluentwidgets import FluentWindow, qconfig, isDarkTheme ``` --- -## 9. 多语言国际化 - -### 9.1 基本用法 +## 8. 主题切换 ```python -from utils import tr, set_language, get_locale_manager - -# 获取翻译文本 -text = tr('navigation.color_extract') - -# 参数化翻译 -text = tr('messages.copy_success.content', value='#FF5733') +qconfig.themeChangedFinished.connect(self._update_styles) -# 切换语言 -set_language('en_US') +def _update_styles(self): + if isDarkTheme(): + # 深色主题样式 + pass ``` -### 9.2 界面支持语言切换 +--- + +## 9. 多语言 ```python +text = tr('navigation.color_extract') + class MyInterface(QWidget): def __init__(self, parent=None): super().__init__(parent) - get_locale_manager().language_changed.connect(self._on_language_changed) - - def _on_language_changed(self, language_code): - self.update_texts() - - def update_texts(self): - self.title_label.setText(tr('my_interface.title')) + get_locale_manager().language_changed.connect(self.update_texts) ``` --- -## 10. Windows 原生标题栏深色模式 +## 10. 日志 -对于继承自 `QDialog` 的对话框,使用 Windows DWM API 设置原生标题栏主题: +日志存储在 `~/.color_card/logs/`(用户主目录,不是项目目录) ```python -from qfluentwidgets import isDarkTheme, qconfig -from utils import set_window_title_bar_theme +# 日志目录自动创建,已存在不会报错 +self._log_dir.mkdir(parents=True, exist_ok=True) -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()) +# 沙箱环境无法访问日志是环境限制,不是代码问题。不要修改日志系统。 ```