From 1154098ad10dadd2659c00420c6683898bce7413 Mon Sep 17 00:00:00 2001 From: Horse861 <929110464@qq.com> Date: Mon, 3 Nov 2025 10:39:08 +0800 Subject: [PATCH 01/39] =?UTF-8?q?1.0-win=E6=89=93=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build_v1.0.py | 331 +++++++++++++++++++++++++++++++++++ package_v1.0.py | 251 ++++++++++++++++++++++++++ resources/icons/app_icon.ico | Bin 0 -> 9142 bytes setup.py | 18 +- src/main.py | 2 +- src/ui/ui.txt | 0 字体颜色功能说明.md | 88 ---------- 7 files changed, 600 insertions(+), 90 deletions(-) create mode 100644 build_v1.0.py create mode 100644 package_v1.0.py create mode 100644 resources/icons/app_icon.ico delete mode 100644 src/ui/ui.txt delete mode 100644 字体颜色功能说明.md 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/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.ico b/resources/icons/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9a97f720c9e1ba46ad6ab16e2ea2d7b6a9c472b7 GIT binary patch literal 9142 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.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/src/main.py b/src/main.py index b119247..de007be 100644 --- a/src/main.py +++ b/src/main.py @@ -85,7 +85,7 @@ def main(): # 设置应用程序属性 app.setApplicationName("MagicWord") - app.setApplicationVersion("0.2.2") + app.setApplicationVersion("1.0.0") app.setOrganizationName("MagicWord") # 设置窗口图标(如果存在) diff --git a/src/ui/ui.txt b/src/ui/ui.txt deleted file mode 100644 index e69de29..0000000 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 -- 2.34.1 From 88500b153b6615d7cfdc2bd86fb91151047215e0 Mon Sep 17 00:00:00 2001 From: Horse861 <929110464@qq.com> Date: Mon, 3 Nov 2025 11:16:52 +0800 Subject: [PATCH 02/39] =?UTF-8?q?feat(=E5=AD=A6=E4=B9=A0=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?):=20=E5=AE=9E=E7=8E=B0=E5=AD=A6=E4=B9=A0=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8E=E6=89=93=E5=AD=97=E6=A8=A1=E5=BC=8F=E7=9A=84=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加学习模式窗口的内容变化信号和关闭信号 在打字模式窗口中处理学习模式的内容同步 优化学习模式窗口的初始化逻辑 --- src/learning_mode_window.py | 21 ++ src/main_window.py | 487 +----------------------------------- src/word_main_window.py | 65 ++++- 3 files changed, 91 insertions(+), 482 deletions(-) diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py index 1ec350b..49305f4 100644 --- a/src/learning_mode_window.py +++ b/src/learning_mode_window.py @@ -14,6 +14,10 @@ from src.file_parser import FileParser from src.ui.theme_manager import theme_manager class LearningModeWindow(QMainWindow): + # 定义内容变化信号 + content_changed = pyqtSignal(str, int) # 参数:内容,位置 + # 定义关闭信号 + closed = pyqtSignal() def __init__(self, parent=None, imported_content="", current_position=0): """ 学习模式窗口 @@ -39,6 +43,9 @@ class LearningModeWindow(QMainWindow): # 初始化打字逻辑 self.init_typing_logic() + # 初始化同步位置跟踪 + self.last_sync_position = current_position + # 如果有导入内容,初始化显示 if self.imported_content: self.initialize_with_imported_content() @@ -335,6 +342,7 @@ class LearningModeWindow(QMainWindow): 文本变化处理 - 根据导入的内容逐步显示 - 更新学习进度 + - 同步内容到打字模式 """ # 如果正在加载文件,跳过处理 if self.is_loading_file: @@ -382,6 +390,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 +398,15 @@ 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("恭喜!学习完成!") @@ -417,6 +435,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() 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/word_main_window.py b/src/word_main_window.py index 2a7f7da..9fcdd7c 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -83,6 +83,10 @@ class WordStyleMainWindow(QMainWindow): self.learning_text = "" # 学习模式下的文本内容 self.cursor_position = 0 # 光标位置 + # 学习模式窗口引用和同步标记 + self.learning_window = None # 学习模式窗口引用 + self.sync_from_learning = False # 从学习模式同步内容的标记 + # 统一文档内容管理 self.unified_document_content = "" # 统一文档内容 self.last_edit_mode = "typing" # 上次编辑模式 @@ -836,6 +840,10 @@ class WordStyleMainWindow(QMainWindow): # 如果正在加载文件,跳过处理 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 +860,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: @@ -1856,10 +1865,24 @@ 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 # 创建学习模式窗口,直接传递导入内容 self.learning_window = LearningModeWindow(self, imported_content, current_position) + # 连接学习模式窗口的内容变化信号 + 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 +1913,46 @@ 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 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 = { -- 2.34.1 From 5d8e05c722aa395ae863a8b19eeb3c250fc153c7 Mon Sep 17 00:00:00 2001 From: Horse861 <929110464@qq.com> Date: Mon, 3 Nov 2025 11:36:54 +0800 Subject: [PATCH 03/39] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test.docx | Bin 0 -> 21936 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test.docx diff --git a/test.docx b/test.docx new file mode 100644 index 0000000000000000000000000000000000000000..2564c62ed0a7fcaa3172e69d2e3600be8a15bf5e GIT binary patch literal 21936 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 literal 0 HcmV?d00001 -- 2.34.1 From 660a12abac4bbf33c707f5a00a380950c40a966f Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 6 Nov 2025 16:39:00 +0800 Subject: [PATCH 04/39] =?UTF-8?q?MAC=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PYQT5_FIX_GUIDE.md | 120 +++++++++++++++++++++ emergency_fix.sh | 55 ++++++++++ fix_pyqt5_complete.py | 237 ++++++++++++++++++++++++++++++++++++++++++ install_pyqt5_safe.py | 65 ++++++++++++ run_magicword.sh | 29 ++++++ set_pyqt5_env.sh | 7 ++ setup_qt_env.py | 65 ++++++++++++ src/main.py | 103 ++++++++++-------- start_app_safe.sh | 29 ++++++ 9 files changed, 666 insertions(+), 44 deletions(-) create mode 100644 PYQT5_FIX_GUIDE.md create mode 100755 emergency_fix.sh create mode 100644 fix_pyqt5_complete.py create mode 100644 install_pyqt5_safe.py create mode 100755 run_magicword.sh create mode 100644 set_pyqt5_env.sh create mode 100644 setup_qt_env.py create mode 100755 start_app_safe.sh 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/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/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_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/main.py b/src/main.py index de007be..2377050 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() 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 -- 2.34.1 From 77e613b8d7ca0edcd12e2a8f7ca34b3de3d5c038 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 6 Nov 2025 16:50:39 +0800 Subject: [PATCH 05/39] =?UTF-8?q?=E5=A4=A9=E6=B0=94=E7=BB=84=E4=BB=B6UI?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/word_style_ui.py | 168 ++++++++++++++++++++++++++++++++++++---- src/word_main_window.py | 32 ++++++++ 2 files changed, 183 insertions(+), 17 deletions(-) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 5150f9f..c78e7e8 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -464,17 +464,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.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; + color: #333333; + 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 +595,20 @@ class WordRibbon(QFrame): self.city_combo.currentTextChanged.connect(self.on_city_changed) # 刷新按钮 - self.refresh_weather_btn = QPushButton("刷新天气") + self.refresh_weather_btn = QPushButton("🔄 刷新") self.refresh_weather_btn.clicked.connect(self.on_refresh_weather) - self.refresh_weather_btn.setFixedSize(80, 25) + self.refresh_weather_btn.setFixedSize(60, 30) # 增大刷新按钮尺寸 + self.refresh_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }") + self.refresh_weather_btn.setToolTip("刷新天气") + + control_layout.addWidget(self.city_combo) + control_layout.addWidget(self.refresh_weather_btn) + + # 添加右侧弹性空间,确保内容居中 + control_layout.addStretch() - weather_layout.addWidget(self.city_combo) - weather_layout.addWidget(self.refresh_weather_btn) + weather_layout.addLayout(weather_display_layout) + weather_layout.addLayout(control_layout) weather_group.setLayout(weather_layout) self.weather_group = weather_group @@ -1016,14 +1132,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 +1166,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}") diff --git a/src/word_main_window.py b/src/word_main_window.py index 9fcdd7c..04ffc71 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -1295,6 +1295,12 @@ class WordStyleMainWindow(QMainWindow): """更新天气显示""" 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', '未知城市') @@ -1318,6 +1324,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', [])}") -- 2.34.1 From 8bd14804f70015267d892aaf2b9f11f7dc86e8f3 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 10 Nov 2025 10:05:14 +0800 Subject: [PATCH 06/39] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E8=BD=AF=E4=BB=B6?= =?UTF-8?q?=E5=9C=A8MacOS=E7=B3=BB=E7=BB=9F=E7=9A=84=E9=B2=81=E6=A3=92?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- final_fix.py | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 final_fix.py diff --git a/final_fix.py b/final_fix.py new file mode 100644 index 0000000..8c8e37f --- /dev/null +++ b/final_fix.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +MagicWord 应用程序最终修复方案 +使用macOS特定的库路径解决方案 +""" + +import sys +import os +import platform + +# 获取项目根目录 +project_root = os.path.dirname(os.path.abspath(__file__)) + +# 添加src目录到Python路径 +sys.path.insert(0, os.path.join(project_root, 'src')) + +# Qt路径设置 +qt_path = os.path.join(project_root, '.venv', 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5') +qt_plugins_path = os.path.join(qt_path, 'plugins') +qt_lib_path = os.path.join(qt_path, 'lib') + +print(f"项目根目录: {project_root}") +print(f"Qt路径: {qt_path}") + +# macOS特定的环境变量设置 +if platform.system() == "Darwin": + # 设置Qt插件路径 + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = qt_plugins_path + + # 关键:设置DYLD_FALLBACK_LIBRARY_PATH,这是macOS的库搜索后备路径 + fallback_paths = [ + qt_lib_path, + '/usr/local/lib', + '/usr/lib', + '/System/Library/Frameworks' + ] + os.environ['DYLD_FALLBACK_LIBRARY_PATH'] = ':'.join(fallback_paths) + + # 设置其他环境变量 + os.environ['DYLD_LIBRARY_PATH'] = qt_lib_path + os.environ['DYLD_FRAMEWORK_PATH'] = qt_lib_path + os.environ['QT_PREFIX_PATH'] = qt_path + os.environ['QT_DEBUG_PLUGINS'] = '1' + + print("macOS环境变量设置完成:") + print(f"DYLD_FALLBACK_LIBRARY_PATH: {os.environ['DYLD_FALLBACK_LIBRARY_PATH']}") + print(f"QT_QPA_PLATFORM_PLUGIN_PATH: {os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']}") + +print("\\n开始导入PyQt5...") + +try: + # 导入PyQt5 + from PyQt5.QtWidgets import QApplication + from PyQt5.QtCore import Qt + print("✓ PyQt5导入成功") + + # 启用高DPI支持 + if hasattr(Qt, 'AA_EnableHighDpiScaling'): + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + if hasattr(Qt, 'AA_UseHighDpiPixmaps'): + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + # 创建QApplication + print("创建QApplication...") + app = QApplication(sys.argv) + app.setApplicationName("MagicWord") + app.setApplicationVersion("1.0") + print("✓ QApplication创建成功") + + # 导入应用程序模块 + print("导入应用程序模块...") + from word_main_window import WordStyleMainWindow + from settings.settings_manager import SettingsManager + print("✓ 应用程序模块导入成功") + + # 创建设置管理器 + settings_manager = SettingsManager() + + # 创建主窗口 + print("创建主窗口...") + main_window = WordStyleMainWindow() + main_window.show() + print("✓ 主窗口创建成功") + + print("\\n🎉 应用程序启动成功!") + print("正在运行应用程序...") + + # 运行应用程序 + sys.exit(app.exec_()) + +except Exception as e: + print(f"\\n❌ 错误: {e}") + import traceback + traceback.print_exc() + + # 如果失败,尝试使用系统Qt + print("\\n尝试使用系统Qt...") + try: + # 清除自定义环境变量 + for key in ['QT_QPA_PLATFORM_PLUGIN_PATH', 'DYLD_LIBRARY_PATH', + 'DYLD_FRAMEWORK_PATH', 'DYLD_FALLBACK_LIBRARY_PATH']: + if key in os.environ: + del os.environ[key] + + from PyQt5.QtWidgets import QApplication + app = QApplication(sys.argv) + print("✓ 使用系统Qt成功") + sys.exit(app.exec_()) + except Exception as e2: + print(f"❌ 系统Qt也失败: {e2}") + sys.exit(1) \ No newline at end of file -- 2.34.1 From 2b6c69f73b222b23e2e020f9b2cfb39a15d9edce Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 10 Nov 2025 10:18:03 +0800 Subject: [PATCH 07/39] =?UTF-8?q?=E6=89=93=E5=AD=97=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E6=8F=92=E5=85=A5=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/word_main_window.py | 78 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/word_main_window.py b/src/word_main_window.py index 04ffc71..fb946ef 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -674,6 +674,11 @@ class WordStyleMainWindow(QMainWindow): # 插入菜单 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) + # 绘图菜单 paint_menu = menubar.addMenu('绘图(D)') @@ -2729,6 +2734,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): """关闭事件处理""" # 清理临时文件 -- 2.34.1 From 6db8f24f21e39366fba9b0171e9dca620cb2db93 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 10 Nov 2025 10:30:40 +0800 Subject: [PATCH 08/39] =?UTF-8?q?=E6=B8=85=E7=90=86=E6=9D=82=E9=A1=B9?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- final_fix.py | 111 --------------------------------------------------- 1 file changed, 111 deletions(-) delete mode 100644 final_fix.py diff --git a/final_fix.py b/final_fix.py deleted file mode 100644 index 8c8e37f..0000000 --- a/final_fix.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -""" -MagicWord 应用程序最终修复方案 -使用macOS特定的库路径解决方案 -""" - -import sys -import os -import platform - -# 获取项目根目录 -project_root = os.path.dirname(os.path.abspath(__file__)) - -# 添加src目录到Python路径 -sys.path.insert(0, os.path.join(project_root, 'src')) - -# Qt路径设置 -qt_path = os.path.join(project_root, '.venv', 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5') -qt_plugins_path = os.path.join(qt_path, 'plugins') -qt_lib_path = os.path.join(qt_path, 'lib') - -print(f"项目根目录: {project_root}") -print(f"Qt路径: {qt_path}") - -# macOS特定的环境变量设置 -if platform.system() == "Darwin": - # 设置Qt插件路径 - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = qt_plugins_path - - # 关键:设置DYLD_FALLBACK_LIBRARY_PATH,这是macOS的库搜索后备路径 - fallback_paths = [ - qt_lib_path, - '/usr/local/lib', - '/usr/lib', - '/System/Library/Frameworks' - ] - os.environ['DYLD_FALLBACK_LIBRARY_PATH'] = ':'.join(fallback_paths) - - # 设置其他环境变量 - os.environ['DYLD_LIBRARY_PATH'] = qt_lib_path - os.environ['DYLD_FRAMEWORK_PATH'] = qt_lib_path - os.environ['QT_PREFIX_PATH'] = qt_path - os.environ['QT_DEBUG_PLUGINS'] = '1' - - print("macOS环境变量设置完成:") - print(f"DYLD_FALLBACK_LIBRARY_PATH: {os.environ['DYLD_FALLBACK_LIBRARY_PATH']}") - print(f"QT_QPA_PLATFORM_PLUGIN_PATH: {os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']}") - -print("\\n开始导入PyQt5...") - -try: - # 导入PyQt5 - from PyQt5.QtWidgets import QApplication - from PyQt5.QtCore import Qt - print("✓ PyQt5导入成功") - - # 启用高DPI支持 - if hasattr(Qt, 'AA_EnableHighDpiScaling'): - QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) - if hasattr(Qt, 'AA_UseHighDpiPixmaps'): - QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) - - # 创建QApplication - print("创建QApplication...") - app = QApplication(sys.argv) - app.setApplicationName("MagicWord") - app.setApplicationVersion("1.0") - print("✓ QApplication创建成功") - - # 导入应用程序模块 - print("导入应用程序模块...") - from word_main_window import WordStyleMainWindow - from settings.settings_manager import SettingsManager - print("✓ 应用程序模块导入成功") - - # 创建设置管理器 - settings_manager = SettingsManager() - - # 创建主窗口 - print("创建主窗口...") - main_window = WordStyleMainWindow() - main_window.show() - print("✓ 主窗口创建成功") - - print("\\n🎉 应用程序启动成功!") - print("正在运行应用程序...") - - # 运行应用程序 - sys.exit(app.exec_()) - -except Exception as e: - print(f"\\n❌ 错误: {e}") - import traceback - traceback.print_exc() - - # 如果失败,尝试使用系统Qt - print("\\n尝试使用系统Qt...") - try: - # 清除自定义环境变量 - for key in ['QT_QPA_PLATFORM_PLUGIN_PATH', 'DYLD_LIBRARY_PATH', - 'DYLD_FRAMEWORK_PATH', 'DYLD_FALLBACK_LIBRARY_PATH']: - if key in os.environ: - del os.environ[key] - - from PyQt5.QtWidgets import QApplication - app = QApplication(sys.argv) - print("✓ 使用系统Qt成功") - sys.exit(app.exec_()) - except Exception as e2: - print(f"❌ 系统Qt也失败: {e2}") - sys.exit(1) \ No newline at end of file -- 2.34.1 From 611a4b2068309a115b506cefe098cbf727c7d702 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 10 Nov 2025 11:24:56 +0800 Subject: [PATCH 09/39] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/file_parser.py | 47 ++++++ src/word_main_window.py | 332 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 374 insertions(+), 5 deletions(-) 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/word_main_window.py b/src/word_main_window.py index fb946ef..1ca91ec 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -685,6 +685,29 @@ class WordStyleMainWindow(QMainWindow): # 设计菜单 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)') @@ -1618,7 +1641,7 @@ class WordStyleMainWindow(QMainWindow): """导入文件 - 仅在导入时存储内容,不立即显示""" file_path, _ = QFileDialog.getOpenFileName( self, "导入文件", "", - "文档文件 (*.docx *.txt *.pdf);;所有文件 (*.*)" + "文档文件 (*.docx *.txt *.pdf *.html);;所有文件 (*.*)" ) if file_path: @@ -1683,7 +1706,7 @@ class WordStyleMainWindow(QMainWindow): # 提取并显示图片(如果有) if images: - self.extract_and_display_images(content, images) + self.extract_and_display_images(file_path=None, images=images) else: # 转换失败,显示错误信息 @@ -2841,11 +2864,310 @@ 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: + 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: + # 创建临时图片文件 + import tempfile + import os + + # 检测图片类型 + 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 -- 2.34.1 From dd72229a5fa059360be9037a3d091ee517d04a93 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 10 Nov 2025 11:40:17 +0800 Subject: [PATCH 10/39] =?UTF-8?q?=E6=9B=B4=E6=96=B0UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/theme_manager.py | 199 +++++++++++++++++++++++++++------------- src/ui/word_style_ui.py | 98 ++++++++++++-------- 2 files changed, 195 insertions(+), 102 deletions(-) diff --git a/src/ui/theme_manager.py b/src/ui/theme_manager.py index 6b42bb1..4169478 100644 --- a/src/ui/theme_manager.py +++ b/src/ui/theme_manager.py @@ -153,53 +153,83 @@ class ThemeManager(QObject): return self._get_light_stylesheet() def _get_dark_stylesheet(self): - """深色主题样式表""" + """深色主题样式表 - 现代极简暗色风格""" return """ - /* 深色主题样式 */ + /* 现代极简暗色主题样式 */ - /* 全局文字颜色 */ + /* 全局文字颜色和字体 - 现代字体 */ QWidget { - color: #e0e0e0; + color: #e2e8f0; + font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; } - /* 主窗口 */ + /* 主窗口 - 深灰色背景 */ QMainWindow { - background-color: #1e1e1e; + background-color: #1a202c; } - /* 菜单栏 */ + /* 菜单栏 - 极简暗色风格 */ QMenuBar { - background-color: #0078d7; - border: 1px solid #005a9e; - font-size: 12px; - color: #ffffff; + background-color: #1a202c; + border: none; + border-bottom: 1px solid #2d3748; + font-size: 14px; + color: #e2e8f0; + padding: 8px 0; } QMenuBar::item { background-color: transparent; - padding: 4px 10px; - color: #e0e0e0; + padding: 8px 16px; + color: #a0aec0; + border-radius: 6px; + margin: 0 2px; } QMenuBar::item:selected { - background-color: #106ebe; + background-color: #2d3748; + color: #e2e8f0; } - /* 菜单 */ + QMenuBar::item:pressed { + background-color: #4a5568; + color: #e2e8f0; + } + + /* 菜单 - 极简暗色风格 */ QMenu { - background-color: #2d2d2d; - border: 1px solid #3c3c3c; - font-size: 12px; - color: #e0e0e0; + background-color: #2d3748; + border: 1px solid #4a5568; + border-radius: 12px; + font-size: 14px; + color: #e2e8f0; + padding: 8px 0; + margin: 4px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); } QMenu::item { - padding: 4px 20px; - color: #e0e0e0; + padding: 10px 20px; + color: #a0aec0; + background-color: transparent; + border-radius: 6px; + margin: 0 8px; } QMenu::item:selected { - background-color: #3c3c3c; + background-color: #4a5568; + color: #e2e8f0; + } + + QMenu::item:pressed { + background-color: #718096; + color: #ffffff; + } + + QMenu::separator { + height: 1px; + background-color: #4a5568; + margin: 8px 16px; } /* 功能区 */ @@ -227,29 +257,32 @@ class ThemeManager(QObject): color: #e0e0e0; } - /* 按钮 */ + /* 工具按钮 - 现代极简风格 */ QToolButton { - border: 1px solid #3c3c3c; - border-radius: 3px; - background-color: #3c3c3c; - font-size: 11px; - color: #e0e0e0; - padding: 3px 6px; + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 8px 12px; + color: #4a5568; + font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-weight: 500; + font-size: 13px; } QToolButton:hover { - background-color: #4a4a4a; - border: 1px solid #5a5a5a; + background-color: #f7fafc; + border: 1px solid #e2e8f0; } QToolButton:pressed { - background-color: #2a2a2a; - border: 1px solid #1a1a1a; + background-color: #edf2f7; + border: 1px solid #cbd5e0; } QToolButton:checked { - background-color: #0078d4; - border: 1px solid #106ebe; + background-color: #ebf8ff; + border: 1px solid #bee3f8; + color: #3182ce; } /* 切换按钮 */ @@ -405,53 +438,83 @@ class ThemeManager(QObject): """ def _get_light_stylesheet(self): - """浅色主题样式表 - 白底黑字""" + """浅色主题样式表 - 现代极简主义风格""" return """ - /* 浅色主题样式 - 白底黑字 */ + /* 现代极简主义浅色主题样式 */ - /* 全局文字颜色 */ + /* 全局文字颜色和字体 - 使用现代字体 */ QWidget { - color: #333333; + color: #2d3748; + font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; } - /* 主窗口 */ + /* 主窗口 - 纯净白色背景 */ QMainWindow { - background-color: #f3f2f1; + background-color: #ffffff; } - /* 菜单栏 */ + /* 菜单栏 - 极简风格 */ QMenuBar { - background-color: #0078d7; - border: 1px solid #005a9e; - font-size: 12px; - color: #ffffff; + background-color: #ffffff; + border: none; + border-bottom: 1px solid #e2e8f0; + font-size: 14px; + color: #2d3748; + padding: 8px 0; } QMenuBar::item { background-color: transparent; - padding: 4px 10px; - color: #333333; + padding: 8px 16px; + color: #4a5568; + border-radius: 6px; + margin: 0 2px; } QMenuBar::item:selected { - background-color: #106ebe; + background-color: #f7fafc; + color: #2d3748; } - /* 菜单 */ + QMenuBar::item:pressed { + background-color: #edf2f7; + color: #2d3748; + } + + /* 菜单 - 极简风格 */ QMenu { background-color: #ffffff; - border: 1px solid #d0d0d0; - font-size: 12px; - color: #333333; + border: 1px solid #e2e8f0; + border-radius: 12px; + font-size: 14px; + color: #2d3748; + padding: 8px 0; + margin: 4px; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); } QMenu::item { - padding: 4px 20px; - color: #333333; + padding: 10px 20px; + color: #4a5568; + background-color: transparent; + border-radius: 6px; + margin: 0 8px; } QMenu::item:selected { - background-color: #f0f0f0; + background-color: #f7fafc; + color: #2d3748; + } + + QMenu::item:pressed { + background-color: #edf2f7; + color: #2d3748; + } + + QMenu::separator { + height: 1px; + background-color: #e2e8f0; + margin: 8px 16px; } /* 功能区 */ @@ -600,21 +663,27 @@ class ThemeManager(QObject): selection-color: #333333; } - /* 文本编辑器 */ + /* 文本编辑区域 - 现代极简风格 */ QTextEdit { background-color: #ffffff; - border: 1px solid #d0d0d0; - color: #000000; - padding: 20px; - line-height: 1.5; + border: none; + font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 14px; + color: #2d3748; + padding: 32px; + line-height: 1.6; + selection-background-color: rgba(66, 153, 225, 0.2); + selection-color: #2d3748; } - /* 状态栏 */ + /* 状态栏 - 现代极简风格 */ QStatusBar { background-color: #ffffff; - border-top: 1px solid #d0d0d0; - font-size: 11px; - color: #333333; + border-top: 1px solid #e2e8f0; + font-size: 12px; + color: #718096; + font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; + padding: 8px; } /* 标签 */ diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index c78e7e8..a1183a4 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -49,13 +49,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; } """) @@ -821,58 +821,67 @@ class WordRibbon(QFrame): return btn 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.setFixedSize(64, 28) # 三个字符及以上(如"左对齐"、"两端对齐") btn.setStyleSheet(""" QToolButton { - border: 1px solid #d0d0d0; - border-radius: 2px; background-color: transparent; - font-size: 12px; - font-weight: bold; - color: #333333; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: #4a5568; + font-size: 13px; + font-weight: 500; } QToolButton:hover { - background-color: #f0f0f0; + background-color: #f7fafc; + border: 1px solid #e2e8f0; + } + QToolButton:pressed { + background-color: #edf2f7; + border: 1px solid #cbd5e0; } QToolButton:checked { - background-color: #e1e1e1; - border: 1px solid #c0c0c0; + background-color: #ebf8ff; + border: 1px solid #bee3f8; + color: #3182ce; } """) return btn def create_color_button(self, text, icon_name): - """创建颜色选择按钮""" + """创建颜色选择按钮 - 现代极简主义风格""" btn = QToolButton() btn.setText(text) btn.setToolButtonStyle(Qt.ToolButtonTextOnly) - btn.setFixedSize(30, 25) + btn.setFixedSize(32, 28) btn.setStyleSheet(""" QToolButton { - border: 1px solid #d0d0d0; - border-radius: 2px; background-color: transparent; - font-size: 12px; - font-weight: bold; - color: #333333; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: #4a5568; + font-size: 13px; + font-weight: 500; } QToolButton:hover { - background-color: #f0f0f0; + background-color: #f7fafc; + border: 1px solid #e2e8f0; } QToolButton:pressed { - background-color: #e1e1e1; - border: 1px solid #c0c0c0; + background-color: #edf2f7; + border: 1px solid #cbd5e0; } """) return btn @@ -883,13 +892,23 @@ class WordStatusBar(QStatusBar): self.setup_ui() def setup_ui(self): - """设置状态栏""" + """设置状态栏 - 现代极简主义风格""" self.setStyleSheet(""" QStatusBar { - background-color: #f3f2f1; - border-top: 1px solid #d0d0d0; - font-size: 11px; - color: #333333; + 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; } """) @@ -988,16 +1007,21 @@ 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; + 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; } """) -- 2.34.1 From 3cb14795b838b93e3be0b193abcaffde2e14d33d Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 13 Nov 2025 15:47:29 +0800 Subject: [PATCH 11/39] =?UTF-8?q?=E5=B0=8F=E4=BF=AE=E5=B0=8F=E8=A1=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++++ run_debug.sh | 7 +++++++ run_fixed.sh | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 run_debug.sh create mode 100755 run_fixed.sh diff --git a/.gitignore b/.gitignore index 4ec21cb..f661aa2 100644 --- a/.gitignore +++ b/.gitignore @@ -198,6 +198,9 @@ temp/ *.orig # Project specific + +# Documentation folder +doc/ dist_package/ dist_package_v0.3/ *.zip @@ -231,6 +234,7 @@ venv/ env/ .venv/ .env/ +new_venv/ # IDE .idea/ 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 -- 2.34.1 From 254d868643179b3913d699dbbb58b6d91ab0694d Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 13 Nov 2025 16:39:30 +0800 Subject: [PATCH 12/39] =?UTF-8?q?=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- resources/icons/app_icon.icns | Bin 0 -> 4433 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/icons/app_icon.icns diff --git a/resources/icons/app_icon.icns b/resources/icons/app_icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..06053a19fe7898e0e807ca8c32a3d9149b162e7a GIT binary patch literal 4433 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 bi3@dg%uSs`Xzuy-yTZiS!l>*l>E3? Date: Fri, 14 Nov 2025 15:43:55 +0800 Subject: [PATCH 13/39] =?UTF-8?q?=E5=BF=AB=E6=8D=B7=E9=94=AE=EF=BC=9Acontr?= =?UTF-8?q?ol+L=E6=88=96command+L=E5=91=BC=E5=87=BA=E5=AD=A6=E4=B9=A0?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/word_main_window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/word_main_window.py b/src/word_main_window.py index 1ca91ec..914abbc 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -1,6 +1,7 @@ # 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, @@ -637,6 +638,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) -- 2.34.1 From 2185df26678dd20d86e86e48bea36647124a3c16 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Fri, 14 Nov 2025 16:05:12 +0800 Subject: [PATCH 14/39] =?UTF-8?q?fix=EF=BC=9AIP=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/word_style_ui.py | 205 ++++++++++++++++++++++++++++++++-------- 1 file changed, 167 insertions(+), 38 deletions(-) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index a1183a4..731e63b 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -1281,27 +1281,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) @@ -1326,7 +1449,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) @@ -1349,26 +1472,14 @@ class WeatherAPI: print(f"pconline接口失败: {e}") pass - # API 3: 使用ip-api.com接口(更稳定的免费服务) + # API 5: 使用淘宝IP接口 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接口 - 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() @@ -1387,25 +1498,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 = { @@ -1553,6 +1681,7 @@ class WeatherAPI: except Exception as e: print(f"获取当前位置失败: {e}") + # 即使出现异常也返回None而不是抛出异常,确保程序继续运行 return None def get_city_weather_by_name(self, city_name): -- 2.34.1 From 9bd8fd220d9df11ca6b842485248cf9996c259f4 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Fri, 14 Nov 2025 16:41:37 +0800 Subject: [PATCH 15/39] =?UTF-8?q?UI=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.py | 6 +- src/ui/theme_manager.py | 616 ++++++++++++++++++++++------------------ 2 files changed, 340 insertions(+), 282 deletions(-) diff --git a/src/main.py b/src/main.py index 2377050..56b0016 100644 --- a/src/main.py +++ b/src/main.py @@ -95,8 +95,10 @@ 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") diff --git a/src/ui/theme_manager.py b/src/ui/theme_manager.py index 4169478..988b05a 100644 --- a/src/ui/theme_manager.py +++ b/src/ui/theme_manager.py @@ -153,99 +153,100 @@ class ThemeManager(QObject): return self._get_light_stylesheet() def _get_dark_stylesheet(self): - """深色主题样式表 - 现代极简暗色风格""" + """深色主题样式表 - Apple设计风格""" return """ - /* 现代极简暗色主题样式 */ + /* Apple设计风格深色主题样式 */ - /* 全局文字颜色和字体 - 现代字体 */ + /* 全局文字颜色和字体 - 使用Apple系统字体 */ QWidget { - color: #e2e8f0; - font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; + 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: #1a202c; + background-color: #2c2c2e; } - /* 菜单栏 - 极简暗色风格 */ + /* 菜单栏 - Apple深色风格 */ QMenuBar { - background-color: #1a202c; + background-color: #2c2c2e; border: none; - border-bottom: 1px solid #2d3748; - font-size: 14px; - color: #e2e8f0; - padding: 8px 0; + border-bottom: 1px solid #404040; + font-size: 13px; + color: #f0f0f0; + padding: 4px 0; } QMenuBar::item { background-color: transparent; - padding: 8px 16px; - color: #a0aec0; - border-radius: 6px; - margin: 0 2px; + padding: 6px 12px; + color: #f0f0f0; + border-radius: 4px; + margin: 0 1px; } QMenuBar::item:selected { - background-color: #2d3748; - color: #e2e8f0; + background-color: #404040; + color: #f0f0f0; } QMenuBar::item:pressed { - background-color: #4a5568; - color: #e2e8f0; + background-color: #505050; + color: #f0f0f0; } - /* 菜单 - 极简暗色风格 */ + /* 菜单 - Apple深色风格 */ QMenu { - background-color: #2d3748; - border: 1px solid #4a5568; - border-radius: 12px; - font-size: 14px; - color: #e2e8f0; - padding: 8px 0; - margin: 4px; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); + background-color: #2c2c2e; + border: 1px solid #404040; + border-radius: 8px; + font-size: 13px; + color: #f0f0f0; + padding: 4px 0; + margin: 2px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } QMenu::item { - padding: 10px 20px; - color: #a0aec0; + color: #f0f0f0; background-color: transparent; - border-radius: 6px; - margin: 0 8px; + border-radius: 4px; + margin: 0 4px; + padding: 4px 20px; } QMenu::item:selected { - background-color: #4a5568; - color: #e2e8f0; + background-color: #0a84ff; + color: #ffffff; } QMenu::item:pressed { - background-color: #718096; + background-color: #0066cc; color: #ffffff; } QMenu::separator { height: 1px; - background-color: #4a5568; - margin: 8px 16px; + 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; } @@ -254,198 +255,235 @@ class ThemeManager(QObject): subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; - color: #e0e0e0; + color: #a0a0a0; } - /* 工具按钮 - 现代极简风格 */ + /* 工具按钮 - Apple深色风格 */ QToolButton { - background-color: transparent; border: 1px solid transparent; - border-radius: 8px; - padding: 8px 12px; - color: #4a5568; - font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; - font-weight: 500; + border-radius: 6px; + background-color: #3a3a3c; font-size: 13px; + color: #f0f0f0; + padding: 6px 12px; } QToolButton:hover { - background-color: #f7fafc; - border: 1px solid #e2e8f0; + background-color: #4a4a4c; + border: 1px solid #5a5a5c; } QToolButton:pressed { - background-color: #edf2f7; - border: 1px solid #cbd5e0; + background-color: #5a5a5c; + border: 1px solid #6a6a6c; } QToolButton:checked { - background-color: #ebf8ff; - border: 1px solid #bee3f8; - color: #3182ce; + 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; - } - - /* 字体下拉框特殊处理 */ - QFontComboBox { - background-color: #3c3c3c; - border: 1px solid #5a5a5a; - border-radius: 2px; - color: #e0e0e0; - padding: 2px 5px; - selection-background-color: #4a4a4a; - selection-color: #e0e0e0; + background-color: #2c2c2e; + border: 1px solid #4a4a4c; + color: #f0f0f0; + selection-background-color: #0a84ff; + selection-color: #ffffff; } - 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: #2d3748; - font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; + 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; } /* 主窗口 - 纯净白色背景 */ @@ -453,84 +491,84 @@ class ThemeManager(QObject): background-color: #ffffff; } - /* 菜单栏 - 极简风格 */ + /* 菜单栏 - Apple风格 */ QMenuBar { background-color: #ffffff; border: none; - border-bottom: 1px solid #e2e8f0; - font-size: 14px; - color: #2d3748; - padding: 8px 0; + border-bottom: 1px solid #e0e0e0; + font-size: 13px; + color: #333333; + padding: 4px 0; } QMenuBar::item { background-color: transparent; - padding: 8px 16px; - color: #4a5568; - border-radius: 6px; - margin: 0 2px; + padding: 6px 12px; + color: #333333; + border-radius: 4px; + margin: 0 1px; } QMenuBar::item:selected { - background-color: #f7fafc; - color: #2d3748; + background-color: #f0f0f0; + color: #333333; } QMenuBar::item:pressed { - background-color: #edf2f7; - color: #2d3748; + background-color: #e0e0e0; + color: #333333; } - /* 菜单 - 极简风格 */ + /* 菜单 - Apple风格 */ QMenu { background-color: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 12px; - font-size: 14px; - color: #2d3748; - padding: 8px 0; - margin: 4px; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); + border: 1px solid #d0d0d0; + border-radius: 8px; + font-size: 13px; + color: #333333; + padding: 4px 0; + margin: 2px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } QMenu::item { - padding: 10px 20px; - color: #4a5568; + color: #333333; background-color: transparent; - border-radius: 6px; - margin: 0 8px; + border-radius: 4px; + margin: 0 4px; + padding: 4px 20px; } QMenu::item:selected { - background-color: #f7fafc; - color: #2d3748; + background-color: #007aff; + color: #ffffff; } QMenu::item:pressed { - background-color: #edf2f7; - color: #2d3748; + background-color: #0062cc; + color: #ffffff; } QMenu::separator { height: 1px; - background-color: #e2e8f0; - margin: 8px 16px; + 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; } @@ -539,151 +577,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; - } - - 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; + selection-background-color: #007aff; + selection-color: #ffffff; } - /* 文本编辑区域 - 现代极简风格 */ + /* 文本编辑区域 - Apple风格 */ QTextEdit { background-color: #ffffff; border: none; - font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; - font-size: 14px; - color: #2d3748; + 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.6; - selection-background-color: rgba(66, 153, 225, 0.2); - selection-color: #2d3748; + line-height: 1.5; + selection-background-color: #b3d9ff; + selection-color: #333333; } - /* 状态栏 - 现代极简风格 */ + /* 状态栏 - Apple风格 */ QStatusBar { - background-color: #ffffff; - border-top: 1px solid #e2e8f0; + background-color: #f6f6f6; + border-top: 1px solid #e0e0e0; font-size: 12px; - color: #718096; - font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif; - padding: 8px; + 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; } /* 标签 */ @@ -692,41 +695,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; } @@ -738,6 +758,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): -- 2.34.1 From fadf1a38677dc5d91961d09de56e18f98219320b Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Fri, 14 Nov 2025 16:50:11 +0800 Subject: [PATCH 16/39] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/word_style_ui.py | 84 ++++++++++++++++++++++++++++++- src/word_main_window.py | 107 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 189 insertions(+), 2 deletions(-) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 731e63b..6a045d1 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -143,7 +143,43 @@ class WordRibbon(QFrame): # 样式组 styles_group = self.create_ribbon_group("样式") styles_layout = QVBoxLayout() - styles_layout.addWidget(QLabel("样式")) + + # 创建标题样式按钮 + title_buttons_layout = QHBoxLayout() + + # 一级标题按钮 + self.heading1_btn = self.create_style_button("标题1") + self.heading1_btn.clicked.connect(self.on_heading1_clicked) + title_buttons_layout.addWidget(self.heading1_btn) + + # 二级标题按钮 + self.heading2_btn = self.create_style_button("标题2") + self.heading2_btn.clicked.connect(self.on_heading2_clicked) + title_buttons_layout.addWidget(self.heading2_btn) + + # 三级标题按钮 + self.heading3_btn = self.create_style_button("标题3") + self.heading3_btn.clicked.connect(self.on_heading3_clicked) + title_buttons_layout.addWidget(self.heading3_btn) + + styles_layout.addLayout(title_buttons_layout) + + # 第二行样式按钮 + style_buttons_layout = QHBoxLayout() + + # 四级标题按钮 + self.heading4_btn = self.create_style_button("标题4") + self.heading4_btn.clicked.connect(self.on_heading4_clicked) + style_buttons_layout.addWidget(self.heading4_btn) + + # 正文按钮 + self.body_text_btn = self.create_style_button("正文") + self.body_text_btn.clicked.connect(self.on_body_text_clicked) + style_buttons_layout.addWidget(self.body_text_btn) + + style_buttons_layout.addStretch() + styles_layout.addLayout(style_buttons_layout) + styles_group.setLayout(styles_layout) layout.addWidget(styles_group) @@ -195,6 +231,26 @@ class WordRibbon(QFrame): """字体颜色按钮点击处理""" pass + def on_heading1_clicked(self): + """一级标题按钮点击处理""" + pass + + def on_heading2_clicked(self): + """二级标题按钮点击处理""" + pass + + def on_heading3_clicked(self): + """三级标题按钮点击处理""" + pass + + def on_heading4_clicked(self): + """四级标题按钮点击处理""" + pass + + def on_body_text_clicked(self): + """正文按钮点击处理""" + pass + def init_theme(self): """初始化主题""" # 连接主题切换信号 @@ -885,6 +941,32 @@ class WordRibbon(QFrame): } """) return btn + + def create_style_button(self, text): + """创建样式按钮 - 现代极简主义风格""" + btn = QPushButton(text) + btn.setFixedSize(60, 28) + btn.setStyleSheet(""" + 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; + } + """) + return btn class WordStatusBar(QStatusBar): def __init__(self, parent=None): diff --git a/src/word_main_window.py b/src/word_main_window.py index 914abbc..0633ef2 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -8,7 +8,7 @@ from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QDialog, QLineEdit, QCheckBox, QPushButton, QListWidget, QListWidgetItem, QScrollArea) 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, ) @@ -852,6 +852,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) @@ -1322,6 +1334,99 @@ 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 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 update_weather_display(self, weather_data): """更新天气显示""" if 'error' in weather_data: -- 2.34.1 From 31c493d2d1aebbfa1b0512039ef94e031afe5ca2 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Fri, 14 Nov 2025 18:12:41 +0800 Subject: [PATCH 17/39] =?UTF-8?q?=E6=8C=89=E9=92=AE=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/word_style_ui.py | 16 ++++++++ src/word_main_window.py | 82 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 6a045d1..fa3bdc4 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -251,6 +251,22 @@ class WordRibbon(QFrame): """正文按钮点击处理""" pass + def on_align_left_clicked(self): + """左对齐按钮点击处理""" + pass + + def on_align_center_clicked(self): + """居中对齐按钮点击处理""" + pass + + def on_align_right_clicked(self): + """右对齐按钮点击处理""" + pass + + def on_align_justify_clicked(self): + """两端对齐按钮点击处理""" + pass + def init_theme(self): """初始化主题""" # 连接主题切换信号 diff --git a/src/word_main_window.py b/src/word_main_window.py index 0633ef2..b3f72e1 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -840,6 +840,9 @@ class WordStyleMainWindow(QMainWindow): # 文本变化信号 self.text_edit.textChanged.connect(self.on_text_changed) + # 光标位置变化信号,用于更新按钮状态 + self.text_edit.cursorPositionChanged.connect(self.update_format_buttons) + # Ribbon按钮信号 # 标签栏已删除,相关代码已移除 @@ -872,6 +875,16 @@ 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) @@ -1354,6 +1367,22 @@ class WordStyleMainWindow(QMainWindow): """正文按钮点击处理""" 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 apply_heading_style(self, level): """应用标题样式""" cursor = self.text_edit.textCursor() @@ -1427,6 +1456,25 @@ class WordStyleMainWindow(QMainWindow): 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: @@ -3368,6 +3416,40 @@ 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}") if __name__ == "__main__": app = QApplication(sys.argv) -- 2.34.1 From 62d1df122c2bc9db642d84259a0142ca6677e827 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 17 Nov 2025 10:04:06 +0800 Subject: [PATCH 18/39] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=A4=A9=E6=B0=94?= =?UTF-8?q?=E7=BB=84=E4=BB=B6+UI=E5=B0=8F=E4=BF=AE=E8=A1=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/word_style_ui.py | 544 ++++++++++++++++++++++++++++++---------- src/word_main_window.py | 5 +- 2 files changed, 410 insertions(+), 139 deletions(-) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index fa3bdc4..a29a043 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -302,6 +302,32 @@ class WordRibbon(QFrame): }} """) + # 更新天气组件样式 + 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) @@ -595,7 +621,7 @@ class WordRibbon(QFrame): 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) # 增加行间距 @@ -624,11 +650,11 @@ class WordRibbon(QFrame): # 温度标签 - 优化垂直居中对齐 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; - color: #333333; padding: 0px; margin: 0px; border: none; @@ -709,7 +735,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() # 创建第一行:类型选择下拉框和刷新按钮 @@ -772,27 +798,76 @@ class WordRibbon(QFrame): self.quote_group = None self.quote_visible = False - def create_ribbon_group(self, title): + def create_ribbon_group(self, title, is_special_group=False): """创建功能区组""" 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; - } - """) + + # 为非特殊组设置最小宽度以确保标题完整显示 + if not is_special_group: + # 根据标题长度计算合适的最小宽度 + min_width = max(100, len(title) * 12 + 40) # 基础宽度+每个字符约12px + group.setMinimumWidth(min_width) + + # 连接主题切换信号以动态更新样式 + theme_manager.theme_changed.connect(lambda: self._update_group_style(group)) + + # 立即应用当前主题样式 + self._update_group_style(group) + 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 @@ -873,25 +948,57 @@ 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() @@ -905,111 +1012,204 @@ class WordRibbon(QFrame): btn.setFixedSize(48, 28) # 两个字符(如"居中") else: btn.setFixedSize(64, 28) # 三个字符及以上(如"左对齐"、"两端对齐") - btn.setStyleSheet(""" - 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; - } - """) + + # 连接主题切换信号以动态更新样式 + 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(32, 28) - btn.setStyleSheet(""" - 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; - } - """) + + # 连接主题切换信号以动态更新样式 + 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) - btn.setStyleSheet(""" - 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; - } - """) + + # 连接主题切换信号以动态更新样式 + 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: #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; - } - """) - # 添加状态栏项目 self.page_label = QLabel("第 1 页,共 1 页") self.words_label = QLabel("字数: 0") @@ -1020,12 +1220,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): """设置输入处理器""" @@ -1106,23 +1355,6 @@ class WordTextEdit(QTextEdit): def setup_ui(self): """设置文本编辑区域样式 - 现代极简主义风格""" - 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; - } - """) - # 设置页面边距和背景 self.setViewportMargins(50, 50, 50, 50) @@ -1136,6 +1368,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): diff --git a/src/word_main_window.py b/src/word_main_window.py index b3f72e1..0b21977 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -136,9 +136,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() -- 2.34.1 From f06a441c2a2cdda8d5f4122040dddbe7c5473416 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 17 Nov 2025 10:38:50 +0800 Subject: [PATCH 19/39] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=97=A5=E5=8E=86?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/calendar_widget.py | 280 ++++++++++++++++++++++++++++++++++++++ src/word_main_window.py | 65 +++++++++ 2 files changed, 345 insertions(+) create mode 100644 src/ui/calendar_widget.py diff --git a/src/ui/calendar_widget.py b/src/ui/calendar_widget.py new file mode 100644 index 0000000..ed66583 --- /dev/null +++ b/src/ui/calendar_widget.py @@ -0,0 +1,280 @@ +# -*- 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 + + +class CalendarWidget(QWidget): + """日历组件类""" + + # 自定义信号 + date_selected = pyqtSignal(str) # 日期字符串信号,用于插入功能 + + def __init__(self, parent=None): + super().__init__(parent) + self.setup_ui() + self.setup_connections() + + 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) + + +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/word_main_window.py b/src/word_main_window.py index 0b21977..80508e8 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -17,6 +17,7 @@ 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.theme_manager import theme_manager @@ -102,6 +103,10 @@ class WordStyleMainWindow(QMainWindow): self.network_service = NetworkService() self.weather_api = WeatherAPI() + # 初始化日历组件 + self.calendar_widget = CalendarWidget(self) + self.calendar_widget.hide() # 默认隐藏 + # 设置窗口属性 self.setWindowTitle("文档1 - MagicWord") self.setGeometry(100, 100, 1200, 800) @@ -508,6 +513,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): """创建菜单栏""" @@ -684,6 +697,11 @@ class WordStyleMainWindow(QMainWindow): # 绘图菜单 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)') @@ -889,6 +907,10 @@ class WordStyleMainWindow(QMainWindow): 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, 'calendar_widget'): + self.calendar_widget.date_selected.connect(self.insert_date_to_editor) def on_text_changed(self): """文本变化处理 - 根据视图模式处理文本变化""" @@ -1382,6 +1404,24 @@ class WordStyleMainWindow(QMainWindow): """两端对齐按钮点击处理""" 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() @@ -3449,6 +3489,31 @@ class WordStyleMainWindow(QMainWindow): 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 = 250 # 减小高度使比例更美观 + 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 = 250 # 减小高度使比例更美观 + + # 将日历组件放置在窗口底部,占据整个宽度 + self.calendar_widget.setGeometry(0, self.height() - calendar_height, + self.width(), calendar_height) + self.calendar_widget.show() + self.calendar_widget.raise_() # 确保日历组件在最上层显示 if __name__ == "__main__": app = QApplication(sys.argv) -- 2.34.1 From 9588630f515543d4bc62ea86aae6f898095f0e13 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 17 Nov 2025 11:19:20 +0800 Subject: [PATCH 20/39] =?UTF-8?q?=E6=8F=92=E5=85=A5=E5=A4=A9=E6=B0=94?= =?UTF-8?q?=E3=80=81=E6=AF=8F=E6=97=A5=E8=B0=8F=E8=A8=80=E3=80=81=E6=97=A5?= =?UTF-8?q?=E5=8E=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/network_service.py | 56 ++++++++++++++++- src/word_main_window.py | 104 ++++++++++++++++++++++++++++++++ test_quote.py | 44 ++++++++++++++ test_weather_insert.py | 60 ++++++++++++++++++ 4 files changed, 263 insertions(+), 1 deletion(-) create mode 100644 test_quote.py create mode 100644 test_weather_insert.py 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/word_main_window.py b/src/word_main_window.py index 80508e8..04341b2 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -694,6 +694,21 @@ class WordStyleMainWindow(QMainWindow): 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)') @@ -3515,6 +3530,95 @@ class WordStyleMainWindow(QMainWindow): 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/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_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 -- 2.34.1 From 2cdf760dd16f2417abb12b152d4cb536ba8007b0 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 17 Nov 2025 11:27:53 +0800 Subject: [PATCH 21/39] =?UTF-8?q?=E6=97=A5=E5=8E=86=E9=BB=91=E7=99=BD?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/calendar_widget.py | 293 ++++++++++++++++++++++++++++++++++++++ src/word_main_window.py | 5 + 2 files changed, 298 insertions(+) diff --git a/src/ui/calendar_widget.py b/src/ui/calendar_widget.py index ed66583..5331faf 100644 --- a/src/ui/calendar_widget.py +++ b/src/ui/calendar_widget.py @@ -13,6 +13,9 @@ from PyQt5.QtWidgets import ( from PyQt5.QtCore import QDate, Qt, pyqtSignal from PyQt5.QtGui import QFont +# 导入主题管理器 +from .theme_manager import theme_manager + class CalendarWidget(QWidget): """日历组件类""" @@ -24,6 +27,7 @@ class CalendarWidget(QWidget): super().__init__(parent) self.setup_ui() self.setup_connections() + self.setup_theme() def setup_ui(self): """设置UI界面""" @@ -266,6 +270,295 @@ class CalendarWidget(QWidget): 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__": diff --git a/src/word_main_window.py b/src/word_main_window.py index 04341b2..dbf2859 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -323,6 +323,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): """更新功能区样式""" -- 2.34.1 From e7ae6a7a1eef8d1ed0c75ec918d6f666c67c0e24 Mon Sep 17 00:00:00 2001 From: Lesacm <1500309685@qq.com> Date: Mon, 17 Nov 2025 11:35:13 +0800 Subject: [PATCH 22/39] =?UTF-8?q?UI=E6=9B=B4=E6=94=B910086?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/word_style_ui.py | 304 ++++++++++++++++++++++------------------ 1 file changed, 171 insertions(+), 133 deletions(-) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index a29a043..40938a9 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -73,139 +73,145 @@ 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) + self.add_separator() + + # ---------- 样式组(单行水平排列) ---------- + self.init_style_preview_group() + self.add_separator() + - # 样式组 - styles_group = self.create_ribbon_group("样式") - styles_layout = QVBoxLayout() - - # 创建标题样式按钮 - title_buttons_layout = QHBoxLayout() - - # 一级标题按钮 - self.heading1_btn = self.create_style_button("标题1") - self.heading1_btn.clicked.connect(self.on_heading1_clicked) - title_buttons_layout.addWidget(self.heading1_btn) - - # 二级标题按钮 - self.heading2_btn = self.create_style_button("标题2") - self.heading2_btn.clicked.connect(self.on_heading2_clicked) - title_buttons_layout.addWidget(self.heading2_btn) - - # 三级标题按钮 - self.heading3_btn = self.create_style_button("标题3") - self.heading3_btn.clicked.connect(self.on_heading3_clicked) - title_buttons_layout.addWidget(self.heading3_btn) - - styles_layout.addLayout(title_buttons_layout) - - # 第二行样式按钮 - style_buttons_layout = QHBoxLayout() - - # 四级标题按钮 - self.heading4_btn = self.create_style_button("标题4") - self.heading4_btn.clicked.connect(self.on_heading4_clicked) - style_buttons_layout.addWidget(self.heading4_btn) - - # 正文按钮 - self.body_text_btn = self.create_style_button("正文") - self.body_text_btn.clicked.connect(self.on_body_text_clicked) - style_buttons_layout.addWidget(self.body_text_btn) - - style_buttons_layout.addStretch() - styles_layout.addLayout(style_buttons_layout) - - styles_group.setLayout(styles_layout) - layout.addWidget(styles_group) - - # 编辑组 - editing_group = self.create_ribbon_group("编辑") - - # 创建查找替换按钮,使用更适合水平排列的样式 + + # ---------- 编辑组 ---------- + 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): """字体变化处理""" @@ -267,6 +273,65 @@ class WordRibbon(QFrame): """两端对齐按钮点击处理""" 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): """初始化主题""" # 连接主题切换信号 @@ -276,31 +341,19 @@ 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: @@ -330,7 +383,10 @@ class WordRibbon(QFrame): # 更新下拉框样式 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() @@ -798,24 +854,6 @@ class WordRibbon(QFrame): self.quote_group = None self.quote_visible = False - def create_ribbon_group(self, title, is_special_group=False): - """创建功能区组""" - group = QGroupBox(title) - - # 为非特殊组设置最小宽度以确保标题完整显示 - if not is_special_group: - # 根据标题长度计算合适的最小宽度 - min_width = max(100, len(title) * 12 + 40) # 基础宽度+每个字符约12px - group.setMinimumWidth(min_width) - - # 连接主题切换信号以动态更新样式 - theme_manager.theme_changed.connect(lambda: self._update_group_style(group)) - - # 立即应用当前主题样式 - self._update_group_style(group) - - return group - def _update_group_style(self, group): """更新组样式以适配当前主题""" is_dark = theme_manager.is_dark_theme() -- 2.34.1 From 43c0bd4cb6b805e36be6b5d14cc1b82368eb4025 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Mon, 17 Nov 2025 11:36:24 +0800 Subject: [PATCH 23/39] =?UTF-8?q?=E5=B0=8F=E4=BF=AE=E8=A1=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/word_main_window.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/word_main_window.py b/src/word_main_window.py index dbf2859..3e2a48c 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -3299,6 +3299,8 @@ class WordStyleMainWindow(QMainWindow): if file_path: try: + import os + import tempfile from docx import Document from docx.shared import Inches @@ -3323,10 +3325,6 @@ class WordStyleMainWindow(QMainWindow): break if img_data: - # 创建临时图片文件 - import tempfile - import os - # 检测图片类型 if img_name.lower().endswith('.png'): img_ext = '.png' -- 2.34.1 From 029ac389f8ff5b92e10d7555f154ca1806007fab Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Wed, 19 Nov 2025 08:56:51 +0800 Subject: [PATCH 24/39] =?UTF-8?q?=E4=BF=AE=E8=A1=A5=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/theme_manager.py | 2 -- src/ui/word_style_ui.py | 78 +++++++++++------------------------------ src/word_main_window.py | 1 - 3 files changed, 21 insertions(+), 60 deletions(-) diff --git a/src/ui/theme_manager.py b/src/ui/theme_manager.py index 988b05a..0978872 100644 --- a/src/ui/theme_manager.py +++ b/src/ui/theme_manager.py @@ -206,7 +206,6 @@ class ThemeManager(QObject): color: #f0f0f0; padding: 4px 0; margin: 2px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } QMenu::item { @@ -528,7 +527,6 @@ class ThemeManager(QObject): color: #333333; padding: 4px 0; margin: 2px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } QMenu::item { diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 40938a9..9b21604 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -158,13 +158,9 @@ class WordRibbon(QFrame): font_layout.addWidget(self.font_size_combo) 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_layout.addWidget(self.bold_btn) font_layout.addWidget(self.italic_btn) @@ -192,6 +188,26 @@ class WordRibbon(QFrame): self.init_style_preview_group() self.add_separator() + # ---------- 快速样式组 ---------- + quick_style_group, quick_style_layout = self.create_ribbon_group("快速样式") + + # 创建样式按钮 + self.heading1_btn = self.create_style_button("标题1") + self.heading2_btn = self.create_style_button("标题2") + self.heading3_btn = self.create_style_button("标题3") + self.heading4_btn = self.create_style_button("标题4") + self.body_text_btn = self.create_style_button("正文") + + # 添加到布局 + quick_style_layout.addWidget(self.heading1_btn) + quick_style_layout.addWidget(self.heading2_btn) + quick_style_layout.addWidget(self.heading3_btn) + quick_style_layout.addWidget(self.heading4_btn) + quick_style_layout.addWidget(self.body_text_btn) + quick_style_layout.addStretch() + layout.addWidget(quick_style_group) + self.add_separator() + # ---------- 编辑组 ---------- @@ -221,57 +237,7 @@ 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 on_heading1_clicked(self): - """一级标题按钮点击处理""" - pass - - def on_heading2_clicked(self): - """二级标题按钮点击处理""" - pass - - def on_heading3_clicked(self): - """三级标题按钮点击处理""" - pass - - def on_heading4_clicked(self): - """四级标题按钮点击处理""" - pass - - def on_body_text_clicked(self): - """正文按钮点击处理""" - pass - - def on_align_left_clicked(self): - """左对齐按钮点击处理""" - pass - - def on_align_center_clicked(self): - """居中对齐按钮点击处理""" - pass - - def on_align_right_clicked(self): - """右对齐按钮点击处理""" - pass - - def on_align_justify_clicked(self): - """两端对齐按钮点击处理""" - pass + def init_style_preview_group(self): """加入 Word 风格的样式预览区域""" @@ -750,7 +716,6 @@ class WordRibbon(QFrame): # 刷新按钮 self.refresh_weather_btn = QPushButton("🔄 刷新") - self.refresh_weather_btn.clicked.connect(self.on_refresh_weather) self.refresh_weather_btn.setFixedSize(60, 30) # 增大刷新按钮尺寸 self.refresh_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }") self.refresh_weather_btn.setToolTip("刷新天气") @@ -805,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) # 添加到第一行布局 diff --git a/src/word_main_window.py b/src/word_main_window.py index 3e2a48c..3bf8569 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -3172,7 +3172,6 @@ class WordStyleMainWindow(QMainWindow): max-width: 100%; height: auto; border-radius: 3px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); }} .image-caption {{ margin-top: 8px; -- 2.34.1 From 7642789edc7c24739fb79e1d95066abd8c6076ff Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 15:35:25 +0800 Subject: [PATCH 25/39] =?UTF-8?q?=E6=97=A5=E5=8E=86UI=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/word_main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/word_main_window.py b/src/word_main_window.py index 3bf8569..9bec582 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -3513,7 +3513,7 @@ class WordStyleMainWindow(QMainWindow): # 如果日历组件可见,调整其大小和位置以适应窗口底部 if hasattr(self, 'calendar_widget') and self.calendar_widget.isVisible(): - calendar_height = 250 # 减小高度使比例更美观 + calendar_height = 350 # 增加高度以确保所有日期都能完整显示 self.calendar_widget.setGeometry(0, self.height() - calendar_height, self.width(), calendar_height) @@ -3524,7 +3524,7 @@ class WordStyleMainWindow(QMainWindow): self.calendar_widget.hide() else: # 设置日历组件位置在窗口底部 - calendar_height = 250 # 减小高度使比例更美观 + calendar_height = 350 # 增加高度以确保所有日期都能完整显示 # 将日历组件放置在窗口底部,占据整个宽度 self.calendar_widget.setGeometry(0, self.height() - calendar_height, -- 2.34.1 From df99246ae4c477402740251f828e47e8c9db28b0 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 16:19:17 +0800 Subject: [PATCH 26/39] =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/learning_mode_window.py | 256 ++++++++++++++++++++++++++++-------- src/word_main_window.py | 129 ++++++++---------- 2 files changed, 256 insertions(+), 129 deletions(-) diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py index 49305f4..18ee641 100644 --- a/src/learning_mode_window.py +++ b/src/learning_mode_window.py @@ -4,21 +4,24 @@ 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) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect -from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor +from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextImageFormat -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): # 定义内容变化信号 content_changed = pyqtSignal(str, int) # 参数:内容,位置 # 定义关闭信号 closed = pyqtSignal() - def __init__(self, parent=None, imported_content="", current_position=0): + def __init__(self, parent=None, imported_content="", current_position=0, image_data=None, image_positions=None): """ 学习模式窗口 - 顶部显示UI.png图片 @@ -29,13 +32,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.inserted_images = set() # 用于跟踪已插入的图片 # 初始化UI self.initUI() @@ -60,6 +68,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] @@ -144,38 +158,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): @@ -301,38 +325,67 @@ 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.inserted_images.clear() + + # 设置打字逻辑 + 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 + + # 计算图片插入位置(简单地每隔一定字符插入一张图片) + image_pos = len(content) // (len(images) + 1) * (i + 1) + image_positions.append({ + 'start_pos': image_pos, + 'end_pos': image_pos + 5, # 图片占位符长度 + 'filename': filename + }) + + # 设置图片数据到打字逻辑 + self.typing_logic.set_image_data(image_data_dict) + self.typing_logic.set_image_positions(image_positions) + + # 显示初始内容(空) + 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 @@ -343,6 +396,7 @@ class LearningModeWindow(QMainWindow): - 根据导入的内容逐步显示 - 更新学习进度 - 同步内容到打字模式 + - 处理图片插入 """ # 如果正在加载文件,跳过处理 if self.is_loading_file: @@ -398,6 +452,9 @@ class LearningModeWindow(QMainWindow): f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)" ) + # 处理图片插入 + self.insert_images_if_needed() + # 只在用户新输入的字符上同步到打字模式 if self.parent_window and hasattr(self.parent_window, 'text_edit'): # 获取用户这一轮新输入的字符(与上一轮相比的新内容) @@ -414,6 +471,90 @@ class LearningModeWindow(QMainWindow): else: self.status_label.setText("继续输入以显示更多内容...") + def insert_images_if_needed(self): + """ + 根据当前进度插入图片 + """ + try: + # 检查是否有图片需要插入 + if not self.typing_logic or not hasattr(self.typing_logic, 'image_positions'): + return + + # 获取需要显示的图片列表 + images_to_display = self.typing_logic.get_images_to_display(self.current_position) + + # 检查当前显示位置是否有图片需要插入 + for image_info in images_to_display: + image_key = f"{image_info['start_pos']}_{image_info['filename']}" + + # 跳过已经插入过的图片 + if image_key in self.inserted_images: + continue + + # 当打字进度达到图片位置时插入图片 + if self.current_position >= image_info['start_pos']: + # 在图片位置插入图片 + cursor = self.text_display_widget.text_display.textCursor() + + # 计算图片应该插入的位置(基于原始内容位置) + insert_position = image_info['start_pos'] + + # 确保插入位置有效(不能超过当前显示内容长度) + if insert_position <= len(self.text_display_widget.text_display.toPlainText()): + # 移动光标到插入位置 + cursor.setPosition(insert_position) + self.text_display_widget.text_display.setTextCursor(cursor) + + # 创建图片格式 + image_format = QTextImageFormat() + + # 获取图片数据 + image_data = None + if hasattr(self.typing_logic, 'image_data') and image_info['filename'] in self.typing_logic.image_data: + image_data = self.typing_logic.image_data[image_info['filename']] + elif 'data' in image_info: + image_data = image_info.get('data') + + # 如果有图片数据,插入图片 + if image_data: + pixmap = QPixmap() + if pixmap.loadFromData(image_data): + # 生成临时文件名 + safe_filename = "".join(c for c in image_info['filename'] if c.isalnum() or c in ('.', '_', '-')) + temp_file = os.path.join(tempfile.gettempdir(), f"magicword_temp_{safe_filename}") + + # 保存图片到临时文件 + if pixmap.save(temp_file): + # 设置图片格式 + image_format.setName(temp_file) + image_format.setWidth(200) + image_format.setHeight(150) + + # 插入图片 + cursor.insertImage(image_format) + + # 标记图片已插入 + self.inserted_images.add(image_key) + + print(f"图片 {image_info['filename']} 已在位置 {insert_position} 插入") + else: + print(f"保存临时图片文件失败: {image_info['filename']}") + else: + print(f"加载图片数据失败: {image_info['filename']}") + else: + # 如果没有图片数据,插入占位符文本 + cursor.insertText(f"[图片: {image_info['filename']}]\n") + + # 恢复光标位置到文本末尾 + cursor = self.text_display_widget.text_display.textCursor() + cursor.movePosition(cursor.End) + self.text_display_widget.text_display.setTextCursor(cursor) + + except Exception as e: + print(f"插入图片失败: {str(e)}") + import traceback + traceback.print_exc() + def show_about(self): """ 显示关于对话框 @@ -424,7 +565,8 @@ class LearningModeWindow(QMainWindow): "• 顶部显示UI界面图片\n" "• 下方为打字输入区域\n" "• 导入文件后逐步显示内容\n" - "• 实时显示学习进度\n\n" + "• 实时显示学习进度\n" + "• 支持图片显示\n\n" "使用方法:\n" "1. 点击'文件'->'导入文件'选择学习材料\n" "2. 在下方文本区域开始打字\n" diff --git a/src/word_main_window.py b/src/word_main_window.py index 9bec582..caab2e5 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -6,7 +6,8 @@ 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, QTextBlockFormat @@ -1853,7 +1854,7 @@ class WordStyleMainWindow(QMainWindow): self.status_bar.showMessage("新建文档 - 打字模式,可以自由开始打字", 3000) def import_file(self): - """导入文件 - 仅在导入时存储内容,不立即显示""" + """导入文件 - 根据模式决定是否立即显示""" file_path, _ = QFileDialog.getOpenFileName( self, "导入文件", "", "文档文件 (*.docx *.txt *.pdf *.html);;所有文件 (*.*)" @@ -1874,7 +1875,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 @@ -1916,8 +1917,9 @@ 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: @@ -1934,7 +1936,7 @@ class WordStyleMainWindow(QMainWindow): content = parser.parse_file(file_path) if content: - # 存储完整内容但不立即显示 + # 存储完整内容 self.imported_content = content self.displayed_chars = 0 @@ -1949,8 +1951,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)}") @@ -1975,7 +1978,7 @@ class WordStyleMainWindow(QMainWindow): # 设置文件加载标志 self.is_loading_file = True - # 存储完整内容但不立即显示 + # 存储完整内容 self.imported_content = content self.displayed_chars = 0 @@ -2149,8 +2152,17 @@ class WordStyleMainWindow(QMainWindow): current_position = 0 self.imported_content = current_text - # 创建学习模式窗口,直接传递导入内容 - self.learning_window = LearningModeWindow(self, imported_content, current_position) + # 准备图片数据 + 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, image_data, image_positions) # 连接学习模式窗口的内容变化信号 self.learning_window.content_changed.connect(self.on_learning_content_changed) @@ -2738,46 +2750,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() @@ -2785,10 +2788,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() @@ -2813,42 +2816,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) -- 2.34.1 From f89f4b9e3598059891696cbbb174ee2c4ec5b707 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 16:38:49 +0800 Subject: [PATCH 27/39] =?UTF-8?q?=E5=AD=A6=E4=B9=A0=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/learning_mode_window.py | 318 +++++++++++++++++++++++++----------- 1 file changed, 222 insertions(+), 96 deletions(-) diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py index 18ee641..4634578 100644 --- a/src/learning_mode_window.py +++ b/src/learning_mode_window.py @@ -4,9 +4,10 @@ import os from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QFrame, QMenuBar, QAction, QFileDialog, QMessageBox, QApplication, - QSplitter, QScrollArea, QStatusBar, QProgressBar, QTextBrowser, QSizePolicy) + 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, QTextImageFormat +from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor # 修复导入路径 from ui.components import CustomTitleBar, TextDisplayWidget @@ -43,7 +44,7 @@ class LearningModeWindow(QMainWindow): self.image_positions = image_positions or [] self.typing_logic = None self.is_loading_file = False - self.inserted_images = set() # 用于跟踪已插入的图片 + self.extracted_images = [] # 用于存储提取的图片数据 # 初始化UI self.initUI() @@ -234,6 +235,7 @@ class LearningModeWindow(QMainWindow): 创建输入区域 - 创建文本显示组件 - 设置与主系统相同的样式 + - 创建图片列表区域 """ # 创建文本显示组件(复用主系统的组件) self.text_display_widget = TextDisplayWidget(self) @@ -250,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): """ @@ -351,8 +386,8 @@ class LearningModeWindow(QMainWindow): self.imported_content = content self.current_position = 0 - # 重置已插入的图片集合 - self.inserted_images.clear() + # 保存提取的图片数据 + self.extracted_images = images # 设置打字逻辑 if self.typing_logic: @@ -363,21 +398,47 @@ class LearningModeWindow(QMainWindow): image_data_dict = {} image_positions = [] - # 为每张图片生成位置信息 + # 为每张图片生成位置信息 - 改进位置计算逻辑 for i, (filename, image_data) in enumerate(images): image_data_dict[filename] = image_data - # 计算图片插入位置(简单地每隔一定字符插入一张图片) - image_pos = len(content) // (len(images) + 1) * (i + 1) + # 改进图片位置计算,确保图片能在用户早期打字时显示 + 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 + 5, # 图片占位符长度 + '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() @@ -452,8 +513,7 @@ class LearningModeWindow(QMainWindow): f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)" ) - # 处理图片插入 - self.insert_images_if_needed() + # 只在用户新输入的字符上同步到打字模式 if self.parent_window and hasattr(self.parent_window, 'text_edit'): @@ -471,89 +531,7 @@ class LearningModeWindow(QMainWindow): else: self.status_label.setText("继续输入以显示更多内容...") - def insert_images_if_needed(self): - """ - 根据当前进度插入图片 - """ - try: - # 检查是否有图片需要插入 - if not self.typing_logic or not hasattr(self.typing_logic, 'image_positions'): - return - - # 获取需要显示的图片列表 - images_to_display = self.typing_logic.get_images_to_display(self.current_position) - - # 检查当前显示位置是否有图片需要插入 - for image_info in images_to_display: - image_key = f"{image_info['start_pos']}_{image_info['filename']}" - - # 跳过已经插入过的图片 - if image_key in self.inserted_images: - continue - - # 当打字进度达到图片位置时插入图片 - if self.current_position >= image_info['start_pos']: - # 在图片位置插入图片 - cursor = self.text_display_widget.text_display.textCursor() - - # 计算图片应该插入的位置(基于原始内容位置) - insert_position = image_info['start_pos'] - - # 确保插入位置有效(不能超过当前显示内容长度) - if insert_position <= len(self.text_display_widget.text_display.toPlainText()): - # 移动光标到插入位置 - cursor.setPosition(insert_position) - self.text_display_widget.text_display.setTextCursor(cursor) - - # 创建图片格式 - image_format = QTextImageFormat() - - # 获取图片数据 - image_data = None - if hasattr(self.typing_logic, 'image_data') and image_info['filename'] in self.typing_logic.image_data: - image_data = self.typing_logic.image_data[image_info['filename']] - elif 'data' in image_info: - image_data = image_info.get('data') - - # 如果有图片数据,插入图片 - if image_data: - pixmap = QPixmap() - if pixmap.loadFromData(image_data): - # 生成临时文件名 - safe_filename = "".join(c for c in image_info['filename'] if c.isalnum() or c in ('.', '_', '-')) - temp_file = os.path.join(tempfile.gettempdir(), f"magicword_temp_{safe_filename}") - - # 保存图片到临时文件 - if pixmap.save(temp_file): - # 设置图片格式 - image_format.setName(temp_file) - image_format.setWidth(200) - image_format.setHeight(150) - - # 插入图片 - cursor.insertImage(image_format) - - # 标记图片已插入 - self.inserted_images.add(image_key) - - print(f"图片 {image_info['filename']} 已在位置 {insert_position} 插入") - else: - print(f"保存临时图片文件失败: {image_info['filename']}") - else: - print(f"加载图片数据失败: {image_info['filename']}") - else: - # 如果没有图片数据,插入占位符文本 - cursor.insertText(f"[图片: {image_info['filename']}]\n") - - # 恢复光标位置到文本末尾 - cursor = self.text_display_widget.text_display.textCursor() - cursor.movePosition(cursor.End) - self.text_display_widget.text_display.setTextCursor(cursor) - - except Exception as e: - print(f"插入图片失败: {str(e)}") - import traceback - traceback.print_exc() + def show_about(self): """ @@ -594,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 -- 2.34.1 From 3ef9200515fa5eaecb239e53a1f72cb91e9fb121 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 16:41:39 +0800 Subject: [PATCH 28/39] =?UTF-8?q?=E9=BB=84=E5=BD=AA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/word_main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/word_main_window.py b/src/word_main_window.py index caab2e5..9a76eb6 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -3406,8 +3406,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: -- 2.34.1 From ffa5e645a5038c5e40700bfeed990514cb088470 Mon Sep 17 00:00:00 2001 From: Lesacm <1500309685@qq.com> Date: Thu, 20 Nov 2025 18:26:08 +0800 Subject: [PATCH 29/39] UI fix --- src/ui/word_style_ui.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 9b21604..562b183 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -188,28 +188,6 @@ class WordRibbon(QFrame): self.init_style_preview_group() self.add_separator() - # ---------- 快速样式组 ---------- - quick_style_group, quick_style_layout = self.create_ribbon_group("快速样式") - - # 创建样式按钮 - self.heading1_btn = self.create_style_button("标题1") - self.heading2_btn = self.create_style_button("标题2") - self.heading3_btn = self.create_style_button("标题3") - self.heading4_btn = self.create_style_button("标题4") - self.body_text_btn = self.create_style_button("正文") - - # 添加到布局 - quick_style_layout.addWidget(self.heading1_btn) - quick_style_layout.addWidget(self.heading2_btn) - quick_style_layout.addWidget(self.heading3_btn) - quick_style_layout.addWidget(self.heading4_btn) - quick_style_layout.addWidget(self.body_text_btn) - quick_style_layout.addStretch() - layout.addWidget(quick_style_group) - self.add_separator() - - - # ---------- 编辑组 ---------- editing_group, editing_layout = self.create_ribbon_group("编辑") self.find_btn = QToolButton() -- 2.34.1 From 7a85f3654485936202af6551ad31d7fb75da170e Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 20:14:09 +0800 Subject: [PATCH 30/39] =?UTF-8?q?=E5=A4=A9=E6=B0=94=E6=82=AC=E6=B5=AE?= =?UTF-8?q?=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/weather_floating_widget.py | 609 ++++++++++++++++++++++++++++++ src/ui/word_style_ui.py | 11 + src/word_main_window.py | 50 +++ 3 files changed, 670 insertions(+) create mode 100644 src/ui/weather_floating_widget.py diff --git a/src/ui/weather_floating_widget.py b/src/ui/weather_floating_widget.py new file mode 100644 index 0000000..d520962 --- /dev/null +++ b/src/ui/weather_floating_widget.py @@ -0,0 +1,609 @@ +# -*- 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(400, 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(15, 15, 15, 15) # 减小内边距 + content_layout.setSpacing(10) # 减小间距 + + # 设置最小尺寸策略 + 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() + + # 关闭按钮 + 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(8) # 适当间距 + weather_display_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距 + + self.weather_icon_label = QLabel("🌞") + self.weather_icon_label.setFont(QFont("Arial", 28)) + self.weather_icon_label.setAlignment(Qt.AlignCenter) + self.weather_icon_label.setFixedSize(60, 60) + weather_display_layout.addWidget(self.weather_icon_label) + + # 温度和城市信息 + temp_city_layout = QVBoxLayout() + temp_city_layout.setSpacing(8) # 增加间距 + temp_city_layout.setContentsMargins(0, 0, 0, 0) + + self.temperature_label = QLabel("25°C") + self.temperature_label.setFont(QFont("Arial", 20, QFont.Bold)) + self.temperature_label.setObjectName("temperatureLabel") + temp_city_layout.addWidget(self.temperature_label) + + self.city_label = QLabel("北京") + self.city_label.setFont(QFont("Arial", 12)) + self.city_label.setObjectName("cityLabel") + temp_city_layout.addWidget(self.city_label) + + weather_display_layout.addLayout(temp_city_layout) + weather_display_layout.addStretch() + + content_layout.addLayout(weather_display_layout) + + # 天气描述 + self.weather_desc_label = QLabel("晴天") + self.weather_desc_label.setFont(QFont("Arial", 12)) + self.weather_desc_label.setObjectName("weatherDescLabel") + self.weather_desc_label.setAlignment(Qt.AlignCenter) + content_layout.addWidget(self.weather_desc_label) + + # 详细信息(湿度、风速) + details_layout = QHBoxLayout() + details_layout.setSpacing(10) # 适当间距 + details_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距 + + self.humidity_label = QLabel("湿度: 45%") + self.humidity_label.setFont(QFont("Arial", 11)) + self.humidity_label.setObjectName("detailLabel") + details_layout.addWidget(self.humidity_label) + + self.wind_label = QLabel("风速: 2级") + self.wind_label.setFont(QFont("Arial", 11)) + self.wind_label.setObjectName("detailLabel") + details_layout.addWidget(self.wind_label) + + content_layout.addLayout(details_layout) + + # 城市选择区域 + city_layout = QHBoxLayout() + city_layout.setSpacing(10) + city_layout.setContentsMargins(0, 0, 0, 0) + + self.city_combo = QComboBox() + self.city_combo.setObjectName("cityCombo") + # 添加所有省会城市,与主窗口保持一致 + self.city_combo.addItems([ + '自动定位', + '北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', # 一线城市 + '天津', '重庆', '苏州', '青岛', '大连', '宁波', '厦门', '无锡', '佛山', # 新一线城市 + '石家庄', '太原', '呼和浩特', '沈阳', '长春', '哈尔滨', # 东北华北 + '合肥', '福州', '南昌', '济南', '郑州', '长沙', '南宁', '海口', # 华东华中华南 + '贵阳', '昆明', '拉萨', '兰州', '西宁', '银川', '乌鲁木齐' # 西南西北 + ]) + self.city_combo.setFixedWidth(120) # 增加城市选择框宽度,与主窗口保持一致 + city_layout.addWidget(self.city_combo) + + city_layout.addStretch() + + content_layout.addLayout(city_layout) + + # 按钮区域 + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + button_layout.setContentsMargins(0, 0, 0, 0) + + self.refresh_btn = QPushButton("刷新") + self.refresh_btn.setObjectName("refreshButton") + button_layout.addWidget(self.refresh_btn) + + button_layout.addStretch() + + self.detail_btn = QPushButton("详情") + self.detail_btn.setObjectName("detailButton") + 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: 8px; + }} + 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: 3px 5px; + 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: 3px 5px; + margin: 2px; + }} + QFrame#separator {{ + background-color: {colors['border']}; + }} + QPushButton#closeButton {{ + background-color: transparent; + border: none; + color: {colors['text']}; + font-weight: bold; + }} + QPushButton#closeButton:hover {{ + background-color: #e81123; + color: white; + border-radius: 3px; + }} + 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 8px; + font-size: 11px; + font-weight: 500; + min-height: 24px; + }} + QComboBox#cityCombo:hover {{ + border-color: {colors['accent']}; + }} + QComboBox#cityCombo::drop-down {{ + border: none; + width: 15px; + }} + QComboBox#cityCombo::down-arrow {{ + image: none; + border-left: 3px solid transparent; + border-right: 3px 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: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + }} + 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: 3px 5px; + 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: 3px 5px; + margin: 2px; + }} + QFrame#separator {{ + background-color: {colors['border']}; + }} + QPushButton#closeButton {{ + background-color: transparent; + border: none; + color: {colors['text']}; + font-weight: bold; + }} + QPushButton#closeButton:hover {{ + background-color: #e81123; + color: white; + border-radius: 3px; + }} + 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 8px; + font-size: 11px; + font-weight: 500; + min-height: 24px; + }} + QComboBox#cityCombo:hover {{ + border-color: {colors['accent']}; + }} + QComboBox#cityCombo::drop-down {{ + border: none; + width: 15px; + }} + QComboBox#cityCombo::down-arrow {{ + image: none; + border-left: 3px solid transparent; + border-right: 3px 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 562b183..102b28c 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -698,8 +698,15 @@ class WordRibbon(QFrame): 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("切换天气悬浮窗口") + control_layout.addWidget(self.city_combo) control_layout.addWidget(self.refresh_weather_btn) + control_layout.addWidget(self.floating_weather_btn) # 添加右侧弹性空间,确保内容居中 control_layout.addStretch() @@ -852,6 +859,10 @@ class WordRibbon(QFrame): """刷新天气按钮点击处理""" pass + def on_floating_weather(self): + """悬浮窗口按钮点击处理""" + pass + def on_city_changed(self, city): """城市选择变化处理""" pass diff --git a/src/word_main_window.py b/src/word_main_window.py index 9a76eb6..eb7afbb 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -19,6 +19,7 @@ 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.theme_manager import theme_manager @@ -108,6 +109,12 @@ class WordStyleMainWindow(QMainWindow): 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.setWindowTitle("文档1 - MagicWord") self.setGeometry(100, 100, 1200, 800) @@ -440,8 +447,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): """刷新天气""" @@ -692,6 +705,11 @@ 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) # 插入菜单 insert_menu = menubar.addMenu('插入(I)') @@ -929,6 +947,9 @@ class WordStyleMainWindow(QMainWindow): 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, 'calendar_widget'): self.calendar_widget.date_selected.connect(self.insert_date_to_editor) @@ -1623,9 +1644,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) @@ -1721,6 +1748,29 @@ 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_weather_tools(self, checked): """切换天气工具组显示""" if checked: -- 2.34.1 From efaf1ae33cf438d6adbb1ad4aca8bde7e61cc74a Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 20:24:04 +0800 Subject: [PATCH 31/39] =?UTF-8?q?=E5=A4=A9=E6=B0=94=E6=82=AC=E6=B5=AE?= =?UTF-8?q?=E7=AA=97=E5=8F=A3fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/weather_floating_widget.py | 130 ++++++++++++++++-------------- 1 file changed, 70 insertions(+), 60 deletions(-) diff --git a/src/ui/weather_floating_widget.py b/src/ui/weather_floating_widget.py index d520962..047c619 100644 --- a/src/ui/weather_floating_widget.py +++ b/src/ui/weather_floating_widget.py @@ -39,7 +39,7 @@ class WeatherFloatingWidget(QDialog): """设置UI界面""" # 设置窗口属性 self.setWindowTitle("天气") - self.setFixedSize(400, 320) # 增加窗口尺寸 + self.setFixedSize(360, 280) # 调整窗口尺寸使其更紧凑 # 创建主框架,用于实现圆角和阴影效果 self.main_frame = QFrame() @@ -52,8 +52,8 @@ class WeatherFloatingWidget(QDialog): # 内容布局 content_layout = QVBoxLayout(self.main_frame) - content_layout.setContentsMargins(15, 15, 15, 15) # 减小内边距 - content_layout.setSpacing(10) # 减小间距 + content_layout.setContentsMargins(10, 10, 10, 10) # 减小内边距使布局更紧凑 + content_layout.setSpacing(6) # 减小间距使布局更紧凑 # 设置最小尺寸策略 self.main_frame.setMinimumSize(380, 300) @@ -65,6 +65,8 @@ class WeatherFloatingWidget(QDialog): 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("×") @@ -82,27 +84,27 @@ class WeatherFloatingWidget(QDialog): # 天气图标和温度显示区域 weather_display_layout = QHBoxLayout() - weather_display_layout.setSpacing(8) # 适当间距 + 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", 28)) + self.weather_icon_label.setFont(QFont("Arial", 24)) # 稍微减小字体大小 self.weather_icon_label.setAlignment(Qt.AlignCenter) - self.weather_icon_label.setFixedSize(60, 60) + self.weather_icon_label.setFixedSize(50, 50) # 减小尺寸 weather_display_layout.addWidget(self.weather_icon_label) # 温度和城市信息 temp_city_layout = QVBoxLayout() - temp_city_layout.setSpacing(8) # 增加间距 + temp_city_layout.setSpacing(4) # 减小间距使布局更紧凑 temp_city_layout.setContentsMargins(0, 0, 0, 0) self.temperature_label = QLabel("25°C") - self.temperature_label.setFont(QFont("Arial", 20, QFont.Bold)) + self.temperature_label.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", 12)) + self.city_label.setFont(QFont("Arial", 11)) # 稍微减小字体大小 self.city_label.setObjectName("cityLabel") temp_city_layout.addWidget(self.city_label) @@ -113,23 +115,23 @@ class WeatherFloatingWidget(QDialog): # 天气描述 self.weather_desc_label = QLabel("晴天") - self.weather_desc_label.setFont(QFont("Arial", 12)) + 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(10) # 适当间距 + details_layout.setSpacing(6) # 减小间距使布局更紧凑 details_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距 self.humidity_label = QLabel("湿度: 45%") - self.humidity_label.setFont(QFont("Arial", 11)) + 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", 11)) + self.wind_label.setFont(QFont("Arial", 10)) # 稍微减小字体大小 self.wind_label.setObjectName("detailLabel") details_layout.addWidget(self.wind_label) @@ -137,7 +139,7 @@ class WeatherFloatingWidget(QDialog): # 城市选择区域 city_layout = QHBoxLayout() - city_layout.setSpacing(10) + city_layout.setSpacing(6) # 减小间距使布局更紧凑 city_layout.setContentsMargins(0, 0, 0, 0) self.city_combo = QComboBox() @@ -151,7 +153,7 @@ class WeatherFloatingWidget(QDialog): '合肥', '福州', '南昌', '济南', '郑州', '长沙', '南宁', '海口', # 华东华中华南 '贵阳', '昆明', '拉萨', '兰州', '西宁', '银川', '乌鲁木齐' # 西南西北 ]) - self.city_combo.setFixedWidth(120) # 增加城市选择框宽度,与主窗口保持一致 + self.city_combo.setFixedWidth(100) # 减小城市选择框宽度使布局更紧凑 city_layout.addWidget(self.city_combo) city_layout.addStretch() @@ -160,17 +162,19 @@ class WeatherFloatingWidget(QDialog): # 按钮区域 button_layout = QHBoxLayout() - button_layout.setSpacing(10) + 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) @@ -209,56 +213,59 @@ class WeatherFloatingWidget(QDialog): QLabel {{ color: {colors['text']}; background-color: transparent; - padding: 4px 6px; - margin: 2px; + padding: 3px 5px; // 适度增加padding使布局更舒适 + margin: 1px; }} QLabel#temperatureLabel {{ color: {colors['accent']}; - font-size: 20px; + font-size: 19px; // 适度增大字体大小 font-weight: bold; - padding: 6px 8px; - margin: 3px; + padding: 5px 7px; // 适度增加padding使布局更舒适 + margin: 2px; }} QLabel#cityLabel {{ color: {colors['text_secondary']}; - font-size: 12px; - padding: 3px 5px; - margin: 2px; + font-size: 12px; // 适度增大字体大小 + padding: 3px 5px; // 适度增加padding使布局更舒适 + margin: 1px; }} QLabel#weatherDescLabel {{ color: {colors['text']}; - font-size: 12px; + font-size: 12px; // 适度增大字体大小 font-weight: 500; - padding: 4px 6px; - margin: 2px; + padding: 3px 5px; // 适度增加padding使布局更舒适 + margin: 1px; }} QLabel#detailLabel {{ color: {colors['text_secondary']}; - font-size: 11px; - padding: 3px 5px; - margin: 2px; + font-size: 11px; // 适度增大字体大小 + padding: 3px 5px; // 适度增加padding使布局更舒适 + margin: 1px; }} QFrame#separator {{ background-color: {colors['border']}; }} QPushButton#closeButton {{ - background-color: transparent; + 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: 3px; + border-radius: 6px; }} QPushButton#refreshButton, QPushButton#detailButton {{ background-color: {colors['accent']}; color: white; border: none; - border-radius: 6px; - padding: 6px 16px; - font-size: 11px; + border-radius: 6px; // 适度增加圆角 + padding: 5px 14px; // 适度增加padding使按钮更舒适 + font-size: 11px; // 适度增大字体大小 font-weight: 500; }} QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{ @@ -268,24 +275,24 @@ class WeatherFloatingWidget(QDialog): background-color: {colors['surface']}; color: {colors['text']}; border: 1px solid {colors['border']}; - border-radius: 6px; - padding: 4px 8px; - font-size: 11px; + border-radius: 6px; // 适度增加圆角 + padding: 4px 7px; // 适度增加padding使布局更舒适 + font-size: 11px; // 适度增大字体大小 font-weight: 500; - min-height: 24px; + min-height: 24px; // 适度增加最小高度使布局更舒适 }} QComboBox#cityCombo:hover {{ border-color: {colors['accent']}; }} QComboBox#cityCombo::drop-down {{ border: none; - width: 15px; + width: 14px; // 适度增加宽度 }} QComboBox#cityCombo::down-arrow {{ image: none; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-top: 5px solid {colors['text']}; + border-left: 2px solid transparent; + border-right: 2px solid transparent; + border-top: 5px solid {colors['text']}; // 适度增加箭头大小 }} """) else: @@ -294,8 +301,8 @@ class WeatherFloatingWidget(QDialog): QFrame#mainFrame {{ background-color: {colors['surface']}; border: 1px solid {colors['border']}; - border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }} QLabel {{ color: {colors['text']}; @@ -313,7 +320,7 @@ class WeatherFloatingWidget(QDialog): QLabel#cityLabel {{ color: {colors['text_secondary']}; font-size: 12px; - padding: 3px 5px; + padding: 4px 6px; margin: 2px; }} QLabel#weatherDescLabel {{ @@ -326,30 +333,33 @@ class WeatherFloatingWidget(QDialog): QLabel#detailLabel {{ color: {colors['text_secondary']}; font-size: 11px; - padding: 3px 5px; + padding: 4px 6px; margin: 2px; }} QFrame#separator {{ background-color: {colors['border']}; }} QPushButton#closeButton {{ - background-color: transparent; + 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: 3px; + border-radius: 6px; }} QPushButton#refreshButton, QPushButton#detailButton {{ background-color: {colors['accent']}; color: white; border: none; - border-radius: 6px; - padding: 6px 16px; - font-size: 11px; + border-radius: 6px; // 适度增加圆角 + padding: 5px 14px; // 适度增加padding使按钮更舒适 + font-size: 11px; // 适度增大字体大小 font-weight: 500; }} QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{ @@ -359,24 +369,24 @@ class WeatherFloatingWidget(QDialog): background-color: {colors['surface']}; color: {colors['text']}; border: 1px solid {colors['border']}; - border-radius: 6px; - padding: 4px 8px; - font-size: 11px; + border-radius: 6px; // 适度增加圆角 + padding: 4px 7px; // 适度增加padding使布局更舒适 + font-size: 11px; // 适度增大字体大小 font-weight: 500; - min-height: 24px; + min-height: 24px; // 适度增加最小高度使布局更舒适 }} QComboBox#cityCombo:hover {{ border-color: {colors['accent']}; }} QComboBox#cityCombo::drop-down {{ border: none; - width: 15px; + width: 14px; // 适度增加宽度 }} QComboBox#cityCombo::down-arrow {{ image: none; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-top: 5px solid {colors['text']}; + border-left: 2px solid transparent; + border-right: 2px solid transparent; + border-top: 5px solid {colors['text']}; // 适度增加箭头大小 }} """) -- 2.34.1 From bfdb9d124c1a7de1ee102023e18986d2a34b9bf8 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 20:36:08 +0800 Subject: [PATCH 32/39] =?UTF-8?q?=E5=8F=A4=E8=AF=97=E5=8F=A5=E6=82=AC?= =?UTF-8?q?=E6=B5=AE=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/quote_floating_widget.py | 415 ++++++++++++++++++++++++++++++++ src/ui/word_style_ui.py | 8 + src/word_main_window.py | 37 ++- 3 files changed, 459 insertions(+), 1 deletion(-) create mode 100644 src/ui/quote_floating_widget.py diff --git a/src/ui/quote_floating_widget.py b/src/ui/quote_floating_widget.py new file mode 100644 index 0000000..ae740b7 --- /dev/null +++ b/src/ui/quote_floating_widget.py @@ -0,0 +1,415 @@ +#!/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() # 刷新请求信号 + + 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() + 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 {{ + background-color: {colors['accent']}; + color: white; + border: none; + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; + font-weight: 500; + }} + QPushButton#refreshButton: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 {{ + background-color: {colors['accent']}; + color: white; + border: none; + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; + font-weight: 500; + }} + QPushButton#refreshButton: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 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/word_style_ui.py b/src/ui/word_style_ui.py index 102b28c..07ad4ff 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() @@ -704,9 +705,16 @@ class WordRibbon(QFrame): 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("切换每日谏言悬浮窗口") + 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.addStretch() diff --git a/src/word_main_window.py b/src/word_main_window.py index eb7afbb..8d8929a 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -20,6 +20,7 @@ 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.theme_manager import theme_manager @@ -115,6 +116,12 @@ class WordStyleMainWindow(QMainWindow): 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.setWindowTitle("文档1 - MagicWord") self.setGeometry(100, 100, 1200, 800) @@ -710,6 +717,11 @@ class WordStyleMainWindow(QMainWindow): 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) # 插入菜单 insert_menu = menubar.addMenu('插入(I)') @@ -946,9 +958,10 @@ class WordStyleMainWindow(QMainWindow): 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'): @@ -1771,6 +1784,22 @@ class WordStyleMainWindow(QMainWindow): """天气悬浮窗口关闭时的处理""" 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_weather_tools(self, checked): """切换天气工具组显示""" if checked: @@ -1805,6 +1834,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): """处理名言获取成功""" -- 2.34.1 From 07ac34ee4685f97a4e35ae3fe587eb28b324d40e Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 20:51:48 +0800 Subject: [PATCH 33/39] =?UTF-8?q?=E5=8F=A4=E8=AF=97=E5=8F=A5=E6=8F=92?= =?UTF-8?q?=E5=85=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/quote_floating_widget.py | 32 ++++++++++++++++++++++++++++---- src/word_main_window.py | 15 +++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/ui/quote_floating_widget.py b/src/ui/quote_floating_widget.py index ae740b7..ad37bfe 100644 --- a/src/ui/quote_floating_widget.py +++ b/src/ui/quote_floating_widget.py @@ -18,6 +18,7 @@ class QuoteFloatingWidget(QWidget): # 定义信号 closed = pyqtSignal() # 窗口关闭信号 refresh_requested = pyqtSignal() # 刷新请求信号 + insert_requested = pyqtSignal(str) # 插入请求信号,传递要插入的文本 def __init__(self, parent=None): super().__init__(parent) @@ -111,6 +112,13 @@ class QuoteFloatingWidget(QWidget): 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) # 设置主布局 @@ -184,7 +192,7 @@ class QuoteFloatingWidget(QWidget): color: white; border-radius: 6px; }} - QPushButton#refreshButton {{ + QPushButton#refreshButton, QPushButton#insertButton {{ background-color: {colors['accent']}; color: white; border: none; @@ -193,7 +201,7 @@ class QuoteFloatingWidget(QWidget): font-size: 11px; font-weight: 500; }} - QPushButton#refreshButton:hover {{ + QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{ background-color: {colors['accent_hover']}; }} """) @@ -254,7 +262,7 @@ class QuoteFloatingWidget(QWidget): color: white; border-radius: 6px; }} - QPushButton#refreshButton {{ + QPushButton#refreshButton, QPushButton#insertButton {{ background-color: {colors['accent']}; color: white; border: none; @@ -263,7 +271,7 @@ class QuoteFloatingWidget(QWidget): font-size: 11px; font-weight: 500; }} - QPushButton#refreshButton:hover {{ + QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{ background-color: {colors['accent_hover']}; }} """) @@ -295,6 +303,22 @@ class QuoteFloatingWidget(QWidget): # 同时直接获取新的内容并更新显示 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: diff --git a/src/word_main_window.py b/src/word_main_window.py index 8d8929a..4f4851d 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -121,6 +121,7 @@ class WordStyleMainWindow(QMainWindow): 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.setWindowTitle("文档1 - MagicWord") @@ -1800,6 +1801,20 @@ class WordStyleMainWindow(QMainWindow): """每日谏言悬浮窗口关闭时的处理""" 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 toggle_weather_tools(self, checked): """切换天气工具组显示""" if checked: -- 2.34.1 From 2415e097486ee35fbf746dfc2a91af909752ff04 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 20:58:53 +0800 Subject: [PATCH 34/39] =?UTF-8?q?=E6=97=A5=E5=8E=86=E6=82=AC=E6=B5=AE?= =?UTF-8?q?=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/calendar_floating_widget.py | 482 +++++++++++++++++++++++++++++ src/ui/word_style_ui.py | 7 + src/word_main_window.py | 48 ++- 3 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 src/ui/calendar_floating_widget.py 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/word_style_ui.py b/src/ui/word_style_ui.py index 07ad4ff..65e0525 100644 --- a/src/ui/word_style_ui.py +++ b/src/ui/word_style_ui.py @@ -711,10 +711,17 @@ class WordRibbon(QFrame): 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() diff --git a/src/word_main_window.py b/src/word_main_window.py index 4f4851d..6a9abcc 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -21,6 +21,7 @@ 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 @@ -116,13 +117,17 @@ class WordStyleMainWindow(QMainWindow): 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) @@ -149,6 +154,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() @@ -723,6 +736,11 @@ class WordStyleMainWindow(QMainWindow): 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)') @@ -1800,6 +1818,22 @@ class WordStyleMainWindow(QMainWindow): 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): """将古诗句插入到光标位置""" @@ -1814,6 +1848,18 @@ class WordStyleMainWindow(QMainWindow): # 从文本中提取诗句部分用于显示 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): """切换天气工具组显示""" -- 2.34.1 From cd39d74ace6c318c61cefbb265cbd3b33b580090 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 21:06:46 +0800 Subject: [PATCH 35/39] =?UTF-8?q?=E5=A4=A9=E6=B0=94=E6=82=AC=E6=B5=AE?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/weather_floating_widget.py | 71 ++++++++++++++++--------------- src/word_main_window.py | 13 +++++- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/src/ui/weather_floating_widget.py b/src/ui/weather_floating_widget.py index 047c619..bb373de 100644 --- a/src/ui/weather_floating_widget.py +++ b/src/ui/weather_floating_widget.py @@ -203,44 +203,45 @@ class WeatherFloatingWidget(QDialog): 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: 8px; + border-radius: 10px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); }} QLabel {{ color: {colors['text']}; background-color: transparent; - padding: 3px 5px; // 适度增加padding使布局更舒适 - margin: 1px; + padding: 4px 6px; + margin: 2px; }} QLabel#temperatureLabel {{ color: {colors['accent']}; - font-size: 19px; // 适度增大字体大小 + font-size: 20px; font-weight: bold; - padding: 5px 7px; // 适度增加padding使布局更舒适 - margin: 2px; + padding: 6px 8px; + margin: 3px; }} QLabel#cityLabel {{ color: {colors['text_secondary']}; - font-size: 12px; // 适度增大字体大小 - padding: 3px 5px; // 适度增加padding使布局更舒适 - margin: 1px; + font-size: 12px; + padding: 4px 6px; + margin: 2px; }} QLabel#weatherDescLabel {{ color: {colors['text']}; - font-size: 12px; // 适度增大字体大小 + font-size: 12px; font-weight: 500; - padding: 3px 5px; // 适度增加padding使布局更舒适 - margin: 1px; + padding: 4px 6px; + margin: 2px; }} QLabel#detailLabel {{ color: {colors['text_secondary']}; - font-size: 11px; // 适度增大字体大小 - padding: 3px 5px; // 适度增加padding使布局更舒适 - margin: 1px; + font-size: 11px; + padding: 4px 6px; + margin: 2px; }} QFrame#separator {{ background-color: {colors['border']}; @@ -263,9 +264,9 @@ class WeatherFloatingWidget(QDialog): background-color: {colors['accent']}; color: white; border: none; - border-radius: 6px; // 适度增加圆角 - padding: 5px 14px; // 适度增加padding使按钮更舒适 - font-size: 11px; // 适度增大字体大小 + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; font-weight: 500; }} QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{ @@ -275,28 +276,28 @@ class WeatherFloatingWidget(QDialog): background-color: {colors['surface']}; color: {colors['text']}; border: 1px solid {colors['border']}; - border-radius: 6px; // 适度增加圆角 - padding: 4px 7px; // 适度增加padding使布局更舒适 - font-size: 11px; // 适度增大字体大小 + border-radius: 6px; + padding: 4px 7px; + font-size: 11px; font-weight: 500; - min-height: 24px; // 适度增加最小高度使布局更舒适 + min-height: 24px; }} QComboBox#cityCombo:hover {{ border-color: {colors['accent']}; }} QComboBox#cityCombo::drop-down {{ border: none; - width: 14px; // 适度增加宽度 + width: 14px; }} QComboBox#cityCombo::down-arrow {{ image: none; border-left: 2px solid transparent; border-right: 2px solid transparent; - border-top: 5px solid {colors['text']}; // 适度增加箭头大小 + border-top: 5px solid {colors['text']}; }} """) else: - # 浅色主题样式 + # 浅色主题样式 - 与每日谏言悬浮窗口保持一致 self.main_frame.setStyleSheet(f""" QFrame#mainFrame {{ background-color: {colors['surface']}; @@ -357,9 +358,9 @@ class WeatherFloatingWidget(QDialog): background-color: {colors['accent']}; color: white; border: none; - border-radius: 6px; // 适度增加圆角 - padding: 5px 14px; // 适度增加padding使按钮更舒适 - font-size: 11px; // 适度增大字体大小 + border-radius: 6px; + padding: 6px 16px; + font-size: 11px; font-weight: 500; }} QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{ @@ -369,24 +370,24 @@ class WeatherFloatingWidget(QDialog): background-color: {colors['surface']}; color: {colors['text']}; border: 1px solid {colors['border']}; - border-radius: 6px; // 适度增加圆角 - padding: 4px 7px; // 适度增加padding使布局更舒适 - font-size: 11px; // 适度增大字体大小 + border-radius: 6px; + padding: 4px 7px; + font-size: 11px; font-weight: 500; - min-height: 24px; // 适度增加最小高度使布局更舒适 + min-height: 24px; }} QComboBox#cityCombo:hover {{ border-color: {colors['accent']}; }} QComboBox#cityCombo::drop-down {{ border: none; - width: 14px; // 适度增加宽度 + width: 14px; }} QComboBox#cityCombo::down-arrow {{ image: none; border-left: 2px solid transparent; border-right: 2px solid transparent; - border-top: 5px solid {colors['text']}; // 适度增加箭头大小 + border-top: 5px solid {colors['text']}; }} """) diff --git a/src/word_main_window.py b/src/word_main_window.py index 6a9abcc..7744c81 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -1655,7 +1655,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 == '自动定位': -- 2.34.1 From db65657f15bb60679263bc78fe1b975c025e95c2 Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 21:56:38 +0800 Subject: [PATCH 36/39] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B4=AA=E5=90=83?= =?UTF-8?q?=E8=9B=87=E6=B8=B8=E6=88=8F=E5=8F=8A=E5=85=B6=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/snake_game.py | 437 ++++++++++++++++++++++++++++++++++++++++ src/word_main_window.py | 26 +++ test_snake_game.py | 22 ++ test_speed_control.py | 71 +++++++ 4 files changed, 556 insertions(+) create mode 100644 src/ui/snake_game.py create mode 100644 test_snake_game.py create mode 100644 test_speed_control.py 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/word_main_window.py b/src/word_main_window.py index 7744c81..fab2880 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -813,6 +813,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)') @@ -2710,6 +2721,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): """显示关于对话框""" # 创建自定义对话框 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) -- 2.34.1 From adca960a879ec84e531c4ab473c92c9852979acc Mon Sep 17 00:00:00 2001 From: Maziang <929110464@qq.com> Date: Thu, 20 Nov 2025 22:40:50 +0800 Subject: [PATCH 37/39] =?UTF-8?q?=E6=B7=BB=E5=8A=A0DeepSeek=20AI=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E7=AA=97=E5=8F=A3=E5=8F=8AAPI=E5=AF=86=E9=92=A5?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + src/deepseek_dialog_window.py | 627 ++++++++++++++++++++++++++++++++++ src/word_main_window.py | 49 +++ 3 files changed, 680 insertions(+) create mode 100644 src/deepseek_dialog_window.py diff --git a/.gitignore b/.gitignore index f661aa2..2d96080 100644 --- a/.gitignore +++ b/.gitignore @@ -198,6 +198,10 @@ temp/ *.orig # Project specific +resources/config/deepseek_api.json +*.key +*.secret +config/*.json # Documentation folder doc/ 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/word_main_window.py b/src/word_main_window.py index fab2880..ff65c45 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -93,6 +93,9 @@ class WordStyleMainWindow(QMainWindow): self.learning_window = None # 学习模式窗口引用 self.sync_from_learning = False # 从学习模式同步内容的标记 + # DeepSeek对话窗口引用 + self.deepseek_dialog = None # DeepSeek对话窗口引用 + # 统一文档内容管理 self.unified_document_content = "" # 统一文档内容 self.last_edit_mode = "typing" # 上次编辑模式 @@ -804,6 +807,12 @@ class WordStyleMainWindow(QMainWindow): # 引用菜单 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)') @@ -2374,6 +2383,46 @@ class WordStyleMainWindow(QMainWindow): 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): """学习模式内容变化时的回调 - 只在末尾追加新内容""" # 设置同步标记,防止递归调用 -- 2.34.1 From d6d29cfe8bcc8f1048f6ed72965e161869c787a4 Mon Sep 17 00:00:00 2001 From: Lesacm <1500309685@qq.com> Date: Fri, 21 Nov 2025 21:18:29 +0800 Subject: [PATCH 38/39] UI fix114514 --- src/word_main_window.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/src/word_main_window.py b/src/word_main_window.py index ff65c45..28973f1 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -702,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) -- 2.34.1 From 2f8cbcc0038384cce608bb78f0354b12765585d8 Mon Sep 17 00:00:00 2001 From: Lesacm <1500309685@qq.com> Date: Fri, 21 Nov 2025 21:18:48 +0800 Subject: [PATCH 39/39] UI fix114514 --- ...求构思和描述文档-迭代一轮.docx | Bin 196903 -> 199047 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/软件需求构思和描述文档-迭代一轮.docx b/doc/软件需求构思和描述文档-迭代一轮.docx index c783b157e0ef31ad65194310ac820a930092b167..8cf1c39c4406c843173615d9ff732c63ffcaac5a 100644 GIT binary patch delta 23567 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%0DGUi)S@RIt+wJ3?te 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 VLn&Duh!6M-0{c&O4M^cY{{g5Qar^)P -- 2.34.1