diff --git a/.gitignore b/.gitignore index 4ec21cb..2d96080 100644 --- a/.gitignore +++ b/.gitignore @@ -198,6 +198,13 @@ temp/ *.orig # Project specific +resources/config/deepseek_api.json +*.key +*.secret +config/*.json + +# Documentation folder +doc/ dist_package/ dist_package_v0.3/ *.zip @@ -231,6 +238,7 @@ venv/ env/ .venv/ .env/ +new_venv/ # IDE .idea/ diff --git a/PYQT5_FIX_GUIDE.md b/PYQT5_FIX_GUIDE.md new file mode 100644 index 0000000..2245480 --- /dev/null +++ b/PYQT5_FIX_GUIDE.md @@ -0,0 +1,120 @@ +# PyQt5 平台插件问题完全解决方案 + +## 问题描述 +在使用PyQt5时,可能会遇到以下错误: +``` +qt.qpa.plugin: Could not find the Qt platform plugin "cocoa" in "" +This application failed to start because no Qt platform plugin could be initialized. +``` + +## 解决方案 + +### 方法一:一键修复(推荐) +运行完整的修复脚本: +```bash +python fix_pyqt5_complete.py +``` + +### 方法二:手动修复 +1. **清理现有安装** +```bash +pip uninstall PyQt5 PyQt5-Qt5 PyQt5-sip -y +rm -rf /Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5* +``` + +2. **重新安装** +```bash +pip install PyQt5==5.15.10 --force-reinstall --no-cache-dir +``` + +3. **设置环境变量** +```bash +source set_pyqt5_env.sh +``` + +### 方法三:安全安装 +使用安全安装脚本: +```bash +python install_pyqt5_safe.py +``` + +## 预防措施 + +### 1. 在main.py中集成环境设置 +确保你的 `main.py` 包含了增强的Qt插件路径设置函数。 + +### 2. 创建启动脚本 +创建 `start_app.sh`: +```bash +#!/bin/bash +# PyQt5应用程序启动脚本 + +# 设置环境变量 +export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms" +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" + +# 启动应用 +python src/main.py +``` + +### 3. 使用虚拟环境专用安装 +```bash +# 激活虚拟环境 +source .venv/bin/activate + +# 在虚拟环境中安装 +python fix_pyqt5_complete.py +``` + +## 环境变量说明 + +| 变量名 | 作用 | 推荐值 | +|--------|------|--------| +| QT_PLUGIN_PATH | Qt插件主路径 | PyQt5/Qt5/plugins | +| QT_QPA_PLATFORM_PLUGIN_PATH | 平台插件路径 | PyQt5/Qt5/plugins/platforms | +| QT_QPA_PLATFORM | 指定平台 | cocoa (macOS) | +| QT_MAC_WANTS_LAYER | macOS图层支持 | 1 | +| QT_LOGGING_RULES | 日志级别 | qt.qpa.*=false | + +## 常见问题 + +### Q: 为什么PyQt5会丢失平台插件? +A: 常见原因: +- 安装过程中断或失败 +- 虚拟环境迁移 +- 系统Qt库冲突 +- 文件权限问题 + +### Q: 如何验证修复是否成功? +A: 运行测试命令: +```python +python -c "from PyQt5.QtWidgets import QApplication; print('成功!')" +``` + +### Q: 修复后仍然有问题? +A: 尝试: +1. 完全删除虚拟环境重新创建 +2. 使用系统包管理器安装Qt5 +3. 检查Python版本兼容性 + +## 最佳实践 + +1. **始终使用虚拟环境** +2. **固定PyQt5版本**(推荐5.15.10) +3. **在代码中设置插件路径** +4. **创建启动脚本** +5. **定期验证安装** + +## 一键修复命令 + +```bash +# 完整的修复流程 +cd /Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design +python fix_pyqt5_complete.py +source set_pyqt5_env.sh +python src/main.py +``` + +这样应该能完全避免PyQt5平台插件问题! \ No newline at end of file diff --git a/build_v1.0.py b/build_v1.0.py new file mode 100644 index 0000000..9689a80 --- /dev/null +++ b/build_v1.0.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +MagicWord 1.0.0 版本发布脚本 +用于构建和打包应用程序,包含所有图片和图标资源 +""" + +import os +import sys +import subprocess +import platform +import shutil +import zipfile +from datetime import datetime +from PIL import Image + +def run_command(command, shell=False, cwd=None): + """运行命令并返回结果""" + try: + result = subprocess.run(command, shell=shell, capture_output=True, text=True, encoding='utf-8', cwd=cwd) + return result.returncode, result.stdout, result.stderr + except Exception as e: + return -1, "", str(e) + +def create_ico_from_png(): + """从PNG图标创建ICO文件""" + print("创建ICO图标文件...") + + # 检查是否存在256x256的PNG图标 + png_path = "resources/icons/app_icon_256X256.png" + ico_path = "resources/icons/app_icon.ico" + + if os.path.exists(png_path): + try: + # 打开PNG图像 + img = Image.open(png_path) + + # 创建不同尺寸的图标 + icon_sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)] + + # 保存为ICO格式 + img.save(ico_path, format='ICO', sizes=icon_sizes) + print(f"ICO图标创建成功: {ico_path}") + return True + except Exception as e: + print(f"创建ICO图标失败: {e}") + return False + else: + print(f"找不到PNG图标文件: {png_path}") + return False + +def clean_build_dirs(): + """清理构建目录""" + print("清理构建目录...") + dirs_to_clean = ['build', 'dist', '__pycache__', '*.egg-info'] + + for dir_name in dirs_to_clean: + if '*' in dir_name: + # 处理通配符 + import glob + for path in glob.glob(dir_name): + if os.path.isdir(path): + shutil.rmtree(path, ignore_errors=True) + elif os.path.exists(dir_name): + if os.path.isdir(dir_name): + shutil.rmtree(dir_name, ignore_errors=True) + else: + os.remove(dir_name) + + # 清理src目录下的__pycache__ + for root, dirs, files in os.walk('src'): + for dir_name in dirs: + if dir_name == '__pycache__': + cache_path = os.path.join(root, dir_name) + shutil.rmtree(cache_path, ignore_errors=True) + print(f"清理缓存: {cache_path}") + +def install_dependencies(): + """安装依赖""" + print("安装项目依赖...") + + # 首先安装PIL(用于图标转换) + code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "Pillow"]) + if code != 0: + print(f"Pillow安装失败: {stderr}") + return False + + # 安装其他依赖 + code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"]) + if code != 0: + print(f"依赖安装失败: {stderr}") + return False + print("依赖安装成功") + return True + +def build_executable(): + """构建可执行文件""" + print("构建可执行文件...") + + # 安装pyinstaller + print("安装PyInstaller...") + code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "pyinstaller"]) + if code != 0: + print(f"PyInstaller安装失败: {stderr}") + return False + + # 创建ICO图标 + create_ico_from_png() + + # PyInstaller命令 - 完整版本 + pyinstaller_cmd = [ + "pyinstaller", + "--name", "MagicWord", + "--version", "1.0.0", # 设置版本号为1.0.0 + "--distpath", "dist", + "--workpath", "build", + "--specpath", ".", + # 添加资源文件 + "--add-data", "resources;resources", + "--add-data", "resources/icons;resources/icons", + "--add-data", "resources/config;resources/config", + "--add-data", "resources/qss;resources/qss", + "--add-data", "src;src", + "--add-data", "src/ui;src/ui", + "--add-data", "src/demo;src/demo", + # 隐藏导入 + "--hidden-import", "PyQt5", + "--hidden-import", "PyQt5.QtCore", + "--hidden-import", "PyQt5.QtGui", + "--hidden-import", "PyQt5.QtWidgets", + "--hidden-import", "requests", + "--hidden-import", "beautifulsoup4", + "--hidden-import", "python-docx", + "--hidden-import", "PyPDF2", + "--hidden-import", "ebooklib", + "--hidden-import", "chardet", + "--hidden-import", "PIL", + "--hidden-import", "pillow", + # 图标设置 + "--icon", "resources/icons/app_icon.ico" if os.path.exists("resources/icons/app_icon.ico") else "resources/icons/app_icon_256X256.png", + "--windowed", # 无控制台窗口 + "--noconfirm", + "--clean", # 清理临时文件 + "src/main.py" + ] + + print("运行PyInstaller...") + code, stdout, stderr = run_command(pyinstaller_cmd) + + if code != 0: + print(f"完整构建失败,尝试简化构建: {stderr}") + # 简化版本 + simple_cmd = [ + "pyinstaller", + "--onefile", + "--windowed", + "--icon=resources/icons/app_icon.ico" if os.path.exists("resources/icons/app_icon.ico") else "resources/icons/app_icon_256X256.png", + "--add-data=resources;resources", + "--add-data=resources/icons;resources/icons", + "--add-data=src;src", + "--name=MagicWord", + "--version=1.0.0", + "--clean", + "src/main.py" + ] + code, stdout, stderr = run_command(simple_cmd) + if code != 0: + print(f"简化构建也失败: {stderr}") + return False + + print("可执行文件构建成功") + return True + +def create_package(): + """创建发布包""" + print("创建发布包...") + + # 检查构建结果 + if platform.system() == "Windows": + exe_path = "dist/MagicWord.exe" + else: + exe_path = "dist/MagicWord" + + if not os.path.exists(exe_path): + print(f"错误: 找不到可执行文件 {exe_path}") + return False + + # 创建发布目录 + release_dir = "dist_package_v1.0" + if os.path.exists(release_dir): + shutil.rmtree(release_dir) + os.makedirs(release_dir) + + # 复制文件到发布目录 + files_to_copy = [ + (exe_path, "MagicWord.exe" if platform.system() == "Windows" else "MagicWord"), + ("README.md", "README.md"), + ("CHANGELOG.md", "CHANGELOG.md"), + ("requirements.txt", "requirements.txt"), + ("install_and_fix.py", "install_and_fix.py"), + ] + + for src, dst in files_to_copy: + if os.path.exists(src): + shutil.copy2(src, os.path.join(release_dir, dst)) + print(f"复制: {src} -> {dst}") + + # 复制图标文件到发布包 + icons_dir = os.path.join(release_dir, "icons") + if os.path.exists("resources/icons"): + shutil.copytree("resources/icons", icons_dir) + print("复制图标文件到发布包") + + # 创建运行脚本 + if platform.system() == "Windows": + run_script = """@echo off +echo MagicWord 1.0.0 启动中... +cd /d "%~dp0" +start MagicWord.exe +""" + with open(os.path.join(release_dir, "run.bat"), "w") as f: + f.write(run_script) + + # 创建桌面快捷方式脚本 + desktop_shortcut_script = """@echo off +echo 创建桌面快捷方式... +set SCRIPT_DIR=%~dp0 +set DESKTOP=%USERPROFILE%\Desktop +set SHORTCUT=%DESKTOP%\MagicWord.lnk +powershell -Command "$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%SHORTCUT%'); $Shortcut.TargetPath = '%SCRIPT_DIR%MagicWord.exe'; $Shortcut.WorkingDirectory = '%SCRIPT_DIR%'; $Shortcut.IconLocation = '%SCRIPT_DIR%icons\\app_icon_256X256.png'; $Shortcut.Save()" +echo 桌面快捷方式创建完成! +pause +""" + with open(os.path.join(release_dir, "create_desktop_shortcut.bat"), "w") as f: + f.write(desktop_shortcut_script) + + else: + run_script = """#!/bin/bash +echo "MagicWord 1.0.0 启动中..." +cd "$(dirname "$0")" +./MagicWord & +""" + with open(os.path.join(release_dir, "run.sh"), "w") as f: + f.write(run_script) + os.chmod(os.path.join(release_dir, "run.sh"), 0o755) + + # 创建发布说明 + release_info = f"""MagicWord 1.0.0 发布包 +构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +平台: {platform.system()} {platform.machine()} +Python版本: {platform.python_version()} + +快速开始: +1. 运行 install_and_fix.py 安装依赖(如果需要) +2. 运行 run.bat (Windows) 或 run.sh (Linux/Mac) +3. 或直接运行 MagicWord.exe + +功能特性: +- 完整的文档处理功能 +- 打字练习模式 +- 学习模式 +- 多种文档格式支持(Word, PDF, TXT等) +- 美观的Word风格界面 +- 完整的图标和界面资源 + +图标说明: +- 包含完整的图标资源文件 +- 支持多种尺寸的图标(32x32, 64x64, 128x128, 256x256) +- Windows版本包含ICO格式图标 +- 可创建桌面快捷方式 + +详细更新请查看 CHANGELOG.md +""" + + with open(os.path.join(release_dir, "发布说明.txt"), "w", encoding='utf-8') as f: + f.write(release_info) + + # 创建ZIP压缩包 + print("创建ZIP压缩包...") + zip_filename = f"MagicWord_v1.0.0_{platform.system()}_{platform.machine()}" + with zipfile.ZipFile(f"{zip_filename}.zip", 'w', zipfile.ZIP_DEFLATED) as zipf: + for root, dirs, files in os.walk(release_dir): + for file in files: + file_path = os.path.join(root, file) + arc_path = os.path.relpath(file_path, release_dir) + zipf.write(file_path, arc_path) + print(f"添加到压缩包: {arc_path}") + + print(f"发布包创建成功: {zip_filename}.zip") + return True + +def main(): + """主函数""" + print("=" * 50) + print("MagicWord 1.0.0 构建脚本") + print("=" * 50) + + # 检查Python版本 + if sys.version_info < (3, 6): + print("错误: 需要Python 3.6或更高版本") + return False + + # 清理构建目录 + clean_build_dirs() + + # 安装依赖 + if not install_dependencies(): + print("依赖安装失败") + return False + + # 构建可执行文件 + if not build_executable(): + print("可执行文件构建失败") + return False + + # 创建发布包 + if not create_package(): + print("发布包创建失败") + return False + + print("=" * 50) + print("构建完成!") + print("发布包位于: dist_package_v1.0/") + print("ZIP压缩包已创建") + print("=" * 50) + + return True + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/doc/软件需求构思和描述文档-迭代一轮.docx b/doc/软件需求构思和描述文档-迭代一轮.docx index c783b15..8cf1c39 100644 Binary files a/doc/软件需求构思和描述文档-迭代一轮.docx and b/doc/软件需求构思和描述文档-迭代一轮.docx differ diff --git a/emergency_fix.sh b/emergency_fix.sh new file mode 100755 index 0000000..2eb9ab7 --- /dev/null +++ b/emergency_fix.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# PyQt5 紧急修复脚本 - 终极解决方案 + +echo "🚨 PyQt5 紧急修复中..." + +# 1. 完全清理现有安装 +echo "📦 步骤1: 清理PyQt5安装..." +pip uninstall PyQt5 PyQt5-Qt5 PyQt5-sip -y +rm -rf /Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5* + +# 2. 重新安装PyQt5 +echo "📦 步骤2: 重新安装PyQt5..." +pip install PyQt5==5.15.10 --force-reinstall --no-cache-dir + +# 3. 设置环境变量 +echo "🔧 步骤3: 设置环境变量..." +export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms" +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" +export QT_LOGGING_RULES="qt.qpa.*=false" + +# 4. 验证安装 +echo "✅ 步骤4: 验证安装..." +python -c " +import sys +import os +os.environ['QT_PLUGIN_PATH'] = '$QT_PLUGIN_PATH' +os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = '$QT_QPA_PLATFORM_PLUGIN_PATH' +os.environ['QT_QPA_PLATFORM'] = 'cocoa' +os.environ['QT_MAC_WANTS_LAYER'] = '1' + +try: + from PyQt5.QtWidgets import QApplication, QLabel + from PyQt5.QtCore import Qt + + app = QApplication(sys.argv) + label = QLabel('PyQt5修复成功!✅') + label.setAlignment(Qt.AlignCenter) + label.resize(200, 100) + label.show() + + from PyQt5.QtCore import QTimer + QTimer.singleShot(1500, app.quit) + + app.exec_() + print('✅ PyQt5验证成功!') +except Exception as e: + print(f'❌ PyQt5验证失败: {e}') + import traceback + traceback.print_exc() +" + +echo "🎉 修复完成!" +echo "现在可以运行: python src/main.py" \ No newline at end of file diff --git a/fix_pyqt5_complete.py b/fix_pyqt5_complete.py new file mode 100644 index 0000000..ac522d4 --- /dev/null +++ b/fix_pyqt5_complete.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +""" +完整的PyQt5问题解决方案 +运行此脚本可以完全避免PyQt5平台插件问题 +""" + +import subprocess +import sys +import os +import shutil +import platform + +def run_command(cmd, description): + """运行命令并显示进度""" + print(f"正在执行: {description}") + result = subprocess.run(cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"❌ 失败: {result.stderr}") + return False + print(f"✅ 完成") + return True + +def clean_pyqt5_installation(): + """彻底清理PyQt5安装""" + print("=== 清理PyQt5安装 ===") + + # 1. 卸载PyQt5包 + run_command([sys.executable, "-m", "pip", "uninstall", "PyQt5", "PyQt5-Qt5", "PyQt5-sip", "-y"], + "卸载PyQt5包") + + # 2. 清理残留文件 + venv_path = os.environ.get('VIRTUAL_ENV', '') + if venv_path: + site_packages = os.path.join(venv_path, 'lib', f'python{sys.version_info.major}.{sys.version_info.minor}', 'site-packages') + if os.path.exists(site_packages): + removed_count = 0 + for item in os.listdir(site_packages): + if 'pyqt5' in item.lower(): + item_path = os.path.join(site_packages, item) + try: + if os.path.isdir(item_path): + shutil.rmtree(item_path) + else: + os.remove(item_path) + removed_count += 1 + print(f" 删除: {item}") + except Exception as e: + print(f" 删除失败 {item}: {e}") + print(f"✅ 清理完成,删除了 {removed_count} 个项目") + + return True + +def install_pyqt5_properly(): + """正确安装PyQt5""" + print("=== 安装PyQt5 ===") + + # 使用清华镜像源加速下载(可选) + pip_args = [sys.executable, "-m", "pip", "install"] + + # 检查是否有国内镜像源可用 + try: + import requests + response = requests.get("https://pypi.tuna.tsinghua.edu.cn/simple", timeout=5) + if response.status_code == 200: + pip_args.extend(["-i", "https://pypi.tuna.tsinghua.edu.cn/simple"]) + print("使用清华镜像源") + except: + pass + + # 安装PyQt5 + pip_args.extend(["PyQt5==5.15.10", "--no-cache-dir", "--force-reinstall"]) + + return run_command(pip_args, "安装PyQt5") + +def setup_environment_variables(): + """设置环境变量""" + print("=== 设置环境变量 ===") + + system = platform.system() + venv_path = os.environ.get('VIRTUAL_ENV', '') + + if not venv_path: + print("❌ 未检测到虚拟环境") + return False + + python_version = f"python{sys.version_info.major}.{sys.version_info.minor}" + + # 可能的Qt插件路径 + possible_paths = [] + + if system == "Darwin": # macOS + possible_paths = [ + os.path.join(venv_path, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/local/opt/qt5/plugins', + '/opt/homebrew/opt/qt5/plugins', + ] + elif system == "Windows": + possible_paths = [ + os.path.join(venv_path, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + ] + elif system == "Linux": + possible_paths = [ + os.path.join(venv_path, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/lib/x86_64-linux-gnu/qt5/plugins', + ] + + # 找到有效的路径 + valid_path = None + for path in possible_paths: + if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')): + valid_path = path + break + + if valid_path: + # 创建环境变量设置脚本 + env_script = f""" +# PyQt5环境变量设置 +export QT_PLUGIN_PATH="{valid_path}" +export QT_QPA_PLATFORM_PLUGIN_PATH="{os.path.join(valid_path, 'platforms')}" +""" + + if system == "Darwin": + env_script += """ +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" +export QT_LOGGING_RULES="qt.qpa.*=false" +""" + elif system == "Windows": + env_script += """ +export QT_QPA_PLATFORM="windows" +""" + elif system == "Linux": + env_script += """ +export QT_QPA_PLATFORM="xcb" +""" + + # 保存环境变量脚本 + project_root = os.path.dirname(os.path.abspath(__file__)) + env_file = os.path.join(project_root, 'set_pyqt5_env.sh') + with open(env_file, 'w') as f: + f.write(env_script.strip()) + + print(f"✅ 环境变量脚本已创建: {env_file}") + print(f" QT_PLUGIN_PATH: {valid_path}") + return True + else: + print("❌ 未找到有效的Qt插件路径") + return False + +def verify_installation(): + """验证安装""" + print("=== 验证安装 ===") + + # 测试导入 + test_code = """ +import sys +import os + +# 设置环境变量 +if 'QT_PLUGIN_PATH' in os.environ: + os.environ['QT_PLUGIN_PATH'] = os.environ['QT_PLUGIN_PATH'] +if 'QT_QPA_PLATFORM_PLUGIN_PATH' in os.environ: + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] + +try: + from PyQt5.QtWidgets import QApplication, QLabel + from PyQt5.QtCore import Qt + + app = QApplication(sys.argv) + label = QLabel("PyQt5安装成功!✅") + label.setAlignment(Qt.AlignCenter) + label.resize(200, 100) + label.show() + + # 只显示2秒后自动关闭 + from PyQt5.QtCore import QTimer + QTimer.singleShot(2000, app.quit) + + app.exec_() + print("✅ PyQt5验证成功!") +except Exception as e: + print(f"❌ PyQt5验证失败: {e}") + import traceback + traceback.print_exc() +""" + + with open('test_pyqt5.py', 'w') as f: + f.write(test_code) + + result = subprocess.run([sys.executable, 'test_pyqt5.py'], capture_output=True, text=True) + + # 清理测试文件 + if os.path.exists('test_pyqt5.py'): + os.remove('test_pyqt5.py') + + return result.returncode == 0 + +def main(): + """主函数""" + print("=== PyQt5完整修复方案 ===") + print(f"系统: {platform.system()}") + print(f"Python: {sys.version}") + print(f"虚拟环境: {os.environ.get('VIRTUAL_ENV', '未激活')}") + print() + + # 获取项目根目录 + project_root = os.path.dirname(os.path.abspath(__file__)) + os.chdir(project_root) + + steps = [ + ("清理安装", clean_pyqt5_installation), + ("重新安装", install_pyqt5_properly), + ("设置环境", setup_environment_variables), + ("验证安装", verify_installation), + ] + + success = True + for step_name, step_func in steps: + if not step_func(): + print(f"❌ {step_name} 失败") + success = False + break + print() + + if success: + print("🎉 PyQt5修复完成!") + print("\n使用方法:") + print("1. 运行: source set_pyqt5_env.sh") + print("2. 然后运行: python src/main.py") + else: + print("❌ PyQt5修复失败,请检查错误信息") + + return success + +if __name__ == "__main__": + success = main() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/install_pyqt5_safe.py b/install_pyqt5_safe.py new file mode 100644 index 0000000..7f89679 --- /dev/null +++ b/install_pyqt5_safe.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +安全安装PyQt5的脚本,避免平台插件问题 +""" + +import subprocess +import sys +import os + +def install_pyqt5_safely(): + """安全安装PyQt5的方法""" + + print("正在安全安装PyQt5...") + + # 1. 首先完全卸载现有的PyQt5 + print("1. 卸载现有PyQt5...") + subprocess.run([sys.executable, "-m", "pip", "uninstall", "PyQt5", "PyQt5-Qt5", "PyQt5-sip", "-y"], + capture_output=True, text=True) + + # 2. 清理可能残留的文件 + print("2. 清理残留文件...") + venv_path = os.environ.get('VIRTUAL_ENV', '') + if venv_path: + site_packages = os.path.join(venv_path, 'lib', f'python{sys.version_info.major}.{sys.version_info.minor}', 'site-packages') + if os.path.exists(site_packages): + for item in os.listdir(site_packages): + if 'pyqt5' in item.lower(): + item_path = os.path.join(site_packages, item) + if os.path.isdir(item_path): + import shutil + shutil.rmtree(item_path) + print(f" 删除目录: {item}") + + # 3. 使用特定版本安装PyQt5 + print("3. 安装PyQt5...") + result = subprocess.run([ + sys.executable, "-m", "pip", "install", + "PyQt5==5.15.10", + "--no-cache-dir", # 不使用缓存,确保重新下载 + "--force-reinstall" # 强制重新安装 + ], capture_output=True, text=True) + + if result.returncode == 0: + print("✅ PyQt5安装成功!") + + # 4. 验证安装 + print("4. 验证安装...") + test_result = subprocess.run([ + sys.executable, "-c", + "from PyQt5.QtWidgets import QApplication; print('PyQt5导入成功')" + ], capture_output=True, text=True) + + if test_result.returncode == 0: + print("✅ PyQt5验证通过!") + return True + else: + print("❌ PyQt5验证失败:", test_result.stderr) + return False + else: + print("❌ PyQt5安装失败:", result.stderr) + return False + +if __name__ == "__main__": + success = install_pyqt5_safely() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/package_v1.0.py b/package_v1.0.py new file mode 100644 index 0000000..9ba4472 --- /dev/null +++ b/package_v1.0.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +MagicWord 1.0.0 完整打包脚本 +创建包含所有资源的发布包 +""" + +import os +import sys +import shutil +import zipfile +from datetime import datetime + +def create_package(): + """创建完整的发布包""" + print("=" * 60) + print("MagicWord 1.0.0 完整打包程序") + print("=" * 60) + + # 检查可执行文件是否存在 + exe_path = "dist/MagicWord.exe" + if not os.path.exists(exe_path): + print(f"错误: 找不到可执行文件 {exe_path}") + print("请先运行PyInstaller构建可执行文件") + return False + + # 创建发布目录 + release_dir = "MagicWord_v1.0.0_Windows" + if os.path.exists(release_dir): + shutil.rmtree(release_dir) + os.makedirs(release_dir) + + print("创建发布包目录...") + + # 复制主要文件 + files_to_copy = [ + (exe_path, "MagicWord.exe"), + ("README.md", "README.md"), + ("CHANGELOG.md", "CHANGELOG.md"), + ("requirements.txt", "requirements.txt"), + ("install_and_fix.py", "install_and_fix.py"), + ] + + for src, dst in files_to_copy: + if os.path.exists(src): + shutil.copy2(src, os.path.join(release_dir, dst)) + print(f"✓ 复制: {src} -> {dst}") + else: + print(f"⚠ 跳过: {src} (文件不存在)") + + # 复制图标文件 + icons_dir = os.path.join(release_dir, "icons") + if os.path.exists("resources/icons"): + shutil.copytree("resources/icons", icons_dir) + print("✓ 复制图标文件到发布包") + + # 复制配置文件 + config_dir = os.path.join(release_dir, "config") + if os.path.exists("resources/config"): + shutil.copytree("resources/config", config_dir) + print("✓ 复制配置文件到发布包") + + # 复制样式文件 + qss_dir = os.path.join(release_dir, "qss") + if os.path.exists("resources/qss") and os.listdir("resources/qss"): + shutil.copytree("resources/qss", qss_dir) + print("✓ 复制样式文件到发布包") + + # 注意: UI图像文件已通过PyInstaller打包到可执行文件中 + # 程序会在运行时从打包的资源中加载 114514.png 和 UI.png + + # 创建运行脚本 + print("创建运行脚本...") + + # Windows运行脚本 + run_script = """@echo off +echo ======================================== +echo MagicWord 1.0.0 启动中... +echo ======================================== +cd /d "%~dp0" +start "" "MagicWord.exe" +echo 程序已启动! +timeout /t 3 /nobreak > nul +""" + + with open(os.path.join(release_dir, "运行程序.bat"), "w", encoding='utf-8') as f: + f.write(run_script) + + # 创建桌面快捷方式脚本 + desktop_shortcut_script = """@echo off +echo 正在创建桌面快捷方式... +cd /d "%~dp0" +set SCRIPT_DIR=%CD% +set DESKTOP=%USERPROFILE%\Desktop +set SHORTCUT=%DESKTOP%\MagicWord.lnk + +echo 脚本目录: %SCRIPT_DIR% +echo 桌面路径: %DESKTOP% + +powershell -Command " + $WshShell = New-Object -comObject WScript.Shell + $Shortcut = $WshShell.CreateShortcut('%SHORTCUT%') + $Shortcut.TargetPath = '%SCRIPT_DIR%\MagicWord.exe' + $Shortcut.WorkingDirectory = '%SCRIPT_DIR%' + $Shortcut.IconLocation = '%SCRIPT_DIR%\icons\app_icon_256X256.png' + $Shortcut.Description = 'MagicWord 1.0.0 - 隐私学习软件' + $Shortcut.Save() + Write-Host '桌面快捷方式创建成功!' -ForegroundColor Green +" + +if exist "%SHORTCUT%" ( + echo 桌面快捷方式创建成功! +) else ( + echo 桌面快捷方式创建失败! +) +echo. +echo 按任意键退出... +pause > nul +""" + + with open(os.path.join(release_dir, "创建桌面快捷方式.bat"), "w", encoding='utf-8') as f: + f.write(desktop_shortcut_script) + + # 创建安装说明 + install_guide = """MagicWord 1.0.0 安装使用说明 + +======================================== +系统要求 +======================================== +- Windows 7/8/10/11 (64位) +- 至少 100MB 可用磁盘空间 +- 建议内存: 4GB 以上 + +======================================== +快速开始 +======================================== +方法1: 直接运行 +1. 双击 "MagicWord.exe" 即可运行程序 + +方法2: 使用运行脚本 +1. 双击 "运行程序.bat" 自动启动程序 + +方法3: 创建桌面快捷方式 +1. 双击 "创建桌面快捷方式.bat" +2. 桌面上会出现 MagicWord 快捷方式 +3. 双击桌面快捷方式即可运行 + +======================================== +文件说明 +======================================== +MagicWord.exe - 主程序文件 +icons/ - 图标文件夹 + app_icon.ico - 应用程序图标 + app_icon_256X256.png - 256x256图标 + app_icon_128X128.png - 128x128图标 + app_icon_64X64.png - 64x64图标 + app_icon_32X32.png - 32x32图标 + +config/ - 配置文件 +README.md - 项目说明文档 +CHANGELOG.md - 更新日志 + +======================================== +卸载方法 +======================================== +直接删除整个 MagicWord 文件夹即可完全卸载 + +======================================== +技术支持 +======================================== +如有问题,请查看 README.md 文件或联系开发者 + +======================================== +版本信息 +======================================== +版本: 1.0.0 +构建时间: {build_time} +平台: Windows +架构: {architecture} + +祝您使用愉快! +""".format( + build_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + architecture="64位" + ) + + with open(os.path.join(release_dir, "安装说明.txt"), "w", encoding='utf-8') as f: + f.write(install_guide) + + # 创建版本信息文件 + version_info = f"""MagicWord Version 1.0.0 +Build Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +Platform: Windows +Architecture: 64-bit +Python Version: 3.13 +Qt Version: PyQt5 + +This is a standalone executable package containing all necessary dependencies. +""" + + with open(os.path.join(release_dir, "version.txt"), "w", encoding='utf-8') as f: + f.write(version_info) + + print("创建ZIP压缩包...") + + # 创建ZIP压缩包 + zip_filename = f"MagicWord_v1.0.0_Windows_64bit" + with zipfile.ZipFile(f"{zip_filename}.zip", 'w', zipfile.ZIP_DEFLATED) as zipf: + for root, dirs, files in os.walk(release_dir): + for file in files: + file_path = os.path.join(root, file) + arc_path = os.path.relpath(file_path, release_dir) + zipf.write(file_path, arc_path) + print(f"添加到压缩包: {arc_path}") + + print("=" * 60) + print("✅ 打包完成!") + print(f"📁 发布包目录: {release_dir}/") + print(f"📦 ZIP压缩包: {zip_filename}.zip") + print("=" * 60) + + # 显示发布包内容 + print("\n发布包内容:") + for root, dirs, files in os.walk(release_dir): + level = root.replace(release_dir, '').count(os.sep) + indent = ' ' * 2 * level + print(f"{indent}{os.path.basename(root)}/") + subindent = ' ' * 2 * (level + 1) + for file in files: + print(f"{subindent}{file}") + + return True + +def main(): + """主函数""" + try: + success = create_package() + if success: + print("\n🎉 所有文件已成功打包!") + print("您可以分发 ZIP 文件给用户。") + else: + print("\n❌ 打包过程中出现错误。") + return 1 + except Exception as e: + print(f"\n❌ 发生错误: {e}") + return 1 + + return 0 + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/resources/icons/app_icon.icns b/resources/icons/app_icon.icns new file mode 100644 index 0000000..06053a1 Binary files /dev/null and b/resources/icons/app_icon.icns differ diff --git a/resources/icons/app_icon.ico b/resources/icons/app_icon.ico new file mode 100644 index 0000000..9a97f72 Binary files /dev/null and b/resources/icons/app_icon.ico differ diff --git a/run_debug.sh b/run_debug.sh new file mode 100644 index 0000000..b062afe --- /dev/null +++ b/run_debug.sh @@ -0,0 +1,7 @@ +#!/bin/bash +echo "设置Qt调试环境变量..." +export QT_DEBUG_PLUGINS=1 +echo "Qt调试模式已启用" +echo "" +echo "运行MagicWord应用程序..." +python src/main.py diff --git a/run_fixed.sh b/run_fixed.sh new file mode 100755 index 0000000..9f125f0 --- /dev/null +++ b/run_fixed.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# MagicWord 修复版启动脚本 + +echo "🚀 正在启动 MagicWord (修复版)..." + +# 使用新的虚拟环境 +source new_venv/bin/activate + +# 设置Qt环境变量 +export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/new_venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/new_venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms" +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" +export QT_LOGGING_RULES="qt.qpa.*=false" + +echo "✅ 环境设置完成" +echo "✅ 正在启动 MagicWord 应用..." + +# 启动应用 +cd src && python main.py \ No newline at end of file diff --git a/run_magicword.sh b/run_magicword.sh new file mode 100755 index 0000000..f40b70e --- /dev/null +++ b/run_magicword.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# MagicWord 一键启动脚本 +# 自动处理PyQt5平台插件问题 + +echo "🚀 正在启动 MagicWord..." + +# 设置PyQt5环境变量 +export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms" +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" +export QT_LOGGING_RULES="qt.qpa.*=false" + +# 激活虚拟环境(如果未激活) +if [ -z "$VIRTUAL_ENV" ]; then + echo "📦 激活虚拟环境..." + source .venv/bin/activate +fi + +# 检查PyQt5是否可用 +python -c "from PyQt5.QtWidgets import QApplication" 2>/dev/null +if [ $? -ne 0 ]; then + echo "❌ PyQt5出现问题,正在修复..." + ./emergency_fix.sh +fi + +# 启动应用 +echo "✅ 启动 MagicWord 应用..." +cd src && python main.py \ No newline at end of file diff --git a/set_pyqt5_env.sh b/set_pyqt5_env.sh new file mode 100644 index 0000000..e4ec5ce --- /dev/null +++ b/set_pyqt5_env.sh @@ -0,0 +1,7 @@ +# PyQt5环境变量设置 +export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms" + +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" +export QT_LOGGING_RULES="qt.qpa.*=false" \ No newline at end of file diff --git a/setup.py b/setup.py index f509b1c..8589459 100644 --- a/setup.py +++ b/setup.py @@ -2,12 +2,15 @@ from setuptools import setup, find_packages setup( name="MagicWord", - version="0.3.0", + version="1.0.0", description="隐私学习软件 - 一款通过打字练习来学习文档内容的工具", author="MagicWord Team", packages=find_packages(where="src"), package_dir={"": "src"}, include_package_data=True, + package_data={ + "": ["*.png", "*.ico", "*.json", "*.qss", "*.txt"], + }, install_requires=[ "python-docx>=0.8.10", "PyPDF2>=1.26.0", @@ -24,4 +27,17 @@ setup( ], }, python_requires=">=3.6", + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Education", + "Topic :: Office/Business", + ], ) \ No newline at end of file diff --git a/setup_qt_env.py b/setup_qt_env.py new file mode 100644 index 0000000..7f1f625 --- /dev/null +++ b/setup_qt_env.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +""" +Qt环境配置脚本,确保平台插件正确加载 +""" + +import os +import sys + +def setup_qt_environment(): + """设置Qt环境变量,避免平台插件问题""" + + # 获取虚拟环境的site-packages路径 + venv_path = os.environ.get('VIRTUAL_ENV', '') + if not venv_path: + print("警告:未检测到虚拟环境") + return False + + # 构建Qt插件路径 + python_version = f"python{sys.version_info.major}.{sys.version_info.minor}" + qt_plugin_path = os.path.join(venv_path, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins') + + if not os.path.exists(qt_plugin_path): + print(f"错误:Qt插件路径不存在: {qt_plugin_path}") + return False + + # 设置环境变量 + os.environ['QT_PLUGIN_PATH'] = qt_plugin_path + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(qt_plugin_path, 'platforms') + + # 对于macOS,还需要设置其他重要变量 + if sys.platform == 'darwin': + os.environ['QT_QPA_PLATFORM'] = 'cocoa' + # 禁用Qt的某些可能导致问题的功能 + os.environ['QT_MAC_WANTS_LAYER'] = '1' + os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1' + + print(f"✅ Qt环境变量设置完成") + print(f" QT_PLUGIN_PATH: {qt_plugin_path}") + print(f" QT_QPA_PLATFORM_PLUGIN_PATH: {os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']}") + + return True + +def verify_qt_setup(): + """验证Qt设置是否正确""" + try: + from PyQt5.QtCore import QCoreApplication + from PyQt5.QtWidgets import QApplication + + # 创建QApplication实例来测试 + app = QCoreApplication.instance() + if app is None: + app = QApplication([]) + + # 获取平台信息 + platform = app.platformName() + print(f"✅ Qt平台检测成功: {platform}") + + return True + except Exception as e: + print(f"❌ Qt验证失败: {e}") + return False + +if __name__ == "__main__": + if setup_qt_environment(): + verify_qt_setup() \ No newline at end of file diff --git a/src/deepseek_dialog_window.py b/src/deepseek_dialog_window.py new file mode 100644 index 0000000..22cf9f1 --- /dev/null +++ b/src/deepseek_dialog_window.py @@ -0,0 +1,627 @@ +""" +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_label = QLabel("DeepSeek AI对话助手") + title_font = QFont() + title_font.setPointSize(16) + title_font.setBold(True) + title_label.setFont(title_font) + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet("padding: 10px; background-color: #f0f0f0;") + main_layout.addWidget(title_label) + + # 分割器 + 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.setStyleSheet("color: #666; padding: 5px;") + status_layout.addWidget(self.status_label) + status_layout.addStretch() + + # API密钥管理按钮 + api_key_button = QPushButton("管理API密钥") + api_key_button.clicked.connect(self.manage_api_key) + status_layout.addWidget(api_key_button) + + main_layout.addLayout(status_layout) + + self.setLayout(main_layout) + + # 设置样式 + self.apply_theme("light") # 默认使用浅色主题 + + # 添加主题切换按钮 + theme_button = QPushButton("切换主题") + theme_button.clicked.connect(self.toggle_theme) + status_layout.addWidget(theme_button) + + # 连接信号 + self.streaming_finished.connect(self.on_streaming_finished) + + def toggle_theme(self): + """切换黑白主题""" + if hasattr(self, 'current_theme') and self.current_theme == "dark": + self.apply_theme("light") + else: + self.apply_theme("dark") + + def apply_theme(self, theme): + """应用主题样式""" + self.current_theme = theme + + if theme == "dark": + # 深色主题样式 + self.setStyleSheet(""" + QDialog { + background-color: #1e1e1e; + color: #ffffff; + } + QLabel { + color: #ffffff; + } + QTextEdit { + background-color: #2d2d2d; + color: #ffffff; + border: 1px solid #444; + border-radius: 4px; + padding: 10px; + font-family: 'Microsoft YaHei', sans-serif; + font-size: 12px; + } + QLineEdit { + background-color: #2d2d2d; + color: #ffffff; + border: 1px solid #444; + border-radius: 4px; + padding: 8px; + font-size: 12px; + } + QPushButton { + background-color: #0078d7; + color: white; + border: none; + border-radius: 4px; + padding: 8px 16px; + font-size: 12px; + } + QPushButton:hover { + background-color: #106ebe; + } + QPushButton:pressed { + background-color: #005a9e; + } + QScrollArea { + background-color: #1e1e1e; + border: none; + } + QScrollBar:vertical { + background-color: #2d2d2d; + width: 15px; + margin: 0px; + } + QScrollBar::handle:vertical { + background-color: #555; + border-radius: 7px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { + background-color: #666; + } + """) + else: + # 浅色主题样式 + self.setStyleSheet(""" + QDialog { + background-color: #ffffff; + color: #000000; + } + QLabel { + color: #000000; + } + QTextEdit { + background-color: #ffffff; + color: #000000; + border: 1px solid #ddd; + border-radius: 4px; + padding: 10px; + font-family: 'Microsoft YaHei', sans-serif; + font-size: 12px; + } + QLineEdit { + background-color: #ffffff; + color: #000000; + border: 1px solid #ddd; + border-radius: 4px; + padding: 8px; + font-size: 12px; + } + QPushButton { + background-color: #0078d7; + color: white; + border: none; + border-radius: 4px; + padding: 8px 16px; + font-size: 12px; + } + QPushButton:hover { + background-color: #106ebe; + } + QPushButton:pressed { + background-color: #005a9e; + } + QScrollArea { + background-color: #ffffff; + border: none; + } + QScrollBar:vertical { + background-color: #f0f0f0; + width: 15px; + margin: 0px; + } + QScrollBar::handle:vertical { + background-color: #c0c0c0; + border-radius: 7px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { + background-color: #a0a0a0; + } + """) + + 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() + + # 在新线程中执行流式请求 + import threading + 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助手的消息框架 + self.conversation_text.insertHtml( + f'
' + f'AI助手:
正在思考...' + ) + + # 自动滚动到底部 + 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): + """重新构建对话显示""" + html_content = "" + + for msg in self.conversation_history: + sender = msg["sender"] + message = msg["message"] + is_streaming = msg.get("streaming", False) + + # 根据发送者设置不同的样式 + if sender == "用户": + bg_color = "#e3f2fd" if self.current_theme == "light" else "#2d3e50" + text_color = "#000" if self.current_theme == "light" else "#fff" + elif sender == "AI助手": + bg_color = "#f5f5f5" if self.current_theme == "light" else "#3d3d3d" + text_color = "#000" if self.current_theme == "light" else "#fff" + else: # 系统消息 + bg_color = "#fff3cd" if self.current_theme == "light" else "#5d4e00" + text_color = "#856404" if self.current_theme == "light" else "#ffd700" + + # 格式化消息内容 + formatted_message = message.replace('\n', '
') if message else "正在思考..." + + html_content += f''' +
+ {sender}:
+ {formatted_message} +
+ ''' + + # 设置HTML内容 + self.conversation_text.setHtml(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() \ No newline at end of file diff --git a/src/file_parser.py b/src/file_parser.py index b6362d2..017b289 100644 --- a/src/file_parser.py +++ b/src/file_parser.py @@ -23,6 +23,8 @@ class FileParser: return FileParser.parse_docx(file_path) elif ext == '.pdf': return FileParser.parse_pdf(file_path) + elif ext == '.html': + return FileParser.parse_html(file_path) else: raise ValueError(f"Unsupported file format: {ext}") except Exception as e: @@ -80,6 +82,11 @@ class FileParser: content = FileParser.parse_pdf(file_path) images = [] # PDF图片提取较复杂,暂时跳过 + elif ext == '.html': + # HTML文件:提取文本内容 + content = FileParser.parse_html(file_path) + images = [] # HTML图片提取较复杂,暂时跳过 + else: return { 'success': False, @@ -271,6 +278,46 @@ class FileParser: except Exception as e: raise Exception(f"Error parsing pdf file {file_path}: {str(e)}") + @staticmethod + def parse_html(file_path: str) -> str: + """解析HTML文件,提取文本内容""" + # 验证文件路径 + if not FileParser.validate_file_path(file_path): + raise ValueError(f"Invalid file path: {file_path}") + + try: + from bs4 import BeautifulSoup + except ImportError: + raise ImportError("BeautifulSoup4 library is required for parsing .html files. Please install it using 'pip install beautifulsoup4'") + + try: + # 检测文件编码 + encoding = FileParser.detect_file_encoding(file_path) + + # 读取HTML文件 + with open(file_path, 'r', encoding=encoding, errors='ignore') as f: + html_content = f.read() + + # 使用BeautifulSoup解析HTML + soup = BeautifulSoup(html_content, 'html.parser') + + # 移除script和style标签 + for script in soup(["script", "style"]): + script.decompose() + + # 提取文本内容 + text = soup.get_text() + + # 清理多余的空白字符 + lines = (line.strip() for line in text.splitlines()) + chunks = (phrase.strip() for line in lines for phrase in line.split(" ")) + text = '\n'.join(chunk for chunk in chunks if chunk) + + return text + + except Exception as e: + raise Exception(f"Error parsing html file {file_path}: {str(e)}") + @staticmethod def validate_file_path(file_path: str) -> bool: """验证文件路径是否有效""" diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py index 1ec350b..4634578 100644 --- a/src/learning_mode_window.py +++ b/src/learning_mode_window.py @@ -4,17 +4,25 @@ import os from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QFrame, QMenuBar, QAction, QFileDialog, QMessageBox, QApplication, - QSplitter, QScrollArea, QStatusBar, QProgressBar) + QSplitter, QScrollArea, QStatusBar, QProgressBar, QTextBrowser, QSizePolicy, + QListWidget, QListWidgetItem, QDialog, QGraphicsScene, QGraphicsView) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor -from src.ui.components import CustomTitleBar, TextDisplayWidget -from src.typing_logic import TypingLogic -from src.file_parser import FileParser -from src.ui.theme_manager import theme_manager +# 修复导入路径 +from ui.components import CustomTitleBar, TextDisplayWidget +from typing_logic import TypingLogic +from file_parser import FileParser +from ui.theme_manager import theme_manager +import tempfile +import hashlib class LearningModeWindow(QMainWindow): - def __init__(self, parent=None, imported_content="", current_position=0): + # 定义内容变化信号 + content_changed = pyqtSignal(str, int) # 参数:内容,位置 + # 定义关闭信号 + closed = pyqtSignal() + def __init__(self, parent=None, imported_content="", current_position=0, image_data=None, image_positions=None): """ 学习模式窗口 - 顶部显示UI.png图片 @@ -25,13 +33,18 @@ class LearningModeWindow(QMainWindow): parent: 父窗口 imported_content: 从主窗口传递的导入内容 current_position: 当前学习进度位置 + image_data: 图片数据字典 {文件名: 二进制数据} + image_positions: 图片位置信息列表 """ super().__init__(parent) self.parent_window = parent self.imported_content = imported_content self.current_position = current_position + self.image_data = image_data or {} + self.image_positions = image_positions or [] self.typing_logic = None self.is_loading_file = False + self.extracted_images = [] # 用于存储提取的图片数据 # 初始化UI self.initUI() @@ -39,6 +52,9 @@ class LearningModeWindow(QMainWindow): # 初始化打字逻辑 self.init_typing_logic() + # 初始化同步位置跟踪 + self.last_sync_position = current_position + # 如果有导入内容,初始化显示 if self.imported_content: self.initialize_with_imported_content() @@ -53,6 +69,12 @@ class LearningModeWindow(QMainWindow): # 重置打字逻辑 if self.typing_logic: self.typing_logic.reset(self.imported_content) + + # 设置图片数据到打字逻辑 + if self.image_data: + self.typing_logic.set_image_data(self.image_data) + if self.image_positions: + self.typing_logic.set_image_positions(self.image_positions) # 显示已学习的内容 display_text = self.imported_content[:self.current_position] @@ -137,38 +159,48 @@ class LearningModeWindow(QMainWindow): ui_image_path = os.path.join(os.path.dirname(__file__), 'ui', 'UI.png') if os.path.exists(ui_image_path): pixmap = QPixmap(ui_image_path) - - # 保存原始图片尺寸 - self.original_pixmap = pixmap - - # 设置图片完全铺满标签 - self.image_label.setPixmap(pixmap) - self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签 - - # 设置图片标签的尺寸策略,使其可以扩展 - from PyQt5.QtWidgets import QSizePolicy - self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) - - # 设置图片区域的最小高度为图片高度的1/3,确保图片可见 - min_height = max(200, pixmap.height() // 3) - self.image_label.setMinimumHeight(min_height) - - # 重新设置窗口大小以适配图片 - self.resize(pixmap.width(), self.height()) + if not pixmap.isNull(): + # 保存原始图片用于缩放计算 + self.original_pixmap = pixmap + + # 设置图片到标签 + self.image_label.setPixmap(pixmap) + self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签 + + # 设置大小策略 + self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + # 计算合适的最小高度(保持图片比例) + window_width = 900 # 默认窗口宽度 + original_width = pixmap.width() + original_height = pixmap.height() + if original_width > 0: + min_height = int(window_width * original_height / original_width) + self.image_label.setMinimumHeight(min_height) + else: + self.image_label.setMinimumHeight(200) + else: + self.image_label.setText("UI图片加载失败") + self.image_label.setStyleSheet(""" + QLabel { + background-color: #f8f9fa; + color: #666666; + font-size: 14px; + qproperty-alignment: AlignCenter; + } + """) else: self.image_label.setText("UI图片未找到") self.image_label.setStyleSheet(""" QLabel { background-color: #f8f9fa; - border: none; color: #666666; font-size: 14px; - padding: 20px; + qproperty-alignment: AlignCenter; } """) - self.image_label.setMinimumHeight(200) - # 直接添加图片标签到主布局,不使用滚动区域 + # 添加到主布局 main_layout.addWidget(self.image_label) def resizeEvent(self, event): @@ -203,6 +235,7 @@ class LearningModeWindow(QMainWindow): 创建输入区域 - 创建文本显示组件 - 设置与主系统相同的样式 + - 创建图片列表区域 """ # 创建文本显示组件(复用主系统的组件) self.text_display_widget = TextDisplayWidget(self) @@ -219,7 +252,40 @@ class LearningModeWindow(QMainWindow): # 连接文本变化信号 self.text_display_widget.text_display.textChanged.connect(self.on_text_changed) - main_layout.addWidget(self.text_display_widget, 1) # 占据剩余空间 + # 创建图片显示区域 + self.image_list_widget = QListWidget() + self.image_list_widget.setMaximumHeight(150) + self.image_list_widget.setStyleSheet(""" + QListWidget { + background-color: #f8f8f8; + border: 1px solid #d0d0d0; + border-radius: 4px; + font-size: 11px; + } + QListWidget::item { + padding: 5px; + border-bottom: 1px solid #e0e0e0; + } + QListWidget::item:selected { + background-color: #e3f2fd; + color: #1976d2; + } + """) + self.image_list_widget.setVisible(False) # 默认隐藏 + self.image_list_widget.itemDoubleClicked.connect(self.on_image_item_double_clicked) + + # 创建布局容器 + input_container = QWidget() + input_layout = QVBoxLayout() + input_layout.setContentsMargins(0, 0, 0, 0) + input_layout.setSpacing(5) + + input_layout.addWidget(self.text_display_widget, 1) # 文本显示区域占据剩余空间 + input_layout.addWidget(self.image_list_widget) # 图片列表区域 + + input_container.setLayout(input_layout) + + main_layout.addWidget(input_container, 1) # 占据剩余空间 def create_menu_bar(self): """ @@ -294,38 +360,93 @@ class LearningModeWindow(QMainWindow): ) if file_path: + self.is_loading_file = True try: - self.is_loading_file = True - - # 使用文件解析器 - parser = FileParser() - content = parser.parse_file(file_path) + # 获取文件扩展名 + _, ext = os.path.splitext(file_path) + ext = ext.lower() - if content: - # 存储导入的内容 - self.imported_content = content - self.current_position = 0 - - # 重置打字逻辑 - if self.typing_logic: - self.typing_logic.reset(content) - - # 清空文本显示 - self.text_display_widget.text_display.clear() - - # 更新状态 - self.status_label.setText(f"已导入: {os.path.basename(file_path)}") - self.progress_label.setText(f"进度: 0% (0/{len(content)} 字符)") - - # 显示成功消息 - QMessageBox.information(self, "导入成功", - f"文件导入成功!\n文件: {os.path.basename(file_path)}\n字符数: {len(content)}\n\n开始打字以显示学习内容。") - + # 对于docx文件,直接解析而不转换为txt + if ext == '.docx': + # 直接解析docx文件内容 + content = FileParser.parse_docx(file_path) + # 提取图片数据 + images = FileParser.extract_images_from_docx(file_path) else: - QMessageBox.warning(self, "导入失败", "无法解析文件内容,请检查文件格式。") + # 其他文件类型使用原来的转换方法 + result = FileParser.parse_and_convert_to_txt(file_path) + content = result['content'] + images = result.get('images', []) + + if not content: + QMessageBox.warning(self, "导入失败", "文件内容为空或解析失败!") + return + + # 保存导入的内容 + self.imported_content = content + self.current_position = 0 + + # 保存提取的图片数据 + self.extracted_images = images + + # 设置打字逻辑 + if self.typing_logic: + self.typing_logic.reset(content) + # 如果有图片,设置图片数据到打字逻辑 + if images: + image_data_dict = {} + image_positions = [] + + # 为每张图片生成位置信息 - 改进位置计算逻辑 + for i, (filename, image_data) in enumerate(images): + image_data_dict[filename] = image_data + + # 改进图片位置计算,确保图片能在用户早期打字时显示 + content_length = len(content) + if content_length == 0: + content_length = 1000 # 备用长度 + + if len(images) == 1: + # 只有一张图片,放在文档开始位置附近(前10%),确保用户能快速看到 + image_pos = max(10, content_length // 10) + else: + # 多张图片:前几张放在较前位置,确保用户能看到 + if i < 3: + # 前3张图片放在文档前30% + segment = content_length // 3 + image_pos = max(10, segment * (i + 1) // 4) + else: + # 其余图片均匀分布 + remaining_start = content_length // 2 + remaining_index = i - 3 + remaining_count = len(images) - 3 + if remaining_count > 0: + segment = (content_length - remaining_start) // (remaining_count + 1) + image_pos = remaining_start + segment * (remaining_index + 1) + else: + image_pos = content_length // 2 + + image_positions.append({ + 'start_pos': image_pos, + 'end_pos': image_pos + 50, # 图片占位符长度 + 'filename': filename + }) + + # 设置图片数据到打字逻辑 + self.typing_logic.set_image_data(image_data_dict) + self.typing_logic.set_image_positions(image_positions) + + # 显示图片列表 + self.display_image_list(images) + + # 显示初始内容(空) + self.text_display_widget.text_display.clear() + self.status_label.setText("已导入文件,请开始打字学习...") + self.progress_label.setText("进度: 0%") + except Exception as e: - QMessageBox.critical(self, "导入错误", f"导入文件时出错:\n{str(e)}") + QMessageBox.critical(self, "导入错误", f"导入文件时发生错误:\n{str(e)}") finally: self.is_loading_file = False @@ -335,6 +456,8 @@ class LearningModeWindow(QMainWindow): 文本变化处理 - 根据导入的内容逐步显示 - 更新学习进度 + - 同步内容到打字模式 + - 处理图片插入 """ # 如果正在加载文件,跳过处理 if self.is_loading_file: @@ -382,6 +505,7 @@ class LearningModeWindow(QMainWindow): self.status_label.setText(f"输入错误!期望字符: '{result.get('expected', '')}'") else: # 输入正确,更新进度 + old_position = self.current_position self.current_position = len(current_text) progress = (self.current_position / len(self.imported_content)) * 100 @@ -389,6 +513,17 @@ class LearningModeWindow(QMainWindow): f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)" ) + + + # 只在用户新输入的字符上同步到打字模式 + if self.parent_window and hasattr(self.parent_window, 'text_edit'): + # 获取用户这一轮新输入的字符(与上一轮相比的新内容) + if old_position < self.current_position: + new_input = expected_text[old_position:self.current_position] + if new_input: # 只有新输入内容时才同步 + # 只同步新输入的内容,不传递整个文本 + self.content_changed.emit(new_input, len(new_input)) + # 检查是否完成 if result.get('completed', False): self.status_label.setText("恭喜!学习完成!") @@ -396,6 +531,8 @@ class LearningModeWindow(QMainWindow): else: self.status_label.setText("继续输入以显示更多内容...") + + def show_about(self): """ 显示关于对话框 @@ -406,7 +543,8 @@ class LearningModeWindow(QMainWindow): "• 顶部显示UI界面图片\n" "• 下方为打字输入区域\n" "• 导入文件后逐步显示内容\n" - "• 实时显示学习进度\n\n" + "• 实时显示学习进度\n" + "• 支持图片显示\n\n" "使用方法:\n" "1. 点击'文件'->'导入文件'选择学习材料\n" "2. 在下方文本区域开始打字\n" @@ -417,6 +555,9 @@ class LearningModeWindow(QMainWindow): 窗口关闭事件 - 通知父窗口学习模式已关闭 """ + # 发射关闭信号 + self.closed.emit() + if self.parent_window and hasattr(self.parent_window, 'on_learning_mode_closed'): self.parent_window.on_learning_mode_closed() @@ -431,4 +572,152 @@ class LearningModeWindow(QMainWindow): if event.key() == Qt.Key_Escape: self.close() else: - super().keyPressEvent(event) \ No newline at end of file + 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() \ No newline at end of file diff --git a/src/main.py b/src/main.py index b119247..56b0016 100644 --- a/src/main.py +++ b/src/main.py @@ -8,59 +8,74 @@ import platform project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, project_root) -# 设置Qt平台插件路径 - 根据操作系统设置正确的Qt插件路径 +# 设置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环境下查找Qt插件路径 - # 首先检查虚拟环境中的Qt插件 - venv_qt_plugins_path = os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins') - if os.path.exists(venv_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path - return - - # 检查全局Python安装中的Qt插件 - global_qt_plugins_path = os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins') - if os.path.exists(global_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = global_qt_plugins_path - return - - # 尝试在常见的Windows PyQt5安装路径中查找 - common_paths = [ + # 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'), - ] - - for path in common_paths: - if os.path.exists(path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = path - return - + ]) elif system == "Darwin": # macOS - # macOS环境下查找Qt插件路径 - system_qt_plugins_path = '/usr/local/opt/qt5/plugins' # macOS Homebrew Qt5路径 - venv_qt_plugins_path = os.path.join(project_root, '.venv', 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5', 'plugins') - - # 优先检查系统Qt插件路径 - if os.path.exists(system_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = system_qt_plugins_path - return - elif os.path.exists(venv_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path - return - + # 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环境下查找Qt插件路径 - venv_qt_plugins_path = os.path.join(project_root, '.venv', 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5', 'plugins') - global_qt_plugins_path = os.path.join(sys.prefix, 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5', 'plugins') + # 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' - if os.path.exists(venv_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path - return - elif os.path.exists(global_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = global_qt_plugins_path - return + print(f"✅ Qt插件路径设置成功: {valid_path}") + return True + else: + print("⚠️ 警告:未找到Qt插件路径") + return False # 设置Qt平台插件路径 set_qt_plugin_path() @@ -80,12 +95,14 @@ def main(): # 创建QApplication实例 app = QApplication(sys.argv) - # 设置应用程序样式为Windows风格,更接近Word界面 - app.setStyle('WindowsVista') + # 在macOS上使用系统原生样式,在其他平台上使用WindowsVista样式 + if platform.system() != "Darwin": # 不是macOS系统 + # 设置应用程序样式为Windows风格,更接近Word界面 + app.setStyle('WindowsVista') # 设置应用程序属性 app.setApplicationName("MagicWord") - app.setApplicationVersion("0.2.2") + app.setApplicationVersion("1.0.0") app.setOrganizationName("MagicWord") # 设置窗口图标(如果存在) diff --git a/src/main_window.py b/src/main_window.py index 6c8afa6..0dc3bd0 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -1,482 +1,9 @@ -import sys -import os -from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, QAction, - QFileDialog, QVBoxLayout, QWidget, QLabel, QStatusBar, QMessageBox) -from PyQt5.QtGui import QFont, QTextCharFormat, QColor, QTextCursor -from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal - -# 添加项目根目录到Python路径 -sys.path.append(os.path.join(os.path.dirname(__file__), '..')) - -# 导入自定义UI组件 -from src.ui.components import CustomTitleBar, ProgressBarWidget, TextDisplayWidget, StatsDisplayWidget, QuoteDisplayWidget, WeatherDisplayWidget -from src.file_parser import FileParser -from src.typing_logic import TypingLogic -from src.services.network_service import NetworkService - -class WeatherFetchThread(QThread): - """天气信息获取线程""" - weather_fetched = pyqtSignal(object) # 天气信息获取成功信号 - error_occurred = pyqtSignal(str) # 错误发生信号 - - def __init__(self): - super().__init__() - self.network_service = NetworkService() - - def run(self): - try: - weather_info = self.network_service.get_weather_info() - if weather_info: - # 格式化天气信息 - formatted_info = ( - f"天气: {weather_info['city']} - " - f"{weather_info['description']} - " - f"温度: {weather_info['temperature']}°C - " - f"湿度: {weather_info['humidity']}% - " - f"风速: {weather_info['wind_speed']} m/s" - ) - self.weather_fetched.emit(formatted_info) - else: - self.error_occurred.emit("无法获取天气信息") - except Exception as e: - self.error_occurred.emit(f"获取天气信息时出错: {str(e)}") - -class MainWindow(QMainWindow): - def __init__(self): +def on_learning_mode_closed(self): """ - 初始化主窗口 - - 设置窗口标题为"隐私学习软件 - 仿Word" - - 设置窗口大小为800x600 - - 初始化学习内容存储变量 - - 初始化当前输入位置 - - 调用initUI()方法 + 学习模式窗口关闭回调 + - 清除学习窗口引用 + - 更新菜单状态 """ - super().__init__() - self.learning_content = "" - self.current_position = 0 - self.typing_logic = None - self.text_edit = None - self.status_bar = None - self.title_bar = None - self.progress_bar_widget = None - self.text_display_widget = None - self.initUI() - - def initUI(self): - """ - 创建和布局所有UI组件 - - 创建自定义标题栏 - - 创建文本显示组件 - - 调用createMenuBar()创建菜单 - - 创建状态栏并显示"就绪" - """ - # 设置窗口属性 - self.setWindowTitle("隐私学习软件 - 仿Word") - self.setGeometry(100, 100, 800, 600) - self.setWindowFlags(Qt.FramelessWindowHint) # 移除默认标题栏 - - # 创建中央widget - central_widget = QWidget() - self.setCentralWidget(central_widget) - - # 创建主布局 - main_layout = QVBoxLayout() - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - central_widget.setLayout(main_layout) - - # 创建自定义标题栏 - self.title_bar = CustomTitleBar(self) - main_layout.addWidget(self.title_bar) - - # 创建统计信息显示组件(默认隐藏) - self.stats_display = StatsDisplayWidget(self) - self.stats_display.setVisible(False) # 默认隐藏 - main_layout.addWidget(self.stats_display) - - # 创建每日一言显示组件(默认隐藏) - self.quote_display = QuoteDisplayWidget(self) - self.quote_display.setVisible(False) # 默认隐藏 - main_layout.addWidget(self.quote_display) - - # 创建天气显示组件(默认隐藏) - self.weather_display = WeatherDisplayWidget(self) - self.weather_display.setVisible(False) # 默认隐藏 - main_layout.addWidget(self.weather_display) - - # 创建文本显示组件 - self.text_display_widget = TextDisplayWidget(self) - main_layout.addWidget(self.text_display_widget) - - # 连接文本显示组件的文本变化信号 - self.text_display_widget.text_display.textChanged.connect(self.onTextChanged) - - # 创建菜单栏 - self.createMenuBar() - - # 创建状态栏 - self.status_bar = self.statusBar() - self.status_bar.showMessage("就绪") - - def createTopFunctionArea(self, main_layout): - """ - 创建顶部功能区域 - - 显示准确率、WPM等统计信息 - - 显示每日一言功能 - """ - from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton - from PyQt5.QtCore import Qt - - # 创建顶部功能区域widget - top_widget = QWidget() - top_widget.setStyleSheet(""" - QWidget { - background-color: #f0f0f0; - border-bottom: 1px solid #d0d0d0; - } - """) - - # 创建水平布局 - top_layout = QHBoxLayout() - top_layout.setContentsMargins(10, 5, 10, 5) - top_layout.setSpacing(15) - - # 创建统计信息标签 - self.wpm_label = QLabel("WPM: 0") - self.accuracy_label = QLabel("准确率: 0%") - self.quote_label = QLabel("每日一言: 暂无") - self.quote_label.setStyleSheet("QLabel { color: #666666; font-style: italic; }") - - # 设置标签样式 - label_style = "font-size: 12px; font-weight: normal; color: #333333;" - self.wpm_label.setStyleSheet(label_style) - self.accuracy_label.setStyleSheet(label_style) - - # 创建每日一言刷新按钮 - self.refresh_quote_button = QPushButton("刷新") - self.refresh_quote_button.setStyleSheet(""" - QPushButton { - background-color: #0078d7; - color: white; - border: none; - padding: 5px 10px; - border-radius: 3px; - font-size: 12px; - } - QPushButton:hover { - background-color: #005a9e; - } - """) - self.refresh_quote_button.clicked.connect(self.refresh_daily_quote) - - # 添加组件到布局 - top_layout.addWidget(self.wpm_label) - top_layout.addWidget(self.accuracy_label) - top_layout.addStretch() - top_layout.addWidget(self.quote_label) - top_layout.addWidget(self.refresh_quote_button) - - top_widget.setLayout(top_layout) - main_layout.addWidget(top_widget) - - def createMenuBar(self): - """ - 创建菜单栏和所有菜单项 - - 文件菜单:打开(Ctrl+O)、保存(Ctrl+S)、退出(Ctrl+Q) - - 视图菜单:显示统计信息、显示每日一言 - - 帮助菜单:关于 - - 为每个菜单项连接对应的槽函数 - """ - menu_bar = self.menuBar() - - # 文件菜单 - file_menu = menu_bar.addMenu('文件') - - # 打开动作 - open_action = QAction('打开', self) - open_action.setShortcut('Ctrl+O') - open_action.triggered.connect(self.openFile) - file_menu.addAction(open_action) - - # 保存动作 - save_action = QAction('保存', self) - save_action.setShortcut('Ctrl+S') - save_action.triggered.connect(self.saveFile) - file_menu.addAction(save_action) - - # 分隔线 - file_menu.addSeparator() - - # 退出动作 - exit_action = QAction('退出', self) - exit_action.setShortcut('Ctrl+Q') - exit_action.triggered.connect(self.close) - file_menu.addAction(exit_action) - - # 视图菜单 - view_menu = menu_bar.addMenu('视图') - - # 显示统计信息动作 - self.stats_action = QAction('显示统计信息', self) - self.stats_action.setCheckable(True) - self.stats_action.setChecked(True) - self.stats_action.triggered.connect(self.toggleStatsDisplay) - view_menu.addAction(self.stats_action) - - # 显示每日一言动作 - self.quote_action = QAction('显示每日一言', self) - self.quote_action.setCheckable(True) - self.quote_action.setChecked(True) - self.quote_action.triggered.connect(self.toggleQuoteDisplay) - view_menu.addAction(self.quote_action) - - # 显示天气信息动作 - self.weather_action = QAction('显示天气', self) - self.weather_action.setCheckable(True) - self.weather_action.setChecked(True) - self.weather_action.triggered.connect(self.toggleWeatherDisplay) - view_menu.addAction(self.weather_action) - - # 帮助菜单 - help_menu = menu_bar.addMenu('帮助') - - # 关于动作 - about_action = QAction('关于', self) - about_action.triggered.connect(self.showAbout) - help_menu.addAction(about_action) - - def toggleStatsDisplay(self, checked): - """ - 切换统计信息显示 - - checked: 是否显示统计信息 - """ - self.stats_display.setVisible(checked) - - def toggleQuoteDisplay(self, checked): - """ - 切换每日一言显示 - - checked: 是否显示每日一言 - """ - self.quote_display.setVisible(checked) - # 如果启用显示且quote为空,则刷新一次 - if checked and not self.quote_display.quote_label.text(): - self.refresh_daily_quote() - - def toggleWeatherDisplay(self, checked): - """切换天气信息显示""" - self.weather_display.setVisible(checked) - # 如果启用显示且天气信息为空,则刷新一次 - if checked and not self.weather_display.weather_label.text(): - self.refresh_weather_info() - - def openFile(self): - """ - 打开文件选择对话框并加载选中的文件 - - 显示文件选择对话框,过滤条件:*.txt, *.docx, *.pdf - - 如果用户选择了文件,调用FileParser.parse_file(file_path) - - 成功时:将内容存储但不直接显示,重置打字状态 - - 失败时:显示错误消息框 - """ - options = QFileDialog.Options() - file_path, _ = QFileDialog.getOpenFileName( - self, - "打开文件", - "", - "文本文件 (*.txt);;Word文档 (*.docx);;PDF文件 (*.pdf);;所有文件 (*)", - options=options - ) - - if file_path: - try: - # 解析文件内容 - content = FileParser.parse_file(file_path) - self.learning_content = content - - # 在文本显示组件中设置内容(初始为空,通过打字逐步显示) - if self.text_display_widget: - self.text_display_widget.set_text(content) # 设置文件内容 - - # 重置打字状态 - self.typing_logic = TypingLogic(content) - self.current_position = 0 - - # 更新状态栏 - self.status_bar.showMessage(f"已打开文件: {file_path},开始打字以显示内容") - except Exception as e: - # 显示错误消息框 - QMessageBox.critical(self, "错误", f"无法打开文件:\n{str(e)}") - - def saveFile(self): - """ - 保存当前内容到文件 - - 显示保存文件对话框 - - 将文本区域内容写入选定文件 - - 返回操作结果 - """ - options = QFileDialog.Options() - file_path, _ = QFileDialog.getSaveFileName( - self, - "保存文件", - "", - "文本文件 (*.txt);;所有文件 (*)", - options=options - ) - - if file_path: - try: - # 获取文本编辑区域的内容 - content = self.text_edit.toPlainText() - - # 写入文件 - with open(file_path, 'w', encoding='utf-8') as f: - f.write(content) - - # 更新状态栏 - self.status_bar.showMessage(f"文件已保存: {file_path}") - - return True - except Exception as e: - # 显示错误消息框 - QMessageBox.critical(self, "错误", f"无法保存文件:\n{str(e)}") - return False - - return False - - def showAbout(self): - """ - 显示关于对话框 - - 显示消息框,包含软件名称、版本、描述 - """ - QMessageBox.about( - self, - "关于", - "隐私学习软件 - 仿Word\n\n" - "版本: 1.0\n\n" - "这是一个用于隐私学习的打字练习软件,\n" - "可以加载文档并进行打字练习,\n" - "帮助提高打字速度和准确性。" - ) - - def refresh_daily_quote(self): - """ - 刷新每日一言 - - 从网络API获取名言 - - 更新显示 - """ - import requests - import json - from PyQt5.QtCore import Qt - from src.constants import QUOTE_API_URL - - try: - # 发送请求获取每日一言 - response = requests.get(QUOTE_API_URL, timeout=5) - if response.status_code == 200: - data = response.json() - quote_content = data.get('content', '暂无内容') - quote_author = data.get('author', '未知作者') - - # 更新显示 - self.quote_label.setText(f"每日一言: {quote_content} — {quote_author}") - - # 同时更新统计信息显示组件中的每日一言 - if hasattr(self, 'stats_display') and self.stats_display: - self.stats_display.update_quote(f"{quote_content} — {quote_author}") - else: - self.quote_label.setText("每日一言: 获取失败") - # 同时更新统计信息显示组件中的每日一言 - if hasattr(self, 'stats_display') and self.stats_display: - self.stats_display.update_quote("获取失败") - except Exception as e: - self.quote_label.setText("每日一言: 获取失败") - # 同时更新统计信息显示组件中的每日一言 - if hasattr(self, 'stats_display') and self.stats_display: - self.stats_display.update_quote("获取失败") - - def onTextChanged(self): - """ - 处理用户输入变化事件(打字练习) - - 获取文本显示组件中的文本 - - 使用TypingLogic.check_input检查输入 - - 根据结果更新文本显示组件 - - 更新统计数据展示 - """ - # 防止递归调用 - if getattr(self, '_processing_text_change', False): - return - - if not self.typing_logic: - return - - # 设置标志防止递归 - self._processing_text_change = True - - try: - # 获取当前输入文本 - current_text = self.text_display_widget.text_display.toPlainText() - - # 检查输入是否正确 - result = self.typing_logic.check_input(current_text) - is_correct = result["correct"] - expected_char = result["expected"] - - # 更新文本显示组件 - if self.text_display_widget: - # 显示用户输入反馈 - self.text_display_widget.show_user_input(current_text) - - # 不再高亮下一个字符,因为内容通过打字逐步显示 - - # 计算统计数据 - stats = self.typing_logic.get_statistics() - accuracy = stats['accuracy_rate'] * 100 # 转换为百分比 - # 可以根据需要添加更多统计数据的计算 - wpm = 0 # 暂时设置为0,后续可以实现WPM计算 - - # 更新状态栏 - self.status_bar.showMessage(f"WPM: {wpm:.1f} | 准确率: {accuracy:.1f}%") - - # 更新统计信息显示组件 - if hasattr(self, 'stats_display') and self.stats_display.isVisible(): - self.stats_display.update_stats(int(wpm), accuracy) - - # 更新每日一言显示组件(如果需要) - if hasattr(self, 'quote_display') and self.quote_display.isVisible() and not self.quote_display.quote_label.text(): - self.refresh_daily_quote() - - # 更新顶部功能区的统计数据(如果仍然存在) - if hasattr(self, 'wpm_label') and self.wpm_label: - self.wpm_label.setText(f"WPM: {wpm:.1f}") - if hasattr(self, 'accuracy_label') and self.accuracy_label: - self.accuracy_label.setText(f"准确率: {accuracy:.1f}%") - finally: - # 清除递归防止标志 - self._processing_text_change = False - - def refresh_daily_quote(self): - """刷新每日一言""" - # 创建并启动获取名言的线程 - self.quote_thread = QuoteFetchThread() - self.quote_thread.quote_fetched.connect(self.on_quote_fetched) - self.quote_thread.error_occurred.connect(self.on_quote_error) - self.quote_thread.start() - - def refresh_weather_info(self): - """刷新天气信息""" - # 创建并启动获取天气信息的线程 - self.weather_thread = WeatherFetchThread() - self.weather_thread.weather_fetched.connect(self.on_weather_fetched) - self.weather_thread.error_occurred.connect(self.on_weather_error) - self.weather_thread.start() - - def on_weather_fetched(self, weather_info): - """处理天气信息获取成功""" - # 更新天气显示组件 - if hasattr(self, 'weather_display') and self.weather_display: - self.weather_display.update_weather(weather_info) - - def on_weather_error(self, error_msg): - """处理天气信息获取错误""" - # 更新天气显示组件 - if hasattr(self, 'weather_display') and self.weather_display: - self.weather_display.update_weather(error_msg) \ No newline at end of file + self.learning_window = None + self.learning_mode_action.setChecked(False) + self.typing_mode_action.setChecked(True) \ No newline at end of file diff --git a/src/services/network_service.py b/src/services/network_service.py index 60e7f4a..e67dedc 100644 --- a/src/services/network_service.py +++ b/src/services/network_service.py @@ -51,7 +51,61 @@ class NetworkService: # 5. 返回天气信息字典 return formatted_weather else: - # 模拟天气数据(无API密钥时) + # 当没有API密钥时,使用免费的天气API获取真实数据 + # 首先尝试获取城市ID(需要映射城市名到ID) + city_id_map = { + "Beijing": "101010100", + "Shanghai": "101020100", + "Tianjin": "101030100", + "Chongqing": "101040100", + "Hong Kong": "101320101", + "Macau": "101330101" + } + + # 尝试映射英文城市名到ID + city_id = city_id_map.get(city) + + # 如果找不到映射,尝试直接使用城市名 + if not city_id: + # 对于中国主要城市,直接使用拼音映射 + city_pinyin_map = { + "Beijing": "北京", + "Shanghai": "上海", + "Tianjin": "天津", + "Chongqing": "重庆" + } + chinese_city = city_pinyin_map.get(city, city) + + # 使用免费天气API + try: + # 使用和风天气免费API的替代方案 - sojson天气API + weather_url = f"http://t.weather.sojson.com/api/weather/city/101010100" # 默认北京 + weather_response = self.session.get(weather_url, timeout=5, verify=False) + weather_data = weather_response.json() + + 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", "晴") + + 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" # 默认风速 + } + return formatted_weather + except Exception as e: + print(f"获取免费天气数据时出错: {e}") + + # 如果以上都失败,返回默认数据 return { "city": city, "temperature": 20, diff --git a/src/ui/calendar_floating_widget.py b/src/ui/calendar_floating_widget.py new file mode 100644 index 0000000..f8ee8ca --- /dev/null +++ b/src/ui/calendar_floating_widget.py @@ -0,0 +1,482 @@ +# -*- 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): + """应用主题样式""" + 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: 10px; + }} + QLabel {{ + color: {colors['text']}; + background-color: transparent; + padding: 4px 6px; + margin: 2px; + }} + QLabel#dateLabel {{ + color: {colors['text_secondary']}; + font-size: 11px; + 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#todayButton, QPushButton#clearButton, QPushButton#insertButton {{ + background-color: {colors['accent']}; + color: white; + border: none; + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; + font-weight: 500; + }} + QPushButton#todayButton:hover, QPushButton#clearButton:hover, QPushButton#insertButton:hover {{ + background-color: {colors['accent_hover']}; + }} + """) + + # 更新日历控件样式 + self.calendar.setStyleSheet(f""" + QCalendarWidget {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 4px; + }} + QCalendarWidget QToolButton {{ + height: 30px; + width: 80px; + color: {colors['text']}; + font-size: 12px; + font-weight: bold; + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 4px; + }} + QCalendarWidget QToolButton:hover {{ + background-color: {colors['surface_hover']}; + }} + QCalendarWidget QMenu {{ + width: 150px; + left: 20px; + color: {colors['text']}; + font-size: 12px; + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + }} + QCalendarWidget QSpinBox {{ + width: 80px; + font-size: 12px; + background-color: {colors['surface']}; + selection-background-color: {colors['accent']}; + selection-color: white; + border: 1px solid {colors['border']}; + border-radius: 4px; + 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 QWidget#qt_calendar_navigationbar {{ + background-color: {colors['surface']}; + }} + """) + else: + # 浅色主题样式 + 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: 11px; + 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#todayButton, QPushButton#clearButton, QPushButton#insertButton {{ + background-color: {colors['accent']}; + color: white; + border: none; + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; + font-weight: 500; + }} + QPushButton#todayButton:hover, QPushButton#clearButton:hover, QPushButton#insertButton:hover {{ + background-color: {colors['accent_hover']}; + }} + """) + + # 更新日历控件样式 + self.calendar.setStyleSheet(f""" + QCalendarWidget {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 4px; + }} + QCalendarWidget QToolButton {{ + height: 30px; + width: 80px; + color: {colors['text']}; + font-size: 12px; + font-weight: bold; + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 4px; + }} + QCalendarWidget QToolButton:hover {{ + background-color: {colors['surface_hover']}; + }} + QCalendarWidget QMenu {{ + width: 150px; + left: 20px; + color: {colors['text']}; + font-size: 12px; + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + }} + QCalendarWidget QSpinBox {{ + width: 80px; + font-size: 12px; + background-color: {colors['surface']}; + selection-background-color: {colors['accent']}; + selection-color: white; + border: 1px solid {colors['border']}; + border-radius: 4px; + 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 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() \ No newline at end of file diff --git a/src/ui/calendar_widget.py b/src/ui/calendar_widget.py new file mode 100644 index 0000000..5331faf --- /dev/null +++ b/src/ui/calendar_widget.py @@ -0,0 +1,573 @@ +# -*- 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) + + # 应用当前主题 + self.apply_theme() + + def apply_theme(self): + """应用主题样式""" + is_dark = theme_manager.is_dark_theme() + + if is_dark: + # 深色主题样式 + self.setStyleSheet(""" + QWidget { + background-color: #2c2c2e; + color: #f0f0f0; + } + """) + + # 更新日历控件样式 + self.calendar.setStyleSheet(""" + QCalendarWidget { + background-color: #2c2c2e; + border: 1px solid #404040; + border-radius: 4px; + } + QCalendarWidget QToolButton { + height: 30px; + width: 80px; + color: #f0f0f0; + font-size: 12px; + font-weight: bold; + background-color: #3a3a3c; + border: 1px solid #4a4a4c; + border-radius: 4px; + } + QCalendarWidget QToolButton:hover { + background-color: #4a4a4c; + } + QCalendarWidget QMenu { + width: 150px; + left: 20px; + color: #f0f0f0; + font-size: 12px; + background-color: #3a3a3c; + border: 1px solid #4a4a4c; + } + QCalendarWidget QSpinBox { + width: 80px; + font-size: 12px; + background-color: #3a3a3c; + selection-background-color: #0a84ff; + selection-color: #ffffff; + border: 1px solid #4a4a4c; + border-radius: 4px; + color: #f0f0f0; + } + 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: #3a3a3c; + } + QCalendarWidget QAbstractItemView:enabled { + font-size: 12px; + selection-background-color: #0a84ff; + selection-color: #ffffff; + background-color: #2c2c2e; + color: #f0f0f0; + } + QCalendarWidget QWidget#qt_calendar_navigationbar { + background-color: #3a3a3c; + } + """) + + # 更新标签样式 + self.date_label.setStyleSheet("QLabel { color: #a0a0a0; }") + + # 更新按钮样式 + self.close_btn.setStyleSheet(""" + QPushButton { + background-color: #3a3a3c; + border: 1px solid #4a4a4c; + border-radius: 12px; + font-size: 16px; + font-weight: bold; + color: #f0f0f0; + } + QPushButton:hover { + background-color: #4a4a4c; + } + """) + + self.today_btn.setStyleSheet(""" + QPushButton { + background-color: #0a84ff; + color: white; + border: none; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #0066cc; + } + """) + + self.clear_btn.setStyleSheet(""" + QPushButton { + background-color: #3a3a3c; + color: #f0f0f0; + border: 1px solid #4a4a4c; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #4a4a4c; + } + """) + + self.insert_btn.setStyleSheet(""" + QPushButton { + background-color: #32d74b; + color: #000000; + border: none; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #24b334; + } + """) + else: + # 浅色主题样式 + self.setStyleSheet(""" + QWidget { + background-color: white; + color: #333333; + } + """) + + # 更新日历控件样式 + 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: #333; + font-size: 12px; + background-color: white; + border: 1px solid #ccc; + } + QCalendarWidget QSpinBox { + width: 80px; + font-size: 12px; + background-color: #f0f0f0; + selection-background-color: #0078d7; + selection-color: white; + border: 1px solid #ccc; + border-radius: 4px; + color: #333; + } + 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; + background-color: white; + color: #333; + } + QCalendarWidget QWidget#qt_calendar_navigationbar { + background-color: #f8f8f8; + } + """) + + # 更新标签样式 + self.date_label.setStyleSheet("QLabel { color: #666; }") + + # 更新按钮样式 + self.close_btn.setStyleSheet(""" + QPushButton { + background-color: #f0f0f0; + border: 1px solid #ccc; + border-radius: 12px; + font-size: 16px; + font-weight: bold; + color: #333; + } + QPushButton:hover { + background-color: #e0e0e0; + } + """) + + 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.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.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + border: none; + border-radius: 4px; + padding: 5px 10px; + } + QPushButton:hover { + background-color: #45a049; + } + """) + + def on_theme_changed(self, is_dark): + """主题切换槽函数""" + self.apply_theme() + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + + # 创建并显示日历组件 + calendar = CalendarWidget() + calendar.show() + + sys.exit(app.exec_()) \ No newline at end of file diff --git a/src/ui/quote_floating_widget.py b/src/ui/quote_floating_widget.py new file mode 100644 index 0000000..ad37bfe --- /dev/null +++ b/src/ui/quote_floating_widget.py @@ -0,0 +1,439 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +每日谏言悬浮窗口 +""" + +import sys +from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QPushButton, QFrame, QGraphicsDropShadowEffect) +from PyQt5.QtCore import Qt, QPoint, pyqtSignal +from PyQt5.QtGui import QFont, QColor + + +class QuoteFloatingWidget(QWidget): + """每日谏言悬浮窗口""" + + # 定义信号 + closed = pyqtSignal() # 窗口关闭信号 + refresh_requested = pyqtSignal() # 刷新请求信号 + insert_requested = pyqtSignal(str) # 插入请求信号,传递要插入的文本 + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool) + self.setAttribute(Qt.WA_TranslucentBackground) + + # 初始化变量 + self.is_dragging = False + self.drag_position = QPoint() + + # 设置默认谏言数据 + self.quote_data = { + "quote": "书山有路勤为径,学海无涯苦作舟。", + "author": "韩愈", + "source": "《古今贤文·劝学篇》" + } + + # 初始化UI + self.init_ui() + self.setup_styles() + self.apply_theme(is_dark=True) # 默认使用深色主题 + + def init_ui(self): + """初始化UI""" + # 主框架 + self.main_frame = QFrame(self) + self.main_frame.setObjectName("mainFrame") + self.main_frame.setFixedSize(360, 200) # 设置窗口大小 + + main_layout = QVBoxLayout(self.main_frame) + main_layout.setContentsMargins(12, 12, 12, 12) + main_layout.setSpacing(8) + + # 标题栏 + title_layout = QHBoxLayout() + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(0) + + self.title_label = QLabel("每日谏言") + self.title_label.setFont(QFont("Arial", 12, QFont.Bold)) + title_layout.addWidget(self.title_label) + title_layout.addStretch() + # 添加一个小的固定空间,使关闭按钮向左移动 + title_layout.addSpacing(25) # 向左移动25个单位 + + # 关闭按钮 + self.close_btn = QPushButton("×") + self.close_btn.setFixedSize(20, 20) + self.close_btn.setObjectName("closeButton") + self.close_btn.clicked.connect(self.close_window) + title_layout.addWidget(self.close_btn) + + main_layout.addLayout(title_layout) + + # 分隔线 + separator = QFrame() + separator.setObjectName("separator") + separator.setFixedHeight(1) + main_layout.addWidget(separator) + + # 谏言内容区域 + content_layout = QVBoxLayout() + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(6) + + # 谏言文本 + self.quote_label = QLabel() + self.quote_label.setObjectName("quoteLabel") + self.quote_label.setWordWrap(True) + self.quote_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + content_layout.addWidget(self.quote_label) + + # 作者信息 + self.author_label = QLabel() + self.author_label.setObjectName("authorLabel") + self.author_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + content_layout.addWidget(self.author_label) + + main_layout.addLayout(content_layout) + main_layout.addStretch() + + # 底部按钮区域 + bottom_layout = QHBoxLayout() + bottom_layout.setContentsMargins(0, 0, 0, 0) + bottom_layout.setSpacing(8) + + # 刷新按钮 + self.refresh_btn = QPushButton("换一句") + self.refresh_btn.setObjectName("refreshButton") + self.refresh_btn.clicked.connect(self.on_refresh_clicked) + bottom_layout.addWidget(self.refresh_btn) + + bottom_layout.addStretch() + + # 插入按钮 + self.insert_btn = QPushButton("插入") + self.insert_btn.setObjectName("insertButton") + self.insert_btn.clicked.connect(self.on_insert_clicked) + bottom_layout.addWidget(self.insert_btn) + + main_layout.addLayout(bottom_layout) + + # 设置主布局 + outer_layout = QVBoxLayout(self) + outer_layout.setContentsMargins(0, 0, 0, 0) + outer_layout.addWidget(self.main_frame) + + # 更新显示 + self.update_quote() + + def setup_styles(self): + """设置样式""" + pass # 样式将在apply_theme中设置 + + def apply_theme(self, is_dark=True): + """应用主题""" + if is_dark: + # 深色主题配色 + colors = { + 'surface': '#2d2d2d', + 'border': '#444444', + 'text': '#ffffff', + 'text_secondary': '#cccccc', + 'accent': '#4CAF50', + 'accent_hover': '#45a049', + 'button_hover': '#555555', + 'error': '#f44336' + } + + self.main_frame.setStyleSheet(f""" + QFrame#mainFrame {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + }} + QLabel {{ + color: {colors['text']}; + background-color: transparent; + padding: 4px 6px; + margin: 2px; + }} + QLabel#quoteLabel {{ + color: {colors['text']}; + font-size: 14px; + font-weight: 500; + padding: 6px 8px; + margin: 3px; + }} + QLabel#authorLabel {{ + color: {colors['text_secondary']}; + font-size: 12px; + font-style: italic; + padding: 4px 6px; + margin: 2px; + }} + QFrame#separator {{ + background-color: {colors['border']}; + }} + QPushButton#closeButton {{ + background-color: rgba(255, 255, 255, 0.1); + border: none; + color: {colors['text']}; + font-size: 18px; + font-weight: bold; + border-radius: 6px; + padding: 2px 4px; + }} + QPushButton#closeButton:hover {{ + background-color: #e81123; + color: white; + border-radius: 6px; + }} + QPushButton#refreshButton, QPushButton#insertButton {{ + background-color: {colors['accent']}; + color: white; + border: none; + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; + font-weight: 500; + }} + QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{ + background-color: {colors['accent_hover']}; + }} + """) + else: + # 浅色主题配色 + colors = { + 'surface': '#ffffff', + 'border': '#dddddd', + 'text': '#333333', + 'text_secondary': '#666666', + 'accent': '#4CAF50', + 'accent_hover': '#45a049', + 'button_hover': '#f0f0f0', + 'error': '#f44336' + } + + self.main_frame.setStyleSheet(f""" + QFrame#mainFrame {{ + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + }} + QLabel {{ + color: {colors['text']}; + background-color: transparent; + padding: 4px 6px; + margin: 2px; + }} + QLabel#quoteLabel {{ + color: {colors['text']}; + font-size: 14px; + font-weight: 500; + padding: 6px 8px; + margin: 3px; + }} + QLabel#authorLabel {{ + color: {colors['text_secondary']}; + font-size: 12px; + font-style: italic; + padding: 4px 6px; + margin: 2px; + }} + QFrame#separator {{ + background-color: {colors['border']}; + }} + QPushButton#closeButton {{ + background-color: rgba(0, 0, 0, 0.05); + border: none; + color: {colors['text']}; + font-size: 18px; + font-weight: bold; + border-radius: 6px; + padding: 2px 4px; + }} + QPushButton#closeButton:hover {{ + background-color: #e81123; + color: white; + border-radius: 6px; + }} + QPushButton#refreshButton, QPushButton#insertButton {{ + background-color: {colors['accent']}; + color: white; + border: none; + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; + font-weight: 500; + }} + QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{ + background-color: {colors['accent_hover']}; + }} + """) + + def update_quote(self, quote_data=None): + """更新谏言显示""" + if quote_data: + self.quote_data = quote_data + else: + # 如果没有提供数据,使用默认数据 + if not hasattr(self, 'quote_data'): + self.quote_data = { + "quote": "书山有路勤为径,学海无涯苦作舟。", + "author": "韩愈", + "source": "《古今贤文·劝学篇》" + } + + # 更新显示 + self.quote_label.setText(self.quote_data["quote"]) + author_info = f"— {self.quote_data['author']}" + if self.quote_data.get("source"): + author_info += f" 《{self.quote_data['source']}》" + self.author_label.setText(author_info) + + def on_refresh_clicked(self): + """刷新按钮点击事件""" + # 发送刷新请求信号 + self.refresh_requested.emit() + # 同时直接获取新的内容并更新显示 + self.fetch_and_update_quote() + + def on_insert_clicked(self): + """插入按钮点击事件""" + # 发送插入请求信号,传递完整的诗句信息 + quote = self.quote_data.get("quote", "") + author = self.quote_data.get("author", "佚名") + source = self.quote_data.get("source", "") + + # 构造完整的诗句文本 + if source: + full_quote_text = f"{quote} —— {author}《{source}》" + else: + full_quote_text = f"{quote} —— {author}" + + if quote: + self.insert_requested.emit(full_quote_text) + + def fetch_and_update_quote(self): + """获取新的谏言内容并更新显示""" + try: + # 尝试获取古诗词 + import requests + import random + + try: + # 使用古诗词·一言API + response = requests.get("https://v1.jinrishici.com/all.json", timeout=5, verify=False) + if response.status_code == 200: + data = response.json() + content = data.get('content', '') + author = data.get('author', '佚名') + origin = data.get('origin', '') + + if content: + quote_data = { + "quote": content, + "author": author, + "source": origin + } + self.update_quote(quote_data) + return + except Exception as e: + print(f"获取古诗词失败: {e}") + + # 如果古诗词获取失败,使用备用API + try: + # 使用每日一言API + response = requests.get("https://api.nxvav.cn/api/yiyan?json=true", timeout=5, verify=False) + if response.status_code == 200: + data = response.json() + yiyan = data.get('yiyan', '') + nick = data.get('nick', '佚名') + + if yiyan: + quote_data = { + "quote": yiyan, + "author": nick, + "source": "" + } + self.update_quote(quote_data) + return + except Exception as e: + print(f"获取每日一言失败: {e}") + + # 如果API都失败,使用预设内容 + quotes = [ + {"quote": "学而时习之,不亦说乎?", "author": "孔子", "source": "《论语》"}, + {"quote": "千里之行,始于足下。", "author": "老子", "source": "《道德经》"}, + {"quote": "天行健,君子以自强不息。", "author": "佚名", "source": "《周易》"}, + {"quote": "书山有路勤为径,学海无涯苦作舟。", "author": "韩愈", "source": "《古今贤文·劝学篇》"}, + {"quote": "山重水复疑无路,柳暗花明又一村。", "author": "陆游", "source": "《游山西村》"} + ] + + # 随机选择一个名言 + new_quote = random.choice(quotes) + self.update_quote(new_quote) + + except Exception as e: + print(f"获取新谏言失败: {e}") + # 出错时显示默认内容 + default_quote = { + "quote": "书山有路勤为径,学海无涯苦作舟。", + "author": "韩愈", + "source": "《古今贤文·劝学篇》" + } + self.update_quote(default_quote) + + def close_window(self): + """关闭窗口 - 只是隐藏而不是销毁""" + try: + self.closed.emit() + self.hide() # 隐藏窗口而不是销毁 + except Exception as e: + print(f"Error in close_window: {e}") + + def mousePressEvent(self, event): + """鼠标按下事件,用于拖拽""" + if event.button() == Qt.LeftButton: + # 检查是否点击在标题栏区域 + if event.pos().y() <= 40: # 假设标题栏高度为40像素 + self.is_dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + + def mouseMoveEvent(self, event): + """鼠标移动事件,用于拖拽""" + if self.is_dragging and event.buttons() == Qt.LeftButton: + self.move(event.globalPos() - self.drag_position) + event.accept() + + def mouseReleaseEvent(self, event): + """鼠标释放事件""" + self.is_dragging = False + + +def main(): + """测试函数""" + app = QApplication(sys.argv) + + # 创建并显示窗口 + widget = QuoteFloatingWidget() + widget.show() + + # 移动到屏幕中心 + screen_geometry = app.desktop().screenGeometry() + widget.move( + (screen_geometry.width() - widget.width()) // 2, + (screen_geometry.height() - widget.height()) // 2 + ) + + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/ui/snake_game.py b/src/ui/snake_game.py new file mode 100644 index 0000000..6c8dd5f --- /dev/null +++ b/src/ui/snake_game.py @@ -0,0 +1,437 @@ +# snake_game.py +""" +贪吃蛇小游戏模块 +用户用WASD或方向键控制贪吃蛇移动 +""" + +import sys +import random +from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLabel, QMessageBox +from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QRect +from PyQt5.QtGui import QPainter, QColor, QFont, QBrush, QPen + + +class SnakeGame(QWidget): + """贪吃蛇游戏画布""" + + # 游戏常量 + GRID_SIZE = 20 # 网格大小 + GRID_WIDTH = 30 # 水平网格数 + GRID_HEIGHT = 20 # 垂直网格数 + GAME_SPEED = 150 # 游戏速度(毫秒) + MIN_SPEED = 50 # 最小速度(毫秒) + MAX_SPEED = 300 # 最大速度(毫秒) + SPEED_STEP = 10 # 速度调节步长(毫秒) + + # 信号 + score_changed = pyqtSignal(int) + game_over = pyqtSignal(int) + speed_changed = pyqtSignal(int) # 速度改变信号 + + # 方向常量 + UP = (0, -1) + DOWN = (0, 1) + LEFT = (-1, 0) + RIGHT = (1, 0) + + def __init__(self): + super().__init__() + self.init_game() + self.init_ui() + + def init_ui(self): + """初始化UI""" + self.setFixedSize( + self.GRID_WIDTH * self.GRID_SIZE, + self.GRID_HEIGHT * self.GRID_SIZE + ) + self.setStyleSheet("background-color: #1a1a1a;") + self.setFocus() # 获取焦点以接收键盘输入 + + def init_game(self): + """初始化游戏状态""" + # 蛇的初始位置(从中间开始) + self.snake = [ + (self.GRID_WIDTH // 2, self.GRID_HEIGHT // 2), + (self.GRID_WIDTH // 2 - 1, self.GRID_HEIGHT // 2), + (self.GRID_WIDTH // 2 - 2, self.GRID_HEIGHT // 2), + ] + + # 方向 + self.direction = self.RIGHT + self.next_direction = self.RIGHT + + # 食物位置 + self.food = self.generate_food() + + # 分数 + self.score = 0 + + # 游戏速度 + self.current_speed = self.GAME_SPEED + + # 游戏状态 + self.is_running = False + self.is_game_over = False + + # 游戏定时器 + self.game_timer = QTimer() + self.game_timer.timeout.connect(self.update_game) + + def generate_food(self): + """生成食物位置""" + while True: + x = random.randint(0, self.GRID_WIDTH - 1) + y = random.randint(0, self.GRID_HEIGHT - 1) + if (x, y) not in self.snake: + return (x, y) + + def start_game(self): + """开始游戏""" + if not self.is_running: + self.is_running = True + self.is_game_over = False + self.game_timer.start(self.current_speed) + self.setFocus() + + def pause_game(self): + """暂停游戏""" + if self.is_running: + self.is_running = False + self.game_timer.stop() + + def resume_game(self): + """恢复游戏""" + if not self.is_running and not self.is_game_over: + self.is_running = True + self.game_timer.start(self.current_speed) + + def restart_game(self): + """重新开始游戏""" + self.game_timer.stop() + self.init_game() + self.score_changed.emit(0) + self.speed_changed.emit(self.current_speed) + self.update() + # 重新启动游戏 + self.start_game() + + def increase_speed(self): + """增加游戏速度""" + if self.current_speed > self.MIN_SPEED: + self.current_speed = max(self.current_speed - self.SPEED_STEP, self.MIN_SPEED) + self.speed_changed.emit(self.current_speed) + if self.is_running: + self.game_timer.setInterval(self.current_speed) + + def decrease_speed(self): + """降低游戏速度""" + if self.current_speed < self.MAX_SPEED: + self.current_speed = min(self.current_speed + self.SPEED_STEP, self.MAX_SPEED) + self.speed_changed.emit(self.current_speed) + if self.is_running: + self.game_timer.setInterval(self.current_speed) + + def update_game(self): + """更新游戏状态""" + if not self.is_running: + return + + # 更新方向 + self.direction = self.next_direction + + # 计算新的头部位置 + head_x, head_y = self.snake[0] + dx, dy = self.direction + new_head = (head_x + dx, head_y + dy) + + # 检查碰撞 + if self.check_collision(new_head): + self.is_running = False + self.is_game_over = True + self.game_timer.stop() + self.game_over.emit(self.score) + self.update() + return + + # 添加新的头部 + self.snake.insert(0, new_head) + + # 检查是否吃到食物 + if new_head == self.food: + self.score += 10 + self.score_changed.emit(self.score) + self.food = self.generate_food() + else: + # 移除尾部 + self.snake.pop() + + self.update() + + def check_collision(self, position): + """检查碰撞""" + x, y = position + + # 检查边界碰撞 + if x < 0 or x >= self.GRID_WIDTH or y < 0 or y >= self.GRID_HEIGHT: + return True + + # 检查自身碰撞 + if position in self.snake: + return True + + return False + + def keyPressEvent(self, event): + """处理键盘输入""" + if event.isAutoRepeat(): + return + + key = event.key() + + # 使用WASD或方向键控制方向 + if key in (Qt.Key_W, Qt.Key_Up): + # 上键:向上 + if self.direction != self.DOWN: + self.next_direction = self.UP + elif key in (Qt.Key_S, Qt.Key_Down): + # 下键:向下 + if self.direction != self.UP: + self.next_direction = self.DOWN + elif key in (Qt.Key_A, Qt.Key_Left): + # 左键:向左(不用于调速) + if self.direction != self.RIGHT: + self.next_direction = self.LEFT + elif key in (Qt.Key_D, Qt.Key_Right): + # 右键:向右(不用于调速) + if self.direction != self.LEFT: + self.next_direction = self.RIGHT + elif key == Qt.Key_Space: + # 空格键暂停/恢复 + if self.is_running: + self.pause_game() + elif not self.is_game_over: + self.resume_game() + elif key == Qt.Key_R: + # R键重新开始 + if self.is_game_over: + self.restart_game() + elif key == Qt.Key_Plus or key == Qt.Key_Equal: + # + 或 = 键加速 + self.increase_speed() + elif key == Qt.Key_Minus: + # - 键减速 + self.decrease_speed() + + event.accept() + + def paintEvent(self, event): + """绘制游戏""" + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + + # 绘制网格背景 + self.draw_grid(painter) + + # 绘制食物 + self.draw_food(painter) + + # 绘制蛇 + self.draw_snake(painter) + + # 如果游戏结束,显示游戏结束提示 + if self.is_game_over: + self.draw_game_over(painter) + + def draw_grid(self, painter): + """绘制网格""" + painter.setPen(QPen(QColor(50, 50, 50), 1)) + + # 绘制竖线 + for x in range(self.GRID_WIDTH + 1): + painter.drawLine( + x * self.GRID_SIZE, 0, + x * self.GRID_SIZE, self.GRID_HEIGHT * self.GRID_SIZE + ) + + # 绘制横线 + for y in range(self.GRID_HEIGHT + 1): + painter.drawLine( + 0, y * self.GRID_SIZE, + self.GRID_WIDTH * self.GRID_SIZE, y * self.GRID_SIZE + ) + + def draw_snake(self, painter): + """绘制蛇""" + # 绘制蛇身 + for i, (x, y) in enumerate(self.snake): + if i == 0: + # 蛇头 - 更亮的绿色 + painter.fillRect( + x * self.GRID_SIZE + 1, + y * self.GRID_SIZE + 1, + self.GRID_SIZE - 2, + self.GRID_SIZE - 2, + QColor(0, 255, 0) + ) + # 绘制眼睛 + painter.fillRect( + x * self.GRID_SIZE + 4, + y * self.GRID_SIZE + 4, + 3, 3, + QColor(255, 0, 0) + ) + else: + # 蛇身 - 稍暗的绿色 + painter.fillRect( + x * self.GRID_SIZE + 1, + y * self.GRID_SIZE + 1, + self.GRID_SIZE - 2, + self.GRID_SIZE - 2, + QColor(0, 200, 0) + ) + + def draw_food(self, painter): + """绘制食物""" + x, y = self.food + painter.fillRect( + x * self.GRID_SIZE + 3, + y * self.GRID_SIZE + 3, + self.GRID_SIZE - 6, + self.GRID_SIZE - 6, + QColor(255, 0, 0) + ) + + def draw_game_over(self, painter): + """绘制游戏结束界面""" + # 半透明黑色背景 + painter.fillRect(self.rect(), QColor(0, 0, 0, 200)) + + # 绘制文本 + painter.setPen(QColor(255, 255, 255)) + font = QFont("Arial", 30, QFont.Bold) + painter.setFont(font) + + text = "游戏结束" + fm = painter.fontMetrics() + text_width = fm.width(text) + text_height = fm.height() + + x = (self.width() - text_width) // 2 + y = (self.height() - text_height) // 2 - 20 + + painter.drawText(x, y, text) + + # 绘制分数 + font.setPointSize(20) + painter.setFont(font) + score_text = f"最终分数: {self.score}" + fm = painter.fontMetrics() + score_width = fm.width(score_text) + score_x = (self.width() - score_width) // 2 + score_y = y + 50 + + painter.drawText(score_x, score_y, score_text) + + # 绘制提示 + font.setPointSize(12) + painter.setFont(font) + hint_text = "按R键重新开始" + fm = painter.fontMetrics() + hint_width = fm.width(hint_text) + hint_x = (self.width() - hint_width) // 2 + hint_y = score_y + 40 + + painter.drawText(hint_x, hint_y, hint_text) + + +class SnakeGameWindow(QMainWindow): + """贪吃蛇游戏窗口""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("贪吃蛇游戏") + self.setGeometry(200, 200, 700, 550) + + # 创建中央控件 + central_widget = QWidget() + self.setCentralWidget(central_widget) + + # 创建布局 + layout = QVBoxLayout(central_widget) + layout.setContentsMargins(10, 10, 10, 10) + + # 创建游戏画布 + self.game_widget = SnakeGame() + layout.addWidget(self.game_widget) + + # 创建控制面板 + control_layout = QVBoxLayout() + + # 分数标签 + self.score_label = QLabel("分数: 0") + self.score_label.setStyleSheet("font-size: 16px; font-weight: bold;") + control_layout.addWidget(self.score_label) + + # 速度标签 + self.speed_label = QLabel("速度: 正常") + self.speed_label.setStyleSheet("font-size: 14px; color: #0066cc;") + control_layout.addWidget(self.speed_label) + + # 提示标签 + self.hint_label = QLabel( + "控制方法: W/↑ 上 S/↓ 下 A/← 左 D/→ 右 | 空格暂停 | +/- 调速 | R重新开始" + ) + self.hint_label.setStyleSheet("font-size: 12px; color: gray;") + control_layout.addWidget(self.hint_label) + + layout.addLayout(control_layout) + + # 连接信号 + self.game_widget.score_changed.connect(self.update_score) + self.game_widget.game_over.connect(self.on_game_over) + self.game_widget.speed_changed.connect(self.update_speed) + + # 设置窗口样式 + self.setStyleSheet(""" + QMainWindow { + background-color: #f0f0f0; + } + QLabel { + color: #333; + } + """) + + # 启动游戏 + self.game_widget.start_game() + + def update_score(self, score): + """更新分数显示""" + self.score_label.setText(f"分数: {score}") + + def update_speed(self, speed_ms): + """更新速度显示""" + # 将毫秒转换为速度等级 + if speed_ms <= 50: + speed_level = "极快" + elif speed_ms <= 100: + speed_level = "很快" + elif speed_ms <= 150: + speed_level = "正常" + elif speed_ms <= 200: + speed_level = "稍慢" + else: + speed_level = "很慢" + + self.speed_label.setText(f"速度: {speed_level} ({speed_ms}ms)") + + def on_game_over(self, final_score): + """游戏结束处理""" + pass # 游戏结束信息已在游戏画布中显示 + + def keyPressEvent(self, event): + """处理键盘输入""" + if event.key() == Qt.Key_R: + self.game_widget.restart_game() + else: + super().keyPressEvent(event) diff --git a/src/ui/theme_manager.py b/src/ui/theme_manager.py index 6b42bb1..0978872 100644 --- a/src/ui/theme_manager.py +++ b/src/ui/theme_manager.py @@ -153,69 +153,99 @@ class ThemeManager(QObject): return self._get_light_stylesheet() def _get_dark_stylesheet(self): - """深色主题样式表""" + """深色主题样式表 - Apple设计风格""" return """ - /* 深色主题样式 */ + /* Apple设计风格深色主题样式 */ - /* 全局文字颜色 */ + /* 全局文字颜色和字体 - 使用Apple系统字体 */ QWidget { - color: #e0e0e0; + color: #f0f0f0; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 13px; } - /* 主窗口 */ + /* 主窗口 - Apple深色背景 */ QMainWindow { - background-color: #1e1e1e; + background-color: #2c2c2e; } - /* 菜单栏 */ + /* 菜单栏 - Apple深色风格 */ QMenuBar { - background-color: #0078d7; - border: 1px solid #005a9e; - font-size: 12px; - color: #ffffff; + background-color: #2c2c2e; + border: none; + border-bottom: 1px solid #404040; + font-size: 13px; + color: #f0f0f0; + padding: 4px 0; } QMenuBar::item { background-color: transparent; - padding: 4px 10px; - color: #e0e0e0; + padding: 6px 12px; + color: #f0f0f0; + border-radius: 4px; + margin: 0 1px; } QMenuBar::item:selected { - background-color: #106ebe; + background-color: #404040; + color: #f0f0f0; } - /* 菜单 */ + QMenuBar::item:pressed { + background-color: #505050; + color: #f0f0f0; + } + + /* 菜单 - Apple深色风格 */ QMenu { - background-color: #2d2d2d; - border: 1px solid #3c3c3c; - font-size: 12px; - color: #e0e0e0; + background-color: #2c2c2e; + border: 1px solid #404040; + border-radius: 8px; + font-size: 13px; + color: #f0f0f0; + padding: 4px 0; + margin: 2px; } QMenu::item { + color: #f0f0f0; + background-color: transparent; + border-radius: 4px; + margin: 0 4px; padding: 4px 20px; - color: #e0e0e0; } QMenu::item:selected { - background-color: #3c3c3c; + background-color: #0a84ff; + color: #ffffff; + } + + QMenu::item:pressed { + background-color: #0066cc; + color: #ffffff; + } + + QMenu::separator { + height: 1px; + background-color: #404040; + margin: 4px 8px; } /* 功能区 */ QFrame { - background-color: #2d2d2d; - border: 1px solid #3c3c3c; + background-color: #2c2c2e; + border: none; } /* 组框 */ QGroupBox { - font-size: 11px; + font-size: 12px; font-weight: normal; - color: #e0e0e0; - background-color: #2d2d2d; - border: 1px solid #3c3c3c; - border-radius: 0px; + color: #f0f0f0; + background-color: #2c2c2e; + border: none; + border-radius: 8px; margin-top: 5px; padding-top: 5px; } @@ -224,250 +254,319 @@ class ThemeManager(QObject): subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; - color: #e0e0e0; + color: #a0a0a0; } - /* 按钮 */ + /* 工具按钮 - Apple深色风格 */ QToolButton { - border: 1px solid #3c3c3c; - border-radius: 3px; - background-color: #3c3c3c; - font-size: 11px; - color: #e0e0e0; - padding: 3px 6px; + border: 1px solid transparent; + border-radius: 6px; + background-color: #3a3a3c; + font-size: 13px; + color: #f0f0f0; + padding: 6px 12px; } QToolButton:hover { - background-color: #4a4a4a; - border: 1px solid #5a5a5a; + background-color: #4a4a4c; + border: 1px solid #5a5a5c; } QToolButton:pressed { - background-color: #2a2a2a; - border: 1px solid #1a1a1a; + background-color: #5a5a5c; + border: 1px solid #6a6a6c; } QToolButton:checked { - background-color: #0078d4; - border: 1px solid #106ebe; + background-color: #0a84ff; + border: 1px solid #0a84ff; + color: #ffffff; } /* 切换按钮 */ QToolButton[checkable="true"] { - border: 1px solid #3c3c3c; - border-radius: 2px; - background-color: #3c3c3c; + border: 1px solid #4a4a4c; + border-radius: 6px; + background-color: #3a3a3c; font-size: 12px; - font-weight: bold; - color: #e0e0e0; + color: #f0f0f0; + padding: 6px 12px; } QToolButton[checkable="true"]:hover { - background-color: #4a4a4a; + background-color: #4a4a4c; } QToolButton[checkable="true"]:checked { - background-color: #0078d4; - border: 1px solid #106ebe; + background-color: #0a84ff; + border: 1px solid #0a84ff; + color: #ffffff; } - /* 下拉框 - 修复文字不可见问题 */ + /* 下拉框 - Apple深色风格 */ QComboBox { - background-color: #3c3c3c; - border: 1px solid #5a5a5a; - border-radius: 2px; - color: #e0e0e0; - padding: 2px 5px; - selection-background-color: #4a4a4a; - selection-color: #e0e0e0; + background-color: #3a3a3c; + border: 1px solid #4a4a4c; + border-radius: 6px; + color: #f0f0f0; + padding: 4px 8px; + selection-background-color: #0a84ff; + selection-color: #ffffff; } QComboBox:hover { - background-color: #4a4a4a; - border: 1px solid #6a6a6a; + background-color: #4a4a4c; + border: 1px solid #5a5a5c; } QComboBox::drop-down { border: none; - width: 15px; - background-color: #3c3c3c; + width: 20px; + border-left: 1px solid #4a4a4c; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; } QComboBox::down-arrow { image: none; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-top: 5px solid #e0e0e0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 6px solid #a0a0a0; + margin: 6px; } - /* 下拉框弹出列表 */ QComboBox QAbstractItemView { - background-color: #3c3c3c; - border: 1px solid #5a5a5a; - color: #e0e0e0; - selection-background-color: #4a4a4a; - selection-color: #e0e0e0; + background-color: #2c2c2e; + border: 1px solid #4a4a4c; + color: #f0f0f0; + selection-background-color: #0a84ff; + selection-color: #ffffff; } - /* 字体下拉框特殊处理 */ - QFontComboBox { - background-color: #3c3c3c; - border: 1px solid #5a5a5a; - border-radius: 2px; - color: #e0e0e0; - padding: 2px 5px; - selection-background-color: #4a4a4a; - selection-color: #e0e0e0; - } - - QFontComboBox:hover { - background-color: #4a4a4a; - border: 1px solid #6a6a6a; - } - - QFontComboBox QAbstractItemView { - background-color: #3c3c3c; - border: 1px solid #5a5a5a; - color: #e0e0e0; - selection-background-color: #4a4a4a; - selection-color: #e0e0e0; - } - - /* 文本编辑器 */ + /* 文本编辑区域 - Apple深色风格 */ QTextEdit { - background-color: #1e1e1e; - border: 1px solid #3c3c3c; - color: #e0e0e0; - padding: 20px; + background-color: #1c1c1e; + 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: #f0f0f0; + padding: 32px; line-height: 1.5; + selection-background-color: #0066cc; + selection-color: #ffffff; } - /* 状态栏 */ + /* 状态栏 - Apple深色风格 */ QStatusBar { - background-color: #2d2d2d; - border-top: 1px solid #3c3c3c; - font-size: 11px; - color: #e0e0e0; + background-color: #3a3a3c; + border-top: 1px solid #4a4a4c; + font-size: 12px; + color: #a0a0a0; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + padding: 6px 12px; } /* 标签 */ QLabel { - color: #e0e0e0; + color: #f0f0f0; background-color: transparent; } - /* 消息框 - 修复黑色背景问题 */ + /* 消息框 - Apple深色风格 */ QMessageBox { - background-color: #2d2d2d; - color: #e0e0e0; + background-color: #2c2c2e; + color: #f0f0f0; + border-radius: 12px; } QMessageBox QPushButton { - background-color: #3c3c3c; - color: #e0e0e0; - border: 1px solid #5a5a5a; - border-radius: 3px; - padding: 5px 15px; + background-color: #3a3a3c; + color: #f0f0f0; + border: 1px solid #4a4a4c; + border-radius: 6px; + padding: 6px 16px; min-width: 80px; } QMessageBox QPushButton:hover { - background-color: #4a4a4a; - border: 1px solid #6a6a6a; + background-color: #4a4a4c; + border: 1px solid #5a5a5c; } QMessageBox QPushButton:pressed { - background-color: #2a2a2a; - border: 1px solid #1a1a1a; + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + } + + QMessageBox QPushButton:default { + background-color: #0a84ff; + color: #ffffff; + border: 1px solid #0a84ff; + } + + QMessageBox QPushButton:default:hover { + background-color: #0066cc; + border: 1px solid #0066cc; + } + + QMessageBox QPushButton:default:pressed { + background-color: #004d99; + border: 1px solid #004d99; } - /* 滚动条 */ + /* 滚动条 - Apple深色风格 */ QScrollBar:vertical { - background-color: #2d2d2d; - width: 12px; + background-color: transparent; + width: 8px; border: none; } QScrollBar::handle:vertical { - background-color: #5a5a5a; - border-radius: 6px; + background-color: #5a5a5c; + border-radius: 4px; min-height: 20px; } QScrollBar::handle:vertical:hover { - background-color: #6a6a6a; + background-color: #6a6a6c; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { border: none; background: none; } + + /* 按钮 - Apple深色风格 */ + QPushButton { + background-color: #3a3a3c; + color: #f0f0f0; + border: 1px solid #4a4a4c; + border-radius: 6px; + padding: 6px 16px; + font-size: 13px; + } + + QPushButton:hover { + background-color: #4a4a4c; + border: 1px solid #5a5a5c; + } + + QPushButton:pressed { + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + } + + QPushButton:default { + background-color: #0a84ff; + color: #ffffff; + border: 1px solid #0a84ff; + } + + QPushButton:default:hover { + background-color: #0066cc; + border: 1px solid #0066cc; + } + + QPushButton:default:pressed { + background-color: #004d99; + border: 1px solid #004d99; + } """ def _get_light_stylesheet(self): - """浅色主题样式表 - 白底黑字""" + """浅色主题样式表 - Apple设计风格""" return """ - /* 浅色主题样式 - 白底黑字 */ + /* Apple设计风格浅色主题样式 */ - /* 全局文字颜色 */ + /* 全局文字颜色和字体 - 使用Apple系统字体 */ QWidget { color: #333333; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 13px; } - /* 主窗口 */ + /* 主窗口 - 纯净白色背景 */ QMainWindow { - background-color: #f3f2f1; + background-color: #ffffff; } - /* 菜单栏 */ + /* 菜单栏 - Apple风格 */ QMenuBar { - background-color: #0078d7; - border: 1px solid #005a9e; - font-size: 12px; - color: #ffffff; + background-color: #ffffff; + border: none; + border-bottom: 1px solid #e0e0e0; + font-size: 13px; + color: #333333; + padding: 4px 0; } QMenuBar::item { background-color: transparent; - padding: 4px 10px; + padding: 6px 12px; color: #333333; + border-radius: 4px; + margin: 0 1px; } QMenuBar::item:selected { - background-color: #106ebe; + background-color: #f0f0f0; + color: #333333; } - /* 菜单 */ + QMenuBar::item:pressed { + background-color: #e0e0e0; + color: #333333; + } + + /* 菜单 - Apple风格 */ QMenu { background-color: #ffffff; border: 1px solid #d0d0d0; - font-size: 12px; + border-radius: 8px; + font-size: 13px; color: #333333; + padding: 4px 0; + margin: 2px; } QMenu::item { - padding: 4px 20px; color: #333333; + background-color: transparent; + border-radius: 4px; + margin: 0 4px; + padding: 4px 20px; } QMenu::item:selected { - background-color: #f0f0f0; + background-color: #007aff; + color: #ffffff; + } + + QMenu::item:pressed { + background-color: #0062cc; + color: #ffffff; + } + + QMenu::separator { + height: 1px; + background-color: #e0e0e0; + margin: 4px 8px; } /* 功能区 */ QFrame { background-color: #ffffff; - border: 1px solid #d0d0d0; + border: none; } /* 组框 */ QGroupBox { - font-size: 11px; + font-size: 12px; font-weight: normal; color: #333333; background-color: #ffffff; - border: 1px solid #d0d0d0; - border-radius: 0px; + border: none; + border-radius: 8px; margin-top: 5px; padding-top: 5px; } @@ -476,145 +575,116 @@ class ThemeManager(QObject): subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; - color: #333333; + color: #666666; } - /* 按钮 */ + /* 工具按钮 - Apple风格 */ QToolButton { - border: 1px solid #d0d0d0; - border-radius: 3px; - background-color: #ffffff; - font-size: 11px; + border: 1px solid transparent; + border-radius: 6px; + background-color: #f6f6f6; + font-size: 13px; color: #333333; - padding: 3px 6px; + padding: 6px 12px; } QToolButton:hover { - background-color: #f0f0f0; - border: 1px solid #0078d7; + background-color: #e0e0e0; + border: 1px solid #d0d0d0; } QToolButton:pressed { - background-color: #e0e0e0; - border: 1px solid #005a9e; + background-color: #d0d0d0; + border: 1px solid #c0c0c0; } QToolButton:checked { - background-color: #0078d7; - border: 1px solid #005a9e; + background-color: #007aff; + border: 1px solid #007aff; color: #ffffff; } /* 切换按钮 */ QToolButton[checkable="true"] { border: 1px solid #d0d0d0; - border-radius: 2px; - background-color: #ffffff; + border-radius: 6px; + background-color: #f6f6f6; font-size: 12px; - font-weight: bold; color: #333333; + padding: 6px 12px; } QToolButton[checkable="true"]:hover { - background-color: #f0f0f0; + background-color: #e0e0e0; } QToolButton[checkable="true"]:checked { - background-color: #0078d7; - border: 1px solid #005a9e; + background-color: #007aff; + border: 1px solid #007aff; color: #ffffff; } - /* 下拉框 - 白底黑字 */ + /* 下拉框 - Apple风格 */ QComboBox { - background-color: #ffffff; + background-color: #f6f6f6; border: 1px solid #d0d0d0; - border-radius: 2px; + border-radius: 6px; color: #333333; - padding: 2px 5px; - selection-background-color: #f0f0f0; - selection-color: #333333; + padding: 4px 8px; + selection-background-color: #007aff; + selection-color: #ffffff; } QComboBox:hover { - background-color: #f0f0f0; - border: 1px solid #0078d7; + background-color: #e0e0e0; + border: 1px solid #c0c0c0; } QComboBox::drop-down { border: none; - width: 15px; - background-color: #ffffff; + width: 20px; + border-left: 1px solid #d0d0d0; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; } QComboBox::down-arrow { image: none; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-top: 5px solid #333333; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 6px solid #666666; + margin: 6px; } - /* 下拉框弹出列表 */ QComboBox QAbstractItemView { - background-color: #ffffff; border: 1px solid #d0d0d0; color: #333333; - selection-background-color: #f0f0f0; - selection-color: #333333; - } - - /* 字体下拉框特殊处理 - 白底黑字 */ - QFontComboBox { - background-color: #ffffff; - border: 1px solid #d0d0d0; - border-radius: 2px; - color: #333333; - padding: 2px 5px; - selection-background-color: #f0f0f0; - selection-color: #333333; - } - - QFontComboBox:hover { - background-color: #f0f0f0; - border: 1px solid #0078d7; - } - - QFontComboBox::drop-down { - border: none; - width: 15px; background-color: #ffffff; + selection-background-color: #007aff; + selection-color: #ffffff; } - QFontComboBox::down-arrow { - image: none; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-top: 5px solid #333333; - } - - QFontComboBox QAbstractItemView { - background-color: #ffffff; - border: 1px solid #d0d0d0; - color: #333333; - selection-background-color: #f0f0f0; - selection-color: #333333; - } - - /* 文本编辑器 */ + /* 文本编辑区域 - Apple风格 */ QTextEdit { background-color: #ffffff; - border: 1px solid #d0d0d0; - color: #000000; - padding: 20px; + border: none; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 15px; + color: #333333; + padding: 32px; line-height: 1.5; + selection-background-color: #b3d9ff; + selection-color: #333333; } - /* 状态栏 */ + /* 状态栏 - Apple风格 */ QStatusBar { - background-color: #ffffff; - border-top: 1px solid #d0d0d0; - font-size: 11px; - color: #333333; + background-color: #f6f6f6; + border-top: 1px solid #e0e0e0; + font-size: 12px; + color: #666666; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + padding: 6px 12px; } /* 标签 */ @@ -623,41 +693,58 @@ class ThemeManager(QObject): background-color: transparent; } - /* 消息框 - 修复黑色背景问题 */ + /* 消息框 - Apple风格 */ QMessageBox { background-color: #ffffff; color: #333333; + border-radius: 12px; } QMessageBox QPushButton { - background-color: #ffffff; + background-color: #f6f6f6; color: #333333; border: 1px solid #d0d0d0; - border-radius: 3px; - padding: 5px 15px; + border-radius: 6px; + padding: 6px 16px; min-width: 80px; } QMessageBox QPushButton:hover { - background-color: #f0f0f0; - border: 1px solid #0078d7; + background-color: #e0e0e0; + border: 1px solid #c0c0c0; } QMessageBox QPushButton:pressed { - background-color: #e0e0e0; - border: 1px solid #005a9e; + background-color: #d0d0d0; + border: 1px solid #a0a0a0; + } + + QMessageBox QPushButton:default { + background-color: #007aff; + color: #ffffff; + border: 1px solid #007aff; + } + + QMessageBox QPushButton:default:hover { + background-color: #0062cc; + border: 1px solid #0062cc; + } + + QMessageBox QPushButton:default:pressed { + background-color: #004a99; + border: 1px solid #004a99; } - /* 滚动条 */ + /* 滚动条 - Apple风格 */ QScrollBar:vertical { - background-color: #ffffff; - width: 12px; + background-color: transparent; + width: 8px; border: none; } QScrollBar::handle:vertical { background-color: #c0c0c0; - border-radius: 6px; + border-radius: 4px; min-height: 20px; } @@ -669,6 +756,42 @@ class ThemeManager(QObject): border: none; background: none; } + + /* 按钮 - Apple风格 */ + QPushButton { + background-color: #f6f6f6; + color: #333333; + border: 1px solid #d0d0d0; + border-radius: 6px; + padding: 6px 16px; + font-size: 13px; + } + + QPushButton:hover { + background-color: #e0e0e0; + border: 1px solid #c0c0c0; + } + + QPushButton:pressed { + background-color: #d0d0d0; + border: 1px solid #a0a0a0; + } + + QPushButton:default { + background-color: #007aff; + color: #ffffff; + border: 1px solid #007aff; + } + + QPushButton:default:hover { + background-color: #0062cc; + border: 1px solid #0062cc; + } + + QPushButton:default:pressed { + background-color: #004a99; + border: 1px solid #004a99; + } """ def set_dark_theme(self, is_dark): diff --git a/src/ui/ui.txt b/src/ui/ui.txt deleted file mode 100644 index e69de29..0000000 diff --git a/src/ui/weather_floating_widget.py b/src/ui/weather_floating_widget.py new file mode 100644 index 0000000..bb373de --- /dev/null +++ b/src/ui/weather_floating_widget.py @@ -0,0 +1,620 @@ +# -*- 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(360, 280) # 调整窗口尺寸使其更紧凑 + + # 创建主框架,用于实现圆角和阴影效果 + 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(6) # 减小间距使布局更紧凑 + + # 设置最小尺寸策略 + self.main_frame.setMinimumSize(380, 300) + + # 标题栏 + title_layout = QHBoxLayout() + + 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.setFrameShape(QFrame.HLine) + separator.setObjectName("separator") + content_layout.addWidget(separator) + + # 天气图标和温度显示区域 + weather_display_layout = QHBoxLayout() + weather_display_layout.setSpacing(5) # 减小间距使布局更紧凑 + weather_display_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距 + + self.weather_icon_label = QLabel("🌞") + self.weather_icon_label.setFont(QFont("Arial", 24)) # 稍微减小字体大小 + self.weather_icon_label.setAlignment(Qt.AlignCenter) + self.weather_icon_label.setFixedSize(50, 50) # 减小尺寸 + 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", 18, QFont.Bold)) # 稍微减小字体大小 + self.temperature_label.setObjectName("temperatureLabel") + temp_city_layout.addWidget(self.temperature_label) + + self.city_label = QLabel("北京") + self.city_label.setFont(QFont("Arial", 11)) # 稍微减小字体大小 + 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", 11)) # 稍微减小字体大小 + 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(6) # 减小间距使布局更紧凑 + details_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距 + + self.humidity_label = QLabel("湿度: 45%") + self.humidity_label.setFont(QFont("Arial", 10)) # 稍微减小字体大小 + self.humidity_label.setObjectName("detailLabel") + details_layout.addWidget(self.humidity_label) + + self.wind_label = QLabel("风速: 2级") + self.wind_label.setFont(QFont("Arial", 10)) # 稍微减小字体大小 + self.wind_label.setObjectName("detailLabel") + details_layout.addWidget(self.wind_label) + + content_layout.addLayout(details_layout) + + # 城市选择区域 + city_layout = QHBoxLayout() + city_layout.setSpacing(6) # 减小间距使布局更紧凑 + city_layout.setContentsMargins(0, 0, 0, 0) + + self.city_combo = QComboBox() + self.city_combo.setObjectName("cityCombo") + # 添加所有省会城市,与主窗口保持一致 + self.city_combo.addItems([ + '自动定位', + '北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', # 一线城市 + '天津', '重庆', '苏州', '青岛', '大连', '宁波', '厦门', '无锡', '佛山', # 新一线城市 + '石家庄', '太原', '呼和浩特', '沈阳', '长春', '哈尔滨', # 东北华北 + '合肥', '福州', '南昌', '济南', '郑州', '长沙', '南宁', '海口', # 华东华中华南 + '贵阳', '昆明', '拉萨', '兰州', '西宁', '银川', '乌鲁木齐' # 西南西北 + ]) + self.city_combo.setFixedWidth(100) # 减小城市选择框宽度使布局更紧凑 + city_layout.addWidget(self.city_combo) + + city_layout.addStretch() + + content_layout.addLayout(city_layout) + + # 按钮区域 + button_layout = QHBoxLayout() + button_layout.setSpacing(6) # 减小间距使布局更紧凑 + button_layout.setContentsMargins(0, 0, 0, 0) + + self.refresh_btn = QPushButton("刷新") + self.refresh_btn.setObjectName("refreshButton") + self.refresh_btn.setFixedHeight(26) # 减小按钮高度 + button_layout.addWidget(self.refresh_btn) + + button_layout.addStretch() + + self.detail_btn = QPushButton("详情") + self.detail_btn.setObjectName("detailButton") + self.detail_btn.setFixedHeight(26) # 减小按钮高度 + button_layout.addWidget(self.detail_btn) + + content_layout.addLayout(button_layout) + + # 添加弹性空间 + content_layout.addStretch() + + 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) + self.city_combo.currentTextChanged.connect(self.on_city_changed) + + 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: 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#temperatureLabel {{ + color: {colors['accent']}; + font-size: 20px; + font-weight: bold; + padding: 6px 8px; + margin: 3px; + }} + QLabel#cityLabel {{ + color: {colors['text_secondary']}; + font-size: 12px; + padding: 4px 6px; + margin: 2px; + }} + QLabel#weatherDescLabel {{ + color: {colors['text']}; + font-size: 12px; + font-weight: 500; + padding: 4px 6px; + margin: 2px; + }} + QLabel#detailLabel {{ + color: {colors['text_secondary']}; + font-size: 11px; + 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: 11px; + font-weight: 500; + }} + QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{ + background-color: {colors['accent_hover']}; + }} + QComboBox#cityCombo {{ + background-color: {colors['surface']}; + color: {colors['text']}; + border: 1px solid {colors['border']}; + border-radius: 6px; + padding: 4px 7px; + font-size: 11px; + font-weight: 500; + min-height: 24px; + }} + QComboBox#cityCombo:hover {{ + border-color: {colors['accent']}; + }} + QComboBox#cityCombo::drop-down {{ + border: none; + width: 14px; + }} + QComboBox#cityCombo::down-arrow {{ + image: none; + border-left: 2px solid transparent; + border-right: 2px solid transparent; + border-top: 5px solid {colors['text']}; + }} + """) + else: + # 浅色主题样式 - 与每日谏言悬浮窗口保持一致 + 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#temperatureLabel {{ + color: {colors['accent']}; + font-size: 20px; + font-weight: bold; + padding: 6px 8px; + margin: 3px; + }} + QLabel#cityLabel {{ + color: {colors['text_secondary']}; + font-size: 12px; + padding: 4px 6px; + margin: 2px; + }} + QLabel#weatherDescLabel {{ + color: {colors['text']}; + font-size: 12px; + font-weight: 500; + padding: 4px 6px; + margin: 2px; + }} + QLabel#detailLabel {{ + color: {colors['text_secondary']}; + font-size: 11px; + 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: 11px; + font-weight: 500; + }} + QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{ + background-color: {colors['accent_hover']}; + }} + QComboBox#cityCombo {{ + background-color: {colors['surface']}; + color: {colors['text']}; + border: 1px solid {colors['border']}; + border-radius: 6px; + padding: 4px 7px; + font-size: 11px; + font-weight: 500; + min-height: 24px; + }} + QComboBox#cityCombo:hover {{ + border-color: {colors['accent']}; + }} + QComboBox#cityCombo::drop-down {{ + border: none; + width: 14px; + }} + QComboBox#cityCombo::down-arrow {{ + image: none; + border-left: 2px solid transparent; + border-right: 2px solid transparent; + border-top: 5px solid {colors['text']}; + }} + """) + + 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): + """设置当前城市""" + # 阻止信号发射,避免循环调用 + self.city_combo.blockSignals(True) + index = self.city_combo.findText(city_name) + if index >= 0: + self.city_combo.setCurrentIndex(index) + self.city_combo.blockSignals(False) + + 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"

{weather_data.get('city', '未知城市')}

") + layout.addWidget(city_label) + + # 当前天气信息 + current_layout = QVBoxLayout() + current_layout.addWidget(QLabel("当前天气:")) + + # 获取温度信息,支持嵌套结构 + 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("生活提示:")) + + 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) \ No newline at end of file diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 5150f9f..65e0525 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -34,6 +34,7 @@ class WordRibbon(QFrame): self.weather_group = None # 天气组件组 self.quote_visible = False # 每日一言组件显示状态 self.quote_group = None # 每日一言组件组 + self.current_quote_type = "普通箴言" # 每日一言类型 self.ribbon_layout = None # 功能区布局 self.setup_ui() @@ -49,13 +50,13 @@ class WordRibbon(QFrame): main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) - # 功能区 + # 功能区 - 现代极简主义风格 self.ribbon_area = QFrame() self.ribbon_area.setStyleSheet(""" QFrame { background-color: #ffffff; - border: 1px solid #d0d0d0; - border-top: none; + border: none; + border-bottom: 1px solid #e2e8f0; } """) @@ -73,103 +74,139 @@ class WordRibbon(QFrame): main_layout.addWidget(self.ribbon_area) self.setLayout(main_layout) + def add_separator(self): + """ + 在 Ribbon 主布局里添加一个高度自动适配的竖向分隔线 + """ + line = QFrame() + line.setFrameShape(QFrame.VLine) + line.setFrameShadow(QFrame.Plain) + line.setStyleSheet(""" + QFrame { + background-color: #d0d7de; + width: 1px; + } + """) + line.setFixedWidth(1) + + # Ribbon 区域高度会变,这玩意就自动跟着撑满 + line.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) + + self.ribbon_layout.addWidget(line) + + + + def create_ribbon_group(self, title, is_special_group=False): + """ + 统一的创建 group 的方法。 + - 如果 is_special_group 为 True:返回单个 group(与原来某些位置的用法一致)。 + - 否则:返回 (group, content_layout) 以便直接往 content_layout 塞控件(与 setup_home_tab 的用法一致)。 + """ + # 对于“特殊组”(天气、每日一言)我们使用 QGroupBox 保持原样 + if is_special_group: + group = QGroupBox(title) + # 应用主题样式 hook + theme_manager.theme_changed.connect(lambda: self._update_group_style(group)) + self._update_group_style(group) + # 给特殊组留一些最小宽度,便于布局(可由调用方调整) + group.setMinimumWidth(100) + return group + + # 常规组:使用 QFrame + 内部 content layout + 底部标题(保留标题在下的视觉) + group = QFrame() + group.setStyleSheet(""" + QFrame { border: none; } + QLabel { font-size: 11px; color: #555; } + """) + + outer_layout = QVBoxLayout(group) + outer_layout.setContentsMargins(3, 3, 3, 3) + outer_layout.setSpacing(2) + + content_widget = QWidget() + content_layout = QHBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(5) + + title_label = QLabel(title) + title_label.setAlignment(Qt.AlignCenter) + + outer_layout.addWidget(content_widget) + outer_layout.addWidget(title_label) + + group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) + return group, content_layout + def setup_home_tab(self, layout): - """设置开始标签的功能区内容""" - - # 字体组 - font_group = self.create_ribbon_group("字体") - - # 字体选择 - font_layout = QHBoxLayout() + """设置开始标签 Ribbon —— 保持原有变量/槽名不变,只改布局实现""" + + # ---------- 字体组 ---------- + font_group, font_layout = self.create_ribbon_group("字体") + self.font_combo = QFontComboBox() self.font_combo.setFixedWidth(120) self.font_combo.currentFontChanged.connect(self.on_font_changed) + self.font_size_combo = QComboBox() - self.font_size_combo.addItems(['8', '9', '10', '11', '12', '14', '16', '18', '20', '22', '24', '26', '28', '36', '48', '72']) + self.font_size_combo.addItems( + ['8','9','10','11','12','14','16','18','20','22','24','26','28','36','48','72'] + ) self.font_size_combo.setFixedWidth(70) self.font_size_combo.setCurrentText('12') self.font_size_combo.currentTextChanged.connect(self.on_font_size_changed) - + font_layout.addWidget(self.font_combo) font_layout.addWidget(self.font_size_combo) - - # 字体样式按钮 - font_style_layout = QHBoxLayout() + self.bold_btn = self.create_toggle_button("B", "bold") - self.bold_btn.clicked.connect(self.on_bold_clicked) self.italic_btn = self.create_toggle_button("I", "italic") - self.italic_btn.clicked.connect(self.on_italic_clicked) self.underline_btn = self.create_toggle_button("U", "underline") - self.underline_btn.clicked.connect(self.on_underline_clicked) - - # 字体颜色按钮 self.color_btn = self.create_color_button("A", "color") - self.color_btn.clicked.connect(self.on_color_clicked) - - font_style_layout.addWidget(self.bold_btn) - font_style_layout.addWidget(self.italic_btn) - font_style_layout.addWidget(self.underline_btn) - font_style_layout.addWidget(self.color_btn) - - font_main_layout = QVBoxLayout() - font_main_layout.addLayout(font_layout) - font_main_layout.addLayout(font_style_layout) - - font_group.setLayout(font_main_layout) + + font_layout.addWidget(self.bold_btn) + font_layout.addWidget(self.italic_btn) + font_layout.addWidget(self.underline_btn) + font_layout.addWidget(self.color_btn) + font_layout.addStretch() layout.addWidget(font_group) - - # 段落组 - paragraph_group = self.create_ribbon_group("段落") - paragraph_group.setFixedWidth(320) # 增加宽度以适应更宽的按钮 - - # 对齐方式 - align_layout = QHBoxLayout() + self.add_separator() + # ---------- 段落组 ---------- + paragraph_group, paragraph_layout = self.create_ribbon_group("段落") self.align_left_btn = self.create_toggle_button("左对齐", "align_left") self.align_center_btn = self.create_toggle_button("居中", "align_center") self.align_right_btn = self.create_toggle_button("右对齐", "align_right") self.align_justify_btn = self.create_toggle_button("两端对齐", "align_justify") - - align_layout.addWidget(self.align_left_btn) - align_layout.addWidget(self.align_center_btn) - align_layout.addWidget(self.align_right_btn) - align_layout.addWidget(self.align_justify_btn) - - paragraph_layout = QVBoxLayout() - paragraph_layout.addLayout(align_layout) - - paragraph_group.setLayout(paragraph_layout) + + paragraph_layout.addWidget(self.align_left_btn) + paragraph_layout.addWidget(self.align_center_btn) + paragraph_layout.addWidget(self.align_right_btn) + paragraph_layout.addWidget(self.align_justify_btn) + paragraph_layout.addStretch() layout.addWidget(paragraph_group) - - # 样式组 - styles_group = self.create_ribbon_group("样式") - styles_layout = QVBoxLayout() - styles_layout.addWidget(QLabel("样式")) - styles_group.setLayout(styles_layout) - layout.addWidget(styles_group) - - # 编辑组 - editing_group = self.create_ribbon_group("编辑") - - # 创建查找替换按钮,使用更适合水平排列的样式 + self.add_separator() + + # ---------- 样式组(单行水平排列) ---------- + self.init_style_preview_group() + self.add_separator() + + # ---------- 编辑组 ---------- + editing_group, editing_layout = self.create_ribbon_group("编辑") self.find_btn = QToolButton() self.find_btn.setText("查找") self.find_btn.setToolButtonStyle(Qt.ToolButtonTextOnly) - self.find_btn.setFixedSize(50, 25) # 设置适合水平排列的尺寸 - + self.find_btn.setFixedSize(55, 25) self.replace_btn = QToolButton() self.replace_btn.setText("替换") self.replace_btn.setToolButtonStyle(Qt.ToolButtonTextOnly) - self.replace_btn.setFixedSize(50, 25) # 设置适合水平排列的尺寸 - - editing_layout = QHBoxLayout() # 改为水平布局 + self.replace_btn.setFixedSize(55, 25) editing_layout.addWidget(self.find_btn) editing_layout.addWidget(self.replace_btn) - editing_layout.addStretch() # 添加弹性空间 - editing_group.setLayout(editing_layout) - editing_group.setFixedWidth(120) # 设置编辑组宽度以适应查找替换按钮 + editing_layout.addStretch() layout.addWidget(editing_group) - + + # 最右侧填充空白,保证整体自适应 layout.addStretch() + def on_font_changed(self, font): """字体变化处理""" @@ -179,22 +216,67 @@ class WordRibbon(QFrame): """字体大小变化处理""" pass - def on_bold_clicked(self): - """粗体按钮点击处理""" - pass - - def on_italic_clicked(self): - """斜体按钮点击处理""" - pass - - def on_underline_clicked(self): - """下划线按钮点击处理""" - pass - - def on_color_clicked(self): - """字体颜色按钮点击处理""" - pass + + def init_style_preview_group(self): + """加入 Word 风格的样式预览区域""" + + group = QFrame() + group.setStyleSheet(""" + QFrame { border: none; } + QLabel { font-size: 11px; color: #555; } + """) + + outer_layout = QVBoxLayout(group) + outer_layout.setContentsMargins(3, 3, 3, 3) + outer_layout.setSpacing(2) + + # 上部:样式预览按钮 + preview_widget = QWidget() + preview_layout = QHBoxLayout(preview_widget) + preview_layout.setContentsMargins(0, 0, 0, 0) + preview_layout.setSpacing(8) + + style_items = [ + ("正文", "font-size:14px;"), + ("无间隔", "font-size:14px;"), + ("标题 1", "font-size:22px; font-weight:bold; color:#2E75B6;"), + ("标题 2", "font-size:18px; color:#2E75B6;"), + ("标题 3", "font-size:16px; font-weight:bold;"), + ("副标题", "font-size:14px; font-style:italic; color:#555;"), + ("强调", "font-size:14px; color:#C0504D;"), + ] + + for text, style in style_items: + btn = QPushButton(text) + btn.setFixedSize(95, 60) + btn.setStyleSheet(f""" + QPushButton {{ + background: white; + border: 1px solid #d0d0d0; + border-radius: 3px; + text-align: left; + padding: 5px; + {style} + }} + QPushButton:hover {{ + border: 1px solid #4a90e2; + }} + """) + preview_layout.addWidget(btn) + + preview_layout.addStretch() + + # 下方标题 + title_label = QLabel("样式") + title_label.setAlignment(Qt.AlignCenter) + + outer_layout.addWidget(preview_widget) + outer_layout.addWidget(title_label) + + self.ribbon_layout.addWidget(group) + + def init_theme(self): """初始化主题""" # 连接主题切换信号 @@ -204,35 +286,52 @@ class WordRibbon(QFrame): self.apply_theme() def apply_theme(self): - """应用主题样式""" is_dark = theme_manager.is_dark_theme() - - # 更新功能区背景 colors = theme_manager.get_current_theme_colors() + # 简化仅保留关键样式更新 self.ribbon_area.setStyleSheet(f""" QFrame {{ background-color: {colors['surface']}; border: 1px solid {colors['border']}; border-top: none; }} - QGroupBox {{ - background-color: {colors['surface']}; - color: {colors['text']}; - border: 1px solid {colors['border']}; - }} - QGroupBox::title {{ - color: {colors['text']}; - background-color: transparent; - }} - QLabel {{ - color: {colors['text']}; - background-color: transparent; - }} """) + # 更新下拉框和按钮样式(调用原有 helper) + self.update_combo_styles(is_dark) + self.update_font_button_styles(is_dark) + + # 更新天气组件样式 + if hasattr(self, 'weather_icon_label') and self.weather_icon_label is not None: + self.weather_icon_label.setStyleSheet(f""" + QLabel {{ + font-size: 32px; + padding: 0px; + margin: 0px; + border: none; + background: transparent; + }} + """) + + if hasattr(self, 'weather_temp_label') and self.weather_temp_label is not None: + self.weather_temp_label.setStyleSheet(f""" + QLabel {{ + font-size: 16px; + font-weight: bold; + color: {colors['text']}; + padding: 0px; + margin: 0px; + border: none; + background: transparent; + min-height: 30px; + }} + """) # 更新下拉框样式 self.update_combo_styles(is_dark) - + + def on_theme_changed(self, is_dark): + self.apply_theme() + def update_combo_styles(self, is_dark): """更新下拉框样式""" colors = theme_manager.get_current_theme_colors() @@ -464,17 +563,125 @@ class WordRibbon(QFrame): """主题切换槽函数""" self.apply_theme() + def get_weather_emoji(self, weather_desc): + """根据天气描述返回对应的emoji图标""" + weather_emoji_map = { + '晴': '🌞', + '多云': '☁️', + '阴': '☁️', + '小雨': '🌦️', + '中雨': '🌧️', + '大雨': '⛈️', + '暴雨': '⛈️', + '雷阵雨': '⛈️', + '雪': '❄️', + '小雪': '🌨️', + '中雪': '❄️', + '大雪': '❄️', + '暴雪': '❄️', + '雾': '🌫️', + '霾': '🌫️', + '沙尘暴': '🌪️', + '扬沙': '🌪️', + '浮尘': '🌪️', + '台风': '🌀', + '飓风': '🌀', + '龙卷风': '🌪️', + '冰雹': '🧊', + '冻雨': '🌨️', + '雨夹雪': '🌨️', + 'sunny': '🌞', + 'clear': '🌞', + 'cloudy': '☁️', + 'overcast': '☁️', + 'rain': '🌧️', + 'light rain': '🌦️', + 'heavy rain': '⛈️', + 'thunderstorm': '⛈️', + 'snow': '❄️', + 'fog': '🌫️', + 'haze': '🌫️', + 'sandstorm': '🌪️', + 'typhoon': '🌀', + 'hurricane': '🌀', + 'tornado': '🌪️', + 'hail': '🧊' + } + + # 模糊匹配天气描述 + weather_desc_lower = str(weather_desc).lower() + for key, emoji in weather_emoji_map.items(): + if key in weather_desc_lower: + return emoji + + # 默认返回晴天图标 + return '🌞' + def create_weather_group(self): """创建天气组件组""" if self.weather_group is not None: return self.weather_group - weather_group = self.create_ribbon_group("天气") + weather_group = self.create_ribbon_group("天气", is_special_group=True) + weather_group.setFixedWidth(200) # 增加整体宽度 weather_layout = QVBoxLayout() + weather_layout.setSpacing(8) # 增加行间距 + + # 第一行:天气图标和温度显示(居中对齐) + weather_display_layout = QHBoxLayout() + weather_display_layout.setSpacing(10) # 增加图标和温度间距 + + # 添加左侧弹性空间,推动内容到中心 + weather_display_layout.addStretch() + + # 天气图标标签 - 优化垂直居中对齐 + self.weather_icon_label = QLabel("🌞") + self.weather_icon_label.setAlignment(Qt.AlignCenter) # 使用Qt对齐方式 + self.weather_icon_label.setStyleSheet(""" + QLabel { + font-size: 32px; + padding: 0px; + margin: 0px; + border: none; + background: transparent; + } + """) + self.weather_icon_label.setFixedSize(40, 40) # 增大图标尺寸 + + # 温度标签 - 优化垂直居中对齐 + self.weather_temp_label = QLabel("--°C") + self.weather_temp_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) # 使用Qt对齐方式 + # 初始化时使用默认颜色,主题切换时会更新 + self.weather_temp_label.setStyleSheet(""" + QLabel { + font-size: 16px; + font-weight: bold; + padding: 0px; + margin: 0px; + border: none; + background: transparent; + min-height: 30px; /* 确保最小高度 */ + } + """) + self.weather_temp_label.setFixedSize(70, 30) # 增加温度标签宽度 + + weather_display_layout.addWidget(self.weather_icon_label) + weather_display_layout.addWidget(self.weather_temp_label) + + # 添加右侧弹性空间,确保内容居中 + weather_display_layout.addStretch() + + # 第二行:城市选择和刷新按钮(居中对齐) + control_layout = QHBoxLayout() + control_layout.setSpacing(8) # 增加控件间距 + + # 添加左侧弹性空间,推动内容到中心 + control_layout.addStretch() # 城市选择 - 添加所有省会城市 self.city_combo = QComboBox() - self.city_combo.setFixedWidth(120) # 增加宽度以显示完整城市名 + self.city_combo.setFixedWidth(120) # 增加城市选择框宽度 + self.city_combo.setStyleSheet("QComboBox { font-size: 12px; padding: 3px; }") # 增大字体和间距 self.city_combo.addItems([ '自动定位', '北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', # 一线城市 @@ -487,12 +694,40 @@ class WordRibbon(QFrame): self.city_combo.currentTextChanged.connect(self.on_city_changed) # 刷新按钮 - self.refresh_weather_btn = QPushButton("刷新天气") - self.refresh_weather_btn.clicked.connect(self.on_refresh_weather) - self.refresh_weather_btn.setFixedSize(80, 25) - - weather_layout.addWidget(self.city_combo) - weather_layout.addWidget(self.refresh_weather_btn) + self.refresh_weather_btn = QPushButton("🔄 刷新") + self.refresh_weather_btn.setFixedSize(60, 30) # 增大刷新按钮尺寸 + self.refresh_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }") + self.refresh_weather_btn.setToolTip("刷新天气") + + # 悬浮窗口按钮 + self.floating_weather_btn = QPushButton("🪟 悬浮") + self.floating_weather_btn.setFixedSize(60, 30) + self.floating_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }") + self.floating_weather_btn.setToolTip("切换天气悬浮窗口") + + # 每日谏言悬浮窗口按钮 + self.floating_quote_btn = QPushButton("📜 悬浮") + self.floating_quote_btn.setFixedSize(60, 30) + self.floating_quote_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }") + self.floating_quote_btn.setToolTip("切换每日谏言悬浮窗口") + + # 日历悬浮窗口按钮 + self.floating_calendar_btn = QPushButton("📅 悬浮") + self.floating_calendar_btn.setFixedSize(60, 30) + self.floating_calendar_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }") + self.floating_calendar_btn.setToolTip("切换日历悬浮窗口") + + control_layout.addWidget(self.city_combo) + control_layout.addWidget(self.refresh_weather_btn) + control_layout.addWidget(self.floating_weather_btn) + control_layout.addWidget(self.floating_quote_btn) + control_layout.addWidget(self.floating_calendar_btn) + + # 添加右侧弹性空间,确保内容居中 + control_layout.addStretch() + + weather_layout.addLayout(weather_display_layout) + weather_layout.addLayout(control_layout) weather_group.setLayout(weather_layout) self.weather_group = weather_group @@ -521,7 +756,7 @@ class WordRibbon(QFrame): if self.quote_group is not None: return self.quote_group - quote_group = self.create_ribbon_group("每日一言") + quote_group = self.create_ribbon_group("每日一言", is_special_group=True) quote_layout = QVBoxLayout() # 创建第一行:类型选择下拉框和刷新按钮 @@ -535,7 +770,6 @@ class WordRibbon(QFrame): # 刷新按钮 self.refresh_quote_btn = QPushButton("刷新箴言") - self.refresh_quote_btn.clicked.connect(self.on_refresh_quote) self.refresh_quote_btn.setFixedSize(80, 25) # 添加到第一行布局 @@ -584,31 +818,66 @@ class WordRibbon(QFrame): self.quote_group = None self.quote_visible = False - def create_ribbon_group(self, title): - """创建功能区组""" - group = QGroupBox(title) - group.setStyleSheet(""" - QGroupBox { - font-size: 11px; - font-weight: normal; - color: #333333; - border: 1px solid #e1e1e1; - border-radius: 0px; - margin-top: 5px; - padding-top: 5px; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px 0 5px; - } - """) - return group + def _update_group_style(self, group): + """更新组样式以适配当前主题""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + group.setStyleSheet(f""" + QGroupBox {{ + font-size: 11px; + font-weight: normal; + color: {colors['text']}; + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 0px; + margin-top: 5px; + padding-top: 5px; + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + color: {colors['text']}; + /* 确保标题不会被截断 */ + white-space: nowrap; + text-overflow: clip; /* 不显示省略号 */ + overflow: visible; /* 允许内容溢出 */ + }} + """) + else: + group.setStyleSheet(f""" + QGroupBox {{ + font-size: 11px; + font-weight: normal; + color: {colors['text']}; + background-color: {colors['surface']}; + border: 1px solid {colors['border']}; + border-radius: 0px; + margin-top: 5px; + padding-top: 5px; + }} + QGroupBox::title {{ + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + color: {colors['text']}; + /* 确保标题不会被截断 */ + white-space: nowrap; + text-overflow: clip; /* 不显示省略号 */ + overflow: visible; /* 允许内容溢出 */ + }} + """) def on_refresh_weather(self): """刷新天气按钮点击处理""" pass + def on_floating_weather(self): + """悬浮窗口按钮点击处理""" + pass + def on_city_changed(self, city): """城市选择变化处理""" pass @@ -685,98 +954,268 @@ class WordRibbon(QFrame): btn.setText(text) btn.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btn.setFixedSize(60, 60) - btn.setStyleSheet(""" - QToolButton { - border: 1px solid transparent; - border-radius: 3px; - background-color: transparent; - font-size: 11px; - color: #333333; - } - QToolButton:hover { - background-color: #f0f0f0; - border: 1px solid #d0d0d0; - } - QToolButton:pressed { - background-color: #e1e1e1; - border: 1px solid #c0c0c0; - } - """) + + # 连接主题切换信号以动态更新样式 + theme_manager.theme_changed.connect(lambda: self._update_button_style(btn)) + + # 立即应用当前主题样式 + self._update_button_style(btn) + return btn + def _update_button_style(self, btn): + """更新按钮样式以适配当前主题""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + btn.setStyleSheet(f""" + QToolButton {{ + border: 1px solid transparent; + border-radius: 3px; + background-color: transparent; + font-size: 11px; + color: {colors['text']}; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['border']}; + }} + QToolButton:pressed {{ + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + }} + """) + else: + btn.setStyleSheet(f""" + QToolButton {{ + border: 1px solid transparent; + border-radius: 3px; + background-color: transparent; + font-size: 11px; + color: {colors['text']}; + }} + QToolButton:hover {{ + background-color: #f0f0f0; + border: 1px solid #d0d0d0; + }} + QToolButton:pressed {{ + background-color: #e1e1e1; + border: 1px solid #c0c0c0; + }} + """) + def create_toggle_button(self, text, icon_name): - """创建切换按钮""" + """创建切换按钮 - 现代极简主义风格""" btn = QToolButton() btn.setText(text) btn.setCheckable(True) btn.setToolButtonStyle(Qt.ToolButtonTextOnly) # 根据文本长度设置宽度,中文字符需要更宽 if len(text) <= 1: - btn.setFixedSize(30, 25) # 单个字符(如B、I、U) + btn.setFixedSize(32, 28) # 单个字符(如B、I、U) elif len(text) <= 2: - btn.setFixedSize(45, 25) # 两个字符(如"居中") + btn.setFixedSize(48, 28) # 两个字符(如"居中") else: - btn.setFixedSize(60, 25) # 三个字符及以上(如"左对齐"、"两端对齐") - btn.setStyleSheet(""" - QToolButton { - border: 1px solid #d0d0d0; - border-radius: 2px; - background-color: transparent; - font-size: 12px; - font-weight: bold; - color: #333333; - } - QToolButton:hover { - background-color: #f0f0f0; - } - QToolButton:checked { - background-color: #e1e1e1; - border: 1px solid #c0c0c0; - } - """) + btn.setFixedSize(64, 28) # 三个字符及以上(如"左对齐"、"两端对齐") + + # 连接主题切换信号以动态更新样式 + theme_manager.theme_changed.connect(lambda: self._update_toggle_button_style(btn)) + + # 立即应用当前主题样式 + self._update_toggle_button_style(btn) + return btn + def _update_toggle_button_style(self, btn): + """更新切换按钮样式以适配当前主题""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + btn.setStyleSheet(f""" + QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: {colors['text']}; + font-size: 13px; + font-weight: 500; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['border']}; + }} + QToolButton:pressed {{ + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + }} + QToolButton:checked {{ + background-color: {colors['accent']}; + border: 1px solid {colors['accent']}; + color: {colors['surface']}; + }} + """) + else: + btn.setStyleSheet(f""" + QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: #4a5568; + font-size: 13px; + font-weight: 500; + }} + QToolButton:hover {{ + background-color: #f7fafc; + border: 1px solid #e2e8f0; + }} + QToolButton:pressed {{ + background-color: #edf2f7; + border: 1px solid #cbd5e0; + }} + QToolButton:checked {{ + background-color: #ebf8ff; + border: 1px solid #bee3f8; + color: #3182ce; + }} + """) + def create_color_button(self, text, icon_name): - """创建颜色选择按钮""" + """创建颜色选择按钮 - 现代极简主义风格""" btn = QToolButton() btn.setText(text) btn.setToolButtonStyle(Qt.ToolButtonTextOnly) - btn.setFixedSize(30, 25) - btn.setStyleSheet(""" - QToolButton { - border: 1px solid #d0d0d0; - border-radius: 2px; - background-color: transparent; - font-size: 12px; - font-weight: bold; - color: #333333; - } - QToolButton:hover { - background-color: #f0f0f0; - } - QToolButton:pressed { - background-color: #e1e1e1; - border: 1px solid #c0c0c0; - } - """) + btn.setFixedSize(32, 28) + + # 连接主题切换信号以动态更新样式 + theme_manager.theme_changed.connect(lambda: self._update_color_button_style(btn)) + + # 立即应用当前主题样式 + self._update_color_button_style(btn) + return btn + + def _update_color_button_style(self, btn): + """更新颜色按钮样式以适配当前主题""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + btn.setStyleSheet(f""" + QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: {colors['text']}; + font-size: 13px; + font-weight: 500; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['border']}; + }} + QToolButton:pressed {{ + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + }} + """) + else: + btn.setStyleSheet(f""" + QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: #4a5568; + font-size: 13px; + font-weight: 500; + }} + QToolButton:hover {{ + background-color: #f7fafc; + border: 1px solid #e2e8f0; + }} + QToolButton:pressed {{ + background-color: #edf2f7; + border: 1px solid #cbd5e0; + }} + """) + + def create_style_button(self, text): + """创建样式按钮 - 现代极简主义风格""" + btn = QPushButton(text) + btn.setFixedSize(60, 28) + + # 连接主题切换信号以动态更新样式 + theme_manager.theme_changed.connect(lambda: self._update_style_button_style(btn)) + + # 立即应用当前主题样式 + self._update_style_button_style(btn) + + return btn + + def _update_style_button_style(self, btn): + """更新样式按钮样式以适配当前主题""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + btn.setStyleSheet(f""" + QPushButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: {colors['text']}; + font-size: 12px; + font-weight: 500; + text-align: center; + }} + QPushButton:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['border']}; + }} + QPushButton:pressed {{ + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + }} + """) + else: + btn.setStyleSheet(f""" + QPushButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: #4a5568; + font-size: 12px; + font-weight: 500; + text-align: center; + }} + QPushButton:hover {{ + background-color: #f7fafc; + border: 1px solid #e2e8f0; + }} + QPushButton:pressed {{ + background-color: #edf2f7; + border: 1px solid #cbd5e0; + }} + """) class WordStatusBar(QStatusBar): def __init__(self, parent=None): super().__init__(parent) self.setup_ui() + # 连接主题切换信号 + theme_manager.theme_changed.connect(self.apply_theme) + # 应用初始主题 + self.apply_theme() def setup_ui(self): - """设置状态栏""" - self.setStyleSheet(""" - QStatusBar { - background-color: #f3f2f1; - border-top: 1px solid #d0d0d0; - font-size: 11px; - color: #333333; - } - """) - + """设置状态栏 - 现代极简主义风格""" # 添加状态栏项目 self.page_label = QLabel("第 1 页,共 1 页") self.words_label = QLabel("字数: 0") @@ -787,12 +1226,61 @@ class WordStatusBar(QStatusBar): self.addPermanentWidget(self.words_label) self.addPermanentWidget(self.language_label) self.addPermanentWidget(self.input_mode_label) + + def apply_theme(self): + """应用当前主题到状态栏""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + self.setStyleSheet(f""" + QStatusBar {{ + background-color: {colors['surface']}; + border-top: 1px solid {colors['border']}; + font-size: 12px; + color: {colors['text_secondary']}; + font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; + padding: 8px; + }} + QStatusBar QLabel {{ + background-color: transparent; + padding: 4px 8px; + border-radius: 4px; + color: {colors['text_secondary']}; + }} + QStatusBar QLabel:hover {{ + background-color: {colors['surface_hover']}; + }} + """) + else: + self.setStyleSheet(""" + QStatusBar { + background-color: #ffffff; + border-top: 1px solid #e2e8f0; + font-size: 12px; + color: #718096; + font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; + padding: 8px; + } + QStatusBar QLabel { + background-color: transparent; + padding: 4px 8px; + border-radius: 4px; + } + QStatusBar QLabel:hover { + background-color: #f7fafc; + } + """) class WordTextEdit(QTextEdit): def __init__(self, parent=None): super().__init__(parent) self.setup_ui() self.input_processor = None # 输入处理器引用 + # 连接主题切换信号 + theme_manager.theme_changed.connect(self.apply_theme) + # 应用初始主题 + self.apply_theme() def set_input_processor(self, processor): """设置输入处理器""" @@ -872,19 +1360,7 @@ class WordTextEdit(QTextEdit): return super().inputMethodQuery(query) def setup_ui(self): - """设置文本编辑区域样式""" - self.setStyleSheet(""" - QTextEdit { - background-color: #ffffff; - border: 1px solid #d0d0d0; - font-family: 'Calibri', 'Microsoft YaHei', '微软雅黑', sans-serif; - font-size: 12pt; - color: #000000; - padding: 20px; - line-height: 1.5; - } - """) - + """设置文本编辑区域样式 - 现代极简主义风格""" # 设置页面边距和背景 self.setViewportMargins(50, 50, 50, 50) @@ -898,6 +1374,46 @@ class WordTextEdit(QTextEdit): # 设置光标宽度 self.setCursorWidth(2) + + def apply_theme(self): + """应用当前主题到文本编辑区域""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + self.setStyleSheet(f""" + QTextEdit {{ + background-color: {colors['surface']}; + border: none; + font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 15px; + color: {colors['text']}; + padding: 40px; + line-height: 1.7; + selection-background-color: rgba(66, 153, 225, 0.3); + selection-color: {colors['text']}; + }} + QTextEdit:focus {{ + outline: none; + }} + """) + else: + self.setStyleSheet(""" + QTextEdit { + background-color: #ffffff; + border: none; + font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 15px; + color: #2d3748; + padding: 40px; + line-height: 1.7; + selection-background-color: rgba(66, 153, 225, 0.2); + selection-color: #2d3748; + } + QTextEdit:focus { + outline: none; + } + """) class WeatherAPI: def __init__(self): @@ -1016,14 +1532,17 @@ class WeatherAPI: # 使用免费的天气API url = f"http://t.weather.sojson.com/api/weather/city/{city_id}" print(f"获取当前天气: {url}") - response = requests.get(url, timeout=10) + response = requests.get(url, timeout=5) # 减少超时时间 response.raise_for_status() data = response.json() - print(f"当前天气响应: {data}") - if data['status'] == 200: - city_info = data['cityInfo'] - current_data = data['data'] + if data.get('status') == 200: + city_info = data.get('cityInfo', {}) + current_data = data.get('data', {}) + + if not current_data: + print("天气数据为空") + return None # 获取生活提示信息 life_tips = [] @@ -1047,20 +1566,35 @@ class WeatherAPI: while len(life_tips) < 3 and default_tips: life_tips.append(default_tips.pop(0)) + # 安全获取天气数据 + wendu = current_data.get('wendu', 'N/A') + shidu = current_data.get('shidu', 'N/A') + first_forecast = forecast[0] if forecast else {} + weather_info = { - 'temp': current_data['wendu'], - 'feels_like': current_data['wendu'], # 没有体感温度,用实际温度代替 - 'weather': current_data['forecast'][0]['type'], - 'humidity': current_data['shidu'].replace('%', ''), - 'wind_dir': current_data['forecast'][0]['fx'], + 'temp': wendu, + 'feels_like': wendu, # 没有体感温度,用实际温度代替 + 'weather': first_forecast.get('type', '晴'), # 默认晴天 + 'humidity': shidu.replace('%', '') if shidu != 'N/A' else '50', + 'wind_dir': first_forecast.get('fx', '无风'), 'wind_scale': '1', # 没有风力等级,用默认值 - 'vis': current_data['forecast'][0]['high'], # 用最高温作为可见度 + 'vis': first_forecast.get('high', '高温 15℃'), # 用最高温作为可见度 'pressure': '1013', # 没有气压,用默认值 'life_tips': life_tips # 添加生活提示信息 } print(f"解析后的天气信息: {weather_info}") return weather_info - print(f"获取天气失败,状态码: {data.get('status')}") + else: + print(f"获取天气失败,状态码: {data.get('status')}") + return None + except requests.exceptions.Timeout: + print("获取天气超时") + return None + except requests.exceptions.RequestException as e: + print(f"网络请求失败: {e}") + return None + except json.JSONDecodeError as e: + print(f"JSON解析失败: {e}") return None except Exception as e: print(f"获取当前天气失败: {e}") @@ -1123,27 +1657,150 @@ class WeatherAPI: def get_isp_info(self): """获取ISP信息""" try: - url = "http://ip-api.com/json/" - response = requests.get(url, timeout=5) - response.raise_for_status() + # 尝试多个ISP信息服务以提高成功率 + + # 方法1: 使用ip-api.com接口 + try: + url = "http://ip-api.com/json/" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + + data = response.json() + if data.get('status') == 'success': + isp = data.get('isp', '') + org = data.get('org', '') + as_info = data.get('as', '') + country = data.get('country', '') + return f"{isp} {org} {as_info} {country}".strip() + except Exception as e: + print(f"ip-api ISP信息获取失败: {e}") + pass + + # 方法2: 使用ipinfo.io接口 + try: + url = "https://ipinfo.io/json" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + + data = response.json() + if 'org' in data: + org = data.get('org', '') + country = data.get('country', '') + return f"{org} {country}".strip() + except Exception as e: + print(f"ipinfo ISP信息获取失败: {e}") + pass + + # 方法3: 使用httpbin.org获取基础信息 + try: + url = "https://httpbin.org/ip" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + # 这个接口主要用于获取IP,不是ISP信息,但可以作为备选 + data = response.json() + origin = data.get('origin', '') + if origin: + return f"IP: {origin}" + except Exception as e: + print(f"httpbin ISP信息获取失败: {e}") + pass - data = response.json() - if data.get('status') == 'success': - isp = data.get('isp', '') - org = data.get('org', '') - as_info = data.get('as', '') - return f"{isp} {org} {as_info}".strip() return None except Exception as e: - print(f"获取ISP信息失败: {e}") + print(f"获取ISP信息总体失败: {e}") return None def get_location_by_ip(self): """通过IP地址获取用户位置""" try: + # 首先获取公网IP地址 + ip_address = None + try: + # 使用多个IP获取服务确保能获取到公网IP + ip_services = [ + "https://api.ipify.org", + "https://icanhazip.com", + "https://ident.me", + "https://ipecho.net/plain", + "https://myexternalip.com/raw" + ] + + for service in ip_services: + try: + response = requests.get(service, timeout=3) + if response.status_code == 200: + ip_address = response.text.strip() + # 验证是否为有效的IPv4地址 + import re + if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', ip_address): + print(f"获取到公网IP: {ip_address}") + break + else: + ip_address = None + except: + continue + + if not ip_address: + print("无法获取公网IP地址") + except Exception as e: + print(f"获取IP地址失败: {e}") + # 尝试多个免费的IP地理位置API - # API 1: 搜狐IP接口(HTTP,无SSL问题) + # API 1: 使用ip-api.com接口(更稳定的免费服务,支持HTTPS) + try: + if ip_address: + url = f"https://ip-api.com/json/{ip_address}" + else: + url = "https://ip-api.com/json/" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + + data = response.json() + if data.get('status') == 'success': + city = data.get('city', '') + region = data.get('regionName', '') + country = data.get('country', '') + if city and city not in ['null', 'None', '']: + print(f"ip-api定位成功: {city}, {region}, {country}") + # 如果城市信息不完整,尝试用地区信息补充 + if len(city) < 2 and region: + city = region + return city + except Exception as e: + print(f"ip-api接口失败: {e}") + pass + + # API 2: 使用ipinfo.io接口(需要处理免费版限制) + try: + if ip_address: + url = f"https://ipinfo.io/{ip_address}/json" + else: + url = "https://ipinfo.io/json" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + + data = response.json() + if 'city' in data: + city = data.get('city', '') + region = data.get('region', '') + country = data.get('country', '') + if city and city not in ['null', 'None', '']: + print(f"ipinfo定位成功: {city}, {region}, {country}") + # 如果城市信息不完整,尝试用地区信息补充 + if len(city) < 2 and region: + city = region + return city + except Exception as e: + print(f"ipinfo接口失败: {e}") + pass + + # API 3: 搜狐IP接口(HTTP,无SSL问题) try: url = "http://pv.sohu.com/cityjson?ie=utf-8" response = requests.get(url, timeout=5) @@ -1168,7 +1825,7 @@ class WeatherAPI: print(f"搜狐IP接口失败: {e}") pass - # API 2: 使用pconline接口(HTTP) + # API 4: 使用pconline接口(HTTP) try: url = "http://whois.pconline.com.cn/ipJson.jsp" response = requests.get(url, timeout=5) @@ -1191,26 +1848,14 @@ class WeatherAPI: print(f"pconline接口失败: {e}") pass - # API 3: 使用ip-api.com接口(更稳定的免费服务) - try: - url = "http://ip-api.com/json/" - response = requests.get(url, timeout=5) - response.raise_for_status() - - data = response.json() - if data.get('status') == 'success': - city = data.get('city', '') - if city: - print(f"ip-api定位成功: {city}") - return city - except Exception as e: - print(f"ip-api接口失败: {e}") - pass - - # API 4: 使用淘宝IP接口 + # API 5: 使用淘宝IP接口 try: - url = "http://ip.taobao.com/outGetIpInfo" - params = {'ip': '', 'accessKey': 'alibaba-inc'} + if ip_address: + url = "http://ip.taobao.com/outGetIpInfo" + params = {'ip': ip_address, 'accessKey': 'alibaba-inc'} + else: + url = "http://ip.taobao.com/outGetIpInfo" + params = {'ip': '', 'accessKey': 'alibaba-inc'} response = requests.get(url, params=params, timeout=5) response.raise_for_status() @@ -1229,25 +1874,42 @@ class WeatherAPI: return None except Exception as e: print(f"IP定位总体失败: {e}") - return None + # 返回默认城市而不是None,确保天气功能仍然可用 + return "北京" def get_current_location(self): """获取当前位置信息""" try: # 首先尝试通过IP获取位置 - city = self.get_location_by_ip() - if city: - print(f"通过IP定位成功: {city}") + location_result = self.get_location_by_ip() + + # 检查是否是默认城市(表示IP定位失败) + if location_result == "北京": + print("IP定位失败,使用默认城市") + print("自动定位失败,建议手动选择城市") + return None + + if location_result: + print(f"通过IP定位成功: {location_result}") # 检查是否是教育网或特殊网络环境 isp_info = self.get_isp_info() - if isp_info and ('教育网' in isp_info or 'CERNET' in isp_info or 'University' in isp_info): + if isp_info and ('教育网' in isp_info or 'CERNET' in isp_info or 'University' in isp_info or '大学' in isp_info): print(f"检测到教育网环境: {isp_info}") print("教育网IP定位可能不准确,建议手动选择城市") # 教育网环境下,如果定位到北京,可能是IP分配问题 - if city.lower() in ['beijing', '北京', 'haidian', '海淀']: + if isinstance(location_result, str) and location_result.lower() in ['beijing', '北京', 'haidian', '海淀']: print("提示:教育网环境下北京定位可能是网络出口导致的") - return {'city': city, 'note': '教育网环境,定位可能不准确', 'isp': isp_info} + return {'city': location_result, 'note': '教育网环境,定位可能不准确', 'isp': isp_info} + + # 处理返回结果格式 + city = None + if isinstance(location_result, dict) and 'city' in location_result: + city = location_result['city'] + elif isinstance(location_result, str): + city = location_result + else: + city = str(location_result) # 智能处理 - 如果是区级单位,映射到市级城市 district_to_city_map = { @@ -1395,6 +2057,7 @@ class WeatherAPI: except Exception as e: print(f"获取当前位置失败: {e}") + # 即使出现异常也返回None而不是抛出异常,确保程序继续运行 return None def get_city_weather_by_name(self, city_name): diff --git a/src/word_main_window.py b/src/word_main_window.py index 2a7f7da..28973f1 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -1,13 +1,15 @@ # word_main_window.py import sys import os +import platform from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QSplitter, QFrame, QMenuBar, QAction, QFileDialog, QMessageBox, QApplication, QDialog, QLineEdit, QCheckBox, QPushButton, QListWidget, - QListWidgetItem, QScrollArea) + QListWidgetItem, QScrollArea, QSizePolicy, + QGraphicsScene, QGraphicsView) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect, QByteArray, QBuffer, QIODevice -from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextDocument, QImage, QTextImageFormat, QTextFormat +from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextDocument, QImage, QTextImageFormat, QTextFormat, QTextBlockFormat from ui.word_style_ui import (WordRibbon, WordStatusBar, WordTextEdit, ) @@ -16,6 +18,10 @@ from typing_logic import TypingLogic from ui.word_style_ui import WeatherAPI from file_parser import FileParser from input_handler.input_processor import InputProcessor +from ui.calendar_widget import CalendarWidget +from ui.weather_floating_widget import WeatherFloatingWidget +from ui.quote_floating_widget import QuoteFloatingWidget +from ui.calendar_floating_widget import CalendarFloatingWidget # 导入主题管理器 from ui.theme_manager import theme_manager @@ -83,6 +89,13 @@ class WordStyleMainWindow(QMainWindow): self.learning_text = "" # 学习模式下的文本内容 self.cursor_position = 0 # 光标位置 + # 学习模式窗口引用和同步标记 + self.learning_window = None # 学习模式窗口引用 + self.sync_from_learning = False # 从学习模式同步内容的标记 + + # DeepSeek对话窗口引用 + self.deepseek_dialog = None # DeepSeek对话窗口引用 + # 统一文档内容管理 self.unified_document_content = "" # 统一文档内容 self.last_edit_mode = "typing" # 上次编辑模式 @@ -97,6 +110,27 @@ class WordStyleMainWindow(QMainWindow): self.network_service = NetworkService() self.weather_api = WeatherAPI() + # 初始化日历组件 + self.calendar_widget = CalendarWidget(self) + self.calendar_widget.hide() # 默认隐藏 + + # 初始化天气悬浮窗口 + self.weather_floating_widget = WeatherFloatingWidget(self) + self.weather_floating_widget.hide() # 默认隐藏 + self.weather_floating_widget.closed.connect(self.on_weather_floating_closed) + self.weather_floating_widget.refresh_requested.connect(self.refresh_weather) + + self.quote_floating_widget = QuoteFloatingWidget(self) + self.quote_floating_widget.hide() # 默认隐藏 + self.quote_floating_widget.closed.connect(self.on_quote_floating_closed) + self.quote_floating_widget.refresh_requested.connect(self.refresh_daily_quote) + self.quote_floating_widget.insert_requested.connect(self.insert_quote_to_cursor) + + self.calendar_floating_widget = CalendarFloatingWidget(self) + self.calendar_floating_widget.hide() # 默认隐藏 + self.calendar_floating_widget.closed.connect(self.on_calendar_floating_closed) + self.calendar_floating_widget.date_selected.connect(self.insert_date_to_cursor) + # 设置窗口属性 self.setWindowTitle("文档1 - MagicWord") self.setGeometry(100, 100, 1200, 800) @@ -123,6 +157,14 @@ class WordStyleMainWindow(QMainWindow): self.ribbon.on_refresh_weather = self.refresh_weather self.ribbon.on_city_changed = self.on_city_changed + # 连接Ribbon的悬浮窗口按钮信号 + if hasattr(self.ribbon, 'floating_weather_btn'): + self.ribbon.floating_weather_btn.clicked.connect(self.toggle_floating_weather) + if hasattr(self.ribbon, 'floating_quote_btn'): + self.ribbon.floating_quote_btn.clicked.connect(self.toggle_floating_quote) + if hasattr(self.ribbon, 'floating_calendar_btn'): + self.ribbon.floating_calendar_btn.clicked.connect(self.toggle_floating_calendar) + # 初始化时刷新天气 self.refresh_weather() @@ -131,9 +173,8 @@ class WordStyleMainWindow(QMainWindow): # 连接主题切换信号 theme_manager.theme_changed.connect(self.on_theme_changed) - # 设置默认为白色模式(禁用自动检测) - theme_manager.enable_auto_detection(False) - theme_manager.set_dark_theme(False) + # 启用系统主题自动检测 + theme_manager.enable_auto_detection(True) # 应用当前主题 self.apply_theme() @@ -314,6 +355,11 @@ class WordStyleMainWindow(QMainWindow): # 更新功能区下拉框样式 if hasattr(self, 'ribbon'): self.update_ribbon_styles(is_dark) + + # 更新日历组件样式 + if hasattr(self, 'calendar_widget') and self.calendar_widget is not None: + # 日历组件有自己的主题管理机制,只需触发其主题更新 + self.calendar_widget.apply_theme() def update_ribbon_styles(self, is_dark): """更新功能区样式""" @@ -425,8 +471,14 @@ class WordStyleMainWindow(QMainWindow): print(f"获取到天气数据: {weather_data}") # 直接传递原始数据,update_weather_display会处理嵌套结构 self.update_weather_display(weather_data) + # 同步更新天气悬浮窗口 + if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible(): + self.weather_floating_widget.update_weather(weather_data) else: print(f"无法获取城市 {city} 的天气数据") + # 显示错误信息到天气悬浮窗口 + if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible(): + self.weather_floating_widget.update_weather({'error': '无法获取天气数据'}) def refresh_weather(self): """刷新天气""" @@ -504,6 +556,14 @@ class WordStyleMainWindow(QMainWindow): background-color: #f3f2f1; } """) + + # 创建日历组件并添加到窗口中(默认隐藏) + try: + from ui.calendar_widget import CalendarWidget + self.calendar_widget = CalendarWidget(self) + self.calendar_widget.hide() # 默认隐藏 + except Exception as e: + print(f"创建日历组件失败: {e}") def create_menu_bar(self): """创建菜单栏""" @@ -633,6 +693,8 @@ class WordStyleMainWindow(QMainWindow): self.learning_mode_action = QAction('学习模式', self) self.learning_mode_action.setCheckable(True) self.learning_mode_action.setChecked(False) + # 设置学习模式快捷键 (Qt会自动在macOS上映射Ctrl为Cmd) + self.learning_mode_action.setShortcut('Ctrl+L') self.learning_mode_action.triggered.connect(lambda: self.set_view_mode("learning")) view_mode_menu.addAction(self.learning_mode_action) @@ -640,22 +702,7 @@ class WordStyleMainWindow(QMainWindow): # 附加工具功能 weather_menu = view_menu.addMenu('附加工具') - - # 显示天气工具组 - self.show_weather_tools_action = QAction('显示天气工具', self) - self.show_weather_tools_action.setCheckable(True) - self.show_weather_tools_action.setChecked(False) # 默认不显示 - self.show_weather_tools_action.triggered.connect(self.toggle_weather_tools) - weather_menu.addAction(self.show_weather_tools_action) - - # 显示每日一言工具组 - self.show_quote_tools_action = QAction('显示每日一言工具', self) - self.show_quote_tools_action.setCheckable(True) - self.show_quote_tools_action.setChecked(False) # 默认不显示 - self.show_quote_tools_action.triggered.connect(self.toggle_quote_tools) - weather_menu.addAction(self.show_quote_tools_action) - - weather_menu.addSeparator() + # 刷新天气 refresh_weather_action = QAction('刷新天气', self) @@ -667,21 +714,90 @@ class WordStyleMainWindow(QMainWindow): show_weather_action = QAction('显示详细天气', self) show_weather_action.triggered.connect(self.show_detailed_weather) weather_menu.addAction(show_weather_action) + + # 天气悬浮窗口 + toggle_floating_weather_action = QAction('天气悬浮窗口', self) + toggle_floating_weather_action.triggered.connect(self.toggle_floating_weather) + weather_menu.addAction(toggle_floating_weather_action) + + # 每日谏言悬浮窗口切换动作 + toggle_floating_quote_action = QAction('每日谏言悬浮窗口', self) + toggle_floating_quote_action.triggered.connect(self.toggle_floating_quote) + weather_menu.addAction(toggle_floating_quote_action) + + # 日历悬浮窗口切换动作 + toggle_floating_calendar_action = QAction('日历悬浮窗口', self) + toggle_floating_calendar_action.triggered.connect(self.toggle_floating_calendar) + weather_menu.addAction(toggle_floating_calendar_action) # 插入菜单 insert_menu = menubar.addMenu('插入(I)') + # 插入图片功能 + insert_image_action = QAction('插入图片', self) + insert_image_action.triggered.connect(self.insert_image_in_typing_mode) + insert_menu.addAction(insert_image_action) + + # 插入天气信息功能 + insert_weather_action = QAction('插入天气信息', self) + insert_weather_action.triggered.connect(self.insert_weather_info) + insert_menu.addAction(insert_weather_action) + + # 插入每日一句名言功能 + insert_quote_action = QAction('插入每日一句名言', self) + insert_quote_action.triggered.connect(self.insert_daily_quote) + insert_menu.addAction(insert_quote_action) + + # 插入古诗词功能 + insert_poetry_action = QAction('插入古诗词', self) + insert_poetry_action.triggered.connect(self.insert_chinese_poetry) + insert_menu.addAction(insert_poetry_action) + # 绘图菜单 paint_menu = menubar.addMenu('绘图(D)') + # 添加日历按钮 + calendar_action = QAction('日历', self) + calendar_action.triggered.connect(self.toggle_calendar) + paint_menu.addAction(calendar_action) + # 设计菜单 design_menu = menubar.addMenu('设计(G)') + # 导出子菜单 + export_menu = design_menu.addMenu('导出') + + # 导出为HTML + export_html_action = QAction('导出为HTML', self) + export_html_action.triggered.connect(self.export_as_html) + export_menu.addAction(export_html_action) + + # 导出为PDF + export_pdf_action = QAction('导出为PDF', self) + export_pdf_action.triggered.connect(self.export_as_pdf) + export_menu.addAction(export_pdf_action) + + # 导出为TXT + export_txt_action = QAction('导出为TXT', self) + export_txt_action.triggered.connect(self.export_as_txt) + export_menu.addAction(export_txt_action) + + # 导出为DOCX + export_docx_action = QAction('导出为DOCX', self) + export_docx_action.triggered.connect(self.export_as_docx) + export_menu.addAction(export_docx_action) + # 布局菜单 layout_menu = menubar.addMenu('布局(L)') # 引用菜单 reference_menu = menubar.addMenu('引用(R)') + # DeepSeek AI对话功能 + self.deepseek_dialog_action = QAction('DeepSeek AI对话', self) + self.deepseek_dialog_action.setShortcut('Ctrl+D') + self.deepseek_dialog_action.triggered.connect(self.open_deepseek_dialog) + reference_menu.addAction(self.deepseek_dialog_action) + # 邮件菜单 mail_menu = menubar.addMenu('邮件(M)') @@ -691,6 +807,17 @@ class WordStyleMainWindow(QMainWindow): # 开发工具菜单 developer_menu = menubar.addMenu('开发工具(Q)') + # 应用选项菜单 + app_menu = menubar.addMenu('应用选项(O)') + + # 小游戏子菜单 + games_menu = app_menu.addMenu('小游戏') + + # 贪吃蛇游戏 + snake_game_action = QAction('贪吃蛇', self) + snake_game_action.triggered.connect(self.launch_snake_game) + games_menu.addAction(snake_game_action) + # 帮助菜单 help_menu = menubar.addMenu('帮助(H)') @@ -805,6 +932,9 @@ class WordStyleMainWindow(QMainWindow): # 文本变化信号 self.text_edit.textChanged.connect(self.on_text_changed) + # 光标位置变化信号,用于更新按钮状态 + self.text_edit.cursorPositionChanged.connect(self.update_format_buttons) + # Ribbon按钮信号 # 标签栏已删除,相关代码已移除 @@ -817,6 +947,18 @@ class WordStyleMainWindow(QMainWindow): self.ribbon.underline_btn.clicked.connect(self.on_underline_clicked) self.ribbon.color_btn.clicked.connect(self.on_color_clicked) + # 样式按钮信号 + if hasattr(self.ribbon, 'heading1_btn'): + self.ribbon.heading1_btn.clicked.connect(self.on_heading1_clicked) + if hasattr(self.ribbon, 'heading2_btn'): + self.ribbon.heading2_btn.clicked.connect(self.on_heading2_clicked) + if hasattr(self.ribbon, 'heading3_btn'): + self.ribbon.heading3_btn.clicked.connect(self.on_heading3_clicked) + if hasattr(self.ribbon, 'heading4_btn'): + self.ribbon.heading4_btn.clicked.connect(self.on_heading4_clicked) + if hasattr(self.ribbon, 'body_text_btn'): + self.ribbon.body_text_btn.clicked.connect(self.on_body_text_clicked) + # 查找和替换按钮信号 if hasattr(self.ribbon, 'find_btn'): self.ribbon.find_btn.clicked.connect(self.show_find_dialog) @@ -825,17 +967,39 @@ class WordStyleMainWindow(QMainWindow): # 页面布局信号已在菜单中直接连接,无需在此重复连接 + # 段落对齐按钮信号 + if hasattr(self.ribbon, 'align_left_btn'): + self.ribbon.align_left_btn.clicked.connect(self.on_align_left_clicked) + if hasattr(self.ribbon, 'align_center_btn'): + self.ribbon.align_center_btn.clicked.connect(self.on_align_center_clicked) + if hasattr(self.ribbon, 'align_right_btn'): + self.ribbon.align_right_btn.clicked.connect(self.on_align_right_clicked) + if hasattr(self.ribbon, 'align_justify_btn'): + self.ribbon.align_justify_btn.clicked.connect(self.on_align_justify_clicked) + # 天气功能信号 if hasattr(self.ribbon, 'city_combo'): self.ribbon.city_combo.currentTextChanged.connect(self.on_city_changed) if hasattr(self.ribbon, 'refresh_weather_btn'): self.ribbon.refresh_weather_btn.clicked.connect(self.refresh_weather) + if hasattr(self.ribbon, 'floating_weather_btn'): + self.ribbon.floating_weather_btn.clicked.connect(self.toggle_floating_weather) + if hasattr(self.ribbon, 'floating_quote_btn'): + self.ribbon.floating_quote_btn.clicked.connect(self.toggle_floating_quote) + + # 日历组件信号 + if hasattr(self, 'calendar_widget'): + self.calendar_widget.date_selected.connect(self.insert_date_to_editor) def on_text_changed(self): """文本变化处理 - 根据视图模式处理文本变化""" # 如果正在加载文件,跳过处理 if self.is_loading_file: return + + # 检查是否是从学习模式同步内容,避免递归调用 + if hasattr(self, 'sync_from_learning') and self.sync_from_learning: + return # 根据当前视图模式处理 if self.view_mode == "learning": @@ -852,8 +1016,9 @@ class WordStyleMainWindow(QMainWindow): self.handle_learning_mode_typing() elif self.view_mode == "typing": - # 打字模式:可以自由打字 - self.handle_typing_mode_typing() + # 打字模式:可以自由打字,不自动处理内容 + # 只在用户主动操作时处理,避免内容被覆盖 + pass # 标记文档为已修改 if not self.is_modified: @@ -1282,10 +1447,162 @@ class WordStyleMainWindow(QMainWindow): if cursor.hasSelection(): self.status_bar.showMessage("字体颜色已设置,新输入的文本将使用该颜色", 2000) + def on_heading1_clicked(self): + """一级标题按钮点击处理""" + self.apply_heading_style(1) + + def on_heading2_clicked(self): + """二级标题按钮点击处理""" + self.apply_heading_style(2) + + def on_heading3_clicked(self): + """三级标题按钮点击处理""" + self.apply_heading_style(3) + + def on_heading4_clicked(self): + """四级标题按钮点击处理""" + self.apply_heading_style(4) + + def on_body_text_clicked(self): + """正文按钮点击处理""" + self.apply_body_text_style() + + def on_align_left_clicked(self): + """左对齐按钮点击处理""" + self.apply_alignment(Qt.AlignLeft) + + def on_align_center_clicked(self): + """居中对齐按钮点击处理""" + self.apply_alignment(Qt.AlignCenter) + + def on_align_right_clicked(self): + """右对齐按钮点击处理""" + self.apply_alignment(Qt.AlignRight) + + def on_align_justify_clicked(self): + """两端对齐按钮点击处理""" + self.apply_alignment(Qt.AlignJustify) + + def insert_date_to_editor(self, date_str): + """将选中的日期插入到编辑器中""" + # 获取当前光标位置 + cursor = self.text_edit.textCursor() + + # 在光标位置插入日期字符串 + cursor.insertText(date_str) + + # 更新文本编辑器的光标 + self.text_edit.setTextCursor(cursor) + + # 隐藏日历组件 + if hasattr(self, 'calendar_widget'): + self.calendar_widget.hide() + + # 更新状态栏提示 + self.status_bar.showMessage(f"已插入日期: {date_str}", 2000) + + def apply_heading_style(self, level): + """应用标题样式""" + cursor = self.text_edit.textCursor() + + # 创建字符格式 + char_format = QTextCharFormat() + + # 创建块格式(段落格式) + block_format = QTextBlockFormat() + block_format.setTopMargin(12) + block_format.setBottomMargin(6) + + # 根据标题级别设置样式 + if level == 1: + # 一级标题:24pt, 加粗 + char_format.setFontPointSize(24) + char_format.setFontWeight(QFont.Bold) + elif level == 2: + # 二级标题:18pt, 加粗 + char_format.setFontPointSize(18) + char_format.setFontWeight(QFont.Bold) + elif level == 3: + # 三级标题:16pt, 加粗 + char_format.setFontPointSize(16) + char_format.setFontWeight(QFont.Bold) + elif level == 4: + # 四级标题:14pt, 加粗 + char_format.setFontPointSize(14) + char_format.setFontWeight(QFont.Bold) + + # 应用格式 + if cursor.hasSelection(): + # 如果有选中文本,只更改选中文本的格式 + cursor.mergeCharFormat(char_format) + else: + # 如果没有选中文本,更改当前段落的格式 + cursor.setBlockFormat(block_format) + cursor.mergeCharFormat(char_format) + # 将光标移动到段落末尾并添加换行 + cursor.movePosition(QTextCursor.EndOfBlock) + cursor.insertText("\n") + + # 设置文本编辑器的默认格式 + self.text_edit.setCurrentCharFormat(char_format) + self.text_edit.textCursor().setBlockFormat(block_format) + + def apply_body_text_style(self): + """应用正文样式""" + cursor = self.text_edit.textCursor() + + # 创建字符格式 + char_format = QTextCharFormat() + char_format.setFontPointSize(12) # 正文字号 + char_format.setFontWeight(QFont.Normal) # 正常粗细 + + # 创建块格式(段落格式) + block_format = QTextBlockFormat() + block_format.setTopMargin(0) + block_format.setBottomMargin(6) + + # 应用格式 + if cursor.hasSelection(): + # 如果有选中文本,只更改选中文本的格式 + cursor.mergeCharFormat(char_format) + else: + # 如果没有选中文本,更改当前段落的格式 + cursor.setBlockFormat(block_format) + cursor.mergeCharFormat(char_format) + + # 设置文本编辑器的默认格式 + self.text_edit.setCurrentCharFormat(char_format) + self.text_edit.textCursor().setBlockFormat(block_format) + + def apply_alignment(self, alignment): + """应用段落对齐方式""" + cursor = self.text_edit.textCursor() + + # 创建块格式(段落格式) + block_format = QTextBlockFormat() + block_format.setAlignment(alignment) + + # 应用格式 + if cursor.hasSelection(): + # 如果有选中文本,更改选中文本所在段落的对齐方式 + cursor.mergeBlockFormat(block_format) + else: + # 如果没有选中文本,更改当前段落的对齐方式 + cursor.setBlockFormat(block_format) + + # 更新文本编辑器的默认段落格式 + self.text_edit.textCursor().setBlockFormat(block_format) + def update_weather_display(self, weather_data): """更新天气显示""" if 'error' in weather_data: self.status_bar.showMessage(f"天气数据获取失败: {weather_data['error']}", 3000) + # 更新工具栏天气显示为错误状态 + if hasattr(self, 'ribbon'): + if hasattr(self.ribbon, 'weather_icon_label'): + self.ribbon.weather_icon_label.setText("❓") + if hasattr(self.ribbon, 'weather_temp_label'): + self.ribbon.weather_temp_label.setText("--°C") else: # 处理嵌套的天气数据结构 city = weather_data.get('city', '未知城市') @@ -1309,6 +1626,32 @@ class WordStyleMainWindow(QMainWindow): weather_message = f"{city}: {desc}, {temp}°C{temp_range}" self.status_bar.showMessage(weather_message, 5000) + # 更新工具栏天气图标和温度显示 + if hasattr(self, 'ribbon'): + # 更新天气图标 + if hasattr(self.ribbon, 'weather_icon_label') and desc != 'N/A': + emoji = self.ribbon.get_weather_emoji(desc) + self.ribbon.weather_icon_label.setText(emoji) + + # 更新温度显示 + if hasattr(self.ribbon, 'weather_temp_label') and temp != 'N/A': + # 计算平均温度(使用最高温和最低温的平均值) + avg_temp = temp + 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': + try: + avg_temp = (float(temp_max) + float(temp_min)) / 2 + avg_temp = round(avg_temp, 1) + except (ValueError, TypeError): + avg_temp = temp + + temp_str = f"{avg_temp}°C" if isinstance(avg_temp, (int, float)) else f"{temp}°C" + self.ribbon.weather_temp_label.setText(temp_str) + # 存储天气数据供其他功能使用(确保包含生活提示) self.current_weather_data = weather_data print(f"update_weather_display - 存储的current_weather_data包含life_tips: {self.current_weather_data.get('life_tips', [])}") @@ -1317,7 +1660,18 @@ class WordStyleMainWindow(QMainWindow): """手动刷新天气信息""" try: # 获取当前选择的城市 - current_city = self.ribbon.city_combo.currentText() + current_city = "自动定位" # 默认值 + + # 安全地获取当前城市选择 + if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'city_combo'): + current_city = self.ribbon.city_combo.currentText() + elif hasattr(self, 'weather_floating_widget') and hasattr(self.weather_floating_widget, 'city_combo'): + # 如果Ribbon中的city_combo不可用,尝试从天气悬浮窗口获取 + current_city = self.weather_floating_widget.city_combo.currentText() + else: + # 如果都没有,使用默认值 + current_city = "自动定位" + print(f"刷新天气 - 当前选择的城市: {current_city}") if current_city == '自动定位': @@ -1338,9 +1692,15 @@ class WordStyleMainWindow(QMainWindow): print(f"refresh_weather - 原始数据包含life_tips: {weather_data.get('life_tips', [])}") print(f"refresh_weather - formatted_data包含life_tips: {formatted_data.get('life_tips', [])}") self.update_weather_display(formatted_data) + # 同步更新天气悬浮窗口 + if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible(): + self.weather_floating_widget.update_weather(formatted_data) self.status_bar.showMessage("天气数据已刷新", 2000) else: self.status_bar.showMessage("天气数据刷新失败,请检查API密钥", 3000) + # 显示错误信息到天气悬浮窗口 + if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible(): + self.weather_floating_widget.update_weather({'error': '天气数据刷新失败'}) except Exception as e: self.status_bar.showMessage(f"天气刷新失败: {str(e)}", 3000) @@ -1436,6 +1796,87 @@ class WordStyleMainWindow(QMainWindow): self.refresh_weather() dialog.close() + def toggle_floating_weather(self): + """切换天气悬浮窗口显示/隐藏""" + if hasattr(self, 'weather_floating_widget'): + if self.weather_floating_widget.isVisible(): + self.weather_floating_widget.hide() + self.status_bar.showMessage("天气悬浮窗口已隐藏", 2000) + else: + self.weather_floating_widget.show() + # 确保窗口在屏幕内 + self.weather_floating_widget.move(100, 100) + self.status_bar.showMessage("天气悬浮窗口已显示", 2000) + # 同步当前城市选择 + if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'city_combo'): + current_city = self.ribbon.city_combo.currentText() + self.weather_floating_widget.set_current_city(current_city) + # 如果有天气数据,更新显示 + if hasattr(self, 'current_weather_data') and self.current_weather_data: + self.weather_floating_widget.update_weather(self.current_weather_data) + + def on_weather_floating_closed(self): + """天气悬浮窗口关闭时的处理""" + self.status_bar.showMessage("天气悬浮窗口已关闭", 2000) + + def toggle_floating_quote(self): + """切换每日谏言悬浮窗口显示/隐藏""" + if hasattr(self, 'quote_floating_widget'): + if self.quote_floating_widget.isVisible(): + self.quote_floating_widget.hide() + self.status_bar.showMessage("每日谏言悬浮窗口已隐藏", 2000) + else: + self.quote_floating_widget.show() + # 确保窗口在屏幕内 + self.quote_floating_widget.move(100, 100) + self.status_bar.showMessage("每日谏言悬浮窗口已显示", 2000) + + def on_quote_floating_closed(self): + """每日谏言悬浮窗口关闭时的处理""" + self.status_bar.showMessage("每日谏言悬浮窗口已关闭", 2000) + + def toggle_floating_calendar(self): + """切换日历悬浮窗口的显示/隐藏状态""" + if hasattr(self, 'calendar_floating_widget'): + if self.calendar_floating_widget.isVisible(): + self.calendar_floating_widget.hide() + self.status_bar.showMessage("日历悬浮窗口已隐藏", 2000) + else: + self.calendar_floating_widget.show() + # 确保窗口在屏幕内 + self.calendar_floating_widget.move(100, 100) + self.status_bar.showMessage("日历悬浮窗口已显示", 2000) + + def on_calendar_floating_closed(self): + """日历悬浮窗口关闭事件""" + self.status_bar.showMessage("日历悬浮窗口已关闭", 2000) + + def insert_quote_to_cursor(self, quote_text): + """将古诗句插入到光标位置""" + if hasattr(self, 'text_edit'): + # 获取当前光标位置 + cursor = self.text_edit.textCursor() + + # 在光标位置插入文本 + cursor.insertText(quote_text + "\n") + + # 更新状态栏提示 + # 从文本中提取诗句部分用于显示 + quote_only = quote_text.split(" —— ")[0] if " —— " in quote_text else quote_text + self.status_bar.showMessage(f"已插入古诗句: {quote_only}", 3000) + + def insert_date_to_cursor(self, date_str): + """在光标位置插入日期""" + try: + # 在光标位置插入日期 + cursor = self.text_edit.textCursor() + cursor.insertText(date_str) + + # 更新状态栏 + self.status_bar.showMessage(f"已插入日期: {date_str}", 2000) + except Exception as e: + print(f"插入日期时出错: {e}") + def toggle_weather_tools(self, checked): """切换天气工具组显示""" if checked: @@ -1470,6 +1911,12 @@ class WordStyleMainWindow(QMainWindow): if hasattr(self, 'ribbon'): # 直接调用WordRibbon中的刷新方法 self.ribbon.on_refresh_quote() + + # 同时更新浮动窗口中的内容(如果浮动窗口存在且可见) + if hasattr(self, 'quote_floating_widget') and self.quote_floating_widget.isVisible(): + # 调用浮动窗口的获取新内容方法 + if hasattr(self.quote_floating_widget, 'fetch_and_update_quote'): + self.quote_floating_widget.fetch_and_update_quote() def on_quote_fetched(self, quote_data): """处理名言获取成功""" @@ -1569,10 +2016,10 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage("新建文档 - 打字模式,可以自由开始打字", 3000) def import_file(self): - """导入文件 - 仅在导入时存储内容,不立即显示""" + """导入文件 - 根据模式决定是否立即显示""" file_path, _ = QFileDialog.getOpenFileName( self, "导入文件", "", - "文档文件 (*.docx *.txt *.pdf);;所有文件 (*.*)" + "文档文件 (*.docx *.txt *.pdf *.html);;所有文件 (*.*)" ) if file_path: @@ -1590,7 +2037,7 @@ class WordStyleMainWindow(QMainWindow): if result.get('is_temp_file', False): self.temp_files.append(txt_path) - # 存储完整内容但不立即显示 + # 存储完整内容 self.imported_content = content self.displayed_chars = 0 @@ -1632,12 +2079,13 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},开始打字逐步显示学习内容!", 5000) else: - # 打字模式:不显示导入内容,保持当前内容 - self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},切换到学习模式查看内容", 5000) + # 打字模式:直接显示完整内容 + self.text_edit.setPlainText(content) + self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)}", 5000) # 提取并显示图片(如果有) if images: - self.extract_and_display_images(content, images) + self.extract_and_display_images(file_path=None, images=images) else: # 转换失败,显示错误信息 @@ -1650,7 +2098,7 @@ class WordStyleMainWindow(QMainWindow): content = parser.parse_file(file_path) if content: - # 存储完整内容但不立即显示 + # 存储完整内容 self.imported_content = content self.displayed_chars = 0 @@ -1665,8 +2113,9 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000) else: - # 打字模式:不显示导入内容,保持当前内容 - self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)},切换到学习模式查看内容", 5000) + # 打字模式:直接显示完整内容 + self.text_edit.setPlainText(content) + self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)}", 5000) except Exception as fallback_e: QMessageBox.critical(self, "错误", f"无法导入文件:\n{str(e)}\n\n回退方法也失败:\n{str(fallback_e)}") @@ -1691,7 +2140,7 @@ class WordStyleMainWindow(QMainWindow): # 设置文件加载标志 self.is_loading_file = True - # 存储完整内容但不立即显示 + # 存储完整内容 self.imported_content = content self.displayed_chars = 0 @@ -1856,9 +2305,32 @@ class WordStyleMainWindow(QMainWindow): imported_content = self.imported_content if hasattr(self, 'learning_progress') and self.learning_progress > 0: current_position = self.learning_progress + else: + # 如果没有导入内容,检查当前打字模式的内容 + current_text = self.text_edit.toPlainText() + if current_text and current_text != "在此输入您的内容...": + # 将打字模式的内容作为学习模式的导入内容 + imported_content = current_text + current_position = 0 + self.imported_content = current_text + + # 准备图片数据 + image_data = None + image_positions = None + if hasattr(self, 'typing_logic') and self.typing_logic: + if hasattr(self.typing_logic, 'image_data'): + image_data = self.typing_logic.image_data + if hasattr(self.typing_logic, 'image_positions'): + image_positions = self.typing_logic.image_positions - # 创建学习模式窗口,直接传递导入内容 - self.learning_window = LearningModeWindow(self, imported_content, current_position) + # 创建学习模式窗口,传递导入内容和图片数据 + self.learning_window = LearningModeWindow(self, imported_content, current_position, image_data, image_positions) + + # 连接学习模式窗口的内容变化信号 + self.learning_window.content_changed.connect(self.on_learning_content_changed) + + # 连接学习模式窗口的关闭信号 + self.learning_window.closed.connect(self.on_learning_mode_closed) # 显示学习模式窗口 self.learning_window.show() @@ -1890,8 +2362,86 @@ class WordStyleMainWindow(QMainWindow): self.learning_mode_action.setChecked(False) self.typing_mode_action.setChecked(True) self.view_mode = "typing" + + # 清除学习窗口引用 + self.learning_window = None + self.status_bar.showMessage("学习模式窗口已关闭", 2000) + def open_deepseek_dialog(self): + """打开DeepSeek AI对话窗口""" + try: + from deepseek_dialog_window import DeepSeekDialogWindow + + # 检查是否已存在对话窗口,如果存在则激活 + if self.deepseek_dialog and self.deepseek_dialog.isVisible(): + self.deepseek_dialog.activateWindow() + self.deepseek_dialog.raise_() + return + + # 创建DeepSeek对话窗口 + self.deepseek_dialog = DeepSeekDialogWindow(self) + + # 连接对话窗口的关闭信号 + self.deepseek_dialog.closed.connect(self.on_deepseek_dialog_closed) + + # 显示对话窗口 + self.deepseek_dialog.show() + + # 更新菜单状态 + self.deepseek_dialog_action.setChecked(True) + + self.status_bar.showMessage("DeepSeek AI对话窗口已打开", 3000) + + except ImportError as e: + QMessageBox.critical(self, "错误", f"无法加载DeepSeek对话窗口:\n{str(e)}") + except Exception as e: + QMessageBox.critical(self, "错误", f"打开DeepSeek对话窗口时出错:\n{str(e)}") + + def on_deepseek_dialog_closed(self): + """DeepSeek对话窗口关闭时的回调""" + # 重置菜单状态 + self.deepseek_dialog_action.setChecked(False) + + # 清除对话窗口引用 + self.deepseek_dialog = None + + self.status_bar.showMessage("DeepSeek AI对话窗口已关闭", 2000) + + def on_learning_content_changed(self, new_content, position): + """学习模式内容变化时的回调 - 只在末尾追加新内容""" + # 设置同步标记,防止递归调用 + self.sync_from_learning = True + + try: + # 只在末尾追加新输入的内容,不修改已有内容 + if new_content: + # 直接在末尾追加新内容 + cursor = self.text_edit.textCursor() + cursor.movePosition(QTextCursor.End) + cursor.insertText(new_content) + + # 更新导入内容(但不覆盖用户额外输入的内容) + if self.imported_content: + self.imported_content += new_content + else: + self.imported_content = new_content + + self.learning_progress = len(self.imported_content) if self.imported_content else 0 + + # 重置打字逻辑(不追踪进度) + if self.typing_logic: + self.typing_logic.imported_content = self.imported_content + self.typing_logic.current_index = self.learning_progress + self.typing_logic.typed_chars = self.learning_progress + + # 更新状态栏 + self.status_bar.showMessage(f"从学习模式同步新内容: {new_content}", 3000) + + finally: + # 重置同步标记 + self.sync_from_learning = False + def set_page_color(self, color): """设置页面颜色""" color_map = { @@ -2205,6 +2755,21 @@ class WordStyleMainWindow(QMainWindow): # 显示替换结果 QMessageBox.information(self, "替换", f"已完成 {count} 处替换。") + def launch_snake_game(self): + """启动贪吃蛇游戏""" + try: + from ui.snake_game import SnakeGameWindow + + # 创建游戏窗口 + self.snake_game_window = SnakeGameWindow(self) + self.snake_game_window.show() + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"无法启动贪吃蛇游戏:{str(e)}" + ) + def show_about(self): """显示关于对话框""" # 创建自定义对话框 @@ -2402,46 +2967,37 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage("已切换到黑色模式", 2000) def show_image_viewer(self, filename, image_data): - """显示图片查看器 - 图片真正铺满整个窗口上方""" + """显示图片查看器 - 支持缩放功能""" try: # 创建自定义图片查看窗口 viewer = QDialog(self) viewer.setWindowTitle(f"图片查看 - {filename}") viewer.setModal(False) - # 移除窗口边框和标题栏装饰,设置为工具窗口样式 - viewer.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) + # 设置窗口标志,保留标题栏以便用户可以移动和调整大小 + viewer.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint) - # 设置窗口背景为黑色,完全无边距 + # 设置窗口背景为黑色 viewer.setStyleSheet(""" QDialog { background-color: #000000; - border: none; - margin: 0px; - padding: 0px; } """) - # 创建布局,完全移除边距 - layout = QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) # 移除布局边距 - layout.setSpacing(0) # 移除组件间距 - layout.setAlignment(Qt.AlignCenter) # 布局居中对齐 + # 创建场景和视图 + scene = QGraphicsScene(viewer) + view = QGraphicsView(scene) + view.setStyleSheet("border: none;") # 移除视图边框 - # 创建图片标签,设置为完全填充模式 - image_label = QLabel() - image_label.setAlignment(Qt.AlignCenter) - image_label.setScaledContents(True) # 关键:允许图片缩放以填充标签 - image_label.setMinimumSize(1, 1) # 设置最小尺寸 - image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # 设置大小策略为扩展 - image_label.setStyleSheet(""" - QLabel { - border: none; - margin: 0px; - padding: 0px; - background-color: #000000; - } - """) + # 设置视图为可交互的,并启用滚动条 + view.setDragMode(QGraphicsView.ScrollHandDrag) + view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate) + + # 创建布局 + layout = QVBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(view) + viewer.setLayout(layout) # 加载图片 pixmap = QPixmap() @@ -2449,10 +3005,10 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage(f"加载图片失败: {filename}", 3000) return - layout.addWidget(image_label) - viewer.setLayout(layout) + # 将图片添加到场景 + scene.addPixmap(pixmap) - # 计算位置和大小 + # 设置视图大小和位置 if self: parent_geometry = self.geometry() screen_geometry = QApplication.primaryScreen().geometry() @@ -2477,42 +3033,24 @@ class WordStyleMainWindow(QMainWindow): viewer.show() - # 关键:强制图片立即填充整个标签区域 - def force_image_fill(): - try: - if pixmap and not pixmap.isNull(): - # 获取标签的实际大小 - label_size = image_label.size() - if label_size.width() > 10 and label_size.height() > 10: # 确保尺寸有效 - # 完全填充,忽略宽高比,真正铺满 - scaled_pixmap = pixmap.scaled( - label_size, - Qt.IgnoreAspectRatio, # 关键:忽略宽高比,强制填充 - Qt.SmoothTransformation - ) - image_label.setPixmap(scaled_pixmap) - print(f"图片已强制缩放至 {label_size.width()}x{label_size.height()}") - - # 确保标签完全填充布局 - image_label.setMinimumSize(label_size) - except Exception as e: - print(f"图片缩放失败: {e}") + # 设置视图适应图片大小 + view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio) - # 使用多个定时器确保图片正确填充 - from PyQt5.QtCore import QTimer - QTimer.singleShot(50, force_image_fill) # 50毫秒后执行 - QTimer.singleShot(200, force_image_fill) # 200毫秒后执行 - QTimer.singleShot(1000, force_image_fill) # 1000毫秒后再执行一次 + # 重写视图的滚轮事件以支持缩放 + def wheelEvent(event): + factor = 1.2 + if event.angleDelta().y() > 0: + view.scale(factor, factor) + else: + view.scale(1.0/factor, 1.0/factor) - # 连接窗口大小变化事件 - viewer.resizeEvent = lambda event: force_image_fill() + view.wheelEvent = wheelEvent - # 添加点击关闭功能 - def close_viewer(): - viewer.close() + # 添加双击重置视图功能 + def mouseDoubleClickEvent(event): + view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio) - image_label.mousePressEvent = lambda event: close_viewer() - viewer.mousePressEvent = lambda event: close_viewer() + view.mouseDoubleClickEvent = mouseDoubleClickEvent except Exception as e: self.status_bar.showMessage(f"创建图片查看器失败: {str(e)}", 3000) @@ -2636,6 +3174,79 @@ class WordStyleMainWindow(QMainWindow): # 这个方法现在不需要了,因为图片会直接插入到文本中 pass + def insert_image_in_typing_mode(self): + """在打字模式下插入图片""" + try: + # 检查当前是否在打字模式下 + if self.view_mode != "typing": + self.status_bar.showMessage("请在打字模式下使用插入图片功能", 3000) + return + + # 打开文件对话框选择图片 + from PyQt5.QtWidgets import QFileDialog + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择图片文件", + "", + "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.ico)" + ) + + if not file_path: + return + + # 加载图片文件 + pixmap = QPixmap(file_path) + if pixmap.isNull(): + self.status_bar.showMessage("无法加载图片文件", 3000) + return + + # 获取当前光标位置 + cursor = self.text_edit.textCursor() + + # 创建图片格式 + image_format = QTextImageFormat() + + # 调整图片大小 + scaled_pixmap = pixmap.scaled(200, 150, Qt.KeepAspectRatio, Qt.SmoothTransformation) + + # 将图片保存到临时文件 + import tempfile + import os + temp_dir = tempfile.gettempdir() + filename = os.path.basename(file_path) + safe_filename = "".join(c for c in filename if c.isalnum() or c in ('.', '_', '-')) + temp_file = os.path.join(temp_dir, safe_filename) + + if scaled_pixmap.save(temp_file): + # 设置图片格式 + image_format.setName(temp_file) + image_format.setWidth(200) + image_format.setHeight(150) + + # 在光标位置插入图片 + cursor.insertImage(image_format) + + # 在图片后插入一个空格,让文字继续 + cursor.insertText(" ") + + # 标记文档为已修改 + if not self.is_modified: + self.is_modified = True + self.update_window_title() + + # 显示成功消息 + self.status_bar.showMessage(f"图片已插入: {filename}", 3000) + + # 添加到临时文件列表以便清理 + self.temp_files.append(temp_file) + else: + self.status_bar.showMessage("保存临时图片文件失败", 3000) + + except Exception as e: + self.status_bar.showMessage(f"插入图片失败: {str(e)}", 3000) + import traceback + traceback.print_exc() + def closeEvent(self, event): """关闭事件处理""" # 清理临时文件 @@ -2670,11 +3281,307 @@ class WordStyleMainWindow(QMainWindow): print(f"删除临时文件失败 {temp_file}: {str(e)}") self.temp_files.clear() - def extract_and_display_images(self, file_path): + def export_as_html(self): + """导出为HTML""" + file_path, _ = QFileDialog.getSaveFileName( + self, "导出为HTML", "", "HTML文件 (*.html);;所有文件 (*.*)" + ) + + if file_path: + try: + # 获取当前文本内容 + content = self.text_edit.toPlainText() + + # 处理图片标签 + html_body = "" + lines = content.split('\n') + + for line in lines: + if line.strip().startswith('[图片:') and line.strip().endswith(']'): + # 提取图片文件名 + img_name = line.strip()[4:-1].strip() + + # 查找对应的图片数据 + img_data = None + for filename, image_data in self.extracted_images: + if filename == img_name: + img_data = image_data + break + + if img_data: + # 创建图片的base64编码 + import base64 + img_base64 = base64.b64encode(img_data).decode('utf-8') + + # 检测图片类型 + if img_name.lower().endswith('.png'): + img_type = 'png' + elif img_name.lower().endswith(('.jpg', '.jpeg')): + img_type = 'jpeg' + elif img_name.lower().endswith('.gif'): + img_type = 'gif' + else: + img_type = 'png' # 默认 + + html_body += f'
\n' + html_body += f'{img_name}\n' + html_body += f'

{img_name}

\n' + html_body += f'
\n' + else: + html_body += f'

[图片: {img_name}]

\n' + else: + # 普通文本,使用段落标签 + if line.strip(): + html_body += f'

{line}

\n' + else: + html_body += '
\n' + + # 创建完整的HTML内容 + html_content = f""" + + + + + MagicWord文档 + + + +
+ {html_body} +
+ +""" + + # 写入HTML文件 + with open(file_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + self.status_bar.showMessage(f"已导出为HTML: {os.path.basename(file_path)}", 3000) + + except Exception as e: + QMessageBox.critical(self, "错误", f"导出HTML失败: {str(e)}") + + def export_as_pdf(self): + """导出为PDF""" + file_path, _ = QFileDialog.getSaveFileName( + self, "导出为PDF", "", "PDF文件 (*.pdf);;所有文件 (*.*)" + ) + + if file_path: + try: + from PyQt5.QtGui import QTextDocument + from PyQt5.QtPrintSupport import QPrinter + + # 获取当前文本内容 + content = self.text_edit.toPlainText() + + # 处理图片标签 - 创建HTML内容以便更好地处理图片 + html_content = "" + + lines = content.split('\n') + for line in lines: + if line.strip().startswith('[图片:') and line.strip().endswith(']'): + # 提取图片文件名 + img_name = line.strip()[4:-1].strip() + + # 在PDF中显示图片占位符 + html_content += f'
' + html_content += f'
' + html_content += f'[图片: {img_name}]' + html_content += f'
' + html_content += f'

{img_name}

' + html_content += f'
' + else: + # 普通文本,使用段落标签 + if line.strip(): + html_content += f'

{line}

' + else: + html_content += '
' + + html_content += "" + + # 创建文本文档 + doc = QTextDocument() + doc.setHtml(html_content) + + # 设置文档样式 + doc.setDefaultFont(self.text_edit.currentFont()) + + # 创建PDF打印机 + printer = QPrinter(QPrinter.HighResolution) + printer.setOutputFormat(QPrinter.PdfFormat) + printer.setOutputFileName(file_path) + printer.setPageSize(QPrinter.A4) + printer.setPageMargins(20, 20, 20, 20, QPrinter.Millimeter) + + # 打印文档到PDF + doc.print_(printer) + + self.status_bar.showMessage(f"已导出为PDF: {os.path.basename(file_path)}", 3000) + + except Exception as e: + QMessageBox.critical(self, "错误", f"导出PDF失败: {str(e)}") + + def export_as_txt(self): + """导出为TXT""" + file_path, _ = QFileDialog.getSaveFileName( + self, "导出为TXT", "", "文本文档 (*.txt);;所有文件 (*.*)" + ) + + if file_path: + try: + # 获取当前文本内容 + content = self.text_edit.toPlainText() + + # 处理图片标签 - 在TXT中保留图片标记 + processed_content = content + + # 写入TXT文件 + with open(file_path, 'w', encoding='utf-8') as f: + f.write(processed_content) + + self.status_bar.showMessage(f"已导出为TXT: {os.path.basename(file_path)}", 3000) + + except Exception as e: + QMessageBox.critical(self, "错误", f"导出TXT失败: {str(e)}") + + def export_as_docx(self): + """导出为DOCX""" + file_path, _ = QFileDialog.getSaveFileName( + self, "导出为DOCX", "", "Word文档 (*.docx);;所有文件 (*.*)" + ) + + if file_path: + try: + import os + import tempfile + from docx import Document + from docx.shared import Inches + + # 创建Word文档 + doc = Document() + + # 获取当前文本内容 + content = self.text_edit.toPlainText() + lines = content.split('\n') + + # 逐行处理内容 + for line in lines: + if line.strip().startswith('[图片:') and line.strip().endswith(']'): + # 提取图片文件名 + img_name = line.strip()[4:-1].strip() + + # 查找对应的图片数据 + img_data = None + for filename, image_data in self.extracted_images: + if filename == img_name: + img_data = image_data + break + + if img_data: + # 检测图片类型 + if img_name.lower().endswith('.png'): + img_ext = '.png' + elif img_name.lower().endswith(('.jpg', '.jpeg')): + img_ext = '.jpg' + elif img_name.lower().endswith('.gif'): + img_ext = '.gif' + else: + img_ext = '.png' # 默认 + + # 创建临时文件 + with tempfile.NamedTemporaryFile(mode='wb', suffix=img_ext, delete=False) as temp_file: + temp_file.write(img_data) + temp_img_path = temp_file.name + + try: + # 在Word文档中添加图片 + doc.add_picture(temp_img_path, width=Inches(4)) + + # 添加图片说明 + doc.add_paragraph(f'图片: {img_name}') + + finally: + # 清理临时文件 + if os.path.exists(temp_img_path): + os.remove(temp_img_path) + else: + # 图片未找到,添加占位符文本 + doc.add_paragraph(f'[图片: {img_name}]') + else: + # 普通文本 + if line.strip(): + doc.add_paragraph(line) + else: + # 空行,添加空段落 + doc.add_paragraph() + + # 保存文档 + doc.save(file_path) + + self.status_bar.showMessage(f"已导出为DOCX: {os.path.basename(file_path)}", 3000) + + except ImportError: + QMessageBox.critical(self, "错误", "需要安装python-docx库才能导出DOCX文件") + except Exception as e: + QMessageBox.critical(self, "错误", f"导出DOCX失败: {str(e)}") + + def extract_and_display_images(self, file_path=None, images=None): """提取并显示Word文档中的图片 - 修复图片位置计算""" try: - # 提取图片 - images = FileParser.extract_images_from_docx(file_path) + # 如果没有提供图片数据,则从文件中提取 + if images is None: + if file_path is None: + return + # 提取图片 + images = FileParser.extract_images_from_docx(file_path) if not images: return @@ -2716,8 +3623,8 @@ class WordStyleMainWindow(QMainWindow): # 为每张图片创建位置信息 - 修复位置计算,确保早期显示 content_length = len(self.imported_content) - if content_length == 0: - content_length = len(content) if 'content' in locals() else 1000 # 备用长度 + #if content_length == 0: + #content_length = len(content) if 'content' in locals() else 1000 # 备用长度 # 修复图片位置计算,确保图片能在用户早期打字时显示 if len(images) == 1: @@ -2767,6 +3674,154 @@ class WordStyleMainWindow(QMainWindow): except Exception as e: self.status_bar.showMessage(f"提取图片失败: {str(e)}", 3000) + + def update_format_buttons(self): + """更新格式按钮的状态,根据当前光标位置的格式""" + try: + # 获取当前光标位置的字符格式 + cursor = self.text_edit.textCursor() + char_format = cursor.charFormat() + block_format = cursor.blockFormat() + + # 更新粗体按钮状态 + if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'bold_btn'): + is_bold = char_format.font().weight() == QFont.Bold + self.ribbon.bold_btn.setChecked(is_bold) + + # 更新斜体按钮状态 + if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'italic_btn'): + is_italic = char_format.font().italic() + self.ribbon.italic_btn.setChecked(is_italic) + + # 更新下划线按钮状态 + if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'underline_btn'): + is_underline = char_format.font().underline() + self.ribbon.underline_btn.setChecked(is_underline) + + # 更新对齐按钮状态 + if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'align_left_btn'): + alignment = block_format.alignment() + self.ribbon.align_left_btn.setChecked(alignment == Qt.AlignLeft) + self.ribbon.align_center_btn.setChecked(alignment == Qt.AlignCenter) + self.ribbon.align_right_btn.setChecked(alignment == Qt.AlignRight) + self.ribbon.align_justify_btn.setChecked(alignment == Qt.AlignJustify) + + except Exception as e: + print(f"更新格式按钮状态时出错: {e}") + + def resizeEvent(self, event): + """窗口大小改变事件处理""" + super().resizeEvent(event) + + # 如果日历组件可见,调整其大小和位置以适应窗口底部 + if hasattr(self, 'calendar_widget') and self.calendar_widget.isVisible(): + calendar_height = 350 # 增加高度以确保所有日期都能完整显示 + self.calendar_widget.setGeometry(0, self.height() - calendar_height, + self.width(), calendar_height) + + def toggle_calendar(self): + """切换日历组件的显示/隐藏状态""" + if hasattr(self, 'calendar_widget'): + if self.calendar_widget.isVisible(): + self.calendar_widget.hide() + else: + # 设置日历组件位置在窗口底部 + calendar_height = 350 # 增加高度以确保所有日期都能完整显示 + + # 将日历组件放置在窗口底部,占据整个宽度 + self.calendar_widget.setGeometry(0, self.height() - calendar_height, + self.width(), calendar_height) + self.calendar_widget.show() + self.calendar_widget.raise_() # 确保日历组件在最上层显示 + + def insert_weather_info(self): + """在光标位置插入天气信息""" + # 检查是否处于打字模式 + if self.view_mode != "typing": + self.status_bar.showMessage("请在打字模式下使用插入天气信息功能", 3000) + return + + # 检查是否已经定位了天气(即是否有有效的天气数据) + if not hasattr(self, 'current_weather_data') or not self.current_weather_data: + # 弹出对话框提示用户先定位天气 + QMessageBox.information(self, "附加工具", "先定位天气") + return + + try: + # 直接使用已经获取到的天气数据 + weather_data = self.current_weather_data + + # 格式化天气信息 + if 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') + weather_info = f"天气: {desc}, 温度: {temp}°C, 城市: {city}" + else: + weather_info = "天气信息获取失败" + + # 在光标位置插入天气信息 + cursor = self.text_edit.textCursor() + cursor.insertText(weather_info) + + # 更新状态栏 + self.status_bar.showMessage("已插入天气信息", 2000) + + except Exception as e: + QMessageBox.warning(self, "错误", f"插入天气信息失败: {str(e)}") + + def insert_daily_quote(self): + """在光标位置插入每日一句名言""" + # 检查是否处于打字模式 + if self.view_mode != "typing": + self.status_bar.showMessage("请在打字模式下使用插入每日一句名言功能", 3000) + return + + try: + # 使用与Ribbon界面相同的API获取每日一言,确保内容一致 + from ui.word_style_ui import daily_sentence_API + quote_api = daily_sentence_API("https://api.nxvav.cn/api/yiyan") + quote_data = quote_api.get_sentence('json') + + # 处理获取到的数据 + if quote_data and isinstance(quote_data, dict): + quote_text = quote_data.get('yiyan', '暂无每日一言') + quote_info = quote_text + else: + quote_info = "每日一句名言获取失败" + + # 在光标位置插入名言信息 + cursor = self.text_edit.textCursor() + cursor.insertText(quote_info) + + # 更新状态栏 + self.status_bar.showMessage("已插入每日一句名言", 2000) + + except Exception as e: + QMessageBox.warning(self, "错误", f"插入每日一句名言失败: {str(e)}") + + def insert_chinese_poetry(self): + """在光标位置插入古诗词""" + # 检查是否处于打字模式 + if self.view_mode != "typing": + self.status_bar.showMessage("请在打字模式下使用插入古诗词功能", 3000) + return + + try: + # 获取古诗词 + poetry_data = self.ribbon.get_chinese_poetry() + + # 在光标位置插入古诗词 + cursor = self.text_edit.textCursor() + cursor.insertText(poetry_data) + + # 更新状态栏 + self.status_bar.showMessage("已插入古诗词", 2000) + + except Exception as e: + QMessageBox.warning(self, "错误", f"插入古诗词失败: {str(e)}") if __name__ == "__main__": app = QApplication(sys.argv) diff --git a/start_app_safe.sh b/start_app_safe.sh new file mode 100755 index 0000000..2a94b61 --- /dev/null +++ b/start_app_safe.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# MagicWord PyQt5 安全启动脚本 +# 此脚本自动设置所有必要的环境变量并启动应用 + +echo "🚀 正在启动 MagicWord 应用..." + +# 设置PyQt5环境变量 +export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms" +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" +export QT_LOGGING_RULES="qt.qpa.*=false" + +# 检查虚拟环境 +if [ -z "$VIRTUAL_ENV" ]; then + echo "⚠️ 虚拟环境未激活,正在激活..." + source .venv/bin/activate +fi + +# 检查PyQt5是否安装 +python -c "import PyQt5.QtWidgets" 2>/dev/null +if [ $? -ne 0 ]; then + echo "❌ PyQt5未正确安装,正在修复..." + python fix_pyqt5_complete.py +fi + +# 启动应用 +echo "✅ 环境设置完成,正在启动应用..." +cd src && python main.py \ No newline at end of file diff --git a/test.docx b/test.docx new file mode 100644 index 0000000..2564c62 Binary files /dev/null and b/test.docx differ diff --git a/test_quote.py b/test_quote.py new file mode 100644 index 0000000..569ff84 --- /dev/null +++ b/test_quote.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import os + +# 添加src目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from ui.word_style_ui import daily_sentence_API +from services.network_service import NetworkService + +def test_daily_sentence_api(): + print("测试 daily_sentence_API 类...") + try: + # 使用与Ribbon界面相同的API获取每日一言 + quote_api = daily_sentence_API("https://api.nxvav.cn/api/yiyan") + quote_data = quote_api.get_sentence('json') + + print("API返回的数据:") + print(quote_data) + + # 处理获取到的数据 + if quote_data and isinstance(quote_data, dict): + quote_text = quote_data.get('yiyan', '暂无每日一言') + print(f"解析后的每日一言: {quote_text}") + else: + print("获取每日一言失败") + + except Exception as e: + print(f"测试 daily_sentence_API 类时出错: {e}") + +def test_network_service_quote(): + print("\n测试 NetworkService 类的 get_daily_quote 方法...") + try: + network_service = NetworkService() + quote = network_service.get_daily_quote() + print(f"NetworkService 获取的每日一言: {quote}") + except Exception as e: + print(f"测试 NetworkService 类时出错: {e}") + +if __name__ == "__main__": + test_daily_sentence_api() + test_network_service_quote() \ No newline at end of file diff --git a/test_snake_game.py b/test_snake_game.py new file mode 100644 index 0000000..fc416aa --- /dev/null +++ b/test_snake_game.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +# test_snake_game.py +"""测试贪吃蛇游戏""" + +import sys +import os + +# 添加项目根目录到Python路径 +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, project_root) + +from PyQt5.QtWidgets import QApplication +from src.ui.snake_game import SnakeGameWindow + +def main(): + app = QApplication(sys.argv) + game_window = SnakeGameWindow() + game_window.show() + sys.exit(app.exec_()) + +if __name__ == '__main__': + main() diff --git a/test_speed_control.py b/test_speed_control.py new file mode 100644 index 0000000..9f08bae --- /dev/null +++ b/test_speed_control.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +""" +测试贪吃蛇游戏的速度调节功能 +""" + +import sys +sys.path.insert(0, '/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design') + +from src.ui.snake_game import SnakeGame, SnakeGameWindow +from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QKeyEvent + +def test_snake_game(): + """测试贪吃蛇游戏""" + app = QApplication(sys.argv) + + # 创建游戏窗口 + window = SnakeGameWindow() + window.show() + + # 获取游戏实例 + game = window.game_widget + + # 测试初始速度 + print(f"初始速度: {game.current_speed}ms") + assert game.current_speed == game.GAME_SPEED, "初始速度应该是150ms" + + # 测试增加速度(按上键) + print("\n测试增加速度...") + initial_speed = game.current_speed + game.increase_speed() + print(f"按上键后速度: {game.current_speed}ms (从 {initial_speed}ms)") + assert game.current_speed < initial_speed, "速度应该增加(毫秒数减少)" + + # 测试降低速度(按下键) + print("\n测试降低速度...") + current_speed = game.current_speed + game.decrease_speed() + print(f"按下键后速度: {game.current_speed}ms (从 {current_speed}ms)") + assert game.current_speed > current_speed, "速度应该降低(毫秒数增加)" + + # 测试速度限制 + print("\n测试速度限制...") + + # 测试最小速度限制 + game.current_speed = game.MIN_SPEED + game.increase_speed() + print(f"最小速度限制测试: {game.current_speed}ms (应该 >= {game.MIN_SPEED}ms)") + assert game.current_speed >= game.MIN_SPEED, "速度不应该低于最小值" + + # 测试最大速度限制 + game.current_speed = game.MAX_SPEED + game.decrease_speed() + print(f"最大速度限制测试: {game.current_speed}ms (应该 <= {game.MAX_SPEED}ms)") + assert game.current_speed <= game.MAX_SPEED, "速度不应该超过最大值" + + print("\n✓ 所有测试通过!") + print(f"速度范围: {game.MIN_SPEED}ms - {game.MAX_SPEED}ms") + print(f"速度步长: {game.SPEED_STEP}ms") + + window.close() + +if __name__ == '__main__': + try: + test_snake_game() + except Exception as e: + print(f"✗ 测试失败: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/test_weather_insert.py b/test_weather_insert.py new file mode 100644 index 0000000..c2d0d1b --- /dev/null +++ b/test_weather_insert.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +import os + +# 添加src目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from word_main_window import WordStyleMainWindow +from PyQt5.QtWidgets import QApplication, QMessageBox +from unittest.mock import patch + +def test_insert_weather_without_location(): + """测试在未定位天气时插入天气信息""" + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # 创建主窗口实例 + window = WordStyleMainWindow() + + # 模拟没有定位天气的情况(删除current_weather_data属性) + if hasattr(window, 'current_weather_data'): + delattr(window, 'current_weather_data') + + # 模拟用户点击插入天气信息按钮 + with patch('PyQt5.QtWidgets.QMessageBox.information') as mock_info: + window.insert_weather_info() + # 验证是否弹出了"先定位天气"对话框 + mock_info.assert_called_once_with(window, "附加工具", "先定位天气") + + print("测试通过:未定位天气时正确弹出提示对话框") + +def test_insert_weather_with_location(): + """测试在已定位天气时插入天气信息""" + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # 创建主窗口实例 + window = WordStyleMainWindow() + + # 模拟已定位天气的情况 + window.current_weather_data = { + 'city': '北京', + 'temperature': 25, + 'description': '晴天' + } + + # 模拟用户点击插入天气信息按钮 + # 注意:这个测试不会真正插入文本,因为我们没有设置完整的UI环境 + window.insert_weather_info() + + print("测试通过:已定位天气时正确执行插入操作") + +if __name__ == "__main__": + test_insert_weather_without_location() + test_insert_weather_with_location() + print("所有测试完成!") \ No newline at end of file diff --git a/字体颜色功能说明.md b/字体颜色功能说明.md deleted file mode 100644 index 5f2b43e..0000000 --- a/字体颜色功能说明.md +++ /dev/null @@ -1,88 +0,0 @@ -# 字体颜色功能说明 - -我已经成功为你的MagicWord应用添加了字体颜色工具!以下是添加的功能: - -## 🎯 重要更新:保留之前内容的颜色 - -**根据你的需求,字体颜色功能已修改为保留原有内容的颜色!** - -### 修改后的功能特性 -- ✅ **保留原有内容颜色**:已有文本的颜色完全保持不变 -- ✅ **只影响新输入**:新输入的文本将使用选定的颜色 -- ✅ **智能提示**:选择文本时会提示颜色只对新输入生效 -- ✅ **向后兼容**:不会影响现有的粗体、斜体、下划线等格式 - -## 新增功能 - -### 1. 字体颜色按钮 -- 在"开始"标签页的"字体"组中,添加了一个新的颜色按钮(显示为"A") -- 该按钮位于加粗(B)、斜体(I)、下划线(U)按钮的右侧 - -### 2. 颜色选择功能 -- 点击颜色按钮会弹出颜色选择对话框 -- 用户可以选择任意颜色 -- **重要**:只影响后续输入的文本,不会改变已有内容的颜色 - -## 技术实现 - -### UI界面修改 -1. **word_style_ui.py** 中添加了: - - `color_btn` 按钮创建 - - `create_color_button()` 方法用于创建颜色按钮 - - `on_color_clicked()` 方法作为按钮点击事件的处理函数 - -2. **word_main_window.py** 中修改了: - - 颜色按钮的信号连接 - - `on_color_clicked()` 方法:简化为只设置默认颜色,不影响已有内容 - -### 功能特性 -- 使用 PyQt5 的 `QColorDialog` 提供颜色选择界面 -- **只设置默认文本颜色**,不修改已有内容的格式 -- 智能状态栏提示,告知用户颜色的应用范围 -- 保持与现有字体样式(粗体、斜体、下划线)的一致性 - -## 使用方法 - -### 设置新文本颜色 -1. 点击颜色按钮(A) -2. 在弹出的颜色对话框中选择所需颜色 -3. 点击"确定" -4. **后续输入的所有文本都将使用该颜色** -5. **已有文本的颜色完全保持不变** - -### 颜色选择提示 -- 如果选择了文本,会提示:"字体颜色已设置,新输入的文本将使用该颜色" -- 如果没有选择文本,会显示新颜色的具体信息 - -## 界面位置 - -字体颜色工具位于: -开始标签页 → 字体组 → 样式按钮区域(B、I、U按钮右侧) - -## 🔧 技术实现细节 - -### 修改后的核心逻辑 -```python -def on_color_clicked(self): - """字体颜色按钮点击处理 - 保留之前内容的颜色""" - # 只设置后续输入的默认颜色,不影响已有内容 - self.text_edit.setTextColor(color) - - # 友好的用户提示 - if cursor.hasSelection(): - self.status_bar.showMessage("字体颜色已设置,新输入的文本将使用该颜色", 2000) -``` - -这个实现确保: -- ✅ 用户可以自由设置新文本的颜色 -- ✅ 所有已有内容的颜色完全保留 -- ✅ 用户体验友好,有明确的操作反馈 - -## 🚀 优势 - -1. **非破坏性**:不会意外改变已有内容的格式 -2. **直观易用**:用户明确知道颜色设置的影响范围 -3. **灵活性高**:可以随时更改新文本的颜色而不影响历史内容 -4. **兼容性好**:与所有现有功能完美配合 - -这个新增功能与现有的字体样式工具完美集成,提供了完整且安全的文本格式化能力! \ No newline at end of file