Compare commits

..

No commits in common. 'main' and 'main' have entirely different histories.
main ... main

16
.gitignore vendored

@ -198,16 +198,6 @@ temp/
*.orig
# Project specific
resources/config/deepseek_api.json
*.key
*.secret
config/*.json
# Documentation folder
doc/
dist_package/
dist_package_v0.3/
*.zip
*.pyc
*.pyo
*.pyd
@ -219,11 +209,6 @@ dist_package_v0.3/
/resources/user_data/
/resources/cache/
# Resources - keep icons and config
!/resources/config/
!/resources/config/icons/
!/resources/icons/
# Test reports
htmlcov/
.coverage
@ -238,7 +223,6 @@ venv/
env/
.venv/
.env/
new_venv/
# IDE
.idea/

@ -4,18 +4,6 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
版本遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
库依赖:
- PyQt5
- python-docx
- PyPDF2
- requests
- beautifulsoup4
依赖地址:
- [PyQt5](https://pypi.org/project/PyQt5/)
- [python-docx](https://pypi.org/project/python-docx/)
- [PyPDF2](https://pypi.org/project/PyPDF2/)
- [requests](https://pypi.org/project/requests/)
- [beautifulsoup4](https://pypi.org/project/beautifulsoup4/)
## [0.1.0] - 2025-10-12
@ -54,101 +42,4 @@
- 云同步功能
- 社区功能和内容分享
## [0.2.0] - 2025-10-19
### 新增
- 实现完整的天气功能集成
- 添加自动IP定位功能自动获取用户地理位置
- 支持中英文城市名智能映射Tianjin → 天津)
- 扩展城市支持到40+个主要城市
- 添加4个不同的IP定位API接口搜狐、pconline、ip-api、淘宝
- 实现天气数据缓存和状态栏显示
- 添加城市选择下拉菜单功能
- 集成3天天气预报功能
- 添加详细的错误处理和调试日志
### 更改
- 重构天气API集成架构
- 优化城市ID映射机制
- 改进错误处理和用户反馈
- 增强网络请求稳定性
- 优化UI界面布局和响应速度
### 修复
- 修复KeyError天气数据访问问题
- 修复自动定位功能失败问题
- 修复城市ID映射错误
- 修复网络请求超时和异常处理
- 修复界面状态更新问题
- 修复中英文城市名混用问题
### 技术改进
- 实现多重IP定位备份机制
- 添加智能城市名解析和映射
- 优化API调用性能和错误恢复
- 增强代码的模块化和可维护性
## [0.2.1] - 2025-10-20
### 新增
- 集成每日一言功能到WordRibbon界面
- 添加每日一言自动获取和显示功能
- 实现每日一言刷新按钮和手动刷新功能
- 添加每日一言显示/隐藏切换功能
- 集成天气功能到WordRibbon工具栏
- 实现天气信息状态栏显示
- 添加城市选择和天气刷新功能
### 更改
- 将视图菜单中的"天气信息"选项重命名为"附加工具"
- 优化每日一言显示格式,移除"每日一言:"前缀
- 改进天气信息状态栏显示文本
- 统一UI界面风格和交互逻辑
- 优化错误处理和用户反馈信息
### 修复
- 修复每日一言API集成问题
- 修复天气数据解析和显示错误
- 修复UI组件显示/隐藏状态同步问题
- 修复网络请求异常处理
### 技术改进
- 重构WordRibbon类结构增强可扩展性
- 优化API调用和数据处理逻辑
- 改进组件间的通信机制
- 增强代码的模块化和可维护性
### 发布/构建与工程维护 - 2025-10-22
- 新增Apple ARM64的软件
### 未来计划-2025-10-23
- 新增断点记录
- 改进页面更像word
- 新增切换输入模式功能
<<<<<<< HEAD
- 详细天气模块中,去除天气预报,只显示当前天气
=======
- 详细天气模块中,去除天气预报,只显示当前天气
## [0.2.2] - 2025-10-25
### 修改
- 更改应用程序图标:现在使用类似 Microsoft Word 的图标,但将字母 "W" 更改为 "M" 以代表 MagicWord
- 图标文件位于 `resources/icons/app_icon.png`
- 支持多种分辨率的图标32x32, 64x64, 128x128, 256x256
## [0.2.3] - 2025-10-26
### 新增
- 全新的 Word 风格用户界面
- 功能区Ribbon设计
- 改进的文档处理功能
- 天气显示功能
- 每日一句名言功能
### 修改
- 重构了整个用户界面以模仿 Microsoft Word
- 改进了打字伪装功能
- 增强了文件处理能力
>>>>>>> shixinglin
[0.1.0]: https://github.com/your-repo/magicword/releases/tag/v0.1.0

@ -1,235 +0,0 @@
# 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编辑体验** 🎉

@ -1,120 +0,0 @@
# 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平台插件问题

@ -1,52 +0,0 @@
# 扫雷游戏说明
## 游戏介绍
这是一个经典的扫雷游戏实现基于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,35 @@
# 使用说明
## 启动应用程序
### 方法一:使用启动脚本(推荐)
```bash
./run_app.sh
```
### 方法二直接运行Python代码
```bash
/usr/bin/python3 src/main.py
```
## 使用步骤
1. 启动应用程序后,点击顶部菜单栏的"文件"选项
2. 选择"打开"或使用快捷键 Ctrl+O
3. 在弹出的文件选择对话框中,选择您要练习的文本文件(支持 .txt 和 .docx 格式)
4. 选择文件后,文件内容将加载到应用程序中,但不会立即显示
5. 在底部的输入区域开始打字练习
6. 随着您的输入,文本内容会逐步显示在主显示区域
7. 应用程序会实时显示打字进度和准确率统计
## 功能说明
- **文本显示**:随着您的输入逐步显示文件内容
- **进度统计**显示WPM每分钟单词数和准确率
- **状态栏**:显示当前操作状态和文件信息
## 注意事项
- 请确保使用系统Python运行应用程序以避免Qt平台插件问题
- 应用程序设计为只有在用户输入时才显示文本内容,这是正常行为
- 支持的文件格式:.txt 和 .docx

@ -1,309 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MagicWord macOS M系列芯片打包脚本
用于构建macOS Apple Silicon原生应用程序
"""
import os
import sys
import subprocess
import platform
import shutil
import plistlib
from datetime import datetime
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 check_system():
"""检查系统是否为macOS Apple Silicon"""
if platform.system() != "Darwin":
print("错误: 此脚本仅支持macOS系统")
return False
# 检查是否为Apple Silicon
machine = platform.machine()
if machine not in ['arm64', 'aarch64']:
print(f"警告: 当前为 {machine} 架构建议Apple Silicon (arm64) 以获得最佳性能")
print(f"系统信息: macOS {platform.mac_ver()[0]}, {machine}")
return True
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("安装项目依赖...")
# 首先确保pip是最新的
run_command([sys.executable, "-m", "pip", "install", "--upgrade", "pip"])
# 安装requirements.txt中的依赖
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
if code != 0:
print(f"依赖安装失败: {stderr}")
return False
# 安装PyInstaller
print("安装PyInstaller...")
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "pyinstaller"])
if code != 0:
print(f"PyInstaller安装失败: {stderr}")
return False
print("依赖安装成功")
return True
def build_macos_app():
"""构建macOS应用包"""
print("构建macOS应用包...")
# PyInstaller命令 - 针对macOS优化
pyinstaller_cmd = [
"pyinstaller",
"--name", "MagicWord",
"--version", "0.3.0",
"--distpath", "dist",
"--workpath", "build",
"--specpath", ".",
# macOS特定的数据文件路径格式
"--add-data", "resources:resources",
"--add-data", "src:src",
# 隐藏导入模块
"--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",
# macOS应用包选项
"--windowed", # 无控制台窗口
"--osx-bundle-identifier", "com.magicword.app",
"--target-architecture", "arm64", # Apple Silicon
"--noconfirm",
"src/main.py"
]
print("运行PyInstaller...")
code, stdout, stderr = run_command(pyinstaller_cmd)
if code != 0:
print(f"构建失败: {stderr}")
print("尝试通用架构...")
# 尝试通用架构
pyinstaller_cmd[-2] = "--target-architecture"
pyinstaller_cmd[-1] = "universal2"
code, stdout, stderr = run_command(pyinstaller_cmd)
if code != 0:
print(f"通用架构构建也失败: {stderr}")
return False
print("macOS应用包构建成功")
return True
def create_app_bundle():
"""创建macOS应用束"""
print("创建macOS应用束...")
app_path = "dist/MagicWord.app"
if not os.path.exists(app_path):
print(f"错误: 找不到应用包 {app_path}")
return False
# 创建Info.plist文件
info_plist = {
'CFBundleName': 'MagicWord',
'CFBundleDisplayName': 'MagicWord - 隐私学习软件',
'CFBundleIdentifier': 'com.magicword.app',
'CFBundleVersion': '0.3.0',
'CFBundleShortVersionString': '0.3.0',
'CFBundleExecutable': 'MagicWord',
'CFBundlePackageType': 'APPL',
'CFBundleSignature': '????',
'LSMinimumSystemVersion': '11.0', # macOS Big Sur及更高版本
'NSHighResolutionCapable': True,
'NSHumanReadableCopyright': 'Copyright © 2024 MagicWord Team. All rights reserved.',
'CFBundleDocumentTypes': [
{
'CFBundleTypeName': 'Text Document',
'CFBundleTypeExtensions': ['txt', 'docx', 'pdf'],
'CFBundleTypeRole': 'Editor'
}
]
}
plist_path = os.path.join(app_path, "Contents", "Info.plist")
with open(plist_path, 'wb') as f:
plistlib.dump(info_plist, f)
# 复制图标文件
icon_files = [
'resources/icons/app_icon_128X128.png',
'resources/icons/app_icon_256X256.png',
'resources/icons/app_icon_32X32.png',
'resources/icons/app_icon_64X64.png'
]
resources_dir = os.path.join(app_path, "Contents", "Resources")
os.makedirs(resources_dir, exist_ok=True)
for icon_file in icon_files:
if os.path.exists(icon_file):
shutil.copy2(icon_file, resources_dir)
print(f"复制图标: {icon_file}")
print("macOS应用束创建完成")
return True
def create_dmg():
"""创建DMG安装包"""
print("创建DMG安装包...")
app_path = "dist/MagicWord.app"
if not os.path.exists(app_path):
print(f"错误: 找不到应用包 {app_path}")
return False
# 创建发布目录
release_dir = "macos_release"
if os.path.exists(release_dir):
shutil.rmtree(release_dir)
os.makedirs(release_dir)
# 复制应用到发布目录
release_app_path = os.path.join(release_dir, "MagicWord.app")
shutil.copytree(app_path, release_app_path)
# 创建应用程序链接
applications_link = os.path.join(release_dir, "Applications")
os.symlink("/Applications", applications_link)
# 创建README文件
readme_content = f"""# MagicWord 0.3.0 for macOS
## 安装说明
1. MagicWord.app 拖拽到 Applications 文件夹
2. 首次运行时如果出现安全提示请前往 系统设置 > 隐私与安全性 允许应用运行
3. 或者右键点击应用选择"打开"
## 系统要求
- macOS Big Sur (11.0) 或更高版本
- Apple Silicon (M1/M2/M3) Intel 处理器
## 功能特性
- 隐私学习通过打字练习来学习文档内容
- 支持多种文档格式TXT, DOCX, PDF
- 智能打字模式
- 美观的Word风格界面
## 版本信息
构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
平台: {platform.system()} {platform.machine()}
Python版本: {platform.python_version()}
## 技术支持
如有问题请查看项目文档或联系开发团队
"""
with open(os.path.join(release_dir, "README.txt"), "w") as f:
f.write(readme_content)
# 创建DMG文件如果系统支持
dmg_name = f"MagicWord-0.3.0-macOS-{platform.machine()}.dmg"
dmg_path = os.path.join("dist", dmg_name)
# 使用hdiutil创建DMG
create_dmg_cmd = [
"hdiutil", "create",
"-volname", "MagicWord",
"-srcfolder", release_dir,
"-ov",
"-format", "UDZO",
dmg_path
]
code, stdout, stderr = run_command(create_dmg_cmd)
if code == 0:
print(f"DMG创建成功: {dmg_path}")
return True
else:
print(f"DMG创建失败: {stderr}")
print("已创建应用包可手动打包DMG")
return False
def main():
"""主函数"""
print("=== MagicWord macOS打包脚本 ===")
print(f"构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 检查系统
if not check_system():
return False
# 清理构建目录
clean_build_dirs()
# 安装依赖
if not install_dependencies():
print("依赖安装失败")
return False
# 构建应用
if not build_macos_app():
print("应用构建失败")
return False
# 创建应用束
if not create_app_bundle():
print("应用束创建失败")
return False
# 创建DMG
create_dmg()
print("\n=== 构建完成 ===")
print("应用位置: dist/MagicWord.app")
print("如需安装请将应用拖拽到Applications文件夹")
print("首次运行时可能需要允许未知来源的应用")
if __name__ == "__main__":
main()

@ -1,258 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MagicWord 0.3.0 版本发布脚本
用于构建和打包应用程序
"""
import os
import sys
import subprocess
import platform
import shutil
import zipfile
from datetime import datetime
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 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("安装项目依赖...")
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
# PyInstaller命令
pyinstaller_cmd = [
"pyinstaller",
"--name", "MagicWord",
"--version", "0.3.0",
"--distpath", "dist",
"--workpath", "build",
"--specpath", ".",
"--add-data", "resources;resources",
"--add-data", "src;src",
"--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",
# 移除有问题的图标参数
# "--icon", "resources/icons/app_icon.ico",
"--windowed", # 无控制台窗口
"--noconfirm",
"src/main.py"
]
# Windows平台特殊处理
if platform.system() == "Windows":
pyinstaller_cmd.extend(["--add-binary", "resources;resources"])
print("运行PyInstaller...")
code, stdout, stderr = run_command(pyinstaller_cmd)
if code != 0:
print(f"构建失败: {stderr}")
print("尝试简化构建...")
# 简化版本
simple_cmd = [
"pyinstaller",
"--onefile",
"--windowed",
"--icon=resources/icons/app_icon.ico",
"--add-data=resources;resources",
"--name=MagicWord",
"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"
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}")
# 创建运行脚本
if platform.system() == "Windows":
run_script = """@echo off
echo MagicWord 0.3.0 启动中...
cd /d "%~dp0"
start MagicWord.exe
"""
with open(os.path.join(release_dir, "run.bat"), "w") as f:
f.write(run_script)
else:
run_script = """#!/bin/bash
echo "MagicWord 0.3.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 0.3.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
主要更新:
- 完整的天气功能集成
- 自动IP定位功能
- 40+个城市支持
- 中英文城市名智能映射
- 每日一言功能集成
- 附加工具菜单原天气信息菜单重命名
详细更新请查看 CHANGELOG.md
"""
with open(os.path.join(release_dir, "RELEASE_INFO.txt"), "w", encoding="utf-8") as f:
f.write(release_info)
# 创建ZIP包
zip_name = f"MagicWord_v0.3.0_{platform.system()}_{platform.machine()}.zip"
with zipfile.ZipFile(zip_name, '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_name = os.path.relpath(file_path, release_dir)
zipf.write(file_path, arc_name)
print(f"发布包创建成功: {zip_name}")
return True
def main():
"""主函数"""
print("=" * 60)
print("MagicWord 0.3.0 版本发布构建脚本")
print("=" * 60)
# 检查Python版本
print(f"Python版本: {platform.python_version()}")
print(f"操作系统: {platform.system()} {platform.machine()}")
# 清理构建目录
clean_build_dirs()
# 安装依赖
print("\n安装依赖...")
if not install_dependencies():
print("依赖安装失败")
sys.exit(1)
# 构建可执行文件
print("\n构建可执行文件...")
if not build_executable():
print("构建失败,尝试手动构建...")
# 如果自动构建失败,提供手动构建说明
print("\n手动构建步骤:")
print("1. 安装PyInstaller: pip install pyinstaller")
print("2. 运行: pyinstaller --onefile --windowed --icon=resources/icons/app_icon.ico src/main.py")
sys.exit(1)
# 创建发布包
print("\n创建发布包...")
if not create_package():
print("发布包创建失败")
sys.exit(1)
print("\n" + "=" * 60)
print("发布构建完成!")
print("发布包已创建在当前目录下")
print("=" * 60)
if __name__ == "__main__":
main()

@ -1,83 +0,0 @@
#!/usr/bin/env python3
"""
MagicWord macOS简化打包脚本
"""
import os
import sys
import subprocess
import shutil
def run_command(cmd):
"""运行命令"""
print(f"运行: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"错误: {result.stderr}")
return False
return True
def main():
print("=== MagicWord macOS打包 ===")
# 清理旧的构建文件
print("清理构建文件...")
for folder in ['build', 'dist', '__pycache__']:
if os.path.exists(folder):
shutil.rmtree(folder)
# 安装依赖
print("安装依赖...")
if not run_command([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]):
return
if not run_command([sys.executable, "-m", "pip", "install", "pyinstaller"]):
return
# 构建命令 - 包含所有必要的图片和图标文件
cmd = [
sys.executable, "-m", "PyInstaller",
"--name", "MagicWord",
"--distpath", "dist",
"--workpath", "build",
# 资源文件
"--add-data", "resources:resources",
# UI图片文件
"--add-data", "src/ui/UI.png:ui",
"--add-data", "src/ui/114514.png:ui",
# 图标文件
"--add-data", "resources/icons/app_icon_32X32.png:resources/icons",
"--add-data", "resources/icons/app_icon_64X64.png:resources/icons",
"--add-data", "resources/icons/app_icon_128X128.png:resources/icons",
"--add-data", "resources/icons/app_icon_256X256.png:resources/icons",
# 隐藏导入模块
"--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",
# macOS应用选项
"--windowed",
"--osx-bundle-identifier", "com.magicword.app",
"--icon", "resources/icons/app_icon_128X128.png",
"src/main.py"
]
print("构建应用...")
if run_command(cmd):
print("✅ 构建成功!")
print(f"应用位置: {os.path.abspath('dist/MagicWord.app')}")
print("\n安装步骤:")
print("1. 将 MagicWord.app 拖拽到 Applications 文件夹")
print("2. 首次运行时,右键点击应用选择'打开'")
else:
print("❌ 构建失败!")
if __name__ == "__main__":
main()

@ -1,110 +0,0 @@
#!/usr/bin/env python3
"""
MagicWord v0.3 打包脚本
版本号0.3
输出目录dist/
图标resources/icons/app_icon_256X256.png
"""
import os
import sys
import subprocess
import shutil
from pathlib import Path
def clean_dist():
"""清理dist目录"""
dist_dir = Path("dist")
if dist_dir.exists():
shutil.rmtree(dist_dir)
dist_dir.mkdir(exist_ok=True)
def build_executable():
"""使用PyInstaller构建可执行文件"""
print("开始构建 MagicWord v0.3...")
# 清理之前的构建
clean_dist()
# PyInstaller 参数
pyinstaller_args = [
"-m", "PyInstaller",
"--onefile", # 单文件模式
"--windowed", # 窗口模式(无控制台)
f"--name=MagicWord_v0.3", # 可执行文件名
f"--icon=resources/icons/app_icon_256X256.png", # 图标文件
"--add-data=resources:resources", # 添加资源文件
"--add-data=src:src", # 添加源代码
"--clean", # 清理临时文件
"--noconfirm", # 不确认覆盖
"src/main.py" # 主程序入口
]
# 执行打包命令
cmd = [sys.executable] + pyinstaller_args
print(f"执行命令: {' '.join(cmd)}")
try:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"打包失败: {result.stderr}")
return False
else:
print("打包成功!")
return True
except Exception as e:
print(f"执行打包时出错: {e}")
return False
def check_output():
"""检查输出文件"""
dist_dir = Path("dist")
exe_files = list(dist_dir.glob("MagicWord_v0.3*"))
if exe_files:
print(f"生成的文件:")
for exe in exe_files:
size = exe.stat().st_size / (1024 * 1024) # MB
print(f" {exe.name} - {size:.1f} MB")
return True
else:
print("未找到生成的可执行文件")
return False
def main():
"""主函数"""
print("=" * 50)
print("MagicWord v0.3 打包工具")
print("=" * 50)
# 检查Python环境
print(f"Python版本: {sys.version}")
print(f"Python路径: {sys.executable}")
# 检查文件是否存在
required_files = [
"src/main.py",
"resources/icons/app_icon_256X256.png"
]
missing_files = []
for file in required_files:
if not Path(file).exists():
missing_files.append(file)
if missing_files:
print(f"缺少必需文件: {missing_files}")
return
# 构建可执行文件
if build_executable():
# 检查输出
if check_output():
print("\n✅ 打包完成!可执行文件位于 dist/ 目录")
else:
print("\n❌ 打包可能存在问题")
else:
print("\n❌ 打包失败")
if __name__ == "__main__":
main()

@ -1,331 +0,0 @@
#!/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)

@ -1,193 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MagicWord 0.2.1 手动发布包创建脚本
"""
import os
import shutil
import zipfile
from datetime import datetime
def create_manual_package():
"""创建手动发布包"""
print("创建 MagicWord 0.2.1 手动发布包...")
# 创建发布目录
release_dir = "dist_package"
if os.path.exists(release_dir):
shutil.rmtree(release_dir)
os.makedirs(release_dir)
# 复制主要文件
files_to_copy = [
("README.md", "README.md"),
("CHANGELOG.md", "CHANGELOG.md"),
("requirements.txt", "requirements.txt"),
("setup.py", "setup.py"),
("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} 不存在")
# 复制源代码
src_dir = os.path.join(release_dir, "src")
if os.path.exists("src"):
shutil.copytree("src", src_dir)
print("复制源代码目录")
else:
print("警告: src 目录不存在")
# 复制资源文件
resources_dir = os.path.join(release_dir, "resources")
if os.path.exists("resources"):
shutil.copytree("resources", resources_dir)
print("复制资源文件目录")
else:
print("警告: resources 目录不存在")
# 创建运行脚本
run_bat = """@echo off
echo MagicWord 0.2.1 启动中...
cd /d "%~dp0"
python src/main.py
pause
"""
run_sh = """#!/bin/bash
echo "MagicWord 0.2.1 启动中..."
cd "$(dirname "$0")"
python3 src/main.py
"""
with open(os.path.join(release_dir, "run.bat"), "w", encoding="utf-8") as f:
f.write(run_bat)
with open(os.path.join(release_dir, "run.sh"), "w", encoding="utf-8") as f:
f.write(run_sh)
# 创建安装脚本
install_bat = """@echo off
echo 正在安装 MagicWord 0.2.1 依赖...
python -m pip install -r requirements.txt
echo 安装完成!
pause
"""
with open(os.path.join(release_dir, "install.bat"), "w", encoding="utf-8") as f:
f.write(install_bat)
# 创建发布说明
release_info = f"""MagicWord 0.2.1 手动发布包
构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
安装和运行说明:
1. 安装依赖:
- Windows: 运行 install.bat
- Linux/Mac: pip install -r requirements.txt
2. 运行应用:
- Windows: 运行 run.bat
- Linux/Mac: 运行 run.sh python3 src/main.py
3. 主要功能:
- 完整的天气功能集成
- 自动IP定位功能
- 40+个城市支持
- 中英文城市名智能映射
- Microsoft Word风格界面
- 每日一言功能集成
- 附加工具菜单原天气信息菜单重命名
详细更新请查看 CHANGELOG.md
注意: 需要Python 3.6+环境
"""
with open(os.path.join(release_dir, "RELEASE_INFO.txt"), "w", encoding="utf-8") as f:
f.write(release_info)
# 创建ZIP包
zip_name = f"MagicWord_v0.2.1_Manual_Package.zip"
with zipfile.ZipFile(zip_name, '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_name = os.path.relpath(file_path, release_dir)
zipf.write(file_path, arc_name)
print(f"\n发布包创建成功: {zip_name}")
print(f"包含文件数量: {sum(len(files) for _, _, files in os.walk(release_dir))}")
return True
def verify_package():
"""验证发布包内容"""
print("\n验证发布包内容...")
release_dir = "dist_package"
if not os.path.exists(release_dir):
print("发布目录不存在")
return False
# 检查关键文件
required_files = [
"README.md",
"CHANGELOG.md",
"requirements.txt",
"src/main.py",
"src/ui/word_style_ui.py",
"run.bat",
"run.sh",
"install.bat",
"RELEASE_INFO.txt"
]
missing_files = []
for file_path in required_files:
full_path = os.path.join(release_dir, file_path)
if os.path.exists(full_path):
print(f"{file_path}")
else:
print(f"{file_path}")
missing_files.append(file_path)
if missing_files:
print(f"\n缺失文件: {', '.join(missing_files)}")
return False
print("\n发布包验证通过!")
return True
def main():
"""主函数"""
print("=" * 60)
print("MagicWord 0.2.1 手动发布包创建工具")
print("=" * 60)
# 创建发布包
if not create_manual_package():
print("发布包创建失败")
return 1
# 验证发布包
if not verify_package():
print("发布包验证失败")
return 1
print("\n" + "=" * 60)
print("手动发布包创建完成!")
print("用户需要手动安装Python依赖并运行应用")
print("=" * 60)
return 0
if __name__ == "__main__":
import sys
sys.exit(main())

@ -1,287 +0,0 @@
《软件工程课程设计》
软件设计规格说明书
MagicWord
组长学号姓名: 230340211 马子昂
成员学号姓名: 230340233 石兴霖
230340219 黄俊源
230340210 陆冲
230340235 马明义
二〇二五年九月
第1章 引言
1.1 软件设计目标和原则
设计目标
隐私学习体验允许用户在看似普通文档编辑的环境中学习内容如试卷、单词或古文从而避免他人的闲言碎语实现“在Word里打开试卷但他人看你是在敲文档”的效果。
多格式文件支持能够打开和处理多种文件格式包括doc、txt、pdf和epub以确保用户能够导入不同类型的学习材料。
学习功能集成:支持单词学习、古文背诵等具体学习场景,通过打字输入输出相应内容,使学习过程交互化和可控。
用户界面仿Word设计提供与Microsoft Word相似的页面布局和体验减少用户的学习曲线增强熟悉感和易用性。
附加功能增强:集成额外功能如查看天气、每日一言、历史上的今天和黄历等,以提升软件的实用性和用户粘性。
图片输出支持:能够处理并输出文件中的图片,确保图片在打字输入过程中正常显示,并控制图片大小。
设计原则
用户中心原则软件设计以用户需求为核心注重隐私保护和用户体验例如通过模仿Word界面来降低使用门槛。
兼容性与扩展性原则支持多种文件格式特别是pdf和epub表明设计时考虑了兼容性和未来可能的功能扩展尽管这带来了技术挑战。
功能丰富性原则:不仅专注于核心学习功能,还集成天气、每日一言等附加元素,以提供更全面的服务,增加软件的价值。
创新性原则:瞄准市场上未有的功能(“开荒”),致力于解决独特痛点,如通过打字输入控制内容输出,体现创新思维。
可行性驱动原则在设计中承认潜在风险如处理pdf文件的困难、图片输出的技术问题强调务实 approach确保开发过程注重技术可行性和问题解决。
1.2 软件设计的约束和限制
- 运行环境要求Windows/MacOS
- 开发语言python
- 标准规范python编码风格规范
- 开发工具PyCharm
第2章 软件体系结构设计
图 2-1 描述了 “MagicWord” 系统的设计架构,包括了用户界面层、业务逻辑层及数据层。其中:
界面层负责显示用户界面,承担了边界类 “伪装界面模块类”“输入处理模块类”“信息展示模块类” 与用户交互有关的职责。
业务逻辑层负责核心业务逻辑处理,它将承担 “进度管理模块类”“伪装学习控制器类”“文件解析引擎类”“格式转换服务类” 等控制类的所有职责。“天气服务客户端类”“资讯内容接口类” 是用于支持与 “天气服务”“资讯服务” 外部子系统的交互,从而实现系统获取天气、资讯内容的功能。
数据层负责系统的数据存储,它包括了 “进度数据存储类”“用户配置存储类”
“内容缓存管理类” 等实体类
图2-1“MagicWord”系统体系结构逻辑视图
第3章 用户界面设计
3.1 系统界面的外观设计及其类表示
图3-1 Magic Word系统界面外观设计欢迎页面
3.2 系统界面流设计
<用简短语言描述该系统界面流的界面职责及跳转关系>
根据“Magic Word”的用例描述以及每个用例的交互图可以发现该软件系统的APP 需要有以下一组界面以支持用户的操作。
主界面仿Word布局
核心工作区:伪装成普通文档编辑界面
实时显示学习内容(文本+图片)
集成状态栏(天气/黄历等附加信息)
隐私保护:动态生成干扰文本,维持"普通文档"外观
文件加载界面
支持多格式导入doc/txt/pdf/epub
文件转换PDF/EPUB转可编辑文本
图片处理:提取图片并生成占位符
学习模式界面
设置学习场景:单词/古文/试卷
配置学习参数:显示节奏、错误标记规则
激活键盘驱动学习机制
图片控制界面
调整图片属性:尺寸/位置
图片压缩优化
文档流重排控制
附加功能面板
展示实用信息:天气/每日一言/历史事件
可折叠侧边栏设计
信息详情查看
图3-Magic Word系统界面流的顺序图
第4章 详细设计
4.1 用例设计
4.1.1打开文件用例实现的设计方案
"打开文件"功能是系统的核心入口功能负责处理用户选择并加载不同格式的学习文件。具体实现过程见图4-1所描述的用例设计顺序图。
图4-1 “打开文件”用例设计顺序图
首先,用户通过界面类"MainUI"对象点击"打开文件"按钮,触发文件选择对话框。用户选择目标文件后,"MainUI"对象向控制类"FileController"发送消息"openFile(filePath)",请求打开指定文件。接收到消息后,"FileController"对象根据文件扩展名判断文件格式,并调用相应的文件解析器:
对于.txt格式调用"TextParser"对象的"parseText(filePath)"方法
对于.doc/.docx格式调用"WordParser"对象的"parseWord(filePath)"方法
对于.pdf格式调用"PDFParser"对象的"parsePDF(filePath)"方法
对于.epub格式调用"EPUBParser"对象的"parseEPUB(filePath)"方法
文件解析器负责读取文件内容并进行格式解析,将解析后的内容(包括文本、图片、格式信息等)封装为"Document"实体对象返回给"FileController"。"FileController"随后调用"DisplayManager"对象的"displayDocument(doc)"方法将文档内容渲染到仿Word界面上。如果文件打开过程中出现错误如文件损坏、格式不支持等系统通过"MainUI"对象的"showError(message)"方法向用户显示错误信息。
4.1.2打字输入并输出文件内容用例实现的设计方案
“打字输入并输出文件内容”功能的实现主要由“FileManager”和“DisplayManager”类协同完成。用户通过界面输入字符系统将其与原文进行比对并动态显示在仿Word界面上。具体实现过程见图4-2所描述的用例设计顺序图。
图4-2 “打字输入并输出内容”用例设计顺序图
首先用户通过“TypingUI”界面输入字符触发“onKeyPress(event)”事件。随后“TypingUI”对象向控制类“TypingController”发送消息“processInput(char)”请求处理输入内容。“TypingController”接着调用“FileManager”对象的“getNextChar()”方法获取下一个待输入字符并与用户输入进行比对。比对结果通过“DisplayManager”对象的“updateDisplay(char, status)”方法更新界面显示其中status表示输入正确或错误。同时“ProgressManager”对象负责更新用户的学习进度。
4.1.3查看天气信息用例实现的设计方案
“查看天气信息”功能通过调用外部天气API实现。用户在主界面点击天气图标后系统获取并显示当前天气信息。具体实现过程见图4-3所描述的用例设计顺序图。
图4-3“查看天气”用例设计顺序图
用户通过“MainUI”界面点击天气按钮触发“onWeatherClick()”事件。“MainUI”对象向控制类“WeatherController”发送消息“fetchWeatherData()”。“WeatherController”调用“WeatherService”对象的“getWeather(location)”方法通过HTTP请求获取天气数据。获取成功后数据返回至“WeatherController”再通过“MainUI”对象的“displayWeather(info)”方法显示在界面上
4.1.4查看每日一言用例实现的设计方案
“查看每日一言”功能通过本地或网络获取每日激励语句。用户在主界面点击“每日一言”按钮后触发。具体实现过程见图4-4所描述的用例设计顺序图。
图4-4 “查看每日一言”用例设计顺序图
用户通过“MainUI”界面点击“每日一言”按钮触发“onDailyQuoteClick()”事件。“MainUI”对象向控制类“QuoteController”发送消息“getDailyQuote()”。“QuoteController”调用“QuoteService”对象的“fetchQuote()”方法获取语句内容。获取成功后内容通过“MainUI”对象的“showQuote(text)”方法显示。
4.1.5查看历史上的今天用例实现的设计方案
“查看历史上的今天”功能通过调用历史事件API实现。用户在主界面点击相应按钮后触发。具体实现过程见图4-5所描述的用例设计顺序图。
图4-5 “查看历史上的今天”用例设计顺序图
用户通过“MainUI”界面点击“历史上的今天”按钮触发“onHistoryClick()”事件。“MainUI”对象向控制类“HistoryController”发送消息“getHistoryEvents()”。“HistoryController”调用“HistoryService”对象的“fetchEvents(date)”方法获取事件列表。获取成功后通过“MainUI”对象的“displayHistory(events)”方法显示。
4.1.6查看黄历信息用例实现的设计方案
“查看黄历信息”功能通过本地数据库或网络API获取黄历信息。用户在主界面点击黄历图标后触发。具体实现过程见图4-6所描述的用例设计顺序图。
图4-6 “查看黄历信息”用例设计顺序图
用户通过“MainUI”界面点击黄历按钮触发“onAlmanacClick()”事件。“MainUI”对象向控制类“AlmanacController”发送消息“getAlmanacData()”。“AlmanacController”调用“AlmanacService”对象的“fetchAlmanac(date)”方法获取数据。获取成功后通过“MainUI”对象的“showAlmanac(info)”方法显示。
4.1.7输出文件中的图片用例实现的设计方案
“输出文件中的图片”功能用于在打字学习过程中显示文档内嵌图片。具体实现过程见图4-7所描述的用例设计顺序图。
图4-7 “输出文件中的图片”用例设计顺序图
当学习内容中包含图片时“DisplayManager”对象调用“ImageRenderer”对象的“renderImage(imageData, position)”方法。“ImageRenderer”负责解码图片数据并调整尺寸随后通过“TypingUI”对象的“displayImage(img)”方法在指定位置显示图片。
4.1.8切换文件格式用例实现的设计方案
“切换文件格式”功能允许用户在不同文件格式如doc、pdf、epub之间切换显示。具体实现过程见图4-8所描述的用例设计顺序图。
图4-8 “切换文件格式”用例设计顺序图
用户通过“MainUI”界面选择文件格式触发“onFormatChange(format)”事件。“MainUI”对象向“FormatController”发送消息“switchFormat(format)”。“FormatController”调用“FileManager”对象的“convertFile(format)”方法进行格式转换转换成功后通过“DisplayManager”更新界面内容。
4.1.9保存当前进度用例实现的设计方案
“保存当前进度”功能用于记录用户的学习进度以便下次继续学习。具体实现过程见图4-9所描述的用例设计顺序图。
图4-9 “保存当前进度”用例设计顺序图
用户通过“MainUI”界面点击保存按钮触发“onSaveProgress()”事件。“MainUI”对象向“ProgressController”发送消息“saveProgress(userId, progress)”。“ProgressController”调用“ProgressManager”对象的“saveToDatabase(progressData)”方法将进度数据存入数据库。保存成功后,界面显示保存成功提示。
4.2 类设计
核心类属性与操作
MainUI
属性currentFilename、statusMessage、displayContent
操作(文件/内容显示、错误提示、状态更新等)。
FileDialog
属性supportedFormats、currentDirectory
操作(浏览目录、选择/取消文件)。
FileManager
属性maxFileSize、supportedFormats
操作(打开/验证文件、解析文本/提取图片)。
InputHandler
属性charCount、imageTriggerThreshold
操作(按键处理、字符输出、重置计数器)。
WeatherServiceController
操作(获取/解析天气数据)。
QuoteServiceController
操作(获取每日名言并缓存)。
HistoryServiceController
操作(获取历史事件并过滤)。
AlmanacServiceController
操作(获取年鉴数据并缓存)。
SettingsManager
属性supportedFormats
操作(更新格式、保存设置)。
ProgressManager
操作(保存/加载进度)。
类间关系
MainUI 依赖多个控制器如 FileManager、InputHandler 等),通过方法调用实现功能。
FileDialog  FileManager 提供文件选择界面二者协作完成文件操作。
FileManager 处理文件后生成 FileContent 对象包含文本、图片等信息
各服务控制器Weather/Quote/History/Almanac分别与对应数据模型Weather/DailyQuote/HistoricalEvent/Almanac关联负责数据获取与缓存。
SettingsManager 管理 UserSettings 配置
ProgressManager 管理 ProgressData 进度信息。
图4-10 MagicWord系统设计类图
4.3 数据模型设计
4.3.1 MagicWord系统数据设计类图
数据库表设计:
1.T_progressData(学习进度表)
2.T_UserSettings(用户设置表)
3.T_User(用户表)
图4-11MagicWord系统数据设计类图
4.3.2 MagicWord系统数据的操作设计
1. ProgressDataLibrary 类设计
为了支持对"T_ProgressData"数据库表的操作,设计模型中有关键设计类"ProgressDataLibrary",它提供了一组方法以实现对学习进度数据的增删改查操作。具体的接口描述如下:
boolean insertProgress(ProgressData progress)
boolean deleteProgress(int progressID)
boolean updateProgress(ProgressData progress)
ProgressData getProgressByID(int progressID)
List<ProgressData> getProgressByUser(int userID)
List<ProgressData> getRecentProgress(int userID, int limit)
ProgressDataLibrary()
~ProgressDataLibrary()
void openDatabase()
void closeDatabase()
2. UserSettingsLibrary 类设计
为了支持对"T_UserSettings"数据库表的操作,设计模型中有关键设计类"UserSettingsLibrary",它提供用户设置数据的管理功能。具体的接口描述如下:
boolean insertSettings(UserSettings settings)
boolean updateSettings(UserSettings settings)
UserSettings getSettingsByUser(int userID)
boolean updateSupportedFormats(int userID, String formats)
boolean updateImageThreshold(int userID, int threshold)
UserSettingsLibrary()
~UserSettingsLibrary()
void openDatabase()
void closeDatabase()
3.UserLibrary 类设计
为了支持对"T_User"数据库表的操作,设计模型中有关键设计类"UserLibrary",它提供用户基本信息的完整管理功能。具体的接口描述如下:
boolean insertUser(User user)
boolean deleteUser(User user)
boolean updateUser(User user)
User getUserByAccount(String account)
User getUserByID(int userID)
boolean verifyUserValidity(String account, String password)
boolean isUsernameExists(String username)
boolean updatePassword(int userID, String newPassword)
UserLibrary()
~UserLibrary()
void openDatabase()
void closeDatabase()
4. 数据库连接管理设计
DatabaseManager 单例类:
static DatabaseManager getInstance()
Connection getConnection()
void releaseConnection(Connection conn)
boolean testConnection()
void beginTransaction()
void commitTransaction()
void rollbackTransaction()
5. 异常处理设计
自定义异常类:
DatabaseConnectionException - 数据库连接异常
DataAccessException - 数据访问异常
UserNotFoundException - 用户不存在异常
DuplicateUserException - 用户重复异常
6. 数据验证规则
用户数据验证:
用户名3-20字符只能包含字母数字
密码6-50字符必须包含字母和数字
邮箱:符合标准邮箱格式
文件路径最大255字符路径有效性检查
进度数据验证:
字符数:非负整数
光标位置:有效范围内
时间戳:合理的时间范围
4.4 部署设计
"MagicWord系统"采用混合部署的方式见图4-X其中"MagicWord客户端"子系统部署在用户本地的Windows或macOS操作系统上"云服务API服务器"子系统部署在云端基于Ubuntu操作系统的云服务器上它通过RESTful API与第三方服务进行交互。云端服务器还部署了MySQL数据库管理系统以保存系统中的用户信息和学习数据。客户端与服务器之间通过HTTPS协议进行网络连接从而实现数据同步和服务交互。
图4-12 MagicWord系统的部署图
其中软件系统各组成部分产品所运行的外部环境如表4-1所示
表4-1 软件与外界环境的交互关系
4.4.1 网络通信架构
客户端与服务器之间的通信采用标准的HTTP/HTTPS协议具体通信模式如下
客户端→服务器通信:
认证接口用户登录验证、Token刷新
数据同步接口:学习进度上传下载、设置同步
内容服务接口:天气信息、每日一言、历史事件等获取
服务器→第三方服务通信:
天气数据通过和风天气API获取实时天气信息
内容服务:从权威数据源获取每日名言、历史事件等
文件解析服务:复杂文件格式的云端解析支持
4.4.2 数据存储策略
系统采用分层数据存储架构,确保数据安全性和访问效率:
本地存储SQLite
用户当前学习进度
个性化界面设置
最近打开的文件记录
网络数据缓存24小时有效期
云端存储MySQL
用户账户信息(加密存储)
跨设备学习进度备份
用户使用统计和分析数据
系统配置和版本信息
4.4.3 安全部署措施
为确保系统安全性,部署过程中采取以下安全措施:
通信安全:
全链路HTTPS加密传输
JWT Token身份认证机制
API访问频率限制和防爬虫保护
敏感数据端到端加密
数据安全:
用户密码采用bcrypt加密存储
个人学习数据隔离存储
定期数据备份和灾难恢复机制
符合GDPR的数据隐私保护规范

@ -1,55 +0,0 @@
#!/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"

@ -1,237 +0,0 @@
#!/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)

@ -1,201 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MagicWord 安装和修复脚本
用于安装依赖并解决Qt平台插件问题
"""
import os
import sys
import subprocess
import platform
import importlib.util
def check_python_version():
"""检查Python版本"""
version = sys.version_info
print(f"当前Python版本: {version.major}.{version.minor}.{version.micro}")
if version.major < 3 or (version.major == 3 and version.minor < 6):
print("错误: 需要Python 3.6或更高版本")
return False
return True
def run_command(command, shell=False):
"""运行命令并返回结果"""
try:
result = subprocess.run(command, shell=shell, capture_output=True, text=True, encoding='utf-8')
return result.returncode, result.stdout, result.stderr
except Exception as e:
return -1, "", str(e)
def install_requirements():
"""安装requirements.txt中的依赖"""
requirements_file = "requirements.txt"
if not os.path.exists(requirements_file):
print(f"错误: 找不到 {requirements_file} 文件")
return False
print("安装项目依赖...")
try:
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "-r", requirements_file])
if code == 0:
print("所有依赖安装成功")
return True
else:
print(f"依赖安装失败: {stderr}")
return False
except Exception as e:
print(f"依赖安装异常: {e}")
return False
def check_pyqt5():
"""检查PyQt5安装"""
print("检查PyQt5安装...")
try:
import PyQt5
pyqt5_path = os.path.dirname(PyQt5.__file__)
print(f"PyQt5已安装路径: {pyqt5_path}")
return True
except ImportError:
print("PyQt5未安装")
return False
def reinstall_pyqt5():
"""重新安装PyQt5"""
print("重新安装PyQt5...")
try:
# 先卸载
print("卸载PyQt5...")
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "uninstall", "PyQt5", "-y"])
# 重新安装
print("安装PyQt5...")
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "PyQt5"])
if code == 0:
print("PyQt5重新安装成功")
return True
else:
print(f"PyQt5重新安装失败: {stderr}")
return False
except Exception as e:
print(f"PyQt5重新安装异常: {e}")
return False
def create_qt_debug_script():
"""创建Qt调试运行脚本"""
system = platform.system()
if system == "Windows":
script_content = """@echo off
echo 设置Qt调试环境变量...
set QT_DEBUG_PLUGINS=1
echo Qt调试模式已启用
echo.
echo 运行MagicWord应用程序...
python src/main.py
pause
"""
script_name = "run_debug.bat"
else:
script_content = """#!/bin/bash
echo "设置Qt调试环境变量..."
export QT_DEBUG_PLUGINS=1
echo "Qt调试模式已启用"
echo ""
echo "运行MagicWord应用程序..."
python src/main.py
"""
script_name = "run_debug.sh"
try:
with open(script_name, "w", encoding="utf-8") as f:
f.write(script_content)
print(f"调试运行脚本创建完成: {script_name}")
return True
except Exception as e:
print(f"创建调试运行脚本失败: {e}")
return False
def main():
"""主函数"""
print("=" * 60)
print("MagicWord 项目安装和修复脚本")
print("=" * 60)
# 检查Python版本
if not check_python_version():
sys.exit(1)
# 升级pip
print("\n升级pip...")
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "--upgrade", "pip"])
if code != 0:
print(f"pip升级警告: {stderr}")
else:
print("pip升级完成")
# 安装依赖
print("\n安装项目依赖...")
if not install_requirements():
print("依赖安装失败")
sys.exit(1)
# 检查PyQt5
print("\n检查PyQt5...")
if not check_pyqt5():
print("PyQt5未安装开始安装...")
code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "PyQt5"])
if code == 0:
print("PyQt5安装成功")
else:
print(f"PyQt5安装失败: {stderr}")
# 尝试重新安装
if not reinstall_pyqt5():
print("PyQt5重新安装也失败了")
# 再次检查PyQt5
if check_pyqt5():
# 检查PyQt5插件路径
try:
import PyQt5
pyqt5_path = os.path.dirname(PyQt5.__file__)
plugin_paths = [
os.path.join(pyqt5_path, "Qt5", "plugins"),
os.path.join(pyqt5_path, "plugins"),
]
plugin_path = None
for path in plugin_paths:
if os.path.exists(path):
plugin_path = path
break
if plugin_path:
print(f"PyQt5插件路径: {plugin_path}")
platforms_path = os.path.join(plugin_path, "platforms")
if os.path.exists(platforms_path):
print(f"Platform插件路径: {platforms_path}")
else:
print("未找到platforms目录")
else:
print("未找到PyQt5插件目录")
except Exception as e:
print(f"检查PyQt5插件时出错: {e}")
# 创建调试脚本
print("\n创建调试运行脚本...")
create_qt_debug_script()
print("\n" + "=" * 60)
print("安装和修复完成!")
print("如果问题仍然存在,请尝试以下方法:")
print("1. 运行创建的调试脚本查看详细错误信息")
print("2. 检查是否安装了Visual C++运行库")
print("3. 确保使用的是Python 3.6或更高版本")
print("=" * 60)
if __name__ == "__main__":
main()

@ -1,65 +0,0 @@
#!/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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

@ -1,251 +0,0 @@
#!/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())

@ -1,106 +0,0 @@
#!/bin/bash
# MagicWord macOS快速打包脚本
echo "=== MagicWord macOS打包脚本 ==="
echo "开始打包进程..."
# 检查Python版本
python_version=$(python3 --version 2>/dev/null || python --version)
echo "Python版本: $python_version"
# 安装依赖
echo "安装依赖..."
pip3 install -r requirements.txt
pip3 install pyinstaller
# 清理旧的构建文件
echo "清理旧的构建文件..."
rm -rf build dist __pycache__ *.spec
find src -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
# 构建应用
echo "构建macOS应用..."
python3 -m pyinstaller \\
--name "MagicWord" \\
--distpath "dist" \\
--workpath "build" \\
--add-data "resources:resources" \\
--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" \\
--windowed \\
--osx-bundle-identifier "com.magicword.app" \\
--target-architecture arm64 \\
--noconfirm \\
src/main.py
# 检查构建结果
if [ -d "dist/MagicWord.app" ]; then
echo "✅ 构建成功!"
echo "应用位置: dist/MagicWord.app"
echo ""
echo "安装步骤:"
echo "1. 将 MagicWord.app 拖拽到 Applications 文件夹"
echo "2. 首次运行时,右键点击应用选择'打开'"
echo "3. 或者在 系统设置 > 隐私与安全性 中允许应用运行"
else
echo "❌ 构建失败!"
exit 1
fi
# 可选创建DMG
read -p "是否创建DMG安装包(y/n): " create_dmg
if [[ $create_dmg =~ ^[Yy]$ ]]; then
echo "创建DMG安装包..."
# 创建发布目录
release_dir="macos_release"
rm -rf "$release_dir"
mkdir -p "$release_dir"
# 复制应用
cp -r "dist/MagicWord.app" "$release_dir/"
# 创建Applications链接
ln -s "/Applications" "$release_dir/Applications"
# 创建README
cat > "$release_dir/README.txt" << 'EOF'
MagicWord for macOS
安装说明:
1. 将 MagicWord.app 拖拽到 Applications 文件夹
2. 首次运行时,右键点击应用选择"打开"
3. 或者在 系统设置 > 隐私与安全性 中允许应用运行
系统要求:
- macOS Big Sur (11.0) 或更高版本
- Apple Silicon (M1/M2/M3) 推荐
功能特性:
- 隐私学习:通过打字练习来学习文档内容
- 支持多种文档格式TXT, DOCX, PDF
- 智能打字模式
- 美观的Word风格界面
EOF
# 创建DMG
dmg_name="MagicWord-0.3.0-macOS-arm64.dmg"
hdiutil create -volname "MagicWord" -srcfolder "$release_dir" -ov -format UDZO "$dmg_name"
if [ -f "$dmg_name" ]; then
echo "✅ DMG创建成功: $dmg_name"
else
echo "⚠️ DMG创建失败但应用包已准备就绪"
fi
fi
echo "=== 打包完成 ==="

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 B

@ -1,8 +0,0 @@
@echo off
echo 设置Qt调试环境变量...
set QT_DEBUG_PLUGINS=1
echo Qt调试模式已启用
echo.
echo 运行MagicWord应用程序...
python src/main.py
pause

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

@ -1,20 +0,0 @@
#!/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

@ -1,29 +0,0 @@
#!/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

@ -1,7 +0,0 @@
# 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,15 +2,12 @@ from setuptools import setup, find_packages
setup(
name="MagicWord",
version="1.0.0",
version="0.1.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",
@ -27,17 +24,4 @@ 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",
],
)

@ -1,65 +0,0 @@
#!/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()

@ -1,58 +0,0 @@
#!/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()

@ -1,773 +0,0 @@
"""
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()

@ -1,12 +1,10 @@
import os
import zipfile
import tempfile
from typing import Union, List, Tuple
from typing import Union
class FileParser:
@staticmethod
def parse_file(file_path: str) -> str:
"""解析文件并返回文本内容"""
# 验证文件路径
if not FileParser.validate_file_path(file_path):
raise ValueError(f"Invalid file path: {file_path}")
@ -23,110 +21,30 @@ 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:
# 统一异常处理
raise Exception(f"Error parsing file {file_path}: {str(e)}")
@staticmethod
def parse_and_convert_to_txt(file_path: str, output_dir: str = None) -> dict:
"""
解析文件并转换为txt格式保留图片和分段
Args:
file_path: 输入文件路径
output_dir: 输出目录如果为None则使用临时目录
Returns:
dict: 包含转换结果的信息
- 'txt_path': 生成的临时txt文件路径
- 'images': 提取的图片列表 [(文件名, 二进制数据), ...]
- 'content': 转换后的文本内容
- 'success': 是否成功
- 'error': 错误信息如果有
"""
try:
# 验证输入文件
if not FileParser.validate_file_path(file_path):
return {
'success': False,
'error': f"Invalid file path: {file_path}"
}
# 使用临时文件而不是永久文件
import tempfile
# 获取文件扩展名
_, ext = os.path.splitext(file_path)
ext = ext.lower()
# 提取文本内容
content = ""
images = []
if ext == '.txt':
# TXT文件直接读取内容
content = FileParser.parse_txt(file_path)
images = [] # TXT文件没有图片
elif ext == '.docx':
# DOCX文件提取文本和图片
content = FileParser.parse_docx(file_path)
images = FileParser.extract_images_from_docx(file_path)
elif ext == '.pdf':
# PDF文件提取文本图片处理较复杂暂时只提取文本
content = FileParser.parse_pdf(file_path)
images = [] # PDF图片提取较复杂暂时跳过
elif ext == '.html':
# HTML文件提取文本内容
content = FileParser.parse_html(file_path)
images = [] # HTML图片提取较复杂暂时跳过
else:
return {
'success': False,
'error': f"Unsupported file format: {ext}"
}
# 创建临时文件而不是永久文件
base_name = os.path.splitext(os.path.basename(file_path))[0]
# 创建临时txt文件程序结束时会被自动清理
with tempfile.NamedTemporaryFile(mode='w', encoding='utf-8',
suffix=f'_{base_name}_converted.txt',
delete=False) as temp_file:
temp_file.write(content)
txt_path = temp_file.name
return {
'success': True,
'txt_path': txt_path,
'images': images,
'content': content,
'original_ext': ext,
'is_temp_file': True # 标记这是临时文件
}
except Exception as e:
return {
'success': False,
'error': str(e)
}
@staticmethod
def parse_txt(file_path: str) -> str:
"""解析TXT文件"""
# 验证文件路径
if not FileParser.validate_file_path(file_path):
raise ValueError(f"Invalid file path: {file_path}")
# 尝试多种编码方式读取文件
encoding = FileParser.detect_file_encoding(file_path)
# 导入工具函数来检测编码
try:
from src.utils.helper_functions import Utils
except ImportError:
# 如果无法导入,使用默认方法检测编码
import chardet
with open(file_path, 'rb') as f:
raw_data = f.read(1024)
encoding = chardet.detect(raw_data)['encoding'] or 'utf-8'
else:
# 使用工具函数检测编码
encoding = Utils.detect_encoding(file_path)
# 读取文件内容
with open(file_path, 'r', encoding=encoding, errors='ignore') as f:
@ -137,85 +55,9 @@ class FileParser:
return content
@staticmethod
def detect_file_encoding(file_path: str) -> str:
"""检测文件编码"""
# 首先尝试UTF-8
try:
with open(file_path, 'r', encoding='utf-8') as f:
f.read()
return 'utf-8'
except UnicodeDecodeError:
pass
# 尝试GBK中文Windows常用
try:
with open(file_path, 'r', encoding='gbk') as f:
f.read()
return 'gbk'
except UnicodeDecodeError:
pass
# 尝试GB2312
try:
with open(file_path, 'r', encoding='gb2312') as f:
f.read()
return 'gb2312'
except UnicodeDecodeError:
pass
# 尝试使用chardet如果可用
try:
import chardet
with open(file_path, 'rb') as f:
raw_data = f.read(1024)
result = chardet.detect(raw_data)
if result and result['encoding']:
return result['encoding']
except ImportError:
pass
# 默认返回UTF-8
return 'utf-8'
@staticmethod
def extract_images_from_docx(file_path: str) -> List[Tuple[str, bytes]]:
"""从Word文档中提取图片
Args:
file_path: Word文档路径
Returns:
图片列表每个元素为(图片文件名, 图片二进制数据)的元组
"""
if not FileParser.validate_file_path(file_path):
raise ValueError(f"Invalid file path: {file_path}")
images = []
try:
# Word文档实际上是ZIP文件可以直接解压
with zipfile.ZipFile(file_path, 'r') as zip_file:
# 遍历ZIP文件中的所有文件
for file_info in zip_file.filelist:
file_name = file_info.filename
# Word文档中的图片通常存储在word/media/目录下
if file_name.startswith('word/media/') and file_info.file_size > 0:
# 读取图片数据
image_data = zip_file.read(file_name)
# 获取图片扩展名
image_ext = os.path.splitext(file_name)[1].lower()
if image_ext in ['.png', '.jpg', '.jpeg', '.gif', '.bmp']:
# 保存图片信息
base_name = os.path.basename(file_name)
images.append((base_name, image_data))
return images
except Exception as e:
raise Exception(f"Error extracting images from docx file {file_path}: {str(e)}")
@staticmethod
def parse_docx(file_path: str) -> str:
"""解析DOCX文件保留段落结构"""
# 验证文件路径
if not FileParser.validate_file_path(file_path):
raise ValueError(f"Invalid file path: {file_path}")
@ -230,16 +72,12 @@ class FileParser:
try:
doc = Document(file_path)
# 提取所有段落文本,保留空行以保持格式
# 提取所有段落文本
paragraphs = []
for paragraph in doc.paragraphs:
text = paragraph.text.strip()
if text: # 非空段落
paragraphs.append(paragraph.text)
else: # 空段落,用空行表示
paragraphs.append("")
paragraphs.append(paragraph.text)
# 用换行符连接所有段落,保留空行
# 用换行符连接所有段落
content = '\n'.join(paragraphs)
return content
@ -248,7 +86,7 @@ class FileParser:
@staticmethod
def parse_pdf(file_path: str) -> str:
"""解析PDF文件保留段落结构"""
# 验证文件路径
if not FileParser.validate_file_path(file_path):
raise ValueError(f"Invalid file path: {file_path}")
@ -266,61 +104,17 @@ class FileParser:
pdf_reader = PyPDF2.PdfReader(file)
# 提取每一页的文本
for i, page in enumerate(pdf_reader.pages):
page_text = page.extract_text()
if page_text:
content += page_text
# 在页面之间添加空行分隔
if i < len(pdf_reader.pages) - 1:
content += "\n\n"
for page in pdf_reader.pages:
content += page.extract_text()
content += "\n"
return content
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:
"""验证文件路径是否有效"""
# 检查文件是否存在
if not os.path.exists(file_path):
return False

@ -28,25 +28,7 @@ class InputProcessor(QObject):
return False
# 2. 根据按键类型处理(字符、功能键等)
# 处理退格键
if key == '\b' or key == '\x7f': # 退格键 (\b) 或删除键 (\x7f)
if self.input_buffer: # 只有当输入缓冲区有内容时才处理
# 删除最后一个字符
deleted_char = self.input_buffer[-1]
self.input_buffer = self.input_buffer[:-1]
self.current_position = max(0, self.current_position - 1)
# 发送退格信号,包含被删除的字符
self.text_changed.emit('\b')
# 发送按键信号用于记录
self.key_pressed.emit(f"backspace_deleted:{deleted_char}")
return True
else:
return False # 缓冲区为空,不处理退格
# 3. 更新输入缓冲区(普通字符)
# 3. 更新输入缓冲区
self.input_buffer += key
self.current_position += 1
@ -60,27 +42,6 @@ class InputProcessor(QObject):
# 6. 返回处理结果
return True
def process_text_commit(self, text: str) -> bool:
"""处理输入法提交的完整文本(支持中文输入)"""
if not text:
return False
# 逐个处理提交的字符
for char in text:
self.input_buffer += char
self.current_position += 1
# 发送每个字符的text_changed信号
self.text_changed.emit(char)
# 发送完整的文本提交信号
self.key_pressed.emit(f"text_commit:{text}")
# 检查是否完成输入
if self.expected_text and self.input_buffer == self.expected_text:
self.input_completed.emit()
return True
def get_current_input(self) -> str:
# 实现获取当前输入逻辑

@ -1,723 +0,0 @@
# learning_mode_window.py
import sys
import os
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QLabel, QFrame, QMenuBar,
QAction, QFileDialog, QMessageBox, QApplication,
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 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):
# 定义内容变化信号
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图片
- 下方显示输入字符页面
- 输入字符显示导入的文件内容
参数:
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()
# 初始化打字逻辑
self.init_typing_logic()
# 初始化同步位置跟踪
self.last_sync_position = current_position
# 如果有导入内容,初始化显示
if self.imported_content:
self.initialize_with_imported_content()
def initialize_with_imported_content(self):
"""
使用从主窗口传递的导入内容初始化学习模式窗口
"""
if not self.imported_content:
return
# 重置打字逻辑
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]
self.text_display_widget.text_display.setPlainText(display_text)
# 更新状态
progress = (self.current_position / len(self.imported_content)) * 100
self.status_label.setText(f"继续学习 - 进度: {self.current_position}/{len(self.imported_content)} 字符")
self.progress_label.setText(f"进度: {progress:.1f}%")
# 设置光标位置到文本末尾
cursor = self.text_display_widget.text_display.textCursor()
cursor.movePosition(cursor.End)
self.text_display_widget.text_display.setTextCursor(cursor)
def initUI(self):
"""
初始化学习模式窗口UI
- 设置窗口标题和大小
- 创建顶部图片区域
- 创建下方输入区域
"""
# 设置窗口属性
self.setWindowTitle("学习模式 - MagicWord")
self.setGeometry(200, 200, 900, 700)
self.setWindowFlags(Qt.Window) # 独立窗口
# 创建中央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.create_top_image_area(main_layout)
# 创建分隔线
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
separator.setStyleSheet("background-color: #d0d0d0;")
main_layout.addWidget(separator)
# 创建输入区域
self.create_input_area(main_layout)
# 创建菜单栏
self.create_menu_bar()
# 创建状态栏
self.create_status_bar()
# 如果有导入内容,更新状态栏显示
if self.imported_content:
progress = (self.current_position / len(self.imported_content)) * 100
self.status_label.setText(f"继续学习 - 进度: {self.current_position}/{len(self.imported_content)} 字符")
self.progress_label.setText(f"进度: {progress:.1f}%")
def create_top_image_area(self, main_layout):
"""
创建顶部图片区域
- 加载并显示UI.png图片完全铺满窗口上方
- 窗口大小自动适配图片大小
"""
# 创建图片标签
self.image_label = QLabel()
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setStyleSheet("""
QLabel {
background-color: #f8f9fa;
border: none;
margin: 0px;
padding: 0px;
}
""")
# 加载UI.png图片
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)
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;
color: #666666;
font-size: 14px;
qproperty-alignment: AlignCenter;
}
""")
# 添加到主布局
main_layout.addWidget(self.image_label)
def resizeEvent(self, event):
"""
窗口大小改变事件
- 动态调整图片显示区域
"""
super().resizeEvent(event)
# 获取窗口宽度
window_width = self.width()
# 如果图片标签存在,调整其大小以适配窗口
if hasattr(self, 'image_label') and self.image_label:
# 设置图片标签的固定宽度为窗口宽度
self.image_label.setFixedWidth(window_width)
# 根据窗口宽度计算合适的高度(保持图片比例)
if hasattr(self, 'original_pixmap') and self.original_pixmap:
original_width = self.original_pixmap.width()
original_height = self.original_pixmap.height()
# 计算保持比例的高度
new_height = int(window_width * original_height / original_width)
self.image_label.setFixedHeight(new_height)
else:
# 如果没有原始图片,使用默认高度
self.image_label.setFixedHeight(300)
def create_input_area(self, main_layout):
"""
创建输入区域
- 创建文本显示组件
- 设置与主系统相同的样式
- 创建图片列表区域
"""
# 创建文本显示组件(复用主系统的组件)
self.text_display_widget = TextDisplayWidget(self)
# 设置样式,使其与主系统保持一致
self.text_display_widget.setStyleSheet("""
TextDisplayWidget {
background-color: white;
border: 1px solid #d0d0d0;
border-radius: 0px;
}
""")
# 连接文本变化信号
self.text_display_widget.text_display.textChanged.connect(self.on_text_changed)
# 创建图片显示区域
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):
"""
创建菜单栏
- 文件菜单导入退出
- 帮助菜单关于
"""
menu_bar = self.menuBar()
# 文件菜单
file_menu = menu_bar.addMenu('文件')
# 导入动作
import_action = QAction('导入文件', self)
import_action.setShortcut('Ctrl+O')
import_action.triggered.connect(self.import_file)
file_menu.addAction(import_action)
file_menu.addSeparator()
# 退出动作
exit_action = QAction('退出学习模式', self)
exit_action.setShortcut('Ctrl+Q')
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 帮助菜单
help_menu = menu_bar.addMenu('帮助')
about_action = QAction('关于', self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def create_status_bar(self):
"""
创建状态栏
- 显示当前状态和学习进度
"""
self.status_bar = self.statusBar()
# 创建状态标签
self.status_label = QLabel("就绪 - 请先导入文件开始学习")
self.progress_label = QLabel("进度: 0%")
# 添加到状态栏
self.status_bar.addWidget(self.status_label)
self.status_bar.addPermanentWidget(self.progress_label)
def init_typing_logic(self):
"""
初始化打字逻辑
- 创建打字逻辑实例
- 设置默认内容
"""
# 创建打字逻辑实例
self.typing_logic = TypingLogic("欢迎使用学习模式!\n\n请先导入文件开始打字学习。")
# 设置文本显示组件的打字逻辑
if hasattr(self.text_display_widget, 'set_typing_logic'):
self.text_display_widget.set_typing_logic(self.typing_logic)
def import_file(self):
"""
导入文件
- 打开文件对话框选择文件
- 解析文件内容
- 重置打字逻辑
"""
file_path, _ = QFileDialog.getOpenFileName(
self, "导入学习文件", "",
"文档文件 (*.docx *.txt *.pdf);;所有文件 (*.*)"
)
if file_path:
self.is_loading_file = True
try:
# 获取文件扩展名
_, ext = os.path.splitext(file_path)
ext = ext.lower()
# 对于docx文件直接解析而不转换为txt
if ext == '.docx':
# 直接解析docx文件内容
content = FileParser.parse_docx(file_path)
# 提取图片数据
images = FileParser.extract_images_from_docx(file_path)
else:
# 其他文件类型使用原来的转换方法
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)}")
finally:
self.is_loading_file = False
def on_text_changed(self):
"""
文本变化处理
- 根据导入的内容逐步显示
- 更新学习进度
- 同步内容到打字模式
- 处理图片插入
"""
# 如果正在加载文件,跳过处理
if self.is_loading_file:
return
# 如果没有导入内容,清空文本
if not self.imported_content:
current_text = self.text_display_widget.text_display.toPlainText()
if current_text:
self.text_display_widget.text_display.clear()
self.status_label.setText("请先导入文件开始学习")
return
# 获取当前文本
current_text = self.text_display_widget.text_display.toPlainText()
# 始终确保显示的是导入的文件内容,而不是用户输入的文字
expected_text = self.imported_content[:len(current_text)]
# 如果当前文本与期望文本不一致,强制恢复到期望文本
if current_text != expected_text:
cursor = self.text_display_widget.text_display.textCursor()
self.text_display_widget.text_display.setPlainText(expected_text)
self.text_display_widget.text_display.setTextCursor(cursor)
# 显示错误信息
if len(current_text) > 0:
expected_char = self.imported_content[len(current_text)-1] if len(current_text) <= len(self.imported_content) else ''
self.status_label.setText(f"输入错误!期望字符: '{expected_char}'")
return
# 检查输入是否正确
if self.typing_logic and len(current_text) > 0:
result = self.typing_logic.check_input(current_text)
if not result['correct']:
# 输入错误,恢复到正确的状态
expected_text = self.imported_content[:len(current_text)]
if current_text != expected_text:
cursor = self.text_display_widget.text_display.textCursor()
self.text_display_widget.text_display.setPlainText(expected_text)
self.text_display_widget.text_display.setTextCursor(cursor)
# 显示错误信息
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
self.progress_label.setText(
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("恭喜!学习完成!")
QMessageBox.information(self, "学习完成", "恭喜您完成了所有学习内容!")
else:
self.status_label.setText("继续输入以显示更多内容...")
def show_about(self):
"""
显示关于对话框
"""
QMessageBox.about(self, "关于学习模式",
"MagicWord 学习模式\n\n"
"功能特点:\n"
"• 顶部显示UI界面图片\n"
"• 下方为打字输入区域\n"
"• 导入文件后逐步显示内容\n"
"• 实时显示学习进度\n"
"• 支持图片显示\n\n"
"使用方法:\n"
"1. 点击'文件'->'导入文件'选择学习材料\n"
"2. 在下方文本区域开始打字\n"
"3. 系统会根据您的输入逐步显示内容")
def closeEvent(self, event):
"""
窗口关闭事件
- 通知父窗口学习模式已关闭
"""
# 发射关闭信号
self.closed.emit()
if self.parent_window and hasattr(self.parent_window, 'on_learning_mode_closed'):
self.parent_window.on_learning_mode_closed()
event.accept()
def keyPressEvent(self, event):
"""
按键事件处理
- 处理特殊按键
"""
# 处理Escape键关闭窗口
if event.key() == Qt.Key_Escape:
self.close()
else:
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()

@ -1,153 +0,0 @@
# 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,9 +1,482 @@
def on_learning_mode_closed(self):
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):
"""
学习模式窗口关闭回调
- 清除学习窗口引用
- 更新菜单状态
初始化主窗口
- 设置窗口标题为"隐私学习软件 - 仿Word"
- 设置窗口大小为800x600
- 初始化学习内容存储变量
- 初始化当前输入位置
- 调用initUI()方法
"""
self.learning_window = None
self.learning_mode_action.setChecked(False)
self.typing_mode_action.setChecked(True)
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
- 如果用户选择了文件调用FileParser.parse_file(file_path)
- 成功时将内容存储但不直接显示重置打字状态
- 失败时显示错误消息框
"""
options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName(
self,
"打开文件",
"",
"文本文件 (*.txt);;Word文档 (*.docx);;所有文件 (*)",
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)

@ -2,7 +2,6 @@
import requests
import json
import os
import time
from typing import Optional, Dict, Any
class NetworkService:
@ -12,354 +11,84 @@ 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_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
def get_weather_info(self) -> Optional[Dict[str, Any]]:
# 实现天气信息获取逻辑
# 如果指定了城市名直接使用该城市否则通过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()
# 1. 获取用户IP地址
try:
ip_response = self.session.get("https://httpbin.org/ip", timeout=5)
ip_data = ip_response.json()
ip = ip_data.get("origin", "")
# 2. 根据IP获取地理位置
# 注意这里使用免费的IP地理位置API实际应用中可能需要更精确的服务
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_response = self.session.get(f"http://ip-api.com/json/{ip}", timeout=5)
location_data = location_response.json()
print(f"地理位置响应: {location_data}")
if location_data.get("status") != "success":
print("地理位置获取失败,使用默认天气数据")
return self.get_default_weather()
return None
city = location_data.get("city", "Unknown")
print(f"获取到的城市: {city}")
if not city:
print("无法获取城市名称,使用默认天气数据")
return self.get_default_weather()
# 保存定位信息到缓存
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}")
# 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)
weather_data = weather_response.json()
print(f"天气数据响应: {weather_data}")
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("穿衣建议: 天气舒适")
# 4. 解析并格式化数据
if weather_response.status_code == 200:
formatted_weather = {
"city": city,
"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 # 生活提示列表
"temperature": weather_data["main"]["temp"],
"description": weather_data["weather"][0]["description"],
"humidity": weather_data["main"]["humidity"],
"wind_speed": weather_data["wind"]["speed"]
}
print(f"成功获取天气数据: {formatted_weather}")
# 缓存天气数据
self.set_weather_cache(formatted_weather, self._cached_location)
# 5. 返回天气信息字典
return formatted_weather
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
else:
# 模拟天气数据无API密钥时
return {
"city": city,
"temperature": 20,
"description": "晴天",
"humidity": 60,
"wind_speed": 3.5
}
except Exception as e:
print(f"获取天气信息时出错: {e}")
return None
def get_daily_quote(self) -> Optional[str]:
# 实现每日一句获取逻辑 - 使用古诗词API
# 实现每日一句获取逻辑
# 1. 调用名言API
try:
# 使用古诗词·一言API - 每次返回随机不同的诗词
response = self.session.get("https://v1.jinrishici.com/all.json", timeout=5, verify=False)
# 使用一个免费的名言API
response = self.session.get("https://api.quotable.io/random", timeout=5)
# 2. 解析返回的古诗词数据
# 2. 解析返回的名言数据
if response.status_code == 200:
poetry_data = response.json()
content = poetry_data.get('content', '')
author = poetry_data.get('author', '')
title = poetry_data.get('origin', '')
quote_data = response.json()
content = quote_data.get("content", "")
author = quote_data.get("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 = "暂无古诗词"
# 3. 格式化名言文本
formatted_quote = f'"{content}" - {author}'
# 4. 返回古诗词字符串
return formatted_poetry
# 4. 返回名言字符串
return formatted_quote
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]:

@ -11,15 +11,11 @@ class TypingLogic:
self.error_count = 0
self.total_chars = len(learning_content)
self.typed_chars = 0
self.image_positions = [] # 存储图片位置信息
self.image_data = {} # 存储图片数据 {图片名称: 二进制数据}
self.image_display_queue = [] # 待显示的图片队列
def check_input(self, user_text: str) -> dict:
"""
检查用户输入与学习材料的匹配情况
- 逐字符比较逻辑
- 支持中文整词匹配
- 进度跟踪
- 准确率计算
- 返回字典包含:
@ -43,31 +39,13 @@ class TypingLogic:
user_text = user_text[:self.total_chars]
current_position = len(user_text)
# 检查当前输入是否正确(支持中文整词匹配)
# 检查当前输入是否正确
correct = True
expected_char = ''
if self.current_index < self.total_chars:
expected_char = self.learning_content[self.current_index]
# 中文整词匹配优化
# 如果当前位置是中文文本的开始,尝试进行整词匹配
if self._is_chinese_text_start(self.current_index):
# 获取期望的中文词组
expected_word = self._get_chinese_word_at(self.current_index)
# 获取用户输入的对应部分
user_word = user_text[self.current_index:min(self.current_index + len(expected_word), len(user_text))]
# 如果用户输入的词组与期望词组匹配,则认为是正确的
if user_word == expected_word:
correct = True
else:
# 如果整词不匹配,回退到逐字符匹配
if len(user_text) > self.current_index and user_text[self.current_index] != expected_char:
correct = False
else:
# 非中文词组开始位置,使用逐字符匹配
if len(user_text) > self.current_index and user_text[self.current_index] != expected_char:
correct = False
if len(user_text) > self.current_index and user_text[self.current_index] != expected_char:
correct = False
else:
# 已经完成所有输入
# 恢复原始的typed_chars值用于准确率计算
@ -97,35 +75,21 @@ class TypingLogic:
"accuracy": accuracy
}
def update_position(self, user_text: str) -> dict:
def update_position(self, user_text: str):
"""
更新当前位置并计算错误数
- 支持中文整词匹配
- 逐字符比较逻辑回退机制
- 错误计数
- 位置更新
- 返回字典包含:
* new_position: 整数新的位置
* error_count: 整数错误数
* completed: 布尔值是否完成
更新当前索引和错误计数
- 根据用户输入更新当前位置
- 计算并更新错误计数
"""
# 更新当前索引位置
self.current_index = len(user_text)
# 重置错误计数
self.error_count = 0
# 使用中文整词匹配优化错误计算
self._calculate_errors_with_chinese_support(user_text)
new_position = len(user_text)
# 检查是否完成
completed = self.current_index >= self.total_chars
# 计算新增的错误数
for i in range(self.current_index, min(new_position, self.total_chars)):
if user_text[i] != self.learning_content[i]:
self.error_count += 1
return {
"new_position": self.current_index,
"error_count": self.error_count,
"completed": completed
}
# 更新当前索引
self.current_index = new_position
def get_expected_text(self, length: int = 10) -> str:
"""
@ -170,9 +134,6 @@ class TypingLogic:
self.current_index = 0
self.error_count = 0
self.typed_chars = 0
self.image_positions = [] # 重置图片位置信息
self.image_data = {} # 重置图片数据
self.image_display_queue = [] # 重置图片显示队列
def get_statistics(self) -> dict:
"""
@ -189,64 +150,6 @@ class TypingLogic:
"accuracy_rate": self._calculate_accuracy()
}
def set_image_positions(self, image_positions: list):
"""
设置图片位置信息
- image_positions: 列表包含图片位置信息
"""
self.image_positions = image_positions
def get_current_image_info(self, position: int) -> dict:
"""
获取当前位置的图片信息
- position: 整数当前输入位置
- 返回字典包含图片信息如果没有图片返回None
"""
for img_info in self.image_positions:
if img_info['start_pos'] <= position <= img_info['end_pos']:
return img_info
return None
def set_image_data(self, image_data: dict):
"""
设置图片数据
- image_data: 字典{图片名称: 二进制数据}
"""
self.image_data = image_data
def get_images_to_display(self, current_position: int) -> list:
"""
获取在当前位置需要显示的图片
- current_position: 整数当前输入位置
- 返回图片信息列表
"""
images_to_display = []
for img_info in self.image_positions:
if img_info['start_pos'] <= current_position <= img_info['end_pos']:
# 尝试获取图片名称(支持多种键名)
image_name = img_info.get('image_name', '') or img_info.get('filename', '')
if image_name in self.image_data:
img_info_copy = img_info.copy()
img_info_copy['image_data'] = self.image_data[image_name]
images_to_display.append(img_info_copy)
return images_to_display
def should_show_image(self, current_position: int) -> bool:
"""
检查在当前位置是否应该显示图片
- current_position: 整数当前输入位置
- 返回布尔值
"""
return len(self.get_images_to_display(current_position)) > 0
def check_image_at_position(self, position: int) -> bool:
"""
检查指定位置是否有图片
- position: 整数位置索引
- 返回布尔值该位置是否有图片
"""
return self.get_current_image_info(position) is not None
def _calculate_accuracy(self) -> float:
"""
计算准确率
@ -269,85 +172,4 @@ class TypingLogic:
return 0.0
finally:
# 清除递归保护标志
self._calculating_accuracy = False
def _is_chinese_text_start(self, index: int) -> bool:
"""
判断指定位置是否是中文文本的开始
- 检查当前字符是否为中文
- 检查前一个字符是否为非中文或空格
"""
if index < 0 or index >= len(self.learning_content):
return False
current_char = self.learning_content[index]
# 检查当前字符是否为中文
if not self._is_chinese_char(current_char):
return False
# 如果是第一个字符,或者是紧跟在非中文字符后的中文,则认为是中文文本的开始
if index == 0:
return True
prev_char = self.learning_content[index - 1]
return not self._is_chinese_char(prev_char) or prev_char.isspace()
def _is_chinese_char(self, char: str) -> bool:
"""
判断字符是否为中文
- 使用Unicode范围判断中文字符
"""
return '\u4e00' <= char <= '\u9fff' or '\u3400' <= char <= '\u4dbf'
def _get_chinese_word_at(self, index: int) -> str:
"""
获取指定位置开始的中文词组
- 从当前位置开始连续获取中文字符
- 遇到非中文字符或字符串结束则停止
"""
if index < 0 or index >= len(self.learning_content):
return ""
word = ""
current_index = index
while current_index < len(self.learning_content):
char = self.learning_content[current_index]
if self._is_chinese_char(char):
word += char
current_index += 1
else:
break
return word
def _calculate_errors_with_chinese_support(self, user_text: str) -> None:
"""
使用中文整词匹配优化错误计算
- 优先尝试中文整词匹配
- 整词匹配失败时回退到逐字符匹配
"""
i = 0
while i < min(len(user_text), len(self.learning_content)):
# 检查当前位置是否为中文文本开始
if self._is_chinese_text_start(i):
# 获取期望的中文词组
expected_word = self._get_chinese_word_at(i)
# 获取用户输入的对应部分
user_word = user_text[i:min(i + len(expected_word), len(user_text))]
# 如果整词匹配成功,跳过整个词组
if user_word == expected_word:
i += len(expected_word)
continue
else:
# 整词匹配失败,回退到逐字符匹配
if user_text[i] != self.learning_content[i]:
self.error_count += 1
else:
# 非中文文本,使用逐字符匹配
if user_text[i] != self.learning_content[i]:
self.error_count += 1
i += 1
self._calculating_accuracy = False

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

@ -1,535 +0,0 @@
# 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()
)

@ -1,508 +0,0 @@
# -*- 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()

@ -1,676 +0,0 @@
# -*- 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_())

@ -1,501 +0,0 @@
# ui/components.py
'''
from PyQt5.QtWidgets import QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout
from PyQt5.QtCore import Qt
class CustomTitleBar(QWidget):
def __init__(self, parent=None):
"""
自定义标题栏
- 创建标题栏UI元素
- 添加窗口控制按钮
"""
super().__init__(parent)
self.parent = parent
self.setup_ui()
def setup_ui(self):
"""
设置标题栏UI
- 初始化所有UI组件
- 设置组件属性和样式
"""
# 创建水平布局
layout = QHBoxLayout()
layout.setContentsMargins(10, 5, 10, 5)
layout.setSpacing(10)
# 创建标题标签
self.title_label = QLabel("MagicWord")
self.title_label.setStyleSheet("color: #333333; font-size: 12px; font-weight: normal;")
# 创建控制按钮
self.minimize_button = QPushButton("")
self.maximize_button = QPushButton("")
self.close_button = QPushButton("×")
# 设置按钮样式
button_style = """
QPushButton {
background-color: transparent;
border: none;
color: #333333;
font-size: 12px;
font-weight: normal;
width: 30px;
height: 30px;
}
QPushButton:hover {
background-color: #d0d0d0;
}
"""
self.minimize_button.setStyleSheet(button_style)
self.maximize_button.setStyleSheet(button_style)
self.close_button.setStyleSheet(button_style + "QPushButton:hover { background-color: #ff5555; color: white; }")
# 添加组件到布局
layout.addWidget(self.title_label)
layout.addStretch()
layout.addWidget(self.minimize_button)
layout.addWidget(self.maximize_button)
layout.addWidget(self.close_button)
self.setLayout(layout)
# 连接按钮事件
self.minimize_button.clicked.connect(self.minimize_window)
self.maximize_button.clicked.connect(self.maximize_window)
self.close_button.clicked.connect(self.close_window)
# 设置标题栏样式
self.setStyleSheet("""
CustomTitleBar {
background-color: #f0f0f0;
border-top-left-radius: 0px;
border-top-right-radius: 0px;
border-bottom: 1px solid #d0d0d0;
}
""")
def minimize_window(self):
"""
最小化窗口
- 触发窗口最小化事件
"""
if self.parent:
self.parent.showMinimized()
def maximize_window(self):
"""
最大化窗口
- 切换窗口最大化状态
"""
if self.parent:
if self.parent.isMaximized():
self.parent.showNormal()
self.maximize_button.setText("")
else:
self.parent.showMaximized()
self.maximize_button.setText("")
def close_window(self):
"""
关闭窗口
- 触发窗口关闭事件
"""
if self.parent:
self.parent.close()
class ProgressBarWidget(QWidget):
def __init__(self, parent=None):
"""
进度条组件
- 显示打字练习进度
- 显示统计信息
"""
super().__init__(parent)
self.setup_ui()
def setup_ui(self):
"""
设置进度条UI
- 初始化所有UI组件
- 设置组件属性和样式
"""
# 创建垂直布局
layout = QVBoxLayout()
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5)
# 导入需要的模块
from PyQt5.QtWidgets import QProgressBar, QHBoxLayout
# 创建水平布局用于统计信息
stats_layout = QHBoxLayout()
stats_layout.setSpacing(15)
# 创建统计信息标签
self.wpm_label = QLabel("WPM: 0")
self.accuracy_label = QLabel("准确率: 0%")
self.time_label = QLabel("用时: 0s")
# 设置标签样式
label_style = "font-size: 12px; font-weight: normal; color: #333333;"
self.wpm_label.setStyleSheet(label_style)
self.accuracy_label.setStyleSheet(label_style)
self.time_label.setStyleSheet(label_style)
# 添加标签到统计布局
stats_layout.addWidget(self.wpm_label)
stats_layout.addWidget(self.accuracy_label)
stats_layout.addWidget(self.time_label)
stats_layout.addStretch()
# 创建进度条
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setTextVisible(True)
self.progress_bar.setFormat("进度: %p%")
# 设置进度条样式
self.progress_bar.setStyleSheet("""
QProgressBar {
border: 1px solid #c0c0c0;
border-radius: 0px;
text-align: center;
background-color: #f0f0f0;
}
QProgressBar::chunk {
background-color: #0078d7;
border-radius: 0px;
}
""")
# 添加组件到主布局
layout.addLayout(stats_layout)
layout.addWidget(self.progress_bar)
self.setLayout(layout)
def update_progress(self, progress: float):
"""
更新进度条
- 设置进度值
- 更新显示
"""
self.progress_bar.setValue(int(progress))
def update_stats(self, wpm: int, accuracy: float, time_elapsed: int):
"""
更新统计信息
- wpm: 每分钟字数
- accuracy: 准确率(%)
- time_elapsed: 用时()
"""
self.wpm_label.setText(f"WPM: {wpm}")
self.accuracy_label.setText(f"准确率: {accuracy:.1f}%")
self.time_label.setText(f"用时: {time_elapsed}s")
class StatsDisplayWidget(QWidget):
def __init__(self, parent=None):
"""
统计信息显示组件
- 显示准确率WPM等统计信息
"""
super().__init__(parent)
self.setup_ui()
def setup_ui(self):
"""
设置统计信息显示UI
- 初始化所有UI组件
- 设置组件属性和样式
"""
# 创建水平布局
layout = QHBoxLayout()
layout.setContentsMargins(10, 5, 10, 5)
layout.setSpacing(15)
# 创建统计信息标签
self.wpm_label = QLabel("WPM: 0")
self.accuracy_label = QLabel("准确率: 0%")
# 设置标签样式
label_style = "font-size: 12px; font-weight: normal; color: #333333;"
self.wpm_label.setStyleSheet(label_style)
self.accuracy_label.setStyleSheet(label_style)
# 添加组件到布局
layout.addWidget(self.wpm_label)
layout.addWidget(self.accuracy_label)
layout.addStretch()
self.setLayout(layout)
# 设置样式
self.setStyleSheet("""
StatsDisplayWidget {
background-color: #f0f0f0;
border-bottom: 1px solid #d0d0d0;
}
""")
def update_stats(self, wpm: int, accuracy: float):
"""
更新统计信息
- wpm: 每分钟字数
- accuracy: 准确率(%)
"""
self.wpm_label.setText(f"WPM: {wpm}")
self.accuracy_label.setText(f"准确率: {accuracy:.1f}%")
class QuoteDisplayWidget(QWidget):
def __init__(self, parent=None):
"""
每日一言显示组件
- 显示每日一言功能
"""
super().__init__(parent)
self.setup_ui()
def setup_ui(self):
"""
设置每日一言显示UI
- 初始化所有UI组件
- 设置组件属性和样式
"""
# 创建水平布局
layout = QHBoxLayout()
layout.setContentsMargins(10, 5, 10, 5)
layout.setSpacing(15)
# 创建每日一言标签
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.quote_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;
}
""")
# 添加组件到布局
layout.addWidget(self.quote_label)
layout.addStretch()
layout.addWidget(self.refresh_quote_button)
self.setLayout(layout)
# 设置样式
self.setStyleSheet("""
QuoteDisplayWidget {
background-color: #f0f0f0;
border-bottom: 1px solid #d0d0d0;
}
""")
def update_quote(self, quote: str):
"""
更新每日一言
- quote: 每日一言内容
"""
self.quote_label.setText(f"每日一言: {quote}")
class WeatherDisplayWidget(QWidget):
def __init__(self, parent=None):
"""
天气显示组件
- 显示天气信息
"""
super().__init__(parent)
self.setup_ui()
def setup_ui(self):
"""
设置天气显示UI
- 初始化所有UI组件
- 设置组件属性和样式
"""
# 创建水平布局
layout = QHBoxLayout()
layout.setContentsMargins(10, 5, 10, 5)
layout.setSpacing(15)
# 创建天气信息标签
self.weather_label = QLabel("天气: 暂无")
# 设置标签样式
label_style = "font-size: 12px; font-weight: normal; color: #333333;"
self.weather_label.setStyleSheet(label_style)
# 创建天气刷新按钮
self.refresh_weather_button = QPushButton("刷新")
self.refresh_weather_button.setStyleSheet("""
QPushButton {
background-color: #0078d7;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
font-size: 12px;
}
QPushButton:hover {
background-color: #005a9e;
}
""")
# 添加组件到布局
layout.addWidget(self.weather_label)
layout.addStretch()
layout.addWidget(self.refresh_weather_button)
self.setLayout(layout)
# 设置样式
self.setStyleSheet("""
WeatherDisplayWidget {
background-color: #f0f0f0;
border-bottom: 1px solid #d0d0d0;
}
""")
def update_weather(self, weather_info: dict):
"""
更新天气信息
- weather_info: 天气信息字典
"""
if weather_info:
city = weather_info.get("city", "未知")
temperature = weather_info.get("temperature", "N/A")
description = weather_info.get("description", "N/A")
self.weather_label.setText(f"天气: {city} {temperature}°C {description}")
else:
self.weather_label.setText("天气: 获取失败")
class TextDisplayWidget(QWidget):
def __init__(self, parent=None):
"""
文本显示组件
- 显示待练习文本
- 高亮当前字符
- 显示用户输入反馈
"""
super().__init__(parent)
self.text_content = ""
self.current_index = 0
self.setup_ui()
def setup_ui(self):
"""
设置文本显示UI
- 初始化文本显示区域
- 设置样式和布局
"""
# 创建垂直布局
layout = QVBoxLayout()
layout.setContentsMargins(20, 20, 20, 20)
# 导入需要的模块
from PyQt5.QtWidgets import QTextEdit
from PyQt5.QtCore import Qt
# 创建文本显示区域
self.text_display = QTextEdit()
self.text_display.setReadOnly(False) # 设置为可编辑
self.text_display.setLineWrapMode(QTextEdit.WidgetWidth)
# 设置文本显示样式
self.text_display.setStyleSheet("""
QTextEdit {
font-family: 'Calibri', 'Segoe UI', 'Microsoft YaHei', sans-serif;
font-size: 12pt;
border: 1px solid #d0d0d0;
border-radius: 0px;
padding: 15px;
background-color: white;
color: black;
}
""")
# 添加组件到布局
layout.addWidget(self.text_display)
self.setLayout(layout)
def set_text(self, text: str):
"""
设置显示文本
- text: 要显示的文本内容
"""
self.text_content = text
self.current_index = 0
# 初始不显示内容,通过打字逐步显示
self.text_display.setHtml("")
def highlight_character(self, position: int):
"""
高亮指定位置的字符
- position: 字符位置索引
"""
if 0 <= position < len(self.text_content):
self.current_index = position
# 不再直接高亮字符,而是通过用户输入来显示内容
pass
def _update_display(self, user_input: str = ""):
"""
更新文本显示
- user_input: 用户输入文本(可选)
"""
# 导入需要的模块
from PyQt5.QtGui import QTextCursor
if not self.text_content:
self.text_display.clear()
return
# 简单显示文本,不使用任何高亮
if user_input:
# 只显示用户已输入的部分文本
displayed_text = self.text_content[:len(user_input)]
else:
# 没有用户输入,不显示任何内容
displayed_text = ""
# 更新文本显示
self.text_display.setPlainText(displayed_text)
# 安全地滚动到光标位置
if user_input and displayed_text:
try:
cursor = self.text_display.textCursor()
# 将光标定位到文本末尾
cursor.setPosition(len(displayed_text))
self.text_display.setTextCursor(cursor)
self.text_display.ensureCursorVisible()
except Exception:
# 如果光标定位失败,忽略错误
pass
def show_user_input(self, input_text: str):
"""
显示用户输入的文本
- input_text: 用户输入的文本
"""
self._update_display(input_text)
'''

@ -1,5 +1,5 @@
# ui/components.py
from PyQt5.QtWidgets import QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QTextEdit, QProgressBar
from PyQt5.QtWidgets import QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout
from PyQt5.QtCore import Qt
class CustomTitleBar(QWidget):
@ -127,6 +127,9 @@ class ProgressBarWidget(QWidget):
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5)
# 导入需要的模块
from PyQt5.QtWidgets import QProgressBar, QHBoxLayout
# 创建水平布局用于统计信息
stats_layout = QHBoxLayout()
stats_layout.setSpacing(15)
@ -407,6 +410,10 @@ class TextDisplayWidget(QWidget):
layout = QVBoxLayout()
layout.setContentsMargins(20, 20, 20, 20)
# 导入需要的模块
from PyQt5.QtWidgets import QTextEdit
from PyQt5.QtCore import Qt
# 创建文本显示区域
self.text_display = QTextEdit()
self.text_display.setReadOnly(False) # 设置为可编辑

@ -1,438 +0,0 @@
# 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)

@ -1,439 +0,0 @@
#!/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()

@ -1,437 +0,0 @@
# 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)

@ -1,844 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
主题管理器 - 负责检测系统主题并管理应用主题切换
"""
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QPalette, QColor
from PyQt5.QtCore import QObject, pyqtSignal
import platform
import subprocess
import os
class ThemeManager(QObject):
"""主题管理器类"""
# 主题切换信号
theme_changed = pyqtSignal(bool) # True表示深色模式False表示浅色模式
def __init__(self):
super().__init__()
self._is_dark_theme = False
self._auto_detect = True
self._detection_timer = None
def is_dark_theme(self):
"""检测是否为深色主题"""
if self._auto_detect:
return self._detect_system_theme()
return self._is_dark_theme
def _detect_system_theme(self):
"""检测系统主题"""
try:
# macOS系统主题检测
if platform.system() == 'Darwin':
return self._detect_macos_theme()
# Windows系统主题检测
elif platform.system() == 'Windows':
return self._detect_windows_theme()
# Linux系统主题检测
else:
return self._detect_linux_theme()
except Exception as e:
print(f"主题检测失败: {e}")
return False
def _detect_macos_theme(self):
"""检测macOS系统主题"""
try:
# 使用osascript命令检测macOS主题
result = subprocess.run([
'osascript', '-e',
'tell application "System Events" to tell appearance preferences to return dark mode'
], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
return 'true' in result.stdout.lower()
# 备用方法:检测系统设置
result = subprocess.run([
'defaults', 'read', '-g', 'AppleInterfaceStyle'
], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
return 'dark' in result.stdout.lower()
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
pass
# 使用Qt的调色板检测作为备用方法
return self._detect_by_palette()
def _detect_windows_theme(self):
"""检测Windows系统主题"""
try:
# Windows 10/11 注册表检测
import winreg
key_path = r'SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize'
value_name = 'AppsUseLightTheme'
try:
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as key:
value, _ = winreg.QueryValueEx(key, value_name)
return value == 0 # 0表示深色模式
except (OSError, FileNotFoundError):
pass
except ImportError:
pass
# 使用Qt的调色板检测作为备用方法
return self._detect_by_palette()
def _detect_linux_theme(self):
"""检测Linux系统主题"""
try:
# 检测GTK主题
result = subprocess.run([
'gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'
], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
theme_name = result.stdout.strip().strip("'")
return any(dark_name in theme_name.lower() for dark_name in ['dark', 'night'])
# 检测颜色方案
result = subprocess.run([
'gsettings', 'get', 'org.gnome.desktop.interface', 'color-scheme'
], capture_output=True, text=True, timeout=5)
if result.returncode == 0:
return 'dark' in result.stdout.lower()
except (subprocess.TimeoutExpired, subprocess.SubprocessError, FileNotFoundError):
pass
# 使用Qt的调色板检测作为备用方法
return self._detect_by_palette()
def _detect_by_palette(self):
"""使用Qt调色板检测主题"""
app = QApplication.instance()
if app is None:
return False
palette = app.palette()
# 检测背景色和文本色的亮度
window_color = palette.color(QPalette.Window)
text_color = palette.color(QPalette.WindowText)
# 计算亮度
def get_luminance(color):
return (0.299 * color.red() + 0.587 * color.green() + 0.114 * color.blue()) / 255
window_luminance = get_luminance(window_color)
text_luminance = get_luminance(text_color)
# 如果背景比文本暗,则为深色主题
return window_luminance < text_luminance
def get_theme_stylesheet(self, is_dark=None):
"""获取主题样式表"""
if is_dark is None:
is_dark = self.is_dark_theme()
if is_dark:
return self._get_dark_stylesheet()
else:
return self._get_light_stylesheet()
def _get_dark_stylesheet(self):
"""深色主题样式表 - 优化版Apple设计风格"""
return """
/* 优化版Apple设计风格深色主题样式 */
/* 全局文字颜色和字体 - 使用Apple系统字体 */
QWidget {
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: #1c1c1e;
}
/* 菜单栏 - 优化版Apple深色风格 */
QMenuBar {
background-color: #2c2c2e;
border: none;
border-bottom: 1px solid #3a3a3c;
font-size: 13px;
color: #e8e8ed;
padding: 4px 0;
}
QMenuBar::item {
background-color: transparent;
padding: 6px 12px;
color: #e8e8ed;
border-radius: 6px;
margin: 0 2px;
}
QMenuBar::item:selected {
background-color: #3a3a3c;
color: #e8e8ed;
}
QMenuBar::item:pressed {
background-color: #4a4a4c;
color: #e8e8ed;
}
/* 菜单 - 优化版Apple深色风格 */
QMenu {
background-color: #2c2c2e;
border: 1px solid #3a3a3c;
border-radius: 8px;
font-size: 13px;
color: #e8e8ed;
padding: 4px 0;
margin: 2px;
}
QMenu::item {
color: #e8e8ed;
background-color: transparent;
border-radius: 6px;
margin: 0 4px;
padding: 6px 20px;
}
QMenu::item:selected {
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: #1c1c1e;
border: none;
}
/* 组框 - 优化标题颜色 */
QGroupBox {
font-size: 12px;
font-weight: normal;
color: #e8e8ed;
background-color: #1c1c1e;
border: none;
border-radius: 8px;
margin-top: 5px;
padding-top: 5px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #8a8a8d;
}
/* 工具按钮 - 优化版Apple深色风格 */
QToolButton {
border: 1px solid transparent;
border-radius: 6px;
background-color: #2c2c2e;
font-size: 13px;
color: #e8e8ed;
padding: 6px 12px;
}
QToolButton:hover {
background-color: #3a3a3c;
border: 1px solid #4a4a4c;
}
QToolButton:pressed {
background-color: #4a4a4c;
border: 1px solid #5a5a5c;
}
QToolButton:checked {
background-color: #0a84ff;
border: 1px solid #0a84ff;
color: #ffffff;
}
/* 切换按钮 - 优化样式 */
QToolButton[checkable="true"] {
border: 1px solid #3a3a3c;
border-radius: 6px;
background-color: #2c2c2e;
font-size: 12px;
color: #e8e8ed;
padding: 6px 12px;
}
QToolButton[checkable="true"]:hover {
background-color: #3a3a3c;
}
QToolButton[checkable="true"]:checked {
background-color: #0a84ff;
border: 1px solid #0a84ff;
color: #ffffff;
}
/* 下拉框 - 优化版Apple深色风格 */
QComboBox {
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: #3a3a3c;
border: 1px solid #4a4a4c;
}
QComboBox::drop-down {
border: none;
width: 20px;
border-left: 1px solid #3a3a3c;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
QComboBox::down-arrow {
image: none;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #8a8a8d;
margin: 6px;
}
QComboBox QAbstractItemView {
background-color: #1c1c1e;
border: 1px solid #3a3a3c;
color: #e8e8ed;
selection-background-color: #0a84ff;
selection-color: #ffffff;
}
/* 文本编辑区域 - 优化版Apple深色风格 */
QTextEdit {
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: #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: #e8e8ed;
background-color: transparent;
}
/* 消息框 - Apple深色风格 */
QMessageBox {
background-color: #2c2c2e;
color: #f0f0f0;
border-radius: 12px;
}
QMessageBox QPushButton {
background-color: #3a3a3c;
color: #f0f0f0;
border: 1px solid #4a4a4c;
border-radius: 6px;
padding: 6px 16px;
min-width: 80px;
}
QMessageBox QPushButton:hover {
background-color: #4a4a4c;
border: 1px solid #5a5a5c;
}
QMessageBox QPushButton:pressed {
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: transparent;
width: 8px;
border: none;
}
QScrollBar::handle:vertical {
background-color: #5a5a5c;
border-radius: 4px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
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: #ffffff;
}
/* 菜单栏 - Apple风格 */
QMenuBar {
background-color: #ffffff;
border: none;
border-bottom: 1px solid #e0e0e0;
font-size: 13px;
color: #333333;
padding: 4px 0;
}
QMenuBar::item {
background-color: transparent;
padding: 6px 12px;
color: #333333;
border-radius: 4px;
margin: 0 1px;
}
QMenuBar::item:selected {
background-color: #f0f0f0;
color: #333333;
}
QMenuBar::item:pressed {
background-color: #e0e0e0;
color: #333333;
}
/* 菜单 - Apple风格 */
QMenu {
background-color: #ffffff;
border: 1px solid #d0d0d0;
border-radius: 8px;
font-size: 13px;
color: #333333;
padding: 4px 0;
margin: 2px;
}
QMenu::item {
color: #333333;
background-color: transparent;
border-radius: 4px;
margin: 0 4px;
padding: 4px 20px;
}
QMenu::item:selected {
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: none;
}
/* 组框 */
QGroupBox {
font-size: 12px;
font-weight: normal;
color: #333333;
background-color: #ffffff;
border: none;
border-radius: 8px;
margin-top: 5px;
padding-top: 5px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: #666666;
}
/* 工具按钮 - Apple风格 */
QToolButton {
border: 1px solid transparent;
border-radius: 6px;
background-color: #f6f6f6;
font-size: 13px;
color: #333333;
padding: 6px 12px;
}
QToolButton:hover {
background-color: #e0e0e0;
border: 1px solid #d0d0d0;
}
QToolButton:pressed {
background-color: #d0d0d0;
border: 1px solid #c0c0c0;
}
QToolButton:checked {
background-color: #007aff;
border: 1px solid #007aff;
color: #ffffff;
}
/* 切换按钮 */
QToolButton[checkable="true"] {
border: 1px solid #d0d0d0;
border-radius: 6px;
background-color: #f6f6f6;
font-size: 12px;
color: #333333;
padding: 6px 12px;
}
QToolButton[checkable="true"]:hover {
background-color: #e0e0e0;
}
QToolButton[checkable="true"]:checked {
background-color: #007aff;
border: 1px solid #007aff;
color: #ffffff;
}
/* 下拉框 - Apple风格 */
QComboBox {
background-color: #f6f6f6;
border: 1px solid #d0d0d0;
border-radius: 6px;
color: #333333;
padding: 4px 8px;
selection-background-color: #007aff;
selection-color: #ffffff;
}
QComboBox:hover {
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
QComboBox::drop-down {
border: none;
width: 20px;
border-left: 1px solid #d0d0d0;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
QComboBox::down-arrow {
image: none;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 6px solid #666666;
margin: 6px;
}
QComboBox QAbstractItemView {
border: 1px solid #d0d0d0;
color: #333333;
background-color: #ffffff;
selection-background-color: #007aff;
selection-color: #ffffff;
}
/* 文本编辑区域 - Apple风格 */
QTextEdit {
background-color: #ffffff;
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: #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;
}
/* 标签 */
QLabel {
color: #333333;
background-color: transparent;
}
/* 消息框 - Apple风格 */
QMessageBox {
background-color: #ffffff;
color: #333333;
border-radius: 12px;
}
QMessageBox QPushButton {
background-color: #f6f6f6;
color: #333333;
border: 1px solid #d0d0d0;
border-radius: 6px;
padding: 6px 16px;
min-width: 80px;
}
QMessageBox QPushButton:hover {
background-color: #e0e0e0;
border: 1px solid #c0c0c0;
}
QMessageBox QPushButton:pressed {
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: transparent;
width: 8px;
border: none;
}
QScrollBar::handle:vertical {
background-color: #c0c0c0;
border-radius: 4px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #a0a0a0;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
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):
"""设置深色主题"""
self._is_dark_theme = is_dark
self._auto_detect = False
self.theme_changed.emit(is_dark)
def enable_auto_detection(self, enabled=True):
"""启用/禁用自动主题检测"""
self._auto_detect = enabled
if enabled:
self.theme_changed.emit(self.is_dark_theme())
def get_current_theme_colors(self):
"""获取当前主题颜色配置"""
is_dark = self.is_dark_theme()
if is_dark:
return {
'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_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_pressed': '#004a99'
}
# 全局主题管理器实例
theme_manager = ThemeManager()

@ -1,577 +0,0 @@
# -*- 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

@ -1,29 +0,0 @@
#!/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

@ -1,169 +0,0 @@
#!/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.

@ -1,269 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MagicWord 0.2.1 版本测试构建脚本
简化版本用于测试功能完整性
"""
import os
import sys
import subprocess
from datetime import datetime
def test_imports():
"""测试所有依赖是否可以正常导入"""
print("测试依赖导入...")
required_modules = [
'PyQt5',
'requests',
'bs4',
'docx',
'PyPDF2',
'ebooklib',
'PIL',
'chardet'
]
failed_modules = []
for module in required_modules:
try:
if module == 'bs4':
import bs4
elif module == 'docx':
import docx
elif module == 'PIL':
import PIL
else:
__import__(module)
print(f"{module}")
except ImportError as e:
print(f"{module}: {e}")
failed_modules.append(module)
if failed_modules:
print(f"\n导入失败的模块: {', '.join(failed_modules)}")
return False
print("所有依赖导入成功!")
return True
def test_source_files():
"""测试源代码文件是否存在"""
print("\n检查源代码文件...")
required_files = [
'src/main.py',
'src/main_window.py',
'src/ui/word_style_ui.py',
'src/file_manager/file_operations.py',
'src/input_handler/input_processor.py',
'src/services/network_service.py',
'resources/icons/app_icon.ico',
'resources/config/app_settings.json'
]
missing_files = []
for file_path in required_files:
if os.path.exists(file_path):
print(f"{file_path}")
else:
print(f"{file_path}")
missing_files.append(file_path)
if missing_files:
print(f"\n缺失的文件: {', '.join(missing_files)}")
return False
print("所有源代码文件检查通过!")
return True
def test_version_info():
"""测试版本信息"""
print("\n检查版本信息...")
# 检查setup.py
if os.path.exists('setup.py'):
with open('setup.py', 'r', encoding='utf-8') as f:
content = f.read()
if 'version="0.2.1"' in content:
print("✓ setup.py 版本号正确")
else:
print("✗ setup.py 版本号不正确")
return False
# 检查CHANGELOG.md
if os.path.exists('CHANGELOG.md'):
with open('CHANGELOG.md', 'r', encoding='utf-8') as f:
content = f.read()
if '0.2.1' in content:
print("✓ CHANGELOG.md 包含0.2.1版本信息")
else:
print("✗ CHANGELOG.md 缺少0.2.1版本信息")
return False
print("版本信息检查通过!")
return True
def test_city_mapping():
"""测试城市映射功能"""
print("\n测试城市映射功能...")
try:
# 直接定义城市映射表进行测试
city_id_map = {
# 主要城市中文映射
'北京': '101010100',
'上海': '101020100',
'广州': '101280101',
'深圳': '101280601',
'杭州': '101210101',
'南京': '101190101',
'成都': '101270101',
'武汉': '101200101',
'西安': '101110101',
'重庆': '101040100',
'天津': '101030100',
'苏州': '101190401',
'青岛': '101120201',
'大连': '101070201',
'沈阳': '101070101',
'哈尔滨': '101050101',
'长春': '101060101',
'石家庄': '101090101',
'太原': '101100101',
'郑州': '101180101',
'济南': '101120101',
'合肥': '101220101',
'南昌': '101240101',
'长沙': '101250101',
'福州': '101230101',
'厦门': '101230201',
'南宁': '101300101',
'海口': '101310101',
'贵阳': '101260101',
'昆明': '101290101',
'拉萨': '101140101',
'兰州': '101160101',
'西宁': '101150101',
'银川': '101170101',
'乌鲁木齐': '101130101',
'呼和浩特': '101080101',
# 英文城市名映射到中文
'Beijing': '北京',
'Shanghai': '上海',
'Guangzhou': '广州',
'Shenzhen': '深圳',
'Hangzhou': '杭州',
'Nanjing': '南京',
'Chengdu': '成都',
'Wuhan': '武汉',
'Xian': '西安',
'Chongqing': '重庆',
'Tianjin': '天津',
'Suzhou': '苏州',
'Qingdao': '青岛',
'Dalian': '大连',
'Shenyang': '沈阳',
'Harbin': '哈尔滨',
'Changchun': '长春',
'Shijiazhuang': '石家庄',
'Taiyuan': '太原',
'Zhengzhou': '郑州',
'Jinan': '济南',
'Hefei': '合肥',
'Nanchang': '南昌',
'Changsha': '长沙',
'Fuzhou': '福州',
'Xiamen': '厦门',
'Nanning': '南宁',
'Haikou': '海口',
'Guiyang': '贵阳',
'Kunming': '昆明',
'Lhasa': '拉萨',
'Lanzhou': '兰州',
'Xining': '西宁',
'Yinchuan': '银川',
'Urumqi': '乌鲁木齐',
'Hohhot': '呼和浩特'
}
# 检查一些主要城市
test_cities = [
('Beijing', '北京'),
('Shanghai', '上海'),
('Guangzhou', '广州'),
('Shenzhen', '深圳'),
('Chengdu', '成都'),
('Hangzhou', '杭州')
]
for eng_name, chn_name in test_cities:
if eng_name in city_id_map:
mapped_name = city_id_map[eng_name]
if mapped_name == chn_name:
print(f"{eng_name} -> {mapped_name}")
else:
print(f"{eng_name} -> {mapped_name} (期望: {chn_name})")
else:
print(f"{eng_name} 未找到映射")
print(f"城市映射表包含 {len(city_id_map)} 个城市")
return True
except Exception as e:
print(f"城市映射测试失败: {e}")
return False
def create_test_report():
"""创建测试报告"""
print("\n" + "="*60)
print("MagicWord 0.2.1 版本功能测试报告")
print("="*60)
print(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
tests = [
("依赖导入测试", test_imports),
("源代码文件检查", test_source_files),
("版本信息检查", test_version_info),
("城市映射功能测试", test_city_mapping)
]
passed = 0
total = len(tests)
for test_name, test_func in tests:
print(f"\n[{test_name}]")
if test_func():
passed += 1
print(f"{test_name} 通过")
else:
print(f"{test_name} 失败")
print(f"\n测试结果: {passed}/{total} 通过")
if passed == total:
print("\n✓ 所有测试通过! 版本准备就绪")
return True
else:
print(f"\n{total - passed} 个测试失败, 需要修复")
return False
def main():
"""主函数"""
success = create_test_report()
if success:
print("\n建议下一步:")
print("1. 运行应用进行手动测试")
print("2. 创建发布包")
else:
print("\n请先修复测试中发现的问题")
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())

@ -1,66 +0,0 @@
#!/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, QPushButton, QVBoxLayout, QWidget, QColorDialog
from PyQt5.QtGui import QColor, QTextCharFormat, QTextCursor
from PyQt5.QtCore import Qt
def test_color_function():
"""测试字体颜色功能"""
app = QApplication(sys.argv)
# 创建主窗口
window = QWidget()
window.setWindowTitle("字体颜色测试")
window.setGeometry(100, 100, 400, 300)
# 创建文本编辑器
text_edit = QTextEdit()
text_edit.setPlainText("这是一段测试文本。请选择这段文字并点击颜色按钮来更改颜色。")
# 创建颜色按钮
color_btn = QPushButton("选择颜色")
def change_color():
"""更改字体颜色"""
color = QColorDialog.getColor(Qt.black, window, "选择字体颜色")
if color.isValid():
cursor = text_edit.textCursor()
if cursor.hasSelection():
# 如果有选中文本,只更改选中文本的颜色
fmt = cursor.charFormat()
fmt.setForeground(color)
cursor.setCharFormat(fmt)
print(f"已更改选中文本颜色为: {color.name()}")
else:
# 如果没有选中文本,更改整个文档的默认颜色
text_edit.setTextColor(color)
print(f"已更改默认文本颜色为: {color.name()}")
color_btn.clicked.connect(change_color)
# 布局
layout = QVBoxLayout()
layout.addWidget(text_edit)
layout.addWidget(color_btn)
window.setLayout(layout)
window.show()
print("字体颜色测试窗口已打开")
print("操作说明:")
print("1. 在文本编辑器中选择一些文字")
print("2. 点击'选择颜色'按钮")
print("3. 选择颜色并确认")
print("4. 观察选中文本的颜色是否改变")
sys.exit(app.exec_())
if __name__ == "__main__":
test_color_function()

@ -1,78 +0,0 @@
# 字体颜色功能测试脚本 - 验证保留之前内容的颜色
import sys
from PyQt5.QtWidgets import QApplication, QTextEdit, QPushButton, QVBoxLayout, QWidget, QColorDialog
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QTextCharFormat, QFont
class ColorTestWindow(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这行文字将保持原有颜色。\n新输入的文字将使用新颜色。")
# 设置一些初始颜色来模拟原有内容
cursor = self.text_edit.textCursor()
cursor.select(cursor.Document)
cursor.setPosition(0)
# 给第一行设置红色
cursor.movePosition(cursor.StartOfLine)
cursor.movePosition(cursor.EndOfLine, cursor.KeepAnchor)
fmt = QTextCharFormat()
fmt.setForeground(QColor("red"))
cursor.setCharFormat(fmt)
# 给第二行设置蓝色
cursor.movePosition(cursor.NextBlock)
cursor.movePosition(cursor.EndOfLine, cursor.KeepAnchor)
fmt.setForeground(QColor("blue"))
cursor.setCharFormat(fmt)
layout.addWidget(self.text_edit)
# 创建颜色选择按钮
self.color_btn = QPushButton("选择新颜色(保留原有内容颜色)")
self.color_btn.clicked.connect(self.on_color_clicked)
layout.addWidget(self.color_btn)
# 创建说明标签
self.info_label = QLabel("点击按钮选择颜色,新输入的文本将使用新颜色,原有内容颜色保持不变")
layout.addWidget(self.info_label)
self.setLayout(layout)
def on_color_clicked(self):
"""字体颜色按钮点击处理 - 保留之前内容的颜色"""
# 显示颜色选择对话框,默认使用当前文本颜色
current_color = self.text_edit.textColor()
color = QColorDialog.getColor(current_color, self, "选择字体颜色")
if color.isValid():
# 只设置后续输入的默认颜色,不影响已有内容
self.text_edit.setTextColor(color)
# 如果有选中文本,提示用户颜色只对新输入生效
cursor = self.text_edit.textCursor()
if cursor.hasSelection():
self.info_label.setText("字体颜色已设置,新输入的文本将使用该颜色(原有内容颜色保持不变)")
else:
self.info_label.setText(f"新颜色已设置: {color.name()},后续输入将使用该颜色")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ColorTestWindow()
window.show()
print("测试说明:")
print("1. 文本编辑器中有三行文字,第一行红色,第二行蓝色,第三行默认黑色")
print("2. 点击颜色选择按钮选择新颜色")
print("3. 在文本末尾输入新文字,观察新文字使用新颜色而原有文字颜色不变")
print("4. 测试验证:保留之前内容的颜色功能是否正常工作")
sys.exit(app.exec_())

@ -1,111 +0,0 @@
#!/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()

@ -1,44 +0,0 @@
#!/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()

@ -1,22 +0,0 @@
#!/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()

@ -1,71 +0,0 @@
#!/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)

@ -1,69 +0,0 @@
#!/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()

@ -1,60 +0,0 @@
#!/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("所有测试完成!")

@ -1,62 +0,0 @@
#!/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()
Loading…
Cancel
Save