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/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/resources/icons/app_icon.icns b/resources/icons/app_icon.icns new file mode 100644 index 0000000..06053a1 Binary files /dev/null and b/resources/icons/app_icon.icns differ diff --git a/run_debug.sh b/run_debug.sh new file mode 100644 index 0000000..b062afe --- /dev/null +++ b/run_debug.sh @@ -0,0 +1,7 @@ +#!/bin/bash +echo "设置Qt调试环境变量..." +export QT_DEBUG_PLUGINS=1 +echo "Qt调试模式已启用" +echo "" +echo "运行MagicWord应用程序..." +python src/main.py diff --git a/run_fixed.sh b/run_fixed.sh new file mode 100755 index 0000000..9f125f0 --- /dev/null +++ b/run_fixed.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# MagicWord 修复版启动脚本 + +echo "🚀 正在启动 MagicWord (修复版)..." + +# 使用新的虚拟环境 +source new_venv/bin/activate + +# 设置Qt环境变量 +export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/new_venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/new_venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms" +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" +export QT_LOGGING_RULES="qt.qpa.*=false" + +echo "✅ 环境设置完成" +echo "✅ 正在启动 MagicWord 应用..." + +# 启动应用 +cd src && python main.py \ No newline at end of file diff --git a/run_magicword.sh b/run_magicword.sh new file mode 100755 index 0000000..f40b70e --- /dev/null +++ b/run_magicword.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# MagicWord 一键启动脚本 +# 自动处理PyQt5平台插件问题 + +echo "🚀 正在启动 MagicWord..." + +# 设置PyQt5环境变量 +export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms" +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" +export QT_LOGGING_RULES="qt.qpa.*=false" + +# 激活虚拟环境(如果未激活) +if [ -z "$VIRTUAL_ENV" ]; then + echo "📦 激活虚拟环境..." + source .venv/bin/activate +fi + +# 检查PyQt5是否可用 +python -c "from PyQt5.QtWidgets import QApplication" 2>/dev/null +if [ $? -ne 0 ]; then + echo "❌ PyQt5出现问题,正在修复..." + ./emergency_fix.sh +fi + +# 启动应用 +echo "✅ 启动 MagicWord 应用..." +cd src && python main.py \ No newline at end of file diff --git a/set_pyqt5_env.sh b/set_pyqt5_env.sh new file mode 100644 index 0000000..e4ec5ce --- /dev/null +++ b/set_pyqt5_env.sh @@ -0,0 +1,7 @@ +# PyQt5环境变量设置 +export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins" +export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms" + +export QT_QPA_PLATFORM="cocoa" +export QT_MAC_WANTS_LAYER="1" +export QT_LOGGING_RULES="qt.qpa.*=false" \ No newline at end of file diff --git a/setup_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/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/main.py b/src/main.py index de007be..56b0016 100644 --- a/src/main.py +++ b/src/main.py @@ -8,59 +8,74 @@ import platform project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, project_root) -# 设置Qt平台插件路径 - 根据操作系统设置正确的Qt插件路径 +# 设置Qt平台插件路径 - 增强版本,完全避免平台插件问题 def set_qt_plugin_path(): + """设置Qt平台插件路径,确保所有平台插件都能正确加载""" system = platform.system() + # 获取Python版本 + python_version = f"python{sys.version_info.major}.{sys.version_info.minor}" + + # 可能的Qt插件路径列表 + possible_paths = [] + if system == "Windows": - # Windows环境下查找Qt插件路径 - # 首先检查虚拟环境中的Qt插件 - venv_qt_plugins_path = os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins') - if os.path.exists(venv_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path - return - - # 检查全局Python安装中的Qt插件 - global_qt_plugins_path = os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins') - if os.path.exists(global_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = global_qt_plugins_path - return - - # 尝试在常见的Windows PyQt5安装路径中查找 - common_paths = [ + # Windows环境下的路径 + possible_paths.extend([ + os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'Python', 'Python39', 'site-packages', 'PyQt5', 'Qt5', 'plugins'), - ] - - for path in common_paths: - if os.path.exists(path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = path - return - + ]) elif system == "Darwin": # macOS - # macOS环境下查找Qt插件路径 - system_qt_plugins_path = '/usr/local/opt/qt5/plugins' # macOS Homebrew Qt5路径 - venv_qt_plugins_path = os.path.join(project_root, '.venv', 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5', 'plugins') - - # 优先检查系统Qt插件路径 - if os.path.exists(system_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = system_qt_plugins_path - return - elif os.path.exists(venv_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path - return - + # macOS环境下的路径 + possible_paths.extend([ + os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/local/opt/qt5/plugins', # Homebrew Qt5 + '/opt/homebrew/opt/qt5/plugins', # Apple Silicon Homebrew + os.path.expanduser('~/Qt/5.15.2/clang_64/plugins'), # Qt官方安装 + ]) elif system == "Linux": - # Linux环境下查找Qt插件路径 - venv_qt_plugins_path = os.path.join(project_root, '.venv', 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5', 'plugins') - global_qt_plugins_path = os.path.join(sys.prefix, 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5', 'plugins') + # Linux环境下的路径 + possible_paths.extend([ + os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'), + '/usr/lib/x86_64-linux-gnu/qt5/plugins', + '/usr/lib/qt5/plugins', + ]) + + # 查找第一个存在的路径 + valid_path = None + for path in possible_paths: + if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')): + valid_path = path + break + + if valid_path: + # 设置Qt插件路径 + os.environ['QT_PLUGIN_PATH'] = valid_path + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(valid_path, 'platforms') + + # 设置平台特定的环境变量 + if system == "Darwin": # macOS + os.environ['QT_QPA_PLATFORM'] = 'cocoa' + os.environ['QT_MAC_WANTS_LAYER'] = '1' + # 禁用可能导致问题的Qt功能 + os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' # 禁用Qt警告日志 + elif system == "Windows": + os.environ['QT_QPA_PLATFORM'] = 'windows' + elif system == "Linux": + os.environ['QT_QPA_PLATFORM'] = 'xcb' + # 对于Linux,可能需要设置DISPLAY + if 'DISPLAY' not in os.environ: + os.environ['DISPLAY'] = ':0' - if os.path.exists(venv_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path - return - elif os.path.exists(global_qt_plugins_path): - os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = global_qt_plugins_path - return + print(f"✅ Qt插件路径设置成功: {valid_path}") + return True + else: + print("⚠️ 警告:未找到Qt插件路径") + return False # 设置Qt平台插件路径 set_qt_plugin_path() @@ -80,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 6b42bb1..988b05a 100644 --- a/src/ui/theme_manager.py +++ b/src/ui/theme_manager.py @@ -153,69 +153,100 @@ class ThemeManager(QObject): return self._get_light_stylesheet() def _get_dark_stylesheet(self): - """深色主题样式表""" + """深色主题样式表 - Apple设计风格""" return """ - /* 深色主题样式 */ + /* Apple设计风格深色主题样式 */ - /* 全局文字颜色 */ + /* 全局文字颜色和字体 - 使用Apple系统字体 */ QWidget { - color: #e0e0e0; + color: #f0f0f0; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 13px; } - /* 主窗口 */ + /* 主窗口 - Apple深色背景 */ QMainWindow { - background-color: #1e1e1e; + background-color: #2c2c2e; } - /* 菜单栏 */ + /* 菜单栏 - Apple深色风格 */ QMenuBar { - background-color: #0078d7; - border: 1px solid #005a9e; - font-size: 12px; - color: #ffffff; + background-color: #2c2c2e; + border: none; + border-bottom: 1px solid #404040; + font-size: 13px; + color: #f0f0f0; + padding: 4px 0; } QMenuBar::item { background-color: transparent; - padding: 4px 10px; - color: #e0e0e0; + padding: 6px 12px; + color: #f0f0f0; + border-radius: 4px; + margin: 0 1px; } QMenuBar::item:selected { - background-color: #106ebe; + background-color: #404040; + color: #f0f0f0; } - /* 菜单 */ + QMenuBar::item:pressed { + background-color: #505050; + color: #f0f0f0; + } + + /* 菜单 - Apple深色风格 */ QMenu { - background-color: #2d2d2d; - border: 1px solid #3c3c3c; - font-size: 12px; - color: #e0e0e0; + background-color: #2c2c2e; + border: 1px solid #404040; + border-radius: 8px; + font-size: 13px; + color: #f0f0f0; + padding: 4px 0; + margin: 2px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } QMenu::item { + color: #f0f0f0; + background-color: transparent; + border-radius: 4px; + margin: 0 4px; padding: 4px 20px; - color: #e0e0e0; } QMenu::item:selected { - background-color: #3c3c3c; + background-color: #0a84ff; + color: #ffffff; + } + + QMenu::item:pressed { + background-color: #0066cc; + color: #ffffff; + } + + QMenu::separator { + height: 1px; + background-color: #404040; + margin: 4px 8px; } /* 功能区 */ QFrame { - background-color: #2d2d2d; - border: 1px solid #3c3c3c; + background-color: #2c2c2e; + border: none; } /* 组框 */ QGroupBox { - font-size: 11px; + font-size: 12px; font-weight: normal; - color: #e0e0e0; - background-color: #2d2d2d; - border: 1px solid #3c3c3c; - border-radius: 0px; + color: #f0f0f0; + background-color: #2c2c2e; + border: none; + border-radius: 8px; margin-top: 5px; padding-top: 5px; } @@ -224,250 +255,320 @@ class ThemeManager(QObject): subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; - color: #e0e0e0; + color: #a0a0a0; } - /* 按钮 */ + /* 工具按钮 - Apple深色风格 */ QToolButton { - border: 1px solid #3c3c3c; - border-radius: 3px; - background-color: #3c3c3c; - font-size: 11px; - color: #e0e0e0; - padding: 3px 6px; + border: 1px solid transparent; + border-radius: 6px; + background-color: #3a3a3c; + font-size: 13px; + color: #f0f0f0; + padding: 6px 12px; } QToolButton:hover { - background-color: #4a4a4a; - border: 1px solid #5a5a5a; + background-color: #4a4a4c; + border: 1px solid #5a5a5c; } QToolButton:pressed { - background-color: #2a2a2a; - border: 1px solid #1a1a1a; + background-color: #5a5a5c; + border: 1px solid #6a6a6c; } QToolButton:checked { - background-color: #0078d4; - border: 1px solid #106ebe; + background-color: #0a84ff; + border: 1px solid #0a84ff; + color: #ffffff; } /* 切换按钮 */ QToolButton[checkable="true"] { - border: 1px solid #3c3c3c; - border-radius: 2px; - background-color: #3c3c3c; + border: 1px solid #4a4a4c; + border-radius: 6px; + background-color: #3a3a3c; font-size: 12px; - font-weight: bold; - color: #e0e0e0; + color: #f0f0f0; + padding: 6px 12px; } QToolButton[checkable="true"]:hover { - background-color: #4a4a4a; + background-color: #4a4a4c; } QToolButton[checkable="true"]:checked { - background-color: #0078d4; - border: 1px solid #106ebe; + background-color: #0a84ff; + border: 1px solid #0a84ff; + color: #ffffff; } - /* 下拉框 - 修复文字不可见问题 */ + /* 下拉框 - Apple深色风格 */ QComboBox { - background-color: #3c3c3c; - border: 1px solid #5a5a5a; - border-radius: 2px; - color: #e0e0e0; - padding: 2px 5px; - selection-background-color: #4a4a4a; - selection-color: #e0e0e0; + background-color: #3a3a3c; + border: 1px solid #4a4a4c; + border-radius: 6px; + color: #f0f0f0; + padding: 4px 8px; + selection-background-color: #0a84ff; + selection-color: #ffffff; } QComboBox:hover { - background-color: #4a4a4a; - border: 1px solid #6a6a6a; + background-color: #4a4a4c; + border: 1px solid #5a5a5c; } QComboBox::drop-down { border: none; - width: 15px; - background-color: #3c3c3c; + width: 20px; + border-left: 1px solid #4a4a4c; + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; } QComboBox::down-arrow { image: none; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-top: 5px solid #e0e0e0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 6px solid #a0a0a0; + margin: 6px; } - /* 下拉框弹出列表 */ QComboBox QAbstractItemView { - background-color: #3c3c3c; - border: 1px solid #5a5a5a; - color: #e0e0e0; - selection-background-color: #4a4a4a; - selection-color: #e0e0e0; + background-color: #2c2c2e; + border: 1px solid #4a4a4c; + color: #f0f0f0; + selection-background-color: #0a84ff; + selection-color: #ffffff; } - /* 字体下拉框特殊处理 */ - QFontComboBox { - background-color: #3c3c3c; - border: 1px solid #5a5a5a; - border-radius: 2px; - color: #e0e0e0; - padding: 2px 5px; - selection-background-color: #4a4a4a; - selection-color: #e0e0e0; - } - - QFontComboBox:hover { - background-color: #4a4a4a; - border: 1px solid #6a6a6a; - } - - QFontComboBox QAbstractItemView { - background-color: #3c3c3c; - border: 1px solid #5a5a5a; - color: #e0e0e0; - selection-background-color: #4a4a4a; - selection-color: #e0e0e0; - } - - /* 文本编辑器 */ + /* 文本编辑区域 - Apple深色风格 */ QTextEdit { - background-color: #1e1e1e; - border: 1px solid #3c3c3c; - color: #e0e0e0; - padding: 20px; + background-color: #1c1c1e; + border: none; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 15px; + color: #f0f0f0; + padding: 32px; line-height: 1.5; + selection-background-color: #0066cc; + selection-color: #ffffff; } - /* 状态栏 */ + /* 状态栏 - Apple深色风格 */ QStatusBar { - background-color: #2d2d2d; - border-top: 1px solid #3c3c3c; - font-size: 11px; - color: #e0e0e0; + background-color: #3a3a3c; + border-top: 1px solid #4a4a4c; + font-size: 12px; + color: #a0a0a0; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + padding: 6px 12px; } /* 标签 */ QLabel { - color: #e0e0e0; + color: #f0f0f0; background-color: transparent; } - /* 消息框 - 修复黑色背景问题 */ + /* 消息框 - Apple深色风格 */ QMessageBox { - background-color: #2d2d2d; - color: #e0e0e0; + background-color: #2c2c2e; + color: #f0f0f0; + border-radius: 12px; } QMessageBox QPushButton { - background-color: #3c3c3c; - color: #e0e0e0; - border: 1px solid #5a5a5a; - border-radius: 3px; - padding: 5px 15px; + background-color: #3a3a3c; + color: #f0f0f0; + border: 1px solid #4a4a4c; + border-radius: 6px; + padding: 6px 16px; min-width: 80px; } QMessageBox QPushButton:hover { - background-color: #4a4a4a; - border: 1px solid #6a6a6a; + background-color: #4a4a4c; + border: 1px solid #5a5a5c; } QMessageBox QPushButton:pressed { - background-color: #2a2a2a; - border: 1px solid #1a1a1a; + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + } + + QMessageBox QPushButton:default { + background-color: #0a84ff; + color: #ffffff; + border: 1px solid #0a84ff; + } + + QMessageBox QPushButton:default:hover { + background-color: #0066cc; + border: 1px solid #0066cc; + } + + QMessageBox QPushButton:default:pressed { + background-color: #004d99; + border: 1px solid #004d99; } - /* 滚动条 */ + /* 滚动条 - Apple深色风格 */ QScrollBar:vertical { - background-color: #2d2d2d; - width: 12px; + background-color: transparent; + width: 8px; border: none; } QScrollBar::handle:vertical { - background-color: #5a5a5a; - border-radius: 6px; + background-color: #5a5a5c; + border-radius: 4px; min-height: 20px; } QScrollBar::handle:vertical:hover { - background-color: #6a6a6a; + background-color: #6a6a6c; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { border: none; background: none; } + + /* 按钮 - Apple深色风格 */ + QPushButton { + background-color: #3a3a3c; + color: #f0f0f0; + border: 1px solid #4a4a4c; + border-radius: 6px; + padding: 6px 16px; + font-size: 13px; + } + + QPushButton:hover { + background-color: #4a4a4c; + border: 1px solid #5a5a5c; + } + + QPushButton:pressed { + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + } + + QPushButton:default { + background-color: #0a84ff; + color: #ffffff; + border: 1px solid #0a84ff; + } + + QPushButton:default:hover { + background-color: #0066cc; + border: 1px solid #0066cc; + } + + QPushButton:default:pressed { + background-color: #004d99; + border: 1px solid #004d99; + } """ def _get_light_stylesheet(self): - """浅色主题样式表 - 白底黑字""" + """浅色主题样式表 - Apple设计风格""" return """ - /* 浅色主题样式 - 白底黑字 */ + /* Apple设计风格浅色主题样式 */ - /* 全局文字颜色 */ + /* 全局文字颜色和字体 - 使用Apple系统字体 */ QWidget { color: #333333; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 13px; } - /* 主窗口 */ + /* 主窗口 - 纯净白色背景 */ QMainWindow { - background-color: #f3f2f1; + background-color: #ffffff; } - /* 菜单栏 */ + /* 菜单栏 - Apple风格 */ QMenuBar { - background-color: #0078d7; - border: 1px solid #005a9e; - font-size: 12px; - color: #ffffff; + background-color: #ffffff; + border: none; + border-bottom: 1px solid #e0e0e0; + font-size: 13px; + color: #333333; + padding: 4px 0; } QMenuBar::item { background-color: transparent; - padding: 4px 10px; + padding: 6px 12px; color: #333333; + border-radius: 4px; + margin: 0 1px; } QMenuBar::item:selected { - background-color: #106ebe; + background-color: #f0f0f0; + color: #333333; } - /* 菜单 */ + QMenuBar::item:pressed { + background-color: #e0e0e0; + color: #333333; + } + + /* 菜单 - Apple风格 */ QMenu { background-color: #ffffff; border: 1px solid #d0d0d0; - font-size: 12px; + border-radius: 8px; + font-size: 13px; color: #333333; + padding: 4px 0; + margin: 2px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } QMenu::item { - padding: 4px 20px; color: #333333; + background-color: transparent; + border-radius: 4px; + margin: 0 4px; + padding: 4px 20px; } QMenu::item:selected { - background-color: #f0f0f0; + background-color: #007aff; + color: #ffffff; + } + + QMenu::item:pressed { + background-color: #0062cc; + color: #ffffff; + } + + QMenu::separator { + height: 1px; + background-color: #e0e0e0; + margin: 4px 8px; } /* 功能区 */ QFrame { background-color: #ffffff; - border: 1px solid #d0d0d0; + border: none; } /* 组框 */ QGroupBox { - font-size: 11px; + font-size: 12px; font-weight: normal; color: #333333; background-color: #ffffff; - border: 1px solid #d0d0d0; - border-radius: 0px; + border: none; + border-radius: 8px; margin-top: 5px; padding-top: 5px; } @@ -476,145 +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; + selection-background-color: #007aff; + selection-color: #ffffff; } - QFontComboBox::down-arrow { - image: none; - border-left: 3px solid transparent; - border-right: 3px solid transparent; - border-top: 5px solid #333333; - } - - QFontComboBox QAbstractItemView { - background-color: #ffffff; - border: 1px solid #d0d0d0; - color: #333333; - selection-background-color: #f0f0f0; - selection-color: #333333; - } - - /* 文本编辑器 */ + /* 文本编辑区域 - Apple风格 */ QTextEdit { background-color: #ffffff; - border: 1px solid #d0d0d0; - color: #000000; - padding: 20px; + border: none; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + font-size: 15px; + color: #333333; + padding: 32px; line-height: 1.5; + selection-background-color: #b3d9ff; + selection-color: #333333; } - /* 状态栏 */ + /* 状态栏 - Apple风格 */ QStatusBar { - background-color: #ffffff; - border-top: 1px solid #d0d0d0; - font-size: 11px; - color: #333333; + background-color: #f6f6f6; + border-top: 1px solid #e0e0e0; + font-size: 12px; + color: #666666; + font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif; + padding: 6px 12px; } /* 标签 */ @@ -623,41 +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; } @@ -669,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): diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py index 5150f9f..a29a043 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; } """) @@ -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,42 @@ 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 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): """初始化主题""" # 连接主题切换信号 @@ -230,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) @@ -464,17 +562,125 @@ class WordRibbon(QFrame): """主题切换槽函数""" self.apply_theme() + def get_weather_emoji(self, weather_desc): + """根据天气描述返回对应的emoji图标""" + weather_emoji_map = { + '晴': '🌞', + '多云': '☁️', + '阴': '☁️', + '小雨': '🌦️', + '中雨': '🌧️', + '大雨': '⛈️', + '暴雨': '⛈️', + '雷阵雨': '⛈️', + '雪': '❄️', + '小雪': '🌨️', + '中雪': '❄️', + '大雪': '❄️', + '暴雪': '❄️', + '雾': '🌫️', + '霾': '🌫️', + '沙尘暴': '🌪️', + '扬沙': '🌪️', + '浮尘': '🌪️', + '台风': '🌀', + '飓风': '🌀', + '龙卷风': '🌪️', + '冰雹': '🧊', + '冻雨': '🌨️', + '雨夹雪': '🌨️', + 'sunny': '🌞', + 'clear': '🌞', + 'cloudy': '☁️', + 'overcast': '☁️', + 'rain': '🌧️', + 'light rain': '🌦️', + 'heavy rain': '⛈️', + 'thunderstorm': '⛈️', + 'snow': '❄️', + 'fog': '🌫️', + 'haze': '🌫️', + 'sandstorm': '🌪️', + 'typhoon': '🌀', + 'hurricane': '🌀', + 'tornado': '🌪️', + 'hail': '🧊' + } + + # 模糊匹配天气描述 + weather_desc_lower = str(weather_desc).lower() + for key, emoji in weather_emoji_map.items(): + if key in weather_desc_lower: + return emoji + + # 默认返回晴天图标 + return '🌞' + def create_weather_group(self): """创建天气组件组""" if self.weather_group is not None: return self.weather_group - weather_group = self.create_ribbon_group("天气") + weather_group = self.create_ribbon_group("天气", is_special_group=True) + weather_group.setFixedWidth(200) # 增加整体宽度 weather_layout = QVBoxLayout() + weather_layout.setSpacing(8) # 增加行间距 + + # 第一行:天气图标和温度显示(居中对齐) + weather_display_layout = QHBoxLayout() + weather_display_layout.setSpacing(10) # 增加图标和温度间距 + + # 添加左侧弹性空间,推动内容到中心 + weather_display_layout.addStretch() + + # 天气图标标签 - 优化垂直居中对齐 + self.weather_icon_label = QLabel("🌞") + self.weather_icon_label.setAlignment(Qt.AlignCenter) # 使用Qt对齐方式 + self.weather_icon_label.setStyleSheet(""" + QLabel { + font-size: 32px; + padding: 0px; + margin: 0px; + border: none; + background: transparent; + } + """) + self.weather_icon_label.setFixedSize(40, 40) # 增大图标尺寸 + + # 温度标签 - 优化垂直居中对齐 + self.weather_temp_label = QLabel("--°C") + self.weather_temp_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) # 使用Qt对齐方式 + # 初始化时使用默认颜色,主题切换时会更新 + self.weather_temp_label.setStyleSheet(""" + QLabel { + font-size: 16px; + font-weight: bold; + padding: 0px; + margin: 0px; + border: none; + background: transparent; + min-height: 30px; /* 确保最小高度 */ + } + """) + self.weather_temp_label.setFixedSize(70, 30) # 增加温度标签宽度 + + weather_display_layout.addWidget(self.weather_icon_label) + weather_display_layout.addWidget(self.weather_temp_label) + + # 添加右侧弹性空间,确保内容居中 + weather_display_layout.addStretch() + + # 第二行:城市选择和刷新按钮(居中对齐) + control_layout = QHBoxLayout() + control_layout.setSpacing(8) # 增加控件间距 + + # 添加左侧弹性空间,推动内容到中心 + control_layout.addStretch() # 城市选择 - 添加所有省会城市 self.city_combo = QComboBox() - self.city_combo.setFixedWidth(120) # 增加宽度以显示完整城市名 + self.city_combo.setFixedWidth(120) # 增加城市选择框宽度 + self.city_combo.setStyleSheet("QComboBox { font-size: 12px; padding: 3px; }") # 增大字体和间距 self.city_combo.addItems([ '自动定位', '北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', # 一线城市 @@ -487,12 +693,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) - weather_layout.addWidget(self.city_combo) - weather_layout.addWidget(self.refresh_weather_btn) + # 添加右侧弹性空间,确保内容居中 + control_layout.addStretch() + + weather_layout.addLayout(weather_display_layout) + weather_layout.addLayout(control_layout) weather_group.setLayout(weather_layout) self.weather_group = weather_group @@ -521,7 +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() # 创建第一行:类型选择下拉框和刷新按钮 @@ -584,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 @@ -685,98 +948,268 @@ class WordRibbon(QFrame): btn.setText(text) btn.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) btn.setFixedSize(60, 60) - btn.setStyleSheet(""" - QToolButton { - border: 1px solid transparent; - border-radius: 3px; - background-color: transparent; - font-size: 11px; - color: #333333; - } - QToolButton:hover { - background-color: #f0f0f0; - border: 1px solid #d0d0d0; - } - QToolButton:pressed { - background-color: #e1e1e1; - border: 1px solid #c0c0c0; - } - """) + + # 连接主题切换信号以动态更新样式 + theme_manager.theme_changed.connect(lambda: self._update_button_style(btn)) + + # 立即应用当前主题样式 + self._update_button_style(btn) + return btn + def _update_button_style(self, btn): + """更新按钮样式以适配当前主题""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + btn.setStyleSheet(f""" + QToolButton {{ + border: 1px solid transparent; + border-radius: 3px; + background-color: transparent; + font-size: 11px; + color: {colors['text']}; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['border']}; + }} + QToolButton:pressed {{ + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + }} + """) + else: + btn.setStyleSheet(f""" + QToolButton {{ + border: 1px solid transparent; + border-radius: 3px; + background-color: transparent; + font-size: 11px; + color: {colors['text']}; + }} + QToolButton:hover {{ + background-color: #f0f0f0; + border: 1px solid #d0d0d0; + }} + QToolButton:pressed {{ + background-color: #e1e1e1; + border: 1px solid #c0c0c0; + }} + """) + def create_toggle_button(self, text, icon_name): - """创建切换按钮""" + """创建切换按钮 - 现代极简主义风格""" btn = QToolButton() btn.setText(text) btn.setCheckable(True) btn.setToolButtonStyle(Qt.ToolButtonTextOnly) # 根据文本长度设置宽度,中文字符需要更宽 if len(text) <= 1: - btn.setFixedSize(30, 25) # 单个字符(如B、I、U) + btn.setFixedSize(32, 28) # 单个字符(如B、I、U) elif len(text) <= 2: - btn.setFixedSize(45, 25) # 两个字符(如"居中") + btn.setFixedSize(48, 28) # 两个字符(如"居中") else: - btn.setFixedSize(60, 25) # 三个字符及以上(如"左对齐"、"两端对齐") - btn.setStyleSheet(""" - QToolButton { - border: 1px solid #d0d0d0; - border-radius: 2px; - background-color: transparent; - font-size: 12px; - font-weight: bold; - color: #333333; - } - QToolButton:hover { - background-color: #f0f0f0; - } - QToolButton:checked { - background-color: #e1e1e1; - border: 1px solid #c0c0c0; - } - """) + btn.setFixedSize(64, 28) # 三个字符及以上(如"左对齐"、"两端对齐") + + # 连接主题切换信号以动态更新样式 + theme_manager.theme_changed.connect(lambda: self._update_toggle_button_style(btn)) + + # 立即应用当前主题样式 + self._update_toggle_button_style(btn) + return btn + def _update_toggle_button_style(self, btn): + """更新切换按钮样式以适配当前主题""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + btn.setStyleSheet(f""" + QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: {colors['text']}; + font-size: 13px; + font-weight: 500; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['border']}; + }} + QToolButton:pressed {{ + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + }} + QToolButton:checked {{ + background-color: {colors['accent']}; + border: 1px solid {colors['accent']}; + color: {colors['surface']}; + }} + """) + else: + btn.setStyleSheet(f""" + QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: #4a5568; + font-size: 13px; + font-weight: 500; + }} + QToolButton:hover {{ + background-color: #f7fafc; + border: 1px solid #e2e8f0; + }} + QToolButton:pressed {{ + background-color: #edf2f7; + border: 1px solid #cbd5e0; + }} + QToolButton:checked {{ + background-color: #ebf8ff; + border: 1px solid #bee3f8; + color: #3182ce; + }} + """) + def create_color_button(self, text, icon_name): - """创建颜色选择按钮""" + """创建颜色选择按钮 - 现代极简主义风格""" btn = QToolButton() btn.setText(text) btn.setToolButtonStyle(Qt.ToolButtonTextOnly) - btn.setFixedSize(30, 25) - btn.setStyleSheet(""" - QToolButton { - border: 1px solid #d0d0d0; - border-radius: 2px; - background-color: transparent; - font-size: 12px; - font-weight: bold; - color: #333333; - } - QToolButton:hover { - background-color: #f0f0f0; - } - QToolButton:pressed { - background-color: #e1e1e1; - border: 1px solid #c0c0c0; - } - """) + btn.setFixedSize(32, 28) + + # 连接主题切换信号以动态更新样式 + theme_manager.theme_changed.connect(lambda: self._update_color_button_style(btn)) + + # 立即应用当前主题样式 + self._update_color_button_style(btn) + + return btn + + def _update_color_button_style(self, btn): + """更新颜色按钮样式以适配当前主题""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + btn.setStyleSheet(f""" + QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: {colors['text']}; + font-size: 13px; + font-weight: 500; + }} + QToolButton:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['border']}; + }} + QToolButton:pressed {{ + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + }} + """) + else: + btn.setStyleSheet(f""" + QToolButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: #4a5568; + font-size: 13px; + font-weight: 500; + }} + QToolButton:hover {{ + background-color: #f7fafc; + border: 1px solid #e2e8f0; + }} + QToolButton:pressed {{ + background-color: #edf2f7; + border: 1px solid #cbd5e0; + }} + """) + + def create_style_button(self, text): + """创建样式按钮 - 现代极简主义风格""" + btn = QPushButton(text) + btn.setFixedSize(60, 28) + + # 连接主题切换信号以动态更新样式 + theme_manager.theme_changed.connect(lambda: self._update_style_button_style(btn)) + + # 立即应用当前主题样式 + self._update_style_button_style(btn) + return btn + + def _update_style_button_style(self, btn): + """更新样式按钮样式以适配当前主题""" + is_dark = theme_manager.is_dark_theme() + colors = theme_manager.get_current_theme_colors() + + if is_dark: + btn.setStyleSheet(f""" + QPushButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: {colors['text']}; + font-size: 12px; + font-weight: 500; + text-align: center; + }} + QPushButton:hover {{ + background-color: {colors['surface_hover']}; + border: 1px solid {colors['border']}; + }} + QPushButton:pressed {{ + background-color: #5a5a5c; + border: 1px solid #6a6a6c; + }} + """) + else: + btn.setStyleSheet(f""" + QPushButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 8px; + padding: 6px 10px; + color: #4a5568; + font-size: 12px; + font-weight: 500; + text-align: center; + }} + QPushButton:hover {{ + background-color: #f7fafc; + border: 1px solid #e2e8f0; + }} + QPushButton:pressed {{ + background-color: #edf2f7; + border: 1px solid #cbd5e0; + }} + """) class WordStatusBar(QStatusBar): def __init__(self, parent=None): super().__init__(parent) self.setup_ui() + # 连接主题切换信号 + theme_manager.theme_changed.connect(self.apply_theme) + # 应用初始主题 + self.apply_theme() def setup_ui(self): - """设置状态栏""" - self.setStyleSheet(""" - QStatusBar { - background-color: #f3f2f1; - border-top: 1px solid #d0d0d0; - font-size: 11px; - color: #333333; - } - """) - + """设置状态栏 - 现代极简主义风格""" # 添加状态栏项目 self.page_label = QLabel("第 1 页,共 1 页") self.words_label = QLabel("字数: 0") @@ -787,12 +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): """设置输入处理器""" @@ -872,19 +1354,7 @@ class WordTextEdit(QTextEdit): return super().inputMethodQuery(query) def setup_ui(self): - """设置文本编辑区域样式""" - self.setStyleSheet(""" - QTextEdit { - background-color: #ffffff; - border: 1px solid #d0d0d0; - font-family: 'Calibri', 'Microsoft YaHei', '微软雅黑', sans-serif; - font-size: 12pt; - color: #000000; - padding: 20px; - line-height: 1.5; - } - """) - + """设置文本编辑区域样式 - 现代极简主义风格""" # 设置页面边距和背景 self.setViewportMargins(50, 50, 50, 50) @@ -898,6 +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): @@ -1016,14 +1526,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 +1560,35 @@ class WeatherAPI: while len(life_tips) < 3 and default_tips: life_tips.append(default_tips.pop(0)) + # 安全获取天气数据 + wendu = current_data.get('wendu', 'N/A') + shidu = current_data.get('shidu', 'N/A') + first_forecast = forecast[0] if forecast else {} + weather_info = { - 'temp': current_data['wendu'], - 'feels_like': current_data['wendu'], # 没有体感温度,用实际温度代替 - 'weather': current_data['forecast'][0]['type'], - 'humidity': current_data['shidu'].replace('%', ''), - 'wind_dir': current_data['forecast'][0]['fx'], + 'temp': wendu, + 'feels_like': wendu, # 没有体感温度,用实际温度代替 + 'weather': first_forecast.get('type', '晴'), # 默认晴天 + 'humidity': shidu.replace('%', '') if shidu != 'N/A' else '50', + 'wind_dir': first_forecast.get('fx', '无风'), 'wind_scale': '1', # 没有风力等级,用默认值 - 'vis': current_data['forecast'][0]['high'], # 用最高温作为可见度 + 'vis': first_forecast.get('high', '高温 15℃'), # 用最高温作为可见度 'pressure': '1013', # 没有气压,用默认值 'life_tips': life_tips # 添加生活提示信息 } print(f"解析后的天气信息: {weather_info}") return weather_info - print(f"获取天气失败,状态码: {data.get('status')}") + else: + print(f"获取天气失败,状态码: {data.get('status')}") + return None + except requests.exceptions.Timeout: + print("获取天气超时") + return None + except requests.exceptions.RequestException as e: + print(f"网络请求失败: {e}") + return None + except json.JSONDecodeError as e: + print(f"JSON解析失败: {e}") return None except Exception as e: print(f"获取当前天气失败: {e}") @@ -1123,27 +1651,150 @@ class WeatherAPI: def get_isp_info(self): """获取ISP信息""" try: - url = "http://ip-api.com/json/" - response = requests.get(url, timeout=5) - response.raise_for_status() + # 尝试多个ISP信息服务以提高成功率 + + # 方法1: 使用ip-api.com接口 + try: + url = "http://ip-api.com/json/" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + + data = response.json() + if data.get('status') == 'success': + isp = data.get('isp', '') + org = data.get('org', '') + as_info = data.get('as', '') + country = data.get('country', '') + return f"{isp} {org} {as_info} {country}".strip() + except Exception as e: + print(f"ip-api ISP信息获取失败: {e}") + pass + + # 方法2: 使用ipinfo.io接口 + try: + url = "https://ipinfo.io/json" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + + data = response.json() + if 'org' in data: + org = data.get('org', '') + country = data.get('country', '') + return f"{org} {country}".strip() + except Exception as e: + print(f"ipinfo ISP信息获取失败: {e}") + pass + + # 方法3: 使用httpbin.org获取基础信息 + try: + url = "https://httpbin.org/ip" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + # 这个接口主要用于获取IP,不是ISP信息,但可以作为备选 + data = response.json() + origin = data.get('origin', '') + if origin: + return f"IP: {origin}" + except Exception as e: + print(f"httpbin ISP信息获取失败: {e}") + pass - data = response.json() - if data.get('status') == 'success': - isp = data.get('isp', '') - org = data.get('org', '') - as_info = data.get('as', '') - return f"{isp} {org} {as_info}".strip() return None except Exception as e: - print(f"获取ISP信息失败: {e}") + print(f"获取ISP信息总体失败: {e}") return None def get_location_by_ip(self): """通过IP地址获取用户位置""" try: + # 首先获取公网IP地址 + ip_address = None + try: + # 使用多个IP获取服务确保能获取到公网IP + ip_services = [ + "https://api.ipify.org", + "https://icanhazip.com", + "https://ident.me", + "https://ipecho.net/plain", + "https://myexternalip.com/raw" + ] + + for service in ip_services: + try: + response = requests.get(service, timeout=3) + if response.status_code == 200: + ip_address = response.text.strip() + # 验证是否为有效的IPv4地址 + import re + if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', ip_address): + print(f"获取到公网IP: {ip_address}") + break + else: + ip_address = None + except: + continue + + if not ip_address: + print("无法获取公网IP地址") + except Exception as e: + print(f"获取IP地址失败: {e}") + # 尝试多个免费的IP地理位置API - # API 1: 搜狐IP接口(HTTP,无SSL问题) + # API 1: 使用ip-api.com接口(更稳定的免费服务,支持HTTPS) + try: + if ip_address: + url = f"https://ip-api.com/json/{ip_address}" + else: + url = "https://ip-api.com/json/" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + + data = response.json() + if data.get('status') == 'success': + city = data.get('city', '') + region = data.get('regionName', '') + country = data.get('country', '') + if city and city not in ['null', 'None', '']: + print(f"ip-api定位成功: {city}, {region}, {country}") + # 如果城市信息不完整,尝试用地区信息补充 + if len(city) < 2 and region: + city = region + return city + except Exception as e: + print(f"ip-api接口失败: {e}") + pass + + # API 2: 使用ipinfo.io接口(需要处理免费版限制) + try: + if ip_address: + url = f"https://ipinfo.io/{ip_address}/json" + else: + url = "https://ipinfo.io/json" + headers = {'User-Agent': 'MagicWord/1.0'} + response = requests.get(url, timeout=5, headers=headers) + response.raise_for_status() + + data = response.json() + if 'city' in data: + city = data.get('city', '') + region = data.get('region', '') + country = data.get('country', '') + if city and city not in ['null', 'None', '']: + print(f"ipinfo定位成功: {city}, {region}, {country}") + # 如果城市信息不完整,尝试用地区信息补充 + if len(city) < 2 and region: + city = region + return city + except Exception as e: + print(f"ipinfo接口失败: {e}") + pass + + # API 3: 搜狐IP接口(HTTP,无SSL问题) try: url = "http://pv.sohu.com/cityjson?ie=utf-8" response = requests.get(url, timeout=5) @@ -1168,7 +1819,7 @@ class WeatherAPI: print(f"搜狐IP接口失败: {e}") pass - # API 2: 使用pconline接口(HTTP) + # API 4: 使用pconline接口(HTTP) try: url = "http://whois.pconline.com.cn/ipJson.jsp" response = requests.get(url, timeout=5) @@ -1191,26 +1842,14 @@ class WeatherAPI: print(f"pconline接口失败: {e}") pass - # API 3: 使用ip-api.com接口(更稳定的免费服务) - try: - url = "http://ip-api.com/json/" - response = requests.get(url, timeout=5) - response.raise_for_status() - - data = response.json() - if data.get('status') == 'success': - city = data.get('city', '') - if city: - print(f"ip-api定位成功: {city}") - return city - except Exception as e: - print(f"ip-api接口失败: {e}") - pass - - # API 4: 使用淘宝IP接口 + # API 5: 使用淘宝IP接口 try: - url = "http://ip.taobao.com/outGetIpInfo" - params = {'ip': '', 'accessKey': 'alibaba-inc'} + if ip_address: + url = "http://ip.taobao.com/outGetIpInfo" + params = {'ip': ip_address, 'accessKey': 'alibaba-inc'} + else: + url = "http://ip.taobao.com/outGetIpInfo" + params = {'ip': '', 'accessKey': 'alibaba-inc'} response = requests.get(url, params=params, timeout=5) response.raise_for_status() @@ -1229,25 +1868,42 @@ class WeatherAPI: return None except Exception as e: print(f"IP定位总体失败: {e}") - return None + # 返回默认城市而不是None,确保天气功能仍然可用 + return "北京" def get_current_location(self): """获取当前位置信息""" try: # 首先尝试通过IP获取位置 - city = self.get_location_by_ip() - if city: - print(f"通过IP定位成功: {city}") + location_result = self.get_location_by_ip() + + # 检查是否是默认城市(表示IP定位失败) + if location_result == "北京": + print("IP定位失败,使用默认城市") + print("自动定位失败,建议手动选择城市") + return None + + if location_result: + print(f"通过IP定位成功: {location_result}") # 检查是否是教育网或特殊网络环境 isp_info = self.get_isp_info() - if isp_info and ('教育网' in isp_info or 'CERNET' in isp_info or 'University' in isp_info): + if isp_info and ('教育网' in isp_info or 'CERNET' in isp_info or 'University' in isp_info or '大学' in isp_info): print(f"检测到教育网环境: {isp_info}") print("教育网IP定位可能不准确,建议手动选择城市") # 教育网环境下,如果定位到北京,可能是IP分配问题 - if city.lower() in ['beijing', '北京', 'haidian', '海淀']: + if isinstance(location_result, str) and location_result.lower() in ['beijing', '北京', 'haidian', '海淀']: print("提示:教育网环境下北京定位可能是网络出口导致的") - return {'city': city, 'note': '教育网环境,定位可能不准确', 'isp': isp_info} + return {'city': location_result, 'note': '教育网环境,定位可能不准确', 'isp': isp_info} + + # 处理返回结果格式 + city = None + if isinstance(location_result, dict) and 'city' in location_result: + city = location_result['city'] + elif isinstance(location_result, str): + city = location_result + else: + city = str(location_result) # 智能处理 - 如果是区级单位,映射到市级城市 district_to_city_map = { @@ -1395,6 +2051,7 @@ class WeatherAPI: except Exception as e: print(f"获取当前位置失败: {e}") + # 即使出现异常也返回None而不是抛出异常,确保程序继续运行 return None def get_city_weather_by_name(self, city_name): diff --git a/src/word_main_window.py b/src/word_main_window.py index 9fcdd7c..0b21977 100644 --- a/src/word_main_window.py +++ b/src/word_main_window.py @@ -1,13 +1,14 @@ # word_main_window.py import sys import os +import platform from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel, QSplitter, QFrame, QMenuBar, QAction, QFileDialog, QMessageBox, QApplication, QDialog, QLineEdit, QCheckBox, QPushButton, QListWidget, QListWidgetItem, QScrollArea) 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, ) @@ -135,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() @@ -637,6 +637,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) @@ -674,12 +676,40 @@ 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)') # 设计菜单 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)') @@ -809,6 +839,9 @@ class WordStyleMainWindow(QMainWindow): # 文本变化信号 self.text_edit.textChanged.connect(self.on_text_changed) + # 光标位置变化信号,用于更新按钮状态 + self.text_edit.cursorPositionChanged.connect(self.update_format_buttons) + # Ribbon按钮信号 # 标签栏已删除,相关代码已移除 @@ -821,6 +854,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) @@ -829,6 +874,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) @@ -1291,10 +1346,144 @@ class WordStyleMainWindow(QMainWindow): if cursor.hasSelection(): self.status_bar.showMessage("字体颜色已设置,新输入的文本将使用该颜色", 2000) + def on_heading1_clicked(self): + """一级标题按钮点击处理""" + self.apply_heading_style(1) + + def on_heading2_clicked(self): + """二级标题按钮点击处理""" + self.apply_heading_style(2) + + def on_heading3_clicked(self): + """三级标题按钮点击处理""" + self.apply_heading_style(3) + + def on_heading4_clicked(self): + """四级标题按钮点击处理""" + self.apply_heading_style(4) + + def on_body_text_clicked(self): + """正文按钮点击处理""" + self.apply_body_text_style() + + def on_align_left_clicked(self): + """左对齐按钮点击处理""" + self.apply_alignment(Qt.AlignLeft) + + def on_align_center_clicked(self): + """居中对齐按钮点击处理""" + self.apply_alignment(Qt.AlignCenter) + + def on_align_right_clicked(self): + """右对齐按钮点击处理""" + self.apply_alignment(Qt.AlignRight) + + def on_align_justify_clicked(self): + """两端对齐按钮点击处理""" + self.apply_alignment(Qt.AlignJustify) + + def apply_heading_style(self, level): + """应用标题样式""" + cursor = self.text_edit.textCursor() + + # 创建字符格式 + char_format = QTextCharFormat() + + # 创建块格式(段落格式) + block_format = QTextBlockFormat() + block_format.setTopMargin(12) + block_format.setBottomMargin(6) + + # 根据标题级别设置样式 + if level == 1: + # 一级标题:24pt, 加粗 + char_format.setFontPointSize(24) + char_format.setFontWeight(QFont.Bold) + elif level == 2: + # 二级标题:18pt, 加粗 + char_format.setFontPointSize(18) + char_format.setFontWeight(QFont.Bold) + elif level == 3: + # 三级标题:16pt, 加粗 + char_format.setFontPointSize(16) + char_format.setFontWeight(QFont.Bold) + elif level == 4: + # 四级标题:14pt, 加粗 + char_format.setFontPointSize(14) + char_format.setFontWeight(QFont.Bold) + + # 应用格式 + if cursor.hasSelection(): + # 如果有选中文本,只更改选中文本的格式 + cursor.mergeCharFormat(char_format) + else: + # 如果没有选中文本,更改当前段落的格式 + cursor.setBlockFormat(block_format) + cursor.mergeCharFormat(char_format) + # 将光标移动到段落末尾并添加换行 + cursor.movePosition(QTextCursor.EndOfBlock) + cursor.insertText("\n") + + # 设置文本编辑器的默认格式 + self.text_edit.setCurrentCharFormat(char_format) + self.text_edit.textCursor().setBlockFormat(block_format) + + def apply_body_text_style(self): + """应用正文样式""" + cursor = self.text_edit.textCursor() + + # 创建字符格式 + char_format = QTextCharFormat() + char_format.setFontPointSize(12) # 正文字号 + char_format.setFontWeight(QFont.Normal) # 正常粗细 + + # 创建块格式(段落格式) + block_format = QTextBlockFormat() + block_format.setTopMargin(0) + block_format.setBottomMargin(6) + + # 应用格式 + if cursor.hasSelection(): + # 如果有选中文本,只更改选中文本的格式 + cursor.mergeCharFormat(char_format) + else: + # 如果没有选中文本,更改当前段落的格式 + cursor.setBlockFormat(block_format) + cursor.mergeCharFormat(char_format) + + # 设置文本编辑器的默认格式 + self.text_edit.setCurrentCharFormat(char_format) + self.text_edit.textCursor().setBlockFormat(block_format) + + def apply_alignment(self, alignment): + """应用段落对齐方式""" + cursor = self.text_edit.textCursor() + + # 创建块格式(段落格式) + block_format = QTextBlockFormat() + block_format.setAlignment(alignment) + + # 应用格式 + if cursor.hasSelection(): + # 如果有选中文本,更改选中文本所在段落的对齐方式 + cursor.mergeBlockFormat(block_format) + else: + # 如果没有选中文本,更改当前段落的对齐方式 + cursor.setBlockFormat(block_format) + + # 更新文本编辑器的默认段落格式 + self.text_edit.textCursor().setBlockFormat(block_format) + def update_weather_display(self, weather_data): """更新天气显示""" if 'error' in weather_data: self.status_bar.showMessage(f"天气数据获取失败: {weather_data['error']}", 3000) + # 更新工具栏天气显示为错误状态 + if hasattr(self, 'ribbon'): + if hasattr(self.ribbon, 'weather_icon_label'): + self.ribbon.weather_icon_label.setText("❓") + if hasattr(self.ribbon, 'weather_temp_label'): + self.ribbon.weather_temp_label.setText("--°C") else: # 处理嵌套的天气数据结构 city = weather_data.get('city', '未知城市') @@ -1318,6 +1507,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', [])}") @@ -1581,7 +1796,7 @@ class WordStyleMainWindow(QMainWindow): """导入文件 - 仅在导入时存储内容,不立即显示""" file_path, _ = QFileDialog.getOpenFileName( self, "导入文件", "", - "文档文件 (*.docx *.txt *.pdf);;所有文件 (*.*)" + "文档文件 (*.docx *.txt *.pdf *.html);;所有文件 (*.*)" ) if file_path: @@ -1646,7 +1861,7 @@ class WordStyleMainWindow(QMainWindow): # 提取并显示图片(如果有) if images: - self.extract_and_display_images(content, images) + self.extract_and_display_images(file_path=None, images=images) else: # 转换失败,显示错误信息 @@ -2697,6 +2912,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): """关闭事件处理""" # 清理临时文件 @@ -2731,11 +3019,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'
{img_name}
\n' + html_body += f'[图片: {img_name}]
\n' + else: + # 普通文本,使用段落标签 + if line.strip(): + html_body += f'{line}
\n' + else: + html_body += '{img_name}
' + html_content += f'{line}
' + else: + html_content += '