Compare commits

...

85 Commits

Author SHA1 Message Date
p9o3yklam fa47159a5b Merge pull request 'final' (#123) from maziang into main
3 months ago
p9o3yklam 6d86fdade2 Merge pull request '扫雷' (#122) from xinglin-shi into main
3 months ago
Maziang 8e6d50b03b 日历
3 months ago
Maziang b34f40ff57 切换城市功能
3 months ago
Maziang 38f73a359c 贪吃蛇
3 months ago
Maziang 5aa73c54e2 导出功能
3 months ago
Maziang 860e21e95e 刷新天气+名言
3 months ago
Maziang c23347f7f9 天气缓存机制
3 months ago
Maziang c1d1ebdec6 重构
3 months ago
Maziang 1b3af3b7bf 精简
3 months ago
Maziang 1fc3567b44 文字同步
3 months ago
Maziang 3544c9027f 大改
3 months ago
石兴霖 9ecbb00d8d Merge branch 'xinglin-shi' of https://bdgit.educoder.net/p9o3yklam/Curriculum_Design into xinglin-shi
3 months ago
石兴霖 a0bc3230fa 扫雷
3 months ago
p9o3yklam 6a4c97f12a Merge pull request 'UI' (#119) from maziang into xinglin-shi
3 months ago
p9o3yklam 8c63a89a6b Merge pull request 'UI' (#118) from maziang into main
3 months ago
Maziang 5651226e57 段落样式优化
3 months ago
Maziang 5e7f48cc1b 样式表优化
3 months ago
Maziang 880908bbbc 最终版本
3 months ago
Maziang e1f77f7200 顶部UI优化
3 months ago
Maziang 271a2741ff 页面小修
3 months ago
Maziang 60be0fda50 添加AI对话面板组件并更新文档编辑区域布局
3 months ago
Maziang 75dbeb24e9 Enhance calendar widget and theme manager with optimized Apple design styles
3 months ago
p9o3yklam f05a9a6d92 Merge pull request 'update' (#116) from main into maziang
3 months ago
pshjeamgr 11fc04750c Merge pull request 'UI fix114514' (#115) from llllllllllllllCC into main
3 months ago
Lesacm 2f8cbcc003 UI fix114514
3 months ago
Lesacm d6d29cfe8b UI fix114514
3 months ago
p9o3yklam 4f360bc9e7 Merge pull request 'update' (#112) from main into llllllllllllllCC
3 months ago
p9o3yklam 6e31087e35 Merge pull request 'update' (#110) from main into maziang
3 months ago
p9o3yklam f3b54dcd1a Merge pull request 'final- version' (#109) from maziang into main
3 months ago
Maziang adca960a87 添加DeepSeek AI对话窗口及API密钥管理功能
3 months ago
Maziang db65657f15 添加贪吃蛇游戏及其控制功能
3 months ago
Maziang cd39d74ace 天气悬浮窗口优化
3 months ago
Maziang 2415e09748 日历悬浮窗口
3 months ago
Maziang 07ac34ee46 古诗句插入功能
3 months ago
Maziang bfdb9d124c 古诗句悬浮窗口
3 months ago
Maziang efaf1ae33c 天气悬浮窗口fix
3 months ago
Maziang 7a85f36544 天气悬浮窗口
3 months ago
p9o3yklam b5bf199469 Merge pull request 'mei1' (#107) from main into llllllllllllllCC
3 months ago
p9o3yklam cd22077341 Merge pull request 'UI' (#106) from main into maziang
3 months ago
pshjeamgr 0d96d9412c Merge pull request 'UI FIX' (#104) from llllllllllllllCC into main
3 months ago
Lesacm ffa5e645a5 UI fix
3 months ago
p9o3yklam 422a73204e Merge pull request 'final' (#100) from main into maziang
3 months ago
p9o3yklam 4153959110 Merge pull request 'final' (#97) from main into llllllllllllllCC
3 months ago
p9o3yklam 96fa7e9383 Merge pull request 'final' (#96) from maziang into main
3 months ago
Maziang 3ef9200515 黄彪修复
3 months ago
Maziang f89f4b9e35 学习模式图片查看
3 months ago
Maziang df99246ae4 图片查看
3 months ago
Maziang 7642789edc 日历UI修复
3 months ago
Maziang 029ac389f8 修补位置样式错误
3 months ago
p9o3yklam 5be326286a Merge pull request 'UI' (#95) from main into maziang
3 months ago
pshjeamgr 4711f0381d Merge pull request 'UI更改10086' (#94) from llllllllllllllCC into main
3 months ago
Maziang 43c0bd4cb6 小修补
3 months ago
Lesacm e7ae6a7a1e UI更改10086
3 months ago
pshjeamgr b4628958eb Merge pull request '123' (#93) from main into llllllllllllllCC
3 months ago
Maziang 2cdf760dd1 日历黑白模式适配
3 months ago
Maziang 9588630f51 插入天气、每日谏言、日历
3 months ago
p9o3yklam a1203599e8 Merge pull request 'Mei' (#92) from main into maziang
3 months ago
Maziang f06a441c2a 新增日历功能
3 months ago
pshjeamgr ec1313cfa6 Merge pull request '123' (#91) from main into llllllllllllllCC
3 months ago
Maziang 62d1df122c 更新天气组件+UI小修补
3 months ago
Maziang 31c493d2d1 按钮逻辑修正
3 months ago
Maziang fadf1a3867 新增样式功能
3 months ago
Maziang 9bd8fd220d UI优化
3 months ago
Maziang 2185df2667 fix:IP定位
3 months ago
Maziang 2cd81a5195 快捷键:control+L或command+L呼出学习模式页面
3 months ago
Maziang 254d868643 图标
3 months ago
Maziang 3cb14795b8 小修小补
3 months ago
Maziang dd72229a5f 更新UI
4 months ago
Maziang 611a4b2068 新增导出文件功能
4 months ago
Maziang 6db8f24f21 清理杂项文件
4 months ago
Maziang 2b6c69f73b 打字模式下插入图片
4 months ago
Maziang 8bd14804f7 增强软件在MacOS系统的鲁棒性
4 months ago
Maziang 77e613b8d7 天气组件UI优化
4 months ago
Maziang 660a12abac MAC适配
4 months ago
pshjeamgr 562fcebaa3 Merge pull request '123' (#90) from main into llllllllllllllCC
4 months ago
p9o3yklam 585be5709e Merge pull request '同步功能' (#89) from maziang into main
4 months ago
Horse861 5d8e05c722 测试文档
4 months ago
Horse861 88500b153b feat(学习模式): 实现学习模式与打字模式的内容同步
4 months ago
pshjeamgr c57099e8e2 Merge pull request '123' (#88) from main into llllllllllllllCC
4 months ago
p9o3yklam 021348f375 Merge pull request '1.0_win' (#87) from main into maziang
4 months ago
Horse861 1154098ad1 1.0-win打包
4 months ago
p9o3yklam 4fb70d4bfb Merge pull request '一轮迭代' (#86) from main into maziang
4 months ago
p9o3yklam fe4e54a47f Merge pull request '一轮迭代' (#83) from main into llllllllllllllCC
4 months ago
p9o3yklam e41152b832 Merge pull request '1.0' (#76) from main into maziang
4 months ago

8
.gitignore vendored

@ -198,6 +198,13 @@ temp/
*.orig
# Project specific
resources/config/deepseek_api.json
*.key
*.secret
config/*.json
# Documentation folder
doc/
dist_package/
dist_package_v0.3/
*.zip
@ -231,6 +238,7 @@ venv/
env/
.venv/
.env/
new_venv/
# IDE
.idea/

@ -0,0 +1,235 @@
# MagicWord - MarkText风格编辑器
基于MarkText开源项目的现代化Markdown编辑器集成了MagicWord的所有核心功能。
## 🎯 功能特性
### 核心编辑功能
- **现代化界面**: 基于MarkText的设计风格简洁优雅
- **多标签编辑**: 支持同时编辑多个文档
- **Markdown支持**: 原生支持Markdown语法高亮和预览
- **文件操作**: 支持新建、打开、保存、另存为等操作
- **主题切换**: 支持浅色/深色主题
### 学习模式集成
- **打字学习**: 集成MagicWord的打字学习功能
- **进度跟踪**: 实时显示学习进度
- **多格式支持**: 支持TXT、DOCX、PDF等格式导入学习
- **智能显示**: 根据打字进度逐步显示内容
### 实用工具
- **天气信息**: 实时显示天气状况
- **每日名言**: 显示励志名言和诗句
- **字数统计**: 实时统计文档字数和字符数
- **文件管理**: 侧边栏显示最近文件
### 高级功能
- **拖放支持**: 支持拖拽文件到编辑器
- **快捷键**: 完整的快捷键支持
- **自动保存**: 定期自动保存文档
- **错误恢复**: 异常关闭时的文档恢复
## 🚀 快速开始
### 方式一:直接运行启动器
```bash
python start_marktext.py
```
### 方式二:通过主程序启动
```bash
python src/main.py
```
### 方式三:运行编辑器模块
```bash
python src/marktext_editor_window.py
```
## 📋 使用说明
### 基本操作
1. **新建文档**: 点击"新建"按钮或使用快捷键 `Ctrl+N`
2. **打开文档**: 点击"打开"按钮或使用快捷键 `Ctrl+O`
3. **保存文档**: 点击"保存"按钮或使用快捷键 `Ctrl+S`
4. **另存为**: 使用快捷键 `Ctrl+Shift+S`
### 模式切换
- **编辑模式**: 默认模式,自由编辑文档
- **学习模式**: 切换到打字学习模式,支持多格式文件导入
### 文件导入学习
1. 点击"导入文件"按钮
2. 选择TXT、DOCX或PDF文件
3. 自动切换到学习模式
4. 开始打字学习,内容会逐步显示
### 天气和名言
- **自动更新**: 每10分钟自动获取最新天气和名言
- **手动刷新**: 在工具菜单中可以手动刷新
- **信息显示**: 底部状态栏实时显示
## 🎨 界面介绍
```
┌─────────────────────────────────────────────────────────────┐
│ 文件 编辑 模式 工具 主题 │
├─────────────────────────────────────────────────────────────┤
│ [新建][打开][保存][撤销][重做][剪切][复制][粘贴][学习模式] │
├─────────────┬───────────────────────────────────────────────┤
│ │ │
│ 侧边栏 │ 编辑器区域 │
│ 文件操作 │ (多标签) │
│ 学习模式 │ │
│ 实用工具 │ │
│ │ │
├─────────────┴───────────────────────────────────────────────┤
│ 天气: 北京 25°C 晴天 | 名言: 励志内容... | 字数: 1234 │
└─────────────────────────────────────────────────────────────┘
```
## ⌨️ 快捷键
| 快捷键 | 功能 |
|--------|------|
| Ctrl+N | 新建文档 |
| Ctrl+O | 打开文档 |
| Ctrl+S | 保存文档 |
| Ctrl+Shift+S | 另存为 |
| Ctrl+Z | 撤销 |
| Ctrl+Y | 重做 |
| Ctrl+X | 剪切 |
| Ctrl+C | 复制 |
| Ctrl+V | 粘贴 |
| Ctrl+Q | 退出应用 |
## 🔧 配置说明
### 环境变量
- `QT_PLUGIN_PATH`: Qt插件路径自动设置
- `QT_QPA_PLATFORM`: 平台类型(自动设置)
### 配置文件
- 主题设置:自动保存用户偏好
- 窗口状态:记住窗口大小和位置
- 最近文件:自动记录最近打开的文档
## 📁 项目结构
```
MagicWord/
├── src/
│ ├── marktext_editor_window.py # MarkText主窗口
│ ├── word_main_window.py # Word风格主窗口
│ ├── main.py # 主程序入口
│ ├── services/ # 服务模块
│ │ ├── network_service.py # 网络服务(天气、名言)
│ │ └── file_service.py # 文件服务
│ ├── ui/ # UI组件
│ │ ├── theme_manager.py # 主题管理
│ │ └── components/ # UI组件
│ └── learning_mode_window.py # 学习模式窗口
├── start_marktext.py # MarkText启动器
├── MARKTEXT_README.md # 本文档
└── requirements.txt # 依赖列表
```
## 🛠️ 开发说明
### 基于MarkText架构
本编辑器基于MarkText的开源项目架构结合MagicWord的功能需求进行定制开发
1. **模块化设计**: 采用组件化架构,便于扩展和维护
2. **信号槽机制**: 使用PyQt的信号槽机制实现组件间通信
3. **主题系统**: 集成MagicWord的主题管理器支持动态主题切换
4. **服务集成**: 整合现有的网络服务、文件服务等
### 核心类说明
#### MarkTextMainWindow
主窗口类,负责:
- UI布局管理
- 菜单和工具栏
- 多标签编辑器管理
- 模式切换
- 状态栏信息更新
#### MarkTextEditor
编辑器组件,负责:
- 文本编辑功能
- 文件加载和保存
- 内容变化通知
- 语法高亮(可扩展)
#### MarkTextSideBar
侧边栏组件,负责:
- 文件操作按钮
- 学习模式控制
- 实用工具集成
### 扩展开发
#### 添加新的编辑器功能
```python
class CustomEditor(MarkTextEditor):
def __init__(self, parent=None):
super().__init__(parent)
# 添加自定义功能
def custom_function(self):
# 实现自定义功能
pass
```
#### 添加新的侧边栏工具
```python
class CustomSideBar(MarkTextSideBar):
def __init__(self, parent=None):
super().__init__(parent)
# 添加自定义工具按钮
def add_custom_tool(self):
# 添加自定义工具
pass
```
## 🔍 故障排除
### 常见问题
1. **Qt插件路径错误**
- 检查PyQt5是否正确安装
- 运行启动器脚本自动设置路径
2. **依赖包缺失**
- 运行 `pip install -r requirements.txt`
- 检查Python版本兼容性
3. **网络功能异常**
- 检查网络连接
- 确认API服务可用性
4. **文件导入失败**
- 检查文件格式支持
- 确认文件权限
### 调试模式
运行应用时添加调试参数:
```bash
python start_marktext.py --debug
```
## 📞 支持
如有问题,请:
1. 检查本README的故障排除部分
2. 查看控制台错误信息
3. 提交Issue到项目仓库
## 📄 许可证
本项目基于MarkText开源项目遵循相应的开源协议。
---
**享受现代化的Markdown编辑体验** 🎉

@ -0,0 +1,120 @@
# PyQt5 平台插件问题完全解决方案
## 问题描述
在使用PyQt5时可能会遇到以下错误
```
qt.qpa.plugin: Could not find the Qt platform plugin "cocoa" in ""
This application failed to start because no Qt platform plugin could be initialized.
```
## 解决方案
### 方法一:一键修复(推荐)
运行完整的修复脚本:
```bash
python fix_pyqt5_complete.py
```
### 方法二:手动修复
1. **清理现有安装**
```bash
pip uninstall PyQt5 PyQt5-Qt5 PyQt5-sip -y
rm -rf /Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5*
```
2. **重新安装**
```bash
pip install PyQt5==5.15.10 --force-reinstall --no-cache-dir
```
3. **设置环境变量**
```bash
source set_pyqt5_env.sh
```
### 方法三:安全安装
使用安全安装脚本:
```bash
python install_pyqt5_safe.py
```
## 预防措施
### 1. 在main.py中集成环境设置
确保你的 `main.py` 包含了增强的Qt插件路径设置函数。
### 2. 创建启动脚本
创建 `start_app.sh`
```bash
#!/bin/bash
# PyQt5应用程序启动脚本
# 设置环境变量
export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
export QT_QPA_PLATFORM="cocoa"
export QT_MAC_WANTS_LAYER="1"
# 启动应用
python src/main.py
```
### 3. 使用虚拟环境专用安装
```bash
# 激活虚拟环境
source .venv/bin/activate
# 在虚拟环境中安装
python fix_pyqt5_complete.py
```
## 环境变量说明
| 变量名 | 作用 | 推荐值 |
|--------|------|--------|
| QT_PLUGIN_PATH | Qt插件主路径 | PyQt5/Qt5/plugins |
| QT_QPA_PLATFORM_PLUGIN_PATH | 平台插件路径 | PyQt5/Qt5/plugins/platforms |
| QT_QPA_PLATFORM | 指定平台 | cocoa (macOS) |
| QT_MAC_WANTS_LAYER | macOS图层支持 | 1 |
| QT_LOGGING_RULES | 日志级别 | qt.qpa.*=false |
## 常见问题
### Q: 为什么PyQt5会丢失平台插件
A: 常见原因:
- 安装过程中断或失败
- 虚拟环境迁移
- 系统Qt库冲突
- 文件权限问题
### Q: 如何验证修复是否成功?
A: 运行测试命令:
```python
python -c "from PyQt5.QtWidgets import QApplication; print('成功!')"
```
### Q: 修复后仍然有问题?
A: 尝试:
1. 完全删除虚拟环境重新创建
2. 使用系统包管理器安装Qt5
3. 检查Python版本兼容性
## 最佳实践
1. **始终使用虚拟环境**
2. **固定PyQt5版本**推荐5.15.10
3. **在代码中设置插件路径**
4. **创建启动脚本**
5. **定期验证安装**
## 一键修复命令
```bash
# 完整的修复流程
cd /Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design
python fix_pyqt5_complete.py
source set_pyqt5_env.sh
python src/main.py
```
这样应该能完全避免PyQt5平台插件问题

@ -0,0 +1,52 @@
# 扫雷游戏说明
## 游戏介绍
这是一个经典的扫雷游戏实现基于PyQt5框架开发。游戏中玩家需要根据数字提示找出所有非地雷的方块避免踩到地雷。
## 游戏特性
- 经典的扫雷游戏玩法
- 三种难度级别:初级(9x9, 10个地雷)、中级(16x16, 40个地雷)、高级(30x16, 99个地雷)
- 计时功能,记录游戏时间
- 右键标记地雷功能
- 自动展开空白区域功能
- 胜负判断和游戏结束提示
## 如何开始游戏
有两种方式可以启动扫雷游戏:
### 方法一:通过主应用启动(推荐)
1. 运行主应用:`python src/main.py`
2. 在菜单栏中选择:应用选项(O) -> 小游戏 -> 扫雷
### 方法二:直接运行测试脚本
1. 运行简化测试脚本:`python simple_minesweeper_test.py`
## 游戏操作说明
- **左键单击**:揭开方块
- **右键单击**:标记/取消标记地雷(旗子)
- **左右键同时单击**(或中键单击):自动展开周围区域(当数字周围的旗子数量等于该数字时)
## 游戏规则
1. 点击任意方块开始游戏
2. 数字表示周围8个方块中地雷的数量
3. 右键点击可以标记疑似地雷的位置
4. 避免点击地雷,否则游戏结束
5. 成功标记所有地雷或揭开所有非地雷方块即可获胜
## 技术实现
- 使用PyQt5构建图形界面
- 自定义按钮类MineButton继承自QPushButton
- 实现了完整的扫雷游戏逻辑,包括地雷生成、数字计算、递归展开等
- 支持多种难度级别的游戏配置
## 文件结构
- `src/ui/minesweeper_game.py`:扫雷游戏核心实现文件
- `simple_minesweeper_test.py`:简化版测试脚本
- `test_minesweeper.py`:完整版测试脚本
## 故障排除
如果遇到Qt平台插件错误请确保
1. 已正确安装PyQt5`pip install PyQt5`
2. 环境变量已正确设置(通常由应用自动处理)
如有任何问题,请联系开发者。

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,331 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MagicWord 1.0.0 版本发布脚本
用于构建和打包应用程序包含所有图片和图标资源
"""
import os
import sys
import subprocess
import platform
import shutil
import zipfile
from datetime import datetime
from PIL import Image
def run_command(command, shell=False, cwd=None):
"""运行命令并返回结果"""
try:
result = subprocess.run(command, shell=shell, capture_output=True, text=True, encoding='utf-8', cwd=cwd)
return result.returncode, result.stdout, result.stderr
except Exception as e:
return -1, "", str(e)
def create_ico_from_png():
"""从PNG图标创建ICO文件"""
print("创建ICO图标文件...")
# 检查是否存在256x256的PNG图标
png_path = "resources/icons/app_icon_256X256.png"
ico_path = "resources/icons/app_icon.ico"
if os.path.exists(png_path):
try:
# 打开PNG图像
img = Image.open(png_path)
# 创建不同尺寸的图标
icon_sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
# 保存为ICO格式
img.save(ico_path, format='ICO', sizes=icon_sizes)
print(f"ICO图标创建成功: {ico_path}")
return True
except Exception as e:
print(f"创建ICO图标失败: {e}")
return False
else:
print(f"找不到PNG图标文件: {png_path}")
return False
def clean_build_dirs():
"""清理构建目录"""
print("清理构建目录...")
dirs_to_clean = ['build', 'dist', '__pycache__', '*.egg-info']
for dir_name in dirs_to_clean:
if '*' in dir_name:
# 处理通配符
import glob
for path in glob.glob(dir_name):
if os.path.isdir(path):
shutil.rmtree(path, ignore_errors=True)
elif os.path.exists(dir_name):
if os.path.isdir(dir_name):
shutil.rmtree(dir_name, ignore_errors=True)
else:
os.remove(dir_name)
# 清理src目录下的__pycache__
for root, dirs, files in os.walk('src'):
for dir_name in dirs:
if dir_name == '__pycache__':
cache_path = os.path.join(root, dir_name)
shutil.rmtree(cache_path, ignore_errors=True)
print(f"清理缓存: {cache_path}")
def install_dependencies():
"""安装依赖"""
print("安装项目依赖...")
# 首先安装PIL用于图标转换
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "Pillow"])
if code != 0:
print(f"Pillow安装失败: {stderr}")
return False
# 安装其他依赖
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
if code != 0:
print(f"依赖安装失败: {stderr}")
return False
print("依赖安装成功")
return True
def build_executable():
"""构建可执行文件"""
print("构建可执行文件...")
# 安装pyinstaller
print("安装PyInstaller...")
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "pyinstaller"])
if code != 0:
print(f"PyInstaller安装失败: {stderr}")
return False
# 创建ICO图标
create_ico_from_png()
# PyInstaller命令 - 完整版本
pyinstaller_cmd = [
"pyinstaller",
"--name", "MagicWord",
"--version", "1.0.0", # 设置版本号为1.0.0
"--distpath", "dist",
"--workpath", "build",
"--specpath", ".",
# 添加资源文件
"--add-data", "resources;resources",
"--add-data", "resources/icons;resources/icons",
"--add-data", "resources/config;resources/config",
"--add-data", "resources/qss;resources/qss",
"--add-data", "src;src",
"--add-data", "src/ui;src/ui",
"--add-data", "src/demo;src/demo",
# 隐藏导入
"--hidden-import", "PyQt5",
"--hidden-import", "PyQt5.QtCore",
"--hidden-import", "PyQt5.QtGui",
"--hidden-import", "PyQt5.QtWidgets",
"--hidden-import", "requests",
"--hidden-import", "beautifulsoup4",
"--hidden-import", "python-docx",
"--hidden-import", "PyPDF2",
"--hidden-import", "ebooklib",
"--hidden-import", "chardet",
"--hidden-import", "PIL",
"--hidden-import", "pillow",
# 图标设置
"--icon", "resources/icons/app_icon.ico" if os.path.exists("resources/icons/app_icon.ico") else "resources/icons/app_icon_256X256.png",
"--windowed", # 无控制台窗口
"--noconfirm",
"--clean", # 清理临时文件
"src/main.py"
]
print("运行PyInstaller...")
code, stdout, stderr = run_command(pyinstaller_cmd)
if code != 0:
print(f"完整构建失败,尝试简化构建: {stderr}")
# 简化版本
simple_cmd = [
"pyinstaller",
"--onefile",
"--windowed",
"--icon=resources/icons/app_icon.ico" if os.path.exists("resources/icons/app_icon.ico") else "resources/icons/app_icon_256X256.png",
"--add-data=resources;resources",
"--add-data=resources/icons;resources/icons",
"--add-data=src;src",
"--name=MagicWord",
"--version=1.0.0",
"--clean",
"src/main.py"
]
code, stdout, stderr = run_command(simple_cmd)
if code != 0:
print(f"简化构建也失败: {stderr}")
return False
print("可执行文件构建成功")
return True
def create_package():
"""创建发布包"""
print("创建发布包...")
# 检查构建结果
if platform.system() == "Windows":
exe_path = "dist/MagicWord.exe"
else:
exe_path = "dist/MagicWord"
if not os.path.exists(exe_path):
print(f"错误: 找不到可执行文件 {exe_path}")
return False
# 创建发布目录
release_dir = "dist_package_v1.0"
if os.path.exists(release_dir):
shutil.rmtree(release_dir)
os.makedirs(release_dir)
# 复制文件到发布目录
files_to_copy = [
(exe_path, "MagicWord.exe" if platform.system() == "Windows" else "MagicWord"),
("README.md", "README.md"),
("CHANGELOG.md", "CHANGELOG.md"),
("requirements.txt", "requirements.txt"),
("install_and_fix.py", "install_and_fix.py"),
]
for src, dst in files_to_copy:
if os.path.exists(src):
shutil.copy2(src, os.path.join(release_dir, dst))
print(f"复制: {src} -> {dst}")
# 复制图标文件到发布包
icons_dir = os.path.join(release_dir, "icons")
if os.path.exists("resources/icons"):
shutil.copytree("resources/icons", icons_dir)
print("复制图标文件到发布包")
# 创建运行脚本
if platform.system() == "Windows":
run_script = """@echo off
echo MagicWord 1.0.0 启动中...
cd /d "%~dp0"
start MagicWord.exe
"""
with open(os.path.join(release_dir, "run.bat"), "w") as f:
f.write(run_script)
# 创建桌面快捷方式脚本
desktop_shortcut_script = """@echo off
echo 创建桌面快捷方式...
set SCRIPT_DIR=%~dp0
set DESKTOP=%USERPROFILE%\Desktop
set SHORTCUT=%DESKTOP%\MagicWord.lnk
powershell -Command "$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%SHORTCUT%'); $Shortcut.TargetPath = '%SCRIPT_DIR%MagicWord.exe'; $Shortcut.WorkingDirectory = '%SCRIPT_DIR%'; $Shortcut.IconLocation = '%SCRIPT_DIR%icons\\app_icon_256X256.png'; $Shortcut.Save()"
echo 桌面快捷方式创建完成
pause
"""
with open(os.path.join(release_dir, "create_desktop_shortcut.bat"), "w") as f:
f.write(desktop_shortcut_script)
else:
run_script = """#!/bin/bash
echo "MagicWord 1.0.0 启动中..."
cd "$(dirname "$0")"
./MagicWord &
"""
with open(os.path.join(release_dir, "run.sh"), "w") as f:
f.write(run_script)
os.chmod(os.path.join(release_dir, "run.sh"), 0o755)
# 创建发布说明
release_info = f"""MagicWord 1.0.0 发布包
构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
平台: {platform.system()} {platform.machine()}
Python版本: {platform.python_version()}
快速开始:
1. 运行 install_and_fix.py 安装依赖如果需要
2. 运行 run.bat (Windows) run.sh (Linux/Mac)
3. 或直接运行 MagicWord.exe
功能特性:
- 完整的文档处理功能
- 打字练习模式
- 学习模式
- 多种文档格式支持Word, PDF, TXT等
- 美观的Word风格界面
- 完整的图标和界面资源
图标说明:
- 包含完整的图标资源文件
- 支持多种尺寸的图标32x32, 64x64, 128x128, 256x256
- Windows版本包含ICO格式图标
- 可创建桌面快捷方式
详细更新请查看 CHANGELOG.md
"""
with open(os.path.join(release_dir, "发布说明.txt"), "w", encoding='utf-8') as f:
f.write(release_info)
# 创建ZIP压缩包
print("创建ZIP压缩包...")
zip_filename = f"MagicWord_v1.0.0_{platform.system()}_{platform.machine()}"
with zipfile.ZipFile(f"{zip_filename}.zip", 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(release_dir):
for file in files:
file_path = os.path.join(root, file)
arc_path = os.path.relpath(file_path, release_dir)
zipf.write(file_path, arc_path)
print(f"添加到压缩包: {arc_path}")
print(f"发布包创建成功: {zip_filename}.zip")
return True
def main():
"""主函数"""
print("=" * 50)
print("MagicWord 1.0.0 构建脚本")
print("=" * 50)
# 检查Python版本
if sys.version_info < (3, 6):
print("错误: 需要Python 3.6或更高版本")
return False
# 清理构建目录
clean_build_dirs()
# 安装依赖
if not install_dependencies():
print("依赖安装失败")
return False
# 构建可执行文件
if not build_executable():
print("可执行文件构建失败")
return False
# 创建发布包
if not create_package():
print("发布包创建失败")
return False
print("=" * 50)
print("构建完成!")
print("发布包位于: dist_package_v1.0/")
print("ZIP压缩包已创建")
print("=" * 50)
return True
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

@ -0,0 +1,55 @@
#!/bin/bash
# PyQt5 紧急修复脚本 - 终极解决方案
echo "🚨 PyQt5 紧急修复中..."
# 1. 完全清理现有安装
echo "📦 步骤1: 清理PyQt5安装..."
pip uninstall PyQt5 PyQt5-Qt5 PyQt5-sip -y
rm -rf /Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5*
# 2. 重新安装PyQt5
echo "📦 步骤2: 重新安装PyQt5..."
pip install PyQt5==5.15.10 --force-reinstall --no-cache-dir
# 3. 设置环境变量
echo "🔧 步骤3: 设置环境变量..."
export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
export QT_QPA_PLATFORM="cocoa"
export QT_MAC_WANTS_LAYER="1"
export QT_LOGGING_RULES="qt.qpa.*=false"
# 4. 验证安装
echo "✅ 步骤4: 验证安装..."
python -c "
import sys
import os
os.environ['QT_PLUGIN_PATH'] = '$QT_PLUGIN_PATH'
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = '$QT_QPA_PLATFORM_PLUGIN_PATH'
os.environ['QT_QPA_PLATFORM'] = 'cocoa'
os.environ['QT_MAC_WANTS_LAYER'] = '1'
try:
from PyQt5.QtWidgets import QApplication, QLabel
from PyQt5.QtCore import Qt
app = QApplication(sys.argv)
label = QLabel('PyQt5修复成功✅')
label.setAlignment(Qt.AlignCenter)
label.resize(200, 100)
label.show()
from PyQt5.QtCore import QTimer
QTimer.singleShot(1500, app.quit)
app.exec_()
print('✅ PyQt5验证成功')
except Exception as e:
print(f'❌ PyQt5验证失败: {e}')
import traceback
traceback.print_exc()
"
echo "🎉 修复完成!"
echo "现在可以运行: python src/main.py"

@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""
完整的PyQt5问题解决方案
运行此脚本可以完全避免PyQt5平台插件问题
"""
import subprocess
import sys
import os
import shutil
import platform
def run_command(cmd, description):
"""运行命令并显示进度"""
print(f"正在执行: {description}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"❌ 失败: {result.stderr}")
return False
print(f"✅ 完成")
return True
def clean_pyqt5_installation():
"""彻底清理PyQt5安装"""
print("=== 清理PyQt5安装 ===")
# 1. 卸载PyQt5包
run_command([sys.executable, "-m", "pip", "uninstall", "PyQt5", "PyQt5-Qt5", "PyQt5-sip", "-y"],
"卸载PyQt5包")
# 2. 清理残留文件
venv_path = os.environ.get('VIRTUAL_ENV', '')
if venv_path:
site_packages = os.path.join(venv_path, 'lib', f'python{sys.version_info.major}.{sys.version_info.minor}', 'site-packages')
if os.path.exists(site_packages):
removed_count = 0
for item in os.listdir(site_packages):
if 'pyqt5' in item.lower():
item_path = os.path.join(site_packages, item)
try:
if os.path.isdir(item_path):
shutil.rmtree(item_path)
else:
os.remove(item_path)
removed_count += 1
print(f" 删除: {item}")
except Exception as e:
print(f" 删除失败 {item}: {e}")
print(f"✅ 清理完成,删除了 {removed_count} 个项目")
return True
def install_pyqt5_properly():
"""正确安装PyQt5"""
print("=== 安装PyQt5 ===")
# 使用清华镜像源加速下载(可选)
pip_args = [sys.executable, "-m", "pip", "install"]
# 检查是否有国内镜像源可用
try:
import requests
response = requests.get("https://pypi.tuna.tsinghua.edu.cn/simple", timeout=5)
if response.status_code == 200:
pip_args.extend(["-i", "https://pypi.tuna.tsinghua.edu.cn/simple"])
print("使用清华镜像源")
except:
pass
# 安装PyQt5
pip_args.extend(["PyQt5==5.15.10", "--no-cache-dir", "--force-reinstall"])
return run_command(pip_args, "安装PyQt5")
def setup_environment_variables():
"""设置环境变量"""
print("=== 设置环境变量 ===")
system = platform.system()
venv_path = os.environ.get('VIRTUAL_ENV', '')
if not venv_path:
print("❌ 未检测到虚拟环境")
return False
python_version = f"python{sys.version_info.major}.{sys.version_info.minor}"
# 可能的Qt插件路径
possible_paths = []
if system == "Darwin": # macOS
possible_paths = [
os.path.join(venv_path, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
'/usr/local/opt/qt5/plugins',
'/opt/homebrew/opt/qt5/plugins',
]
elif system == "Windows":
possible_paths = [
os.path.join(venv_path, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
]
elif system == "Linux":
possible_paths = [
os.path.join(venv_path, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
'/usr/lib/x86_64-linux-gnu/qt5/plugins',
]
# 找到有效的路径
valid_path = None
for path in possible_paths:
if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')):
valid_path = path
break
if valid_path:
# 创建环境变量设置脚本
env_script = f"""
# PyQt5环境变量设置
export QT_PLUGIN_PATH="{valid_path}"
export QT_QPA_PLATFORM_PLUGIN_PATH="{os.path.join(valid_path, 'platforms')}"
"""
if system == "Darwin":
env_script += """
export QT_QPA_PLATFORM="cocoa"
export QT_MAC_WANTS_LAYER="1"
export QT_LOGGING_RULES="qt.qpa.*=false"
"""
elif system == "Windows":
env_script += """
export QT_QPA_PLATFORM="windows"
"""
elif system == "Linux":
env_script += """
export QT_QPA_PLATFORM="xcb"
"""
# 保存环境变量脚本
project_root = os.path.dirname(os.path.abspath(__file__))
env_file = os.path.join(project_root, 'set_pyqt5_env.sh')
with open(env_file, 'w') as f:
f.write(env_script.strip())
print(f"✅ 环境变量脚本已创建: {env_file}")
print(f" QT_PLUGIN_PATH: {valid_path}")
return True
else:
print("❌ 未找到有效的Qt插件路径")
return False
def verify_installation():
"""验证安装"""
print("=== 验证安装 ===")
# 测试导入
test_code = """
import sys
import os
# 设置环境变量
if 'QT_PLUGIN_PATH' in os.environ:
os.environ['QT_PLUGIN_PATH'] = os.environ['QT_PLUGIN_PATH']
if 'QT_QPA_PLATFORM_PLUGIN_PATH' in os.environ:
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']
try:
from PyQt5.QtWidgets import QApplication, QLabel
from PyQt5.QtCore import Qt
app = QApplication(sys.argv)
label = QLabel("PyQt5安装成功")
label.setAlignment(Qt.AlignCenter)
label.resize(200, 100)
label.show()
# 只显示2秒后自动关闭
from PyQt5.QtCore import QTimer
QTimer.singleShot(2000, app.quit)
app.exec_()
print("✅ PyQt5验证成功")
except Exception as e:
print(f"❌ PyQt5验证失败: {e}")
import traceback
traceback.print_exc()
"""
with open('test_pyqt5.py', 'w') as f:
f.write(test_code)
result = subprocess.run([sys.executable, 'test_pyqt5.py'], capture_output=True, text=True)
# 清理测试文件
if os.path.exists('test_pyqt5.py'):
os.remove('test_pyqt5.py')
return result.returncode == 0
def main():
"""主函数"""
print("=== PyQt5完整修复方案 ===")
print(f"系统: {platform.system()}")
print(f"Python: {sys.version}")
print(f"虚拟环境: {os.environ.get('VIRTUAL_ENV', '未激活')}")
print()
# 获取项目根目录
project_root = os.path.dirname(os.path.abspath(__file__))
os.chdir(project_root)
steps = [
("清理安装", clean_pyqt5_installation),
("重新安装", install_pyqt5_properly),
("设置环境", setup_environment_variables),
("验证安装", verify_installation),
]
success = True
for step_name, step_func in steps:
if not step_func():
print(f"{step_name} 失败")
success = False
break
print()
if success:
print("🎉 PyQt5修复完成")
print("\n使用方法:")
print("1. 运行: source set_pyqt5_env.sh")
print("2. 然后运行: python src/main.py")
else:
print("❌ PyQt5修复失败请检查错误信息")
return success
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)

@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""
安全安装PyQt5的脚本避免平台插件问题
"""
import subprocess
import sys
import os
def install_pyqt5_safely():
"""安全安装PyQt5的方法"""
print("正在安全安装PyQt5...")
# 1. 首先完全卸载现有的PyQt5
print("1. 卸载现有PyQt5...")
subprocess.run([sys.executable, "-m", "pip", "uninstall", "PyQt5", "PyQt5-Qt5", "PyQt5-sip", "-y"],
capture_output=True, text=True)
# 2. 清理可能残留的文件
print("2. 清理残留文件...")
venv_path = os.environ.get('VIRTUAL_ENV', '')
if venv_path:
site_packages = os.path.join(venv_path, 'lib', f'python{sys.version_info.major}.{sys.version_info.minor}', 'site-packages')
if os.path.exists(site_packages):
for item in os.listdir(site_packages):
if 'pyqt5' in item.lower():
item_path = os.path.join(site_packages, item)
if os.path.isdir(item_path):
import shutil
shutil.rmtree(item_path)
print(f" 删除目录: {item}")
# 3. 使用特定版本安装PyQt5
print("3. 安装PyQt5...")
result = subprocess.run([
sys.executable, "-m", "pip", "install",
"PyQt5==5.15.10",
"--no-cache-dir", # 不使用缓存,确保重新下载
"--force-reinstall" # 强制重新安装
], capture_output=True, text=True)
if result.returncode == 0:
print("✅ PyQt5安装成功")
# 4. 验证安装
print("4. 验证安装...")
test_result = subprocess.run([
sys.executable, "-c",
"from PyQt5.QtWidgets import QApplication; print('PyQt5导入成功')"
], capture_output=True, text=True)
if test_result.returncode == 0:
print("✅ PyQt5验证通过")
return True
else:
print("❌ PyQt5验证失败:", test_result.stderr)
return False
else:
print("❌ PyQt5安装失败:", result.stderr)
return False
if __name__ == "__main__":
success = install_pyqt5_safely()
sys.exit(0 if success else 1)

@ -0,0 +1,251 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MagicWord 1.0.0 完整打包脚本
创建包含所有资源的发布包
"""
import os
import sys
import shutil
import zipfile
from datetime import datetime
def create_package():
"""创建完整的发布包"""
print("=" * 60)
print("MagicWord 1.0.0 完整打包程序")
print("=" * 60)
# 检查可执行文件是否存在
exe_path = "dist/MagicWord.exe"
if not os.path.exists(exe_path):
print(f"错误: 找不到可执行文件 {exe_path}")
print("请先运行PyInstaller构建可执行文件")
return False
# 创建发布目录
release_dir = "MagicWord_v1.0.0_Windows"
if os.path.exists(release_dir):
shutil.rmtree(release_dir)
os.makedirs(release_dir)
print("创建发布包目录...")
# 复制主要文件
files_to_copy = [
(exe_path, "MagicWord.exe"),
("README.md", "README.md"),
("CHANGELOG.md", "CHANGELOG.md"),
("requirements.txt", "requirements.txt"),
("install_and_fix.py", "install_and_fix.py"),
]
for src, dst in files_to_copy:
if os.path.exists(src):
shutil.copy2(src, os.path.join(release_dir, dst))
print(f"✓ 复制: {src} -> {dst}")
else:
print(f"⚠ 跳过: {src} (文件不存在)")
# 复制图标文件
icons_dir = os.path.join(release_dir, "icons")
if os.path.exists("resources/icons"):
shutil.copytree("resources/icons", icons_dir)
print("✓ 复制图标文件到发布包")
# 复制配置文件
config_dir = os.path.join(release_dir, "config")
if os.path.exists("resources/config"):
shutil.copytree("resources/config", config_dir)
print("✓ 复制配置文件到发布包")
# 复制样式文件
qss_dir = os.path.join(release_dir, "qss")
if os.path.exists("resources/qss") and os.listdir("resources/qss"):
shutil.copytree("resources/qss", qss_dir)
print("✓ 复制样式文件到发布包")
# 注意: UI图像文件已通过PyInstaller打包到可执行文件中
# 程序会在运行时从打包的资源中加载 114514.png 和 UI.png
# 创建运行脚本
print("创建运行脚本...")
# Windows运行脚本
run_script = """@echo off
echo ========================================
echo MagicWord 1.0.0 启动中...
echo ========================================
cd /d "%~dp0"
start "" "MagicWord.exe"
echo 程序已启动
timeout /t 3 /nobreak > nul
"""
with open(os.path.join(release_dir, "运行程序.bat"), "w", encoding='utf-8') as f:
f.write(run_script)
# 创建桌面快捷方式脚本
desktop_shortcut_script = """@echo off
echo 正在创建桌面快捷方式...
cd /d "%~dp0"
set SCRIPT_DIR=%CD%
set DESKTOP=%USERPROFILE%\Desktop
set SHORTCUT=%DESKTOP%\MagicWord.lnk
echo 脚本目录: %SCRIPT_DIR%
echo 桌面路径: %DESKTOP%
powershell -Command "
$WshShell = New-Object -comObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut('%SHORTCUT%')
$Shortcut.TargetPath = '%SCRIPT_DIR%\MagicWord.exe'
$Shortcut.WorkingDirectory = '%SCRIPT_DIR%'
$Shortcut.IconLocation = '%SCRIPT_DIR%\icons\app_icon_256X256.png'
$Shortcut.Description = 'MagicWord 1.0.0 - 隐私学习软件'
$Shortcut.Save()
Write-Host '桌面快捷方式创建成功!' -ForegroundColor Green
"
if exist "%SHORTCUT%" (
echo 桌面快捷方式创建成功
) else (
echo 桌面快捷方式创建失败
)
echo.
echo 按任意键退出...
pause > nul
"""
with open(os.path.join(release_dir, "创建桌面快捷方式.bat"), "w", encoding='utf-8') as f:
f.write(desktop_shortcut_script)
# 创建安装说明
install_guide = """MagicWord 1.0.0 安装使用说明
========================================
系统要求
========================================
- Windows 7/8/10/11 (64)
- 至少 100MB 可用磁盘空间
- 建议内存: 4GB 以上
========================================
快速开始
========================================
方法1: 直接运行
1. 双击 "MagicWord.exe" 即可运行程序
方法2: 使用运行脚本
1. 双击 "运行程序.bat" 自动启动程序
方法3: 创建桌面快捷方式
1. 双击 "创建桌面快捷方式.bat"
2. 桌面上会出现 MagicWord 快捷方式
3. 双击桌面快捷方式即可运行
========================================
文件说明
========================================
MagicWord.exe - 主程序文件
icons/ - 图标文件夹
app_icon.ico - 应用程序图标
app_icon_256X256.png - 256x256图标
app_icon_128X128.png - 128x128图标
app_icon_64X64.png - 64x64图标
app_icon_32X32.png - 32x32图标
config/ - 配置文件
README.md - 项目说明文档
CHANGELOG.md - 更新日志
========================================
卸载方法
========================================
直接删除整个 MagicWord 文件夹即可完全卸载
========================================
技术支持
========================================
如有问题请查看 README.md 文件或联系开发者
========================================
版本信息
========================================
版本: 1.0.0
构建时间: {build_time}
平台: Windows
架构: {architecture}
祝您使用愉快
""".format(
build_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
architecture="64位"
)
with open(os.path.join(release_dir, "安装说明.txt"), "w", encoding='utf-8') as f:
f.write(install_guide)
# 创建版本信息文件
version_info = f"""MagicWord Version 1.0.0
Build Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
Platform: Windows
Architecture: 64-bit
Python Version: 3.13
Qt Version: PyQt5
This is a standalone executable package containing all necessary dependencies.
"""
with open(os.path.join(release_dir, "version.txt"), "w", encoding='utf-8') as f:
f.write(version_info)
print("创建ZIP压缩包...")
# 创建ZIP压缩包
zip_filename = f"MagicWord_v1.0.0_Windows_64bit"
with zipfile.ZipFile(f"{zip_filename}.zip", 'w', zipfile.ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(release_dir):
for file in files:
file_path = os.path.join(root, file)
arc_path = os.path.relpath(file_path, release_dir)
zipf.write(file_path, arc_path)
print(f"添加到压缩包: {arc_path}")
print("=" * 60)
print("✅ 打包完成!")
print(f"📁 发布包目录: {release_dir}/")
print(f"📦 ZIP压缩包: {zip_filename}.zip")
print("=" * 60)
# 显示发布包内容
print("\n发布包内容:")
for root, dirs, files in os.walk(release_dir):
level = root.replace(release_dir, '').count(os.sep)
indent = ' ' * 2 * level
print(f"{indent}{os.path.basename(root)}/")
subindent = ' ' * 2 * (level + 1)
for file in files:
print(f"{subindent}{file}")
return True
def main():
"""主函数"""
try:
success = create_package()
if success:
print("\n🎉 所有文件已成功打包!")
print("您可以分发 ZIP 文件给用户。")
else:
print("\n❌ 打包过程中出现错误。")
return 1
except Exception as e:
print(f"\n❌ 发生错误: {e}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

@ -0,0 +1,7 @@
#!/bin/bash
echo "设置Qt调试环境变量..."
export QT_DEBUG_PLUGINS=1
echo "Qt调试模式已启用"
echo ""
echo "运行MagicWord应用程序..."
python src/main.py

@ -0,0 +1,20 @@
#!/bin/bash
# MagicWord 修复版启动脚本
echo "🚀 正在启动 MagicWord (修复版)..."
# 使用新的虚拟环境
source new_venv/bin/activate
# 设置Qt环境变量
export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/new_venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/new_venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
export QT_QPA_PLATFORM="cocoa"
export QT_MAC_WANTS_LAYER="1"
export QT_LOGGING_RULES="qt.qpa.*=false"
echo "✅ 环境设置完成"
echo "✅ 正在启动 MagicWord 应用..."
# 启动应用
cd src && python main.py

@ -0,0 +1,29 @@
#!/bin/bash
# MagicWord 一键启动脚本
# 自动处理PyQt5平台插件问题
echo "🚀 正在启动 MagicWord..."
# 设置PyQt5环境变量
export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
export QT_QPA_PLATFORM="cocoa"
export QT_MAC_WANTS_LAYER="1"
export QT_LOGGING_RULES="qt.qpa.*=false"
# 激活虚拟环境(如果未激活)
if [ -z "$VIRTUAL_ENV" ]; then
echo "📦 激活虚拟环境..."
source .venv/bin/activate
fi
# 检查PyQt5是否可用
python -c "from PyQt5.QtWidgets import QApplication" 2>/dev/null
if [ $? -ne 0 ]; then
echo "❌ PyQt5出现问题正在修复..."
./emergency_fix.sh
fi
# 启动应用
echo "✅ 启动 MagicWord 应用..."
cd src && python main.py

@ -0,0 +1,7 @@
# PyQt5环境变量设置
export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
export QT_QPA_PLATFORM="cocoa"
export QT_MAC_WANTS_LAYER="1"
export QT_LOGGING_RULES="qt.qpa.*=false"

@ -2,12 +2,15 @@ from setuptools import setup, find_packages
setup(
name="MagicWord",
version="0.3.0",
version="1.0.0",
description="隐私学习软件 - 一款通过打字练习来学习文档内容的工具",
author="MagicWord Team",
packages=find_packages(where="src"),
package_dir={"": "src"},
include_package_data=True,
package_data={
"": ["*.png", "*.ico", "*.json", "*.qss", "*.txt"],
},
install_requires=[
"python-docx>=0.8.10",
"PyPDF2>=1.26.0",
@ -24,4 +27,17 @@ setup(
],
},
python_requires=">=3.6",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: End Users/Desktop",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Topic :: Education",
"Topic :: Office/Business",
],
)

@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""
Qt环境配置脚本确保平台插件正确加载
"""
import os
import sys
def setup_qt_environment():
"""设置Qt环境变量避免平台插件问题"""
# 获取虚拟环境的site-packages路径
venv_path = os.environ.get('VIRTUAL_ENV', '')
if not venv_path:
print("警告:未检测到虚拟环境")
return False
# 构建Qt插件路径
python_version = f"python{sys.version_info.major}.{sys.version_info.minor}"
qt_plugin_path = os.path.join(venv_path, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins')
if not os.path.exists(qt_plugin_path):
print(f"错误Qt插件路径不存在: {qt_plugin_path}")
return False
# 设置环境变量
os.environ['QT_PLUGIN_PATH'] = qt_plugin_path
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(qt_plugin_path, 'platforms')
# 对于macOS还需要设置其他重要变量
if sys.platform == 'darwin':
os.environ['QT_QPA_PLATFORM'] = 'cocoa'
# 禁用Qt的某些可能导致问题的功能
os.environ['QT_MAC_WANTS_LAYER'] = '1'
os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1'
print(f"✅ Qt环境变量设置完成")
print(f" QT_PLUGIN_PATH: {qt_plugin_path}")
print(f" QT_QPA_PLATFORM_PLUGIN_PATH: {os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']}")
return True
def verify_qt_setup():
"""验证Qt设置是否正确"""
try:
from PyQt5.QtCore import QCoreApplication
from PyQt5.QtWidgets import QApplication
# 创建QApplication实例来测试
app = QCoreApplication.instance()
if app is None:
app = QApplication([])
# 获取平台信息
platform = app.platformName()
print(f"✅ Qt平台检测成功: {platform}")
return True
except Exception as e:
print(f"❌ Qt验证失败: {e}")
return False
if __name__ == "__main__":
if setup_qt_environment():
verify_qt_setup()

@ -0,0 +1,58 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
简化版扫雷游戏测试脚本
用于快速测试扫雷游戏模块是否能正常运行
"""
import sys
import os
# 添加项目根目录到Python路径
project_root = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, project_root)
# 设置Qt环境变量
def setup_qt_env():
"""设置基本的Qt环境变量"""
venv_plugins = os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins')
if os.path.exists(venv_plugins):
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(venv_plugins, 'platforms')
print(f"✅ Qt平台插件路径已设置: {os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']}")
return True
else:
print("❌ 未找到Qt平台插件路径")
return False
# 尝试设置Qt环境
if not setup_qt_env():
print("警告Qt环境设置失败游戏可能无法正常显示")
try:
from PyQt5.QtWidgets import QApplication, QMainWindow
from src.ui.minesweeper_game import MinesweeperWindow
def main():
app = QApplication(sys.argv)
# 创建并显示扫雷游戏窗口
window = MinesweeperWindow()
window.show()
print("🎮 扫雷游戏窗口已显示")
print(" 请检查是否有游戏窗口弹出")
print(" 如果没有窗口弹出请关闭此窗口并检查Qt环境配置")
sys.exit(app.exec_())
if __name__ == '__main__':
main()
except ImportError as e:
print(f"❌ 导入错误: {e}")
print("请确保已正确安装PyQt5")
except Exception as e:
print(f"❌ 运行错误: {e}")
import traceback
traceback.print_exc()

@ -0,0 +1,773 @@
"""
DeepSeek对话窗口模块
提供与DeepSeek AI对话的功能
"""
import os
import json
import requests
import threading
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTextEdit,
QLineEdit, QPushButton, QLabel, QMessageBox,
QSplitter, QScrollArea, QWidget, QFrame)
from PyQt5.QtCore import Qt, pyqtSignal, QTimer
from PyQt5.QtGui import QFont, QTextCursor
class DeepSeekDialogWindow(QDialog):
"""DeepSeek对话窗口"""
closed = pyqtSignal() # 窗口关闭信号
streaming_finished = pyqtSignal() # 流式输出完成信号
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.api_key = ""
self.conversation_history = []
# 流式输出相关变量
self.is_streaming = False
self.current_streaming_content = ""
self.streaming_message_id = ""
self.streaming_thread = None
self.streaming_timer = None
# 从本地加载API密钥
self.load_api_key()
# 如果没有API密钥显示设置对话框
if not self.api_key:
self.show_api_key_dialog()
self.init_ui()
def load_api_key(self):
"""从本地文件加载API密钥"""
config_file = os.path.join(os.path.dirname(__file__), "..", "resources", "config", "deepseek_api.json")
try:
if os.path.exists(config_file):
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.api_key = config.get('api_key', '')
except Exception as e:
print(f"加载API密钥失败: {e}")
def save_api_key(self, api_key):
"""保存API密钥到本地文件"""
config_dir = os.path.join(os.path.dirname(__file__), "..", "resources", "config")
config_file = os.path.join(config_dir, "deepseek_api.json")
try:
# 确保配置目录存在
os.makedirs(config_dir, exist_ok=True)
config = {'api_key': api_key}
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
self.api_key = api_key
return True
except Exception as e:
QMessageBox.critical(self, "错误", f"保存API密钥失败: {e}")
return False
def show_api_key_dialog(self):
"""显示API密钥输入对话框"""
dialog = QDialog(self)
dialog.setWindowTitle("DeepSeek API密钥设置")
dialog.setModal(True)
dialog.setFixedSize(400, 200)
layout = QVBoxLayout()
# 说明文本
info_label = QLabel("请输入您的DeepSeek API密钥")
info_label.setWordWrap(True)
layout.addWidget(info_label)
# API密钥输入框
api_key_layout = QHBoxLayout()
api_key_label = QLabel("API密钥:")
self.api_key_input = QLineEdit()
self.api_key_input.setPlaceholderText("请输入您的DeepSeek API密钥")
self.api_key_input.setEchoMode(QLineEdit.Password)
api_key_layout.addWidget(api_key_label)
api_key_layout.addWidget(self.api_key_input)
layout.addLayout(api_key_layout)
# 按钮布局
button_layout = QHBoxLayout()
save_button = QPushButton("保存")
save_button.clicked.connect(lambda: self.save_and_close(dialog))
cancel_button = QPushButton("取消")
cancel_button.clicked.connect(dialog.reject)
button_layout.addWidget(save_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
dialog.setLayout(layout)
if dialog.exec_() == QDialog.Accepted:
return True
else:
QMessageBox.warning(self, "警告", "未设置API密钥无法使用对话功能")
return False
def save_and_close(self, dialog):
"""保存API密钥并关闭对话框"""
api_key = self.api_key_input.text().strip()
if not api_key:
QMessageBox.warning(self, "警告", "请输入有效的API密钥")
return
if self.save_api_key(api_key):
QMessageBox.information(self, "成功", "API密钥已保存")
dialog.accept()
def init_ui(self):
"""初始化用户界面"""
self.setWindowTitle("DeepSeek AI对话")
self.setMinimumSize(800, 600)
# 主布局
main_layout = QVBoxLayout()
# 标题栏容器 - 包含标题和主题切换按钮
title_container = QFrame()
title_container_layout = QHBoxLayout()
title_container_layout.setContentsMargins(0, 0, 10, 0)
# 标题
title_label = QLabel("DeepSeek AI对话助手")
title_font = QFont()
title_font.setPointSize(16)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
title_container_layout.addWidget(title_label)
title_container_layout.addStretch()
# 主题切换按钮
theme_button = QPushButton("🌓 切换主题")
theme_button.clicked.connect(self.toggle_theme)
theme_button.setFixedSize(120, 32)
theme_button.setToolTip("点击切换深色/浅色主题")
title_container_layout.addWidget(theme_button)
title_container.setLayout(title_container_layout)
main_layout.addWidget(title_container)
# 分割器
splitter = QSplitter(Qt.Vertical)
# 对话显示区域
self.create_conversation_area(splitter)
# 输入区域
self.create_input_area(splitter)
splitter.setSizes([400, 200])
main_layout.addWidget(splitter)
# 状态栏
status_layout = QHBoxLayout()
self.status_label = QLabel("就绪")
self.status_label.setObjectName("status_label")
status_layout.addWidget(self.status_label)
status_layout.addStretch()
# API密钥管理按钮
api_key_button = QPushButton("🔑 管理API密钥")
api_key_button.clicked.connect(self.manage_api_key)
api_key_button.setFixedSize(120, 32)
status_layout.addWidget(api_key_button)
main_layout.addLayout(status_layout)
self.setLayout(main_layout)
# 设置样式
self.apply_theme() # 使用主题管理器自动检测主题
# 连接信号
self.streaming_finished.connect(self.on_streaming_finished)
def toggle_theme(self):
"""切换黑白主题 - 使用主题管理器"""
try:
from .ui.theme_manager import theme_manager
except ImportError:
# 处理直接运行的情况
from ui.theme_manager import theme_manager
# 切换主题
current_is_dark = theme_manager.is_dark_theme()
theme_manager.set_dark_theme(not current_is_dark)
# 重新应用主题
self.apply_theme()
# 更新对话显示
self.rebuild_conversation_display()
def apply_theme(self, is_dark=None):
"""应用主题样式 - 与主题管理器同步"""
try:
from .ui.theme_manager import theme_manager
except ImportError:
# 处理直接运行的情况
from ui.theme_manager import theme_manager
if is_dark is None:
is_dark = theme_manager.is_dark_theme()
# 获取主题管理器的样式表
base_stylesheet = theme_manager.get_theme_stylesheet(is_dark)
# 添加对话框特定的样式优化
dialog_stylesheet = base_stylesheet + f"""
/* DeepSeek对话框特定样式 */
QDialog {{
background-color: {'#1c1c1e' if is_dark else '#ffffff'};
}}
/* 标题栏优化 */
QLabel {{
padding: 15px;
background-color: {'#2c2c2e' if is_dark else '#f8f8f8'};
border-bottom: 1px solid {'#3a3a3c' if is_dark else '#e0e0e0'};
font-size: 18px;
font-weight: 600;
color: {'#f0f0f0' if is_dark else '#333333'};
}}
/* 对话区域优化 */
QTextEdit#conversation_text {{
background-color: {'#121212' if is_dark else '#ffffff'};
border: 1px solid {'#3a3a3c' if is_dark else '#e0e0e0'};
border-radius: 12px;
padding: 20px;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 14px;
line-height: 1.6;
color: {'#e8e8ed' if is_dark else '#333333'};
}}
/* 输入框优化 */
QTextEdit#input_edit {{
background-color: {'#2c2c2e' if is_dark else '#f8f8f8'};
border: 1px solid {'#3a3a3c' if is_dark else '#e0e0e0'};
border-radius: 12px;
padding: 15px;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 14px;
color: {'#e8e8ed' if is_dark else '#333333'};
}}
QTextEdit#input_edit:focus {{
border: 2px solid {'#0a84ff' if is_dark else '#0078d7'};
}}
/* 按钮优化 */
QPushButton {{
border-radius: 8px;
padding: 10px 20px;
font-weight: 500;
font-size: 13px;
min-width: 80px;
background-color: {'#3a3a3c' if is_dark else '#f0f0f0'};
color: {'#f0f0f0' if is_dark else '#333333'};
border: 1px solid {'#4a4a4c' if is_dark else '#d0d0d0'};
}}
QPushButton:hover {{
background-color: {'#4a4a4c' if is_dark else '#e0e0e0'};
}}
QPushButton:pressed {{
background-color: {'#5a5a5c' if is_dark else '#d0d0d0'};
}}
QPushButton:default {{
background-color: #0a84ff;
color: #ffffff;
border: 1px solid #0a84ff;
}}
QPushButton:default:hover {{
background-color: #0066cc;
border: 1px solid #0066cc;
}}
QPushButton:default:pressed {{
background-color: #004d99;
border: 1px solid #004d99;
}}
/* 滚动区域优化 */
QScrollArea {{
border: none;
background-color: transparent;
}}
/* 状态栏优化 */
QLabel#status_label {{
color: {'#a0a0a0' if is_dark else '#666666'};
font-size: 12px;
padding: 8px;
}}
"""
self.setStyleSheet(dialog_stylesheet)
def create_conversation_area(self, parent):
"""创建对话显示区域"""
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
conversation_widget = QWidget()
conversation_layout = QVBoxLayout()
# 对话显示文本框
self.conversation_text = QTextEdit()
self.conversation_text.setReadOnly(True)
self.conversation_text.setFont(QFont("Microsoft YaHei", 10))
conversation_layout.addWidget(self.conversation_text)
conversation_widget.setLayout(conversation_layout)
scroll_area.setWidget(conversation_widget)
parent.addWidget(scroll_area)
def create_input_area(self, parent):
"""创建输入区域"""
input_widget = QWidget()
input_layout = QVBoxLayout()
# 输入框
input_label = QLabel("输入您的问题:")
self.input_edit = QTextEdit()
self.input_edit.setMaximumHeight(80)
self.input_edit.setPlaceholderText("请输入您的问题...")
# 按钮布局
button_layout = QHBoxLayout()
send_button = QPushButton("发送")
send_button.clicked.connect(self.send_message)
clear_button = QPushButton("清空对话")
clear_button.clicked.connect(self.clear_conversation)
button_layout.addWidget(send_button)
button_layout.addWidget(clear_button)
button_layout.addStretch()
input_layout.addWidget(input_label)
input_layout.addWidget(self.input_edit)
input_layout.addLayout(button_layout)
input_widget.setLayout(input_layout)
parent.addWidget(input_widget)
def send_message(self):
"""发送消息到DeepSeek API流式输出"""
if not self.api_key:
QMessageBox.warning(self, "警告", "请先设置API密钥")
self.show_api_key_dialog()
return
message = self.input_edit.toPlainText().strip()
if not message:
QMessageBox.warning(self, "警告", "请输入消息内容")
return
# 禁用发送按钮
self.input_edit.setEnabled(False)
self.status_label.setText("正在发送消息...")
try:
# 添加用户消息到对话历史
self.add_message_to_conversation("用户", message)
# 开始流式输出AI回复
self.start_streaming_response(message)
except Exception as e:
error_msg = f"发送消息失败: {str(e)}"
self.add_message_to_conversation("系统", error_msg)
self.status_label.setText("发送失败")
QMessageBox.critical(self, "错误", error_msg)
finally:
# 重新启用发送按钮
self.input_edit.setEnabled(True)
def start_streaming_response(self, message):
"""开始流式输出AI回复"""
# 清空输入框
self.input_edit.clear()
# 开始流式输出
self.current_streaming_content = ""
self.is_streaming = True
self.status_label.setText("正在接收AI回复...")
# 添加AI助手的初始消息占位符
self.add_streaming_message_start()
# 在新线程中执行流式请求
self.streaming_thread = threading.Thread(target=self.call_deepseek_api_stream, args=(message,))
self.streaming_thread.daemon = True
self.streaming_thread.start()
# 启动定时器更新显示
self.streaming_timer = QTimer()
self.streaming_timer.timeout.connect(self.update_streaming_display)
self.streaming_timer.start(100) # 每100毫秒更新一次显示
def add_streaming_message_start(self):
"""添加流式消息的开始部分"""
# 创建AI助手的消息占位符
self.streaming_message_id = f"ai_message_{len(self.conversation_history)}"
# 添加到对话历史
self.conversation_history.append({"sender": "AI助手", "message": "", "streaming": True})
# 在对话区域添加占位符
cursor = self.conversation_text.textCursor()
cursor.movePosition(QTextCursor.End)
self.conversation_text.setTextCursor(cursor)
# 添加AI助手的消息框架 - 优化样式
try:
from .ui.theme_manager import theme_manager
except ImportError:
from ui.theme_manager import theme_manager
is_dark = theme_manager.is_dark_theme()
# 使用与普通消息一致的样式
bg_color = "#3a3a3c" if is_dark else "#f0f0f0"
text_color = "#e8e8ed" if is_dark else "#333333"
box_shadow = "0 2px 8px rgba(0, 0, 0, 0.1);" if not is_dark else "0 2px 8px rgba(0, 0, 0, 0.3);"
self.conversation_text.insertHtml(
f'<div id="{self.streaming_message_id}" style="'
f'background-color: {bg_color}; '
f'color: {text_color}; '
f'padding: 16px 20px; '
f'margin: 12px 0; '
f'border-radius: 18px; '
f'max-width: 85%; '
f'margin-left: 0; margin-right: auto; '
f'word-wrap: break-word; '
f'line-height: 1.6; '
f'box-shadow: {box_shadow}'
f'">'
f'<div style="'
f'font-weight: 600; '
f'margin-bottom: 8px; '
f'font-size: 14px; '
f'opacity: 0.9; '
f'display: flex; '
f'align-items: center;'
f'">'
f'<span style="'
f'display: inline-block; '
f'width: 10px; '
f'height: 10px; '
f'border-radius: 50%; '
f'background-color: #ff9500; '
f'margin-right: 8px;'
f'"></span>'
f'AI助手'
f'</div>'
f'<div style="font-size: 15px;"><span style="color: #666;">正在思考...</span></div>'
f'</div>'
)
# 自动滚动到底部
self.conversation_text.ensureCursorVisible()
def update_streaming_display(self):
"""更新流式显示"""
if hasattr(self, 'current_streaming_content') and self.current_streaming_content:
# 使用更简单的方法:重新构建整个对话历史
self.rebuild_conversation_display()
# 自动滚动到底部
self.conversation_text.ensureCursorVisible()
def rebuild_conversation_display(self):
"""重新构建对话显示 - 优化主题适配"""
try:
from .ui.theme_manager import theme_manager
except ImportError:
# 处理直接运行的情况
from ui.theme_manager import theme_manager
is_dark = theme_manager.is_dark_theme()
html_content = ""
for i, msg in enumerate(self.conversation_history):
sender = msg["sender"]
message = msg["message"]
is_streaming = msg.get("streaming", False)
# 根据发送者和主题设置不同的样式 - 优化颜色对比度和阴影效果
if sender == "用户":
bg_color = "#0a84ff" # 统一的用户消息颜色
text_color = "#ffffff"
align_style = "margin-left: auto; margin-right: 0;"
box_shadow = "0 2px 8px rgba(10, 132, 255, 0.3);" if not is_dark else "0 2px 8px rgba(10, 132, 255, 0.5);"
elif sender == "AI助手":
bg_color = "#3a3a3c" if is_dark else "#f0f0f0"
text_color = "#e8e8ed" if is_dark else "#333333"
align_style = "margin-left: 0; margin-right: auto;"
box_shadow = "0 2px 8px rgba(0, 0, 0, 0.1);" if not is_dark else "0 2px 8px rgba(0, 0, 0, 0.3);"
else: # 系统消息
bg_color = "#5d4e00" if is_dark else "#fff3cd"
text_color = "#ffd700" if is_dark else "#856404"
align_style = "margin: 0 auto;"
box_shadow = "0 2px 8px rgba(93, 78, 0, 0.2);" if not is_dark else "0 2px 8px rgba(93, 78, 0, 0.4);"
# 格式化消息内容,处理换行和特殊字符
if message:
# 将换行符转换为<br>标签
formatted_message = message.replace('\n', '<br>')
# 处理多个连续空格
formatted_message = formatted_message.replace(' ', '&nbsp;&nbsp;')
else:
formatted_message = "正在思考..." if is_streaming else ""
# 为每个消息添加唯一的ID便于调试
message_id = f"msg-{i}"
# 优化消息气泡样式 - 添加阴影、更好的边距和动画效果
html_content += f'''
<div id="{message_id}" style="
background-color: {bg_color};
color: {text_color};
padding: 16px 20px;
margin: 12px 0;
border-radius: 18px;
max-width: 85%;
{align_style}
word-wrap: break-word;
line-height: 1.6;
box-shadow: {box_shadow}
transition: all 0.3s ease;
">
<div style="
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
opacity: 0.9;
display: flex;
align-items: center;
">
<span style="
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: {'#4cd964' if sender == '用户' else '#ff9500' if sender == 'AI助手' else '#ffcc00'};
margin-right: 8px;
"></span>
{sender}
</div>
<div style="
font-size: 15px;
white-space: pre-wrap;
">{formatted_message}</div>
</div>
'''
# 添加现代滚动条样式和整体容器样式
scrollbar_style = f"""
<style>
/* 整体容器样式 */
body {{
background-color: {'#121212' if is_dark else '#ffffff'};
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
}}
/* 滚动条样式 */
::-webkit-scrollbar {{
width: 10px;
}}
::-webkit-scrollbar-track {{
background: {'#1c1c1e' if is_dark else '#f8f8f8'};
border-radius: 5px;
}}
::-webkit-scrollbar-thumb {{
background: {'#5a5a5c' if is_dark else '#c0c0c0'};
border-radius: 5px;
}}
::-webkit-scrollbar-thumb:hover {{
background: {'#6a6a6c' if is_dark else '#a0a0a0'};
}}
/* 消息进入动画 */
@keyframes fadeIn {{
from {{ opacity: 0; transform: translateY(10px); }}
to {{ opacity: 1; transform: translateY(0); }}
}}
#msg-{len(self.conversation_history)-1} {{
animation: fadeIn 0.3s ease-out;
}}
</style>
"""
# 设置HTML内容
self.conversation_text.setHtml(scrollbar_style + html_content)
def call_deepseek_api_stream(self, message):
"""调用DeepSeek API流式版本"""
url = "https://api.deepseek.com/v1/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
# 构建对话历史
messages = [{"role": "user", "content": message}]
data = {
"model": "deepseek-chat",
"messages": messages,
"stream": True, # 启用流式输出
"temperature": 0.7,
"max_tokens": 2000
}
try:
response = requests.post(url, headers=headers, json=data, stream=True, timeout=30)
if response.status_code == 200:
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
data_str = line[6:] # 去掉 'data: ' 前缀
if data_str == '[DONE]':
break
try:
data_obj = json.loads(data_str)
if 'choices' in data_obj and len(data_obj['choices']) > 0:
delta = data_obj['choices'][0].get('delta', {})
if 'content' in delta:
content = delta['content']
self.current_streaming_content += content
except json.JSONDecodeError:
pass
else:
error_msg = f"API调用失败: {response.status_code} - {response.text}"
self.current_streaming_content = f"错误: {error_msg}"
except Exception as e:
self.current_streaming_content = f"请求失败: {str(e)}"
finally:
# 流式输出结束
self.is_streaming = False
if hasattr(self, 'streaming_timer'):
self.streaming_timer.stop()
# 更新对话历史
if hasattr(self, 'current_streaming_content') and self.current_streaming_content:
# 更新对话历史中的消息
for msg in self.conversation_history:
if msg.get('streaming') and msg.get('sender') == 'AI助手':
msg['message'] = self.current_streaming_content
msg['streaming'] = False
break
# 使用信号槽机制安全地更新UI
self.streaming_finished.emit()
else:
self.status_label.setText("接收失败")
def call_deepseek_api(self, message):
"""调用DeepSeek API非流式版本保留作为备用"""
url = "https://api.deepseek.com/v1/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
# 构建对话历史
messages = [{"role": "user", "content": message}]
data = {
"model": "deepseek-chat",
"messages": messages,
"stream": False,
"temperature": 0.7,
"max_tokens": 2000
}
response = requests.post(url, headers=headers, json=data, timeout=30)
if response.status_code == 200:
result = response.json()
return result["choices"][0]["message"]["content"]
else:
error_msg = f"API调用失败: {response.status_code} - {response.text}"
raise Exception(error_msg)
def add_message_to_conversation(self, sender, message):
"""添加消息到对话显示区域"""
# 添加到对话历史
self.conversation_history.append({"sender": sender, "message": message})
# 使用新的对话显示系统
self.rebuild_conversation_display()
# 自动滚动到底部
self.conversation_text.ensureCursorVisible()
def on_streaming_finished(self):
"""流式输出完成处理"""
# 安全地更新UI在主线程中执行
self.rebuild_conversation_display()
self.status_label.setText("消息接收完成")
def clear_conversation(self):
"""清空对话历史"""
reply = QMessageBox.question(self, "确认", "确定要清空对话历史吗?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.Yes:
self.conversation_history.clear()
self.rebuild_conversation_display()
self.status_label.setText("对话已清空")
def manage_api_key(self):
"""管理API密钥"""
self.show_api_key_dialog()
def closeEvent(self, event):
"""关闭事件处理"""
# 发出关闭信号
self.closed.emit()
if self.parent:
# 通知父窗口对话窗口已关闭
if hasattr(self.parent, 'on_deepseek_dialog_closed'):
self.parent.on_deepseek_dialog_closed()
event.accept()

@ -23,6 +23,8 @@ class FileParser:
return FileParser.parse_docx(file_path)
elif ext == '.pdf':
return FileParser.parse_pdf(file_path)
elif ext == '.html':
return FileParser.parse_html(file_path)
else:
raise ValueError(f"Unsupported file format: {ext}")
except Exception as e:
@ -80,6 +82,11 @@ class FileParser:
content = FileParser.parse_pdf(file_path)
images = [] # PDF图片提取较复杂暂时跳过
elif ext == '.html':
# HTML文件提取文本内容
content = FileParser.parse_html(file_path)
images = [] # HTML图片提取较复杂暂时跳过
else:
return {
'success': False,
@ -271,6 +278,46 @@ class FileParser:
except Exception as e:
raise Exception(f"Error parsing pdf file {file_path}: {str(e)}")
@staticmethod
def parse_html(file_path: str) -> str:
"""解析HTML文件提取文本内容"""
# 验证文件路径
if not FileParser.validate_file_path(file_path):
raise ValueError(f"Invalid file path: {file_path}")
try:
from bs4 import BeautifulSoup
except ImportError:
raise ImportError("BeautifulSoup4 library is required for parsing .html files. Please install it using 'pip install beautifulsoup4'")
try:
# 检测文件编码
encoding = FileParser.detect_file_encoding(file_path)
# 读取HTML文件
with open(file_path, 'r', encoding=encoding, errors='ignore') as f:
html_content = f.read()
# 使用BeautifulSoup解析HTML
soup = BeautifulSoup(html_content, 'html.parser')
# 移除script和style标签
for script in soup(["script", "style"]):
script.decompose()
# 提取文本内容
text = soup.get_text()
# 清理多余的空白字符
lines = (line.strip() for line in text.splitlines())
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
text = '\n'.join(chunk for chunk in chunks if chunk)
return text
except Exception as e:
raise Exception(f"Error parsing html file {file_path}: {str(e)}")
@staticmethod
def validate_file_path(file_path: str) -> bool:
"""验证文件路径是否有效"""

@ -4,17 +4,25 @@ import os
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QLabel, QFrame, QMenuBar,
QAction, QFileDialog, QMessageBox, QApplication,
QSplitter, QScrollArea, QStatusBar, QProgressBar)
QSplitter, QScrollArea, QStatusBar, QProgressBar, QTextBrowser, QSizePolicy,
QListWidget, QListWidgetItem, QDialog, QGraphicsScene, QGraphicsView)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect
from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor
from src.ui.components import CustomTitleBar, TextDisplayWidget
from src.typing_logic import TypingLogic
from src.file_parser import FileParser
from src.ui.theme_manager import theme_manager
# 修复导入路径
from ui.components import CustomTitleBar, TextDisplayWidget
from typing_logic import TypingLogic
from file_parser import FileParser
from ui.theme_manager import theme_manager
import tempfile
import hashlib
class LearningModeWindow(QMainWindow):
def __init__(self, parent=None, imported_content="", current_position=0):
# 定义内容变化信号
content_changed = pyqtSignal(str, int) # 参数:内容,位置
# 定义关闭信号
closed = pyqtSignal()
def __init__(self, parent=None, imported_content="", current_position=0, image_data=None, image_positions=None):
"""
学习模式窗口
- 顶部显示UI.png图片
@ -25,13 +33,18 @@ class LearningModeWindow(QMainWindow):
parent: 父窗口
imported_content: 从主窗口传递的导入内容
current_position: 当前学习进度位置
image_data: 图片数据字典 {文件名: 二进制数据}
image_positions: 图片位置信息列表
"""
super().__init__(parent)
self.parent_window = parent
self.imported_content = imported_content
self.current_position = current_position
self.image_data = image_data or {}
self.image_positions = image_positions or []
self.typing_logic = None
self.is_loading_file = False
self.extracted_images = [] # 用于存储提取的图片数据
# 初始化UI
self.initUI()
@ -39,6 +52,9 @@ class LearningModeWindow(QMainWindow):
# 初始化打字逻辑
self.init_typing_logic()
# 初始化同步位置跟踪
self.last_sync_position = current_position
# 如果有导入内容,初始化显示
if self.imported_content:
self.initialize_with_imported_content()
@ -53,6 +69,12 @@ class LearningModeWindow(QMainWindow):
# 重置打字逻辑
if self.typing_logic:
self.typing_logic.reset(self.imported_content)
# 设置图片数据到打字逻辑
if self.image_data:
self.typing_logic.set_image_data(self.image_data)
if self.image_positions:
self.typing_logic.set_image_positions(self.image_positions)
# 显示已学习的内容
display_text = self.imported_content[:self.current_position]
@ -137,38 +159,48 @@ class LearningModeWindow(QMainWindow):
ui_image_path = os.path.join(os.path.dirname(__file__), 'ui', 'UI.png')
if os.path.exists(ui_image_path):
pixmap = QPixmap(ui_image_path)
# 保存原始图片尺寸
self.original_pixmap = pixmap
# 设置图片完全铺满标签
self.image_label.setPixmap(pixmap)
self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签
# 设置图片标签的尺寸策略,使其可以扩展
from PyQt5.QtWidgets import QSizePolicy
self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# 设置图片区域的最小高度为图片高度的1/3确保图片可见
min_height = max(200, pixmap.height() // 3)
self.image_label.setMinimumHeight(min_height)
# 重新设置窗口大小以适配图片
self.resize(pixmap.width(), self.height())
if not pixmap.isNull():
# 保存原始图片用于缩放计算
self.original_pixmap = pixmap
# 设置图片到标签
self.image_label.setPixmap(pixmap)
self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签
# 设置大小策略
self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# 计算合适的最小高度(保持图片比例)
window_width = 900 # 默认窗口宽度
original_width = pixmap.width()
original_height = pixmap.height()
if original_width > 0:
min_height = int(window_width * original_height / original_width)
self.image_label.setMinimumHeight(min_height)
else:
self.image_label.setMinimumHeight(200)
else:
self.image_label.setText("UI图片加载失败")
self.image_label.setStyleSheet("""
QLabel {
background-color: #f8f9fa;
color: #666666;
font-size: 14px;
qproperty-alignment: AlignCenter;
}
""")
else:
self.image_label.setText("UI图片未找到")
self.image_label.setStyleSheet("""
QLabel {
background-color: #f8f9fa;
border: none;
color: #666666;
font-size: 14px;
padding: 20px;
qproperty-alignment: AlignCenter;
}
""")
self.image_label.setMinimumHeight(200)
# 直接添加图片标签到主布局,不使用滚动区域
# 添加到主布局
main_layout.addWidget(self.image_label)
def resizeEvent(self, event):
@ -203,6 +235,7 @@ class LearningModeWindow(QMainWindow):
创建输入区域
- 创建文本显示组件
- 设置与主系统相同的样式
- 创建图片列表区域
"""
# 创建文本显示组件(复用主系统的组件)
self.text_display_widget = TextDisplayWidget(self)
@ -219,7 +252,40 @@ class LearningModeWindow(QMainWindow):
# 连接文本变化信号
self.text_display_widget.text_display.textChanged.connect(self.on_text_changed)
main_layout.addWidget(self.text_display_widget, 1) # 占据剩余空间
# 创建图片显示区域
self.image_list_widget = QListWidget()
self.image_list_widget.setMaximumHeight(150)
self.image_list_widget.setStyleSheet("""
QListWidget {
background-color: #f8f8f8;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 11px;
}
QListWidget::item {
padding: 5px;
border-bottom: 1px solid #e0e0e0;
}
QListWidget::item:selected {
background-color: #e3f2fd;
color: #1976d2;
}
""")
self.image_list_widget.setVisible(False) # 默认隐藏
self.image_list_widget.itemDoubleClicked.connect(self.on_image_item_double_clicked)
# 创建布局容器
input_container = QWidget()
input_layout = QVBoxLayout()
input_layout.setContentsMargins(0, 0, 0, 0)
input_layout.setSpacing(5)
input_layout.addWidget(self.text_display_widget, 1) # 文本显示区域占据剩余空间
input_layout.addWidget(self.image_list_widget) # 图片列表区域
input_container.setLayout(input_layout)
main_layout.addWidget(input_container, 1) # 占据剩余空间
def create_menu_bar(self):
"""
@ -294,38 +360,93 @@ class LearningModeWindow(QMainWindow):
)
if file_path:
self.is_loading_file = True
try:
self.is_loading_file = True
# 使用文件解析器
parser = FileParser()
content = parser.parse_file(file_path)
# 获取文件扩展名
_, ext = os.path.splitext(file_path)
ext = ext.lower()
if content:
# 存储导入的内容
self.imported_content = content
self.current_position = 0
# 重置打字逻辑
if self.typing_logic:
self.typing_logic.reset(content)
# 清空文本显示
self.text_display_widget.text_display.clear()
# 更新状态
self.status_label.setText(f"已导入: {os.path.basename(file_path)}")
self.progress_label.setText(f"进度: 0% (0/{len(content)} 字符)")
# 显示成功消息
QMessageBox.information(self, "导入成功",
f"文件导入成功!\n文件: {os.path.basename(file_path)}\n字符数: {len(content)}\n\n开始打字以显示学习内容。")
# 对于docx文件直接解析而不转换为txt
if ext == '.docx':
# 直接解析docx文件内容
content = FileParser.parse_docx(file_path)
# 提取图片数据
images = FileParser.extract_images_from_docx(file_path)
else:
QMessageBox.warning(self, "导入失败", "无法解析文件内容,请检查文件格式。")
# 其他文件类型使用原来的转换方法
result = FileParser.parse_and_convert_to_txt(file_path)
content = result['content']
images = result.get('images', [])
if not content:
QMessageBox.warning(self, "导入失败", "文件内容为空或解析失败!")
return
# 保存导入的内容
self.imported_content = content
self.current_position = 0
# 保存提取的图片数据
self.extracted_images = images
# 设置打字逻辑
if self.typing_logic:
self.typing_logic.reset(content)
# 如果有图片,设置图片数据到打字逻辑
if images:
image_data_dict = {}
image_positions = []
# 为每张图片生成位置信息 - 改进位置计算逻辑
for i, (filename, image_data) in enumerate(images):
image_data_dict[filename] = image_data
# 改进图片位置计算,确保图片能在用户早期打字时显示
content_length = len(content)
if content_length == 0:
content_length = 1000 # 备用长度
if len(images) == 1:
# 只有一张图片放在文档开始位置附近前10%),确保用户能快速看到
image_pos = max(10, content_length // 10)
else:
# 多张图片:前几张放在较前位置,确保用户能看到
if i < 3:
# 前3张图片放在文档前30%
segment = content_length // 3
image_pos = max(10, segment * (i + 1) // 4)
else:
# 其余图片均匀分布
remaining_start = content_length // 2
remaining_index = i - 3
remaining_count = len(images) - 3
if remaining_count > 0:
segment = (content_length - remaining_start) // (remaining_count + 1)
image_pos = remaining_start + segment * (remaining_index + 1)
else:
image_pos = content_length // 2
image_positions.append({
'start_pos': image_pos,
'end_pos': image_pos + 50, # 图片占位符长度
'filename': filename
})
# 设置图片数据到打字逻辑
self.typing_logic.set_image_data(image_data_dict)
self.typing_logic.set_image_positions(image_positions)
# 显示图片列表
self.display_image_list(images)
# 显示初始内容(空)
self.text_display_widget.text_display.clear()
self.status_label.setText("已导入文件,请开始打字学习...")
self.progress_label.setText("进度: 0%")
except Exception as e:
QMessageBox.critical(self, "导入错误", f"导入文件时出错:\n{str(e)}")
QMessageBox.critical(self, "导入错误", f"导入文件时发生错误:\n{str(e)}")
finally:
self.is_loading_file = False
@ -335,6 +456,8 @@ class LearningModeWindow(QMainWindow):
文本变化处理
- 根据导入的内容逐步显示
- 更新学习进度
- 同步内容到打字模式
- 处理图片插入
"""
# 如果正在加载文件,跳过处理
if self.is_loading_file:
@ -382,6 +505,7 @@ class LearningModeWindow(QMainWindow):
self.status_label.setText(f"输入错误!期望字符: '{result.get('expected', '')}'")
else:
# 输入正确,更新进度
old_position = self.current_position
self.current_position = len(current_text)
progress = (self.current_position / len(self.imported_content)) * 100
@ -389,6 +513,17 @@ class LearningModeWindow(QMainWindow):
f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)"
)
# 只在用户新输入的字符上同步到打字模式
if self.parent_window:
# 获取用户这一轮新输入的字符(与上一轮相比的新内容)
if old_position < self.current_position:
new_input = expected_text[old_position:self.current_position]
if new_input: # 只有新输入内容时才同步
# 只同步新输入的内容,不传递整个文本
self.content_changed.emit(new_input, len(new_input))
# 检查是否完成
if result.get('completed', False):
self.status_label.setText("恭喜!学习完成!")
@ -396,6 +531,8 @@ class LearningModeWindow(QMainWindow):
else:
self.status_label.setText("继续输入以显示更多内容...")
def show_about(self):
"""
显示关于对话框
@ -406,7 +543,8 @@ class LearningModeWindow(QMainWindow):
"• 顶部显示UI界面图片\n"
"• 下方为打字输入区域\n"
"• 导入文件后逐步显示内容\n"
"• 实时显示学习进度\n\n"
"• 实时显示学习进度\n"
"• 支持图片显示\n\n"
"使用方法:\n"
"1. 点击'文件'->'导入文件'选择学习材料\n"
"2. 在下方文本区域开始打字\n"
@ -417,6 +555,9 @@ class LearningModeWindow(QMainWindow):
窗口关闭事件
- 通知父窗口学习模式已关闭
"""
# 发射关闭信号
self.closed.emit()
if self.parent_window and hasattr(self.parent_window, 'on_learning_mode_closed'):
self.parent_window.on_learning_mode_closed()
@ -431,4 +572,152 @@ class LearningModeWindow(QMainWindow):
if event.key() == Qt.Key_Escape:
self.close()
else:
super().keyPressEvent(event)
super().keyPressEvent(event)
def display_image_list(self, images):
"""
显示图片列表
"""
try:
# 清空之前的图片列表
self.image_list_widget.clear()
# 如果没有图片,隐藏图片列表区域
if not images:
self.image_list_widget.setVisible(False)
return
# 显示图片列表区域
self.image_list_widget.setVisible(True)
# 添加图片项到列表
for index, (filename, image_data) in enumerate(images):
# 创建缩略图
pixmap = QPixmap()
if pixmap.loadFromData(image_data):
# 创建缩略图
thumbnail = pixmap.scaled(60, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# 创建列表项
item = QListWidgetItem()
item.setIcon(QIcon(thumbnail))
item.setText(f"{filename} ({pixmap.width()}x{pixmap.height()})")
item.setData(Qt.UserRole, index) # 保存图片索引
self.image_list_widget.addItem(item)
else:
# 如果无法加载图片,显示默认文本
item = QListWidgetItem(f"{filename} (无法预览)")
item.setData(Qt.UserRole, index)
self.image_list_widget.addItem(item)
# 更新状态栏
self.status_label.setText(f"已提取 {len(images)} 张图片,双击查看大图")
except Exception as e:
self.status_label.setText(f"显示图片列表失败: {str(e)}")
def on_image_item_double_clicked(self, item):
"""
双击图片项时显示大图
"""
try:
# 获取图片索引
index = item.data(Qt.UserRole)
if 0 <= index < len(self.extracted_images):
image_filename, image_data = self.extracted_images[index]
self.show_image_viewer(image_filename, image_data)
except Exception as e:
self.status_label.setText(f"显示图片失败: {str(e)}")
def show_image_viewer(self, filename, image_data):
"""
显示图片查看器 - 支持缩放功能
"""
try:
# 创建自定义图片查看窗口
viewer = QDialog(self)
viewer.setWindowTitle(f"图片查看 - {filename}")
viewer.setModal(False)
# 设置窗口标志,保留标题栏以便用户可以移动和调整大小
viewer.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint)
# 设置窗口背景为黑色
viewer.setStyleSheet("""
QDialog {
background-color: #000000;
}
""")
# 创建场景和视图
scene = QGraphicsScene(viewer)
view = QGraphicsView(scene)
view.setStyleSheet("border: none;") # 移除视图边框
# 设置视图为可交互的,并启用滚动条
view.setDragMode(QGraphicsView.ScrollHandDrag)
view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
# 创建布局
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
viewer.setLayout(layout)
# 加载图片
pixmap = QPixmap()
if not pixmap.loadFromData(image_data):
self.status_label.setText(f"加载图片失败: {filename}")
return
# 将图片添加到场景
scene.addPixmap(pixmap)
# 设置视图大小和位置
if self:
parent_geometry = self.geometry()
screen_geometry = QApplication.primaryScreen().geometry()
# 设置窗口宽度与主窗口相同高度为屏幕高度的40%
window_width = parent_geometry.width()
window_height = int(screen_geometry.height() * 0.4)
# 计算位置:显示在主窗口正上方
x = parent_geometry.x()
y = parent_geometry.y() - window_height
# 确保不会超出屏幕边界
if y < screen_geometry.top():
y = parent_geometry.y() + 50 # 如果上方空间不足,显示在下方
# 调整宽度确保不超出屏幕
if x + window_width > screen_geometry.right():
window_width = screen_geometry.right() - x
viewer.setGeometry(x, y, window_width, window_height)
viewer.show()
# 设置视图适应图片大小
view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
# 重写视图的滚轮事件以支持缩放
def wheelEvent(event):
factor = 1.2
if event.angleDelta().y() > 0:
view.scale(factor, factor)
else:
view.scale(1.0/factor, 1.0/factor)
view.wheelEvent = wheelEvent
# 添加双击重置视图功能
def mouseDoubleClickEvent(event):
view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
view.mouseDoubleClickEvent = mouseDoubleClickEvent
except Exception as e:
self.status_label.setText(f"创建图片查看器失败: {str(e)}")
import traceback
traceback.print_exc()

@ -0,0 +1,153 @@
# main.py
import sys
import traceback
import os
import platform
# 添加项目根目录到Python路径
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
# 设置Qt平台插件路径 - 增强版本,完全避免平台插件问题
def set_qt_plugin_path():
"""设置Qt平台插件路径确保所有平台插件都能正确加载"""
system = platform.system()
# 获取Python版本
python_version = f"python{sys.version_info.major}.{sys.version_info.minor}"
# 可能的Qt插件路径列表
possible_paths = []
if system == "Windows":
# Windows环境下的路径
possible_paths.extend([
os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'Python', 'Python39', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
])
elif system == "Darwin": # macOS
# macOS环境下的路径
possible_paths.extend([
os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
'/usr/local/opt/qt5/plugins', # Homebrew Qt5
'/opt/homebrew/opt/qt5/plugins', # Apple Silicon Homebrew
os.path.expanduser('~/Qt/5.15.2/clang_64/plugins'), # Qt官方安装
])
elif system == "Linux":
# Linux环境下的路径
possible_paths.extend([
os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
'/usr/lib/x86_64-linux-gnu/qt5/plugins',
'/usr/lib/qt5/plugins',
])
# 查找第一个存在的路径
valid_path = None
for path in possible_paths:
if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')):
valid_path = path
break
if valid_path:
# 设置Qt插件路径
os.environ['QT_PLUGIN_PATH'] = valid_path
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(valid_path, 'platforms')
# 设置平台特定的环境变量
if system == "Darwin": # macOS
os.environ['QT_QPA_PLATFORM'] = 'cocoa'
os.environ['QT_MAC_WANTS_LAYER'] = '1'
# 禁用可能导致问题的Qt功能
os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' # 禁用Qt警告日志
elif system == "Windows":
os.environ['QT_QPA_PLATFORM'] = 'windows'
elif system == "Linux":
os.environ['QT_QPA_PLATFORM'] = 'xcb'
# 对于Linux可能需要设置DISPLAY
if 'DISPLAY' not in os.environ:
os.environ['DISPLAY'] = ':0'
print(f"✅ Qt插件路径设置成功: {valid_path}")
return True
else:
print("⚠️ 警告未找到Qt插件路径")
return False
# 设置Qt平台插件路径
set_qt_plugin_path()
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
from word_main_window import WordStyleMainWindow
# 设置高DPI支持必须在QApplication创建之前
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
def main():
"""应用程序入口函数 - Word风格版本"""
try:
# 创建QApplication实例
app = QApplication(sys.argv)
# 在macOS上使用系统原生样式在其他平台上使用WindowsVista样式
if platform.system() != "Darwin": # 不是macOS系统
# 设置应用程序样式为Windows风格更接近Word界面
app.setStyle('WindowsVista')
# 设置应用程序属性
app.setApplicationName("MagicWord")
app.setApplicationVersion("1.0.0")
app.setOrganizationName("MagicWord")
# 设置窗口图标(如果存在)
icon_files = [
'app_icon_32*32.png',
'app_icon_64*64.png',
'app_icon_128*128.png',
'app_icon_256*256.png',
'app_icon.png'
]
icon_path = None
for icon_file in icon_files:
test_path = os.path.join(project_root, 'resources', 'icons', icon_file)
if os.path.exists(test_path):
icon_path = test_path
break
if icon_path and os.path.exists(icon_path):
app.setWindowIcon(QIcon(icon_path))
else:
# 使用默认图标
app.setWindowIcon(QIcon())
# 创建MarkText风格的主窗口基于MarkText架构
try:
from main import MarkTextMainWindow
main_window = MarkTextMainWindow()
print("✅ 已启动MarkText风格编辑器")
except ImportError as e:
print(f"⚠️ MarkText编辑器导入失败: {e}回退到Word风格")
# 回退到Word风格的主窗口
main_window = WordStyleMainWindow()
main_window.show()
# 启动事件循环并返回退出码
exit_code = app.exec_()
sys.exit(exit_code)
except Exception as e:
# 打印详细的错误信息
print(f"应用程序发生未捕获的异常: {e}")
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

@ -1,482 +1,9 @@
import sys
import os
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, QAction,
QFileDialog, QVBoxLayout, QWidget, QLabel, QStatusBar, QMessageBox)
from PyQt5.QtGui import QFont, QTextCharFormat, QColor, QTextCursor
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
# 添加项目根目录到Python路径
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
# 导入自定义UI组件
from src.ui.components import CustomTitleBar, ProgressBarWidget, TextDisplayWidget, StatsDisplayWidget, QuoteDisplayWidget, WeatherDisplayWidget
from src.file_parser import FileParser
from src.typing_logic import TypingLogic
from src.services.network_service import NetworkService
class WeatherFetchThread(QThread):
"""天气信息获取线程"""
weather_fetched = pyqtSignal(object) # 天气信息获取成功信号
error_occurred = pyqtSignal(str) # 错误发生信号
def __init__(self):
super().__init__()
self.network_service = NetworkService()
def run(self):
try:
weather_info = self.network_service.get_weather_info()
if weather_info:
# 格式化天气信息
formatted_info = (
f"天气: {weather_info['city']} - "
f"{weather_info['description']} - "
f"温度: {weather_info['temperature']}°C - "
f"湿度: {weather_info['humidity']}% - "
f"风速: {weather_info['wind_speed']} m/s"
)
self.weather_fetched.emit(formatted_info)
else:
self.error_occurred.emit("无法获取天气信息")
except Exception as e:
self.error_occurred.emit(f"获取天气信息时出错: {str(e)}")
class MainWindow(QMainWindow):
def __init__(self):
def on_learning_mode_closed(self):
"""
初始化主窗口
- 设置窗口标题为"隐私学习软件 - 仿Word"
- 设置窗口大小为800x600
- 初始化学习内容存储变量
- 初始化当前输入位置
- 调用initUI()方法
学习模式窗口关闭回调
- 清除学习窗口引用
- 更新菜单状态
"""
super().__init__()
self.learning_content = ""
self.current_position = 0
self.typing_logic = None
self.text_edit = None
self.status_bar = None
self.title_bar = None
self.progress_bar_widget = None
self.text_display_widget = None
self.initUI()
def initUI(self):
"""
创建和布局所有UI组件
- 创建自定义标题栏
- 创建文本显示组件
- 调用createMenuBar()创建菜单
- 创建状态栏并显示"就绪"
"""
# 设置窗口属性
self.setWindowTitle("隐私学习软件 - 仿Word")
self.setGeometry(100, 100, 800, 600)
self.setWindowFlags(Qt.FramelessWindowHint) # 移除默认标题栏
# 创建中央widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建主布局
main_layout = QVBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
central_widget.setLayout(main_layout)
# 创建自定义标题栏
self.title_bar = CustomTitleBar(self)
main_layout.addWidget(self.title_bar)
# 创建统计信息显示组件(默认隐藏)
self.stats_display = StatsDisplayWidget(self)
self.stats_display.setVisible(False) # 默认隐藏
main_layout.addWidget(self.stats_display)
# 创建每日一言显示组件(默认隐藏)
self.quote_display = QuoteDisplayWidget(self)
self.quote_display.setVisible(False) # 默认隐藏
main_layout.addWidget(self.quote_display)
# 创建天气显示组件(默认隐藏)
self.weather_display = WeatherDisplayWidget(self)
self.weather_display.setVisible(False) # 默认隐藏
main_layout.addWidget(self.weather_display)
# 创建文本显示组件
self.text_display_widget = TextDisplayWidget(self)
main_layout.addWidget(self.text_display_widget)
# 连接文本显示组件的文本变化信号
self.text_display_widget.text_display.textChanged.connect(self.onTextChanged)
# 创建菜单栏
self.createMenuBar()
# 创建状态栏
self.status_bar = self.statusBar()
self.status_bar.showMessage("就绪")
def createTopFunctionArea(self, main_layout):
"""
创建顶部功能区域
- 显示准确率WPM等统计信息
- 显示每日一言功能
"""
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton
from PyQt5.QtCore import Qt
# 创建顶部功能区域widget
top_widget = QWidget()
top_widget.setStyleSheet("""
QWidget {
background-color: #f0f0f0;
border-bottom: 1px solid #d0d0d0;
}
""")
# 创建水平布局
top_layout = QHBoxLayout()
top_layout.setContentsMargins(10, 5, 10, 5)
top_layout.setSpacing(15)
# 创建统计信息标签
self.wpm_label = QLabel("WPM: 0")
self.accuracy_label = QLabel("准确率: 0%")
self.quote_label = QLabel("每日一言: 暂无")
self.quote_label.setStyleSheet("QLabel { color: #666666; font-style: italic; }")
# 设置标签样式
label_style = "font-size: 12px; font-weight: normal; color: #333333;"
self.wpm_label.setStyleSheet(label_style)
self.accuracy_label.setStyleSheet(label_style)
# 创建每日一言刷新按钮
self.refresh_quote_button = QPushButton("刷新")
self.refresh_quote_button.setStyleSheet("""
QPushButton {
background-color: #0078d7;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.refresh_quote_button.clicked.connect(self.refresh_daily_quote)
# 添加组件到布局
top_layout.addWidget(self.wpm_label)
top_layout.addWidget(self.accuracy_label)
top_layout.addStretch()
top_layout.addWidget(self.quote_label)
top_layout.addWidget(self.refresh_quote_button)
top_widget.setLayout(top_layout)
main_layout.addWidget(top_widget)
def createMenuBar(self):
"""
创建菜单栏和所有菜单项
- 文件菜单打开(Ctrl+O)保存(Ctrl+S)退出(Ctrl+Q)
- 视图菜单显示统计信息显示每日一言
- 帮助菜单关于
- 为每个菜单项连接对应的槽函数
"""
menu_bar = self.menuBar()
# 文件菜单
file_menu = menu_bar.addMenu('文件')
# 打开动作
open_action = QAction('打开', self)
open_action.setShortcut('Ctrl+O')
open_action.triggered.connect(self.openFile)
file_menu.addAction(open_action)
# 保存动作
save_action = QAction('保存', self)
save_action.setShortcut('Ctrl+S')
save_action.triggered.connect(self.saveFile)
file_menu.addAction(save_action)
# 分隔线
file_menu.addSeparator()
# 退出动作
exit_action = QAction('退出', self)
exit_action.setShortcut('Ctrl+Q')
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 视图菜单
view_menu = menu_bar.addMenu('视图')
# 显示统计信息动作
self.stats_action = QAction('显示统计信息', self)
self.stats_action.setCheckable(True)
self.stats_action.setChecked(True)
self.stats_action.triggered.connect(self.toggleStatsDisplay)
view_menu.addAction(self.stats_action)
# 显示每日一言动作
self.quote_action = QAction('显示每日一言', self)
self.quote_action.setCheckable(True)
self.quote_action.setChecked(True)
self.quote_action.triggered.connect(self.toggleQuoteDisplay)
view_menu.addAction(self.quote_action)
# 显示天气信息动作
self.weather_action = QAction('显示天气', self)
self.weather_action.setCheckable(True)
self.weather_action.setChecked(True)
self.weather_action.triggered.connect(self.toggleWeatherDisplay)
view_menu.addAction(self.weather_action)
# 帮助菜单
help_menu = menu_bar.addMenu('帮助')
# 关于动作
about_action = QAction('关于', self)
about_action.triggered.connect(self.showAbout)
help_menu.addAction(about_action)
def toggleStatsDisplay(self, checked):
"""
切换统计信息显示
- checked: 是否显示统计信息
"""
self.stats_display.setVisible(checked)
def toggleQuoteDisplay(self, checked):
"""
切换每日一言显示
- checked: 是否显示每日一言
"""
self.quote_display.setVisible(checked)
# 如果启用显示且quote为空则刷新一次
if checked and not self.quote_display.quote_label.text():
self.refresh_daily_quote()
def toggleWeatherDisplay(self, checked):
"""切换天气信息显示"""
self.weather_display.setVisible(checked)
# 如果启用显示且天气信息为空,则刷新一次
if checked and not self.weather_display.weather_label.text():
self.refresh_weather_info()
def openFile(self):
"""
打开文件选择对话框并加载选中的文件
- 显示文件选择对话框过滤条件*.txt, *.docx, *.pdf
- 如果用户选择了文件调用FileParser.parse_file(file_path)
- 成功时将内容存储但不直接显示重置打字状态
- 失败时显示错误消息框
"""
options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName(
self,
"打开文件",
"",
"文本文件 (*.txt);;Word文档 (*.docx);;PDF文件 (*.pdf);;所有文件 (*)",
options=options
)
if file_path:
try:
# 解析文件内容
content = FileParser.parse_file(file_path)
self.learning_content = content
# 在文本显示组件中设置内容(初始为空,通过打字逐步显示)
if self.text_display_widget:
self.text_display_widget.set_text(content) # 设置文件内容
# 重置打字状态
self.typing_logic = TypingLogic(content)
self.current_position = 0
# 更新状态栏
self.status_bar.showMessage(f"已打开文件: {file_path},开始打字以显示内容")
except Exception as e:
# 显示错误消息框
QMessageBox.critical(self, "错误", f"无法打开文件:\n{str(e)}")
def saveFile(self):
"""
保存当前内容到文件
- 显示保存文件对话框
- 将文本区域内容写入选定文件
- 返回操作结果
"""
options = QFileDialog.Options()
file_path, _ = QFileDialog.getSaveFileName(
self,
"保存文件",
"",
"文本文件 (*.txt);;所有文件 (*)",
options=options
)
if file_path:
try:
# 获取文本编辑区域的内容
content = self.text_edit.toPlainText()
# 写入文件
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
# 更新状态栏
self.status_bar.showMessage(f"文件已保存: {file_path}")
return True
except Exception as e:
# 显示错误消息框
QMessageBox.critical(self, "错误", f"无法保存文件:\n{str(e)}")
return False
return False
def showAbout(self):
"""
显示关于对话框
- 显示消息框包含软件名称版本描述
"""
QMessageBox.about(
self,
"关于",
"隐私学习软件 - 仿Word\n\n"
"版本: 1.0\n\n"
"这是一个用于隐私学习的打字练习软件,\n"
"可以加载文档并进行打字练习,\n"
"帮助提高打字速度和准确性。"
)
def refresh_daily_quote(self):
"""
刷新每日一言
- 从网络API获取名言
- 更新显示
"""
import requests
import json
from PyQt5.QtCore import Qt
from src.constants import QUOTE_API_URL
try:
# 发送请求获取每日一言
response = requests.get(QUOTE_API_URL, timeout=5)
if response.status_code == 200:
data = response.json()
quote_content = data.get('content', '暂无内容')
quote_author = data.get('author', '未知作者')
# 更新显示
self.quote_label.setText(f"每日一言: {quote_content}{quote_author}")
# 同时更新统计信息显示组件中的每日一言
if hasattr(self, 'stats_display') and self.stats_display:
self.stats_display.update_quote(f"{quote_content}{quote_author}")
else:
self.quote_label.setText("每日一言: 获取失败")
# 同时更新统计信息显示组件中的每日一言
if hasattr(self, 'stats_display') and self.stats_display:
self.stats_display.update_quote("获取失败")
except Exception as e:
self.quote_label.setText("每日一言: 获取失败")
# 同时更新统计信息显示组件中的每日一言
if hasattr(self, 'stats_display') and self.stats_display:
self.stats_display.update_quote("获取失败")
def onTextChanged(self):
"""
处理用户输入变化事件打字练习
- 获取文本显示组件中的文本
- 使用TypingLogic.check_input检查输入
- 根据结果更新文本显示组件
- 更新统计数据展示
"""
# 防止递归调用
if getattr(self, '_processing_text_change', False):
return
if not self.typing_logic:
return
# 设置标志防止递归
self._processing_text_change = True
try:
# 获取当前输入文本
current_text = self.text_display_widget.text_display.toPlainText()
# 检查输入是否正确
result = self.typing_logic.check_input(current_text)
is_correct = result["correct"]
expected_char = result["expected"]
# 更新文本显示组件
if self.text_display_widget:
# 显示用户输入反馈
self.text_display_widget.show_user_input(current_text)
# 不再高亮下一个字符,因为内容通过打字逐步显示
# 计算统计数据
stats = self.typing_logic.get_statistics()
accuracy = stats['accuracy_rate'] * 100 # 转换为百分比
# 可以根据需要添加更多统计数据的计算
wpm = 0 # 暂时设置为0后续可以实现WPM计算
# 更新状态栏
self.status_bar.showMessage(f"WPM: {wpm:.1f} | 准确率: {accuracy:.1f}%")
# 更新统计信息显示组件
if hasattr(self, 'stats_display') and self.stats_display.isVisible():
self.stats_display.update_stats(int(wpm), accuracy)
# 更新每日一言显示组件(如果需要)
if hasattr(self, 'quote_display') and self.quote_display.isVisible() and not self.quote_display.quote_label.text():
self.refresh_daily_quote()
# 更新顶部功能区的统计数据(如果仍然存在)
if hasattr(self, 'wpm_label') and self.wpm_label:
self.wpm_label.setText(f"WPM: {wpm:.1f}")
if hasattr(self, 'accuracy_label') and self.accuracy_label:
self.accuracy_label.setText(f"准确率: {accuracy:.1f}%")
finally:
# 清除递归防止标志
self._processing_text_change = False
def refresh_daily_quote(self):
"""刷新每日一言"""
# 创建并启动获取名言的线程
self.quote_thread = QuoteFetchThread()
self.quote_thread.quote_fetched.connect(self.on_quote_fetched)
self.quote_thread.error_occurred.connect(self.on_quote_error)
self.quote_thread.start()
def refresh_weather_info(self):
"""刷新天气信息"""
# 创建并启动获取天气信息的线程
self.weather_thread = WeatherFetchThread()
self.weather_thread.weather_fetched.connect(self.on_weather_fetched)
self.weather_thread.error_occurred.connect(self.on_weather_error)
self.weather_thread.start()
def on_weather_fetched(self, weather_info):
"""处理天气信息获取成功"""
# 更新天气显示组件
if hasattr(self, 'weather_display') and self.weather_display:
self.weather_display.update_weather(weather_info)
def on_weather_error(self, error_msg):
"""处理天气信息获取错误"""
# 更新天气显示组件
if hasattr(self, 'weather_display') and self.weather_display:
self.weather_display.update_weather(error_msg)
self.learning_window = None
self.learning_mode_action.setChecked(False)
self.typing_mode_action.setChecked(True)

@ -2,6 +2,7 @@
import requests
import json
import os
import time
from typing import Optional, Dict, Any
class NetworkService:
@ -11,84 +12,354 @@ class NetworkService:
self.api_key = None
self.cache = {}
self.session = requests.Session()
# 天气缓存相关属性
self._cached_weather_data = None # 缓存的天气数据
self._cached_location = None # 缓存的定位信息
self._weather_cache_timestamp = None # 缓存时间戳
def get_weather_info(self) -> Optional[Dict[str, Any]]:
def get_cached_weather_data(self):
"""获取缓存的天气数据"""
return self._cached_weather_data
def get_cached_location(self):
"""获取缓存的定位信息"""
return self._cached_location
def set_weather_cache(self, weather_data, location):
"""设置天气缓存"""
self._cached_weather_data = weather_data
self._cached_location = location
self._weather_cache_timestamp = time.time()
def clear_weather_cache(self):
"""清除天气缓存"""
self._cached_weather_data = None
self._cached_location = None
self._weather_cache_timestamp = None
def is_weather_cache_valid(self):
"""检查天气缓存是否有效30分钟内"""
if self._weather_cache_timestamp is None:
return False
return (time.time() - self._weather_cache_timestamp) < 1800 # 30分钟
def get_user_ip(self):
"""获取用户IP地址 - 使用多个备用服务"""
# 首先尝试获取本地IP
try:
import socket
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
if local_ip and not local_ip.startswith("127."):
print(f"获取到本地IP: {local_ip}")
return local_ip
except Exception as e:
print(f"获取本地IP失败: {e}")
# 如果本地IP获取失败使用备用外部服务
ip_services = [
"https://httpbin.org/ip",
"https://api.ipify.org?format=json",
"https://ipapi.co/json/"
]
for service in ip_services:
try:
print(f"尝试从 {service} 获取IP地址...")
response = self.session.get(service, timeout=3, verify=False)
if response.status_code == 200:
data = response.json()
ip = data.get("origin") or data.get("ip") or data.get("ip_address")
if ip:
print(f"成功从 {service} 获取IP: {ip}")
return ip
except Exception as e:
print(f"{service} 获取IP失败: {e}")
continue
print("所有IP获取服务都失败了使用默认IP")
return "8.8.8.8" # 使用Google DNS作为默认IP
def get_default_weather(self):
"""获取默认天气数据"""
return {
"city": "北京",
"temperature": 20,
"description": "晴天",
"humidity": 60,
"wind_speed": 3.5
}
def clear_weather_cache(self):
"""清除天气缓存"""
self._cached_weather_data = None
self._cached_location = None
self._weather_cache_time = None
print("天气缓存已清除")
def get_weather_info(self, use_cache: bool = True) -> Optional[Dict[str, Any]]:
"""获取天气信息,支持缓存机制"""
# 如果启用缓存且缓存有效,直接返回缓存数据
if use_cache and self.is_weather_cache_valid():
print("使用缓存的天气数据")
return self._cached_weather_data
# 如果没有指定城市,使用自动定位
return self.get_weather_info_by_city(None, use_cache)
def get_weather_info_by_city(self, city_name: Optional[str] = None, use_cache: bool = True) -> Optional[Dict[str, Any]]:
"""根据城市名获取天气信息"""
# 如果启用缓存且缓存有效,直接返回缓存数据
if use_cache and self.is_weather_cache_valid() and not city_name:
print("使用缓存的天气数据")
return self._cached_weather_data
# 实现天气信息获取逻辑
# 1. 获取用户IP地址
try:
ip_response = self.session.get("https://httpbin.org/ip", timeout=5, verify=False)
ip_data = ip_response.json()
ip = ip_data.get("origin", "")
# 如果指定了城市名直接使用该城市否则通过IP定位
if city_name:
print(f"使用指定城市: {city_name}")
city = city_name
# 清除缓存以确保获取新数据
if hasattr(self, 'clear_weather_cache'):
self.clear_weather_cache()
else:
# 1. 获取用户IP地址 - 使用多个备用服务
print("开始获取天气信息...")
ip = self.get_user_ip()
print(f"获取到的IP地址: {ip}")
if not ip:
print("无法获取IP地址使用默认天气数据")
return self.get_default_weather()
# 2. 根据IP获取地理位置
# 注意这里使用免费的IP地理位置API实际应用中可能需要更精确的服务
location_response = self.session.get(f"http://ip-api.com/json/{ip}", timeout=5, verify=False)
location_url = f"http://ip-api.com/json/{ip}"
print(f"请求地理位置: {location_url}")
location_response = self.session.get(location_url, timeout=5, verify=False)
location_data = location_response.json()
print(f"地理位置响应: {location_data}")
if location_data.get("status") != "success":
return None
print("地理位置获取失败,使用默认天气数据")
return self.get_default_weather()
city = location_data.get("city", "Unknown")
print(f"获取到的城市: {city}")
if not city:
print("无法获取城市名称,使用默认天气数据")
return self.get_default_weather()
# 3. 调用天气API获取天气数据
# 注意这里使用OpenWeatherMap API作为示例需要API密钥
# 在实际应用中需要设置有效的API密钥
if self.api_key:
weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={self.api_key}&units=metric&lang=zh_cn"
# 保存定位信息到缓存
self._cached_location = {
"ip": ip,
"city": city,
"country": location_data.get("country", "Unknown"),
"region": location_data.get("regionName", "Unknown")
}
# 3. 调用天气API获取天气数据
# 注意这里使用OpenWeatherMap API作为示例需要API密钥
# 在实际应用中需要设置有效的API密钥
if self.api_key:
weather_url = f"http://api.openweathermap.org/data/2.5/weather?q={city}&appid={self.api_key}&units=metric&lang=zh_cn"
weather_response = self.session.get(weather_url, timeout=5, verify=False)
weather_data = weather_response.json()
# 4. 解析并格式化数据
if weather_response.status_code == 200:
formatted_weather = {
"city": city,
"temperature": weather_data["main"]["temp"],
"description": weather_data["weather"][0]["description"],
"humidity": weather_data["main"]["humidity"],
"wind_speed": weather_data["wind"]["speed"]
}
# 5. 返回天气信息字典
return formatted_weather
else:
# 当没有API密钥时使用免费的天气API获取真实数据
# 首先尝试获取城市ID需要映射城市名到ID
city_id_map = {
"Beijing": "101010100",
"Shanghai": "101020100",
"Tianjin": "101030100",
"Chongqing": "101040100",
"Hong Kong": "101320101",
"Macau": "101330101",
# 添加中文城市名映射
"北京": "101010100",
"上海": "101020100",
"天津": "101030100",
"重庆": "101040100",
"香港": "101320101",
"澳门": "101330101",
# 添加更多主要城市
"广州": "101280101",
"深圳": "101280601",
"杭州": "101210101",
"南京": "101190101",
"成都": "101270101",
"武汉": "101200101",
"西安": "101110101",
"沈阳": "101070101",
"青岛": "101120201",
"大连": "101070201",
"苏州": "101190401",
"无锡": "101190201"
}
# 尝试映射英文城市名到ID
city_id = city_id_map.get(city)
# 如果找到城市ID使用该ID获取天气否则使用默认北京ID
if city_id:
weather_city_id = city_id
print(f"使用城市ID获取天气: {city} -> {weather_city_id}")
else:
# 对于中国主要城市,直接使用拼音映射
city_pinyin_map = {
"Beijing": "北京",
"Shanghai": "上海",
"Tianjin": "天津",
"Chongqing": "重庆",
"Guangzhou": "广州",
"Shenzhen": "深圳",
"Hangzhou": "杭州",
"Nanjing": "南京",
"Chengdu": "成都",
"Wuhan": "武汉",
"Xi\'an": "西安",
"Shenyang": "沈阳"
}
chinese_city = city_pinyin_map.get(city, city)
weather_city_id = "101010100" # 默认北京ID
print(f"使用默认城市ID获取天气: {city} -> {weather_city_id}")
# 使用免费天气API获取天气数据
try:
# 使用和风天气免费API的替代方案 - sojson天气API
weather_url = f"http://t.weather.sojson.com/api/weather/city/{weather_city_id}"
print(f"请求天气数据: {weather_url}")
weather_response = self.session.get(weather_url, timeout=5, verify=False)
print(f"天气响应状态码: {weather_response.status_code}")
weather_data = weather_response.json()
print(f"天气数据响应: {weather_data}")
# 4. 解析并格式化数据
if weather_response.status_code == 200:
if weather_data.get("status") == 200:
# 解析天气数据
current_data = weather_data.get("data", {})
wendu = current_data.get("wendu", "N/A")
shidu = current_data.get("shidu", "N/A")
forecast = current_data.get("forecast", [])
# 获取第一个预报项作为当前天气
current_weather = forecast[0] if forecast else {}
weather_type = current_weather.get("type", "")
# 获取生活指数信息
lifetips = []
if current_weather:
# 从预报数据中提取生活提示
ganmao = current_weather.get("ganmao", "")
if ganmao:
lifetips.append(f"感冒指数: {ganmao}")
# 添加其他生活指数(基于天气类型推断)
if "" in weather_type:
lifetips.append("出行建议: 记得带伞")
elif "" in weather_type:
lifetips.append("出行建议: 适合户外活动")
elif "" in weather_type:
lifetips.append("出行建议: 注意防滑保暖")
elif "" in weather_type or "" in weather_type:
lifetips.append("健康提醒: 减少户外运动")
# 温度相关建议
temp = float(wendu) if wendu != "N/A" else 20
if temp > 30:
lifetips.append("穿衣建议: 注意防暑降温")
elif temp < 5:
lifetips.append("穿衣建议: 注意保暖防寒")
elif temp < 15:
lifetips.append("穿衣建议: 适当添加衣物")
else:
lifetips.append("穿衣建议: 天气舒适")
formatted_weather = {
"city": city,
"temperature": weather_data["main"]["temp"],
"description": weather_data["weather"][0]["description"],
"humidity": weather_data["main"]["humidity"],
"wind_speed": weather_data["wind"]["speed"]
"temperature": float(wendu) if wendu != "N/A" else 20,
"description": weather_type,
"humidity": shidu.replace("%", "") if shidu != "N/A" else "60",
"wind_speed": "3.5", # 默认风速
"lifetips": lifetips # 生活提示列表
}
# 5. 返回天气信息字典
print(f"成功获取天气数据: {formatted_weather}")
# 缓存天气数据
self.set_weather_cache(formatted_weather, self._cached_location)
return formatted_weather
else:
# 模拟天气数据无API密钥时
return {
"city": city,
"temperature": 20,
"description": "晴天",
"humidity": 60,
"wind_speed": 3.5
}
except Exception as e:
print(f"获取天气信息时出错: {e}")
return None
else:
print(f"天气API返回错误状态: {weather_data.get('status')}")
except Exception as e:
print(f"获取免费天气数据时出错: {e}")
# 如果以上都失败,返回默认数据
default_weather = {
"city": city,
"temperature": 20,
"description": "晴天",
"humidity": 60,
"wind_speed": 3.5,
"lifetips": [
"穿衣建议: 天气舒适",
"出行建议: 适合户外活动",
"健康提醒: 保持良好心情"
]
}
print(f"使用默认天气数据 for {city}")
# 缓存默认天气数据
self.set_weather_cache(default_weather, self._cached_location)
return default_weather
def get_daily_quote(self) -> Optional[str]:
# 实现每日一句获取逻辑
# 1. 调用名言API
# 实现每日一句获取逻辑 - 使用古诗词API
try:
# 使用一个免费的名言API禁用SSL验证以避免证书问题
response = self.session.get("https://api.quotable.io/random", timeout=5, verify=False)
# 使用古诗词·一言API - 每次返回随机不同的诗词
response = self.session.get("https://v1.jinrishici.com/all.json", timeout=5, verify=False)
# 2. 解析返回的名言数据
# 2. 解析返回的古诗词数据
if response.status_code == 200:
quote_data = response.json()
content = quote_data.get("content", "")
author = quote_data.get("author", "")
poetry_data = response.json()
content = poetry_data.get('content', '')
author = poetry_data.get('author', '')
title = poetry_data.get('origin', '')
# 3. 格式化名言文本
formatted_quote = f'"{content}" - {author}'
# 3. 格式化古诗词文本
if content and author and title:
formatted_poetry = f"{content}{author}{title}"
elif content and author:
formatted_poetry = f"{content}{author}"
elif content:
formatted_poetry = content
else:
formatted_poetry = "暂无古诗词"
# 4. 返回名言字符串
return formatted_quote
# 4. 返回古诗词字符串
return formatted_poetry
else:
# 如果API调用失败返回默认名言
return "书山有路勤为径,学海无涯苦作舟。"
# 如果API调用失败返回默认古诗词
return "山重水复疑无路,柳暗花明又一村"
except Exception as e:
print(f"获取每日一句时出错: {e}")
# 出错时返回默认名言
return "书山有路勤为径,学海无涯苦作舟。"
print(f"获取古诗词时出错: {e}")
# 出错时返回默认古诗词
return "山重水复疑无路,柳暗花明又一村"
def download_image(self, url: str) -> Optional[bytes]:

@ -0,0 +1,535 @@
# ai_chat_panel.py - AI对话面板组件
import json
import os
import requests
import threading
from datetime import datetime
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLineEdit,
QPushButton, QLabel, QScrollArea, QFrame, QMessageBox
)
from PyQt5.QtCore import Qt, pyqtSignal, QThread, QTimer, QSize, pyqtSlot
from PyQt5.QtGui import QFont, QColor, QTextCursor, QIcon, QPixmap
from PyQt5.QtGui import QTextDocument, QTextCharFormat
# 导入主题管理器
try:
from .theme_manager import theme_manager
except ImportError:
# 处理直接运行的情况
from ui.theme_manager import theme_manager
class AIChatPanel(QWidget):
"""AI对话面板"""
# 信号定义 - 用于线程安全的UI更新
update_chat_display = pyqtSignal(str) # 更新聊天显示信号
def __init__(self, parent=None):
super().__init__(parent)
self.api_key = ""
self.conversation_history = []
self.current_streaming_content = ""
self.is_streaming = False
self.streaming_thread = None
self.streaming_timer = None
# 加载API密钥
self.load_api_key()
self.init_ui()
# 连接信号到槽
self.update_chat_display.connect(self.on_update_chat_display)
# 连接主题变化信号
theme_manager.theme_changed.connect(self.on_theme_changed)
def load_api_key(self):
"""从本地文件加载API密钥"""
config_file = os.path.join(
os.path.dirname(__file__),
"..", "..", "resources", "config", "deepseek_api.json"
)
try:
if os.path.exists(config_file):
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
self.api_key = config.get('api_key', '')
except Exception as e:
print(f"加载API密钥失败: {e}")
def init_ui(self):
"""初始化UI"""
main_layout = QVBoxLayout()
main_layout.setContentsMargins(8, 8, 8, 8)
main_layout.setSpacing(8)
# 标题栏
header_layout = QHBoxLayout()
header_label = QLabel("AI 助手")
header_font = QFont()
header_font.setBold(True)
header_font.setPointSize(11)
header_label.setFont(header_font)
header_layout.addWidget(header_label)
header_layout.addStretch()
# 清空历史按钮
clear_btn = QPushButton("清空")
clear_btn.setObjectName("clear_btn") # 设置对象名称
clear_btn.setMaximumWidth(60)
clear_btn.setFont(QFont("微软雅黑", 9))
clear_btn.clicked.connect(self.clear_history)
clear_btn.setStyleSheet("""
QPushButton {
background-color: #f0f0f0;
color: #333333;
border: 1px solid #d0d0d0;
border-radius: 6px;
padding: 6px 12px;
font-weight: 500;
font-size: 12px;
min-width: 50px;
}
QPushButton:hover {
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
QPushButton:pressed {
background-color: #d0d0d0;
border: 1px solid #b0b0b0;
}
/* 深色主题 */
QPushButton[darkTheme="true"] {
background-color: #3c3c3c;
color: #e0e0e0;
border: 1px solid #4c4c4c;
}
QPushButton[darkTheme="true"]:hover {
background-color: #4c4c4c;
border: 1px solid #5c5c5c;
}
QPushButton[darkTheme="true"]:pressed {
background-color: #5c5c5c;
border: 1px solid #6c6c6c;
}
""")
header_layout.addWidget(clear_btn)
main_layout.addLayout(header_layout)
# 分割线
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setStyleSheet("color: #d0d0d0;")
main_layout.addWidget(line)
# 对话显示区域
self.chat_display = QTextEdit()
self.chat_display.setReadOnly(True)
self.chat_display.setStyleSheet("""
QTextEdit {
background-color: #ffffff;
border: 1px solid #d0d0d0;
border-radius: 8px;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 11pt;
padding: 12px;
color: #333333;
line-height: 1.5;
}
/* 深色主题 */
QTextEdit[darkTheme="true"] {
background-color: #1e1e1e;
border: 1px solid #3c3c3c;
color: #e0e0e0;
}
""")
self.chat_display.setMinimumHeight(400)
main_layout.addWidget(self.chat_display)
# 输入区域
input_layout = QVBoxLayout()
input_layout.setSpacing(6)
# 输入框
self.input_field = QLineEdit()
self.input_field.setPlaceholderText("输入您的问题或请求...")
self.input_field.setStyleSheet("""
QLineEdit {
background-color: #f9f9f9;
border: 1px solid #d0d0d0;
border-radius: 8px;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 11pt;
padding: 10px 12px;
color: #333333;
}
QLineEdit:focus {
border: 2px solid #0078d4;
background-color: #ffffff;
}
/* 深色主题 */
QLineEdit[darkTheme="true"] {
background-color: #2d2d2d;
border: 1px solid #3c3c3c;
color: #e0e0e0;
}
QLineEdit[darkTheme="true"]:focus {
border: 2px solid #0078d4;
background-color: #1e1e1e;
}
""")
self.input_field.returnPressed.connect(self.send_user_message)
input_layout.addWidget(self.input_field)
# 按钮区域
button_layout = QHBoxLayout()
button_layout.setSpacing(6)
# 发送按钮
self.send_btn = QPushButton("发送")
self.send_btn.setFont(QFont("微软雅黑", 9))
self.send_btn.setStyleSheet("""
QPushButton {
background-color: #0078d4;
color: white;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-weight: 500;
font-size: 12px;
min-width: 70px;
}
QPushButton:hover {
background-color: #0063b1;
}
QPushButton:pressed {
background-color: #005a9e;
}
/* 深色主题 */
QPushButton[darkTheme="true"] {
background-color: #0078d4;
}
QPushButton[darkTheme="true"]:hover {
background-color: #0063b1;
}
QPushButton[darkTheme="true"]:pressed {
background-color: #005a9e;
}
""")
self.send_btn.clicked.connect(self.send_user_message)
button_layout.addStretch()
button_layout.addWidget(self.send_btn)
input_layout.addLayout(button_layout)
main_layout.addLayout(input_layout)
self.setLayout(main_layout)
# 设置背景颜色
self.setStyleSheet("""
AIChatPanel {
background-color: #f5f5f5;
border-left: 1px solid #d0d0d0;
}
/* 深色主题 */
AIChatPanel[darkTheme="true"] {
background-color: #2d2d2d;
border-left: 1px solid #3c3c3c;
}
""")
# 初始化主题
self.apply_theme()
def apply_theme(self):
"""应用当前主题"""
is_dark = theme_manager.is_dark_theme()
# 设置属性用于样式表选择器
self.setProperty("darkTheme", is_dark)
# 更新子控件的属性
self.chat_display.setProperty("darkTheme", is_dark)
self.input_field.setProperty("darkTheme", is_dark)
self.send_btn.setProperty("darkTheme", is_dark)
self.findChild(QPushButton, "clear_btn").setProperty("darkTheme", is_dark) if self.findChild(QPushButton, "clear_btn") else None
# 重新应用样式表
self.style().unpolish(self)
self.style().polish(self)
self.update()
# 更新聊天显示样式
self.chat_display.style().unpolish(self.chat_display)
self.chat_display.style().polish(self.chat_display)
self.chat_display.update()
# 更新输入框样式
self.input_field.style().unpolish(self.input_field)
self.input_field.style().polish(self.input_field)
self.input_field.update()
# 更新按钮样式
self.send_btn.style().unpolish(self.send_btn)
self.send_btn.style().polish(self.send_btn)
self.send_btn.update()
clear_btn = self.findChild(QPushButton, "clear_btn")
if clear_btn:
clear_btn.style().unpolish(clear_btn)
clear_btn.style().polish(clear_btn)
clear_btn.update()
def on_theme_changed(self, is_dark):
"""主题变化处理"""
self.apply_theme()
def send_user_message(self):
"""发送用户消息"""
if not self.api_key:
QMessageBox.warning(self, "警告", "请先配置DeepSeek API密钥")
return
message = self.input_field.text().strip()
if not message:
return
# 清空输入框
self.input_field.clear()
# 禁用发送按钮
self.send_btn.setEnabled(False)
self.input_field.setEnabled(False)
# 显示用户消息
self.add_message_to_display("用户", message)
# 添加用户消息到对话历史
self.conversation_history.append({"sender": "用户", "message": message})
# 显示AI正在思考
self.add_message_to_display("AI助手", "正在思考...")
self.conversation_history.append({"sender": "AI助手", "message": ""})
# 在新线程中调用API
self.streaming_thread = threading.Thread(
target=self.call_deepseek_api_stream,
args=(message,)
)
self.streaming_thread.daemon = True
self.streaming_thread.start()
# 启动定时器更新显示
self.streaming_timer = QTimer()
self.streaming_timer.timeout.connect(self.update_streaming_display)
self.streaming_timer.start(100) # 每100毫秒更新一次显示
def add_message_to_display(self, sender, message):
"""添加消息到显示区域"""
cursor = self.chat_display.textCursor()
cursor.movePosition(QTextCursor.End)
self.chat_display.setTextCursor(cursor)
# 设置格式
char_format = QTextCharFormat()
char_format.setFont(QFont("微软雅黑", 11))
if sender == "用户":
# 根据主题设置用户消息颜色
if theme_manager.is_dark_theme():
char_format.setForeground(QColor("#4A90E2")) # 深色主题下的蓝色
else:
char_format.setForeground(QColor("#0078d4")) # 浅色主题下的蓝色
char_format.setFontWeight(60) # 中等粗体
prefix = "你: "
else: # AI助手
# 根据主题设置AI消息颜色
if theme_manager.is_dark_theme():
char_format.setForeground(QColor("#e0e0e0")) # 深色主题下的浅灰色
else:
char_format.setForeground(QColor("#000000")) # 浅色主题下的纯黑色,提高可读性
char_format.setFontWeight(50) # 正常粗体
prefix = "AI: "
# 插入时间戳
timestamp_format = QTextCharFormat()
if theme_manager.is_dark_theme():
timestamp_format.setForeground(QColor("#a0a0a0")) # 深色主题下的灰色
else:
timestamp_format.setForeground(QColor("#666666")) # 浅色主题下的深灰色,提高可读性
timestamp_format.setFont(QFont("微软雅黑", 9))
cursor.insertText(f"\n[{datetime.now().strftime('%H:%M:%S')}] ", timestamp_format)
# 插入前缀
cursor.insertText(prefix, char_format)
# 插入消息
cursor.insertText(message, char_format)
# 自动滚动到底部
self.chat_display.verticalScrollBar().setValue(
self.chat_display.verticalScrollBar().maximum()
)
def rebuild_chat_display(self):
"""重新构建聊天显示"""
self.chat_display.clear()
cursor = self.chat_display.textCursor()
for msg in self.conversation_history:
sender = msg["sender"]
message = msg["message"]
# 设置格式
char_format = QTextCharFormat()
char_format.setFont(QFont("微软雅黑", 11))
if sender == "用户":
# 根据主题设置用户消息颜色
if theme_manager.is_dark_theme():
char_format.setForeground(QColor("#4A90E2")) # 深色主题下的蓝色
else:
char_format.setForeground(QColor("#0078d4")) # 浅色主题下的蓝色
char_format.setFontWeight(60) # 中等粗体
prefix = "你: "
else:
# 根据主题设置AI消息颜色
if theme_manager.is_dark_theme():
char_format.setForeground(QColor("#e0e0e0")) # 深色主题下的浅灰色
else:
char_format.setForeground(QColor("#000000")) # 浅色主题下的纯黑色,提高可读性
char_format.setFontWeight(50) # 正常粗体
prefix = "AI: "
# 插入分隔符
if self.chat_display.toPlainText():
cursor.insertText("\n")
# 插入时间戳
timestamp_format = QTextCharFormat()
if theme_manager.is_dark_theme():
timestamp_format.setForeground(QColor("#a0a0a0")) # 深色主题下的灰色
else:
timestamp_format.setForeground(QColor("#666666")) # 浅色主题下的深灰色,提高可读性
timestamp_format.setFont(QFont("微软雅黑", 9))
cursor.insertText(f"[{datetime.now().strftime('%H:%M:%S')}] ", timestamp_format)
# 插入前缀
cursor.insertText(prefix, char_format)
# 插入消息
cursor.insertText(message, char_format)
def call_deepseek_api_stream(self, message):
"""调用DeepSeek API流式版本"""
url = "https://api.deepseek.com/v1/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
messages = [{"role": "user", "content": message}]
data = {
"model": "deepseek-chat",
"messages": messages,
"stream": True,
"temperature": 0.7,
"max_tokens": 2000
}
self.is_streaming = True
self.current_streaming_content = ""
try:
response = requests.post(url, headers=headers, json=data, stream=True, timeout=30)
if response.status_code == 200:
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
data_str = line[6:]
if data_str == '[DONE]':
break
try:
data_obj = json.loads(data_str)
if 'choices' in data_obj and len(data_obj['choices']) > 0:
delta = data_obj['choices'][0].get('delta', {})
if 'content' in delta:
content = delta['content']
self.current_streaming_content += content
except json.JSONDecodeError:
pass
else:
error_msg = f"API调用失败: {response.status_code}"
self.current_streaming_content = error_msg
except requests.exceptions.Timeout:
self.current_streaming_content = "请求超时,请重试"
except Exception as e:
self.current_streaming_content = f"错误: {str(e)}"
finally:
self.is_streaming = False
# 停止定时器
if self.streaming_timer:
self.streaming_timer.stop()
# 最后更新一次显示,使用信号在主线程中进行
self.update_chat_display.emit(self.current_streaming_content)
@pyqtSlot(str)
def on_update_chat_display(self, content):
"""在主线程中更新聊天显示"""
# 更新最后一条AI消息
if len(self.conversation_history) > 0:
self.conversation_history[-1]["message"] = content
# 重新构建显示
self.rebuild_chat_display()
# 自动滚动到底部
self.chat_display.verticalScrollBar().setValue(
self.chat_display.verticalScrollBar().maximum()
)
# 重新启用输入
self.send_btn.setEnabled(True)
self.input_field.setEnabled(True)
self.input_field.setFocus()
def clear_history(self):
"""清空聊天历史"""
reply = QMessageBox.question(
self,
"确认",
"确定要清空聊天历史吗?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.conversation_history = []
self.chat_display.clear()
self.input_field.clear()
def update_streaming_display(self):
"""更新流式显示"""
if self.is_streaming and self.current_streaming_content:
# 重新显示所有对话
self.rebuild_chat_display()
self.chat_display.verticalScrollBar().setValue(
self.chat_display.verticalScrollBar().maximum()
)

@ -0,0 +1,508 @@
# -*- coding: utf-8 -*-
"""
日历悬浮窗口模块
提供一个可拖拽的日历悬浮窗口用于在应用程序中显示和选择日期
"""
import sys
from PyQt5.QtWidgets import (
QWidget, QCalendarWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFrame
)
from PyQt5.QtCore import QDate, Qt, pyqtSignal, QPoint
from PyQt5.QtGui import QFont
# 导入主题管理器
from .theme_manager import theme_manager
class CalendarFloatingWidget(QWidget):
"""日历悬浮窗口类"""
# 自定义信号
closed = pyqtSignal() # 窗口关闭信号
date_selected = pyqtSignal(str) # 日期字符串信号,用于插入功能
def __init__(self, parent=None):
super().__init__(parent)
self.drag_position = None
self.is_dragging = False
self.setup_ui()
self.setup_connections()
self.setup_theme()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground)
def setup_ui(self):
"""设置UI界面"""
# 设置窗口属性
self.setWindowTitle("日历")
self.setFixedSize(360, 320) # 设置窗口大小
# 创建主框架,用于实现圆角和阴影效果
self.main_frame = QFrame()
self.main_frame.setObjectName("mainFrame")
# 主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(self.main_frame)
# 内容布局
content_layout = QVBoxLayout(self.main_frame)
content_layout.setContentsMargins(10, 10, 10, 10)
content_layout.setSpacing(8)
# 标题栏
title_layout = QHBoxLayout()
title_layout.setContentsMargins(0, 0, 0, 0)
title_layout.setSpacing(0)
self.title_label = QLabel("日历")
self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
title_layout.addWidget(self.title_label)
title_layout.addStretch()
# 添加一个小的固定空间,使关闭按钮向左移动
title_layout.addSpacing(25) # 向左移动25个单位
# 关闭按钮
self.close_btn = QPushButton("×")
self.close_btn.setFixedSize(20, 20)
self.close_btn.setObjectName("closeButton")
title_layout.addWidget(self.close_btn)
content_layout.addLayout(title_layout)
# 分隔线
separator = QFrame()
separator.setObjectName("separator")
separator.setFixedHeight(1)
content_layout.addWidget(separator)
# 日历控件
self.calendar = QCalendarWidget()
self.calendar.setGridVisible(True)
self.calendar.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
self.calendar.setNavigationBarVisible(True)
content_layout.addWidget(self.calendar)
# 当前日期显示
self.date_label = QLabel()
self.date_label.setAlignment(Qt.AlignCenter)
self.date_label.setFont(QFont("Arial", 10))
self.date_label.setObjectName("dateLabel")
self.update_date_label()
content_layout.addWidget(self.date_label)
# 操作按钮
button_layout = QHBoxLayout()
button_layout.setContentsMargins(0, 0, 0, 0)
button_layout.setSpacing(6)
self.today_btn = QPushButton("今天")
self.today_btn.setObjectName("todayButton")
button_layout.addWidget(self.today_btn)
self.insert_btn = QPushButton("插入")
self.insert_btn.setObjectName("insertButton")
button_layout.addWidget(self.insert_btn)
button_layout.addStretch()
self.clear_btn = QPushButton("清除")
self.clear_btn.setObjectName("clearButton")
button_layout.addWidget(self.clear_btn)
content_layout.addLayout(button_layout)
def setup_connections(self):
"""设置信号连接"""
self.calendar.clicked.connect(self.on_date_selected)
self.today_btn.clicked.connect(self.on_today_clicked)
self.clear_btn.clicked.connect(self.on_clear_clicked)
self.close_btn.clicked.connect(self.close_window)
self.insert_btn.clicked.connect(self.on_insert_clicked)
def setup_theme(self):
"""设置主题"""
# 连接主题切换信号
theme_manager.theme_changed.connect(self.on_theme_changed)
# 应用当前主题
self.apply_theme()
def apply_theme(self):
"""应用主题样式 - 优化Apple设计风格"""
is_dark = theme_manager.is_dark_theme()
colors = theme_manager.get_current_theme_colors()
if is_dark:
# 深色主题样式 - 优化版Apple设计风格
self.main_frame.setStyleSheet(f"""
QFrame#mainFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 10px;
}}
QLabel {{
color: {colors['text']};
background-color: transparent;
padding: 4px 6px;
margin: 2px;
}}
QLabel#dateLabel {{
color: {colors['text_secondary']};
font-size: 12px;
padding: 4px 6px;
margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
background-color: rgba(255, 255, 255, 0.1);
border: none;
color: {colors['text']};
font-size: 18px;
font-weight: bold;
border-radius: 6px;
padding: 2px 4px;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
border-radius: 6px;
}}
QPushButton#closeButton:pressed {{
background-color: #c50e1f;
}}
QPushButton#todayButton, QPushButton#clearButton, QPushButton#insertButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 12px;
font-weight: 500;
}}
QPushButton#todayButton:hover, QPushButton#clearButton:hover, QPushButton#insertButton:hover {{
background-color: {colors['accent_hover']};
}}
QPushButton#todayButton:pressed, QPushButton#clearButton:pressed, QPushButton#insertButton:pressed {{
background-color: {colors['accent_pressed']};
}}
""")
# 更新日历控件样式 - 深色主题优化版Apple设计风格
self.calendar.setStyleSheet(f"""
QCalendarWidget {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 8px;
}}
QCalendarWidget QToolButton {{
height: 32px;
width: 85px;
color: {colors['text']};
font-size: 13px;
font-weight: 500;
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 6px;
}}
QCalendarWidget QToolButton:hover {{
background-color: {colors['surface_hover']};
}}
QCalendarWidget QToolButton:pressed {{
background-color: {colors['surface_pressed']};
}}
QCalendarWidget QMenu {{
width: 150px;
left: 20px;
color: {colors['text']};
font-size: 12px;
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 6px;
}}
QCalendarWidget QSpinBox {{
width: 85px;
font-size: 12px;
background-color: {colors['surface']};
selection-background-color: {colors['accent']};
selection-color: white;
border: 1px solid {colors['border']};
border-radius: 6px;
color: {colors['text']};
}}
QCalendarWidget QSpinBox::up-button {{
subcontrol-origin: border;
subcontrol-position: top right;
width: 20px;
}}
QCalendarWidget QSpinBox::down-button {{
subcontrol-origin: border;
subcontrol-position: bottom right;
width: 20px;
}}
QCalendarWidget QSpinBox::up-arrow {{
width: 10px;
height: 10px;
}}
QCalendarWidget QSpinBox::down-arrow {{
width: 10px;
height: 10px;
}}
QCalendarWidget QWidget {{
alternate-background-color: {colors['surface']};
}}
QCalendarWidget QAbstractItemView:enabled {{
font-size: 12px;
selection-background-color: {colors['accent']};
selection-color: white;
background-color: {colors['surface']};
color: {colors['text']};
}}
QCalendarWidget QAbstractItemView:disabled {{
color: {colors['text_disabled']};
}}
QCalendarWidget QWidget#qt_calendar_navigationbar {{
background-color: {colors['surface']};
}}
""")
else:
# 浅色主题样式 - 优化版Apple设计风格
self.main_frame.setStyleSheet(f"""
QFrame#mainFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}}
QLabel {{
color: {colors['text']};
background-color: transparent;
padding: 4px 6px;
margin: 2px;
}}
QLabel#dateLabel {{
color: {colors['text_secondary']};
font-size: 12px;
padding: 4px 6px;
margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
background-color: rgba(0, 0, 0, 0.05);
border: none;
color: {colors['text']};
font-size: 18px;
font-weight: bold;
border-radius: 6px;
padding: 2px 4px;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
border-radius: 6px;
}}
QPushButton#closeButton:pressed {{
background-color: #c50e1f;
}}
QPushButton#todayButton, QPushButton#clearButton, QPushButton#insertButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 12px;
font-weight: 500;
}}
QPushButton#todayButton:hover, QPushButton#clearButton:hover, QPushButton#insertButton:hover {{
background-color: {colors['accent_hover']};
}}
QPushButton#todayButton:pressed, QPushButton#clearButton:pressed, QPushButton#insertButton:pressed {{
background-color: {colors['accent_pressed']};
}}
""")
# 更新日历控件样式 - 浅色主题优化版Apple设计风格
self.calendar.setStyleSheet(f"""
QCalendarWidget {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 8px;
}}
QCalendarWidget QToolButton {{
height: 32px;
width: 85px;
color: {colors['text']};
font-size: 13px;
font-weight: 500;
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 6px;
}}
QCalendarWidget QToolButton:hover {{
background-color: {colors['surface_hover']};
}}
QCalendarWidget QToolButton:pressed {{
background-color: {colors['surface_pressed']};
}}
QCalendarWidget QMenu {{
width: 150px;
left: 20px;
color: {colors['text']};
font-size: 12px;
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 6px;
}}
QCalendarWidget QSpinBox {{
width: 85px;
font-size: 12px;
background-color: {colors['surface']};
selection-background-color: {colors['accent']};
selection-color: white;
border: 1px solid {colors['border']};
border-radius: 6px;
color: {colors['text']};
}}
QCalendarWidget QSpinBox::up-button {{
subcontrol-origin: border;
subcontrol-position: top right;
width: 20px;
}}
QCalendarWidget QSpinBox::down-button {{
subcontrol-origin: border;
subcontrol-position: bottom right;
width: 20px;
}}
QCalendarWidget QSpinBox::up-arrow {{
width: 10px;
height: 10px;
}}
QCalendarWidget QSpinBox::down-arrow {{
width: 10px;
height: 10px;
}}
QCalendarWidget QWidget {{
alternate-background-color: {colors['surface']};
}}
QCalendarWidget QAbstractItemView:enabled {{
font-size: 12px;
selection-background-color: {colors['accent']};
selection-color: white;
background-color: {colors['surface']};
color: {colors['text']};
}}
QCalendarWidget QAbstractItemView:disabled {{
color: {colors['text_disabled']};
}}
QCalendarWidget QWidget#qt_calendar_navigationbar {{
background-color: {colors['surface']};
}}
""")
def on_theme_changed(self, is_dark):
"""主题切换槽函数"""
self.apply_theme()
def mousePressEvent(self, event):
"""鼠标按下事件,用于拖拽"""
if event.button() == Qt.LeftButton:
# 检查是否点击在标题栏区域
if event.pos().y() <= 40: # 假设标题栏高度为40像素
self.is_dragging = True
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
"""鼠标移动事件,用于拖拽"""
if self.is_dragging and event.buttons() == Qt.LeftButton:
self.move(event.globalPos() - self.drag_position)
event.accept()
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
self.is_dragging = False
def on_date_selected(self, date):
"""日期选择事件"""
self.update_date_label(date)
def on_today_clicked(self):
"""今天按钮点击事件"""
today = QDate.currentDate()
self.calendar.setSelectedDate(today)
self.update_date_label(today)
def on_clear_clicked(self):
"""清除按钮点击事件"""
self.calendar.setSelectedDate(QDate())
self.date_label.setText("未选择日期")
def on_insert_clicked(self):
"""插入按钮点击事件"""
selected_date = self.calendar.selectedDate()
if selected_date.isValid():
# 发送信号,将选中的日期传递给主窗口
date_str = selected_date.toString("yyyy年MM月dd日 dddd")
self.date_selected.emit(date_str)
def update_date_label(self, date=None):
"""更新日期显示标签"""
if date is None:
date = self.calendar.selectedDate()
if date.isValid():
date_str = date.toString("yyyy年MM月dd日 (ddd)")
self.date_label.setText(f"选中日期: {date_str}")
else:
self.date_label.setText("未选择日期")
def get_selected_date(self):
"""获取选中的日期"""
return self.calendar.selectedDate()
def set_selected_date(self, date):
"""设置选中的日期"""
if isinstance(date, str):
date = QDate.fromString(date, "yyyy-MM-dd")
self.calendar.setSelectedDate(date)
self.update_date_label(date)
def close_window(self):
"""关闭窗口 - 只是隐藏而不是销毁"""
try:
self.closed.emit()
self.hide() # 隐藏窗口而不是销毁
except Exception as e:
print(f"Error in close_window: {e}")
def main():
"""测试函数"""
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
# 创建并显示窗口
widget = CalendarFloatingWidget()
widget.show()
# 移动到屏幕中心
screen_geometry = app.desktop().screenGeometry()
widget.move(
(screen_geometry.width() - widget.width()) // 2,
(screen_geometry.height() - widget.height()) // 2
)
sys.exit(app.exec_())
if __name__ == "__main__":
main()

@ -0,0 +1,676 @@
# -*- coding: utf-8 -*-
"""
日历组件模块
提供一个可嵌入的日历控件用于在应用程序中显示和选择日期
"""
import sys
from PyQt5.QtWidgets import (
QWidget, QCalendarWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QLabel, QFrame
)
from PyQt5.QtCore import QDate, Qt, pyqtSignal
from PyQt5.QtGui import QFont
# 导入主题管理器
from .theme_manager import theme_manager
class CalendarWidget(QWidget):
"""日历组件类"""
# 自定义信号
date_selected = pyqtSignal(str) # 日期字符串信号,用于插入功能
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.setup_connections()
self.setup_theme()
def setup_ui(self):
"""设置UI界面"""
# 设置窗口属性
self.setWindowTitle("日历")
# 不再固定宽度,让其可以调整大小
# 设置白色背景
self.setStyleSheet("background-color: white;")
# 主布局
main_layout = QVBoxLayout()
main_layout.setContentsMargins(10, 10, 10, 10)
main_layout.setSpacing(10)
# 标题栏
title_layout = QHBoxLayout()
title_label = QLabel("日历")
title_label.setFont(QFont("Arial", 12, QFont.Bold))
title_layout.addWidget(title_label)
title_layout.addStretch()
# 关闭按钮
self.close_btn = QPushButton("×")
self.close_btn.setFixedSize(25, 25)
self.close_btn.setStyleSheet("""
QPushButton {
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 12px;
font-size: 16px;
font-weight: bold;
}
QPushButton:hover {
background-color: #e0e0e0;
}
""")
title_layout.addWidget(self.close_btn)
main_layout.addLayout(title_layout)
# 分隔线
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
main_layout.addWidget(separator)
# 日历控件
self.calendar = QCalendarWidget()
self.calendar.setGridVisible(True)
self.calendar.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
self.calendar.setNavigationBarVisible(True)
# 设置样式
self.calendar.setStyleSheet("""
QCalendarWidget {
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
}
QCalendarWidget QToolButton {
height: 30px;
width: 80px;
color: #333;
font-size: 12px;
font-weight: bold;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
}
QCalendarWidget QToolButton:hover {
background-color: #e0e0e0;
}
QCalendarWidget QMenu {
width: 150px;
left: 20px;
color: white;
font-size: 12px;
background-color: rgb(64, 64, 64);
}
QCalendarWidget QSpinBox {
width: 80px;
font-size: 12px;
background-color: #f0f0f0;
selection-background-color: rgb(64, 64, 64);
selection-color: white;
border: 1px solid #ccc;
border-radius: 4px;
}
QCalendarWidget QSpinBox::up-button {
subcontrol-origin: border;
subcontrol-position: top right;
width: 20px;
}
QCalendarWidget QSpinBox::down-button {
subcontrol-origin: border;
subcontrol-position: bottom right;
width: 20px;
}
QCalendarWidget QSpinBox::up-arrow {
width: 10px;
height: 10px;
}
QCalendarWidget QSpinBox::down-arrow {
width: 10px;
height: 10px;
}
QCalendarWidget QWidget {
alternate-background-color: #f0f0f0;
}
QCalendarWidget QAbstractItemView:enabled {
font-size: 12px;
selection-background-color: #0078d7;
selection-color: white;
}
QCalendarWidget QWidget#qt_calendar_navigationbar {
background-color: #f8f8f8;
}
""")
main_layout.addWidget(self.calendar)
# 当前日期显示
self.date_label = QLabel()
self.date_label.setAlignment(Qt.AlignCenter)
self.date_label.setFont(QFont("Arial", 10))
self.date_label.setStyleSheet("QLabel { color: #666; }")
self.update_date_label()
main_layout.addWidget(self.date_label)
# 操作按钮
button_layout = QHBoxLayout()
self.today_btn = QPushButton("今天")
self.today_btn.setStyleSheet("""
QPushButton {
background-color: #0078d7;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
self.clear_btn = QPushButton("清除")
self.clear_btn.setStyleSheet("""
QPushButton {
background-color: #f0f0f0;
color: #333;
border: 1px solid #ccc;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #e0e0e0;
}
""")
# 插入按钮
self.insert_btn = QPushButton("插入")
self.insert_btn.setStyleSheet("""
QPushButton {
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: #45a049;
}
""")
button_layout.addWidget(self.today_btn)
button_layout.addWidget(self.insert_btn)
button_layout.addStretch()
button_layout.addWidget(self.clear_btn)
main_layout.addLayout(button_layout)
self.setLayout(main_layout)
def setup_connections(self):
"""设置信号连接"""
self.calendar.clicked.connect(self.on_date_selected)
self.today_btn.clicked.connect(self.on_today_clicked)
self.clear_btn.clicked.connect(self.on_clear_clicked)
self.close_btn.clicked.connect(self.on_close_clicked)
self.insert_btn.clicked.connect(self.on_insert_clicked)
def on_date_selected(self, date):
"""日期选择事件"""
self.update_date_label(date)
def on_today_clicked(self):
"""今天按钮点击事件"""
today = QDate.currentDate()
self.calendar.setSelectedDate(today)
self.update_date_label(today)
def on_clear_clicked(self):
"""清除按钮点击事件"""
self.calendar.setSelectedDate(QDate())
self.date_label.setText("未选择日期")
def on_close_clicked(self):
"""关闭按钮点击事件"""
self.hide()
def on_insert_clicked(self):
"""插入按钮点击事件"""
selected_date = self.calendar.selectedDate()
if selected_date.isValid():
# 发送信号,将选中的日期传递给主窗口
date_str = selected_date.toString("yyyy年MM月dd日 dddd")
self.date_selected.emit(date_str)
def update_date_label(self, date=None):
"""更新日期显示标签"""
if date is None:
date = self.calendar.selectedDate()
if date.isValid():
date_str = date.toString("yyyy年MM月dd日 (ddd)")
self.date_label.setText(f"选中日期: {date_str}")
else:
self.date_label.setText("未选择日期")
def get_selected_date(self):
"""获取选中的日期"""
return self.calendar.selectedDate()
def set_selected_date(self, date):
"""设置选中的日期"""
if isinstance(date, str):
date = QDate.fromString(date, "yyyy-MM-dd")
self.calendar.setSelectedDate(date)
self.update_date_label(date)
def setup_theme(self):
"""设置主题"""
# 连接主题切换信号
theme_manager.theme_changed.connect(self.on_theme_changed)
# 应用当前主题
current_theme = theme_manager.is_dark_theme()
self.apply_theme(current_theme)
def apply_theme(self, is_dark_theme):
"""应用主题样式 - 优化Apple设计风格"""
is_dark = theme_manager.is_dark_theme()
if is_dark:
# 深色主题样式 - 优化版Apple设计风格
self.setStyleSheet("""
QWidget {
background-color: #1c1c1e;
color: #e8e8ed;
}
""")
# 更新日历控件样式
self.calendar.setStyleSheet("""
QCalendarWidget {
background-color: #1c1c1e;
border: 1px solid #3a3a3c;
border-radius: 8px;
}
QCalendarWidget QToolButton {
height: 32px;
width: 85px;
color: #e8e8ed;
font-size: 13px;
font-weight: 500;
background-color: #2c2c2e;
border: 1px solid #3a3a3c;
border-radius: 6px;
}
QCalendarWidget QToolButton:pressed {
background-color: #4a4a4c;
border: 1px solid #5a5a5c;
}
QCalendarWidget QToolButton:hover {
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
}
QCalendarWidget QMenu {
width: 160px;
left: 20px;
color: #e8e8ed;
font-size: 13px;
background-color: #2c2c2e;
border: 1px solid #3a3a3c;
border-radius: 6px;
}
QCalendarWidget QMenu::item:selected {
background-color: #0a84ff;
color: #ffffff;
}
QCalendarWidget QSpinBox {
width: 85px;
font-size: 13px;
background-color: #2c2c2e;
selection-background-color: #0a84ff;
selection-color: #ffffff;
border: 1px solid #3a3a3c;
border-radius: 6px;
color: #e8e8ed;
padding: 2px;
}
QCalendarWidget QSpinBox::up-button {
subcontrol-origin: border;
subcontrol-position: top right;
width: 22px;
border: 1px solid #3a3a3c;
background-color: #2c2c2e;
border-radius: 0 6px 0 0;
}
QCalendarWidget QSpinBox::down-button {
subcontrol-origin: border;
subcontrol-position: bottom right;
width: 22px;
border: 1px solid #3a3a3c;
background-color: #2c2c2e;
border-radius: 0 0 6px 0;
}
QCalendarWidget QSpinBox::up-button:hover,
QCalendarWidget QSpinBox::down-button:hover {
background-color: #3a3a3c;
}
QCalendarWidget QSpinBox::up-button:pressed,
QCalendarWidget QSpinBox::down-button:pressed {
background-color: #4a4a4c;
}
QCalendarWidget QSpinBox::up-arrow {
width: 12px;
height: 12px;
}
QCalendarWidget QSpinBox::down-arrow {
width: 12px;
height: 12px;
}
QCalendarWidget QWidget {
alternate-background-color: #2c2c2e;
}
QCalendarWidget QAbstractItemView:enabled {
font-size: 13px;
selection-background-color: #0a84ff;
selection-color: #ffffff;
background-color: #121212;
color: #e8e8ed;
}
QCalendarWidget QAbstractItemView:disabled {
color: #8a8a8d;
}
QCalendarWidget QWidget#qt_calendar_navigationbar {
background-color: #2c2c2e;
}
""")
# 更新标签样式
self.date_label.setStyleSheet("QLabel { color: #8a8a8d; font-size: 12px; font-weight: 500; }")
# 更新按钮样式
self.close_btn.setStyleSheet("""
QPushButton {
background-color: #2c2c2e;
border: 1px solid #3a3a3c;
border-radius: 14px;
font-size: 18px;
font-weight: 600;
color: #e8e8ed;
padding: 6px;
}
QPushButton:hover {
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
}
QPushButton:pressed {
background-color: #4a4a4c;
border: 1px solid #5a5a5c;
}
""")
self.today_btn.setStyleSheet("""
QPushButton {
background-color: #0a84ff;
color: #ffffff;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-weight: 500;
font-size: 12px;
}
QPushButton:hover {
background-color: #0071e3;
}
QPushButton:pressed {
background-color: #0051d5;
}
""")
self.clear_btn.setStyleSheet("""
QPushButton {
background-color: #2c2c2e;
color: #e8e8ed;
border: 1px solid #3a3a3c;
border-radius: 6px;
padding: 6px 12px;
font-weight: 500;
font-size: 12px;
}
QPushButton:hover {
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
}
QPushButton:pressed {
background-color: #4a4a4c;
border: 1px solid #5a5a5c;
}
""")
self.insert_btn.setStyleSheet("""
QPushButton {
background-color: #34c759;
color: #ffffff;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-weight: 500;
font-size: 12px;
}
QPushButton:hover {
background-color: #30d158;
}
QPushButton:pressed {
background-color: #2eb750;
}
""")
else:
# 浅色主题样式 - 优化版Apple设计风格
self.setStyleSheet("""
QWidget {
background-color: #f8f8f8;
color: #333333;
}
""")
# 更新日历控件样式
self.calendar.setStyleSheet("""
QCalendarWidget {
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
QCalendarWidget QToolButton {
height: 32px;
width: 85px;
color: #333333;
font-size: 13px;
font-weight: 500;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
QCalendarWidget QToolButton:pressed {
background-color: #f0f0f0;
border: 1px solid #c0c0c0;
}
QCalendarWidget QToolButton:hover {
background-color: #f0f0f0;
border: 1px solid #d0d0d0;
}
QCalendarWidget QMenu {
width: 160px;
left: 20px;
color: #333333;
font-size: 13px;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 6px;
}
QCalendarWidget QMenu::item:selected {
background-color: #007aff;
color: #ffffff;
}
QCalendarWidget QSpinBox {
width: 85px;
font-size: 13px;
background-color: #ffffff;
selection-background-color: #007aff;
selection-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 6px;
color: #333333;
padding: 2px;
}
QCalendarWidget QSpinBox::up-button {
subcontrol-origin: border;
subcontrol-position: top right;
width: 22px;
border: 1px solid #e0e0e0;
background-color: #ffffff;
border-radius: 0 6px 0 0;
}
QCalendarWidget QSpinBox::down-button {
subcontrol-origin: border;
subcontrol-position: bottom right;
width: 22px;
border: 1px solid #e0e0e0;
background-color: #ffffff;
border-radius: 0 0 6px 0;
}
QCalendarWidget QSpinBox::up-button:hover,
QCalendarWidget QSpinBox::down-button:hover {
background-color: #f0f0f0;
}
QCalendarWidget QSpinBox::up-button:pressed,
QCalendarWidget QSpinBox::down-button:pressed {
background-color: #e0e0e0;
}
QCalendarWidget QSpinBox::up-arrow {
width: 12px;
height: 12px;
}
QCalendarWidget QSpinBox::down-arrow {
width: 12px;
height: 12px;
}
QCalendarWidget QWidget {
alternate-background-color: #f8f8f8;
}
QCalendarWidget QAbstractItemView:enabled {
font-size: 13px;
selection-background-color: #007aff;
selection-color: #ffffff;
background-color: #ffffff;
color: #333333;
}
QCalendarWidget QAbstractItemView:disabled {
color: #999999;
}
QCalendarWidget QWidget#qt_calendar_navigationbar {
background-color: #f8f8f8;
}
""")
# 更新标签样式
self.date_label.setStyleSheet("QLabel { color: #666666; font-size: 12px; font-weight: 500; }")
# 更新按钮样式
self.close_btn.setStyleSheet("""
QPushButton {
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 14px;
font-size: 18px;
font-weight: 600;
color: #333333;
padding: 6px;
}
QPushButton:hover {
background-color: #f0f0f0;
border: 1px solid #d0d0d0;
}
QPushButton:pressed {
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
""")
self.today_btn.setStyleSheet("""
QPushButton {
background-color: #007aff;
color: #ffffff;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-weight: 500;
font-size: 12px;
}
QPushButton:hover {
background-color: #0056b3;
}
QPushButton:pressed {
background-color: #004494;
}
""")
self.clear_btn.setStyleSheet("""
QPushButton {
background-color: #ffffff;
color: #333333;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 6px 12px;
font-weight: 500;
font-size: 12px;
}
QPushButton:hover {
background-color: #f0f0f0;
border: 1px solid #d0d0d0;
}
QPushButton:pressed {
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
""")
self.insert_btn.setStyleSheet("""
QPushButton {
background-color: #34c759;
color: #ffffff;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-weight: 500;
font-size: 12px;
}
QPushButton:hover {
background-color: #2e8b57;
}
QPushButton:pressed {
background-color: #267349;
}
""")
def on_theme_changed(self, is_dark):
"""主题切换槽函数"""
self.apply_theme(is_dark)
if __name__ == "__main__":
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
# 创建并显示日历组件
calendar = CalendarWidget()
calendar.show()
sys.exit(app.exec_())

@ -0,0 +1,438 @@
# minesweeper_game.py
"""
扫雷游戏模块
用户通过鼠标点击揭开方块右键标记地雷
"""
import sys
import random
from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QMessageBox
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QRect
from PyQt5.QtGui import QPainter, QColor, QFont, QBrush, QPen
class MineButton(QPushButton):
"""自定义按钮类,用于表示扫雷游戏中的方块"""
left_clicked = pyqtSignal(int, int)
right_clicked = pyqtSignal(int, int)
def __init__(self, row, col):
super().__init__()
self.row = row
self.col = col
self.setFixedSize(30, 30)
self.setStyleSheet("""
QPushButton {
background-color: #cccccc;
border: 1px solid #999999;
font-weight: bold;
}
QPushButton:pressed {
background-color: #bbbbbb;
}
""")
self.setText("")
def mousePressEvent(self, event):
"""重写鼠标按下事件"""
if event.button() == Qt.LeftButton:
self.left_clicked.emit(self.row, self.col)
elif event.button() == Qt.RightButton:
self.right_clicked.emit(self.row, self.col)
else:
super().mousePressEvent(event)
class MinesweeperGame(QWidget):
"""扫雷游戏主类"""
# 游戏常量
ROWS = 10
COLS = 10
MINES = 15
# 游戏状态
HIDDEN = 0 # 隐藏
REVEALED = 1 # 已揭开
FLAGGED = 2 # 已标记
# 信号
game_won = pyqtSignal()
game_lost = pyqtSignal()
mine_count_changed = pyqtSignal(int)
def __init__(self):
super().__init__()
self.init_game()
self.init_ui()
def init_ui(self):
"""初始化UI"""
layout = QVBoxLayout()
layout.setSpacing(0)
layout.setContentsMargins(10, 10, 10, 10)
# 创建按钮网格
self.grid_layout = QVBoxLayout()
self.buttons = []
for row in range(self.ROWS):
button_row = []
row_layout = QHBoxLayout()
row_layout.setSpacing(0)
for col in range(self.COLS):
button = MineButton(row, col)
button.left_clicked.connect(self.on_left_click)
button.right_clicked.connect(self.on_right_click)
row_layout.addWidget(button)
button_row.append(button)
self.buttons.append(button_row)
self.grid_layout.addLayout(row_layout)
layout.addLayout(self.grid_layout)
self.setLayout(layout)
def init_game(self):
"""初始化游戏状态"""
# 初始化游戏板
self.board = [[0 for _ in range(self.COLS)] for _ in range(self.ROWS)]
self.state = [[self.HIDDEN for _ in range(self.COLS)] for _ in range(self.ROWS)]
self.mine_positions = set()
self.flag_count = 0
self.revealed_count = 0
self.game_over = False
self.first_click = True
# 放置地雷
self.place_mines()
# 计算数字
self.calculate_numbers()
def place_mines(self):
"""随机放置地雷"""
mines_placed = 0
while mines_placed < self.MINES:
row = random.randint(0, self.ROWS - 1)
col = random.randint(0, self.COLS - 1)
# 不要在第一个点击的位置放置地雷
if (row, col) not in self.mine_positions:
self.mine_positions.add((row, col))
self.board[row][col] = -1 # -1 表示地雷
mines_placed += 1
def calculate_numbers(self):
"""计算每个非地雷方块周围的地雷数量"""
for row in range(self.ROWS):
for col in range(self.COLS):
# 如果不是地雷,则计算周围地雷数
if self.board[row][col] != -1:
count = 0
# 检查周围的8个方块
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0:
continue
r, c = row + dr, col + dc
if 0 <= r < self.ROWS and 0 <= c < self.COLS:
if self.board[r][c] == -1:
count += 1
self.board[row][col] = count
def on_left_click(self, row, col):
"""处理左键点击"""
if self.game_over or self.state[row][col] == self.FLAGGED:
return
# 第一次点击时确保不会点到地雷
if self.first_click:
self.first_click = False
if (row, col) in self.mine_positions:
# 重新放置地雷
self.mine_positions.remove((row, col))
self.board[row][col] = 0
# 找一个没有地雷的位置放置新地雷
new_row, new_col = row, col
while (new_row, new_col) == (row, col) or (new_row, new_col) in self.mine_positions:
new_row = random.randint(0, self.ROWS - 1)
new_col = random.randint(0, self.COLS - 1)
self.mine_positions.add((new_row, new_col))
self.board[new_row][new_col] = -1
# 重新计算数字
self.calculate_numbers()
self.reveal_cell(row, col)
self.update_display()
# 检查游戏是否结束
if self.check_win():
self.win_game()
elif self.check_loss(row, col):
self.lose_game()
def on_right_click(self, row, col):
"""处理右键点击(标记/取消标记)"""
if self.game_over or self.state[row][col] == self.REVEALED:
return
if self.state[row][col] == self.HIDDEN:
# 标记为地雷
self.state[row][col] = self.FLAGGED
self.flag_count += 1
self.buttons[row][col].setText("🚩")
self.buttons[row][col].setStyleSheet("""
QPushButton {
background-color: #ffcc00;
border: 1px solid #999999;
font-weight: bold;
}
""")
elif self.state[row][col] == self.FLAGGED:
# 取消标记
self.state[row][col] = self.HIDDEN
self.flag_count -= 1
self.buttons[row][col].setText("")
self.buttons[row][col].setStyleSheet("""
QPushButton {
background-color: #cccccc;
border: 1px solid #999999;
font-weight: bold;
}
""")
self.mine_count_changed.emit(self.MINES - self.flag_count)
def reveal_cell(self, row, col):
"""揭开指定位置的方块"""
# 如果已经揭开或标记,则不做任何操作
if self.state[row][col] != self.HIDDEN:
return
# 揭开方块
self.state[row][col] = self.REVEALED
self.revealed_count += 1
# 如果是空白方块(周围没有地雷),则递归揭开周围的方块
if self.board[row][col] == 0:
for dr in [-1, 0, 1]:
for dc in [-1, 0, 1]:
if dr == 0 and dc == 0:
continue
r, c = row + dr, col + dc
if 0 <= r < self.ROWS and 0 <= c < self.COLS:
if self.state[r][c] == self.HIDDEN:
self.reveal_cell(r, c)
def update_display(self):
"""更新显示"""
for row in range(self.ROWS):
for col in range(self.COLS):
button = self.buttons[row][col]
cell_state = self.state[row][col]
cell_value = self.board[row][col]
if cell_state == self.REVEALED:
if cell_value == -1:
# 地雷
button.setText("💣")
button.setStyleSheet("""
QPushButton {
background-color: #ff6666;
border: 1px solid #999999;
font-weight: bold;
}
""")
else:
# 数字
button.setStyleSheet("""
QPushButton {
background-color: #eeeeee;
border: 1px solid #999999;
font-weight: bold;
}
""")
if cell_value > 0:
# 设置数字颜色
colors = ["", "#0000ff", "#008000", "#ff0000", "#000080", "#800000", "#008080", "#000000", "#808080"]
button.setText(str(cell_value))
button.setStyleSheet(f"""
QPushButton {{
background-color: #eeeeee;
border: 1px solid #999999;
color: {colors[cell_value]};
font-weight: bold;
}}
""")
else:
button.setText("")
elif cell_state == self.FLAGGED:
# 已标记的方块保持标记状态
pass
else:
# 隐藏的方块
button.setText("")
button.setStyleSheet("""
QPushButton {
background-color: #cccccc;
border: 1px solid #999999;
font-weight: bold;
}
QPushButton:pressed {
background-color: #bbbbbb;
}
""")
def check_win(self):
"""检查是否获胜"""
# 如果所有非地雷方块都已揭开,则获胜
total_cells = self.ROWS * self.COLS
return self.revealed_count == total_cells - self.MINES
def check_loss(self, row, col):
"""检查是否失败"""
# 如果点击的是地雷,则失败
return (row, col) in self.mine_positions and self.state[row][col] == self.REVEALED
def win_game(self):
"""游戏胜利"""
self.game_over = True
self.game_won.emit()
def lose_game(self):
"""游戏失败"""
self.game_over = True
# 显示所有地雷
for row in range(self.ROWS):
for col in range(self.COLS):
if (row, col) in self.mine_positions and self.state[row][col] != self.FLAGGED:
self.state[row][col] = self.REVEALED
self.update_display()
self.game_lost.emit()
def restart_game(self):
"""重新开始游戏"""
# 重置游戏状态
self.init_game()
self.update_display()
self.mine_count_changed.emit(self.MINES)
class MinesweeperWindow(QMainWindow):
"""扫雷游戏窗口"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("扫雷游戏")
self.setGeometry(200, 200, 400, 500)
# 创建中央控件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建布局
main_layout = QVBoxLayout(central_widget)
main_layout.setContentsMargins(10, 10, 10, 10)
# 创建顶部控制面板
control_layout = QHBoxLayout()
# 地雷计数器
self.mine_count_label = QLabel(f"地雷: {15}")
self.mine_count_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #cc0000;")
# 重新开始按钮
self.restart_button = QPushButton("😊")
self.restart_button.setFixedSize(40, 40)
self.restart_button.setStyleSheet("""
QPushButton {
background-color: #ffffff;
border: 2px solid #cccccc;
border-radius: 5px;
font-size: 20px;
}
QPushButton:hover {
background-color: #f0f0f0;
}
""")
self.restart_button.clicked.connect(self.restart_game)
# 时间计数器
self.time_label = QLabel("时间: 0")
self.time_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #0066cc;")
control_layout.addWidget(self.mine_count_label)
control_layout.addStretch()
control_layout.addWidget(self.restart_button)
control_layout.addStretch()
control_layout.addWidget(self.time_label)
main_layout.addLayout(control_layout)
# 创建游戏区域
self.game_widget = MinesweeperGame()
main_layout.addWidget(self.game_widget)
# 创建提示标签
self.hint_label = QLabel("左键揭开方块,右键标记地雷")
self.hint_label.setStyleSheet("font-size: 12px; color: gray; text-align: center;")
self.hint_label.setAlignment(Qt.AlignCenter)
main_layout.addWidget(self.hint_label)
# 连接信号
self.game_widget.game_won.connect(self.on_game_won)
self.game_widget.game_lost.connect(self.on_game_lost)
self.game_widget.mine_count_changed.connect(self.update_mine_count)
# 设置窗口样式
self.setStyleSheet("""
QMainWindow {
background-color: #f0f0f0;
}
QLabel {
color: #333;
}
""")
# 初始化计时器
self.timer = QTimer()
self.timer.timeout.connect(self.update_time)
self.seconds = 0
self.timer.start(1000)
def update_mine_count(self, count):
"""更新地雷计数显示"""
self.mine_count_label.setText(f"地雷: {count}")
def update_time(self):
"""更新时间显示"""
if not self.game_widget.game_over:
self.seconds += 1
self.time_label.setText(f"时间: {self.seconds}")
def on_game_won(self):
"""游戏胜利处理"""
self.timer.stop()
self.restart_button.setText("😎")
QMessageBox.information(self, "恭喜", f"你赢了!用时{self.seconds}")
def on_game_lost(self):
"""游戏失败处理"""
self.timer.stop()
self.restart_button.setText("😵")
QMessageBox.warning(self, "游戏结束", "你踩到地雷了!")
def restart_game(self):
"""重新开始游戏"""
self.timer.stop()
self.seconds = 0
self.time_label.setText("时间: 0")
self.restart_button.setText("😊")
self.game_widget.restart_game()
self.timer.start(1000)

@ -0,0 +1,439 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
每日谏言悬浮窗口
"""
import sys
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QFrame, QGraphicsDropShadowEffect)
from PyQt5.QtCore import Qt, QPoint, pyqtSignal
from PyQt5.QtGui import QFont, QColor
class QuoteFloatingWidget(QWidget):
"""每日谏言悬浮窗口"""
# 定义信号
closed = pyqtSignal() # 窗口关闭信号
refresh_requested = pyqtSignal() # 刷新请求信号
insert_requested = pyqtSignal(str) # 插入请求信号,传递要插入的文本
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground)
# 初始化变量
self.is_dragging = False
self.drag_position = QPoint()
# 设置默认谏言数据
self.quote_data = {
"quote": "书山有路勤为径,学海无涯苦作舟。",
"author": "韩愈",
"source": "《古今贤文·劝学篇》"
}
# 初始化UI
self.init_ui()
self.setup_styles()
self.apply_theme(is_dark=True) # 默认使用深色主题
def init_ui(self):
"""初始化UI"""
# 主框架
self.main_frame = QFrame(self)
self.main_frame.setObjectName("mainFrame")
self.main_frame.setFixedSize(360, 200) # 设置窗口大小
main_layout = QVBoxLayout(self.main_frame)
main_layout.setContentsMargins(12, 12, 12, 12)
main_layout.setSpacing(8)
# 标题栏
title_layout = QHBoxLayout()
title_layout.setContentsMargins(0, 0, 0, 0)
title_layout.setSpacing(0)
self.title_label = QLabel("每日谏言")
self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
title_layout.addWidget(self.title_label)
title_layout.addStretch()
# 添加一个小的固定空间,使关闭按钮向左移动
title_layout.addSpacing(25) # 向左移动25个单位
# 关闭按钮
self.close_btn = QPushButton("×")
self.close_btn.setFixedSize(20, 20)
self.close_btn.setObjectName("closeButton")
self.close_btn.clicked.connect(self.close_window)
title_layout.addWidget(self.close_btn)
main_layout.addLayout(title_layout)
# 分隔线
separator = QFrame()
separator.setObjectName("separator")
separator.setFixedHeight(1)
main_layout.addWidget(separator)
# 谏言内容区域
content_layout = QVBoxLayout()
content_layout.setContentsMargins(0, 0, 0, 0)
content_layout.setSpacing(6)
# 谏言文本
self.quote_label = QLabel()
self.quote_label.setObjectName("quoteLabel")
self.quote_label.setWordWrap(True)
self.quote_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
content_layout.addWidget(self.quote_label)
# 作者信息
self.author_label = QLabel()
self.author_label.setObjectName("authorLabel")
self.author_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
content_layout.addWidget(self.author_label)
main_layout.addLayout(content_layout)
main_layout.addStretch()
# 底部按钮区域
bottom_layout = QHBoxLayout()
bottom_layout.setContentsMargins(0, 0, 0, 0)
bottom_layout.setSpacing(8)
# 刷新按钮
self.refresh_btn = QPushButton("换一句")
self.refresh_btn.setObjectName("refreshButton")
self.refresh_btn.clicked.connect(self.on_refresh_clicked)
bottom_layout.addWidget(self.refresh_btn)
bottom_layout.addStretch()
# 插入按钮
self.insert_btn = QPushButton("插入")
self.insert_btn.setObjectName("insertButton")
self.insert_btn.clicked.connect(self.on_insert_clicked)
bottom_layout.addWidget(self.insert_btn)
main_layout.addLayout(bottom_layout)
# 设置主布局
outer_layout = QVBoxLayout(self)
outer_layout.setContentsMargins(0, 0, 0, 0)
outer_layout.addWidget(self.main_frame)
# 更新显示
self.update_quote()
def setup_styles(self):
"""设置样式"""
pass # 样式将在apply_theme中设置
def apply_theme(self, is_dark=True):
"""应用主题"""
if is_dark:
# 深色主题配色
colors = {
'surface': '#2d2d2d',
'border': '#444444',
'text': '#ffffff',
'text_secondary': '#cccccc',
'accent': '#4CAF50',
'accent_hover': '#45a049',
'button_hover': '#555555',
'error': '#f44336'
}
self.main_frame.setStyleSheet(f"""
QFrame#mainFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}}
QLabel {{
color: {colors['text']};
background-color: transparent;
padding: 4px 6px;
margin: 2px;
}}
QLabel#quoteLabel {{
color: {colors['text']};
font-size: 14px;
font-weight: 500;
padding: 6px 8px;
margin: 3px;
}}
QLabel#authorLabel {{
color: {colors['text_secondary']};
font-size: 12px;
font-style: italic;
padding: 4px 6px;
margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
background-color: rgba(255, 255, 255, 0.1);
border: none;
color: {colors['text']};
font-size: 18px;
font-weight: bold;
border-radius: 6px;
padding: 2px 4px;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
border-radius: 6px;
}}
QPushButton#refreshButton, QPushButton#insertButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 11px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{
background-color: {colors['accent_hover']};
}}
""")
else:
# 浅色主题配色
colors = {
'surface': '#ffffff',
'border': '#dddddd',
'text': '#333333',
'text_secondary': '#666666',
'accent': '#4CAF50',
'accent_hover': '#45a049',
'button_hover': '#f0f0f0',
'error': '#f44336'
}
self.main_frame.setStyleSheet(f"""
QFrame#mainFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}}
QLabel {{
color: {colors['text']};
background-color: transparent;
padding: 4px 6px;
margin: 2px;
}}
QLabel#quoteLabel {{
color: {colors['text']};
font-size: 14px;
font-weight: 500;
padding: 6px 8px;
margin: 3px;
}}
QLabel#authorLabel {{
color: {colors['text_secondary']};
font-size: 12px;
font-style: italic;
padding: 4px 6px;
margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
background-color: rgba(0, 0, 0, 0.05);
border: none;
color: {colors['text']};
font-size: 18px;
font-weight: bold;
border-radius: 6px;
padding: 2px 4px;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
border-radius: 6px;
}}
QPushButton#refreshButton, QPushButton#insertButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 11px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{
background-color: {colors['accent_hover']};
}}
""")
def update_quote(self, quote_data=None):
"""更新谏言显示"""
if quote_data:
self.quote_data = quote_data
else:
# 如果没有提供数据,使用默认数据
if not hasattr(self, 'quote_data'):
self.quote_data = {
"quote": "书山有路勤为径,学海无涯苦作舟。",
"author": "韩愈",
"source": "《古今贤文·劝学篇》"
}
# 更新显示
self.quote_label.setText(self.quote_data["quote"])
author_info = f"{self.quote_data['author']}"
if self.quote_data.get("source"):
author_info += f"{self.quote_data['source']}"
self.author_label.setText(author_info)
def on_refresh_clicked(self):
"""刷新按钮点击事件"""
# 发送刷新请求信号
self.refresh_requested.emit()
# 同时直接获取新的内容并更新显示
self.fetch_and_update_quote()
def on_insert_clicked(self):
"""插入按钮点击事件"""
# 发送插入请求信号,传递完整的诗句信息
quote = self.quote_data.get("quote", "")
author = self.quote_data.get("author", "佚名")
source = self.quote_data.get("source", "")
# 构造完整的诗句文本
if source:
full_quote_text = f"{quote} —— {author}{source}"
else:
full_quote_text = f"{quote} —— {author}"
if quote:
self.insert_requested.emit(full_quote_text)
def fetch_and_update_quote(self):
"""获取新的谏言内容并更新显示"""
try:
# 尝试获取古诗词
import requests
import random
try:
# 使用古诗词·一言API
response = requests.get("https://v1.jinrishici.com/all.json", timeout=5, verify=False)
if response.status_code == 200:
data = response.json()
content = data.get('content', '')
author = data.get('author', '佚名')
origin = data.get('origin', '')
if content:
quote_data = {
"quote": content,
"author": author,
"source": origin
}
self.update_quote(quote_data)
return
except Exception as e:
print(f"获取古诗词失败: {e}")
# 如果古诗词获取失败使用备用API
try:
# 使用每日一言API
response = requests.get("https://api.nxvav.cn/api/yiyan?json=true", timeout=5, verify=False)
if response.status_code == 200:
data = response.json()
yiyan = data.get('yiyan', '')
nick = data.get('nick', '佚名')
if yiyan:
quote_data = {
"quote": yiyan,
"author": nick,
"source": ""
}
self.update_quote(quote_data)
return
except Exception as e:
print(f"获取每日一言失败: {e}")
# 如果API都失败使用预设内容
quotes = [
{"quote": "学而时习之,不亦说乎?", "author": "孔子", "source": "《论语》"},
{"quote": "千里之行,始于足下。", "author": "老子", "source": "《道德经》"},
{"quote": "天行健,君子以自强不息。", "author": "佚名", "source": "《周易》"},
{"quote": "书山有路勤为径,学海无涯苦作舟。", "author": "韩愈", "source": "《古今贤文·劝学篇》"},
{"quote": "山重水复疑无路,柳暗花明又一村。", "author": "陆游", "source": "《游山西村》"}
]
# 随机选择一个名言
new_quote = random.choice(quotes)
self.update_quote(new_quote)
except Exception as e:
print(f"获取新谏言失败: {e}")
# 出错时显示默认内容
default_quote = {
"quote": "书山有路勤为径,学海无涯苦作舟。",
"author": "韩愈",
"source": "《古今贤文·劝学篇》"
}
self.update_quote(default_quote)
def close_window(self):
"""关闭窗口 - 只是隐藏而不是销毁"""
try:
self.closed.emit()
self.hide() # 隐藏窗口而不是销毁
except Exception as e:
print(f"Error in close_window: {e}")
def mousePressEvent(self, event):
"""鼠标按下事件,用于拖拽"""
if event.button() == Qt.LeftButton:
# 检查是否点击在标题栏区域
if event.pos().y() <= 40: # 假设标题栏高度为40像素
self.is_dragging = True
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
"""鼠标移动事件,用于拖拽"""
if self.is_dragging and event.buttons() == Qt.LeftButton:
self.move(event.globalPos() - self.drag_position)
event.accept()
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
self.is_dragging = False
def main():
"""测试函数"""
app = QApplication(sys.argv)
# 创建并显示窗口
widget = QuoteFloatingWidget()
widget.show()
# 移动到屏幕中心
screen_geometry = app.desktop().screenGeometry()
widget.move(
(screen_geometry.width() - widget.width()) // 2,
(screen_geometry.height() - widget.height()) // 2
)
sys.exit(app.exec_())
if __name__ == "__main__":
main()

@ -0,0 +1,437 @@
# snake_game.py
"""
贪吃蛇小游戏模块
用户用WASD或方向键控制贪吃蛇移动
"""
import sys
import random
from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLabel, QMessageBox
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QRect
from PyQt5.QtGui import QPainter, QColor, QFont, QBrush, QPen
class SnakeGame(QWidget):
"""贪吃蛇游戏画布"""
# 游戏常量
GRID_SIZE = 20 # 网格大小
GRID_WIDTH = 30 # 水平网格数
GRID_HEIGHT = 20 # 垂直网格数
GAME_SPEED = 150 # 游戏速度(毫秒)
MIN_SPEED = 50 # 最小速度(毫秒)
MAX_SPEED = 300 # 最大速度(毫秒)
SPEED_STEP = 10 # 速度调节步长(毫秒)
# 信号
score_changed = pyqtSignal(int)
game_over = pyqtSignal(int)
speed_changed = pyqtSignal(int) # 速度改变信号
# 方向常量
UP = (0, -1)
DOWN = (0, 1)
LEFT = (-1, 0)
RIGHT = (1, 0)
def __init__(self):
super().__init__()
self.init_game()
self.init_ui()
def init_ui(self):
"""初始化UI"""
self.setFixedSize(
self.GRID_WIDTH * self.GRID_SIZE,
self.GRID_HEIGHT * self.GRID_SIZE
)
self.setStyleSheet("background-color: #1a1a1a;")
self.setFocus() # 获取焦点以接收键盘输入
def init_game(self):
"""初始化游戏状态"""
# 蛇的初始位置(从中间开始)
self.snake = [
(self.GRID_WIDTH // 2, self.GRID_HEIGHT // 2),
(self.GRID_WIDTH // 2 - 1, self.GRID_HEIGHT // 2),
(self.GRID_WIDTH // 2 - 2, self.GRID_HEIGHT // 2),
]
# 方向
self.direction = self.RIGHT
self.next_direction = self.RIGHT
# 食物位置
self.food = self.generate_food()
# 分数
self.score = 0
# 游戏速度
self.current_speed = self.GAME_SPEED
# 游戏状态
self.is_running = False
self.is_game_over = False
# 游戏定时器
self.game_timer = QTimer()
self.game_timer.timeout.connect(self.update_game)
def generate_food(self):
"""生成食物位置"""
while True:
x = random.randint(0, self.GRID_WIDTH - 1)
y = random.randint(0, self.GRID_HEIGHT - 1)
if (x, y) not in self.snake:
return (x, y)
def start_game(self):
"""开始游戏"""
if not self.is_running:
self.is_running = True
self.is_game_over = False
self.game_timer.start(self.current_speed)
self.setFocus()
def pause_game(self):
"""暂停游戏"""
if self.is_running:
self.is_running = False
self.game_timer.stop()
def resume_game(self):
"""恢复游戏"""
if not self.is_running and not self.is_game_over:
self.is_running = True
self.game_timer.start(self.current_speed)
def restart_game(self):
"""重新开始游戏"""
self.game_timer.stop()
self.init_game()
self.score_changed.emit(0)
self.speed_changed.emit(self.current_speed)
self.update()
# 重新启动游戏
self.start_game()
def increase_speed(self):
"""增加游戏速度"""
if self.current_speed > self.MIN_SPEED:
self.current_speed = max(self.current_speed - self.SPEED_STEP, self.MIN_SPEED)
self.speed_changed.emit(self.current_speed)
if self.is_running:
self.game_timer.setInterval(self.current_speed)
def decrease_speed(self):
"""降低游戏速度"""
if self.current_speed < self.MAX_SPEED:
self.current_speed = min(self.current_speed + self.SPEED_STEP, self.MAX_SPEED)
self.speed_changed.emit(self.current_speed)
if self.is_running:
self.game_timer.setInterval(self.current_speed)
def update_game(self):
"""更新游戏状态"""
if not self.is_running:
return
# 更新方向
self.direction = self.next_direction
# 计算新的头部位置
head_x, head_y = self.snake[0]
dx, dy = self.direction
new_head = (head_x + dx, head_y + dy)
# 检查碰撞
if self.check_collision(new_head):
self.is_running = False
self.is_game_over = True
self.game_timer.stop()
self.game_over.emit(self.score)
self.update()
return
# 添加新的头部
self.snake.insert(0, new_head)
# 检查是否吃到食物
if new_head == self.food:
self.score += 10
self.score_changed.emit(self.score)
self.food = self.generate_food()
else:
# 移除尾部
self.snake.pop()
self.update()
def check_collision(self, position):
"""检查碰撞"""
x, y = position
# 检查边界碰撞
if x < 0 or x >= self.GRID_WIDTH or y < 0 or y >= self.GRID_HEIGHT:
return True
# 检查自身碰撞
if position in self.snake:
return True
return False
def keyPressEvent(self, event):
"""处理键盘输入"""
if event.isAutoRepeat():
return
key = event.key()
# 使用WASD或方向键控制方向
if key in (Qt.Key_W, Qt.Key_Up):
# 上键:向上
if self.direction != self.DOWN:
self.next_direction = self.UP
elif key in (Qt.Key_S, Qt.Key_Down):
# 下键:向下
if self.direction != self.UP:
self.next_direction = self.DOWN
elif key in (Qt.Key_A, Qt.Key_Left):
# 左键:向左(不用于调速)
if self.direction != self.RIGHT:
self.next_direction = self.LEFT
elif key in (Qt.Key_D, Qt.Key_Right):
# 右键:向右(不用于调速)
if self.direction != self.LEFT:
self.next_direction = self.RIGHT
elif key == Qt.Key_Space:
# 空格键暂停/恢复
if self.is_running:
self.pause_game()
elif not self.is_game_over:
self.resume_game()
elif key == Qt.Key_R:
# R键重新开始
if self.is_game_over:
self.restart_game()
elif key == Qt.Key_Plus or key == Qt.Key_Equal:
# + 或 = 键加速
self.increase_speed()
elif key == Qt.Key_Minus:
# - 键减速
self.decrease_speed()
event.accept()
def paintEvent(self, event):
"""绘制游戏"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 绘制网格背景
self.draw_grid(painter)
# 绘制食物
self.draw_food(painter)
# 绘制蛇
self.draw_snake(painter)
# 如果游戏结束,显示游戏结束提示
if self.is_game_over:
self.draw_game_over(painter)
def draw_grid(self, painter):
"""绘制网格"""
painter.setPen(QPen(QColor(50, 50, 50), 1))
# 绘制竖线
for x in range(self.GRID_WIDTH + 1):
painter.drawLine(
x * self.GRID_SIZE, 0,
x * self.GRID_SIZE, self.GRID_HEIGHT * self.GRID_SIZE
)
# 绘制横线
for y in range(self.GRID_HEIGHT + 1):
painter.drawLine(
0, y * self.GRID_SIZE,
self.GRID_WIDTH * self.GRID_SIZE, y * self.GRID_SIZE
)
def draw_snake(self, painter):
"""绘制蛇"""
# 绘制蛇身
for i, (x, y) in enumerate(self.snake):
if i == 0:
# 蛇头 - 更亮的绿色
painter.fillRect(
x * self.GRID_SIZE + 1,
y * self.GRID_SIZE + 1,
self.GRID_SIZE - 2,
self.GRID_SIZE - 2,
QColor(0, 255, 0)
)
# 绘制眼睛
painter.fillRect(
x * self.GRID_SIZE + 4,
y * self.GRID_SIZE + 4,
3, 3,
QColor(255, 0, 0)
)
else:
# 蛇身 - 稍暗的绿色
painter.fillRect(
x * self.GRID_SIZE + 1,
y * self.GRID_SIZE + 1,
self.GRID_SIZE - 2,
self.GRID_SIZE - 2,
QColor(0, 200, 0)
)
def draw_food(self, painter):
"""绘制食物"""
x, y = self.food
painter.fillRect(
x * self.GRID_SIZE + 3,
y * self.GRID_SIZE + 3,
self.GRID_SIZE - 6,
self.GRID_SIZE - 6,
QColor(255, 0, 0)
)
def draw_game_over(self, painter):
"""绘制游戏结束界面"""
# 半透明黑色背景
painter.fillRect(self.rect(), QColor(0, 0, 0, 200))
# 绘制文本
painter.setPen(QColor(255, 255, 255))
font = QFont("Arial", 30, QFont.Bold)
painter.setFont(font)
text = "游戏结束"
fm = painter.fontMetrics()
text_width = fm.width(text)
text_height = fm.height()
x = (self.width() - text_width) // 2
y = (self.height() - text_height) // 2 - 20
painter.drawText(x, y, text)
# 绘制分数
font.setPointSize(20)
painter.setFont(font)
score_text = f"最终分数: {self.score}"
fm = painter.fontMetrics()
score_width = fm.width(score_text)
score_x = (self.width() - score_width) // 2
score_y = y + 50
painter.drawText(score_x, score_y, score_text)
# 绘制提示
font.setPointSize(12)
painter.setFont(font)
hint_text = "按R键重新开始"
fm = painter.fontMetrics()
hint_width = fm.width(hint_text)
hint_x = (self.width() - hint_width) // 2
hint_y = score_y + 40
painter.drawText(hint_x, hint_y, hint_text)
class SnakeGameWindow(QMainWindow):
"""贪吃蛇游戏窗口"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("贪吃蛇游戏")
self.setGeometry(200, 200, 700, 550)
# 创建中央控件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 创建布局
layout = QVBoxLayout(central_widget)
layout.setContentsMargins(10, 10, 10, 10)
# 创建游戏画布
self.game_widget = SnakeGame()
layout.addWidget(self.game_widget)
# 创建控制面板
control_layout = QVBoxLayout()
# 分数标签
self.score_label = QLabel("分数: 0")
self.score_label.setStyleSheet("font-size: 16px; font-weight: bold;")
control_layout.addWidget(self.score_label)
# 速度标签
self.speed_label = QLabel("速度: 正常")
self.speed_label.setStyleSheet("font-size: 14px; color: #0066cc;")
control_layout.addWidget(self.speed_label)
# 提示标签
self.hint_label = QLabel(
"控制方法: W/↑ 上 S/↓ 下 A/← 左 D/→ 右 | 空格暂停 | +/- 调速 | R重新开始"
)
self.hint_label.setStyleSheet("font-size: 12px; color: gray;")
control_layout.addWidget(self.hint_label)
layout.addLayout(control_layout)
# 连接信号
self.game_widget.score_changed.connect(self.update_score)
self.game_widget.game_over.connect(self.on_game_over)
self.game_widget.speed_changed.connect(self.update_speed)
# 设置窗口样式
self.setStyleSheet("""
QMainWindow {
background-color: #f0f0f0;
}
QLabel {
color: #333;
}
""")
# 启动游戏
self.game_widget.start_game()
def update_score(self, score):
"""更新分数显示"""
self.score_label.setText(f"分数: {score}")
def update_speed(self, speed_ms):
"""更新速度显示"""
# 将毫秒转换为速度等级
if speed_ms <= 50:
speed_level = "极快"
elif speed_ms <= 100:
speed_level = "很快"
elif speed_ms <= 150:
speed_level = "正常"
elif speed_ms <= 200:
speed_level = "稍慢"
else:
speed_level = "很慢"
self.speed_label.setText(f"速度: {speed_level} ({speed_ms}ms)")
def on_game_over(self, final_score):
"""游戏结束处理"""
pass # 游戏结束信息已在游戏画布中显示
def keyPressEvent(self, event):
"""处理键盘输入"""
if event.key() == Qt.Key_R:
self.game_widget.restart_game()
else:
super().keyPressEvent(event)

@ -153,69 +153,99 @@ class ThemeManager(QObject):
return self._get_light_stylesheet()
def _get_dark_stylesheet(self):
"""深色主题样式表"""
"""深色主题样式表 - 优化版Apple设计风格"""
return """
/* 深色主题样式 */
/* 优化版Apple设计风格深色主题样式 */
/* 全局文字颜色 */
/* 全局文字颜色和字体 - 使用Apple系统字体 */
QWidget {
color: #e0e0e0;
color: #e8e8ed;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 13px;
}
/* 主窗口 */
/* 主窗口 - 优化后的深色背景 */
QMainWindow {
background-color: #1e1e1e;
background-color: #1c1c1e;
}
/* 菜单栏 */
/* 菜单栏 - 优化版Apple深色风格 */
QMenuBar {
background-color: #0078d7;
border: 1px solid #005a9e;
font-size: 12px;
color: #ffffff;
background-color: #2c2c2e;
border: none;
border-bottom: 1px solid #3a3a3c;
font-size: 13px;
color: #e8e8ed;
padding: 4px 0;
}
QMenuBar::item {
background-color: transparent;
padding: 4px 10px;
color: #e0e0e0;
padding: 6px 12px;
color: #e8e8ed;
border-radius: 6px;
margin: 0 2px;
}
QMenuBar::item:selected {
background-color: #106ebe;
background-color: #3a3a3c;
color: #e8e8ed;
}
QMenuBar::item:pressed {
background-color: #4a4a4c;
color: #e8e8ed;
}
/* 菜单 */
/* 菜单 - 优化版Apple深色风格 */
QMenu {
background-color: #2d2d2d;
border: 1px solid #3c3c3c;
font-size: 12px;
color: #e0e0e0;
background-color: #2c2c2e;
border: 1px solid #3a3a3c;
border-radius: 8px;
font-size: 13px;
color: #e8e8ed;
padding: 4px 0;
margin: 2px;
}
QMenu::item {
padding: 4px 20px;
color: #e0e0e0;
color: #e8e8ed;
background-color: transparent;
border-radius: 6px;
margin: 0 4px;
padding: 6px 20px;
}
QMenu::item:selected {
background-color: #3c3c3c;
background-color: #0a84ff;
color: #ffffff;
}
/* 功能区 */
QMenu::item:pressed {
background-color: #0066cc;
color: #ffffff;
}
QMenu::separator {
height: 1px;
background-color: #3a3a3c;
margin: 4px 8px;
}
/* 功能区 - 优化背景色 */
QFrame {
background-color: #2d2d2d;
border: 1px solid #3c3c3c;
background-color: #1c1c1e;
border: none;
}
/* 组框 */
/* 组框 - 优化标题颜色 */
QGroupBox {
font-size: 11px;
font-size: 12px;
font-weight: normal;
color: #e0e0e0;
background-color: #2d2d2d;
border: 1px solid #3c3c3c;
border-radius: 0px;
color: #e8e8ed;
background-color: #1c1c1e;
border: none;
border-radius: 8px;
margin-top: 5px;
padding-top: 5px;
}
@ -224,250 +254,319 @@ class ThemeManager(QObject):
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #e0e0e0;
color: #8a8a8d;
}
/* 按钮 */
/* 工具按钮 - 优化版Apple深色风格 */
QToolButton {
border: 1px solid #3c3c3c;
border-radius: 3px;
background-color: #3c3c3c;
font-size: 11px;
color: #e0e0e0;
padding: 3px 6px;
border: 1px solid transparent;
border-radius: 6px;
background-color: #2c2c2e;
font-size: 13px;
color: #e8e8ed;
padding: 6px 12px;
}
QToolButton:hover {
background-color: #4a4a4a;
border: 1px solid #5a5a5a;
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
}
QToolButton:pressed {
background-color: #2a2a2a;
border: 1px solid #1a1a1a;
background-color: #4a4a4c;
border: 1px solid #5a5a5c;
}
QToolButton:checked {
background-color: #0078d4;
border: 1px solid #106ebe;
background-color: #0a84ff;
border: 1px solid #0a84ff;
color: #ffffff;
}
/* 切换按钮 */
/* 切换按钮 - 优化样式 */
QToolButton[checkable="true"] {
border: 1px solid #3c3c3c;
border-radius: 2px;
background-color: #3c3c3c;
border: 1px solid #3a3a3c;
border-radius: 6px;
background-color: #2c2c2e;
font-size: 12px;
font-weight: bold;
color: #e0e0e0;
color: #e8e8ed;
padding: 6px 12px;
}
QToolButton[checkable="true"]:hover {
background-color: #4a4a4a;
background-color: #3a3a3c;
}
QToolButton[checkable="true"]:checked {
background-color: #0078d4;
border: 1px solid #106ebe;
background-color: #0a84ff;
border: 1px solid #0a84ff;
color: #ffffff;
}
/* 下拉框 - 修复文字不可见问题 */
/* 下拉框 - 优化版Apple深色风格 */
QComboBox {
background-color: #3c3c3c;
border: 1px solid #5a5a5a;
border-radius: 2px;
color: #e0e0e0;
padding: 2px 5px;
selection-background-color: #4a4a4a;
selection-color: #e0e0e0;
background-color: #2c2c2e;
border: 1px solid #3a3a3c;
border-radius: 6px;
color: #e8e8ed;
padding: 4px 8px;
selection-background-color: #0a84ff;
selection-color: #ffffff;
}
QComboBox:hover {
background-color: #4a4a4a;
border: 1px solid #6a6a6a;
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
}
QComboBox::drop-down {
border: none;
width: 15px;
background-color: #3c3c3c;
width: 20px;
border-left: 1px solid #3a3a3c;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
QComboBox::down-arrow {
image: none;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 5px solid #e0e0e0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #8a8a8d;
margin: 6px;
}
/* 下拉框弹出列表 */
QComboBox QAbstractItemView {
background-color: #3c3c3c;
border: 1px solid #5a5a5a;
color: #e0e0e0;
selection-background-color: #4a4a4a;
selection-color: #e0e0e0;
}
/* 字体下拉框特殊处理 */
QFontComboBox {
background-color: #3c3c3c;
border: 1px solid #5a5a5a;
border-radius: 2px;
color: #e0e0e0;
padding: 2px 5px;
selection-background-color: #4a4a4a;
selection-color: #e0e0e0;
}
QFontComboBox:hover {
background-color: #4a4a4a;
border: 1px solid #6a6a6a;
}
QFontComboBox QAbstractItemView {
background-color: #3c3c3c;
border: 1px solid #5a5a5a;
color: #e0e0e0;
selection-background-color: #4a4a4a;
selection-color: #e0e0e0;
background-color: #1c1c1e;
border: 1px solid #3a3a3c;
color: #e8e8ed;
selection-background-color: #0a84ff;
selection-color: #ffffff;
}
/* 文本编辑 */
/* 文本编辑区域 - 优化版Apple深色风格 */
QTextEdit {
background-color: #1e1e1e;
border: 1px solid #3c3c3c;
color: #e0e0e0;
padding: 20px;
line-height: 1.5;
background-color: #121212;
border: none;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 15px;
color: #e8e8ed;
padding: 32px;
line-height: 1.6;
selection-background-color: #0a84ff;
selection-color: #ffffff;
}
/* 状态栏 */
/* 状态栏 - 优化版Apple深色风格 */
QStatusBar {
background-color: #2d2d2d;
border-top: 1px solid #3c3c3c;
font-size: 11px;
color: #e0e0e0;
background-color: #2c2c2e;
border-top: 1px solid #3a3a3c;
font-size: 12px;
color: #8a8a8d;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
padding: 6px 12px;
}
/* 标签 */
/* 标签 - 优化颜色 */
QLabel {
color: #e0e0e0;
color: #e8e8ed;
background-color: transparent;
}
/* 消息框 - 修复黑色背景问题 */
/* 消息框 - Apple深色风格 */
QMessageBox {
background-color: #2d2d2d;
color: #e0e0e0;
background-color: #2c2c2e;
color: #f0f0f0;
border-radius: 12px;
}
QMessageBox QPushButton {
background-color: #3c3c3c;
color: #e0e0e0;
border: 1px solid #5a5a5a;
border-radius: 3px;
padding: 5px 15px;
background-color: #3a3a3c;
color: #f0f0f0;
border: 1px solid #4a4a4c;
border-radius: 6px;
padding: 6px 16px;
min-width: 80px;
}
QMessageBox QPushButton:hover {
background-color: #4a4a4a;
border: 1px solid #6a6a6a;
background-color: #4a4a4c;
border: 1px solid #5a5a5c;
}
QMessageBox QPushButton:pressed {
background-color: #2a2a2a;
border: 1px solid #1a1a1a;
background-color: #5a5a5c;
border: 1px solid #6a6a6c;
}
QMessageBox QPushButton:default {
background-color: #0a84ff;
color: #ffffff;
border: 1px solid #0a84ff;
}
QMessageBox QPushButton:default:hover {
background-color: #0066cc;
border: 1px solid #0066cc;
}
/* 滚动条 */
QMessageBox QPushButton:default:pressed {
background-color: #004d99;
border: 1px solid #004d99;
}
/* 滚动条 - Apple深色风格 */
QScrollBar:vertical {
background-color: #2d2d2d;
width: 12px;
background-color: transparent;
width: 8px;
border: none;
}
QScrollBar::handle:vertical {
background-color: #5a5a5a;
border-radius: 6px;
background-color: #5a5a5c;
border-radius: 4px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #6a6a6a;
background-color: #6a6a6c;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
border: none;
background: none;
}
/* 按钮 - 优化版Apple深色风格 */
QPushButton {
background-color: #2c2c2e;
color: #e8e8ed;
border: 1px solid #3a3a3c;
border-radius: 6px;
padding: 6px 16px;
font-size: 13px;
}
QPushButton:hover {
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
}
QPushButton:pressed {
background-color: #4a4a4c;
border: 1px solid #5a5a5c;
}
QPushButton:default {
background-color: #0a84ff;
color: #ffffff;
border: 1px solid #0a84ff;
}
QPushButton:default:hover {
background-color: #0066cc;
border: 1px solid #0066cc;
}
QPushButton:default:pressed {
background-color: #004d99;
border: 1px solid #004d99;
}
"""
def _get_light_stylesheet(self):
"""浅色主题样式表 - 白底黑字"""
"""浅色主题样式表 - Apple设计风格"""
return """
/* 浅色主题样式 - 白底黑字 */
/* Apple设计风格浅色主题样式 */
/* 全局文字颜色 */
/* 全局文字颜色和字体 - 使用Apple系统字体 */
QWidget {
color: #333333;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 13px;
}
/* 主窗口 */
/* 主窗口 - 纯净白色背景 */
QMainWindow {
background-color: #f3f2f1;
background-color: #ffffff;
}
/* 菜单栏 */
/* 菜单栏 - Apple风格 */
QMenuBar {
background-color: #0078d7;
border: 1px solid #005a9e;
font-size: 12px;
color: #ffffff;
background-color: #ffffff;
border: none;
border-bottom: 1px solid #e0e0e0;
font-size: 13px;
color: #333333;
padding: 4px 0;
}
QMenuBar::item {
background-color: transparent;
padding: 4px 10px;
padding: 6px 12px;
color: #333333;
border-radius: 4px;
margin: 0 1px;
}
QMenuBar::item:selected {
background-color: #106ebe;
background-color: #f0f0f0;
color: #333333;
}
/* 菜单 */
QMenuBar::item:pressed {
background-color: #e0e0e0;
color: #333333;
}
/* 菜单 - Apple风格 */
QMenu {
background-color: #ffffff;
border: 1px solid #d0d0d0;
font-size: 12px;
border-radius: 8px;
font-size: 13px;
color: #333333;
padding: 4px 0;
margin: 2px;
}
QMenu::item {
padding: 4px 20px;
color: #333333;
background-color: transparent;
border-radius: 4px;
margin: 0 4px;
padding: 4px 20px;
}
QMenu::item:selected {
background-color: #f0f0f0;
background-color: #007aff;
color: #ffffff;
}
QMenu::item:pressed {
background-color: #0062cc;
color: #ffffff;
}
QMenu::separator {
height: 1px;
background-color: #e0e0e0;
margin: 4px 8px;
}
/* 功能区 */
QFrame {
background-color: #ffffff;
border: 1px solid #d0d0d0;
border: none;
}
/* 组框 */
QGroupBox {
font-size: 11px;
font-size: 12px;
font-weight: normal;
color: #333333;
background-color: #ffffff;
border: 1px solid #d0d0d0;
border-radius: 0px;
border: none;
border-radius: 8px;
margin-top: 5px;
padding-top: 5px;
}
@ -476,145 +575,116 @@ class ThemeManager(QObject):
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #333333;
color: #666666;
}
/* 按钮 */
/* 工具按钮 - Apple风格 */
QToolButton {
border: 1px solid #d0d0d0;
border-radius: 3px;
background-color: #ffffff;
font-size: 11px;
border: 1px solid transparent;
border-radius: 6px;
background-color: #f6f6f6;
font-size: 13px;
color: #333333;
padding: 3px 6px;
padding: 6px 12px;
}
QToolButton:hover {
background-color: #f0f0f0;
border: 1px solid #0078d7;
background-color: #e0e0e0;
border: 1px solid #d0d0d0;
}
QToolButton:pressed {
background-color: #e0e0e0;
border: 1px solid #005a9e;
background-color: #d0d0d0;
border: 1px solid #c0c0c0;
}
QToolButton:checked {
background-color: #0078d7;
border: 1px solid #005a9e;
background-color: #007aff;
border: 1px solid #007aff;
color: #ffffff;
}
/* 切换按钮 */
QToolButton[checkable="true"] {
border: 1px solid #d0d0d0;
border-radius: 2px;
background-color: #ffffff;
border-radius: 6px;
background-color: #f6f6f6;
font-size: 12px;
font-weight: bold;
color: #333333;
padding: 6px 12px;
}
QToolButton[checkable="true"]:hover {
background-color: #f0f0f0;
background-color: #e0e0e0;
}
QToolButton[checkable="true"]:checked {
background-color: #0078d7;
border: 1px solid #005a9e;
background-color: #007aff;
border: 1px solid #007aff;
color: #ffffff;
}
/* 下拉框 - 白底黑字 */
/* 下拉框 - Apple风格 */
QComboBox {
background-color: #ffffff;
background-color: #f6f6f6;
border: 1px solid #d0d0d0;
border-radius: 2px;
border-radius: 6px;
color: #333333;
padding: 2px 5px;
selection-background-color: #f0f0f0;
selection-color: #333333;
padding: 4px 8px;
selection-background-color: #007aff;
selection-color: #ffffff;
}
QComboBox:hover {
background-color: #f0f0f0;
border: 1px solid #0078d7;
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
QComboBox::drop-down {
border: none;
width: 15px;
background-color: #ffffff;
width: 20px;
border-left: 1px solid #d0d0d0;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
QComboBox::down-arrow {
image: none;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 5px solid #333333;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #666666;
margin: 6px;
}
/* 下拉框弹出列表 */
QComboBox QAbstractItemView {
background-color: #ffffff;
border: 1px solid #d0d0d0;
color: #333333;
selection-background-color: #f0f0f0;
selection-color: #333333;
}
/* 字体下拉框特殊处理 - 白底黑字 */
QFontComboBox {
background-color: #ffffff;
border: 1px solid #d0d0d0;
border-radius: 2px;
color: #333333;
padding: 2px 5px;
selection-background-color: #f0f0f0;
selection-color: #333333;
}
QFontComboBox:hover {
background-color: #f0f0f0;
border: 1px solid #0078d7;
selection-background-color: #007aff;
selection-color: #ffffff;
}
QFontComboBox::drop-down {
border: none;
width: 15px;
background-color: #ffffff;
}
QFontComboBox::down-arrow {
image: none;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 5px solid #333333;
}
QFontComboBox QAbstractItemView {
background-color: #ffffff;
border: 1px solid #d0d0d0;
color: #333333;
selection-background-color: #f0f0f0;
selection-color: #333333;
}
/* 文本编辑器 */
/* 文本编辑区域 - Apple风格 */
QTextEdit {
background-color: #ffffff;
border: 1px solid #d0d0d0;
color: #000000;
padding: 20px;
border: none;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
font-size: 15px;
color: #333333;
padding: 32px;
line-height: 1.5;
selection-background-color: #b3d9ff;
selection-color: #333333;
}
/* 状态栏 */
/* 状态栏 - Apple风格 */
QStatusBar {
background-color: #ffffff;
border-top: 1px solid #d0d0d0;
font-size: 11px;
color: #333333;
background-color: #f6f6f6;
border-top: 1px solid #e0e0e0;
font-size: 12px;
color: #666666;
font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
padding: 6px 12px;
}
/* 标签 */
@ -623,41 +693,58 @@ class ThemeManager(QObject):
background-color: transparent;
}
/* 消息框 - 修复黑色背景问题 */
/* 消息框 - Apple风格 */
QMessageBox {
background-color: #ffffff;
color: #333333;
border-radius: 12px;
}
QMessageBox QPushButton {
background-color: #ffffff;
background-color: #f6f6f6;
color: #333333;
border: 1px solid #d0d0d0;
border-radius: 3px;
padding: 5px 15px;
border-radius: 6px;
padding: 6px 16px;
min-width: 80px;
}
QMessageBox QPushButton:hover {
background-color: #f0f0f0;
border: 1px solid #0078d7;
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
QMessageBox QPushButton:pressed {
background-color: #e0e0e0;
border: 1px solid #005a9e;
background-color: #d0d0d0;
border: 1px solid #a0a0a0;
}
QMessageBox QPushButton:default {
background-color: #007aff;
color: #ffffff;
border: 1px solid #007aff;
}
QMessageBox QPushButton:default:hover {
background-color: #0062cc;
border: 1px solid #0062cc;
}
QMessageBox QPushButton:default:pressed {
background-color: #004a99;
border: 1px solid #004a99;
}
/* 滚动条 */
/* 滚动条 - Apple风格 */
QScrollBar:vertical {
background-color: #ffffff;
width: 12px;
background-color: transparent;
width: 8px;
border: none;
}
QScrollBar::handle:vertical {
background-color: #c0c0c0;
border-radius: 6px;
border-radius: 4px;
min-height: 20px;
}
@ -669,6 +756,42 @@ class ThemeManager(QObject):
border: none;
background: none;
}
/* 按钮 - Apple风格 */
QPushButton {
background-color: #f6f6f6;
color: #333333;
border: 1px solid #d0d0d0;
border-radius: 6px;
padding: 6px 16px;
font-size: 13px;
}
QPushButton:hover {
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
QPushButton:pressed {
background-color: #d0d0d0;
border: 1px solid #a0a0a0;
}
QPushButton:default {
background-color: #007aff;
color: #ffffff;
border: 1px solid #007aff;
}
QPushButton:default:hover {
background-color: #0062cc;
border: 1px solid #0062cc;
}
QPushButton:default:pressed {
background-color: #004a99;
border: 1px solid #004a99;
}
"""
def set_dark_theme(self, is_dark):
@ -692,22 +815,28 @@ class ThemeManager(QObject):
'background': '#1e1e1e',
'surface': '#2d2d2d',
'surface_hover': '#3c3c3c',
'surface_pressed': '#4a4a4c',
'text': '#e0e0e0',
'text_secondary': '#b0b0b0',
'text_disabled': '#8a8a8d',
'border': '#3c3c3c',
'accent': '#0078d4',
'accent_hover': '#106ebe'
'accent_hover': '#106ebe',
'accent_pressed': '#005a9e'
}
else:
return {
'background': '#f3f2f1',
'surface': '#ffffff',
'surface_hover': '#f0f0f0',
'surface_pressed': '#e0e0e0',
'text': '#333333',
'text_secondary': '#666666',
'text_disabled': '#999999',
'border': '#d0d0d0',
'accent': '#0078d7',
'accent_hover': '#005a9e'
'accent_hover': '#005a9e',
'accent_pressed': '#004a99'
}

@ -0,0 +1,577 @@
# -*- coding: utf-8 -*-
"""
天气悬浮窗口模块
提供一个可拖拽的天气悬浮窗口显示当前天气信息
"""
import sys
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFrame, QTextEdit, QDialog, QComboBox
)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QPoint, QThread
from PyQt5.QtGui import QFont, QPalette, QColor
# 导入主题管理器
from .theme_manager import theme_manager
class WeatherFloatingWidget(QDialog):
"""天气悬浮窗口类"""
# 自定义信号
closed = pyqtSignal()
refresh_requested = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.drag_position = None
self.is_dragging = False
self.weather_data = None
self.setup_ui()
self.setup_connections()
self.setup_theme()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
self.setAttribute(Qt.WA_TranslucentBackground)
def setup_ui(self):
"""设置UI界面"""
# 设置窗口属性
self.setWindowTitle("天气")
self.setFixedSize(320, 240) # 调整窗口尺寸使其更紧凑
# 创建主框架,用于实现圆角和阴影效果
self.main_frame = QFrame()
self.main_frame.setObjectName("mainFrame")
# 主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(self.main_frame)
# 内容布局
content_layout = QVBoxLayout(self.main_frame)
content_layout.setContentsMargins(12, 12, 12, 12) # 优化内边距
content_layout.setSpacing(8) # 优化间距
# 标题栏
title_layout = QHBoxLayout()
title_layout.setContentsMargins(0, 0, 0, 0)
title_layout.setSpacing(0)
self.title_label = QLabel("天气信息")
self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
title_layout.addWidget(self.title_label)
title_layout.addStretch()
# 关闭按钮 - 修复被遮挡问题
self.close_btn = QPushButton("×")
self.close_btn.setFixedSize(24, 24)
self.close_btn.setObjectName("closeButton")
title_layout.addWidget(self.close_btn)
content_layout.addLayout(title_layout)
# 分隔线
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setObjectName("separator")
separator.setFixedHeight(1)
content_layout.addWidget(separator)
# 天气图标和温度显示区域
weather_display_layout = QHBoxLayout()
weather_display_layout.setSpacing(10)
weather_display_layout.setContentsMargins(0, 0, 0, 0)
self.weather_icon_label = QLabel("🌞")
self.weather_icon_label.setFont(QFont("Arial", 28))
self.weather_icon_label.setAlignment(Qt.AlignCenter)
self.weather_icon_label.setFixedSize(60, 60)
weather_display_layout.addWidget(self.weather_icon_label)
# 温度和城市信息
temp_city_layout = QVBoxLayout()
temp_city_layout.setSpacing(4)
temp_city_layout.setContentsMargins(0, 0, 0, 0)
self.temperature_label = QLabel("25°C")
self.temperature_label.setFont(QFont("Arial", 20, QFont.Bold))
self.temperature_label.setObjectName("temperatureLabel")
temp_city_layout.addWidget(self.temperature_label)
self.city_label = QLabel("北京")
self.city_label.setFont(QFont("Arial", 12))
self.city_label.setObjectName("cityLabel")
temp_city_layout.addWidget(self.city_label)
weather_display_layout.addLayout(temp_city_layout)
weather_display_layout.addStretch()
content_layout.addLayout(weather_display_layout)
# 天气描述
self.weather_desc_label = QLabel("晴天")
self.weather_desc_label.setFont(QFont("Arial", 12))
self.weather_desc_label.setObjectName("weatherDescLabel")
self.weather_desc_label.setAlignment(Qt.AlignCenter)
content_layout.addWidget(self.weather_desc_label)
# 详细信息(湿度、风速)
details_layout = QHBoxLayout()
details_layout.setSpacing(12)
details_layout.setContentsMargins(0, 0, 0, 0)
self.humidity_label = QLabel("湿度: 45%")
self.humidity_label.setFont(QFont("Arial", 11))
self.humidity_label.setObjectName("detailLabel")
details_layout.addWidget(self.humidity_label)
self.wind_label = QLabel("风速: 2级")
self.wind_label.setFont(QFont("Arial", 11))
self.wind_label.setObjectName("detailLabel")
details_layout.addWidget(self.wind_label)
details_layout.addStretch()
content_layout.addLayout(details_layout)
# 添加弹性空间
content_layout.addStretch()
# 城市选择和按钮区域
control_layout = QHBoxLayout()
control_layout.setSpacing(8)
control_layout.setContentsMargins(0, 0, 0, 0)
# 城市选择下拉框
self.city_combo = QComboBox()
self.city_combo.addItems([
'自动定位',
# 直辖市
'北京', '上海', '天津', '重庆',
# 省会城市
'石家庄', '太原', '呼和浩特', '沈阳', '长春', '哈尔滨', '南京', '杭州', '合肥', '福州',
'南昌', '济南', '郑州', '武汉', '长沙', '广州', '南宁', '海口', '成都', '贵阳',
'昆明', '拉萨', '西安', '兰州', '西宁', '银川', '乌鲁木齐',
# 特别行政区
'香港', '澳门',
# 台湾省主要城市
'台北', '高雄',
# 主要地级市和经济中心城市
'深圳', '青岛', '大连', '宁波', '厦门', '苏州', '无锡', '佛山', '东莞', '中山',
'泉州', '南通', '常州', '徐州', '温州', '烟台', '威海', '嘉兴', '湖州', '绍兴',
'金华', '台州', '芜湖', '蚌埠', '安庆', '阜阳', '九江', '赣州', '吉安', '上饶',
'淄博', '枣庄', '东营', '潍坊', '济宁', '泰安', '威海', '日照', '临沂', '德州',
'聊城', '滨州', '菏泽', '洛阳', '平顶山', '安阳', '鹤壁', '新乡', '焦作', '濮阳',
'许昌', '漯河', '三门峡', '商丘', '信阳', '周口', '驻马店', '黄石', '十堰', '宜昌',
'襄阳', '鄂州', '荆门', '孝感', '荆州', '黄冈', '咸宁', '随州', '株洲', '湘潭',
'衡阳', '邵阳', '岳阳', '常德', '张家界', '益阳', '郴州', '永州', '怀化', '娄底',
'韶关', '珠海', '汕头', '惠州', '江门', '湛江', '茂名', '肇庆', '梅州', '汕尾',
'河源', '阳江', '清远', '潮州', '揭阳', '云浮', '柳州', '桂林', '梧州', '北海',
'防城港', '钦州', '贵港', '玉林', '百色', '贺州', '河池', '来宾', '崇左', '三亚',
'儋州', '五指山', '琼海', '文昌', '万宁', '东方'
])
self.city_combo.setCurrentText('自动定位')
self.city_combo.currentTextChanged.connect(self.on_city_changed)
control_layout.addWidget(self.city_combo)
self.refresh_btn = QPushButton("刷新")
self.refresh_btn.setObjectName("refreshButton")
self.refresh_btn.setFixedHeight(28)
control_layout.addWidget(self.refresh_btn)
control_layout.addStretch()
self.detail_btn = QPushButton("详情")
self.detail_btn.setObjectName("detailButton")
self.detail_btn.setFixedHeight(28)
control_layout.addWidget(self.detail_btn)
content_layout.addLayout(control_layout)
def setup_connections(self):
"""设置信号连接"""
self.close_btn.clicked.connect(self.close_window)
self.refresh_btn.clicked.connect(self.on_refresh_clicked)
self.detail_btn.clicked.connect(self.show_detailed_weather)
def setup_theme(self):
"""设置主题"""
# 连接主题切换信号
theme_manager.theme_changed.connect(self.on_theme_changed)
# 应用当前主题
self.apply_theme()
def apply_theme(self):
"""应用主题样式"""
is_dark = theme_manager.is_dark_theme()
colors = theme_manager.get_current_theme_colors()
if is_dark:
# 深色主题样式 - 与每日谏言悬浮窗口保持一致
self.main_frame.setStyleSheet(f"""
QFrame#mainFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}}
QLabel {{
color: {colors['text']};
background-color: transparent;
padding: 4px 6px;
margin: 2px;
}}
QLabel#temperatureLabel {{
color: {colors['accent']};
font-size: 22px;
font-weight: bold;
padding: 0px 8px;
margin: 0px 3px;
}}
QLabel#cityLabel {{
color: {colors['text_secondary']};
font-size: 13px;
padding: 4px 6px;
margin: 2px;
}}
QLabel#weatherDescLabel {{
color: {colors['text']};
font-size: 13px;
font-weight: 500;
padding: 4px 6px;
margin: 2px;
}}
QLabel#detailLabel {{
color: {colors['text_secondary']};
font-size: 12px;
padding: 4px 6px;
margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
background-color: rgba(255, 255, 255, 0.1);
border: none;
color: {colors['text']};
font-size: 18px;
font-weight: bold;
border-radius: 6px;
padding: 2px 4px;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
border-radius: 6px;
}}
QPushButton#refreshButton, QPushButton#detailButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 12px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
background-color: {colors['accent_hover']};
}}
""")
else:
# 浅色主题样式 - 与每日谏言悬浮窗口保持一致
self.main_frame.setStyleSheet(f"""
QFrame#mainFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}}
QLabel {{
color: {colors['text']};
background-color: transparent;
padding: 4px 6px;
margin: 2px;
}}
QLabel#temperatureLabel {{
color: {colors['accent']};
font-size: 22px;
font-weight: bold;
padding: 0px 8px;
margin: 0px 3px;
}}
QLabel#cityLabel {{
color: {colors['text_secondary']};
font-size: 13px;
padding: 4px 6px;
margin: 2px;
}}
QLabel#weatherDescLabel {{
color: {colors['text']};
font-size: 13px;
font-weight: 500;
padding: 4px 6px;
margin: 2px;
}}
QLabel#detailLabel {{
color: {colors['text_secondary']};
font-size: 12px;
padding: 4px 6px;
margin: 2px;
}}
QFrame#separator {{
background-color: {colors['border']};
}}
QPushButton#closeButton {{
background-color: rgba(0, 0, 0, 0.05);
border: none;
color: {colors['text']};
font-size: 18px;
font-weight: bold;
border-radius: 6px;
padding: 2px 4px;
}}
QPushButton#closeButton:hover {{
background-color: #e81123;
color: white;
border-radius: 6px;
}}
QPushButton#refreshButton, QPushButton#detailButton {{
background-color: {colors['accent']};
color: white;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 12px;
font-weight: 500;
}}
QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
background-color: {colors['accent_hover']};
}}
""")
def on_theme_changed(self, is_dark):
"""主题切换槽函数"""
self.apply_theme()
def mousePressEvent(self, event):
"""鼠标按下事件,用于拖拽"""
if event.button() == Qt.LeftButton:
# 检查是否点击在标题栏区域
if event.pos().y() <= 40: # 假设标题栏高度为40像素
self.is_dragging = True
self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event):
"""鼠标移动事件,用于拖拽"""
if self.is_dragging and event.buttons() == Qt.LeftButton:
self.move(event.globalPos() - self.drag_position)
event.accept()
def mouseReleaseEvent(self, event):
"""鼠标释放事件"""
self.is_dragging = False
def update_weather(self, weather_data):
"""更新天气信息"""
self.weather_data = weather_data
if weather_data and 'error' not in weather_data:
# 获取天气数据
city = weather_data.get('city', '未知城市')
current_data = weather_data.get('current', {})
temp = current_data.get('temp', 'N/A')
desc = current_data.get('weather', 'N/A')
humidity = current_data.get('humidity', 'N/A')
wind_scale = current_data.get('wind_scale', 'N/A')
# 更新显示
self.city_label.setText(city)
self.temperature_label.setText(f"{temp}°C")
self.weather_desc_label.setText(desc)
self.humidity_label.setText(f"湿度: {humidity}%")
self.wind_label.setText(f"风速: {wind_scale}")
# 更新天气图标
emoji = self.get_weather_emoji(desc)
self.weather_icon_label.setText(emoji)
else:
# 显示错误信息
self.city_label.setText("获取失败")
self.temperature_label.setText("--°C")
self.weather_desc_label.setText("无法获取天气数据")
self.humidity_label.setText("湿度: --%")
self.wind_label.setText("风速: --级")
self.weather_icon_label.setText("")
def get_weather_emoji(self, weather_desc):
"""根据天气描述返回对应的emoji"""
if not weather_desc:
return "🌞"
weather_desc_lower = weather_desc.lower()
# 天气图标映射
weather_emoji_map = {
'': '🌞',
'多云': '',
'': '☁️',
'': '🌧️',
'小雨': '🌦️',
'中雨': '🌧️',
'大雨': '⛈️',
'暴雨': '🌩️',
'': '❄️',
'小雪': '🌨️',
'中雪': '❄️',
'大雪': '☃️',
'': '🌫️',
'': '😷',
'': '💨',
'大风': '🌪️',
'': '⛈️',
'雷阵雨': '⛈️',
'冰雹': '🌨️',
'沙尘': '🌪️'
}
for key, emoji in weather_emoji_map.items():
if key in weather_desc_lower:
return emoji
# 默认返回晴天图标
return '🌞'
def on_refresh_clicked(self):
"""刷新按钮点击事件"""
self.refresh_requested.emit()
def on_city_changed(self, city_name):
"""城市选择变化事件"""
# 发射城市变化信号,通知主窗口更新天气
if hasattr(self.parent(), 'on_city_changed'):
self.parent().on_city_changed(city_name)
def set_current_city(self, city_name):
"""设置当前城市"""
if hasattr(self, 'city_combo'):
self.city_combo.setCurrentText(city_name)
def close_window(self):
"""关闭窗口 - 只是隐藏而不是销毁"""
try:
self.closed.emit()
self.hide() # 隐藏窗口而不是销毁
except Exception as e:
print(f"Error in close_window: {e}")
def show_detailed_weather(self):
"""显示详细天气信息对话框"""
# 检查是否有天气数据
if not self.weather_data or 'error' in self.weather_data:
from PyQt5.QtWidgets import QMessageBox
QMessageBox.information(self, "天气信息", "暂无天气数据,请先刷新天气信息")
return
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit
weather_data = self.weather_data
# 创建对话框
dialog = QDialog(self)
dialog.setWindowTitle("详细天气")
dialog.setMinimumWidth(400)
layout = QVBoxLayout()
# 城市信息
city_label = QLabel(f"<h2>{weather_data.get('city', '未知城市')}</h2>")
layout.addWidget(city_label)
# 当前天气信息
current_layout = QVBoxLayout()
current_layout.addWidget(QLabel("<b>当前天气:</b>"))
# 获取温度信息,支持嵌套结构
current_data = weather_data.get('current', {})
temp = current_data.get('temp', 'N/A')
if temp != 'N/A' and isinstance(temp, str):
temp = float(temp) if temp.replace('.', '').isdigit() else temp
# 从预报数据中获取最高和最低气温
temp_range = ""
temp_max = 'N/A'
temp_min = 'N/A'
if 'forecast' in weather_data and weather_data['forecast']:
forecast_data = weather_data['forecast'][0] # 今天的预报
if isinstance(forecast_data, dict):
temp_max = forecast_data.get('temp_max', 'N/A')
temp_min = forecast_data.get('temp_min', 'N/A')
if temp_max != 'N/A' and temp_min != 'N/A':
temp_range = f" ({temp_min}°C~{temp_max}°C)"
current_info = f"""
当前温度: {temp}°C{temp_range}
最高气温: {temp_max}°C
最低气温: {temp_min}°C
天气状况: {current_data.get('weather', 'N/A')}
"""
current_text = QTextEdit()
current_text.setPlainText(current_info.strip())
current_text.setReadOnly(True)
current_layout.addWidget(current_text)
layout.addLayout(current_layout)
# 生活提示信息(替换原来的天气预报)
life_tips = weather_data.get('life_tips', [])
if life_tips:
tips_layout = QVBoxLayout()
tips_layout.addWidget(QLabel("<b>生活提示:</b>"))
tips_text = QTextEdit()
tips_info = ""
for tip in life_tips:
tips_info += f"{tip}\n"
tips_text.setPlainText(tips_info.strip())
tips_text.setReadOnly(True)
tips_layout.addWidget(tips_text)
layout.addLayout(tips_layout)
# 按钮
button_layout = QHBoxLayout()
refresh_button = QPushButton("刷新")
refresh_button.clicked.connect(lambda: self.refresh_weather_and_close(dialog))
close_button = QPushButton("关闭")
close_button.clicked.connect(dialog.close)
button_layout.addWidget(refresh_button)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
dialog.setLayout(layout)
dialog.exec_()
def refresh_weather_and_close(self, dialog):
"""刷新天气并关闭对话框"""
self.on_refresh_clicked()
dialog.close()
def closeEvent(self, event):
"""窗口关闭事件 - 只是隐藏而不是销毁"""
self.closed.emit()
self.hide() # 隐藏窗口而不是销毁
event.ignore()
def show_at_position(self, x, y):
"""在指定位置显示窗口"""
self.move(x, y)
self.show()
def update_position(self, x, y):
"""更新窗口位置"""
self.move(x, y)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,29 @@
#!/bin/bash
# MagicWord PyQt5 安全启动脚本
# 此脚本自动设置所有必要的环境变量并启动应用
echo "🚀 正在启动 MagicWord 应用..."
# 设置PyQt5环境变量
export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
export QT_QPA_PLATFORM="cocoa"
export QT_MAC_WANTS_LAYER="1"
export QT_LOGGING_RULES="qt.qpa.*=false"
# 检查虚拟环境
if [ -z "$VIRTUAL_ENV" ]; then
echo "⚠️ 虚拟环境未激活,正在激活..."
source .venv/bin/activate
fi
# 检查PyQt5是否安装
python -c "import PyQt5.QtWidgets" 2>/dev/null
if [ $? -ne 0 ]; then
echo "❌ PyQt5未正确安装正在修复..."
python fix_pyqt5_complete.py
fi
# 启动应用
echo "✅ 环境设置完成,正在启动应用..."
cd src && python main.py

@ -0,0 +1,169 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MagicWord - MarkText风格启动器
基于MarkText开源编辑器的现代化Markdown编辑器
集成MagicWord现有功能学习模式天气名言等
"""
import sys
import os
import platform
import traceback
# 添加项目根目录到Python路径
project_root = os.path.dirname(os.path.abspath(__file__))
src_path = os.path.join(project_root, 'src')
sys.path.insert(0, project_root)
sys.path.insert(0, src_path)
# 设置Qt平台插件路径
def setup_qt_environment():
"""设置Qt环境变量"""
system = platform.system()
python_version = f"python{sys.version_info.major}.{sys.version_info.minor}"
possible_paths = []
if system == "Windows":
possible_paths.extend([
os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
])
elif system == "Darwin": # macOS
possible_paths.extend([
os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
'/usr/local/opt/qt5/plugins',
'/opt/homebrew/opt/qt5/plugins',
])
elif system == "Linux":
possible_paths.extend([
os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
'/usr/lib/x86_64-linux-gnu/qt5/plugins',
'/usr/lib/qt5/plugins',
])
for path in possible_paths:
if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')):
os.environ['QT_PLUGIN_PATH'] = path
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(path, 'platforms')
if system == "Darwin":
os.environ['QT_QPA_PLATFORM'] = 'cocoa'
os.environ['QT_MAC_WANTS_LAYER'] = '1'
elif system == "Windows":
os.environ['QT_QPA_PLATFORM'] = 'windows'
elif system == "Linux":
os.environ['QT_QPA_PLATFORM'] = 'xcb'
print(f"✅ Qt插件路径设置成功: {path}")
return True
print("⚠️ 警告未找到Qt插件路径")
return False
# 检查依赖
def check_dependencies():
"""检查必要的依赖"""
missing_deps = []
try:
import PyQt5
except ImportError:
missing_deps.append("PyQt5")
try:
import requests
except ImportError:
missing_deps.append("requests")
try:
import docx
except ImportError:
missing_deps.append("python-docx")
try:
import fitz # PyMuPDF
except ImportError:
missing_deps.append("PyMuPDF")
if missing_deps:
print(f"❌ 缺少依赖包: {', '.join(missing_deps)}")
print("请运行: pip install " + " ".join(missing_deps))
return False
return True
def main():
"""主函数"""
try:
print("🚀 启动MagicWord - MarkText风格编辑器")
# 检查依赖
if not check_dependencies():
sys.exit(1)
# 设置Qt环境
setup_qt_environment()
# 导入Qt相关模块
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QIcon
# 设置高DPI支持
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
# 创建应用
app = QApplication(sys.argv)
app.setApplicationName("MagicWord")
app.setApplicationVersion("1.0.0")
app.setOrganizationName("MagicWord")
# 设置样式
if platform.system() != "Darwin":
app.setStyle('Fusion')
# 设置图标
icon_path = os.path.join(project_root, 'resources', 'icons', 'app_icon.png')
if os.path.exists(icon_path):
app.setWindowIcon(QIcon(icon_path))
# 导入并创建MarkText主窗口
try:
from main import MarkTextMainWindow
main_window = MarkTextMainWindow()
print("✅ MarkText编辑器启动成功")
except ImportError as e:
print(f"❌ MarkText编辑器导入失败: {e}")
print("正在尝试启动Word风格编辑器...")
try:
from word_main_window import WordStyleMainWindow
main_window = WordStyleMainWindow()
print("✅ Word风格编辑器启动成功")
except ImportError as e2:
print(f"❌ Word风格编辑器也启动失败: {e2}")
sys.exit(1)
# 显示窗口
main_window.show()
# 运行应用
exit_code = app.exec_()
print(f"👋 应用已退出,退出码: {exit_code}")
sys.exit(exit_code)
except KeyboardInterrupt:
print("\n👋 用户中断,正在退出...")
sys.exit(0)
except Exception as e:
print(f"❌ 发生未预期的错误: {e}")
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

Binary file not shown.

@ -0,0 +1,111 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
扫雷游戏测试脚本
用于测试扫雷游戏模块是否能正常运行
"""
import sys
import os
import platform
# 添加项目根目录到Python路径
project_root = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, project_root)
# 设置Qt平台插件路径 - 增强版本,完全避免平台插件问题
def set_qt_plugin_path():
"""设置Qt平台插件路径确保所有平台插件都能正确加载"""
system = platform.system()
# 获取Python版本
python_version = f"python{sys.version_info.major}.{sys.version_info.minor}"
# 可能的Qt插件路径列表
possible_paths = []
if system == "Windows":
# Windows环境下的路径
possible_paths.extend([
os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'Python', 'Python39', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
])
elif system == "Darwin": # macOS
# macOS环境下的路径
possible_paths.extend([
os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
'/usr/local/opt/qt5/plugins', # Homebrew Qt5
'/opt/homebrew/opt/qt5/plugins', # Apple Silicon Homebrew
os.path.expanduser('~/Qt/5.15.2/clang_64/plugins'), # Qt官方安装
])
elif system == "Linux":
# Linux环境下的路径
possible_paths.extend([
os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
'/usr/lib/x86_64-linux-gnu/qt5/plugins',
'/usr/lib/qt5/plugins',
])
# 查找第一个存在的路径
valid_path = None
for path in possible_paths:
if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')):
valid_path = path
break
if valid_path:
# 设置Qt插件路径
os.environ['QT_PLUGIN_PATH'] = valid_path
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(valid_path, 'platforms')
# 设置平台特定的环境变量
if system == "Darwin": # macOS
os.environ['QT_QPA_PLATFORM'] = 'cocoa'
os.environ['QT_MAC_WANTS_LAYER'] = '1'
# 禁用可能导致问题的Qt功能
os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' # 禁用Qt警告日志
elif system == "Windows":
os.environ['QT_QPA_PLATFORM'] = 'windows'
elif system == "Linux":
os.environ['QT_QPA_PLATFORM'] = 'xcb'
# 对于Linux可能需要设置DISPLAY
if 'DISPLAY' not in os.environ:
os.environ['DISPLAY'] = ':0'
print(f"✅ Qt插件路径设置成功: {valid_path}")
return True
else:
print("⚠️ 警告未找到Qt插件路径")
return False
# 设置Qt平台插件路径
set_qt_plugin_path()
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt
from src.ui.minesweeper_game import MinesweeperWindow
def main():
# 设置高DPI支持必须在QApplication创建之前
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)
app = QApplication(sys.argv)
# 设置应用程序样式
if platform.system() != "Darwin": # 不是macOS系统
app.setStyle('WindowsVista')
# 创建并显示扫雷游戏窗口
window = MinesweeperWindow()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

@ -0,0 +1,44 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import os
# 添加src目录到Python路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from ui.word_style_ui import daily_sentence_API
from services.network_service import NetworkService
def test_daily_sentence_api():
print("测试 daily_sentence_API 类...")
try:
# 使用与Ribbon界面相同的API获取每日一言
quote_api = daily_sentence_API("https://api.nxvav.cn/api/yiyan")
quote_data = quote_api.get_sentence('json')
print("API返回的数据:")
print(quote_data)
# 处理获取到的数据
if quote_data and isinstance(quote_data, dict):
quote_text = quote_data.get('yiyan', '暂无每日一言')
print(f"解析后的每日一言: {quote_text}")
else:
print("获取每日一言失败")
except Exception as e:
print(f"测试 daily_sentence_API 类时出错: {e}")
def test_network_service_quote():
print("\n测试 NetworkService 类的 get_daily_quote 方法...")
try:
network_service = NetworkService()
quote = network_service.get_daily_quote()
print(f"NetworkService 获取的每日一言: {quote}")
except Exception as e:
print(f"测试 NetworkService 类时出错: {e}")
if __name__ == "__main__":
test_daily_sentence_api()
test_network_service_quote()

@ -0,0 +1,22 @@
#!/usr/bin/env python3
# test_snake_game.py
"""测试贪吃蛇游戏"""
import sys
import os
# 添加项目根目录到Python路径
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
from PyQt5.QtWidgets import QApplication
from src.ui.snake_game import SnakeGameWindow
def main():
app = QApplication(sys.argv)
game_window = SnakeGameWindow()
game_window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""
测试贪吃蛇游戏的速度调节功能
"""
import sys
sys.path.insert(0, '/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design')
from src.ui.snake_game import SnakeGame, SnakeGameWindow
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QKeyEvent
def test_snake_game():
"""测试贪吃蛇游戏"""
app = QApplication(sys.argv)
# 创建游戏窗口
window = SnakeGameWindow()
window.show()
# 获取游戏实例
game = window.game_widget
# 测试初始速度
print(f"初始速度: {game.current_speed}ms")
assert game.current_speed == game.GAME_SPEED, "初始速度应该是150ms"
# 测试增加速度(按上键)
print("\n测试增加速度...")
initial_speed = game.current_speed
game.increase_speed()
print(f"按上键后速度: {game.current_speed}ms (从 {initial_speed}ms)")
assert game.current_speed < initial_speed, "速度应该增加(毫秒数减少)"
# 测试降低速度(按下键)
print("\n测试降低速度...")
current_speed = game.current_speed
game.decrease_speed()
print(f"按下键后速度: {game.current_speed}ms (从 {current_speed}ms)")
assert game.current_speed > current_speed, "速度应该降低(毫秒数增加)"
# 测试速度限制
print("\n测试速度限制...")
# 测试最小速度限制
game.current_speed = game.MIN_SPEED
game.increase_speed()
print(f"最小速度限制测试: {game.current_speed}ms (应该 >= {game.MIN_SPEED}ms)")
assert game.current_speed >= game.MIN_SPEED, "速度不应该低于最小值"
# 测试最大速度限制
game.current_speed = game.MAX_SPEED
game.decrease_speed()
print(f"最大速度限制测试: {game.current_speed}ms (应该 <= {game.MAX_SPEED}ms)")
assert game.current_speed <= game.MAX_SPEED, "速度不应该超过最大值"
print("\n✓ 所有测试通过!")
print(f"速度范围: {game.MIN_SPEED}ms - {game.MAX_SPEED}ms")
print(f"速度步长: {game.SPEED_STEP}ms")
window.close()
if __name__ == '__main__':
try:
test_snake_game()
except Exception as e:
print(f"✗ 测试失败: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
简化测试脚本 - 验证学习模式内容同步功能
"""
import sys
import os
sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
from PyQt5.QtWidgets import QApplication, QTextEdit, QVBoxLayout, QWidget
from learning_mode_window import LearningModeWindow
class TestWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("测试主窗口")
self.setGeometry(100, 100, 600, 400)
# 创建布局
layout = QVBoxLayout()
# 创建文本编辑器
self.text_edit = QTextEdit()
self.text_edit.setPlainText("初始内容\n")
layout.addWidget(self.text_edit)
self.setLayout(layout)
# 创建学习模式窗口
self.learning_window = LearningModeWindow(parent=self)
self.learning_window.imported_content = "这是一段测试内容。"
self.learning_window.current_position = 0
self.learning_window.show()
# 连接内容同步信号
self.learning_window.content_changed.connect(self.on_content_changed)
print("测试开始...")
print(f"主窗口内容: {repr(self.text_edit.toPlainText())}")
print(f"学习窗口内容: {repr(self.learning_window.imported_content)}")
print(f"学习窗口当前位置: {self.learning_window.current_position}")
# 模拟用户输入正确内容
print("\n模拟用户输入正确内容...")
self.learning_window.current_position = 3 # 用户输入了3个字符
self.learning_window.on_text_changed() # 调用文本变化处理
def on_content_changed(self, new_content, position):
"""内容同步回调"""
print(f"收到内容同步信号: new_content={repr(new_content)}, position={position}")
# 在文本编辑器末尾添加新内容
current_text = self.text_edit.toPlainText()
self.text_edit.setPlainText(current_text + new_content)
print(f"主窗口更新后内容: {repr(self.text_edit.toPlainText())}")
def test_content_sync():
"""测试内容同步功能"""
app = QApplication(sys.argv)
test_window = TestWindow()
test_window.show()
print("\n测试完成!")
# 运行应用
sys.exit(app.exec_())
if __name__ == "__main__":
test_content_sync()

@ -0,0 +1,60 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import os
# 添加src目录到Python路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from word_main_window import WordStyleMainWindow
from PyQt5.QtWidgets import QApplication, QMessageBox
from unittest.mock import patch
def test_insert_weather_without_location():
"""测试在未定位天气时插入天气信息"""
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# 创建主窗口实例
window = WordStyleMainWindow()
# 模拟没有定位天气的情况删除current_weather_data属性
if hasattr(window, 'current_weather_data'):
delattr(window, 'current_weather_data')
# 模拟用户点击插入天气信息按钮
with patch('PyQt5.QtWidgets.QMessageBox.information') as mock_info:
window.insert_weather_info()
# 验证是否弹出了"先定位天气"对话框
mock_info.assert_called_once_with(window, "附加工具", "先定位天气")
print("测试通过:未定位天气时正确弹出提示对话框")
def test_insert_weather_with_location():
"""测试在已定位天气时插入天气信息"""
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
# 创建主窗口实例
window = WordStyleMainWindow()
# 模拟已定位天气的情况
window.current_weather_data = {
'city': '北京',
'temperature': 25,
'description': '晴天'
}
# 模拟用户点击插入天气信息按钮
# 注意这个测试不会真正插入文本因为我们没有设置完整的UI环境
window.insert_weather_info()
print("测试通过:已定位天气时正确执行插入操作")
if __name__ == "__main__":
test_insert_weather_without_location()
test_insert_weather_with_location()
print("所有测试完成!")

@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""
测试天气生活提示功能
"""
import sys
import os
# 添加src目录到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
from services.network_service import NetworkService
def test_weather_with_lifetips():
"""测试包含生活提示的天气功能"""
print("🌤 测试天气生活提示功能")
print("=" * 50)
# 创建网络服务实例
network_service = NetworkService()
# 获取天气信息
print("正在获取天气信息...")
weather_info = network_service.get_weather_info()
if weather_info:
print(f"✅ 成功获取天气数据:")
print(f"城市: {weather_info['city']}")
print(f"温度: {weather_info['temperature']}°C")
print(f"天气: {weather_info['description']}")
print(f"湿度: {weather_info['humidity']}%")
print(f"风速: {weather_info['wind_speed']}m/s")
# 显示生活提示
lifetips = weather_info.get('lifetips', [])
if lifetips:
print(f"\n🌟 生活提示 ({len(lifetips)}条):")
for i, tip in enumerate(lifetips, 1):
print(f" {i}. {tip}")
else:
print("⚠️ 未获取到生活提示")
# 模拟显示详细信息格式
print(f"\n📋 详细信息显示格式:")
weather_text = f"{weather_info['city']}: {weather_info['temperature']}°C, {weather_info['description']}"
weather_text += f"\n湿度: {weather_info['humidity']}%"
weather_text += f"\n风速: {weather_info['wind_speed']}m/s"
if lifetips:
weather_text += "\n\n🌟 生活提示:"
for tip in lifetips:
weather_text += f"\n{tip}"
print(weather_text)
else:
print("❌ 获取天气信息失败")
print("\n" + "=" * 50)
print("测试完成!")
if __name__ == "__main__":
test_weather_with_lifetips()

@ -1,88 +0,0 @@
# 字体颜色功能说明
我已经成功为你的MagicWord应用添加了字体颜色工具以下是添加的功能
## 🎯 重要更新:保留之前内容的颜色
**根据你的需求,字体颜色功能已修改为保留原有内容的颜色!**
### 修改后的功能特性
- ✅ **保留原有内容颜色**:已有文本的颜色完全保持不变
- ✅ **只影响新输入**:新输入的文本将使用选定的颜色
- ✅ **智能提示**:选择文本时会提示颜色只对新输入生效
- ✅ **向后兼容**:不会影响现有的粗体、斜体、下划线等格式
## 新增功能
### 1. 字体颜色按钮
- 在"开始"标签页的"字体"组中,添加了一个新的颜色按钮(显示为"A"
- 该按钮位于加粗(B)、斜体(I)、下划线(U)按钮的右侧
### 2. 颜色选择功能
- 点击颜色按钮会弹出颜色选择对话框
- 用户可以选择任意颜色
- **重要**:只影响后续输入的文本,不会改变已有内容的颜色
## 技术实现
### UI界面修改
1. **word_style_ui.py** 中添加了:
- `color_btn` 按钮创建
- `create_color_button()` 方法用于创建颜色按钮
- `on_color_clicked()` 方法作为按钮点击事件的处理函数
2. **word_main_window.py** 中修改了:
- 颜色按钮的信号连接
- `on_color_clicked()` 方法:简化为只设置默认颜色,不影响已有内容
### 功能特性
- 使用 PyQt5 的 `QColorDialog` 提供颜色选择界面
- **只设置默认文本颜色**,不修改已有内容的格式
- 智能状态栏提示,告知用户颜色的应用范围
- 保持与现有字体样式(粗体、斜体、下划线)的一致性
## 使用方法
### 设置新文本颜色
1. 点击颜色按钮A
2. 在弹出的颜色对话框中选择所需颜色
3. 点击"确定"
4. **后续输入的所有文本都将使用该颜色**
5. **已有文本的颜色完全保持不变**
### 颜色选择提示
- 如果选择了文本,会提示:"字体颜色已设置,新输入的文本将使用该颜色"
- 如果没有选择文本,会显示新颜色的具体信息
## 界面位置
字体颜色工具位于:
开始标签页 → 字体组 → 样式按钮区域B、I、U按钮右侧
## 🔧 技术实现细节
### 修改后的核心逻辑
```python
def on_color_clicked(self):
"""字体颜色按钮点击处理 - 保留之前内容的颜色"""
# 只设置后续输入的默认颜色,不影响已有内容
self.text_edit.setTextColor(color)
# 友好的用户提示
if cursor.hasSelection():
self.status_bar.showMessage("字体颜色已设置,新输入的文本将使用该颜色", 2000)
```
这个实现确保:
- ✅ 用户可以自由设置新文本的颜色
- ✅ 所有已有内容的颜色完全保留
- ✅ 用户体验友好,有明确的操作反馈
## 🚀 优势
1. **非破坏性**:不会意外改变已有内容的格式
2. **直观易用**:用户明确知道颜色设置的影响范围
3. **灵活性高**:可以随时更改新文本的颜色而不影响历史内容
4. **兼容性好**:与所有现有功能完美配合
这个新增功能与现有的字体样式工具完美集成,提供了完整且安全的文本格式化能力!
Loading…
Cancel
Save