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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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/28] =?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