diff --git a/.gitignore b/.gitignore
index 4ec21cb..2d96080 100644
--- a/.gitignore
+++ b/.gitignore
@@ -198,6 +198,13 @@ temp/
*.orig
# Project specific
+resources/config/deepseek_api.json
+*.key
+*.secret
+config/*.json
+
+# Documentation folder
+doc/
dist_package/
dist_package_v0.3/
*.zip
@@ -231,6 +238,7 @@ venv/
env/
.venv/
.env/
+new_venv/
# IDE
.idea/
diff --git a/PYQT5_FIX_GUIDE.md b/PYQT5_FIX_GUIDE.md
new file mode 100644
index 0000000..2245480
--- /dev/null
+++ b/PYQT5_FIX_GUIDE.md
@@ -0,0 +1,120 @@
+# PyQt5 平台插件问题完全解决方案
+
+## 问题描述
+在使用PyQt5时,可能会遇到以下错误:
+```
+qt.qpa.plugin: Could not find the Qt platform plugin "cocoa" in ""
+This application failed to start because no Qt platform plugin could be initialized.
+```
+
+## 解决方案
+
+### 方法一:一键修复(推荐)
+运行完整的修复脚本:
+```bash
+python fix_pyqt5_complete.py
+```
+
+### 方法二:手动修复
+1. **清理现有安装**
+```bash
+pip uninstall PyQt5 PyQt5-Qt5 PyQt5-sip -y
+rm -rf /Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5*
+```
+
+2. **重新安装**
+```bash
+pip install PyQt5==5.15.10 --force-reinstall --no-cache-dir
+```
+
+3. **设置环境变量**
+```bash
+source set_pyqt5_env.sh
+```
+
+### 方法三:安全安装
+使用安全安装脚本:
+```bash
+python install_pyqt5_safe.py
+```
+
+## 预防措施
+
+### 1. 在main.py中集成环境设置
+确保你的 `main.py` 包含了增强的Qt插件路径设置函数。
+
+### 2. 创建启动脚本
+创建 `start_app.sh`:
+```bash
+#!/bin/bash
+# PyQt5应用程序启动脚本
+
+# 设置环境变量
+export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
+export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
+export QT_QPA_PLATFORM="cocoa"
+export QT_MAC_WANTS_LAYER="1"
+
+# 启动应用
+python src/main.py
+```
+
+### 3. 使用虚拟环境专用安装
+```bash
+# 激活虚拟环境
+source .venv/bin/activate
+
+# 在虚拟环境中安装
+python fix_pyqt5_complete.py
+```
+
+## 环境变量说明
+
+| 变量名 | 作用 | 推荐值 |
+|--------|------|--------|
+| QT_PLUGIN_PATH | Qt插件主路径 | PyQt5/Qt5/plugins |
+| QT_QPA_PLATFORM_PLUGIN_PATH | 平台插件路径 | PyQt5/Qt5/plugins/platforms |
+| QT_QPA_PLATFORM | 指定平台 | cocoa (macOS) |
+| QT_MAC_WANTS_LAYER | macOS图层支持 | 1 |
+| QT_LOGGING_RULES | 日志级别 | qt.qpa.*=false |
+
+## 常见问题
+
+### Q: 为什么PyQt5会丢失平台插件?
+A: 常见原因:
+- 安装过程中断或失败
+- 虚拟环境迁移
+- 系统Qt库冲突
+- 文件权限问题
+
+### Q: 如何验证修复是否成功?
+A: 运行测试命令:
+```python
+python -c "from PyQt5.QtWidgets import QApplication; print('成功!')"
+```
+
+### Q: 修复后仍然有问题?
+A: 尝试:
+1. 完全删除虚拟环境重新创建
+2. 使用系统包管理器安装Qt5
+3. 检查Python版本兼容性
+
+## 最佳实践
+
+1. **始终使用虚拟环境**
+2. **固定PyQt5版本**(推荐5.15.10)
+3. **在代码中设置插件路径**
+4. **创建启动脚本**
+5. **定期验证安装**
+
+## 一键修复命令
+
+```bash
+# 完整的修复流程
+cd /Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design
+python fix_pyqt5_complete.py
+source set_pyqt5_env.sh
+python src/main.py
+```
+
+这样应该能完全避免PyQt5平台插件问题!
\ No newline at end of file
diff --git a/build_v1.0.py b/build_v1.0.py
new file mode 100644
index 0000000..9689a80
--- /dev/null
+++ b/build_v1.0.py
@@ -0,0 +1,331 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+MagicWord 1.0.0 版本发布脚本
+用于构建和打包应用程序,包含所有图片和图标资源
+"""
+
+import os
+import sys
+import subprocess
+import platform
+import shutil
+import zipfile
+from datetime import datetime
+from PIL import Image
+
+def run_command(command, shell=False, cwd=None):
+ """运行命令并返回结果"""
+ try:
+ result = subprocess.run(command, shell=shell, capture_output=True, text=True, encoding='utf-8', cwd=cwd)
+ return result.returncode, result.stdout, result.stderr
+ except Exception as e:
+ return -1, "", str(e)
+
+def create_ico_from_png():
+ """从PNG图标创建ICO文件"""
+ print("创建ICO图标文件...")
+
+ # 检查是否存在256x256的PNG图标
+ png_path = "resources/icons/app_icon_256X256.png"
+ ico_path = "resources/icons/app_icon.ico"
+
+ if os.path.exists(png_path):
+ try:
+ # 打开PNG图像
+ img = Image.open(png_path)
+
+ # 创建不同尺寸的图标
+ icon_sizes = [(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)]
+
+ # 保存为ICO格式
+ img.save(ico_path, format='ICO', sizes=icon_sizes)
+ print(f"ICO图标创建成功: {ico_path}")
+ return True
+ except Exception as e:
+ print(f"创建ICO图标失败: {e}")
+ return False
+ else:
+ print(f"找不到PNG图标文件: {png_path}")
+ return False
+
+def clean_build_dirs():
+ """清理构建目录"""
+ print("清理构建目录...")
+ dirs_to_clean = ['build', 'dist', '__pycache__', '*.egg-info']
+
+ for dir_name in dirs_to_clean:
+ if '*' in dir_name:
+ # 处理通配符
+ import glob
+ for path in glob.glob(dir_name):
+ if os.path.isdir(path):
+ shutil.rmtree(path, ignore_errors=True)
+ elif os.path.exists(dir_name):
+ if os.path.isdir(dir_name):
+ shutil.rmtree(dir_name, ignore_errors=True)
+ else:
+ os.remove(dir_name)
+
+ # 清理src目录下的__pycache__
+ for root, dirs, files in os.walk('src'):
+ for dir_name in dirs:
+ if dir_name == '__pycache__':
+ cache_path = os.path.join(root, dir_name)
+ shutil.rmtree(cache_path, ignore_errors=True)
+ print(f"清理缓存: {cache_path}")
+
+def install_dependencies():
+ """安装依赖"""
+ print("安装项目依赖...")
+
+ # 首先安装PIL(用于图标转换)
+ code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "Pillow"])
+ if code != 0:
+ print(f"Pillow安装失败: {stderr}")
+ return False
+
+ # 安装其他依赖
+ code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"])
+ if code != 0:
+ print(f"依赖安装失败: {stderr}")
+ return False
+ print("依赖安装成功")
+ return True
+
+def build_executable():
+ """构建可执行文件"""
+ print("构建可执行文件...")
+
+ # 安装pyinstaller
+ print("安装PyInstaller...")
+ code, stdout, stderr = run_command([sys.executable, "-m", "pip", "install", "pyinstaller"])
+ if code != 0:
+ print(f"PyInstaller安装失败: {stderr}")
+ return False
+
+ # 创建ICO图标
+ create_ico_from_png()
+
+ # PyInstaller命令 - 完整版本
+ pyinstaller_cmd = [
+ "pyinstaller",
+ "--name", "MagicWord",
+ "--version", "1.0.0", # 设置版本号为1.0.0
+ "--distpath", "dist",
+ "--workpath", "build",
+ "--specpath", ".",
+ # 添加资源文件
+ "--add-data", "resources;resources",
+ "--add-data", "resources/icons;resources/icons",
+ "--add-data", "resources/config;resources/config",
+ "--add-data", "resources/qss;resources/qss",
+ "--add-data", "src;src",
+ "--add-data", "src/ui;src/ui",
+ "--add-data", "src/demo;src/demo",
+ # 隐藏导入
+ "--hidden-import", "PyQt5",
+ "--hidden-import", "PyQt5.QtCore",
+ "--hidden-import", "PyQt5.QtGui",
+ "--hidden-import", "PyQt5.QtWidgets",
+ "--hidden-import", "requests",
+ "--hidden-import", "beautifulsoup4",
+ "--hidden-import", "python-docx",
+ "--hidden-import", "PyPDF2",
+ "--hidden-import", "ebooklib",
+ "--hidden-import", "chardet",
+ "--hidden-import", "PIL",
+ "--hidden-import", "pillow",
+ # 图标设置
+ "--icon", "resources/icons/app_icon.ico" if os.path.exists("resources/icons/app_icon.ico") else "resources/icons/app_icon_256X256.png",
+ "--windowed", # 无控制台窗口
+ "--noconfirm",
+ "--clean", # 清理临时文件
+ "src/main.py"
+ ]
+
+ print("运行PyInstaller...")
+ code, stdout, stderr = run_command(pyinstaller_cmd)
+
+ if code != 0:
+ print(f"完整构建失败,尝试简化构建: {stderr}")
+ # 简化版本
+ simple_cmd = [
+ "pyinstaller",
+ "--onefile",
+ "--windowed",
+ "--icon=resources/icons/app_icon.ico" if os.path.exists("resources/icons/app_icon.ico") else "resources/icons/app_icon_256X256.png",
+ "--add-data=resources;resources",
+ "--add-data=resources/icons;resources/icons",
+ "--add-data=src;src",
+ "--name=MagicWord",
+ "--version=1.0.0",
+ "--clean",
+ "src/main.py"
+ ]
+ code, stdout, stderr = run_command(simple_cmd)
+ if code != 0:
+ print(f"简化构建也失败: {stderr}")
+ return False
+
+ print("可执行文件构建成功")
+ return True
+
+def create_package():
+ """创建发布包"""
+ print("创建发布包...")
+
+ # 检查构建结果
+ if platform.system() == "Windows":
+ exe_path = "dist/MagicWord.exe"
+ else:
+ exe_path = "dist/MagicWord"
+
+ if not os.path.exists(exe_path):
+ print(f"错误: 找不到可执行文件 {exe_path}")
+ return False
+
+ # 创建发布目录
+ release_dir = "dist_package_v1.0"
+ if os.path.exists(release_dir):
+ shutil.rmtree(release_dir)
+ os.makedirs(release_dir)
+
+ # 复制文件到发布目录
+ files_to_copy = [
+ (exe_path, "MagicWord.exe" if platform.system() == "Windows" else "MagicWord"),
+ ("README.md", "README.md"),
+ ("CHANGELOG.md", "CHANGELOG.md"),
+ ("requirements.txt", "requirements.txt"),
+ ("install_and_fix.py", "install_and_fix.py"),
+ ]
+
+ for src, dst in files_to_copy:
+ if os.path.exists(src):
+ shutil.copy2(src, os.path.join(release_dir, dst))
+ print(f"复制: {src} -> {dst}")
+
+ # 复制图标文件到发布包
+ icons_dir = os.path.join(release_dir, "icons")
+ if os.path.exists("resources/icons"):
+ shutil.copytree("resources/icons", icons_dir)
+ print("复制图标文件到发布包")
+
+ # 创建运行脚本
+ if platform.system() == "Windows":
+ run_script = """@echo off
+echo MagicWord 1.0.0 启动中...
+cd /d "%~dp0"
+start MagicWord.exe
+"""
+ with open(os.path.join(release_dir, "run.bat"), "w") as f:
+ f.write(run_script)
+
+ # 创建桌面快捷方式脚本
+ desktop_shortcut_script = """@echo off
+echo 创建桌面快捷方式...
+set SCRIPT_DIR=%~dp0
+set DESKTOP=%USERPROFILE%\Desktop
+set SHORTCUT=%DESKTOP%\MagicWord.lnk
+powershell -Command "$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut('%SHORTCUT%'); $Shortcut.TargetPath = '%SCRIPT_DIR%MagicWord.exe'; $Shortcut.WorkingDirectory = '%SCRIPT_DIR%'; $Shortcut.IconLocation = '%SCRIPT_DIR%icons\\app_icon_256X256.png'; $Shortcut.Save()"
+echo 桌面快捷方式创建完成!
+pause
+"""
+ with open(os.path.join(release_dir, "create_desktop_shortcut.bat"), "w") as f:
+ f.write(desktop_shortcut_script)
+
+ else:
+ run_script = """#!/bin/bash
+echo "MagicWord 1.0.0 启动中..."
+cd "$(dirname "$0")"
+./MagicWord &
+"""
+ with open(os.path.join(release_dir, "run.sh"), "w") as f:
+ f.write(run_script)
+ os.chmod(os.path.join(release_dir, "run.sh"), 0o755)
+
+ # 创建发布说明
+ release_info = f"""MagicWord 1.0.0 发布包
+构建时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
+平台: {platform.system()} {platform.machine()}
+Python版本: {platform.python_version()}
+
+快速开始:
+1. 运行 install_and_fix.py 安装依赖(如果需要)
+2. 运行 run.bat (Windows) 或 run.sh (Linux/Mac)
+3. 或直接运行 MagicWord.exe
+
+功能特性:
+- 完整的文档处理功能
+- 打字练习模式
+- 学习模式
+- 多种文档格式支持(Word, PDF, TXT等)
+- 美观的Word风格界面
+- 完整的图标和界面资源
+
+图标说明:
+- 包含完整的图标资源文件
+- 支持多种尺寸的图标(32x32, 64x64, 128x128, 256x256)
+- Windows版本包含ICO格式图标
+- 可创建桌面快捷方式
+
+详细更新请查看 CHANGELOG.md
+"""
+
+ with open(os.path.join(release_dir, "发布说明.txt"), "w", encoding='utf-8') as f:
+ f.write(release_info)
+
+ # 创建ZIP压缩包
+ print("创建ZIP压缩包...")
+ zip_filename = f"MagicWord_v1.0.0_{platform.system()}_{platform.machine()}"
+ with zipfile.ZipFile(f"{zip_filename}.zip", 'w', zipfile.ZIP_DEFLATED) as zipf:
+ for root, dirs, files in os.walk(release_dir):
+ for file in files:
+ file_path = os.path.join(root, file)
+ arc_path = os.path.relpath(file_path, release_dir)
+ zipf.write(file_path, arc_path)
+ print(f"添加到压缩包: {arc_path}")
+
+ print(f"发布包创建成功: {zip_filename}.zip")
+ return True
+
+def main():
+ """主函数"""
+ print("=" * 50)
+ print("MagicWord 1.0.0 构建脚本")
+ print("=" * 50)
+
+ # 检查Python版本
+ if sys.version_info < (3, 6):
+ print("错误: 需要Python 3.6或更高版本")
+ return False
+
+ # 清理构建目录
+ clean_build_dirs()
+
+ # 安装依赖
+ if not install_dependencies():
+ print("依赖安装失败")
+ return False
+
+ # 构建可执行文件
+ if not build_executable():
+ print("可执行文件构建失败")
+ return False
+
+ # 创建发布包
+ if not create_package():
+ print("发布包创建失败")
+ return False
+
+ print("=" * 50)
+ print("构建完成!")
+ print("发布包位于: dist_package_v1.0/")
+ print("ZIP压缩包已创建")
+ print("=" * 50)
+
+ return True
+
+if __name__ == "__main__":
+ success = main()
+ sys.exit(0 if success else 1)
\ No newline at end of file
diff --git a/doc/软件需求构思和描述文档-迭代一轮.docx b/doc/软件需求构思和描述文档-迭代一轮.docx
index c783b15..8cf1c39 100644
Binary files a/doc/软件需求构思和描述文档-迭代一轮.docx and b/doc/软件需求构思和描述文档-迭代一轮.docx differ
diff --git a/emergency_fix.sh b/emergency_fix.sh
new file mode 100755
index 0000000..2eb9ab7
--- /dev/null
+++ b/emergency_fix.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+# PyQt5 紧急修复脚本 - 终极解决方案
+
+echo "🚨 PyQt5 紧急修复中..."
+
+# 1. 完全清理现有安装
+echo "📦 步骤1: 清理PyQt5安装..."
+pip uninstall PyQt5 PyQt5-Qt5 PyQt5-sip -y
+rm -rf /Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5*
+
+# 2. 重新安装PyQt5
+echo "📦 步骤2: 重新安装PyQt5..."
+pip install PyQt5==5.15.10 --force-reinstall --no-cache-dir
+
+# 3. 设置环境变量
+echo "🔧 步骤3: 设置环境变量..."
+export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
+export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
+export QT_QPA_PLATFORM="cocoa"
+export QT_MAC_WANTS_LAYER="1"
+export QT_LOGGING_RULES="qt.qpa.*=false"
+
+# 4. 验证安装
+echo "✅ 步骤4: 验证安装..."
+python -c "
+import sys
+import os
+os.environ['QT_PLUGIN_PATH'] = '$QT_PLUGIN_PATH'
+os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = '$QT_QPA_PLATFORM_PLUGIN_PATH'
+os.environ['QT_QPA_PLATFORM'] = 'cocoa'
+os.environ['QT_MAC_WANTS_LAYER'] = '1'
+
+try:
+ from PyQt5.QtWidgets import QApplication, QLabel
+ from PyQt5.QtCore import Qt
+
+ app = QApplication(sys.argv)
+ label = QLabel('PyQt5修复成功!✅')
+ label.setAlignment(Qt.AlignCenter)
+ label.resize(200, 100)
+ label.show()
+
+ from PyQt5.QtCore import QTimer
+ QTimer.singleShot(1500, app.quit)
+
+ app.exec_()
+ print('✅ PyQt5验证成功!')
+except Exception as e:
+ print(f'❌ PyQt5验证失败: {e}')
+ import traceback
+ traceback.print_exc()
+"
+
+echo "🎉 修复完成!"
+echo "现在可以运行: python src/main.py"
\ No newline at end of file
diff --git a/fix_pyqt5_complete.py b/fix_pyqt5_complete.py
new file mode 100644
index 0000000..ac522d4
--- /dev/null
+++ b/fix_pyqt5_complete.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python3
+"""
+完整的PyQt5问题解决方案
+运行此脚本可以完全避免PyQt5平台插件问题
+"""
+
+import subprocess
+import sys
+import os
+import shutil
+import platform
+
+def run_command(cmd, description):
+ """运行命令并显示进度"""
+ print(f"正在执行: {description}")
+ result = subprocess.run(cmd, capture_output=True, text=True)
+ if result.returncode != 0:
+ print(f"❌ 失败: {result.stderr}")
+ return False
+ print(f"✅ 完成")
+ return True
+
+def clean_pyqt5_installation():
+ """彻底清理PyQt5安装"""
+ print("=== 清理PyQt5安装 ===")
+
+ # 1. 卸载PyQt5包
+ run_command([sys.executable, "-m", "pip", "uninstall", "PyQt5", "PyQt5-Qt5", "PyQt5-sip", "-y"],
+ "卸载PyQt5包")
+
+ # 2. 清理残留文件
+ venv_path = os.environ.get('VIRTUAL_ENV', '')
+ if venv_path:
+ site_packages = os.path.join(venv_path, 'lib', f'python{sys.version_info.major}.{sys.version_info.minor}', 'site-packages')
+ if os.path.exists(site_packages):
+ removed_count = 0
+ for item in os.listdir(site_packages):
+ if 'pyqt5' in item.lower():
+ item_path = os.path.join(site_packages, item)
+ try:
+ if os.path.isdir(item_path):
+ shutil.rmtree(item_path)
+ else:
+ os.remove(item_path)
+ removed_count += 1
+ print(f" 删除: {item}")
+ except Exception as e:
+ print(f" 删除失败 {item}: {e}")
+ print(f"✅ 清理完成,删除了 {removed_count} 个项目")
+
+ return True
+
+def install_pyqt5_properly():
+ """正确安装PyQt5"""
+ print("=== 安装PyQt5 ===")
+
+ # 使用清华镜像源加速下载(可选)
+ pip_args = [sys.executable, "-m", "pip", "install"]
+
+ # 检查是否有国内镜像源可用
+ try:
+ import requests
+ response = requests.get("https://pypi.tuna.tsinghua.edu.cn/simple", timeout=5)
+ if response.status_code == 200:
+ pip_args.extend(["-i", "https://pypi.tuna.tsinghua.edu.cn/simple"])
+ print("使用清华镜像源")
+ except:
+ pass
+
+ # 安装PyQt5
+ pip_args.extend(["PyQt5==5.15.10", "--no-cache-dir", "--force-reinstall"])
+
+ return run_command(pip_args, "安装PyQt5")
+
+def setup_environment_variables():
+ """设置环境变量"""
+ print("=== 设置环境变量 ===")
+
+ system = platform.system()
+ venv_path = os.environ.get('VIRTUAL_ENV', '')
+
+ if not venv_path:
+ print("❌ 未检测到虚拟环境")
+ return False
+
+ python_version = f"python{sys.version_info.major}.{sys.version_info.minor}"
+
+ # 可能的Qt插件路径
+ possible_paths = []
+
+ if system == "Darwin": # macOS
+ possible_paths = [
+ os.path.join(venv_path, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
+ '/usr/local/opt/qt5/plugins',
+ '/opt/homebrew/opt/qt5/plugins',
+ ]
+ elif system == "Windows":
+ possible_paths = [
+ os.path.join(venv_path, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
+ ]
+ elif system == "Linux":
+ possible_paths = [
+ os.path.join(venv_path, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
+ '/usr/lib/x86_64-linux-gnu/qt5/plugins',
+ ]
+
+ # 找到有效的路径
+ valid_path = None
+ for path in possible_paths:
+ if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')):
+ valid_path = path
+ break
+
+ if valid_path:
+ # 创建环境变量设置脚本
+ env_script = f"""
+# PyQt5环境变量设置
+export QT_PLUGIN_PATH="{valid_path}"
+export QT_QPA_PLATFORM_PLUGIN_PATH="{os.path.join(valid_path, 'platforms')}"
+"""
+
+ if system == "Darwin":
+ env_script += """
+export QT_QPA_PLATFORM="cocoa"
+export QT_MAC_WANTS_LAYER="1"
+export QT_LOGGING_RULES="qt.qpa.*=false"
+"""
+ elif system == "Windows":
+ env_script += """
+export QT_QPA_PLATFORM="windows"
+"""
+ elif system == "Linux":
+ env_script += """
+export QT_QPA_PLATFORM="xcb"
+"""
+
+ # 保存环境变量脚本
+ project_root = os.path.dirname(os.path.abspath(__file__))
+ env_file = os.path.join(project_root, 'set_pyqt5_env.sh')
+ with open(env_file, 'w') as f:
+ f.write(env_script.strip())
+
+ print(f"✅ 环境变量脚本已创建: {env_file}")
+ print(f" QT_PLUGIN_PATH: {valid_path}")
+ return True
+ else:
+ print("❌ 未找到有效的Qt插件路径")
+ return False
+
+def verify_installation():
+ """验证安装"""
+ print("=== 验证安装 ===")
+
+ # 测试导入
+ test_code = """
+import sys
+import os
+
+# 设置环境变量
+if 'QT_PLUGIN_PATH' in os.environ:
+ os.environ['QT_PLUGIN_PATH'] = os.environ['QT_PLUGIN_PATH']
+if 'QT_QPA_PLATFORM_PLUGIN_PATH' in os.environ:
+ os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']
+
+try:
+ from PyQt5.QtWidgets import QApplication, QLabel
+ from PyQt5.QtCore import Qt
+
+ app = QApplication(sys.argv)
+ label = QLabel("PyQt5安装成功!✅")
+ label.setAlignment(Qt.AlignCenter)
+ label.resize(200, 100)
+ label.show()
+
+ # 只显示2秒后自动关闭
+ from PyQt5.QtCore import QTimer
+ QTimer.singleShot(2000, app.quit)
+
+ app.exec_()
+ print("✅ PyQt5验证成功!")
+except Exception as e:
+ print(f"❌ PyQt5验证失败: {e}")
+ import traceback
+ traceback.print_exc()
+"""
+
+ with open('test_pyqt5.py', 'w') as f:
+ f.write(test_code)
+
+ result = subprocess.run([sys.executable, 'test_pyqt5.py'], capture_output=True, text=True)
+
+ # 清理测试文件
+ if os.path.exists('test_pyqt5.py'):
+ os.remove('test_pyqt5.py')
+
+ return result.returncode == 0
+
+def main():
+ """主函数"""
+ print("=== PyQt5完整修复方案 ===")
+ print(f"系统: {platform.system()}")
+ print(f"Python: {sys.version}")
+ print(f"虚拟环境: {os.environ.get('VIRTUAL_ENV', '未激活')}")
+ print()
+
+ # 获取项目根目录
+ project_root = os.path.dirname(os.path.abspath(__file__))
+ os.chdir(project_root)
+
+ steps = [
+ ("清理安装", clean_pyqt5_installation),
+ ("重新安装", install_pyqt5_properly),
+ ("设置环境", setup_environment_variables),
+ ("验证安装", verify_installation),
+ ]
+
+ success = True
+ for step_name, step_func in steps:
+ if not step_func():
+ print(f"❌ {step_name} 失败")
+ success = False
+ break
+ print()
+
+ if success:
+ print("🎉 PyQt5修复完成!")
+ print("\n使用方法:")
+ print("1. 运行: source set_pyqt5_env.sh")
+ print("2. 然后运行: python src/main.py")
+ else:
+ print("❌ PyQt5修复失败,请检查错误信息")
+
+ return success
+
+if __name__ == "__main__":
+ success = main()
+ sys.exit(0 if success else 1)
\ No newline at end of file
diff --git a/install_pyqt5_safe.py b/install_pyqt5_safe.py
new file mode 100644
index 0000000..7f89679
--- /dev/null
+++ b/install_pyqt5_safe.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+"""
+安全安装PyQt5的脚本,避免平台插件问题
+"""
+
+import subprocess
+import sys
+import os
+
+def install_pyqt5_safely():
+ """安全安装PyQt5的方法"""
+
+ print("正在安全安装PyQt5...")
+
+ # 1. 首先完全卸载现有的PyQt5
+ print("1. 卸载现有PyQt5...")
+ subprocess.run([sys.executable, "-m", "pip", "uninstall", "PyQt5", "PyQt5-Qt5", "PyQt5-sip", "-y"],
+ capture_output=True, text=True)
+
+ # 2. 清理可能残留的文件
+ print("2. 清理残留文件...")
+ venv_path = os.environ.get('VIRTUAL_ENV', '')
+ if venv_path:
+ site_packages = os.path.join(venv_path, 'lib', f'python{sys.version_info.major}.{sys.version_info.minor}', 'site-packages')
+ if os.path.exists(site_packages):
+ for item in os.listdir(site_packages):
+ if 'pyqt5' in item.lower():
+ item_path = os.path.join(site_packages, item)
+ if os.path.isdir(item_path):
+ import shutil
+ shutil.rmtree(item_path)
+ print(f" 删除目录: {item}")
+
+ # 3. 使用特定版本安装PyQt5
+ print("3. 安装PyQt5...")
+ result = subprocess.run([
+ sys.executable, "-m", "pip", "install",
+ "PyQt5==5.15.10",
+ "--no-cache-dir", # 不使用缓存,确保重新下载
+ "--force-reinstall" # 强制重新安装
+ ], capture_output=True, text=True)
+
+ if result.returncode == 0:
+ print("✅ PyQt5安装成功!")
+
+ # 4. 验证安装
+ print("4. 验证安装...")
+ test_result = subprocess.run([
+ sys.executable, "-c",
+ "from PyQt5.QtWidgets import QApplication; print('PyQt5导入成功')"
+ ], capture_output=True, text=True)
+
+ if test_result.returncode == 0:
+ print("✅ PyQt5验证通过!")
+ return True
+ else:
+ print("❌ PyQt5验证失败:", test_result.stderr)
+ return False
+ else:
+ print("❌ PyQt5安装失败:", result.stderr)
+ return False
+
+if __name__ == "__main__":
+ success = install_pyqt5_safely()
+ sys.exit(0 if success else 1)
\ No newline at end of file
diff --git a/package_v1.0.py b/package_v1.0.py
new file mode 100644
index 0000000..9ba4472
--- /dev/null
+++ b/package_v1.0.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+MagicWord 1.0.0 完整打包脚本
+创建包含所有资源的发布包
+"""
+
+import os
+import sys
+import shutil
+import zipfile
+from datetime import datetime
+
+def create_package():
+ """创建完整的发布包"""
+ print("=" * 60)
+ print("MagicWord 1.0.0 完整打包程序")
+ print("=" * 60)
+
+ # 检查可执行文件是否存在
+ exe_path = "dist/MagicWord.exe"
+ if not os.path.exists(exe_path):
+ print(f"错误: 找不到可执行文件 {exe_path}")
+ print("请先运行PyInstaller构建可执行文件")
+ return False
+
+ # 创建发布目录
+ release_dir = "MagicWord_v1.0.0_Windows"
+ if os.path.exists(release_dir):
+ shutil.rmtree(release_dir)
+ os.makedirs(release_dir)
+
+ print("创建发布包目录...")
+
+ # 复制主要文件
+ files_to_copy = [
+ (exe_path, "MagicWord.exe"),
+ ("README.md", "README.md"),
+ ("CHANGELOG.md", "CHANGELOG.md"),
+ ("requirements.txt", "requirements.txt"),
+ ("install_and_fix.py", "install_and_fix.py"),
+ ]
+
+ for src, dst in files_to_copy:
+ if os.path.exists(src):
+ shutil.copy2(src, os.path.join(release_dir, dst))
+ print(f"✓ 复制: {src} -> {dst}")
+ else:
+ print(f"⚠ 跳过: {src} (文件不存在)")
+
+ # 复制图标文件
+ icons_dir = os.path.join(release_dir, "icons")
+ if os.path.exists("resources/icons"):
+ shutil.copytree("resources/icons", icons_dir)
+ print("✓ 复制图标文件到发布包")
+
+ # 复制配置文件
+ config_dir = os.path.join(release_dir, "config")
+ if os.path.exists("resources/config"):
+ shutil.copytree("resources/config", config_dir)
+ print("✓ 复制配置文件到发布包")
+
+ # 复制样式文件
+ qss_dir = os.path.join(release_dir, "qss")
+ if os.path.exists("resources/qss") and os.listdir("resources/qss"):
+ shutil.copytree("resources/qss", qss_dir)
+ print("✓ 复制样式文件到发布包")
+
+ # 注意: UI图像文件已通过PyInstaller打包到可执行文件中
+ # 程序会在运行时从打包的资源中加载 114514.png 和 UI.png
+
+ # 创建运行脚本
+ print("创建运行脚本...")
+
+ # Windows运行脚本
+ run_script = """@echo off
+echo ========================================
+echo MagicWord 1.0.0 启动中...
+echo ========================================
+cd /d "%~dp0"
+start "" "MagicWord.exe"
+echo 程序已启动!
+timeout /t 3 /nobreak > nul
+"""
+
+ with open(os.path.join(release_dir, "运行程序.bat"), "w", encoding='utf-8') as f:
+ f.write(run_script)
+
+ # 创建桌面快捷方式脚本
+ desktop_shortcut_script = """@echo off
+echo 正在创建桌面快捷方式...
+cd /d "%~dp0"
+set SCRIPT_DIR=%CD%
+set DESKTOP=%USERPROFILE%\Desktop
+set SHORTCUT=%DESKTOP%\MagicWord.lnk
+
+echo 脚本目录: %SCRIPT_DIR%
+echo 桌面路径: %DESKTOP%
+
+powershell -Command "
+ $WshShell = New-Object -comObject WScript.Shell
+ $Shortcut = $WshShell.CreateShortcut('%SHORTCUT%')
+ $Shortcut.TargetPath = '%SCRIPT_DIR%\MagicWord.exe'
+ $Shortcut.WorkingDirectory = '%SCRIPT_DIR%'
+ $Shortcut.IconLocation = '%SCRIPT_DIR%\icons\app_icon_256X256.png'
+ $Shortcut.Description = 'MagicWord 1.0.0 - 隐私学习软件'
+ $Shortcut.Save()
+ Write-Host '桌面快捷方式创建成功!' -ForegroundColor Green
+"
+
+if exist "%SHORTCUT%" (
+ echo 桌面快捷方式创建成功!
+) else (
+ echo 桌面快捷方式创建失败!
+)
+echo.
+echo 按任意键退出...
+pause > nul
+"""
+
+ with open(os.path.join(release_dir, "创建桌面快捷方式.bat"), "w", encoding='utf-8') as f:
+ f.write(desktop_shortcut_script)
+
+ # 创建安装说明
+ install_guide = """MagicWord 1.0.0 安装使用说明
+
+========================================
+系统要求
+========================================
+- Windows 7/8/10/11 (64位)
+- 至少 100MB 可用磁盘空间
+- 建议内存: 4GB 以上
+
+========================================
+快速开始
+========================================
+方法1: 直接运行
+1. 双击 "MagicWord.exe" 即可运行程序
+
+方法2: 使用运行脚本
+1. 双击 "运行程序.bat" 自动启动程序
+
+方法3: 创建桌面快捷方式
+1. 双击 "创建桌面快捷方式.bat"
+2. 桌面上会出现 MagicWord 快捷方式
+3. 双击桌面快捷方式即可运行
+
+========================================
+文件说明
+========================================
+MagicWord.exe - 主程序文件
+icons/ - 图标文件夹
+ app_icon.ico - 应用程序图标
+ app_icon_256X256.png - 256x256图标
+ app_icon_128X128.png - 128x128图标
+ app_icon_64X64.png - 64x64图标
+ app_icon_32X32.png - 32x32图标
+
+config/ - 配置文件
+README.md - 项目说明文档
+CHANGELOG.md - 更新日志
+
+========================================
+卸载方法
+========================================
+直接删除整个 MagicWord 文件夹即可完全卸载
+
+========================================
+技术支持
+========================================
+如有问题,请查看 README.md 文件或联系开发者
+
+========================================
+版本信息
+========================================
+版本: 1.0.0
+构建时间: {build_time}
+平台: Windows
+架构: {architecture}
+
+祝您使用愉快!
+""".format(
+ build_time=datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
+ architecture="64位"
+ )
+
+ with open(os.path.join(release_dir, "安装说明.txt"), "w", encoding='utf-8') as f:
+ f.write(install_guide)
+
+ # 创建版本信息文件
+ version_info = f"""MagicWord Version 1.0.0
+Build Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
+Platform: Windows
+Architecture: 64-bit
+Python Version: 3.13
+Qt Version: PyQt5
+
+This is a standalone executable package containing all necessary dependencies.
+"""
+
+ with open(os.path.join(release_dir, "version.txt"), "w", encoding='utf-8') as f:
+ f.write(version_info)
+
+ print("创建ZIP压缩包...")
+
+ # 创建ZIP压缩包
+ zip_filename = f"MagicWord_v1.0.0_Windows_64bit"
+ with zipfile.ZipFile(f"{zip_filename}.zip", 'w', zipfile.ZIP_DEFLATED) as zipf:
+ for root, dirs, files in os.walk(release_dir):
+ for file in files:
+ file_path = os.path.join(root, file)
+ arc_path = os.path.relpath(file_path, release_dir)
+ zipf.write(file_path, arc_path)
+ print(f"添加到压缩包: {arc_path}")
+
+ print("=" * 60)
+ print("✅ 打包完成!")
+ print(f"📁 发布包目录: {release_dir}/")
+ print(f"📦 ZIP压缩包: {zip_filename}.zip")
+ print("=" * 60)
+
+ # 显示发布包内容
+ print("\n发布包内容:")
+ for root, dirs, files in os.walk(release_dir):
+ level = root.replace(release_dir, '').count(os.sep)
+ indent = ' ' * 2 * level
+ print(f"{indent}{os.path.basename(root)}/")
+ subindent = ' ' * 2 * (level + 1)
+ for file in files:
+ print(f"{subindent}{file}")
+
+ return True
+
+def main():
+ """主函数"""
+ try:
+ success = create_package()
+ if success:
+ print("\n🎉 所有文件已成功打包!")
+ print("您可以分发 ZIP 文件给用户。")
+ else:
+ print("\n❌ 打包过程中出现错误。")
+ return 1
+ except Exception as e:
+ print(f"\n❌ 发生错误: {e}")
+ return 1
+
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())
\ No newline at end of file
diff --git a/resources/icons/app_icon.icns b/resources/icons/app_icon.icns
new file mode 100644
index 0000000..06053a1
Binary files /dev/null and b/resources/icons/app_icon.icns differ
diff --git a/resources/icons/app_icon.ico b/resources/icons/app_icon.ico
new file mode 100644
index 0000000..9a97f72
Binary files /dev/null and b/resources/icons/app_icon.ico differ
diff --git a/run_debug.sh b/run_debug.sh
new file mode 100644
index 0000000..b062afe
--- /dev/null
+++ b/run_debug.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+echo "设置Qt调试环境变量..."
+export QT_DEBUG_PLUGINS=1
+echo "Qt调试模式已启用"
+echo ""
+echo "运行MagicWord应用程序..."
+python src/main.py
diff --git a/run_fixed.sh b/run_fixed.sh
new file mode 100755
index 0000000..9f125f0
--- /dev/null
+++ b/run_fixed.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# MagicWord 修复版启动脚本
+
+echo "🚀 正在启动 MagicWord (修复版)..."
+
+# 使用新的虚拟环境
+source new_venv/bin/activate
+
+# 设置Qt环境变量
+export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/new_venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
+export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/new_venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
+export QT_QPA_PLATFORM="cocoa"
+export QT_MAC_WANTS_LAYER="1"
+export QT_LOGGING_RULES="qt.qpa.*=false"
+
+echo "✅ 环境设置完成"
+echo "✅ 正在启动 MagicWord 应用..."
+
+# 启动应用
+cd src && python main.py
\ No newline at end of file
diff --git a/run_magicword.sh b/run_magicword.sh
new file mode 100755
index 0000000..f40b70e
--- /dev/null
+++ b/run_magicword.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# MagicWord 一键启动脚本
+# 自动处理PyQt5平台插件问题
+
+echo "🚀 正在启动 MagicWord..."
+
+# 设置PyQt5环境变量
+export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
+export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
+export QT_QPA_PLATFORM="cocoa"
+export QT_MAC_WANTS_LAYER="1"
+export QT_LOGGING_RULES="qt.qpa.*=false"
+
+# 激活虚拟环境(如果未激活)
+if [ -z "$VIRTUAL_ENV" ]; then
+ echo "📦 激活虚拟环境..."
+ source .venv/bin/activate
+fi
+
+# 检查PyQt5是否可用
+python -c "from PyQt5.QtWidgets import QApplication" 2>/dev/null
+if [ $? -ne 0 ]; then
+ echo "❌ PyQt5出现问题,正在修复..."
+ ./emergency_fix.sh
+fi
+
+# 启动应用
+echo "✅ 启动 MagicWord 应用..."
+cd src && python main.py
\ No newline at end of file
diff --git a/set_pyqt5_env.sh b/set_pyqt5_env.sh
new file mode 100644
index 0000000..e4ec5ce
--- /dev/null
+++ b/set_pyqt5_env.sh
@@ -0,0 +1,7 @@
+# PyQt5环境变量设置
+export QT_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins"
+export QT_QPA_PLATFORM_PLUGIN_PATH="/Users/maziang/Documents/CodingWorkPlace/Code/Curriculum_Design/.venv/lib/python3.9/site-packages/PyQt5/Qt5/plugins/platforms"
+
+export QT_QPA_PLATFORM="cocoa"
+export QT_MAC_WANTS_LAYER="1"
+export QT_LOGGING_RULES="qt.qpa.*=false"
\ No newline at end of file
diff --git a/setup.py b/setup.py
index f509b1c..8589459 100644
--- a/setup.py
+++ b/setup.py
@@ -2,12 +2,15 @@ from setuptools import setup, find_packages
setup(
name="MagicWord",
- version="0.3.0",
+ version="1.0.0",
description="隐私学习软件 - 一款通过打字练习来学习文档内容的工具",
author="MagicWord Team",
packages=find_packages(where="src"),
package_dir={"": "src"},
include_package_data=True,
+ package_data={
+ "": ["*.png", "*.ico", "*.json", "*.qss", "*.txt"],
+ },
install_requires=[
"python-docx>=0.8.10",
"PyPDF2>=1.26.0",
@@ -24,4 +27,17 @@ setup(
],
},
python_requires=">=3.6",
+ classifiers=[
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: End Users/Desktop",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Topic :: Education",
+ "Topic :: Office/Business",
+ ],
)
\ No newline at end of file
diff --git a/setup_qt_env.py b/setup_qt_env.py
new file mode 100644
index 0000000..7f1f625
--- /dev/null
+++ b/setup_qt_env.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+"""
+Qt环境配置脚本,确保平台插件正确加载
+"""
+
+import os
+import sys
+
+def setup_qt_environment():
+ """设置Qt环境变量,避免平台插件问题"""
+
+ # 获取虚拟环境的site-packages路径
+ venv_path = os.environ.get('VIRTUAL_ENV', '')
+ if not venv_path:
+ print("警告:未检测到虚拟环境")
+ return False
+
+ # 构建Qt插件路径
+ python_version = f"python{sys.version_info.major}.{sys.version_info.minor}"
+ qt_plugin_path = os.path.join(venv_path, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins')
+
+ if not os.path.exists(qt_plugin_path):
+ print(f"错误:Qt插件路径不存在: {qt_plugin_path}")
+ return False
+
+ # 设置环境变量
+ os.environ['QT_PLUGIN_PATH'] = qt_plugin_path
+ os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(qt_plugin_path, 'platforms')
+
+ # 对于macOS,还需要设置其他重要变量
+ if sys.platform == 'darwin':
+ os.environ['QT_QPA_PLATFORM'] = 'cocoa'
+ # 禁用Qt的某些可能导致问题的功能
+ os.environ['QT_MAC_WANTS_LAYER'] = '1'
+ os.environ['QT_AUTO_SCREEN_SCALE_FACTOR'] = '1'
+
+ print(f"✅ Qt环境变量设置完成")
+ print(f" QT_PLUGIN_PATH: {qt_plugin_path}")
+ print(f" QT_QPA_PLATFORM_PLUGIN_PATH: {os.environ['QT_QPA_PLATFORM_PLUGIN_PATH']}")
+
+ return True
+
+def verify_qt_setup():
+ """验证Qt设置是否正确"""
+ try:
+ from PyQt5.QtCore import QCoreApplication
+ from PyQt5.QtWidgets import QApplication
+
+ # 创建QApplication实例来测试
+ app = QCoreApplication.instance()
+ if app is None:
+ app = QApplication([])
+
+ # 获取平台信息
+ platform = app.platformName()
+ print(f"✅ Qt平台检测成功: {platform}")
+
+ return True
+ except Exception as e:
+ print(f"❌ Qt验证失败: {e}")
+ return False
+
+if __name__ == "__main__":
+ if setup_qt_environment():
+ verify_qt_setup()
\ No newline at end of file
diff --git a/src/deepseek_dialog_window.py b/src/deepseek_dialog_window.py
new file mode 100644
index 0000000..22cf9f1
--- /dev/null
+++ b/src/deepseek_dialog_window.py
@@ -0,0 +1,627 @@
+"""
+DeepSeek对话窗口模块
+提供与DeepSeek AI对话的功能
+"""
+
+import os
+import json
+import requests
+import threading
+from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTextEdit,
+ QLineEdit, QPushButton, QLabel, QMessageBox,
+ QSplitter, QScrollArea, QWidget, QFrame)
+from PyQt5.QtCore import Qt, pyqtSignal, QTimer
+from PyQt5.QtGui import QFont, QTextCursor
+
+
+class DeepSeekDialogWindow(QDialog):
+ """DeepSeek对话窗口"""
+
+ closed = pyqtSignal() # 窗口关闭信号
+ streaming_finished = pyqtSignal() # 流式输出完成信号
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.parent = parent
+ self.api_key = ""
+ self.conversation_history = []
+
+ # 流式输出相关变量
+ self.is_streaming = False
+ self.current_streaming_content = ""
+ self.streaming_message_id = ""
+ self.streaming_thread = None
+ self.streaming_timer = None
+
+ # 从本地加载API密钥
+ self.load_api_key()
+
+ # 如果没有API密钥,显示设置对话框
+ if not self.api_key:
+ self.show_api_key_dialog()
+
+ self.init_ui()
+
+ def load_api_key(self):
+ """从本地文件加载API密钥"""
+ config_file = os.path.join(os.path.dirname(__file__), "..", "resources", "config", "deepseek_api.json")
+
+ try:
+ if os.path.exists(config_file):
+ with open(config_file, 'r', encoding='utf-8') as f:
+ config = json.load(f)
+ self.api_key = config.get('api_key', '')
+ except Exception as e:
+ print(f"加载API密钥失败: {e}")
+
+ def save_api_key(self, api_key):
+ """保存API密钥到本地文件"""
+ config_dir = os.path.join(os.path.dirname(__file__), "..", "resources", "config")
+ config_file = os.path.join(config_dir, "deepseek_api.json")
+
+ try:
+ # 确保配置目录存在
+ os.makedirs(config_dir, exist_ok=True)
+
+ config = {'api_key': api_key}
+ with open(config_file, 'w', encoding='utf-8') as f:
+ json.dump(config, f, ensure_ascii=False, indent=2)
+
+ self.api_key = api_key
+ return True
+ except Exception as e:
+ QMessageBox.critical(self, "错误", f"保存API密钥失败: {e}")
+ return False
+
+ def show_api_key_dialog(self):
+ """显示API密钥输入对话框"""
+ dialog = QDialog(self)
+ dialog.setWindowTitle("DeepSeek API密钥设置")
+ dialog.setModal(True)
+ dialog.setFixedSize(400, 200)
+
+ layout = QVBoxLayout()
+
+ # 说明文本
+ info_label = QLabel("请输入您的DeepSeek API密钥:")
+ info_label.setWordWrap(True)
+ layout.addWidget(info_label)
+
+ # API密钥输入框
+ api_key_layout = QHBoxLayout()
+ api_key_label = QLabel("API密钥:")
+ self.api_key_input = QLineEdit()
+ self.api_key_input.setPlaceholderText("请输入您的DeepSeek API密钥")
+ self.api_key_input.setEchoMode(QLineEdit.Password)
+ api_key_layout.addWidget(api_key_label)
+ api_key_layout.addWidget(self.api_key_input)
+ layout.addLayout(api_key_layout)
+
+ # 按钮布局
+ button_layout = QHBoxLayout()
+
+ save_button = QPushButton("保存")
+ save_button.clicked.connect(lambda: self.save_and_close(dialog))
+
+ cancel_button = QPushButton("取消")
+ cancel_button.clicked.connect(dialog.reject)
+
+ button_layout.addWidget(save_button)
+ button_layout.addWidget(cancel_button)
+ layout.addLayout(button_layout)
+
+ dialog.setLayout(layout)
+
+ if dialog.exec_() == QDialog.Accepted:
+ return True
+ else:
+ QMessageBox.warning(self, "警告", "未设置API密钥,无法使用对话功能")
+ return False
+
+ def save_and_close(self, dialog):
+ """保存API密钥并关闭对话框"""
+ api_key = self.api_key_input.text().strip()
+ if not api_key:
+ QMessageBox.warning(self, "警告", "请输入有效的API密钥")
+ return
+
+ if self.save_api_key(api_key):
+ QMessageBox.information(self, "成功", "API密钥已保存")
+ dialog.accept()
+
+ def init_ui(self):
+ """初始化用户界面"""
+ self.setWindowTitle("DeepSeek AI对话")
+ self.setMinimumSize(800, 600)
+
+ # 主布局
+ main_layout = QVBoxLayout()
+
+ # 标题
+ title_label = QLabel("DeepSeek AI对话助手")
+ title_font = QFont()
+ title_font.setPointSize(16)
+ title_font.setBold(True)
+ title_label.setFont(title_font)
+ title_label.setAlignment(Qt.AlignCenter)
+ title_label.setStyleSheet("padding: 10px; background-color: #f0f0f0;")
+ main_layout.addWidget(title_label)
+
+ # 分割器
+ splitter = QSplitter(Qt.Vertical)
+
+ # 对话显示区域
+ self.create_conversation_area(splitter)
+
+ # 输入区域
+ self.create_input_area(splitter)
+
+ splitter.setSizes([400, 200])
+ main_layout.addWidget(splitter)
+
+ # 状态栏
+ status_layout = QHBoxLayout()
+ self.status_label = QLabel("就绪")
+ self.status_label.setStyleSheet("color: #666; padding: 5px;")
+ status_layout.addWidget(self.status_label)
+ status_layout.addStretch()
+
+ # API密钥管理按钮
+ api_key_button = QPushButton("管理API密钥")
+ api_key_button.clicked.connect(self.manage_api_key)
+ status_layout.addWidget(api_key_button)
+
+ main_layout.addLayout(status_layout)
+
+ self.setLayout(main_layout)
+
+ # 设置样式
+ self.apply_theme("light") # 默认使用浅色主题
+
+ # 添加主题切换按钮
+ theme_button = QPushButton("切换主题")
+ theme_button.clicked.connect(self.toggle_theme)
+ status_layout.addWidget(theme_button)
+
+ # 连接信号
+ self.streaming_finished.connect(self.on_streaming_finished)
+
+ def toggle_theme(self):
+ """切换黑白主题"""
+ if hasattr(self, 'current_theme') and self.current_theme == "dark":
+ self.apply_theme("light")
+ else:
+ self.apply_theme("dark")
+
+ def apply_theme(self, theme):
+ """应用主题样式"""
+ self.current_theme = theme
+
+ if theme == "dark":
+ # 深色主题样式
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #1e1e1e;
+ color: #ffffff;
+ }
+ QLabel {
+ color: #ffffff;
+ }
+ QTextEdit {
+ background-color: #2d2d2d;
+ color: #ffffff;
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 10px;
+ font-family: 'Microsoft YaHei', sans-serif;
+ font-size: 12px;
+ }
+ QLineEdit {
+ background-color: #2d2d2d;
+ color: #ffffff;
+ border: 1px solid #444;
+ border-radius: 4px;
+ padding: 8px;
+ font-size: 12px;
+ }
+ QPushButton {
+ background-color: #0078d7;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px 16px;
+ font-size: 12px;
+ }
+ QPushButton:hover {
+ background-color: #106ebe;
+ }
+ QPushButton:pressed {
+ background-color: #005a9e;
+ }
+ QScrollArea {
+ background-color: #1e1e1e;
+ border: none;
+ }
+ QScrollBar:vertical {
+ background-color: #2d2d2d;
+ width: 15px;
+ margin: 0px;
+ }
+ QScrollBar::handle:vertical {
+ background-color: #555;
+ border-radius: 7px;
+ min-height: 20px;
+ }
+ QScrollBar::handle:vertical:hover {
+ background-color: #666;
+ }
+ """)
+ else:
+ # 浅色主题样式
+ self.setStyleSheet("""
+ QDialog {
+ background-color: #ffffff;
+ color: #000000;
+ }
+ QLabel {
+ color: #000000;
+ }
+ QTextEdit {
+ background-color: #ffffff;
+ color: #000000;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 10px;
+ font-family: 'Microsoft YaHei', sans-serif;
+ font-size: 12px;
+ }
+ QLineEdit {
+ background-color: #ffffff;
+ color: #000000;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ padding: 8px;
+ font-size: 12px;
+ }
+ QPushButton {
+ background-color: #0078d7;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px 16px;
+ font-size: 12px;
+ }
+ QPushButton:hover {
+ background-color: #106ebe;
+ }
+ QPushButton:pressed {
+ background-color: #005a9e;
+ }
+ QScrollArea {
+ background-color: #ffffff;
+ border: none;
+ }
+ QScrollBar:vertical {
+ background-color: #f0f0f0;
+ width: 15px;
+ margin: 0px;
+ }
+ QScrollBar::handle:vertical {
+ background-color: #c0c0c0;
+ border-radius: 7px;
+ min-height: 20px;
+ }
+ QScrollBar::handle:vertical:hover {
+ background-color: #a0a0a0;
+ }
+ """)
+
+ def create_conversation_area(self, parent):
+ """创建对话显示区域"""
+ scroll_area = QScrollArea()
+ scroll_area.setWidgetResizable(True)
+
+ conversation_widget = QWidget()
+ conversation_layout = QVBoxLayout()
+
+ # 对话显示文本框
+ self.conversation_text = QTextEdit()
+ self.conversation_text.setReadOnly(True)
+ self.conversation_text.setFont(QFont("Microsoft YaHei", 10))
+ conversation_layout.addWidget(self.conversation_text)
+
+ conversation_widget.setLayout(conversation_layout)
+ scroll_area.setWidget(conversation_widget)
+
+ parent.addWidget(scroll_area)
+
+ def create_input_area(self, parent):
+ """创建输入区域"""
+ input_widget = QWidget()
+ input_layout = QVBoxLayout()
+
+ # 输入框
+ input_label = QLabel("输入您的问题:")
+ self.input_edit = QTextEdit()
+ self.input_edit.setMaximumHeight(80)
+ self.input_edit.setPlaceholderText("请输入您的问题...")
+
+ # 按钮布局
+ button_layout = QHBoxLayout()
+
+ send_button = QPushButton("发送")
+ send_button.clicked.connect(self.send_message)
+
+ clear_button = QPushButton("清空对话")
+ clear_button.clicked.connect(self.clear_conversation)
+
+ button_layout.addWidget(send_button)
+ button_layout.addWidget(clear_button)
+ button_layout.addStretch()
+
+ input_layout.addWidget(input_label)
+ input_layout.addWidget(self.input_edit)
+ input_layout.addLayout(button_layout)
+
+ input_widget.setLayout(input_layout)
+ parent.addWidget(input_widget)
+
+ def send_message(self):
+ """发送消息到DeepSeek API(流式输出)"""
+ if not self.api_key:
+ QMessageBox.warning(self, "警告", "请先设置API密钥")
+ self.show_api_key_dialog()
+ return
+
+ message = self.input_edit.toPlainText().strip()
+ if not message:
+ QMessageBox.warning(self, "警告", "请输入消息内容")
+ return
+
+ # 禁用发送按钮
+ self.input_edit.setEnabled(False)
+ self.status_label.setText("正在发送消息...")
+
+ try:
+ # 添加用户消息到对话历史
+ self.add_message_to_conversation("用户", message)
+
+ # 开始流式输出AI回复
+ self.start_streaming_response(message)
+
+ except Exception as e:
+ error_msg = f"发送消息失败: {str(e)}"
+ self.add_message_to_conversation("系统", error_msg)
+ self.status_label.setText("发送失败")
+ QMessageBox.critical(self, "错误", error_msg)
+
+ finally:
+ # 重新启用发送按钮
+ self.input_edit.setEnabled(True)
+
+ def start_streaming_response(self, message):
+ """开始流式输出AI回复"""
+ # 清空输入框
+ self.input_edit.clear()
+
+ # 开始流式输出
+ self.current_streaming_content = ""
+ self.is_streaming = True
+ self.status_label.setText("正在接收AI回复...")
+
+ # 添加AI助手的初始消息占位符
+ self.add_streaming_message_start()
+
+ # 在新线程中执行流式请求
+ import threading
+ self.streaming_thread = threading.Thread(target=self.call_deepseek_api_stream, args=(message,))
+ self.streaming_thread.daemon = True
+ self.streaming_thread.start()
+
+ # 启动定时器更新显示
+ self.streaming_timer = QTimer()
+ self.streaming_timer.timeout.connect(self.update_streaming_display)
+ self.streaming_timer.start(100) # 每100毫秒更新一次显示
+
+ def add_streaming_message_start(self):
+ """添加流式消息的开始部分"""
+ # 创建AI助手的消息占位符
+ self.streaming_message_id = f"ai_message_{len(self.conversation_history)}"
+
+ # 添加到对话历史
+ self.conversation_history.append({"sender": "AI助手", "message": "", "streaming": True})
+
+ # 在对话区域添加占位符
+ cursor = self.conversation_text.textCursor()
+ cursor.movePosition(QTextCursor.End)
+ self.conversation_text.setTextCursor(cursor)
+
+ # 添加AI助手的消息框架
+ self.conversation_text.insertHtml(
+ f'
'
+ f'
AI助手:正在思考...'
+ )
+
+ # 自动滚动到底部
+ self.conversation_text.ensureCursorVisible()
+
+ def update_streaming_display(self):
+ """更新流式显示"""
+ if hasattr(self, 'current_streaming_content') and self.current_streaming_content:
+ # 使用更简单的方法:重新构建整个对话历史
+ self.rebuild_conversation_display()
+
+ # 自动滚动到底部
+ self.conversation_text.ensureCursorVisible()
+
+ def rebuild_conversation_display(self):
+ """重新构建对话显示"""
+ html_content = ""
+
+ for msg in self.conversation_history:
+ sender = msg["sender"]
+ message = msg["message"]
+ is_streaming = msg.get("streaming", False)
+
+ # 根据发送者设置不同的样式
+ if sender == "用户":
+ bg_color = "#e3f2fd" if self.current_theme == "light" else "#2d3e50"
+ text_color = "#000" if self.current_theme == "light" else "#fff"
+ elif sender == "AI助手":
+ bg_color = "#f5f5f5" if self.current_theme == "light" else "#3d3d3d"
+ text_color = "#000" if self.current_theme == "light" else "#fff"
+ else: # 系统消息
+ bg_color = "#fff3cd" if self.current_theme == "light" else "#5d4e00"
+ text_color = "#856404" if self.current_theme == "light" else "#ffd700"
+
+ # 格式化消息内容
+ formatted_message = message.replace('\n', '
') if message else "正在思考..."
+
+ html_content += f'''
+
+ {sender}:
+ {formatted_message}
+
+ '''
+
+ # 设置HTML内容
+ self.conversation_text.setHtml(html_content)
+
+ def call_deepseek_api_stream(self, message):
+ """调用DeepSeek API(流式版本)"""
+ url = "https://api.deepseek.com/v1/chat/completions"
+
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {self.api_key}"
+ }
+
+ # 构建对话历史
+ messages = [{"role": "user", "content": message}]
+
+ data = {
+ "model": "deepseek-chat",
+ "messages": messages,
+ "stream": True, # 启用流式输出
+ "temperature": 0.7,
+ "max_tokens": 2000
+ }
+
+ try:
+ response = requests.post(url, headers=headers, json=data, stream=True, timeout=30)
+
+ if response.status_code == 200:
+ for line in response.iter_lines():
+ if line:
+ line = line.decode('utf-8')
+ if line.startswith('data: '):
+ data_str = line[6:] # 去掉 'data: ' 前缀
+ if data_str == '[DONE]':
+ break
+
+ try:
+ data_obj = json.loads(data_str)
+ if 'choices' in data_obj and len(data_obj['choices']) > 0:
+ delta = data_obj['choices'][0].get('delta', {})
+ if 'content' in delta:
+ content = delta['content']
+ self.current_streaming_content += content
+ except json.JSONDecodeError:
+ pass
+ else:
+ error_msg = f"API调用失败: {response.status_code} - {response.text}"
+ self.current_streaming_content = f"错误: {error_msg}"
+
+ except Exception as e:
+ self.current_streaming_content = f"请求失败: {str(e)}"
+
+ finally:
+ # 流式输出结束
+ self.is_streaming = False
+ if hasattr(self, 'streaming_timer'):
+ self.streaming_timer.stop()
+
+ # 更新对话历史
+ if hasattr(self, 'current_streaming_content') and self.current_streaming_content:
+ # 更新对话历史中的消息
+ for msg in self.conversation_history:
+ if msg.get('streaming') and msg.get('sender') == 'AI助手':
+ msg['message'] = self.current_streaming_content
+ msg['streaming'] = False
+ break
+
+ # 使用信号槽机制安全地更新UI
+ self.streaming_finished.emit()
+ else:
+ self.status_label.setText("接收失败")
+
+ def call_deepseek_api(self, message):
+ """调用DeepSeek API(非流式版本,保留作为备用)"""
+ url = "https://api.deepseek.com/v1/chat/completions"
+
+ headers = {
+ "Content-Type": "application/json",
+ "Authorization": f"Bearer {self.api_key}"
+ }
+
+ # 构建对话历史
+ messages = [{"role": "user", "content": message}]
+
+ data = {
+ "model": "deepseek-chat",
+ "messages": messages,
+ "stream": False,
+ "temperature": 0.7,
+ "max_tokens": 2000
+ }
+
+ response = requests.post(url, headers=headers, json=data, timeout=30)
+
+ if response.status_code == 200:
+ result = response.json()
+ return result["choices"][0]["message"]["content"]
+ else:
+ error_msg = f"API调用失败: {response.status_code} - {response.text}"
+ raise Exception(error_msg)
+
+ def add_message_to_conversation(self, sender, message):
+ """添加消息到对话显示区域"""
+ # 添加到对话历史
+ self.conversation_history.append({"sender": sender, "message": message})
+
+ # 使用新的对话显示系统
+ self.rebuild_conversation_display()
+
+ # 自动滚动到底部
+ self.conversation_text.ensureCursorVisible()
+
+ def on_streaming_finished(self):
+ """流式输出完成处理"""
+ # 安全地更新UI(在主线程中执行)
+ self.rebuild_conversation_display()
+ self.status_label.setText("消息接收完成")
+
+ def clear_conversation(self):
+ """清空对话历史"""
+ reply = QMessageBox.question(self, "确认", "确定要清空对话历史吗?",
+ QMessageBox.Yes | QMessageBox.No)
+
+ if reply == QMessageBox.Yes:
+ self.conversation_history.clear()
+ self.rebuild_conversation_display()
+ self.status_label.setText("对话已清空")
+
+ def manage_api_key(self):
+ """管理API密钥"""
+ self.show_api_key_dialog()
+
+ def closeEvent(self, event):
+ """关闭事件处理"""
+ # 发出关闭信号
+ self.closed.emit()
+
+ if self.parent:
+ # 通知父窗口对话窗口已关闭
+ if hasattr(self.parent, 'on_deepseek_dialog_closed'):
+ self.parent.on_deepseek_dialog_closed()
+ event.accept()
\ No newline at end of file
diff --git a/src/file_parser.py b/src/file_parser.py
index b6362d2..017b289 100644
--- a/src/file_parser.py
+++ b/src/file_parser.py
@@ -23,6 +23,8 @@ class FileParser:
return FileParser.parse_docx(file_path)
elif ext == '.pdf':
return FileParser.parse_pdf(file_path)
+ elif ext == '.html':
+ return FileParser.parse_html(file_path)
else:
raise ValueError(f"Unsupported file format: {ext}")
except Exception as e:
@@ -80,6 +82,11 @@ class FileParser:
content = FileParser.parse_pdf(file_path)
images = [] # PDF图片提取较复杂,暂时跳过
+ elif ext == '.html':
+ # HTML文件:提取文本内容
+ content = FileParser.parse_html(file_path)
+ images = [] # HTML图片提取较复杂,暂时跳过
+
else:
return {
'success': False,
@@ -271,6 +278,46 @@ class FileParser:
except Exception as e:
raise Exception(f"Error parsing pdf file {file_path}: {str(e)}")
+ @staticmethod
+ def parse_html(file_path: str) -> str:
+ """解析HTML文件,提取文本内容"""
+ # 验证文件路径
+ if not FileParser.validate_file_path(file_path):
+ raise ValueError(f"Invalid file path: {file_path}")
+
+ try:
+ from bs4 import BeautifulSoup
+ except ImportError:
+ raise ImportError("BeautifulSoup4 library is required for parsing .html files. Please install it using 'pip install beautifulsoup4'")
+
+ try:
+ # 检测文件编码
+ encoding = FileParser.detect_file_encoding(file_path)
+
+ # 读取HTML文件
+ with open(file_path, 'r', encoding=encoding, errors='ignore') as f:
+ html_content = f.read()
+
+ # 使用BeautifulSoup解析HTML
+ soup = BeautifulSoup(html_content, 'html.parser')
+
+ # 移除script和style标签
+ for script in soup(["script", "style"]):
+ script.decompose()
+
+ # 提取文本内容
+ text = soup.get_text()
+
+ # 清理多余的空白字符
+ lines = (line.strip() for line in text.splitlines())
+ chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
+ text = '\n'.join(chunk for chunk in chunks if chunk)
+
+ return text
+
+ except Exception as e:
+ raise Exception(f"Error parsing html file {file_path}: {str(e)}")
+
@staticmethod
def validate_file_path(file_path: str) -> bool:
"""验证文件路径是否有效"""
diff --git a/src/learning_mode_window.py b/src/learning_mode_window.py
index 1ec350b..4634578 100644
--- a/src/learning_mode_window.py
+++ b/src/learning_mode_window.py
@@ -4,17 +4,25 @@ import os
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QLabel, QFrame, QMenuBar,
QAction, QFileDialog, QMessageBox, QApplication,
- QSplitter, QScrollArea, QStatusBar, QProgressBar)
+ QSplitter, QScrollArea, QStatusBar, QProgressBar, QTextBrowser, QSizePolicy,
+ QListWidget, QListWidgetItem, QDialog, QGraphicsScene, QGraphicsView)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect
from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor
-from src.ui.components import CustomTitleBar, TextDisplayWidget
-from src.typing_logic import TypingLogic
-from src.file_parser import FileParser
-from src.ui.theme_manager import theme_manager
+# 修复导入路径
+from ui.components import CustomTitleBar, TextDisplayWidget
+from typing_logic import TypingLogic
+from file_parser import FileParser
+from ui.theme_manager import theme_manager
+import tempfile
+import hashlib
class LearningModeWindow(QMainWindow):
- def __init__(self, parent=None, imported_content="", current_position=0):
+ # 定义内容变化信号
+ content_changed = pyqtSignal(str, int) # 参数:内容,位置
+ # 定义关闭信号
+ closed = pyqtSignal()
+ def __init__(self, parent=None, imported_content="", current_position=0, image_data=None, image_positions=None):
"""
学习模式窗口
- 顶部显示UI.png图片
@@ -25,13 +33,18 @@ class LearningModeWindow(QMainWindow):
parent: 父窗口
imported_content: 从主窗口传递的导入内容
current_position: 当前学习进度位置
+ image_data: 图片数据字典 {文件名: 二进制数据}
+ image_positions: 图片位置信息列表
"""
super().__init__(parent)
self.parent_window = parent
self.imported_content = imported_content
self.current_position = current_position
+ self.image_data = image_data or {}
+ self.image_positions = image_positions or []
self.typing_logic = None
self.is_loading_file = False
+ self.extracted_images = [] # 用于存储提取的图片数据
# 初始化UI
self.initUI()
@@ -39,6 +52,9 @@ class LearningModeWindow(QMainWindow):
# 初始化打字逻辑
self.init_typing_logic()
+ # 初始化同步位置跟踪
+ self.last_sync_position = current_position
+
# 如果有导入内容,初始化显示
if self.imported_content:
self.initialize_with_imported_content()
@@ -53,6 +69,12 @@ class LearningModeWindow(QMainWindow):
# 重置打字逻辑
if self.typing_logic:
self.typing_logic.reset(self.imported_content)
+
+ # 设置图片数据到打字逻辑
+ if self.image_data:
+ self.typing_logic.set_image_data(self.image_data)
+ if self.image_positions:
+ self.typing_logic.set_image_positions(self.image_positions)
# 显示已学习的内容
display_text = self.imported_content[:self.current_position]
@@ -137,38 +159,48 @@ class LearningModeWindow(QMainWindow):
ui_image_path = os.path.join(os.path.dirname(__file__), 'ui', 'UI.png')
if os.path.exists(ui_image_path):
pixmap = QPixmap(ui_image_path)
-
- # 保存原始图片尺寸
- self.original_pixmap = pixmap
-
- # 设置图片完全铺满标签
- self.image_label.setPixmap(pixmap)
- self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签
-
- # 设置图片标签的尺寸策略,使其可以扩展
- from PyQt5.QtWidgets import QSizePolicy
- self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
-
- # 设置图片区域的最小高度为图片高度的1/3,确保图片可见
- min_height = max(200, pixmap.height() // 3)
- self.image_label.setMinimumHeight(min_height)
-
- # 重新设置窗口大小以适配图片
- self.resize(pixmap.width(), self.height())
+ if not pixmap.isNull():
+ # 保存原始图片用于缩放计算
+ self.original_pixmap = pixmap
+
+ # 设置图片到标签
+ self.image_label.setPixmap(pixmap)
+ self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签
+
+ # 设置大小策略
+ self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
+
+ # 计算合适的最小高度(保持图片比例)
+ window_width = 900 # 默认窗口宽度
+ original_width = pixmap.width()
+ original_height = pixmap.height()
+ if original_width > 0:
+ min_height = int(window_width * original_height / original_width)
+ self.image_label.setMinimumHeight(min_height)
+ else:
+ self.image_label.setMinimumHeight(200)
+ else:
+ self.image_label.setText("UI图片加载失败")
+ self.image_label.setStyleSheet("""
+ QLabel {
+ background-color: #f8f9fa;
+ color: #666666;
+ font-size: 14px;
+ qproperty-alignment: AlignCenter;
+ }
+ """)
else:
self.image_label.setText("UI图片未找到")
self.image_label.setStyleSheet("""
QLabel {
background-color: #f8f9fa;
- border: none;
color: #666666;
font-size: 14px;
- padding: 20px;
+ qproperty-alignment: AlignCenter;
}
""")
- self.image_label.setMinimumHeight(200)
- # 直接添加图片标签到主布局,不使用滚动区域
+ # 添加到主布局
main_layout.addWidget(self.image_label)
def resizeEvent(self, event):
@@ -203,6 +235,7 @@ class LearningModeWindow(QMainWindow):
创建输入区域
- 创建文本显示组件
- 设置与主系统相同的样式
+ - 创建图片列表区域
"""
# 创建文本显示组件(复用主系统的组件)
self.text_display_widget = TextDisplayWidget(self)
@@ -219,7 +252,40 @@ class LearningModeWindow(QMainWindow):
# 连接文本变化信号
self.text_display_widget.text_display.textChanged.connect(self.on_text_changed)
- main_layout.addWidget(self.text_display_widget, 1) # 占据剩余空间
+ # 创建图片显示区域
+ self.image_list_widget = QListWidget()
+ self.image_list_widget.setMaximumHeight(150)
+ self.image_list_widget.setStyleSheet("""
+ QListWidget {
+ background-color: #f8f8f8;
+ border: 1px solid #d0d0d0;
+ border-radius: 4px;
+ font-size: 11px;
+ }
+ QListWidget::item {
+ padding: 5px;
+ border-bottom: 1px solid #e0e0e0;
+ }
+ QListWidget::item:selected {
+ background-color: #e3f2fd;
+ color: #1976d2;
+ }
+ """)
+ self.image_list_widget.setVisible(False) # 默认隐藏
+ self.image_list_widget.itemDoubleClicked.connect(self.on_image_item_double_clicked)
+
+ # 创建布局容器
+ input_container = QWidget()
+ input_layout = QVBoxLayout()
+ input_layout.setContentsMargins(0, 0, 0, 0)
+ input_layout.setSpacing(5)
+
+ input_layout.addWidget(self.text_display_widget, 1) # 文本显示区域占据剩余空间
+ input_layout.addWidget(self.image_list_widget) # 图片列表区域
+
+ input_container.setLayout(input_layout)
+
+ main_layout.addWidget(input_container, 1) # 占据剩余空间
def create_menu_bar(self):
"""
@@ -294,38 +360,93 @@ class LearningModeWindow(QMainWindow):
)
if file_path:
+ self.is_loading_file = True
try:
- self.is_loading_file = True
-
- # 使用文件解析器
- parser = FileParser()
- content = parser.parse_file(file_path)
+ # 获取文件扩展名
+ _, ext = os.path.splitext(file_path)
+ ext = ext.lower()
- if content:
- # 存储导入的内容
- self.imported_content = content
- self.current_position = 0
-
- # 重置打字逻辑
- if self.typing_logic:
- self.typing_logic.reset(content)
-
- # 清空文本显示
- self.text_display_widget.text_display.clear()
-
- # 更新状态
- self.status_label.setText(f"已导入: {os.path.basename(file_path)}")
- self.progress_label.setText(f"进度: 0% (0/{len(content)} 字符)")
-
- # 显示成功消息
- QMessageBox.information(self, "导入成功",
- f"文件导入成功!\n文件: {os.path.basename(file_path)}\n字符数: {len(content)}\n\n开始打字以显示学习内容。")
-
+ # 对于docx文件,直接解析而不转换为txt
+ if ext == '.docx':
+ # 直接解析docx文件内容
+ content = FileParser.parse_docx(file_path)
+ # 提取图片数据
+ images = FileParser.extract_images_from_docx(file_path)
else:
- QMessageBox.warning(self, "导入失败", "无法解析文件内容,请检查文件格式。")
+ # 其他文件类型使用原来的转换方法
+ result = FileParser.parse_and_convert_to_txt(file_path)
+ content = result['content']
+ images = result.get('images', [])
+
+ if not content:
+ QMessageBox.warning(self, "导入失败", "文件内容为空或解析失败!")
+ return
+
+ # 保存导入的内容
+ self.imported_content = content
+ self.current_position = 0
+
+ # 保存提取的图片数据
+ self.extracted_images = images
+
+ # 设置打字逻辑
+ if self.typing_logic:
+ self.typing_logic.reset(content)
+ # 如果有图片,设置图片数据到打字逻辑
+ if images:
+ image_data_dict = {}
+ image_positions = []
+
+ # 为每张图片生成位置信息 - 改进位置计算逻辑
+ for i, (filename, image_data) in enumerate(images):
+ image_data_dict[filename] = image_data
+
+ # 改进图片位置计算,确保图片能在用户早期打字时显示
+ content_length = len(content)
+ if content_length == 0:
+ content_length = 1000 # 备用长度
+
+ if len(images) == 1:
+ # 只有一张图片,放在文档开始位置附近(前10%),确保用户能快速看到
+ image_pos = max(10, content_length // 10)
+ else:
+ # 多张图片:前几张放在较前位置,确保用户能看到
+ if i < 3:
+ # 前3张图片放在文档前30%
+ segment = content_length // 3
+ image_pos = max(10, segment * (i + 1) // 4)
+ else:
+ # 其余图片均匀分布
+ remaining_start = content_length // 2
+ remaining_index = i - 3
+ remaining_count = len(images) - 3
+ if remaining_count > 0:
+ segment = (content_length - remaining_start) // (remaining_count + 1)
+ image_pos = remaining_start + segment * (remaining_index + 1)
+ else:
+ image_pos = content_length // 2
+
+ image_positions.append({
+ 'start_pos': image_pos,
+ 'end_pos': image_pos + 50, # 图片占位符长度
+ 'filename': filename
+ })
+
+ # 设置图片数据到打字逻辑
+ self.typing_logic.set_image_data(image_data_dict)
+ self.typing_logic.set_image_positions(image_positions)
+
+ # 显示图片列表
+ self.display_image_list(images)
+
+ # 显示初始内容(空)
+ self.text_display_widget.text_display.clear()
+ self.status_label.setText("已导入文件,请开始打字学习...")
+ self.progress_label.setText("进度: 0%")
+
except Exception as e:
- QMessageBox.critical(self, "导入错误", f"导入文件时出错:\n{str(e)}")
+ QMessageBox.critical(self, "导入错误", f"导入文件时发生错误:\n{str(e)}")
finally:
self.is_loading_file = False
@@ -335,6 +456,8 @@ class LearningModeWindow(QMainWindow):
文本变化处理
- 根据导入的内容逐步显示
- 更新学习进度
+ - 同步内容到打字模式
+ - 处理图片插入
"""
# 如果正在加载文件,跳过处理
if self.is_loading_file:
@@ -382,6 +505,7 @@ class LearningModeWindow(QMainWindow):
self.status_label.setText(f"输入错误!期望字符: '{result.get('expected', '')}'")
else:
# 输入正确,更新进度
+ old_position = self.current_position
self.current_position = len(current_text)
progress = (self.current_position / len(self.imported_content)) * 100
@@ -389,6 +513,17 @@ class LearningModeWindow(QMainWindow):
f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)"
)
+
+
+ # 只在用户新输入的字符上同步到打字模式
+ if self.parent_window and hasattr(self.parent_window, 'text_edit'):
+ # 获取用户这一轮新输入的字符(与上一轮相比的新内容)
+ if old_position < self.current_position:
+ new_input = expected_text[old_position:self.current_position]
+ if new_input: # 只有新输入内容时才同步
+ # 只同步新输入的内容,不传递整个文本
+ self.content_changed.emit(new_input, len(new_input))
+
# 检查是否完成
if result.get('completed', False):
self.status_label.setText("恭喜!学习完成!")
@@ -396,6 +531,8 @@ class LearningModeWindow(QMainWindow):
else:
self.status_label.setText("继续输入以显示更多内容...")
+
+
def show_about(self):
"""
显示关于对话框
@@ -406,7 +543,8 @@ class LearningModeWindow(QMainWindow):
"• 顶部显示UI界面图片\n"
"• 下方为打字输入区域\n"
"• 导入文件后逐步显示内容\n"
- "• 实时显示学习进度\n\n"
+ "• 实时显示学习进度\n"
+ "• 支持图片显示\n\n"
"使用方法:\n"
"1. 点击'文件'->'导入文件'选择学习材料\n"
"2. 在下方文本区域开始打字\n"
@@ -417,6 +555,9 @@ class LearningModeWindow(QMainWindow):
窗口关闭事件
- 通知父窗口学习模式已关闭
"""
+ # 发射关闭信号
+ self.closed.emit()
+
if self.parent_window and hasattr(self.parent_window, 'on_learning_mode_closed'):
self.parent_window.on_learning_mode_closed()
@@ -431,4 +572,152 @@ class LearningModeWindow(QMainWindow):
if event.key() == Qt.Key_Escape:
self.close()
else:
- super().keyPressEvent(event)
\ No newline at end of file
+ super().keyPressEvent(event)
+
+ def display_image_list(self, images):
+ """
+ 显示图片列表
+ """
+ try:
+ # 清空之前的图片列表
+ self.image_list_widget.clear()
+
+ # 如果没有图片,隐藏图片列表区域
+ if not images:
+ self.image_list_widget.setVisible(False)
+ return
+
+ # 显示图片列表区域
+ self.image_list_widget.setVisible(True)
+
+ # 添加图片项到列表
+ for index, (filename, image_data) in enumerate(images):
+ # 创建缩略图
+ pixmap = QPixmap()
+ if pixmap.loadFromData(image_data):
+ # 创建缩略图
+ thumbnail = pixmap.scaled(60, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation)
+
+ # 创建列表项
+ item = QListWidgetItem()
+ item.setIcon(QIcon(thumbnail))
+ item.setText(f"{filename} ({pixmap.width()}x{pixmap.height()})")
+ item.setData(Qt.UserRole, index) # 保存图片索引
+ self.image_list_widget.addItem(item)
+ else:
+ # 如果无法加载图片,显示默认文本
+ item = QListWidgetItem(f"{filename} (无法预览)")
+ item.setData(Qt.UserRole, index)
+ self.image_list_widget.addItem(item)
+
+ # 更新状态栏
+ self.status_label.setText(f"已提取 {len(images)} 张图片,双击查看大图")
+
+ except Exception as e:
+ self.status_label.setText(f"显示图片列表失败: {str(e)}")
+
+ def on_image_item_double_clicked(self, item):
+ """
+ 双击图片项时显示大图
+ """
+ try:
+ # 获取图片索引
+ index = item.data(Qt.UserRole)
+ if 0 <= index < len(self.extracted_images):
+ image_filename, image_data = self.extracted_images[index]
+ self.show_image_viewer(image_filename, image_data)
+ except Exception as e:
+ self.status_label.setText(f"显示图片失败: {str(e)}")
+
+ def show_image_viewer(self, filename, image_data):
+ """
+ 显示图片查看器 - 支持缩放功能
+ """
+ try:
+ # 创建自定义图片查看窗口
+ viewer = QDialog(self)
+ viewer.setWindowTitle(f"图片查看 - {filename}")
+ viewer.setModal(False)
+
+ # 设置窗口标志,保留标题栏以便用户可以移动和调整大小
+ viewer.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint)
+
+ # 设置窗口背景为黑色
+ viewer.setStyleSheet("""
+ QDialog {
+ background-color: #000000;
+ }
+ """)
+
+ # 创建场景和视图
+ scene = QGraphicsScene(viewer)
+ view = QGraphicsView(scene)
+ view.setStyleSheet("border: none;") # 移除视图边框
+
+ # 设置视图为可交互的,并启用滚动条
+ view.setDragMode(QGraphicsView.ScrollHandDrag)
+ view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
+
+ # 创建布局
+ layout = QVBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(view)
+ viewer.setLayout(layout)
+
+ # 加载图片
+ pixmap = QPixmap()
+ if not pixmap.loadFromData(image_data):
+ self.status_label.setText(f"加载图片失败: {filename}")
+ return
+
+ # 将图片添加到场景
+ scene.addPixmap(pixmap)
+
+ # 设置视图大小和位置
+ if self:
+ parent_geometry = self.geometry()
+ screen_geometry = QApplication.primaryScreen().geometry()
+
+ # 设置窗口宽度与主窗口相同,高度为屏幕高度的40%
+ window_width = parent_geometry.width()
+ window_height = int(screen_geometry.height() * 0.4)
+
+ # 计算位置:显示在主窗口正上方
+ x = parent_geometry.x()
+ y = parent_geometry.y() - window_height
+
+ # 确保不会超出屏幕边界
+ if y < screen_geometry.top():
+ y = parent_geometry.y() + 50 # 如果上方空间不足,显示在下方
+
+ # 调整宽度确保不超出屏幕
+ if x + window_width > screen_geometry.right():
+ window_width = screen_geometry.right() - x
+
+ viewer.setGeometry(x, y, window_width, window_height)
+
+ viewer.show()
+
+ # 设置视图适应图片大小
+ view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
+
+ # 重写视图的滚轮事件以支持缩放
+ def wheelEvent(event):
+ factor = 1.2
+ if event.angleDelta().y() > 0:
+ view.scale(factor, factor)
+ else:
+ view.scale(1.0/factor, 1.0/factor)
+
+ view.wheelEvent = wheelEvent
+
+ # 添加双击重置视图功能
+ def mouseDoubleClickEvent(event):
+ view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
+
+ view.mouseDoubleClickEvent = mouseDoubleClickEvent
+
+ except Exception as e:
+ self.status_label.setText(f"创建图片查看器失败: {str(e)}")
+ import traceback
+ traceback.print_exc()
\ No newline at end of file
diff --git a/src/main.py b/src/main.py
index b119247..56b0016 100644
--- a/src/main.py
+++ b/src/main.py
@@ -8,59 +8,74 @@ import platform
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_root)
-# 设置Qt平台插件路径 - 根据操作系统设置正确的Qt插件路径
+# 设置Qt平台插件路径 - 增强版本,完全避免平台插件问题
def set_qt_plugin_path():
+ """设置Qt平台插件路径,确保所有平台插件都能正确加载"""
system = platform.system()
+ # 获取Python版本
+ python_version = f"python{sys.version_info.major}.{sys.version_info.minor}"
+
+ # 可能的Qt插件路径列表
+ possible_paths = []
+
if system == "Windows":
- # Windows环境下查找Qt插件路径
- # 首先检查虚拟环境中的Qt插件
- venv_qt_plugins_path = os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins')
- if os.path.exists(venv_qt_plugins_path):
- os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path
- return
-
- # 检查全局Python安装中的Qt插件
- global_qt_plugins_path = os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins')
- if os.path.exists(global_qt_plugins_path):
- os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = global_qt_plugins_path
- return
-
- # 尝试在常见的Windows PyQt5安装路径中查找
- common_paths = [
+ # Windows环境下的路径
+ possible_paths.extend([
+ os.path.join(project_root, '.venv', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
+ os.path.join(sys.prefix, 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(os.path.expanduser('~'), 'AppData', 'Local', 'Programs', 'Python', 'Python39', 'Lib', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'Python', 'Python39', 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
- ]
-
- for path in common_paths:
- if os.path.exists(path):
- os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = path
- return
-
+ ])
elif system == "Darwin": # macOS
- # macOS环境下查找Qt插件路径
- system_qt_plugins_path = '/usr/local/opt/qt5/plugins' # macOS Homebrew Qt5路径
- venv_qt_plugins_path = os.path.join(project_root, '.venv', 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5', 'plugins')
-
- # 优先检查系统Qt插件路径
- if os.path.exists(system_qt_plugins_path):
- os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = system_qt_plugins_path
- return
- elif os.path.exists(venv_qt_plugins_path):
- os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path
- return
-
+ # macOS环境下的路径
+ possible_paths.extend([
+ os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
+ os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
+ '/usr/local/opt/qt5/plugins', # Homebrew Qt5
+ '/opt/homebrew/opt/qt5/plugins', # Apple Silicon Homebrew
+ os.path.expanduser('~/Qt/5.15.2/clang_64/plugins'), # Qt官方安装
+ ])
elif system == "Linux":
- # Linux环境下查找Qt插件路径
- venv_qt_plugins_path = os.path.join(project_root, '.venv', 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5', 'plugins')
- global_qt_plugins_path = os.path.join(sys.prefix, 'lib', 'python3.9', 'site-packages', 'PyQt5', 'Qt5', 'plugins')
+ # Linux环境下的路径
+ possible_paths.extend([
+ os.path.join(project_root, '.venv', 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
+ os.path.join(sys.prefix, 'lib', python_version, 'site-packages', 'PyQt5', 'Qt5', 'plugins'),
+ '/usr/lib/x86_64-linux-gnu/qt5/plugins',
+ '/usr/lib/qt5/plugins',
+ ])
+
+ # 查找第一个存在的路径
+ valid_path = None
+ for path in possible_paths:
+ if os.path.exists(path) and os.path.exists(os.path.join(path, 'platforms')):
+ valid_path = path
+ break
+
+ if valid_path:
+ # 设置Qt插件路径
+ os.environ['QT_PLUGIN_PATH'] = valid_path
+ os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = os.path.join(valid_path, 'platforms')
+
+ # 设置平台特定的环境变量
+ if system == "Darwin": # macOS
+ os.environ['QT_QPA_PLATFORM'] = 'cocoa'
+ os.environ['QT_MAC_WANTS_LAYER'] = '1'
+ # 禁用可能导致问题的Qt功能
+ os.environ['QT_LOGGING_RULES'] = 'qt.qpa.*=false' # 禁用Qt警告日志
+ elif system == "Windows":
+ os.environ['QT_QPA_PLATFORM'] = 'windows'
+ elif system == "Linux":
+ os.environ['QT_QPA_PLATFORM'] = 'xcb'
+ # 对于Linux,可能需要设置DISPLAY
+ if 'DISPLAY' not in os.environ:
+ os.environ['DISPLAY'] = ':0'
- if os.path.exists(venv_qt_plugins_path):
- os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path
- return
- elif os.path.exists(global_qt_plugins_path):
- os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = global_qt_plugins_path
- return
+ print(f"✅ Qt插件路径设置成功: {valid_path}")
+ return True
+ else:
+ print("⚠️ 警告:未找到Qt插件路径")
+ return False
# 设置Qt平台插件路径
set_qt_plugin_path()
@@ -80,12 +95,14 @@ def main():
# 创建QApplication实例
app = QApplication(sys.argv)
- # 设置应用程序样式为Windows风格,更接近Word界面
- app.setStyle('WindowsVista')
+ # 在macOS上使用系统原生样式,在其他平台上使用WindowsVista样式
+ if platform.system() != "Darwin": # 不是macOS系统
+ # 设置应用程序样式为Windows风格,更接近Word界面
+ app.setStyle('WindowsVista')
# 设置应用程序属性
app.setApplicationName("MagicWord")
- app.setApplicationVersion("0.2.2")
+ app.setApplicationVersion("1.0.0")
app.setOrganizationName("MagicWord")
# 设置窗口图标(如果存在)
diff --git a/src/main_window.py b/src/main_window.py
index 6c8afa6..0dc3bd0 100644
--- a/src/main_window.py
+++ b/src/main_window.py
@@ -1,482 +1,9 @@
-import sys
-import os
-from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, QAction,
- QFileDialog, QVBoxLayout, QWidget, QLabel, QStatusBar, QMessageBox)
-from PyQt5.QtGui import QFont, QTextCharFormat, QColor, QTextCursor
-from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal
-
-# 添加项目根目录到Python路径
-sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
-
-# 导入自定义UI组件
-from src.ui.components import CustomTitleBar, ProgressBarWidget, TextDisplayWidget, StatsDisplayWidget, QuoteDisplayWidget, WeatherDisplayWidget
-from src.file_parser import FileParser
-from src.typing_logic import TypingLogic
-from src.services.network_service import NetworkService
-
-class WeatherFetchThread(QThread):
- """天气信息获取线程"""
- weather_fetched = pyqtSignal(object) # 天气信息获取成功信号
- error_occurred = pyqtSignal(str) # 错误发生信号
-
- def __init__(self):
- super().__init__()
- self.network_service = NetworkService()
-
- def run(self):
- try:
- weather_info = self.network_service.get_weather_info()
- if weather_info:
- # 格式化天气信息
- formatted_info = (
- f"天气: {weather_info['city']} - "
- f"{weather_info['description']} - "
- f"温度: {weather_info['temperature']}°C - "
- f"湿度: {weather_info['humidity']}% - "
- f"风速: {weather_info['wind_speed']} m/s"
- )
- self.weather_fetched.emit(formatted_info)
- else:
- self.error_occurred.emit("无法获取天气信息")
- except Exception as e:
- self.error_occurred.emit(f"获取天气信息时出错: {str(e)}")
-
-class MainWindow(QMainWindow):
- def __init__(self):
+def on_learning_mode_closed(self):
"""
- 初始化主窗口
- - 设置窗口标题为"隐私学习软件 - 仿Word"
- - 设置窗口大小为800x600
- - 初始化学习内容存储变量
- - 初始化当前输入位置
- - 调用initUI()方法
+ 学习模式窗口关闭回调
+ - 清除学习窗口引用
+ - 更新菜单状态
"""
- super().__init__()
- self.learning_content = ""
- self.current_position = 0
- self.typing_logic = None
- self.text_edit = None
- self.status_bar = None
- self.title_bar = None
- self.progress_bar_widget = None
- self.text_display_widget = None
- self.initUI()
-
- def initUI(self):
- """
- 创建和布局所有UI组件
- - 创建自定义标题栏
- - 创建文本显示组件
- - 调用createMenuBar()创建菜单
- - 创建状态栏并显示"就绪"
- """
- # 设置窗口属性
- self.setWindowTitle("隐私学习软件 - 仿Word")
- self.setGeometry(100, 100, 800, 600)
- self.setWindowFlags(Qt.FramelessWindowHint) # 移除默认标题栏
-
- # 创建中央widget
- central_widget = QWidget()
- self.setCentralWidget(central_widget)
-
- # 创建主布局
- main_layout = QVBoxLayout()
- main_layout.setContentsMargins(0, 0, 0, 0)
- main_layout.setSpacing(0)
- central_widget.setLayout(main_layout)
-
- # 创建自定义标题栏
- self.title_bar = CustomTitleBar(self)
- main_layout.addWidget(self.title_bar)
-
- # 创建统计信息显示组件(默认隐藏)
- self.stats_display = StatsDisplayWidget(self)
- self.stats_display.setVisible(False) # 默认隐藏
- main_layout.addWidget(self.stats_display)
-
- # 创建每日一言显示组件(默认隐藏)
- self.quote_display = QuoteDisplayWidget(self)
- self.quote_display.setVisible(False) # 默认隐藏
- main_layout.addWidget(self.quote_display)
-
- # 创建天气显示组件(默认隐藏)
- self.weather_display = WeatherDisplayWidget(self)
- self.weather_display.setVisible(False) # 默认隐藏
- main_layout.addWidget(self.weather_display)
-
- # 创建文本显示组件
- self.text_display_widget = TextDisplayWidget(self)
- main_layout.addWidget(self.text_display_widget)
-
- # 连接文本显示组件的文本变化信号
- self.text_display_widget.text_display.textChanged.connect(self.onTextChanged)
-
- # 创建菜单栏
- self.createMenuBar()
-
- # 创建状态栏
- self.status_bar = self.statusBar()
- self.status_bar.showMessage("就绪")
-
- def createTopFunctionArea(self, main_layout):
- """
- 创建顶部功能区域
- - 显示准确率、WPM等统计信息
- - 显示每日一言功能
- """
- from PyQt5.QtWidgets import QWidget, QHBoxLayout, QLabel, QPushButton
- from PyQt5.QtCore import Qt
-
- # 创建顶部功能区域widget
- top_widget = QWidget()
- top_widget.setStyleSheet("""
- QWidget {
- background-color: #f0f0f0;
- border-bottom: 1px solid #d0d0d0;
- }
- """)
-
- # 创建水平布局
- top_layout = QHBoxLayout()
- top_layout.setContentsMargins(10, 5, 10, 5)
- top_layout.setSpacing(15)
-
- # 创建统计信息标签
- self.wpm_label = QLabel("WPM: 0")
- self.accuracy_label = QLabel("准确率: 0%")
- self.quote_label = QLabel("每日一言: 暂无")
- self.quote_label.setStyleSheet("QLabel { color: #666666; font-style: italic; }")
-
- # 设置标签样式
- label_style = "font-size: 12px; font-weight: normal; color: #333333;"
- self.wpm_label.setStyleSheet(label_style)
- self.accuracy_label.setStyleSheet(label_style)
-
- # 创建每日一言刷新按钮
- self.refresh_quote_button = QPushButton("刷新")
- self.refresh_quote_button.setStyleSheet("""
- QPushButton {
- background-color: #0078d7;
- color: white;
- border: none;
- padding: 5px 10px;
- border-radius: 3px;
- font-size: 12px;
- }
- QPushButton:hover {
- background-color: #005a9e;
- }
- """)
- self.refresh_quote_button.clicked.connect(self.refresh_daily_quote)
-
- # 添加组件到布局
- top_layout.addWidget(self.wpm_label)
- top_layout.addWidget(self.accuracy_label)
- top_layout.addStretch()
- top_layout.addWidget(self.quote_label)
- top_layout.addWidget(self.refresh_quote_button)
-
- top_widget.setLayout(top_layout)
- main_layout.addWidget(top_widget)
-
- def createMenuBar(self):
- """
- 创建菜单栏和所有菜单项
- - 文件菜单:打开(Ctrl+O)、保存(Ctrl+S)、退出(Ctrl+Q)
- - 视图菜单:显示统计信息、显示每日一言
- - 帮助菜单:关于
- - 为每个菜单项连接对应的槽函数
- """
- menu_bar = self.menuBar()
-
- # 文件菜单
- file_menu = menu_bar.addMenu('文件')
-
- # 打开动作
- open_action = QAction('打开', self)
- open_action.setShortcut('Ctrl+O')
- open_action.triggered.connect(self.openFile)
- file_menu.addAction(open_action)
-
- # 保存动作
- save_action = QAction('保存', self)
- save_action.setShortcut('Ctrl+S')
- save_action.triggered.connect(self.saveFile)
- file_menu.addAction(save_action)
-
- # 分隔线
- file_menu.addSeparator()
-
- # 退出动作
- exit_action = QAction('退出', self)
- exit_action.setShortcut('Ctrl+Q')
- exit_action.triggered.connect(self.close)
- file_menu.addAction(exit_action)
-
- # 视图菜单
- view_menu = menu_bar.addMenu('视图')
-
- # 显示统计信息动作
- self.stats_action = QAction('显示统计信息', self)
- self.stats_action.setCheckable(True)
- self.stats_action.setChecked(True)
- self.stats_action.triggered.connect(self.toggleStatsDisplay)
- view_menu.addAction(self.stats_action)
-
- # 显示每日一言动作
- self.quote_action = QAction('显示每日一言', self)
- self.quote_action.setCheckable(True)
- self.quote_action.setChecked(True)
- self.quote_action.triggered.connect(self.toggleQuoteDisplay)
- view_menu.addAction(self.quote_action)
-
- # 显示天气信息动作
- self.weather_action = QAction('显示天气', self)
- self.weather_action.setCheckable(True)
- self.weather_action.setChecked(True)
- self.weather_action.triggered.connect(self.toggleWeatherDisplay)
- view_menu.addAction(self.weather_action)
-
- # 帮助菜单
- help_menu = menu_bar.addMenu('帮助')
-
- # 关于动作
- about_action = QAction('关于', self)
- about_action.triggered.connect(self.showAbout)
- help_menu.addAction(about_action)
-
- def toggleStatsDisplay(self, checked):
- """
- 切换统计信息显示
- - checked: 是否显示统计信息
- """
- self.stats_display.setVisible(checked)
-
- def toggleQuoteDisplay(self, checked):
- """
- 切换每日一言显示
- - checked: 是否显示每日一言
- """
- self.quote_display.setVisible(checked)
- # 如果启用显示且quote为空,则刷新一次
- if checked and not self.quote_display.quote_label.text():
- self.refresh_daily_quote()
-
- def toggleWeatherDisplay(self, checked):
- """切换天气信息显示"""
- self.weather_display.setVisible(checked)
- # 如果启用显示且天气信息为空,则刷新一次
- if checked and not self.weather_display.weather_label.text():
- self.refresh_weather_info()
-
- def openFile(self):
- """
- 打开文件选择对话框并加载选中的文件
- - 显示文件选择对话框,过滤条件:*.txt, *.docx, *.pdf
- - 如果用户选择了文件,调用FileParser.parse_file(file_path)
- - 成功时:将内容存储但不直接显示,重置打字状态
- - 失败时:显示错误消息框
- """
- options = QFileDialog.Options()
- file_path, _ = QFileDialog.getOpenFileName(
- self,
- "打开文件",
- "",
- "文本文件 (*.txt);;Word文档 (*.docx);;PDF文件 (*.pdf);;所有文件 (*)",
- options=options
- )
-
- if file_path:
- try:
- # 解析文件内容
- content = FileParser.parse_file(file_path)
- self.learning_content = content
-
- # 在文本显示组件中设置内容(初始为空,通过打字逐步显示)
- if self.text_display_widget:
- self.text_display_widget.set_text(content) # 设置文件内容
-
- # 重置打字状态
- self.typing_logic = TypingLogic(content)
- self.current_position = 0
-
- # 更新状态栏
- self.status_bar.showMessage(f"已打开文件: {file_path},开始打字以显示内容")
- except Exception as e:
- # 显示错误消息框
- QMessageBox.critical(self, "错误", f"无法打开文件:\n{str(e)}")
-
- def saveFile(self):
- """
- 保存当前内容到文件
- - 显示保存文件对话框
- - 将文本区域内容写入选定文件
- - 返回操作结果
- """
- options = QFileDialog.Options()
- file_path, _ = QFileDialog.getSaveFileName(
- self,
- "保存文件",
- "",
- "文本文件 (*.txt);;所有文件 (*)",
- options=options
- )
-
- if file_path:
- try:
- # 获取文本编辑区域的内容
- content = self.text_edit.toPlainText()
-
- # 写入文件
- with open(file_path, 'w', encoding='utf-8') as f:
- f.write(content)
-
- # 更新状态栏
- self.status_bar.showMessage(f"文件已保存: {file_path}")
-
- return True
- except Exception as e:
- # 显示错误消息框
- QMessageBox.critical(self, "错误", f"无法保存文件:\n{str(e)}")
- return False
-
- return False
-
- def showAbout(self):
- """
- 显示关于对话框
- - 显示消息框,包含软件名称、版本、描述
- """
- QMessageBox.about(
- self,
- "关于",
- "隐私学习软件 - 仿Word\n\n"
- "版本: 1.0\n\n"
- "这是一个用于隐私学习的打字练习软件,\n"
- "可以加载文档并进行打字练习,\n"
- "帮助提高打字速度和准确性。"
- )
-
- def refresh_daily_quote(self):
- """
- 刷新每日一言
- - 从网络API获取名言
- - 更新显示
- """
- import requests
- import json
- from PyQt5.QtCore import Qt
- from src.constants import QUOTE_API_URL
-
- try:
- # 发送请求获取每日一言
- response = requests.get(QUOTE_API_URL, timeout=5)
- if response.status_code == 200:
- data = response.json()
- quote_content = data.get('content', '暂无内容')
- quote_author = data.get('author', '未知作者')
-
- # 更新显示
- self.quote_label.setText(f"每日一言: {quote_content} — {quote_author}")
-
- # 同时更新统计信息显示组件中的每日一言
- if hasattr(self, 'stats_display') and self.stats_display:
- self.stats_display.update_quote(f"{quote_content} — {quote_author}")
- else:
- self.quote_label.setText("每日一言: 获取失败")
- # 同时更新统计信息显示组件中的每日一言
- if hasattr(self, 'stats_display') and self.stats_display:
- self.stats_display.update_quote("获取失败")
- except Exception as e:
- self.quote_label.setText("每日一言: 获取失败")
- # 同时更新统计信息显示组件中的每日一言
- if hasattr(self, 'stats_display') and self.stats_display:
- self.stats_display.update_quote("获取失败")
-
- def onTextChanged(self):
- """
- 处理用户输入变化事件(打字练习)
- - 获取文本显示组件中的文本
- - 使用TypingLogic.check_input检查输入
- - 根据结果更新文本显示组件
- - 更新统计数据展示
- """
- # 防止递归调用
- if getattr(self, '_processing_text_change', False):
- return
-
- if not self.typing_logic:
- return
-
- # 设置标志防止递归
- self._processing_text_change = True
-
- try:
- # 获取当前输入文本
- current_text = self.text_display_widget.text_display.toPlainText()
-
- # 检查输入是否正确
- result = self.typing_logic.check_input(current_text)
- is_correct = result["correct"]
- expected_char = result["expected"]
-
- # 更新文本显示组件
- if self.text_display_widget:
- # 显示用户输入反馈
- self.text_display_widget.show_user_input(current_text)
-
- # 不再高亮下一个字符,因为内容通过打字逐步显示
-
- # 计算统计数据
- stats = self.typing_logic.get_statistics()
- accuracy = stats['accuracy_rate'] * 100 # 转换为百分比
- # 可以根据需要添加更多统计数据的计算
- wpm = 0 # 暂时设置为0,后续可以实现WPM计算
-
- # 更新状态栏
- self.status_bar.showMessage(f"WPM: {wpm:.1f} | 准确率: {accuracy:.1f}%")
-
- # 更新统计信息显示组件
- if hasattr(self, 'stats_display') and self.stats_display.isVisible():
- self.stats_display.update_stats(int(wpm), accuracy)
-
- # 更新每日一言显示组件(如果需要)
- if hasattr(self, 'quote_display') and self.quote_display.isVisible() and not self.quote_display.quote_label.text():
- self.refresh_daily_quote()
-
- # 更新顶部功能区的统计数据(如果仍然存在)
- if hasattr(self, 'wpm_label') and self.wpm_label:
- self.wpm_label.setText(f"WPM: {wpm:.1f}")
- if hasattr(self, 'accuracy_label') and self.accuracy_label:
- self.accuracy_label.setText(f"准确率: {accuracy:.1f}%")
- finally:
- # 清除递归防止标志
- self._processing_text_change = False
-
- def refresh_daily_quote(self):
- """刷新每日一言"""
- # 创建并启动获取名言的线程
- self.quote_thread = QuoteFetchThread()
- self.quote_thread.quote_fetched.connect(self.on_quote_fetched)
- self.quote_thread.error_occurred.connect(self.on_quote_error)
- self.quote_thread.start()
-
- def refresh_weather_info(self):
- """刷新天气信息"""
- # 创建并启动获取天气信息的线程
- self.weather_thread = WeatherFetchThread()
- self.weather_thread.weather_fetched.connect(self.on_weather_fetched)
- self.weather_thread.error_occurred.connect(self.on_weather_error)
- self.weather_thread.start()
-
- def on_weather_fetched(self, weather_info):
- """处理天气信息获取成功"""
- # 更新天气显示组件
- if hasattr(self, 'weather_display') and self.weather_display:
- self.weather_display.update_weather(weather_info)
-
- def on_weather_error(self, error_msg):
- """处理天气信息获取错误"""
- # 更新天气显示组件
- if hasattr(self, 'weather_display') and self.weather_display:
- self.weather_display.update_weather(error_msg)
\ No newline at end of file
+ self.learning_window = None
+ self.learning_mode_action.setChecked(False)
+ self.typing_mode_action.setChecked(True)
\ No newline at end of file
diff --git a/src/services/network_service.py b/src/services/network_service.py
index 60e7f4a..e67dedc 100644
--- a/src/services/network_service.py
+++ b/src/services/network_service.py
@@ -51,7 +51,61 @@ class NetworkService:
# 5. 返回天气信息字典
return formatted_weather
else:
- # 模拟天气数据(无API密钥时)
+ # 当没有API密钥时,使用免费的天气API获取真实数据
+ # 首先尝试获取城市ID(需要映射城市名到ID)
+ city_id_map = {
+ "Beijing": "101010100",
+ "Shanghai": "101020100",
+ "Tianjin": "101030100",
+ "Chongqing": "101040100",
+ "Hong Kong": "101320101",
+ "Macau": "101330101"
+ }
+
+ # 尝试映射英文城市名到ID
+ city_id = city_id_map.get(city)
+
+ # 如果找不到映射,尝试直接使用城市名
+ if not city_id:
+ # 对于中国主要城市,直接使用拼音映射
+ city_pinyin_map = {
+ "Beijing": "北京",
+ "Shanghai": "上海",
+ "Tianjin": "天津",
+ "Chongqing": "重庆"
+ }
+ chinese_city = city_pinyin_map.get(city, city)
+
+ # 使用免费天气API
+ try:
+ # 使用和风天气免费API的替代方案 - sojson天气API
+ weather_url = f"http://t.weather.sojson.com/api/weather/city/101010100" # 默认北京
+ weather_response = self.session.get(weather_url, timeout=5, verify=False)
+ weather_data = weather_response.json()
+
+ if weather_data.get("status") == 200:
+ # 解析天气数据
+ current_data = weather_data.get("data", {})
+ wendu = current_data.get("wendu", "N/A")
+ shidu = current_data.get("shidu", "N/A")
+ forecast = current_data.get("forecast", [])
+
+ # 获取第一个预报项作为当前天气
+ current_weather = forecast[0] if forecast else {}
+ weather_type = current_weather.get("type", "晴")
+
+ formatted_weather = {
+ "city": city,
+ "temperature": float(wendu) if wendu != "N/A" else 20,
+ "description": weather_type,
+ "humidity": shidu.replace("%", "") if shidu != "N/A" else "60",
+ "wind_speed": "3.5" # 默认风速
+ }
+ return formatted_weather
+ except Exception as e:
+ print(f"获取免费天气数据时出错: {e}")
+
+ # 如果以上都失败,返回默认数据
return {
"city": city,
"temperature": 20,
diff --git a/src/ui/calendar_floating_widget.py b/src/ui/calendar_floating_widget.py
new file mode 100644
index 0000000..f8ee8ca
--- /dev/null
+++ b/src/ui/calendar_floating_widget.py
@@ -0,0 +1,482 @@
+# -*- coding: utf-8 -*-
+
+"""
+日历悬浮窗口模块
+提供一个可拖拽的日历悬浮窗口,用于在应用程序中显示和选择日期
+"""
+
+import sys
+from PyQt5.QtWidgets import (
+ QWidget, QCalendarWidget, QVBoxLayout, QHBoxLayout,
+ QPushButton, QLabel, QFrame
+)
+from PyQt5.QtCore import QDate, Qt, pyqtSignal, QPoint
+from PyQt5.QtGui import QFont
+
+# 导入主题管理器
+from .theme_manager import theme_manager
+
+
+class CalendarFloatingWidget(QWidget):
+ """日历悬浮窗口类"""
+
+ # 自定义信号
+ closed = pyqtSignal() # 窗口关闭信号
+ date_selected = pyqtSignal(str) # 日期字符串信号,用于插入功能
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.drag_position = None
+ self.is_dragging = False
+ self.setup_ui()
+ self.setup_connections()
+ self.setup_theme()
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
+ self.setAttribute(Qt.WA_TranslucentBackground)
+
+ def setup_ui(self):
+ """设置UI界面"""
+ # 设置窗口属性
+ self.setWindowTitle("日历")
+ self.setFixedSize(360, 320) # 设置窗口大小
+
+ # 创建主框架,用于实现圆角和阴影效果
+ self.main_frame = QFrame()
+ self.main_frame.setObjectName("mainFrame")
+
+ # 主布局
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.addWidget(self.main_frame)
+
+ # 内容布局
+ content_layout = QVBoxLayout(self.main_frame)
+ content_layout.setContentsMargins(10, 10, 10, 10)
+ content_layout.setSpacing(8)
+
+ # 标题栏
+ title_layout = QHBoxLayout()
+ title_layout.setContentsMargins(0, 0, 0, 0)
+ title_layout.setSpacing(0)
+
+ self.title_label = QLabel("日历")
+ self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
+ title_layout.addWidget(self.title_label)
+ title_layout.addStretch()
+ # 添加一个小的固定空间,使关闭按钮向左移动
+ title_layout.addSpacing(25) # 向左移动25个单位
+
+ # 关闭按钮
+ self.close_btn = QPushButton("×")
+ self.close_btn.setFixedSize(20, 20)
+ self.close_btn.setObjectName("closeButton")
+ title_layout.addWidget(self.close_btn)
+
+ content_layout.addLayout(title_layout)
+
+ # 分隔线
+ separator = QFrame()
+ separator.setObjectName("separator")
+ separator.setFixedHeight(1)
+ content_layout.addWidget(separator)
+
+ # 日历控件
+ self.calendar = QCalendarWidget()
+ self.calendar.setGridVisible(True)
+ self.calendar.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
+ self.calendar.setNavigationBarVisible(True)
+ content_layout.addWidget(self.calendar)
+
+ # 当前日期显示
+ self.date_label = QLabel()
+ self.date_label.setAlignment(Qt.AlignCenter)
+ self.date_label.setFont(QFont("Arial", 10))
+ self.date_label.setObjectName("dateLabel")
+ self.update_date_label()
+ content_layout.addWidget(self.date_label)
+
+ # 操作按钮
+ button_layout = QHBoxLayout()
+ button_layout.setContentsMargins(0, 0, 0, 0)
+ button_layout.setSpacing(6)
+
+ self.today_btn = QPushButton("今天")
+ self.today_btn.setObjectName("todayButton")
+ button_layout.addWidget(self.today_btn)
+
+ self.insert_btn = QPushButton("插入")
+ self.insert_btn.setObjectName("insertButton")
+ button_layout.addWidget(self.insert_btn)
+
+ button_layout.addStretch()
+
+ self.clear_btn = QPushButton("清除")
+ self.clear_btn.setObjectName("clearButton")
+ button_layout.addWidget(self.clear_btn)
+
+ content_layout.addLayout(button_layout)
+
+ def setup_connections(self):
+ """设置信号连接"""
+ self.calendar.clicked.connect(self.on_date_selected)
+ self.today_btn.clicked.connect(self.on_today_clicked)
+ self.clear_btn.clicked.connect(self.on_clear_clicked)
+ self.close_btn.clicked.connect(self.close_window)
+ self.insert_btn.clicked.connect(self.on_insert_clicked)
+
+ def setup_theme(self):
+ """设置主题"""
+ # 连接主题切换信号
+ theme_manager.theme_changed.connect(self.on_theme_changed)
+
+ # 应用当前主题
+ self.apply_theme()
+
+ def apply_theme(self):
+ """应用主题样式"""
+ is_dark = theme_manager.is_dark_theme()
+ colors = theme_manager.get_current_theme_colors()
+
+ if is_dark:
+ # 深色主题样式
+ self.main_frame.setStyleSheet(f"""
+ QFrame#mainFrame {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 10px;
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#dateLabel {{
+ color: {colors['text_secondary']};
+ font-size: 11px;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(255, 255, 255, 0.1);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#todayButton, QPushButton#clearButton, QPushButton#insertButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#todayButton:hover, QPushButton#clearButton:hover, QPushButton#insertButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ """)
+
+ # 更新日历控件样式
+ self.calendar.setStyleSheet(f"""
+ QCalendarWidget {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ }}
+ QCalendarWidget QToolButton {{
+ height: 30px;
+ width: 80px;
+ color: {colors['text']};
+ font-size: 12px;
+ font-weight: bold;
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ }}
+ QCalendarWidget QToolButton:hover {{
+ background-color: {colors['surface_hover']};
+ }}
+ QCalendarWidget QMenu {{
+ width: 150px;
+ left: 20px;
+ color: {colors['text']};
+ font-size: 12px;
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ }}
+ QCalendarWidget QSpinBox {{
+ width: 80px;
+ font-size: 12px;
+ background-color: {colors['surface']};
+ selection-background-color: {colors['accent']};
+ selection-color: white;
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ color: {colors['text']};
+ }}
+ QCalendarWidget QSpinBox::up-button {{
+ subcontrol-origin: border;
+ subcontrol-position: top right;
+ width: 20px;
+ }}
+ QCalendarWidget QSpinBox::down-button {{
+ subcontrol-origin: border;
+ subcontrol-position: bottom right;
+ width: 20px;
+ }}
+ QCalendarWidget QSpinBox::up-arrow {{
+ width: 10px;
+ height: 10px;
+ }}
+ QCalendarWidget QSpinBox::down-arrow {{
+ width: 10px;
+ height: 10px;
+ }}
+ QCalendarWidget QWidget {{
+ alternate-background-color: {colors['surface']};
+ }}
+ QCalendarWidget QAbstractItemView:enabled {{
+ font-size: 12px;
+ selection-background-color: {colors['accent']};
+ selection-color: white;
+ background-color: {colors['surface']};
+ color: {colors['text']};
+ }}
+ QCalendarWidget QWidget#qt_calendar_navigationbar {{
+ background-color: {colors['surface']};
+ }}
+ """)
+ else:
+ # 浅色主题样式
+ self.main_frame.setStyleSheet(f"""
+ QFrame#mainFrame {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#dateLabel {{
+ color: {colors['text_secondary']};
+ font-size: 11px;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(0, 0, 0, 0.05);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#todayButton, QPushButton#clearButton, QPushButton#insertButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#todayButton:hover, QPushButton#clearButton:hover, QPushButton#insertButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ """)
+
+ # 更新日历控件样式
+ self.calendar.setStyleSheet(f"""
+ QCalendarWidget {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ }}
+ QCalendarWidget QToolButton {{
+ height: 30px;
+ width: 80px;
+ color: {colors['text']};
+ font-size: 12px;
+ font-weight: bold;
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ }}
+ QCalendarWidget QToolButton:hover {{
+ background-color: {colors['surface_hover']};
+ }}
+ QCalendarWidget QMenu {{
+ width: 150px;
+ left: 20px;
+ color: {colors['text']};
+ font-size: 12px;
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ }}
+ QCalendarWidget QSpinBox {{
+ width: 80px;
+ font-size: 12px;
+ background-color: {colors['surface']};
+ selection-background-color: {colors['accent']};
+ selection-color: white;
+ border: 1px solid {colors['border']};
+ border-radius: 4px;
+ color: {colors['text']};
+ }}
+ QCalendarWidget QSpinBox::up-button {{
+ subcontrol-origin: border;
+ subcontrol-position: top right;
+ width: 20px;
+ }}
+ QCalendarWidget QSpinBox::down-button {{
+ subcontrol-origin: border;
+ subcontrol-position: bottom right;
+ width: 20px;
+ }}
+ QCalendarWidget QSpinBox::up-arrow {{
+ width: 10px;
+ height: 10px;
+ }}
+ QCalendarWidget QSpinBox::down-arrow {{
+ width: 10px;
+ height: 10px;
+ }}
+ QCalendarWidget QWidget {{
+ alternate-background-color: {colors['surface']};
+ }}
+ QCalendarWidget QAbstractItemView:enabled {{
+ font-size: 12px;
+ selection-background-color: {colors['accent']};
+ selection-color: white;
+ background-color: {colors['surface']};
+ color: {colors['text']};
+ }}
+ QCalendarWidget QWidget#qt_calendar_navigationbar {{
+ background-color: {colors['surface']};
+ }}
+ """)
+
+ def on_theme_changed(self, is_dark):
+ """主题切换槽函数"""
+ self.apply_theme()
+
+ def mousePressEvent(self, event):
+ """鼠标按下事件,用于拖拽"""
+ if event.button() == Qt.LeftButton:
+ # 检查是否点击在标题栏区域
+ if event.pos().y() <= 40: # 假设标题栏高度为40像素
+ self.is_dragging = True
+ self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
+ event.accept()
+
+ def mouseMoveEvent(self, event):
+ """鼠标移动事件,用于拖拽"""
+ if self.is_dragging and event.buttons() == Qt.LeftButton:
+ self.move(event.globalPos() - self.drag_position)
+ event.accept()
+
+ def mouseReleaseEvent(self, event):
+ """鼠标释放事件"""
+ self.is_dragging = False
+
+ def on_date_selected(self, date):
+ """日期选择事件"""
+ self.update_date_label(date)
+
+ def on_today_clicked(self):
+ """今天按钮点击事件"""
+ today = QDate.currentDate()
+ self.calendar.setSelectedDate(today)
+ self.update_date_label(today)
+
+ def on_clear_clicked(self):
+ """清除按钮点击事件"""
+ self.calendar.setSelectedDate(QDate())
+ self.date_label.setText("未选择日期")
+
+ def on_insert_clicked(self):
+ """插入按钮点击事件"""
+ selected_date = self.calendar.selectedDate()
+ if selected_date.isValid():
+ # 发送信号,将选中的日期传递给主窗口
+ date_str = selected_date.toString("yyyy年MM月dd日 dddd")
+ self.date_selected.emit(date_str)
+
+ def update_date_label(self, date=None):
+ """更新日期显示标签"""
+ if date is None:
+ date = self.calendar.selectedDate()
+
+ if date.isValid():
+ date_str = date.toString("yyyy年MM月dd日 (ddd)")
+ self.date_label.setText(f"选中日期: {date_str}")
+ else:
+ self.date_label.setText("未选择日期")
+
+ def get_selected_date(self):
+ """获取选中的日期"""
+ return self.calendar.selectedDate()
+
+ def set_selected_date(self, date):
+ """设置选中的日期"""
+ if isinstance(date, str):
+ date = QDate.fromString(date, "yyyy-MM-dd")
+ self.calendar.setSelectedDate(date)
+ self.update_date_label(date)
+
+ def close_window(self):
+ """关闭窗口 - 只是隐藏而不是销毁"""
+ try:
+ self.closed.emit()
+ self.hide() # 隐藏窗口而不是销毁
+ except Exception as e:
+ print(f"Error in close_window: {e}")
+
+
+def main():
+ """测试函数"""
+ from PyQt5.QtWidgets import QApplication
+
+ app = QApplication(sys.argv)
+
+ # 创建并显示窗口
+ widget = CalendarFloatingWidget()
+ widget.show()
+
+ # 移动到屏幕中心
+ screen_geometry = app.desktop().screenGeometry()
+ widget.move(
+ (screen_geometry.width() - widget.width()) // 2,
+ (screen_geometry.height() - widget.height()) // 2
+ )
+
+ sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/src/ui/calendar_widget.py b/src/ui/calendar_widget.py
new file mode 100644
index 0000000..5331faf
--- /dev/null
+++ b/src/ui/calendar_widget.py
@@ -0,0 +1,573 @@
+# -*- coding: utf-8 -*-
+
+"""
+日历组件模块
+提供一个可嵌入的日历控件,用于在应用程序中显示和选择日期
+"""
+
+import sys
+from PyQt5.QtWidgets import (
+ QWidget, QCalendarWidget, QVBoxLayout, QHBoxLayout,
+ QPushButton, QLabel, QFrame
+)
+from PyQt5.QtCore import QDate, Qt, pyqtSignal
+from PyQt5.QtGui import QFont
+
+# 导入主题管理器
+from .theme_manager import theme_manager
+
+
+class CalendarWidget(QWidget):
+ """日历组件类"""
+
+ # 自定义信号
+ date_selected = pyqtSignal(str) # 日期字符串信号,用于插入功能
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setup_ui()
+ self.setup_connections()
+ self.setup_theme()
+
+ def setup_ui(self):
+ """设置UI界面"""
+ # 设置窗口属性
+ self.setWindowTitle("日历")
+ # 不再固定宽度,让其可以调整大小
+
+ # 设置白色背景
+ self.setStyleSheet("background-color: white;")
+
+ # 主布局
+ main_layout = QVBoxLayout()
+ main_layout.setContentsMargins(10, 10, 10, 10)
+ main_layout.setSpacing(10)
+
+ # 标题栏
+ title_layout = QHBoxLayout()
+ title_label = QLabel("日历")
+ title_label.setFont(QFont("Arial", 12, QFont.Bold))
+ title_layout.addWidget(title_label)
+ title_layout.addStretch()
+
+ # 关闭按钮
+ self.close_btn = QPushButton("×")
+ self.close_btn.setFixedSize(25, 25)
+ self.close_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #f0f0f0;
+ border: 1px solid #ccc;
+ border-radius: 12px;
+ font-size: 16px;
+ font-weight: bold;
+ }
+ QPushButton:hover {
+ background-color: #e0e0e0;
+ }
+ """)
+ title_layout.addWidget(self.close_btn)
+
+ main_layout.addLayout(title_layout)
+
+ # 分隔线
+ separator = QFrame()
+ separator.setFrameShape(QFrame.HLine)
+ separator.setFrameShadow(QFrame.Sunken)
+ main_layout.addWidget(separator)
+
+ # 日历控件
+ self.calendar = QCalendarWidget()
+ self.calendar.setGridVisible(True)
+ self.calendar.setVerticalHeaderFormat(QCalendarWidget.NoVerticalHeader)
+ self.calendar.setNavigationBarVisible(True)
+
+ # 设置样式
+ self.calendar.setStyleSheet("""
+ QCalendarWidget {
+ background-color: white;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ }
+ QCalendarWidget QToolButton {
+ height: 30px;
+ width: 80px;
+ color: #333;
+ font-size: 12px;
+ font-weight: bold;
+ background-color: #f0f0f0;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ }
+ QCalendarWidget QToolButton:hover {
+ background-color: #e0e0e0;
+ }
+ QCalendarWidget QMenu {
+ width: 150px;
+ left: 20px;
+ color: white;
+ font-size: 12px;
+ background-color: rgb(64, 64, 64);
+ }
+ QCalendarWidget QSpinBox {
+ width: 80px;
+ font-size: 12px;
+ background-color: #f0f0f0;
+ selection-background-color: rgb(64, 64, 64);
+ selection-color: white;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ }
+ QCalendarWidget QSpinBox::up-button {
+ subcontrol-origin: border;
+ subcontrol-position: top right;
+ width: 20px;
+ }
+ QCalendarWidget QSpinBox::down-button {
+ subcontrol-origin: border;
+ subcontrol-position: bottom right;
+ width: 20px;
+ }
+ QCalendarWidget QSpinBox::up-arrow {
+ width: 10px;
+ height: 10px;
+ }
+ QCalendarWidget QSpinBox::down-arrow {
+ width: 10px;
+ height: 10px;
+ }
+ QCalendarWidget QWidget {
+ alternate-background-color: #f0f0f0;
+ }
+ QCalendarWidget QAbstractItemView:enabled {
+ font-size: 12px;
+ selection-background-color: #0078d7;
+ selection-color: white;
+ }
+ QCalendarWidget QWidget#qt_calendar_navigationbar {
+ background-color: #f8f8f8;
+ }
+ """)
+
+ main_layout.addWidget(self.calendar)
+
+ # 当前日期显示
+ self.date_label = QLabel()
+ self.date_label.setAlignment(Qt.AlignCenter)
+ self.date_label.setFont(QFont("Arial", 10))
+ self.date_label.setStyleSheet("QLabel { color: #666; }")
+ self.update_date_label()
+
+ main_layout.addWidget(self.date_label)
+
+ # 操作按钮
+ button_layout = QHBoxLayout()
+
+ self.today_btn = QPushButton("今天")
+ self.today_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #0078d7;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 10px;
+ }
+ QPushButton:hover {
+ background-color: #005a9e;
+ }
+ """)
+
+ self.clear_btn = QPushButton("清除")
+ self.clear_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #f0f0f0;
+ color: #333;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 5px 10px;
+ }
+ QPushButton:hover {
+ background-color: #e0e0e0;
+ }
+ """)
+
+ # 插入按钮
+ self.insert_btn = QPushButton("插入")
+ self.insert_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 10px;
+ }
+ QPushButton:hover {
+ background-color: #45a049;
+ }
+ """)
+
+ button_layout.addWidget(self.today_btn)
+ button_layout.addWidget(self.insert_btn)
+ button_layout.addStretch()
+ button_layout.addWidget(self.clear_btn)
+
+ main_layout.addLayout(button_layout)
+
+ self.setLayout(main_layout)
+
+ def setup_connections(self):
+ """设置信号连接"""
+ self.calendar.clicked.connect(self.on_date_selected)
+ self.today_btn.clicked.connect(self.on_today_clicked)
+ self.clear_btn.clicked.connect(self.on_clear_clicked)
+ self.close_btn.clicked.connect(self.on_close_clicked)
+ self.insert_btn.clicked.connect(self.on_insert_clicked)
+
+ def on_date_selected(self, date):
+ """日期选择事件"""
+ self.update_date_label(date)
+
+ def on_today_clicked(self):
+ """今天按钮点击事件"""
+ today = QDate.currentDate()
+ self.calendar.setSelectedDate(today)
+ self.update_date_label(today)
+
+ def on_clear_clicked(self):
+ """清除按钮点击事件"""
+ self.calendar.setSelectedDate(QDate())
+ self.date_label.setText("未选择日期")
+
+ def on_close_clicked(self):
+ """关闭按钮点击事件"""
+ self.hide()
+
+ def on_insert_clicked(self):
+ """插入按钮点击事件"""
+ selected_date = self.calendar.selectedDate()
+ if selected_date.isValid():
+ # 发送信号,将选中的日期传递给主窗口
+ date_str = selected_date.toString("yyyy年MM月dd日 dddd")
+ self.date_selected.emit(date_str)
+
+ def update_date_label(self, date=None):
+ """更新日期显示标签"""
+ if date is None:
+ date = self.calendar.selectedDate()
+
+ if date.isValid():
+ date_str = date.toString("yyyy年MM月dd日 (ddd)")
+ self.date_label.setText(f"选中日期: {date_str}")
+ else:
+ self.date_label.setText("未选择日期")
+
+ def get_selected_date(self):
+ """获取选中的日期"""
+ return self.calendar.selectedDate()
+
+ def set_selected_date(self, date):
+ """设置选中的日期"""
+ if isinstance(date, str):
+ date = QDate.fromString(date, "yyyy-MM-dd")
+ self.calendar.setSelectedDate(date)
+ self.update_date_label(date)
+
+ def setup_theme(self):
+ """设置主题"""
+ # 连接主题切换信号
+ theme_manager.theme_changed.connect(self.on_theme_changed)
+
+ # 应用当前主题
+ self.apply_theme()
+
+ def apply_theme(self):
+ """应用主题样式"""
+ is_dark = theme_manager.is_dark_theme()
+
+ if is_dark:
+ # 深色主题样式
+ self.setStyleSheet("""
+ QWidget {
+ background-color: #2c2c2e;
+ color: #f0f0f0;
+ }
+ """)
+
+ # 更新日历控件样式
+ self.calendar.setStyleSheet("""
+ QCalendarWidget {
+ background-color: #2c2c2e;
+ border: 1px solid #404040;
+ border-radius: 4px;
+ }
+ QCalendarWidget QToolButton {
+ height: 30px;
+ width: 80px;
+ color: #f0f0f0;
+ font-size: 12px;
+ font-weight: bold;
+ background-color: #3a3a3c;
+ border: 1px solid #4a4a4c;
+ border-radius: 4px;
+ }
+ QCalendarWidget QToolButton:hover {
+ background-color: #4a4a4c;
+ }
+ QCalendarWidget QMenu {
+ width: 150px;
+ left: 20px;
+ color: #f0f0f0;
+ font-size: 12px;
+ background-color: #3a3a3c;
+ border: 1px solid #4a4a4c;
+ }
+ QCalendarWidget QSpinBox {
+ width: 80px;
+ font-size: 12px;
+ background-color: #3a3a3c;
+ selection-background-color: #0a84ff;
+ selection-color: #ffffff;
+ border: 1px solid #4a4a4c;
+ border-radius: 4px;
+ color: #f0f0f0;
+ }
+ QCalendarWidget QSpinBox::up-button {
+ subcontrol-origin: border;
+ subcontrol-position: top right;
+ width: 20px;
+ }
+ QCalendarWidget QSpinBox::down-button {
+ subcontrol-origin: border;
+ subcontrol-position: bottom right;
+ width: 20px;
+ }
+ QCalendarWidget QSpinBox::up-arrow {
+ width: 10px;
+ height: 10px;
+ }
+ QCalendarWidget QSpinBox::down-arrow {
+ width: 10px;
+ height: 10px;
+ }
+ QCalendarWidget QWidget {
+ alternate-background-color: #3a3a3c;
+ }
+ QCalendarWidget QAbstractItemView:enabled {
+ font-size: 12px;
+ selection-background-color: #0a84ff;
+ selection-color: #ffffff;
+ background-color: #2c2c2e;
+ color: #f0f0f0;
+ }
+ QCalendarWidget QWidget#qt_calendar_navigationbar {
+ background-color: #3a3a3c;
+ }
+ """)
+
+ # 更新标签样式
+ self.date_label.setStyleSheet("QLabel { color: #a0a0a0; }")
+
+ # 更新按钮样式
+ self.close_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #3a3a3c;
+ border: 1px solid #4a4a4c;
+ border-radius: 12px;
+ font-size: 16px;
+ font-weight: bold;
+ color: #f0f0f0;
+ }
+ QPushButton:hover {
+ background-color: #4a4a4c;
+ }
+ """)
+
+ self.today_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #0a84ff;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 10px;
+ }
+ QPushButton:hover {
+ background-color: #0066cc;
+ }
+ """)
+
+ self.clear_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #3a3a3c;
+ color: #f0f0f0;
+ border: 1px solid #4a4a4c;
+ border-radius: 4px;
+ padding: 5px 10px;
+ }
+ QPushButton:hover {
+ background-color: #4a4a4c;
+ }
+ """)
+
+ self.insert_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #32d74b;
+ color: #000000;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 10px;
+ }
+ QPushButton:hover {
+ background-color: #24b334;
+ }
+ """)
+ else:
+ # 浅色主题样式
+ self.setStyleSheet("""
+ QWidget {
+ background-color: white;
+ color: #333333;
+ }
+ """)
+
+ # 更新日历控件样式
+ self.calendar.setStyleSheet("""
+ QCalendarWidget {
+ background-color: white;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ }
+ QCalendarWidget QToolButton {
+ height: 30px;
+ width: 80px;
+ color: #333;
+ font-size: 12px;
+ font-weight: bold;
+ background-color: #f0f0f0;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ }
+ QCalendarWidget QToolButton:hover {
+ background-color: #e0e0e0;
+ }
+ QCalendarWidget QMenu {
+ width: 150px;
+ left: 20px;
+ color: #333;
+ font-size: 12px;
+ background-color: white;
+ border: 1px solid #ccc;
+ }
+ QCalendarWidget QSpinBox {
+ width: 80px;
+ font-size: 12px;
+ background-color: #f0f0f0;
+ selection-background-color: #0078d7;
+ selection-color: white;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ color: #333;
+ }
+ QCalendarWidget QSpinBox::up-button {
+ subcontrol-origin: border;
+ subcontrol-position: top right;
+ width: 20px;
+ }
+ QCalendarWidget QSpinBox::down-button {
+ subcontrol-origin: border;
+ subcontrol-position: bottom right;
+ width: 20px;
+ }
+ QCalendarWidget QSpinBox::up-arrow {
+ width: 10px;
+ height: 10px;
+ }
+ QCalendarWidget QSpinBox::down-arrow {
+ width: 10px;
+ height: 10px;
+ }
+ QCalendarWidget QWidget {
+ alternate-background-color: #f0f0f0;
+ }
+ QCalendarWidget QAbstractItemView:enabled {
+ font-size: 12px;
+ selection-background-color: #0078d7;
+ selection-color: white;
+ background-color: white;
+ color: #333;
+ }
+ QCalendarWidget QWidget#qt_calendar_navigationbar {
+ background-color: #f8f8f8;
+ }
+ """)
+
+ # 更新标签样式
+ self.date_label.setStyleSheet("QLabel { color: #666; }")
+
+ # 更新按钮样式
+ self.close_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #f0f0f0;
+ border: 1px solid #ccc;
+ border-radius: 12px;
+ font-size: 16px;
+ font-weight: bold;
+ color: #333;
+ }
+ QPushButton:hover {
+ background-color: #e0e0e0;
+ }
+ """)
+
+ self.today_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #0078d7;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 10px;
+ }
+ QPushButton:hover {
+ background-color: #005a9e;
+ }
+ """)
+
+ self.clear_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #f0f0f0;
+ color: #333;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ padding: 5px 10px;
+ }
+ QPushButton:hover {
+ background-color: #e0e0e0;
+ }
+ """)
+
+ self.insert_btn.setStyleSheet("""
+ QPushButton {
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 5px 10px;
+ }
+ QPushButton:hover {
+ background-color: #45a049;
+ }
+ """)
+
+ def on_theme_changed(self, is_dark):
+ """主题切换槽函数"""
+ self.apply_theme()
+
+
+if __name__ == "__main__":
+ from PyQt5.QtWidgets import QApplication
+
+ app = QApplication(sys.argv)
+
+ # 创建并显示日历组件
+ calendar = CalendarWidget()
+ calendar.show()
+
+ sys.exit(app.exec_())
\ No newline at end of file
diff --git a/src/ui/quote_floating_widget.py b/src/ui/quote_floating_widget.py
new file mode 100644
index 0000000..ad37bfe
--- /dev/null
+++ b/src/ui/quote_floating_widget.py
@@ -0,0 +1,439 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+每日谏言悬浮窗口
+"""
+
+import sys
+from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,
+ QLabel, QPushButton, QFrame, QGraphicsDropShadowEffect)
+from PyQt5.QtCore import Qt, QPoint, pyqtSignal
+from PyQt5.QtGui import QFont, QColor
+
+
+class QuoteFloatingWidget(QWidget):
+ """每日谏言悬浮窗口"""
+
+ # 定义信号
+ closed = pyqtSignal() # 窗口关闭信号
+ refresh_requested = pyqtSignal() # 刷新请求信号
+ insert_requested = pyqtSignal(str) # 插入请求信号,传递要插入的文本
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
+ self.setAttribute(Qt.WA_TranslucentBackground)
+
+ # 初始化变量
+ self.is_dragging = False
+ self.drag_position = QPoint()
+
+ # 设置默认谏言数据
+ self.quote_data = {
+ "quote": "书山有路勤为径,学海无涯苦作舟。",
+ "author": "韩愈",
+ "source": "《古今贤文·劝学篇》"
+ }
+
+ # 初始化UI
+ self.init_ui()
+ self.setup_styles()
+ self.apply_theme(is_dark=True) # 默认使用深色主题
+
+ def init_ui(self):
+ """初始化UI"""
+ # 主框架
+ self.main_frame = QFrame(self)
+ self.main_frame.setObjectName("mainFrame")
+ self.main_frame.setFixedSize(360, 200) # 设置窗口大小
+
+ main_layout = QVBoxLayout(self.main_frame)
+ main_layout.setContentsMargins(12, 12, 12, 12)
+ main_layout.setSpacing(8)
+
+ # 标题栏
+ title_layout = QHBoxLayout()
+ title_layout.setContentsMargins(0, 0, 0, 0)
+ title_layout.setSpacing(0)
+
+ self.title_label = QLabel("每日谏言")
+ self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
+ title_layout.addWidget(self.title_label)
+ title_layout.addStretch()
+ # 添加一个小的固定空间,使关闭按钮向左移动
+ title_layout.addSpacing(25) # 向左移动25个单位
+
+ # 关闭按钮
+ self.close_btn = QPushButton("×")
+ self.close_btn.setFixedSize(20, 20)
+ self.close_btn.setObjectName("closeButton")
+ self.close_btn.clicked.connect(self.close_window)
+ title_layout.addWidget(self.close_btn)
+
+ main_layout.addLayout(title_layout)
+
+ # 分隔线
+ separator = QFrame()
+ separator.setObjectName("separator")
+ separator.setFixedHeight(1)
+ main_layout.addWidget(separator)
+
+ # 谏言内容区域
+ content_layout = QVBoxLayout()
+ content_layout.setContentsMargins(0, 0, 0, 0)
+ content_layout.setSpacing(6)
+
+ # 谏言文本
+ self.quote_label = QLabel()
+ self.quote_label.setObjectName("quoteLabel")
+ self.quote_label.setWordWrap(True)
+ self.quote_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
+ content_layout.addWidget(self.quote_label)
+
+ # 作者信息
+ self.author_label = QLabel()
+ self.author_label.setObjectName("authorLabel")
+ self.author_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
+ content_layout.addWidget(self.author_label)
+
+ main_layout.addLayout(content_layout)
+ main_layout.addStretch()
+
+ # 底部按钮区域
+ bottom_layout = QHBoxLayout()
+ bottom_layout.setContentsMargins(0, 0, 0, 0)
+ bottom_layout.setSpacing(8)
+
+ # 刷新按钮
+ self.refresh_btn = QPushButton("换一句")
+ self.refresh_btn.setObjectName("refreshButton")
+ self.refresh_btn.clicked.connect(self.on_refresh_clicked)
+ bottom_layout.addWidget(self.refresh_btn)
+
+ bottom_layout.addStretch()
+
+ # 插入按钮
+ self.insert_btn = QPushButton("插入")
+ self.insert_btn.setObjectName("insertButton")
+ self.insert_btn.clicked.connect(self.on_insert_clicked)
+ bottom_layout.addWidget(self.insert_btn)
+
+ main_layout.addLayout(bottom_layout)
+
+ # 设置主布局
+ outer_layout = QVBoxLayout(self)
+ outer_layout.setContentsMargins(0, 0, 0, 0)
+ outer_layout.addWidget(self.main_frame)
+
+ # 更新显示
+ self.update_quote()
+
+ def setup_styles(self):
+ """设置样式"""
+ pass # 样式将在apply_theme中设置
+
+ def apply_theme(self, is_dark=True):
+ """应用主题"""
+ if is_dark:
+ # 深色主题配色
+ colors = {
+ 'surface': '#2d2d2d',
+ 'border': '#444444',
+ 'text': '#ffffff',
+ 'text_secondary': '#cccccc',
+ 'accent': '#4CAF50',
+ 'accent_hover': '#45a049',
+ 'button_hover': '#555555',
+ 'error': '#f44336'
+ }
+
+ self.main_frame.setStyleSheet(f"""
+ QFrame#mainFrame {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#quoteLabel {{
+ color: {colors['text']};
+ font-size: 14px;
+ font-weight: 500;
+ padding: 6px 8px;
+ margin: 3px;
+ }}
+ QLabel#authorLabel {{
+ color: {colors['text_secondary']};
+ font-size: 12px;
+ font-style: italic;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(255, 255, 255, 0.1);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#refreshButton, QPushButton#insertButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ """)
+ else:
+ # 浅色主题配色
+ colors = {
+ 'surface': '#ffffff',
+ 'border': '#dddddd',
+ 'text': '#333333',
+ 'text_secondary': '#666666',
+ 'accent': '#4CAF50',
+ 'accent_hover': '#45a049',
+ 'button_hover': '#f0f0f0',
+ 'error': '#f44336'
+ }
+
+ self.main_frame.setStyleSheet(f"""
+ QFrame#mainFrame {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#quoteLabel {{
+ color: {colors['text']};
+ font-size: 14px;
+ font-weight: 500;
+ padding: 6px 8px;
+ margin: 3px;
+ }}
+ QLabel#authorLabel {{
+ color: {colors['text_secondary']};
+ font-size: 12px;
+ font-style: italic;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(0, 0, 0, 0.05);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#refreshButton, QPushButton#insertButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#refreshButton:hover, QPushButton#insertButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ """)
+
+ def update_quote(self, quote_data=None):
+ """更新谏言显示"""
+ if quote_data:
+ self.quote_data = quote_data
+ else:
+ # 如果没有提供数据,使用默认数据
+ if not hasattr(self, 'quote_data'):
+ self.quote_data = {
+ "quote": "书山有路勤为径,学海无涯苦作舟。",
+ "author": "韩愈",
+ "source": "《古今贤文·劝学篇》"
+ }
+
+ # 更新显示
+ self.quote_label.setText(self.quote_data["quote"])
+ author_info = f"— {self.quote_data['author']}"
+ if self.quote_data.get("source"):
+ author_info += f" 《{self.quote_data['source']}》"
+ self.author_label.setText(author_info)
+
+ def on_refresh_clicked(self):
+ """刷新按钮点击事件"""
+ # 发送刷新请求信号
+ self.refresh_requested.emit()
+ # 同时直接获取新的内容并更新显示
+ self.fetch_and_update_quote()
+
+ def on_insert_clicked(self):
+ """插入按钮点击事件"""
+ # 发送插入请求信号,传递完整的诗句信息
+ quote = self.quote_data.get("quote", "")
+ author = self.quote_data.get("author", "佚名")
+ source = self.quote_data.get("source", "")
+
+ # 构造完整的诗句文本
+ if source:
+ full_quote_text = f"{quote} —— {author}《{source}》"
+ else:
+ full_quote_text = f"{quote} —— {author}"
+
+ if quote:
+ self.insert_requested.emit(full_quote_text)
+
+ def fetch_and_update_quote(self):
+ """获取新的谏言内容并更新显示"""
+ try:
+ # 尝试获取古诗词
+ import requests
+ import random
+
+ try:
+ # 使用古诗词·一言API
+ response = requests.get("https://v1.jinrishici.com/all.json", timeout=5, verify=False)
+ if response.status_code == 200:
+ data = response.json()
+ content = data.get('content', '')
+ author = data.get('author', '佚名')
+ origin = data.get('origin', '')
+
+ if content:
+ quote_data = {
+ "quote": content,
+ "author": author,
+ "source": origin
+ }
+ self.update_quote(quote_data)
+ return
+ except Exception as e:
+ print(f"获取古诗词失败: {e}")
+
+ # 如果古诗词获取失败,使用备用API
+ try:
+ # 使用每日一言API
+ response = requests.get("https://api.nxvav.cn/api/yiyan?json=true", timeout=5, verify=False)
+ if response.status_code == 200:
+ data = response.json()
+ yiyan = data.get('yiyan', '')
+ nick = data.get('nick', '佚名')
+
+ if yiyan:
+ quote_data = {
+ "quote": yiyan,
+ "author": nick,
+ "source": ""
+ }
+ self.update_quote(quote_data)
+ return
+ except Exception as e:
+ print(f"获取每日一言失败: {e}")
+
+ # 如果API都失败,使用预设内容
+ quotes = [
+ {"quote": "学而时习之,不亦说乎?", "author": "孔子", "source": "《论语》"},
+ {"quote": "千里之行,始于足下。", "author": "老子", "source": "《道德经》"},
+ {"quote": "天行健,君子以自强不息。", "author": "佚名", "source": "《周易》"},
+ {"quote": "书山有路勤为径,学海无涯苦作舟。", "author": "韩愈", "source": "《古今贤文·劝学篇》"},
+ {"quote": "山重水复疑无路,柳暗花明又一村。", "author": "陆游", "source": "《游山西村》"}
+ ]
+
+ # 随机选择一个名言
+ new_quote = random.choice(quotes)
+ self.update_quote(new_quote)
+
+ except Exception as e:
+ print(f"获取新谏言失败: {e}")
+ # 出错时显示默认内容
+ default_quote = {
+ "quote": "书山有路勤为径,学海无涯苦作舟。",
+ "author": "韩愈",
+ "source": "《古今贤文·劝学篇》"
+ }
+ self.update_quote(default_quote)
+
+ def close_window(self):
+ """关闭窗口 - 只是隐藏而不是销毁"""
+ try:
+ self.closed.emit()
+ self.hide() # 隐藏窗口而不是销毁
+ except Exception as e:
+ print(f"Error in close_window: {e}")
+
+ def mousePressEvent(self, event):
+ """鼠标按下事件,用于拖拽"""
+ if event.button() == Qt.LeftButton:
+ # 检查是否点击在标题栏区域
+ if event.pos().y() <= 40: # 假设标题栏高度为40像素
+ self.is_dragging = True
+ self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
+ event.accept()
+
+ def mouseMoveEvent(self, event):
+ """鼠标移动事件,用于拖拽"""
+ if self.is_dragging and event.buttons() == Qt.LeftButton:
+ self.move(event.globalPos() - self.drag_position)
+ event.accept()
+
+ def mouseReleaseEvent(self, event):
+ """鼠标释放事件"""
+ self.is_dragging = False
+
+
+def main():
+ """测试函数"""
+ app = QApplication(sys.argv)
+
+ # 创建并显示窗口
+ widget = QuoteFloatingWidget()
+ widget.show()
+
+ # 移动到屏幕中心
+ screen_geometry = app.desktop().screenGeometry()
+ widget.move(
+ (screen_geometry.width() - widget.width()) // 2,
+ (screen_geometry.height() - widget.height()) // 2
+ )
+
+ sys.exit(app.exec_())
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/src/ui/snake_game.py b/src/ui/snake_game.py
new file mode 100644
index 0000000..6c8dd5f
--- /dev/null
+++ b/src/ui/snake_game.py
@@ -0,0 +1,437 @@
+# snake_game.py
+"""
+贪吃蛇小游戏模块
+用户用WASD或方向键控制贪吃蛇移动
+"""
+
+import sys
+import random
+from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QLabel, QMessageBox
+from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QRect
+from PyQt5.QtGui import QPainter, QColor, QFont, QBrush, QPen
+
+
+class SnakeGame(QWidget):
+ """贪吃蛇游戏画布"""
+
+ # 游戏常量
+ GRID_SIZE = 20 # 网格大小
+ GRID_WIDTH = 30 # 水平网格数
+ GRID_HEIGHT = 20 # 垂直网格数
+ GAME_SPEED = 150 # 游戏速度(毫秒)
+ MIN_SPEED = 50 # 最小速度(毫秒)
+ MAX_SPEED = 300 # 最大速度(毫秒)
+ SPEED_STEP = 10 # 速度调节步长(毫秒)
+
+ # 信号
+ score_changed = pyqtSignal(int)
+ game_over = pyqtSignal(int)
+ speed_changed = pyqtSignal(int) # 速度改变信号
+
+ # 方向常量
+ UP = (0, -1)
+ DOWN = (0, 1)
+ LEFT = (-1, 0)
+ RIGHT = (1, 0)
+
+ def __init__(self):
+ super().__init__()
+ self.init_game()
+ self.init_ui()
+
+ def init_ui(self):
+ """初始化UI"""
+ self.setFixedSize(
+ self.GRID_WIDTH * self.GRID_SIZE,
+ self.GRID_HEIGHT * self.GRID_SIZE
+ )
+ self.setStyleSheet("background-color: #1a1a1a;")
+ self.setFocus() # 获取焦点以接收键盘输入
+
+ def init_game(self):
+ """初始化游戏状态"""
+ # 蛇的初始位置(从中间开始)
+ self.snake = [
+ (self.GRID_WIDTH // 2, self.GRID_HEIGHT // 2),
+ (self.GRID_WIDTH // 2 - 1, self.GRID_HEIGHT // 2),
+ (self.GRID_WIDTH // 2 - 2, self.GRID_HEIGHT // 2),
+ ]
+
+ # 方向
+ self.direction = self.RIGHT
+ self.next_direction = self.RIGHT
+
+ # 食物位置
+ self.food = self.generate_food()
+
+ # 分数
+ self.score = 0
+
+ # 游戏速度
+ self.current_speed = self.GAME_SPEED
+
+ # 游戏状态
+ self.is_running = False
+ self.is_game_over = False
+
+ # 游戏定时器
+ self.game_timer = QTimer()
+ self.game_timer.timeout.connect(self.update_game)
+
+ def generate_food(self):
+ """生成食物位置"""
+ while True:
+ x = random.randint(0, self.GRID_WIDTH - 1)
+ y = random.randint(0, self.GRID_HEIGHT - 1)
+ if (x, y) not in self.snake:
+ return (x, y)
+
+ def start_game(self):
+ """开始游戏"""
+ if not self.is_running:
+ self.is_running = True
+ self.is_game_over = False
+ self.game_timer.start(self.current_speed)
+ self.setFocus()
+
+ def pause_game(self):
+ """暂停游戏"""
+ if self.is_running:
+ self.is_running = False
+ self.game_timer.stop()
+
+ def resume_game(self):
+ """恢复游戏"""
+ if not self.is_running and not self.is_game_over:
+ self.is_running = True
+ self.game_timer.start(self.current_speed)
+
+ def restart_game(self):
+ """重新开始游戏"""
+ self.game_timer.stop()
+ self.init_game()
+ self.score_changed.emit(0)
+ self.speed_changed.emit(self.current_speed)
+ self.update()
+ # 重新启动游戏
+ self.start_game()
+
+ def increase_speed(self):
+ """增加游戏速度"""
+ if self.current_speed > self.MIN_SPEED:
+ self.current_speed = max(self.current_speed - self.SPEED_STEP, self.MIN_SPEED)
+ self.speed_changed.emit(self.current_speed)
+ if self.is_running:
+ self.game_timer.setInterval(self.current_speed)
+
+ def decrease_speed(self):
+ """降低游戏速度"""
+ if self.current_speed < self.MAX_SPEED:
+ self.current_speed = min(self.current_speed + self.SPEED_STEP, self.MAX_SPEED)
+ self.speed_changed.emit(self.current_speed)
+ if self.is_running:
+ self.game_timer.setInterval(self.current_speed)
+
+ def update_game(self):
+ """更新游戏状态"""
+ if not self.is_running:
+ return
+
+ # 更新方向
+ self.direction = self.next_direction
+
+ # 计算新的头部位置
+ head_x, head_y = self.snake[0]
+ dx, dy = self.direction
+ new_head = (head_x + dx, head_y + dy)
+
+ # 检查碰撞
+ if self.check_collision(new_head):
+ self.is_running = False
+ self.is_game_over = True
+ self.game_timer.stop()
+ self.game_over.emit(self.score)
+ self.update()
+ return
+
+ # 添加新的头部
+ self.snake.insert(0, new_head)
+
+ # 检查是否吃到食物
+ if new_head == self.food:
+ self.score += 10
+ self.score_changed.emit(self.score)
+ self.food = self.generate_food()
+ else:
+ # 移除尾部
+ self.snake.pop()
+
+ self.update()
+
+ def check_collision(self, position):
+ """检查碰撞"""
+ x, y = position
+
+ # 检查边界碰撞
+ if x < 0 or x >= self.GRID_WIDTH or y < 0 or y >= self.GRID_HEIGHT:
+ return True
+
+ # 检查自身碰撞
+ if position in self.snake:
+ return True
+
+ return False
+
+ def keyPressEvent(self, event):
+ """处理键盘输入"""
+ if event.isAutoRepeat():
+ return
+
+ key = event.key()
+
+ # 使用WASD或方向键控制方向
+ if key in (Qt.Key_W, Qt.Key_Up):
+ # 上键:向上
+ if self.direction != self.DOWN:
+ self.next_direction = self.UP
+ elif key in (Qt.Key_S, Qt.Key_Down):
+ # 下键:向下
+ if self.direction != self.UP:
+ self.next_direction = self.DOWN
+ elif key in (Qt.Key_A, Qt.Key_Left):
+ # 左键:向左(不用于调速)
+ if self.direction != self.RIGHT:
+ self.next_direction = self.LEFT
+ elif key in (Qt.Key_D, Qt.Key_Right):
+ # 右键:向右(不用于调速)
+ if self.direction != self.LEFT:
+ self.next_direction = self.RIGHT
+ elif key == Qt.Key_Space:
+ # 空格键暂停/恢复
+ if self.is_running:
+ self.pause_game()
+ elif not self.is_game_over:
+ self.resume_game()
+ elif key == Qt.Key_R:
+ # R键重新开始
+ if self.is_game_over:
+ self.restart_game()
+ elif key == Qt.Key_Plus or key == Qt.Key_Equal:
+ # + 或 = 键加速
+ self.increase_speed()
+ elif key == Qt.Key_Minus:
+ # - 键减速
+ self.decrease_speed()
+
+ event.accept()
+
+ def paintEvent(self, event):
+ """绘制游戏"""
+ painter = QPainter(self)
+ painter.setRenderHint(QPainter.Antialiasing)
+
+ # 绘制网格背景
+ self.draw_grid(painter)
+
+ # 绘制食物
+ self.draw_food(painter)
+
+ # 绘制蛇
+ self.draw_snake(painter)
+
+ # 如果游戏结束,显示游戏结束提示
+ if self.is_game_over:
+ self.draw_game_over(painter)
+
+ def draw_grid(self, painter):
+ """绘制网格"""
+ painter.setPen(QPen(QColor(50, 50, 50), 1))
+
+ # 绘制竖线
+ for x in range(self.GRID_WIDTH + 1):
+ painter.drawLine(
+ x * self.GRID_SIZE, 0,
+ x * self.GRID_SIZE, self.GRID_HEIGHT * self.GRID_SIZE
+ )
+
+ # 绘制横线
+ for y in range(self.GRID_HEIGHT + 1):
+ painter.drawLine(
+ 0, y * self.GRID_SIZE,
+ self.GRID_WIDTH * self.GRID_SIZE, y * self.GRID_SIZE
+ )
+
+ def draw_snake(self, painter):
+ """绘制蛇"""
+ # 绘制蛇身
+ for i, (x, y) in enumerate(self.snake):
+ if i == 0:
+ # 蛇头 - 更亮的绿色
+ painter.fillRect(
+ x * self.GRID_SIZE + 1,
+ y * self.GRID_SIZE + 1,
+ self.GRID_SIZE - 2,
+ self.GRID_SIZE - 2,
+ QColor(0, 255, 0)
+ )
+ # 绘制眼睛
+ painter.fillRect(
+ x * self.GRID_SIZE + 4,
+ y * self.GRID_SIZE + 4,
+ 3, 3,
+ QColor(255, 0, 0)
+ )
+ else:
+ # 蛇身 - 稍暗的绿色
+ painter.fillRect(
+ x * self.GRID_SIZE + 1,
+ y * self.GRID_SIZE + 1,
+ self.GRID_SIZE - 2,
+ self.GRID_SIZE - 2,
+ QColor(0, 200, 0)
+ )
+
+ def draw_food(self, painter):
+ """绘制食物"""
+ x, y = self.food
+ painter.fillRect(
+ x * self.GRID_SIZE + 3,
+ y * self.GRID_SIZE + 3,
+ self.GRID_SIZE - 6,
+ self.GRID_SIZE - 6,
+ QColor(255, 0, 0)
+ )
+
+ def draw_game_over(self, painter):
+ """绘制游戏结束界面"""
+ # 半透明黑色背景
+ painter.fillRect(self.rect(), QColor(0, 0, 0, 200))
+
+ # 绘制文本
+ painter.setPen(QColor(255, 255, 255))
+ font = QFont("Arial", 30, QFont.Bold)
+ painter.setFont(font)
+
+ text = "游戏结束"
+ fm = painter.fontMetrics()
+ text_width = fm.width(text)
+ text_height = fm.height()
+
+ x = (self.width() - text_width) // 2
+ y = (self.height() - text_height) // 2 - 20
+
+ painter.drawText(x, y, text)
+
+ # 绘制分数
+ font.setPointSize(20)
+ painter.setFont(font)
+ score_text = f"最终分数: {self.score}"
+ fm = painter.fontMetrics()
+ score_width = fm.width(score_text)
+ score_x = (self.width() - score_width) // 2
+ score_y = y + 50
+
+ painter.drawText(score_x, score_y, score_text)
+
+ # 绘制提示
+ font.setPointSize(12)
+ painter.setFont(font)
+ hint_text = "按R键重新开始"
+ fm = painter.fontMetrics()
+ hint_width = fm.width(hint_text)
+ hint_x = (self.width() - hint_width) // 2
+ hint_y = score_y + 40
+
+ painter.drawText(hint_x, hint_y, hint_text)
+
+
+class SnakeGameWindow(QMainWindow):
+ """贪吃蛇游戏窗口"""
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.setWindowTitle("贪吃蛇游戏")
+ self.setGeometry(200, 200, 700, 550)
+
+ # 创建中央控件
+ central_widget = QWidget()
+ self.setCentralWidget(central_widget)
+
+ # 创建布局
+ layout = QVBoxLayout(central_widget)
+ layout.setContentsMargins(10, 10, 10, 10)
+
+ # 创建游戏画布
+ self.game_widget = SnakeGame()
+ layout.addWidget(self.game_widget)
+
+ # 创建控制面板
+ control_layout = QVBoxLayout()
+
+ # 分数标签
+ self.score_label = QLabel("分数: 0")
+ self.score_label.setStyleSheet("font-size: 16px; font-weight: bold;")
+ control_layout.addWidget(self.score_label)
+
+ # 速度标签
+ self.speed_label = QLabel("速度: 正常")
+ self.speed_label.setStyleSheet("font-size: 14px; color: #0066cc;")
+ control_layout.addWidget(self.speed_label)
+
+ # 提示标签
+ self.hint_label = QLabel(
+ "控制方法: W/↑ 上 S/↓ 下 A/← 左 D/→ 右 | 空格暂停 | +/- 调速 | R重新开始"
+ )
+ self.hint_label.setStyleSheet("font-size: 12px; color: gray;")
+ control_layout.addWidget(self.hint_label)
+
+ layout.addLayout(control_layout)
+
+ # 连接信号
+ self.game_widget.score_changed.connect(self.update_score)
+ self.game_widget.game_over.connect(self.on_game_over)
+ self.game_widget.speed_changed.connect(self.update_speed)
+
+ # 设置窗口样式
+ self.setStyleSheet("""
+ QMainWindow {
+ background-color: #f0f0f0;
+ }
+ QLabel {
+ color: #333;
+ }
+ """)
+
+ # 启动游戏
+ self.game_widget.start_game()
+
+ def update_score(self, score):
+ """更新分数显示"""
+ self.score_label.setText(f"分数: {score}")
+
+ def update_speed(self, speed_ms):
+ """更新速度显示"""
+ # 将毫秒转换为速度等级
+ if speed_ms <= 50:
+ speed_level = "极快"
+ elif speed_ms <= 100:
+ speed_level = "很快"
+ elif speed_ms <= 150:
+ speed_level = "正常"
+ elif speed_ms <= 200:
+ speed_level = "稍慢"
+ else:
+ speed_level = "很慢"
+
+ self.speed_label.setText(f"速度: {speed_level} ({speed_ms}ms)")
+
+ def on_game_over(self, final_score):
+ """游戏结束处理"""
+ pass # 游戏结束信息已在游戏画布中显示
+
+ def keyPressEvent(self, event):
+ """处理键盘输入"""
+ if event.key() == Qt.Key_R:
+ self.game_widget.restart_game()
+ else:
+ super().keyPressEvent(event)
diff --git a/src/ui/theme_manager.py b/src/ui/theme_manager.py
index 6b42bb1..0978872 100644
--- a/src/ui/theme_manager.py
+++ b/src/ui/theme_manager.py
@@ -153,69 +153,99 @@ class ThemeManager(QObject):
return self._get_light_stylesheet()
def _get_dark_stylesheet(self):
- """深色主题样式表"""
+ """深色主题样式表 - Apple设计风格"""
return """
- /* 深色主题样式 */
+ /* Apple设计风格深色主题样式 */
- /* 全局文字颜色 */
+ /* 全局文字颜色和字体 - 使用Apple系统字体 */
QWidget {
- color: #e0e0e0;
+ color: #f0f0f0;
+ font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ font-size: 13px;
}
- /* 主窗口 */
+ /* 主窗口 - Apple深色背景 */
QMainWindow {
- background-color: #1e1e1e;
+ background-color: #2c2c2e;
}
- /* 菜单栏 */
+ /* 菜单栏 - Apple深色风格 */
QMenuBar {
- background-color: #0078d7;
- border: 1px solid #005a9e;
- font-size: 12px;
- color: #ffffff;
+ background-color: #2c2c2e;
+ border: none;
+ border-bottom: 1px solid #404040;
+ font-size: 13px;
+ color: #f0f0f0;
+ padding: 4px 0;
}
QMenuBar::item {
background-color: transparent;
- padding: 4px 10px;
- color: #e0e0e0;
+ padding: 6px 12px;
+ color: #f0f0f0;
+ border-radius: 4px;
+ margin: 0 1px;
}
QMenuBar::item:selected {
- background-color: #106ebe;
+ background-color: #404040;
+ color: #f0f0f0;
}
- /* 菜单 */
+ QMenuBar::item:pressed {
+ background-color: #505050;
+ color: #f0f0f0;
+ }
+
+ /* 菜单 - Apple深色风格 */
QMenu {
- background-color: #2d2d2d;
- border: 1px solid #3c3c3c;
- font-size: 12px;
- color: #e0e0e0;
+ background-color: #2c2c2e;
+ border: 1px solid #404040;
+ border-radius: 8px;
+ font-size: 13px;
+ color: #f0f0f0;
+ padding: 4px 0;
+ margin: 2px;
}
QMenu::item {
+ color: #f0f0f0;
+ background-color: transparent;
+ border-radius: 4px;
+ margin: 0 4px;
padding: 4px 20px;
- color: #e0e0e0;
}
QMenu::item:selected {
- background-color: #3c3c3c;
+ background-color: #0a84ff;
+ color: #ffffff;
+ }
+
+ QMenu::item:pressed {
+ background-color: #0066cc;
+ color: #ffffff;
+ }
+
+ QMenu::separator {
+ height: 1px;
+ background-color: #404040;
+ margin: 4px 8px;
}
/* 功能区 */
QFrame {
- background-color: #2d2d2d;
- border: 1px solid #3c3c3c;
+ background-color: #2c2c2e;
+ border: none;
}
/* 组框 */
QGroupBox {
- font-size: 11px;
+ font-size: 12px;
font-weight: normal;
- color: #e0e0e0;
- background-color: #2d2d2d;
- border: 1px solid #3c3c3c;
- border-radius: 0px;
+ color: #f0f0f0;
+ background-color: #2c2c2e;
+ border: none;
+ border-radius: 8px;
margin-top: 5px;
padding-top: 5px;
}
@@ -224,250 +254,319 @@ class ThemeManager(QObject):
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
- color: #e0e0e0;
+ color: #a0a0a0;
}
- /* 按钮 */
+ /* 工具按钮 - Apple深色风格 */
QToolButton {
- border: 1px solid #3c3c3c;
- border-radius: 3px;
- background-color: #3c3c3c;
- font-size: 11px;
- color: #e0e0e0;
- padding: 3px 6px;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ background-color: #3a3a3c;
+ font-size: 13px;
+ color: #f0f0f0;
+ padding: 6px 12px;
}
QToolButton:hover {
- background-color: #4a4a4a;
- border: 1px solid #5a5a5a;
+ background-color: #4a4a4c;
+ border: 1px solid #5a5a5c;
}
QToolButton:pressed {
- background-color: #2a2a2a;
- border: 1px solid #1a1a1a;
+ background-color: #5a5a5c;
+ border: 1px solid #6a6a6c;
}
QToolButton:checked {
- background-color: #0078d4;
- border: 1px solid #106ebe;
+ background-color: #0a84ff;
+ border: 1px solid #0a84ff;
+ color: #ffffff;
}
/* 切换按钮 */
QToolButton[checkable="true"] {
- border: 1px solid #3c3c3c;
- border-radius: 2px;
- background-color: #3c3c3c;
+ border: 1px solid #4a4a4c;
+ border-radius: 6px;
+ background-color: #3a3a3c;
font-size: 12px;
- font-weight: bold;
- color: #e0e0e0;
+ color: #f0f0f0;
+ padding: 6px 12px;
}
QToolButton[checkable="true"]:hover {
- background-color: #4a4a4a;
+ background-color: #4a4a4c;
}
QToolButton[checkable="true"]:checked {
- background-color: #0078d4;
- border: 1px solid #106ebe;
+ background-color: #0a84ff;
+ border: 1px solid #0a84ff;
+ color: #ffffff;
}
- /* 下拉框 - 修复文字不可见问题 */
+ /* 下拉框 - Apple深色风格 */
QComboBox {
- background-color: #3c3c3c;
- border: 1px solid #5a5a5a;
- border-radius: 2px;
- color: #e0e0e0;
- padding: 2px 5px;
- selection-background-color: #4a4a4a;
- selection-color: #e0e0e0;
+ background-color: #3a3a3c;
+ border: 1px solid #4a4a4c;
+ border-radius: 6px;
+ color: #f0f0f0;
+ padding: 4px 8px;
+ selection-background-color: #0a84ff;
+ selection-color: #ffffff;
}
QComboBox:hover {
- background-color: #4a4a4a;
- border: 1px solid #6a6a6a;
+ background-color: #4a4a4c;
+ border: 1px solid #5a5a5c;
}
QComboBox::drop-down {
border: none;
- width: 15px;
- background-color: #3c3c3c;
+ width: 20px;
+ border-left: 1px solid #4a4a4c;
+ border-top-right-radius: 6px;
+ border-bottom-right-radius: 6px;
}
QComboBox::down-arrow {
image: none;
- border-left: 3px solid transparent;
- border-right: 3px solid transparent;
- border-top: 5px solid #e0e0e0;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 6px solid #a0a0a0;
+ margin: 6px;
}
- /* 下拉框弹出列表 */
QComboBox QAbstractItemView {
- background-color: #3c3c3c;
- border: 1px solid #5a5a5a;
- color: #e0e0e0;
- selection-background-color: #4a4a4a;
- selection-color: #e0e0e0;
+ background-color: #2c2c2e;
+ border: 1px solid #4a4a4c;
+ color: #f0f0f0;
+ selection-background-color: #0a84ff;
+ selection-color: #ffffff;
}
- /* 字体下拉框特殊处理 */
- QFontComboBox {
- background-color: #3c3c3c;
- border: 1px solid #5a5a5a;
- border-radius: 2px;
- color: #e0e0e0;
- padding: 2px 5px;
- selection-background-color: #4a4a4a;
- selection-color: #e0e0e0;
- }
-
- QFontComboBox:hover {
- background-color: #4a4a4a;
- border: 1px solid #6a6a6a;
- }
-
- QFontComboBox QAbstractItemView {
- background-color: #3c3c3c;
- border: 1px solid #5a5a5a;
- color: #e0e0e0;
- selection-background-color: #4a4a4a;
- selection-color: #e0e0e0;
- }
-
- /* 文本编辑器 */
+ /* 文本编辑区域 - Apple深色风格 */
QTextEdit {
- background-color: #1e1e1e;
- border: 1px solid #3c3c3c;
- color: #e0e0e0;
- padding: 20px;
+ background-color: #1c1c1e;
+ border: none;
+ font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ font-size: 15px;
+ color: #f0f0f0;
+ padding: 32px;
line-height: 1.5;
+ selection-background-color: #0066cc;
+ selection-color: #ffffff;
}
- /* 状态栏 */
+ /* 状态栏 - Apple深色风格 */
QStatusBar {
- background-color: #2d2d2d;
- border-top: 1px solid #3c3c3c;
- font-size: 11px;
- color: #e0e0e0;
+ background-color: #3a3a3c;
+ border-top: 1px solid #4a4a4c;
+ font-size: 12px;
+ color: #a0a0a0;
+ font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ padding: 6px 12px;
}
/* 标签 */
QLabel {
- color: #e0e0e0;
+ color: #f0f0f0;
background-color: transparent;
}
- /* 消息框 - 修复黑色背景问题 */
+ /* 消息框 - Apple深色风格 */
QMessageBox {
- background-color: #2d2d2d;
- color: #e0e0e0;
+ background-color: #2c2c2e;
+ color: #f0f0f0;
+ border-radius: 12px;
}
QMessageBox QPushButton {
- background-color: #3c3c3c;
- color: #e0e0e0;
- border: 1px solid #5a5a5a;
- border-radius: 3px;
- padding: 5px 15px;
+ background-color: #3a3a3c;
+ color: #f0f0f0;
+ border: 1px solid #4a4a4c;
+ border-radius: 6px;
+ padding: 6px 16px;
min-width: 80px;
}
QMessageBox QPushButton:hover {
- background-color: #4a4a4a;
- border: 1px solid #6a6a6a;
+ background-color: #4a4a4c;
+ border: 1px solid #5a5a5c;
}
QMessageBox QPushButton:pressed {
- background-color: #2a2a2a;
- border: 1px solid #1a1a1a;
+ background-color: #5a5a5c;
+ border: 1px solid #6a6a6c;
+ }
+
+ QMessageBox QPushButton:default {
+ background-color: #0a84ff;
+ color: #ffffff;
+ border: 1px solid #0a84ff;
+ }
+
+ QMessageBox QPushButton:default:hover {
+ background-color: #0066cc;
+ border: 1px solid #0066cc;
+ }
+
+ QMessageBox QPushButton:default:pressed {
+ background-color: #004d99;
+ border: 1px solid #004d99;
}
- /* 滚动条 */
+ /* 滚动条 - Apple深色风格 */
QScrollBar:vertical {
- background-color: #2d2d2d;
- width: 12px;
+ background-color: transparent;
+ width: 8px;
border: none;
}
QScrollBar::handle:vertical {
- background-color: #5a5a5a;
- border-radius: 6px;
+ background-color: #5a5a5c;
+ border-radius: 4px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
- background-color: #6a6a6a;
+ background-color: #6a6a6c;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
border: none;
background: none;
}
+
+ /* 按钮 - Apple深色风格 */
+ QPushButton {
+ background-color: #3a3a3c;
+ color: #f0f0f0;
+ border: 1px solid #4a4a4c;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 13px;
+ }
+
+ QPushButton:hover {
+ background-color: #4a4a4c;
+ border: 1px solid #5a5a5c;
+ }
+
+ QPushButton:pressed {
+ background-color: #5a5a5c;
+ border: 1px solid #6a6a6c;
+ }
+
+ QPushButton:default {
+ background-color: #0a84ff;
+ color: #ffffff;
+ border: 1px solid #0a84ff;
+ }
+
+ QPushButton:default:hover {
+ background-color: #0066cc;
+ border: 1px solid #0066cc;
+ }
+
+ QPushButton:default:pressed {
+ background-color: #004d99;
+ border: 1px solid #004d99;
+ }
"""
def _get_light_stylesheet(self):
- """浅色主题样式表 - 白底黑字"""
+ """浅色主题样式表 - Apple设计风格"""
return """
- /* 浅色主题样式 - 白底黑字 */
+ /* Apple设计风格浅色主题样式 */
- /* 全局文字颜色 */
+ /* 全局文字颜色和字体 - 使用Apple系统字体 */
QWidget {
color: #333333;
+ font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ font-size: 13px;
}
- /* 主窗口 */
+ /* 主窗口 - 纯净白色背景 */
QMainWindow {
- background-color: #f3f2f1;
+ background-color: #ffffff;
}
- /* 菜单栏 */
+ /* 菜单栏 - Apple风格 */
QMenuBar {
- background-color: #0078d7;
- border: 1px solid #005a9e;
- font-size: 12px;
- color: #ffffff;
+ background-color: #ffffff;
+ border: none;
+ border-bottom: 1px solid #e0e0e0;
+ font-size: 13px;
+ color: #333333;
+ padding: 4px 0;
}
QMenuBar::item {
background-color: transparent;
- padding: 4px 10px;
+ padding: 6px 12px;
color: #333333;
+ border-radius: 4px;
+ margin: 0 1px;
}
QMenuBar::item:selected {
- background-color: #106ebe;
+ background-color: #f0f0f0;
+ color: #333333;
}
- /* 菜单 */
+ QMenuBar::item:pressed {
+ background-color: #e0e0e0;
+ color: #333333;
+ }
+
+ /* 菜单 - Apple风格 */
QMenu {
background-color: #ffffff;
border: 1px solid #d0d0d0;
- font-size: 12px;
+ border-radius: 8px;
+ font-size: 13px;
color: #333333;
+ padding: 4px 0;
+ margin: 2px;
}
QMenu::item {
- padding: 4px 20px;
color: #333333;
+ background-color: transparent;
+ border-radius: 4px;
+ margin: 0 4px;
+ padding: 4px 20px;
}
QMenu::item:selected {
- background-color: #f0f0f0;
+ background-color: #007aff;
+ color: #ffffff;
+ }
+
+ QMenu::item:pressed {
+ background-color: #0062cc;
+ color: #ffffff;
+ }
+
+ QMenu::separator {
+ height: 1px;
+ background-color: #e0e0e0;
+ margin: 4px 8px;
}
/* 功能区 */
QFrame {
background-color: #ffffff;
- border: 1px solid #d0d0d0;
+ border: none;
}
/* 组框 */
QGroupBox {
- font-size: 11px;
+ font-size: 12px;
font-weight: normal;
color: #333333;
background-color: #ffffff;
- border: 1px solid #d0d0d0;
- border-radius: 0px;
+ border: none;
+ border-radius: 8px;
margin-top: 5px;
padding-top: 5px;
}
@@ -476,145 +575,116 @@ class ThemeManager(QObject):
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
- color: #333333;
+ color: #666666;
}
- /* 按钮 */
+ /* 工具按钮 - Apple风格 */
QToolButton {
- border: 1px solid #d0d0d0;
- border-radius: 3px;
- background-color: #ffffff;
- font-size: 11px;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ background-color: #f6f6f6;
+ font-size: 13px;
color: #333333;
- padding: 3px 6px;
+ padding: 6px 12px;
}
QToolButton:hover {
- background-color: #f0f0f0;
- border: 1px solid #0078d7;
+ background-color: #e0e0e0;
+ border: 1px solid #d0d0d0;
}
QToolButton:pressed {
- background-color: #e0e0e0;
- border: 1px solid #005a9e;
+ background-color: #d0d0d0;
+ border: 1px solid #c0c0c0;
}
QToolButton:checked {
- background-color: #0078d7;
- border: 1px solid #005a9e;
+ background-color: #007aff;
+ border: 1px solid #007aff;
color: #ffffff;
}
/* 切换按钮 */
QToolButton[checkable="true"] {
border: 1px solid #d0d0d0;
- border-radius: 2px;
- background-color: #ffffff;
+ border-radius: 6px;
+ background-color: #f6f6f6;
font-size: 12px;
- font-weight: bold;
color: #333333;
+ padding: 6px 12px;
}
QToolButton[checkable="true"]:hover {
- background-color: #f0f0f0;
+ background-color: #e0e0e0;
}
QToolButton[checkable="true"]:checked {
- background-color: #0078d7;
- border: 1px solid #005a9e;
+ background-color: #007aff;
+ border: 1px solid #007aff;
color: #ffffff;
}
- /* 下拉框 - 白底黑字 */
+ /* 下拉框 - Apple风格 */
QComboBox {
- background-color: #ffffff;
+ background-color: #f6f6f6;
border: 1px solid #d0d0d0;
- border-radius: 2px;
+ border-radius: 6px;
color: #333333;
- padding: 2px 5px;
- selection-background-color: #f0f0f0;
- selection-color: #333333;
+ padding: 4px 8px;
+ selection-background-color: #007aff;
+ selection-color: #ffffff;
}
QComboBox:hover {
- background-color: #f0f0f0;
- border: 1px solid #0078d7;
+ background-color: #e0e0e0;
+ border: 1px solid #c0c0c0;
}
QComboBox::drop-down {
border: none;
- width: 15px;
- background-color: #ffffff;
+ width: 20px;
+ border-left: 1px solid #d0d0d0;
+ border-top-right-radius: 6px;
+ border-bottom-right-radius: 6px;
}
QComboBox::down-arrow {
image: none;
- border-left: 3px solid transparent;
- border-right: 3px solid transparent;
- border-top: 5px solid #333333;
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 6px solid #666666;
+ margin: 6px;
}
- /* 下拉框弹出列表 */
QComboBox QAbstractItemView {
- background-color: #ffffff;
border: 1px solid #d0d0d0;
color: #333333;
- selection-background-color: #f0f0f0;
- selection-color: #333333;
- }
-
- /* 字体下拉框特殊处理 - 白底黑字 */
- QFontComboBox {
- background-color: #ffffff;
- border: 1px solid #d0d0d0;
- border-radius: 2px;
- color: #333333;
- padding: 2px 5px;
- selection-background-color: #f0f0f0;
- selection-color: #333333;
- }
-
- QFontComboBox:hover {
- background-color: #f0f0f0;
- border: 1px solid #0078d7;
- }
-
- QFontComboBox::drop-down {
- border: none;
- width: 15px;
background-color: #ffffff;
+ selection-background-color: #007aff;
+ selection-color: #ffffff;
}
- QFontComboBox::down-arrow {
- image: none;
- border-left: 3px solid transparent;
- border-right: 3px solid transparent;
- border-top: 5px solid #333333;
- }
-
- QFontComboBox QAbstractItemView {
- background-color: #ffffff;
- border: 1px solid #d0d0d0;
- color: #333333;
- selection-background-color: #f0f0f0;
- selection-color: #333333;
- }
-
- /* 文本编辑器 */
+ /* 文本编辑区域 - Apple风格 */
QTextEdit {
background-color: #ffffff;
- border: 1px solid #d0d0d0;
- color: #000000;
- padding: 20px;
+ border: none;
+ font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ font-size: 15px;
+ color: #333333;
+ padding: 32px;
line-height: 1.5;
+ selection-background-color: #b3d9ff;
+ selection-color: #333333;
}
- /* 状态栏 */
+ /* 状态栏 - Apple风格 */
QStatusBar {
- background-color: #ffffff;
- border-top: 1px solid #d0d0d0;
- font-size: 11px;
- color: #333333;
+ background-color: #f6f6f6;
+ border-top: 1px solid #e0e0e0;
+ font-size: 12px;
+ color: #666666;
+ font-family: '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Helvetica', 'Arial', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ padding: 6px 12px;
}
/* 标签 */
@@ -623,41 +693,58 @@ class ThemeManager(QObject):
background-color: transparent;
}
- /* 消息框 - 修复黑色背景问题 */
+ /* 消息框 - Apple风格 */
QMessageBox {
background-color: #ffffff;
color: #333333;
+ border-radius: 12px;
}
QMessageBox QPushButton {
- background-color: #ffffff;
+ background-color: #f6f6f6;
color: #333333;
border: 1px solid #d0d0d0;
- border-radius: 3px;
- padding: 5px 15px;
+ border-radius: 6px;
+ padding: 6px 16px;
min-width: 80px;
}
QMessageBox QPushButton:hover {
- background-color: #f0f0f0;
- border: 1px solid #0078d7;
+ background-color: #e0e0e0;
+ border: 1px solid #c0c0c0;
}
QMessageBox QPushButton:pressed {
- background-color: #e0e0e0;
- border: 1px solid #005a9e;
+ background-color: #d0d0d0;
+ border: 1px solid #a0a0a0;
+ }
+
+ QMessageBox QPushButton:default {
+ background-color: #007aff;
+ color: #ffffff;
+ border: 1px solid #007aff;
+ }
+
+ QMessageBox QPushButton:default:hover {
+ background-color: #0062cc;
+ border: 1px solid #0062cc;
+ }
+
+ QMessageBox QPushButton:default:pressed {
+ background-color: #004a99;
+ border: 1px solid #004a99;
}
- /* 滚动条 */
+ /* 滚动条 - Apple风格 */
QScrollBar:vertical {
- background-color: #ffffff;
- width: 12px;
+ background-color: transparent;
+ width: 8px;
border: none;
}
QScrollBar::handle:vertical {
background-color: #c0c0c0;
- border-radius: 6px;
+ border-radius: 4px;
min-height: 20px;
}
@@ -669,6 +756,42 @@ class ThemeManager(QObject):
border: none;
background: none;
}
+
+ /* 按钮 - Apple风格 */
+ QPushButton {
+ background-color: #f6f6f6;
+ color: #333333;
+ border: 1px solid #d0d0d0;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 13px;
+ }
+
+ QPushButton:hover {
+ background-color: #e0e0e0;
+ border: 1px solid #c0c0c0;
+ }
+
+ QPushButton:pressed {
+ background-color: #d0d0d0;
+ border: 1px solid #a0a0a0;
+ }
+
+ QPushButton:default {
+ background-color: #007aff;
+ color: #ffffff;
+ border: 1px solid #007aff;
+ }
+
+ QPushButton:default:hover {
+ background-color: #0062cc;
+ border: 1px solid #0062cc;
+ }
+
+ QPushButton:default:pressed {
+ background-color: #004a99;
+ border: 1px solid #004a99;
+ }
"""
def set_dark_theme(self, is_dark):
diff --git a/src/ui/ui.txt b/src/ui/ui.txt
deleted file mode 100644
index e69de29..0000000
diff --git a/src/ui/weather_floating_widget.py b/src/ui/weather_floating_widget.py
new file mode 100644
index 0000000..bb373de
--- /dev/null
+++ b/src/ui/weather_floating_widget.py
@@ -0,0 +1,620 @@
+# -*- coding: utf-8 -*-
+
+"""
+天气悬浮窗口模块
+提供一个可拖拽的天气悬浮窗口,显示当前天气信息
+"""
+
+import sys
+from PyQt5.QtWidgets import (
+ QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
+ QFrame, QTextEdit, QDialog, QComboBox
+)
+from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QPoint, QThread
+from PyQt5.QtGui import QFont, QPalette, QColor
+
+# 导入主题管理器
+from .theme_manager import theme_manager
+
+
+class WeatherFloatingWidget(QDialog):
+ """天气悬浮窗口类"""
+
+ # 自定义信号
+ closed = pyqtSignal()
+ refresh_requested = pyqtSignal()
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.drag_position = None
+ self.is_dragging = False
+ self.weather_data = None
+ self.setup_ui()
+ self.setup_connections()
+ self.setup_theme()
+ self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool)
+ self.setAttribute(Qt.WA_TranslucentBackground)
+
+ def setup_ui(self):
+ """设置UI界面"""
+ # 设置窗口属性
+ self.setWindowTitle("天气")
+ self.setFixedSize(360, 280) # 调整窗口尺寸使其更紧凑
+
+ # 创建主框架,用于实现圆角和阴影效果
+ self.main_frame = QFrame()
+ self.main_frame.setObjectName("mainFrame")
+
+ # 主布局
+ main_layout = QVBoxLayout(self)
+ main_layout.setContentsMargins(0, 0, 0, 0)
+ main_layout.addWidget(self.main_frame)
+
+ # 内容布局
+ content_layout = QVBoxLayout(self.main_frame)
+ content_layout.setContentsMargins(10, 10, 10, 10) # 减小内边距使布局更紧凑
+ content_layout.setSpacing(6) # 减小间距使布局更紧凑
+
+ # 设置最小尺寸策略
+ self.main_frame.setMinimumSize(380, 300)
+
+ # 标题栏
+ title_layout = QHBoxLayout()
+
+ self.title_label = QLabel("天气信息")
+ self.title_label.setFont(QFont("Arial", 12, QFont.Bold))
+ title_layout.addWidget(self.title_label)
+ title_layout.addStretch()
+ # 添加一个小的固定空间,使关闭按钮向左移动
+ title_layout.addSpacing(25) # 向左移动25个单位
+
+ # 关闭按钮
+ self.close_btn = QPushButton("×")
+ self.close_btn.setFixedSize(20, 20)
+ self.close_btn.setObjectName("closeButton")
+ title_layout.addWidget(self.close_btn)
+
+ content_layout.addLayout(title_layout)
+
+ # 分隔线
+ separator = QFrame()
+ separator.setFrameShape(QFrame.HLine)
+ separator.setObjectName("separator")
+ content_layout.addWidget(separator)
+
+ # 天气图标和温度显示区域
+ weather_display_layout = QHBoxLayout()
+ weather_display_layout.setSpacing(5) # 减小间距使布局更紧凑
+ weather_display_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距
+
+ self.weather_icon_label = QLabel("🌞")
+ self.weather_icon_label.setFont(QFont("Arial", 24)) # 稍微减小字体大小
+ self.weather_icon_label.setAlignment(Qt.AlignCenter)
+ self.weather_icon_label.setFixedSize(50, 50) # 减小尺寸
+ weather_display_layout.addWidget(self.weather_icon_label)
+
+ # 温度和城市信息
+ temp_city_layout = QVBoxLayout()
+ temp_city_layout.setSpacing(4) # 减小间距使布局更紧凑
+ temp_city_layout.setContentsMargins(0, 0, 0, 0)
+
+ self.temperature_label = QLabel("25°C")
+ self.temperature_label.setFont(QFont("Arial", 18, QFont.Bold)) # 稍微减小字体大小
+ self.temperature_label.setObjectName("temperatureLabel")
+ temp_city_layout.addWidget(self.temperature_label)
+
+ self.city_label = QLabel("北京")
+ self.city_label.setFont(QFont("Arial", 11)) # 稍微减小字体大小
+ self.city_label.setObjectName("cityLabel")
+ temp_city_layout.addWidget(self.city_label)
+
+ weather_display_layout.addLayout(temp_city_layout)
+ weather_display_layout.addStretch()
+
+ content_layout.addLayout(weather_display_layout)
+
+ # 天气描述
+ self.weather_desc_label = QLabel("晴天")
+ self.weather_desc_label.setFont(QFont("Arial", 11)) # 稍微减小字体大小
+ self.weather_desc_label.setObjectName("weatherDescLabel")
+ self.weather_desc_label.setAlignment(Qt.AlignCenter)
+ content_layout.addWidget(self.weather_desc_label)
+
+ # 详细信息(湿度、风速)
+ details_layout = QHBoxLayout()
+ details_layout.setSpacing(6) # 减小间距使布局更紧凑
+ details_layout.setContentsMargins(2, 2, 2, 2) # 减小内边距
+
+ self.humidity_label = QLabel("湿度: 45%")
+ self.humidity_label.setFont(QFont("Arial", 10)) # 稍微减小字体大小
+ self.humidity_label.setObjectName("detailLabel")
+ details_layout.addWidget(self.humidity_label)
+
+ self.wind_label = QLabel("风速: 2级")
+ self.wind_label.setFont(QFont("Arial", 10)) # 稍微减小字体大小
+ self.wind_label.setObjectName("detailLabel")
+ details_layout.addWidget(self.wind_label)
+
+ content_layout.addLayout(details_layout)
+
+ # 城市选择区域
+ city_layout = QHBoxLayout()
+ city_layout.setSpacing(6) # 减小间距使布局更紧凑
+ city_layout.setContentsMargins(0, 0, 0, 0)
+
+ self.city_combo = QComboBox()
+ self.city_combo.setObjectName("cityCombo")
+ # 添加所有省会城市,与主窗口保持一致
+ self.city_combo.addItems([
+ '自动定位',
+ '北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', # 一线城市
+ '天津', '重庆', '苏州', '青岛', '大连', '宁波', '厦门', '无锡', '佛山', # 新一线城市
+ '石家庄', '太原', '呼和浩特', '沈阳', '长春', '哈尔滨', # 东北华北
+ '合肥', '福州', '南昌', '济南', '郑州', '长沙', '南宁', '海口', # 华东华中华南
+ '贵阳', '昆明', '拉萨', '兰州', '西宁', '银川', '乌鲁木齐' # 西南西北
+ ])
+ self.city_combo.setFixedWidth(100) # 减小城市选择框宽度使布局更紧凑
+ city_layout.addWidget(self.city_combo)
+
+ city_layout.addStretch()
+
+ content_layout.addLayout(city_layout)
+
+ # 按钮区域
+ button_layout = QHBoxLayout()
+ button_layout.setSpacing(6) # 减小间距使布局更紧凑
+ button_layout.setContentsMargins(0, 0, 0, 0)
+
+ self.refresh_btn = QPushButton("刷新")
+ self.refresh_btn.setObjectName("refreshButton")
+ self.refresh_btn.setFixedHeight(26) # 减小按钮高度
+ button_layout.addWidget(self.refresh_btn)
+
+ button_layout.addStretch()
+
+ self.detail_btn = QPushButton("详情")
+ self.detail_btn.setObjectName("detailButton")
+ self.detail_btn.setFixedHeight(26) # 减小按钮高度
+ button_layout.addWidget(self.detail_btn)
+
+ content_layout.addLayout(button_layout)
+
+ # 添加弹性空间
+ content_layout.addStretch()
+
+ def setup_connections(self):
+ """设置信号连接"""
+ self.close_btn.clicked.connect(self.close_window)
+ self.refresh_btn.clicked.connect(self.on_refresh_clicked)
+ self.detail_btn.clicked.connect(self.show_detailed_weather)
+ self.city_combo.currentTextChanged.connect(self.on_city_changed)
+
+ def setup_theme(self):
+ """设置主题"""
+ # 连接主题切换信号
+ theme_manager.theme_changed.connect(self.on_theme_changed)
+
+ # 应用当前主题
+ self.apply_theme()
+
+ def apply_theme(self):
+ """应用主题样式"""
+ is_dark = theme_manager.is_dark_theme()
+ colors = theme_manager.get_current_theme_colors()
+
+ if is_dark:
+ # 深色主题样式 - 与每日谏言悬浮窗口保持一致
+ self.main_frame.setStyleSheet(f"""
+ QFrame#mainFrame {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#temperatureLabel {{
+ color: {colors['accent']};
+ font-size: 20px;
+ font-weight: bold;
+ padding: 6px 8px;
+ margin: 3px;
+ }}
+ QLabel#cityLabel {{
+ color: {colors['text_secondary']};
+ font-size: 12px;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#weatherDescLabel {{
+ color: {colors['text']};
+ font-size: 12px;
+ font-weight: 500;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#detailLabel {{
+ color: {colors['text_secondary']};
+ font-size: 11px;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(255, 255, 255, 0.1);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#refreshButton, QPushButton#detailButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ QComboBox#cityCombo {{
+ background-color: {colors['surface']};
+ color: {colors['text']};
+ border: 1px solid {colors['border']};
+ border-radius: 6px;
+ padding: 4px 7px;
+ font-size: 11px;
+ font-weight: 500;
+ min-height: 24px;
+ }}
+ QComboBox#cityCombo:hover {{
+ border-color: {colors['accent']};
+ }}
+ QComboBox#cityCombo::drop-down {{
+ border: none;
+ width: 14px;
+ }}
+ QComboBox#cityCombo::down-arrow {{
+ image: none;
+ border-left: 2px solid transparent;
+ border-right: 2px solid transparent;
+ border-top: 5px solid {colors['text']};
+ }}
+ """)
+ else:
+ # 浅色主题样式 - 与每日谏言悬浮窗口保持一致
+ self.main_frame.setStyleSheet(f"""
+ QFrame#mainFrame {{
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 10px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }}
+ QLabel {{
+ color: {colors['text']};
+ background-color: transparent;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#temperatureLabel {{
+ color: {colors['accent']};
+ font-size: 20px;
+ font-weight: bold;
+ padding: 6px 8px;
+ margin: 3px;
+ }}
+ QLabel#cityLabel {{
+ color: {colors['text_secondary']};
+ font-size: 12px;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#weatherDescLabel {{
+ color: {colors['text']};
+ font-size: 12px;
+ font-weight: 500;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QLabel#detailLabel {{
+ color: {colors['text_secondary']};
+ font-size: 11px;
+ padding: 4px 6px;
+ margin: 2px;
+ }}
+ QFrame#separator {{
+ background-color: {colors['border']};
+ }}
+ QPushButton#closeButton {{
+ background-color: rgba(0, 0, 0, 0.05);
+ border: none;
+ color: {colors['text']};
+ font-size: 18px;
+ font-weight: bold;
+ border-radius: 6px;
+ padding: 2px 4px;
+ }}
+ QPushButton#closeButton:hover {{
+ background-color: #e81123;
+ color: white;
+ border-radius: 6px;
+ }}
+ QPushButton#refreshButton, QPushButton#detailButton {{
+ background-color: {colors['accent']};
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 16px;
+ font-size: 11px;
+ font-weight: 500;
+ }}
+ QPushButton#refreshButton:hover, QPushButton#detailButton:hover {{
+ background-color: {colors['accent_hover']};
+ }}
+ QComboBox#cityCombo {{
+ background-color: {colors['surface']};
+ color: {colors['text']};
+ border: 1px solid {colors['border']};
+ border-radius: 6px;
+ padding: 4px 7px;
+ font-size: 11px;
+ font-weight: 500;
+ min-height: 24px;
+ }}
+ QComboBox#cityCombo:hover {{
+ border-color: {colors['accent']};
+ }}
+ QComboBox#cityCombo::drop-down {{
+ border: none;
+ width: 14px;
+ }}
+ QComboBox#cityCombo::down-arrow {{
+ image: none;
+ border-left: 2px solid transparent;
+ border-right: 2px solid transparent;
+ border-top: 5px solid {colors['text']};
+ }}
+ """)
+
+ def on_theme_changed(self, is_dark):
+ """主题切换槽函数"""
+ self.apply_theme()
+
+ def mousePressEvent(self, event):
+ """鼠标按下事件,用于拖拽"""
+ if event.button() == Qt.LeftButton:
+ # 检查是否点击在标题栏区域
+ if event.pos().y() <= 40: # 假设标题栏高度为40像素
+ self.is_dragging = True
+ self.drag_position = event.globalPos() - self.frameGeometry().topLeft()
+ event.accept()
+
+ def mouseMoveEvent(self, event):
+ """鼠标移动事件,用于拖拽"""
+ if self.is_dragging and event.buttons() == Qt.LeftButton:
+ self.move(event.globalPos() - self.drag_position)
+ event.accept()
+
+ def mouseReleaseEvent(self, event):
+ """鼠标释放事件"""
+ self.is_dragging = False
+
+ def update_weather(self, weather_data):
+ """更新天气信息"""
+ self.weather_data = weather_data
+
+ if weather_data and 'error' not in weather_data:
+ # 获取天气数据
+ city = weather_data.get('city', '未知城市')
+ current_data = weather_data.get('current', {})
+ temp = current_data.get('temp', 'N/A')
+ desc = current_data.get('weather', 'N/A')
+ humidity = current_data.get('humidity', 'N/A')
+ wind_scale = current_data.get('wind_scale', 'N/A')
+
+ # 更新显示
+ self.city_label.setText(city)
+ self.temperature_label.setText(f"{temp}°C")
+ self.weather_desc_label.setText(desc)
+ self.humidity_label.setText(f"湿度: {humidity}%")
+ self.wind_label.setText(f"风速: {wind_scale}级")
+
+ # 更新天气图标
+ emoji = self.get_weather_emoji(desc)
+ self.weather_icon_label.setText(emoji)
+ else:
+ # 显示错误信息
+ self.city_label.setText("获取失败")
+ self.temperature_label.setText("--°C")
+ self.weather_desc_label.setText("无法获取天气数据")
+ self.humidity_label.setText("湿度: --%")
+ self.wind_label.setText("风速: --级")
+ self.weather_icon_label.setText("❓")
+
+ def get_weather_emoji(self, weather_desc):
+ """根据天气描述返回对应的emoji"""
+ if not weather_desc:
+ return "🌞"
+
+ weather_desc_lower = weather_desc.lower()
+
+ # 天气图标映射
+ weather_emoji_map = {
+ '晴': '🌞',
+ '多云': '⛅',
+ '阴': '☁️',
+ '雨': '🌧️',
+ '小雨': '🌦️',
+ '中雨': '🌧️',
+ '大雨': '⛈️',
+ '暴雨': '🌩️',
+ '雪': '❄️',
+ '小雪': '🌨️',
+ '中雪': '❄️',
+ '大雪': '☃️',
+ '雾': '🌫️',
+ '霾': '😷',
+ '风': '💨',
+ '大风': '🌪️',
+ '雷': '⛈️',
+ '雷阵雨': '⛈️',
+ '冰雹': '🌨️',
+ '沙尘': '🌪️'
+ }
+
+ for key, emoji in weather_emoji_map.items():
+ if key in weather_desc_lower:
+ return emoji
+
+ # 默认返回晴天图标
+ return '🌞'
+
+ def on_refresh_clicked(self):
+ """刷新按钮点击事件"""
+ self.refresh_requested.emit()
+
+ def on_city_changed(self, city_name):
+ """城市选择变化事件"""
+ # 发射城市变化信号,通知主窗口更新天气
+ if hasattr(self.parent(), 'on_city_changed'):
+ self.parent().on_city_changed(city_name)
+
+ def set_current_city(self, city_name):
+ """设置当前城市"""
+ # 阻止信号发射,避免循环调用
+ self.city_combo.blockSignals(True)
+ index = self.city_combo.findText(city_name)
+ if index >= 0:
+ self.city_combo.setCurrentIndex(index)
+ self.city_combo.blockSignals(False)
+
+ def close_window(self):
+ """关闭窗口 - 只是隐藏而不是销毁"""
+ try:
+ self.closed.emit()
+ self.hide() # 隐藏窗口而不是销毁
+ except Exception as e:
+ print(f"Error in close_window: {e}")
+
+ def show_detailed_weather(self):
+ """显示详细天气信息对话框"""
+ # 检查是否有天气数据
+ if not self.weather_data or 'error' in self.weather_data:
+ from PyQt5.QtWidgets import QMessageBox
+ QMessageBox.information(self, "天气信息", "暂无天气数据,请先刷新天气信息")
+ return
+
+ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit
+
+ weather_data = self.weather_data
+
+ # 创建对话框
+ dialog = QDialog(self)
+ dialog.setWindowTitle("详细天气")
+ dialog.setMinimumWidth(400)
+
+ layout = QVBoxLayout()
+
+ # 城市信息
+ city_label = QLabel(f"
{weather_data.get('city', '未知城市')}
")
+ layout.addWidget(city_label)
+
+ # 当前天气信息
+ current_layout = QVBoxLayout()
+ current_layout.addWidget(QLabel("
当前天气:"))
+
+ # 获取温度信息,支持嵌套结构
+ current_data = weather_data.get('current', {})
+ temp = current_data.get('temp', 'N/A')
+ if temp != 'N/A' and isinstance(temp, str):
+ temp = float(temp) if temp.replace('.', '').isdigit() else temp
+
+ # 从预报数据中获取最高和最低气温
+ temp_range = ""
+ temp_max = 'N/A'
+ temp_min = 'N/A'
+ if 'forecast' in weather_data and weather_data['forecast']:
+ forecast_data = weather_data['forecast'][0] # 今天的预报
+ if isinstance(forecast_data, dict):
+ temp_max = forecast_data.get('temp_max', 'N/A')
+ temp_min = forecast_data.get('temp_min', 'N/A')
+ if temp_max != 'N/A' and temp_min != 'N/A':
+ temp_range = f" ({temp_min}°C~{temp_max}°C)"
+
+ current_info = f"""
+当前温度: {temp}°C{temp_range}
+最高气温: {temp_max}°C
+最低气温: {temp_min}°C
+天气状况: {current_data.get('weather', 'N/A')}
+ """
+ current_text = QTextEdit()
+ current_text.setPlainText(current_info.strip())
+ current_text.setReadOnly(True)
+ current_layout.addWidget(current_text)
+
+ layout.addLayout(current_layout)
+
+ # 生活提示信息(替换原来的天气预报)
+ life_tips = weather_data.get('life_tips', [])
+ if life_tips:
+ tips_layout = QVBoxLayout()
+ tips_layout.addWidget(QLabel("
生活提示:"))
+
+ tips_text = QTextEdit()
+ tips_info = ""
+ for tip in life_tips:
+ tips_info += f"• {tip}\n"
+
+ tips_text.setPlainText(tips_info.strip())
+ tips_text.setReadOnly(True)
+ tips_layout.addWidget(tips_text)
+ layout.addLayout(tips_layout)
+
+ # 按钮
+ button_layout = QHBoxLayout()
+ refresh_button = QPushButton("刷新")
+ refresh_button.clicked.connect(lambda: self.refresh_weather_and_close(dialog))
+ close_button = QPushButton("关闭")
+ close_button.clicked.connect(dialog.close)
+
+ button_layout.addWidget(refresh_button)
+ button_layout.addWidget(close_button)
+ layout.addLayout(button_layout)
+
+ dialog.setLayout(layout)
+ dialog.exec_()
+
+ def refresh_weather_and_close(self, dialog):
+ """刷新天气并关闭对话框"""
+ self.on_refresh_clicked()
+ dialog.close()
+
+ def closeEvent(self, event):
+ """窗口关闭事件 - 只是隐藏而不是销毁"""
+ self.closed.emit()
+ self.hide() # 隐藏窗口而不是销毁
+ event.ignore()
+
+ def show_at_position(self, x, y):
+ """在指定位置显示窗口"""
+ self.move(x, y)
+ self.show()
+
+ def update_position(self, x, y):
+ """更新窗口位置"""
+ self.move(x, y)
\ No newline at end of file
diff --git a/src/ui/word_style_ui.py b/src/ui/word_style_ui.py
index 5150f9f..65e0525 100644
--- a/src/ui/word_style_ui.py
+++ b/src/ui/word_style_ui.py
@@ -34,6 +34,7 @@ class WordRibbon(QFrame):
self.weather_group = None # 天气组件组
self.quote_visible = False # 每日一言组件显示状态
self.quote_group = None # 每日一言组件组
+ self.current_quote_type = "普通箴言" # 每日一言类型
self.ribbon_layout = None # 功能区布局
self.setup_ui()
@@ -49,13 +50,13 @@ class WordRibbon(QFrame):
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
- # 功能区
+ # 功能区 - 现代极简主义风格
self.ribbon_area = QFrame()
self.ribbon_area.setStyleSheet("""
QFrame {
background-color: #ffffff;
- border: 1px solid #d0d0d0;
- border-top: none;
+ border: none;
+ border-bottom: 1px solid #e2e8f0;
}
""")
@@ -73,103 +74,139 @@ class WordRibbon(QFrame):
main_layout.addWidget(self.ribbon_area)
self.setLayout(main_layout)
+ def add_separator(self):
+ """
+ 在 Ribbon 主布局里添加一个高度自动适配的竖向分隔线
+ """
+ line = QFrame()
+ line.setFrameShape(QFrame.VLine)
+ line.setFrameShadow(QFrame.Plain)
+ line.setStyleSheet("""
+ QFrame {
+ background-color: #d0d7de;
+ width: 1px;
+ }
+ """)
+ line.setFixedWidth(1)
+
+ # Ribbon 区域高度会变,这玩意就自动跟着撑满
+ line.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding)
+
+ self.ribbon_layout.addWidget(line)
+
+
+
+ def create_ribbon_group(self, title, is_special_group=False):
+ """
+ 统一的创建 group 的方法。
+ - 如果 is_special_group 为 True:返回单个 group(与原来某些位置的用法一致)。
+ - 否则:返回 (group, content_layout) 以便直接往 content_layout 塞控件(与 setup_home_tab 的用法一致)。
+ """
+ # 对于“特殊组”(天气、每日一言)我们使用 QGroupBox 保持原样
+ if is_special_group:
+ group = QGroupBox(title)
+ # 应用主题样式 hook
+ theme_manager.theme_changed.connect(lambda: self._update_group_style(group))
+ self._update_group_style(group)
+ # 给特殊组留一些最小宽度,便于布局(可由调用方调整)
+ group.setMinimumWidth(100)
+ return group
+
+ # 常规组:使用 QFrame + 内部 content layout + 底部标题(保留标题在下的视觉)
+ group = QFrame()
+ group.setStyleSheet("""
+ QFrame { border: none; }
+ QLabel { font-size: 11px; color: #555; }
+ """)
+
+ outer_layout = QVBoxLayout(group)
+ outer_layout.setContentsMargins(3, 3, 3, 3)
+ outer_layout.setSpacing(2)
+
+ content_widget = QWidget()
+ content_layout = QHBoxLayout(content_widget)
+ content_layout.setContentsMargins(0, 0, 0, 0)
+ content_layout.setSpacing(5)
+
+ title_label = QLabel(title)
+ title_label.setAlignment(Qt.AlignCenter)
+
+ outer_layout.addWidget(content_widget)
+ outer_layout.addWidget(title_label)
+
+ group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
+ return group, content_layout
+
def setup_home_tab(self, layout):
- """设置开始标签的功能区内容"""
-
- # 字体组
- font_group = self.create_ribbon_group("字体")
-
- # 字体选择
- font_layout = QHBoxLayout()
+ """设置开始标签 Ribbon —— 保持原有变量/槽名不变,只改布局实现"""
+
+ # ---------- 字体组 ----------
+ font_group, font_layout = self.create_ribbon_group("字体")
+
self.font_combo = QFontComboBox()
self.font_combo.setFixedWidth(120)
self.font_combo.currentFontChanged.connect(self.on_font_changed)
+
self.font_size_combo = QComboBox()
- self.font_size_combo.addItems(['8', '9', '10', '11', '12', '14', '16', '18', '20', '22', '24', '26', '28', '36', '48', '72'])
+ self.font_size_combo.addItems(
+ ['8','9','10','11','12','14','16','18','20','22','24','26','28','36','48','72']
+ )
self.font_size_combo.setFixedWidth(70)
self.font_size_combo.setCurrentText('12')
self.font_size_combo.currentTextChanged.connect(self.on_font_size_changed)
-
+
font_layout.addWidget(self.font_combo)
font_layout.addWidget(self.font_size_combo)
-
- # 字体样式按钮
- font_style_layout = QHBoxLayout()
+
self.bold_btn = self.create_toggle_button("B", "bold")
- self.bold_btn.clicked.connect(self.on_bold_clicked)
self.italic_btn = self.create_toggle_button("I", "italic")
- self.italic_btn.clicked.connect(self.on_italic_clicked)
self.underline_btn = self.create_toggle_button("U", "underline")
- self.underline_btn.clicked.connect(self.on_underline_clicked)
-
- # 字体颜色按钮
self.color_btn = self.create_color_button("A", "color")
- self.color_btn.clicked.connect(self.on_color_clicked)
-
- font_style_layout.addWidget(self.bold_btn)
- font_style_layout.addWidget(self.italic_btn)
- font_style_layout.addWidget(self.underline_btn)
- font_style_layout.addWidget(self.color_btn)
-
- font_main_layout = QVBoxLayout()
- font_main_layout.addLayout(font_layout)
- font_main_layout.addLayout(font_style_layout)
-
- font_group.setLayout(font_main_layout)
+
+ font_layout.addWidget(self.bold_btn)
+ font_layout.addWidget(self.italic_btn)
+ font_layout.addWidget(self.underline_btn)
+ font_layout.addWidget(self.color_btn)
+ font_layout.addStretch()
layout.addWidget(font_group)
-
- # 段落组
- paragraph_group = self.create_ribbon_group("段落")
- paragraph_group.setFixedWidth(320) # 增加宽度以适应更宽的按钮
-
- # 对齐方式
- align_layout = QHBoxLayout()
+ self.add_separator()
+ # ---------- 段落组 ----------
+ paragraph_group, paragraph_layout = self.create_ribbon_group("段落")
self.align_left_btn = self.create_toggle_button("左对齐", "align_left")
self.align_center_btn = self.create_toggle_button("居中", "align_center")
self.align_right_btn = self.create_toggle_button("右对齐", "align_right")
self.align_justify_btn = self.create_toggle_button("两端对齐", "align_justify")
-
- align_layout.addWidget(self.align_left_btn)
- align_layout.addWidget(self.align_center_btn)
- align_layout.addWidget(self.align_right_btn)
- align_layout.addWidget(self.align_justify_btn)
-
- paragraph_layout = QVBoxLayout()
- paragraph_layout.addLayout(align_layout)
-
- paragraph_group.setLayout(paragraph_layout)
+
+ paragraph_layout.addWidget(self.align_left_btn)
+ paragraph_layout.addWidget(self.align_center_btn)
+ paragraph_layout.addWidget(self.align_right_btn)
+ paragraph_layout.addWidget(self.align_justify_btn)
+ paragraph_layout.addStretch()
layout.addWidget(paragraph_group)
-
- # 样式组
- styles_group = self.create_ribbon_group("样式")
- styles_layout = QVBoxLayout()
- styles_layout.addWidget(QLabel("样式"))
- styles_group.setLayout(styles_layout)
- layout.addWidget(styles_group)
-
- # 编辑组
- editing_group = self.create_ribbon_group("编辑")
-
- # 创建查找替换按钮,使用更适合水平排列的样式
+ self.add_separator()
+
+ # ---------- 样式组(单行水平排列) ----------
+ self.init_style_preview_group()
+ self.add_separator()
+
+ # ---------- 编辑组 ----------
+ editing_group, editing_layout = self.create_ribbon_group("编辑")
self.find_btn = QToolButton()
self.find_btn.setText("查找")
self.find_btn.setToolButtonStyle(Qt.ToolButtonTextOnly)
- self.find_btn.setFixedSize(50, 25) # 设置适合水平排列的尺寸
-
+ self.find_btn.setFixedSize(55, 25)
self.replace_btn = QToolButton()
self.replace_btn.setText("替换")
self.replace_btn.setToolButtonStyle(Qt.ToolButtonTextOnly)
- self.replace_btn.setFixedSize(50, 25) # 设置适合水平排列的尺寸
-
- editing_layout = QHBoxLayout() # 改为水平布局
+ self.replace_btn.setFixedSize(55, 25)
editing_layout.addWidget(self.find_btn)
editing_layout.addWidget(self.replace_btn)
- editing_layout.addStretch() # 添加弹性空间
- editing_group.setLayout(editing_layout)
- editing_group.setFixedWidth(120) # 设置编辑组宽度以适应查找替换按钮
+ editing_layout.addStretch()
layout.addWidget(editing_group)
-
+
+ # 最右侧填充空白,保证整体自适应
layout.addStretch()
+
def on_font_changed(self, font):
"""字体变化处理"""
@@ -179,22 +216,67 @@ class WordRibbon(QFrame):
"""字体大小变化处理"""
pass
- def on_bold_clicked(self):
- """粗体按钮点击处理"""
- pass
-
- def on_italic_clicked(self):
- """斜体按钮点击处理"""
- pass
-
- def on_underline_clicked(self):
- """下划线按钮点击处理"""
- pass
-
- def on_color_clicked(self):
- """字体颜色按钮点击处理"""
- pass
+
+ def init_style_preview_group(self):
+ """加入 Word 风格的样式预览区域"""
+
+ group = QFrame()
+ group.setStyleSheet("""
+ QFrame { border: none; }
+ QLabel { font-size: 11px; color: #555; }
+ """)
+
+ outer_layout = QVBoxLayout(group)
+ outer_layout.setContentsMargins(3, 3, 3, 3)
+ outer_layout.setSpacing(2)
+
+ # 上部:样式预览按钮
+ preview_widget = QWidget()
+ preview_layout = QHBoxLayout(preview_widget)
+ preview_layout.setContentsMargins(0, 0, 0, 0)
+ preview_layout.setSpacing(8)
+
+ style_items = [
+ ("正文", "font-size:14px;"),
+ ("无间隔", "font-size:14px;"),
+ ("标题 1", "font-size:22px; font-weight:bold; color:#2E75B6;"),
+ ("标题 2", "font-size:18px; color:#2E75B6;"),
+ ("标题 3", "font-size:16px; font-weight:bold;"),
+ ("副标题", "font-size:14px; font-style:italic; color:#555;"),
+ ("强调", "font-size:14px; color:#C0504D;"),
+ ]
+
+ for text, style in style_items:
+ btn = QPushButton(text)
+ btn.setFixedSize(95, 60)
+ btn.setStyleSheet(f"""
+ QPushButton {{
+ background: white;
+ border: 1px solid #d0d0d0;
+ border-radius: 3px;
+ text-align: left;
+ padding: 5px;
+ {style}
+ }}
+ QPushButton:hover {{
+ border: 1px solid #4a90e2;
+ }}
+ """)
+ preview_layout.addWidget(btn)
+
+ preview_layout.addStretch()
+
+ # 下方标题
+ title_label = QLabel("样式")
+ title_label.setAlignment(Qt.AlignCenter)
+
+ outer_layout.addWidget(preview_widget)
+ outer_layout.addWidget(title_label)
+
+ self.ribbon_layout.addWidget(group)
+
+
def init_theme(self):
"""初始化主题"""
# 连接主题切换信号
@@ -204,35 +286,52 @@ class WordRibbon(QFrame):
self.apply_theme()
def apply_theme(self):
- """应用主题样式"""
is_dark = theme_manager.is_dark_theme()
-
- # 更新功能区背景
colors = theme_manager.get_current_theme_colors()
+ # 简化仅保留关键样式更新
self.ribbon_area.setStyleSheet(f"""
QFrame {{
background-color: {colors['surface']};
border: 1px solid {colors['border']};
border-top: none;
}}
- QGroupBox {{
- background-color: {colors['surface']};
- color: {colors['text']};
- border: 1px solid {colors['border']};
- }}
- QGroupBox::title {{
- color: {colors['text']};
- background-color: transparent;
- }}
- QLabel {{
- color: {colors['text']};
- background-color: transparent;
- }}
""")
+ # 更新下拉框和按钮样式(调用原有 helper)
+ self.update_combo_styles(is_dark)
+ self.update_font_button_styles(is_dark)
+
+ # 更新天气组件样式
+ if hasattr(self, 'weather_icon_label') and self.weather_icon_label is not None:
+ self.weather_icon_label.setStyleSheet(f"""
+ QLabel {{
+ font-size: 32px;
+ padding: 0px;
+ margin: 0px;
+ border: none;
+ background: transparent;
+ }}
+ """)
+
+ if hasattr(self, 'weather_temp_label') and self.weather_temp_label is not None:
+ self.weather_temp_label.setStyleSheet(f"""
+ QLabel {{
+ font-size: 16px;
+ font-weight: bold;
+ color: {colors['text']};
+ padding: 0px;
+ margin: 0px;
+ border: none;
+ background: transparent;
+ min-height: 30px;
+ }}
+ """)
# 更新下拉框样式
self.update_combo_styles(is_dark)
-
+
+ def on_theme_changed(self, is_dark):
+ self.apply_theme()
+
def update_combo_styles(self, is_dark):
"""更新下拉框样式"""
colors = theme_manager.get_current_theme_colors()
@@ -464,17 +563,125 @@ class WordRibbon(QFrame):
"""主题切换槽函数"""
self.apply_theme()
+ def get_weather_emoji(self, weather_desc):
+ """根据天气描述返回对应的emoji图标"""
+ weather_emoji_map = {
+ '晴': '🌞',
+ '多云': '☁️',
+ '阴': '☁️',
+ '小雨': '🌦️',
+ '中雨': '🌧️',
+ '大雨': '⛈️',
+ '暴雨': '⛈️',
+ '雷阵雨': '⛈️',
+ '雪': '❄️',
+ '小雪': '🌨️',
+ '中雪': '❄️',
+ '大雪': '❄️',
+ '暴雪': '❄️',
+ '雾': '🌫️',
+ '霾': '🌫️',
+ '沙尘暴': '🌪️',
+ '扬沙': '🌪️',
+ '浮尘': '🌪️',
+ '台风': '🌀',
+ '飓风': '🌀',
+ '龙卷风': '🌪️',
+ '冰雹': '🧊',
+ '冻雨': '🌨️',
+ '雨夹雪': '🌨️',
+ 'sunny': '🌞',
+ 'clear': '🌞',
+ 'cloudy': '☁️',
+ 'overcast': '☁️',
+ 'rain': '🌧️',
+ 'light rain': '🌦️',
+ 'heavy rain': '⛈️',
+ 'thunderstorm': '⛈️',
+ 'snow': '❄️',
+ 'fog': '🌫️',
+ 'haze': '🌫️',
+ 'sandstorm': '🌪️',
+ 'typhoon': '🌀',
+ 'hurricane': '🌀',
+ 'tornado': '🌪️',
+ 'hail': '🧊'
+ }
+
+ # 模糊匹配天气描述
+ weather_desc_lower = str(weather_desc).lower()
+ for key, emoji in weather_emoji_map.items():
+ if key in weather_desc_lower:
+ return emoji
+
+ # 默认返回晴天图标
+ return '🌞'
+
def create_weather_group(self):
"""创建天气组件组"""
if self.weather_group is not None:
return self.weather_group
- weather_group = self.create_ribbon_group("天气")
+ weather_group = self.create_ribbon_group("天气", is_special_group=True)
+ weather_group.setFixedWidth(200) # 增加整体宽度
weather_layout = QVBoxLayout()
+ weather_layout.setSpacing(8) # 增加行间距
+
+ # 第一行:天气图标和温度显示(居中对齐)
+ weather_display_layout = QHBoxLayout()
+ weather_display_layout.setSpacing(10) # 增加图标和温度间距
+
+ # 添加左侧弹性空间,推动内容到中心
+ weather_display_layout.addStretch()
+
+ # 天气图标标签 - 优化垂直居中对齐
+ self.weather_icon_label = QLabel("🌞")
+ self.weather_icon_label.setAlignment(Qt.AlignCenter) # 使用Qt对齐方式
+ self.weather_icon_label.setStyleSheet("""
+ QLabel {
+ font-size: 32px;
+ padding: 0px;
+ margin: 0px;
+ border: none;
+ background: transparent;
+ }
+ """)
+ self.weather_icon_label.setFixedSize(40, 40) # 增大图标尺寸
+
+ # 温度标签 - 优化垂直居中对齐
+ self.weather_temp_label = QLabel("--°C")
+ self.weather_temp_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) # 使用Qt对齐方式
+ # 初始化时使用默认颜色,主题切换时会更新
+ self.weather_temp_label.setStyleSheet("""
+ QLabel {
+ font-size: 16px;
+ font-weight: bold;
+ padding: 0px;
+ margin: 0px;
+ border: none;
+ background: transparent;
+ min-height: 30px; /* 确保最小高度 */
+ }
+ """)
+ self.weather_temp_label.setFixedSize(70, 30) # 增加温度标签宽度
+
+ weather_display_layout.addWidget(self.weather_icon_label)
+ weather_display_layout.addWidget(self.weather_temp_label)
+
+ # 添加右侧弹性空间,确保内容居中
+ weather_display_layout.addStretch()
+
+ # 第二行:城市选择和刷新按钮(居中对齐)
+ control_layout = QHBoxLayout()
+ control_layout.setSpacing(8) # 增加控件间距
+
+ # 添加左侧弹性空间,推动内容到中心
+ control_layout.addStretch()
# 城市选择 - 添加所有省会城市
self.city_combo = QComboBox()
- self.city_combo.setFixedWidth(120) # 增加宽度以显示完整城市名
+ self.city_combo.setFixedWidth(120) # 增加城市选择框宽度
+ self.city_combo.setStyleSheet("QComboBox { font-size: 12px; padding: 3px; }") # 增大字体和间距
self.city_combo.addItems([
'自动定位',
'北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', # 一线城市
@@ -487,12 +694,40 @@ class WordRibbon(QFrame):
self.city_combo.currentTextChanged.connect(self.on_city_changed)
# 刷新按钮
- self.refresh_weather_btn = QPushButton("刷新天气")
- self.refresh_weather_btn.clicked.connect(self.on_refresh_weather)
- self.refresh_weather_btn.setFixedSize(80, 25)
-
- weather_layout.addWidget(self.city_combo)
- weather_layout.addWidget(self.refresh_weather_btn)
+ self.refresh_weather_btn = QPushButton("🔄 刷新")
+ self.refresh_weather_btn.setFixedSize(60, 30) # 增大刷新按钮尺寸
+ self.refresh_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
+ self.refresh_weather_btn.setToolTip("刷新天气")
+
+ # 悬浮窗口按钮
+ self.floating_weather_btn = QPushButton("🪟 悬浮")
+ self.floating_weather_btn.setFixedSize(60, 30)
+ self.floating_weather_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
+ self.floating_weather_btn.setToolTip("切换天气悬浮窗口")
+
+ # 每日谏言悬浮窗口按钮
+ self.floating_quote_btn = QPushButton("📜 悬浮")
+ self.floating_quote_btn.setFixedSize(60, 30)
+ self.floating_quote_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
+ self.floating_quote_btn.setToolTip("切换每日谏言悬浮窗口")
+
+ # 日历悬浮窗口按钮
+ self.floating_calendar_btn = QPushButton("📅 悬浮")
+ self.floating_calendar_btn.setFixedSize(60, 30)
+ self.floating_calendar_btn.setStyleSheet("QPushButton { font-size: 11px; padding: 5px; }")
+ self.floating_calendar_btn.setToolTip("切换日历悬浮窗口")
+
+ control_layout.addWidget(self.city_combo)
+ control_layout.addWidget(self.refresh_weather_btn)
+ control_layout.addWidget(self.floating_weather_btn)
+ control_layout.addWidget(self.floating_quote_btn)
+ control_layout.addWidget(self.floating_calendar_btn)
+
+ # 添加右侧弹性空间,确保内容居中
+ control_layout.addStretch()
+
+ weather_layout.addLayout(weather_display_layout)
+ weather_layout.addLayout(control_layout)
weather_group.setLayout(weather_layout)
self.weather_group = weather_group
@@ -521,7 +756,7 @@ class WordRibbon(QFrame):
if self.quote_group is not None:
return self.quote_group
- quote_group = self.create_ribbon_group("每日一言")
+ quote_group = self.create_ribbon_group("每日一言", is_special_group=True)
quote_layout = QVBoxLayout()
# 创建第一行:类型选择下拉框和刷新按钮
@@ -535,7 +770,6 @@ class WordRibbon(QFrame):
# 刷新按钮
self.refresh_quote_btn = QPushButton("刷新箴言")
- self.refresh_quote_btn.clicked.connect(self.on_refresh_quote)
self.refresh_quote_btn.setFixedSize(80, 25)
# 添加到第一行布局
@@ -584,31 +818,66 @@ class WordRibbon(QFrame):
self.quote_group = None
self.quote_visible = False
- def create_ribbon_group(self, title):
- """创建功能区组"""
- group = QGroupBox(title)
- group.setStyleSheet("""
- QGroupBox {
- font-size: 11px;
- font-weight: normal;
- color: #333333;
- border: 1px solid #e1e1e1;
- border-radius: 0px;
- margin-top: 5px;
- padding-top: 5px;
- }
- QGroupBox::title {
- subcontrol-origin: margin;
- left: 10px;
- padding: 0 5px 0 5px;
- }
- """)
- return group
+ def _update_group_style(self, group):
+ """更新组样式以适配当前主题"""
+ is_dark = theme_manager.is_dark_theme()
+ colors = theme_manager.get_current_theme_colors()
+
+ if is_dark:
+ group.setStyleSheet(f"""
+ QGroupBox {{
+ font-size: 11px;
+ font-weight: normal;
+ color: {colors['text']};
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 0px;
+ margin-top: 5px;
+ padding-top: 5px;
+ }}
+ QGroupBox::title {{
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 5px 0 5px;
+ color: {colors['text']};
+ /* 确保标题不会被截断 */
+ white-space: nowrap;
+ text-overflow: clip; /* 不显示省略号 */
+ overflow: visible; /* 允许内容溢出 */
+ }}
+ """)
+ else:
+ group.setStyleSheet(f"""
+ QGroupBox {{
+ font-size: 11px;
+ font-weight: normal;
+ color: {colors['text']};
+ background-color: {colors['surface']};
+ border: 1px solid {colors['border']};
+ border-radius: 0px;
+ margin-top: 5px;
+ padding-top: 5px;
+ }}
+ QGroupBox::title {{
+ subcontrol-origin: margin;
+ left: 10px;
+ padding: 0 5px 0 5px;
+ color: {colors['text']};
+ /* 确保标题不会被截断 */
+ white-space: nowrap;
+ text-overflow: clip; /* 不显示省略号 */
+ overflow: visible; /* 允许内容溢出 */
+ }}
+ """)
def on_refresh_weather(self):
"""刷新天气按钮点击处理"""
pass
+ def on_floating_weather(self):
+ """悬浮窗口按钮点击处理"""
+ pass
+
def on_city_changed(self, city):
"""城市选择变化处理"""
pass
@@ -685,98 +954,268 @@ class WordRibbon(QFrame):
btn.setText(text)
btn.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
btn.setFixedSize(60, 60)
- btn.setStyleSheet("""
- QToolButton {
- border: 1px solid transparent;
- border-radius: 3px;
- background-color: transparent;
- font-size: 11px;
- color: #333333;
- }
- QToolButton:hover {
- background-color: #f0f0f0;
- border: 1px solid #d0d0d0;
- }
- QToolButton:pressed {
- background-color: #e1e1e1;
- border: 1px solid #c0c0c0;
- }
- """)
+
+ # 连接主题切换信号以动态更新样式
+ theme_manager.theme_changed.connect(lambda: self._update_button_style(btn))
+
+ # 立即应用当前主题样式
+ self._update_button_style(btn)
+
return btn
+ def _update_button_style(self, btn):
+ """更新按钮样式以适配当前主题"""
+ is_dark = theme_manager.is_dark_theme()
+ colors = theme_manager.get_current_theme_colors()
+
+ if is_dark:
+ btn.setStyleSheet(f"""
+ QToolButton {{
+ border: 1px solid transparent;
+ border-radius: 3px;
+ background-color: transparent;
+ font-size: 11px;
+ color: {colors['text']};
+ }}
+ QToolButton:hover {{
+ background-color: {colors['surface_hover']};
+ border: 1px solid {colors['border']};
+ }}
+ QToolButton:pressed {{
+ background-color: #5a5a5c;
+ border: 1px solid #6a6a6c;
+ }}
+ """)
+ else:
+ btn.setStyleSheet(f"""
+ QToolButton {{
+ border: 1px solid transparent;
+ border-radius: 3px;
+ background-color: transparent;
+ font-size: 11px;
+ color: {colors['text']};
+ }}
+ QToolButton:hover {{
+ background-color: #f0f0f0;
+ border: 1px solid #d0d0d0;
+ }}
+ QToolButton:pressed {{
+ background-color: #e1e1e1;
+ border: 1px solid #c0c0c0;
+ }}
+ """)
+
def create_toggle_button(self, text, icon_name):
- """创建切换按钮"""
+ """创建切换按钮 - 现代极简主义风格"""
btn = QToolButton()
btn.setText(text)
btn.setCheckable(True)
btn.setToolButtonStyle(Qt.ToolButtonTextOnly)
# 根据文本长度设置宽度,中文字符需要更宽
if len(text) <= 1:
- btn.setFixedSize(30, 25) # 单个字符(如B、I、U)
+ btn.setFixedSize(32, 28) # 单个字符(如B、I、U)
elif len(text) <= 2:
- btn.setFixedSize(45, 25) # 两个字符(如"居中")
+ btn.setFixedSize(48, 28) # 两个字符(如"居中")
else:
- btn.setFixedSize(60, 25) # 三个字符及以上(如"左对齐"、"两端对齐")
- btn.setStyleSheet("""
- QToolButton {
- border: 1px solid #d0d0d0;
- border-radius: 2px;
- background-color: transparent;
- font-size: 12px;
- font-weight: bold;
- color: #333333;
- }
- QToolButton:hover {
- background-color: #f0f0f0;
- }
- QToolButton:checked {
- background-color: #e1e1e1;
- border: 1px solid #c0c0c0;
- }
- """)
+ btn.setFixedSize(64, 28) # 三个字符及以上(如"左对齐"、"两端对齐")
+
+ # 连接主题切换信号以动态更新样式
+ theme_manager.theme_changed.connect(lambda: self._update_toggle_button_style(btn))
+
+ # 立即应用当前主题样式
+ self._update_toggle_button_style(btn)
+
return btn
+ def _update_toggle_button_style(self, btn):
+ """更新切换按钮样式以适配当前主题"""
+ is_dark = theme_manager.is_dark_theme()
+ colors = theme_manager.get_current_theme_colors()
+
+ if is_dark:
+ btn.setStyleSheet(f"""
+ QToolButton {{
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ padding: 6px 10px;
+ color: {colors['text']};
+ font-size: 13px;
+ font-weight: 500;
+ }}
+ QToolButton:hover {{
+ background-color: {colors['surface_hover']};
+ border: 1px solid {colors['border']};
+ }}
+ QToolButton:pressed {{
+ background-color: #5a5a5c;
+ border: 1px solid #6a6a6c;
+ }}
+ QToolButton:checked {{
+ background-color: {colors['accent']};
+ border: 1px solid {colors['accent']};
+ color: {colors['surface']};
+ }}
+ """)
+ else:
+ btn.setStyleSheet(f"""
+ QToolButton {{
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ padding: 6px 10px;
+ color: #4a5568;
+ font-size: 13px;
+ font-weight: 500;
+ }}
+ QToolButton:hover {{
+ background-color: #f7fafc;
+ border: 1px solid #e2e8f0;
+ }}
+ QToolButton:pressed {{
+ background-color: #edf2f7;
+ border: 1px solid #cbd5e0;
+ }}
+ QToolButton:checked {{
+ background-color: #ebf8ff;
+ border: 1px solid #bee3f8;
+ color: #3182ce;
+ }}
+ """)
+
def create_color_button(self, text, icon_name):
- """创建颜色选择按钮"""
+ """创建颜色选择按钮 - 现代极简主义风格"""
btn = QToolButton()
btn.setText(text)
btn.setToolButtonStyle(Qt.ToolButtonTextOnly)
- btn.setFixedSize(30, 25)
- btn.setStyleSheet("""
- QToolButton {
- border: 1px solid #d0d0d0;
- border-radius: 2px;
- background-color: transparent;
- font-size: 12px;
- font-weight: bold;
- color: #333333;
- }
- QToolButton:hover {
- background-color: #f0f0f0;
- }
- QToolButton:pressed {
- background-color: #e1e1e1;
- border: 1px solid #c0c0c0;
- }
- """)
+ btn.setFixedSize(32, 28)
+
+ # 连接主题切换信号以动态更新样式
+ theme_manager.theme_changed.connect(lambda: self._update_color_button_style(btn))
+
+ # 立即应用当前主题样式
+ self._update_color_button_style(btn)
+
return btn
+
+ def _update_color_button_style(self, btn):
+ """更新颜色按钮样式以适配当前主题"""
+ is_dark = theme_manager.is_dark_theme()
+ colors = theme_manager.get_current_theme_colors()
+
+ if is_dark:
+ btn.setStyleSheet(f"""
+ QToolButton {{
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ padding: 6px 10px;
+ color: {colors['text']};
+ font-size: 13px;
+ font-weight: 500;
+ }}
+ QToolButton:hover {{
+ background-color: {colors['surface_hover']};
+ border: 1px solid {colors['border']};
+ }}
+ QToolButton:pressed {{
+ background-color: #5a5a5c;
+ border: 1px solid #6a6a6c;
+ }}
+ """)
+ else:
+ btn.setStyleSheet(f"""
+ QToolButton {{
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ padding: 6px 10px;
+ color: #4a5568;
+ font-size: 13px;
+ font-weight: 500;
+ }}
+ QToolButton:hover {{
+ background-color: #f7fafc;
+ border: 1px solid #e2e8f0;
+ }}
+ QToolButton:pressed {{
+ background-color: #edf2f7;
+ border: 1px solid #cbd5e0;
+ }}
+ """)
+
+ def create_style_button(self, text):
+ """创建样式按钮 - 现代极简主义风格"""
+ btn = QPushButton(text)
+ btn.setFixedSize(60, 28)
+
+ # 连接主题切换信号以动态更新样式
+ theme_manager.theme_changed.connect(lambda: self._update_style_button_style(btn))
+
+ # 立即应用当前主题样式
+ self._update_style_button_style(btn)
+
+ return btn
+
+ def _update_style_button_style(self, btn):
+ """更新样式按钮样式以适配当前主题"""
+ is_dark = theme_manager.is_dark_theme()
+ colors = theme_manager.get_current_theme_colors()
+
+ if is_dark:
+ btn.setStyleSheet(f"""
+ QPushButton {{
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ padding: 6px 10px;
+ color: {colors['text']};
+ font-size: 12px;
+ font-weight: 500;
+ text-align: center;
+ }}
+ QPushButton:hover {{
+ background-color: {colors['surface_hover']};
+ border: 1px solid {colors['border']};
+ }}
+ QPushButton:pressed {{
+ background-color: #5a5a5c;
+ border: 1px solid #6a6a6c;
+ }}
+ """)
+ else:
+ btn.setStyleSheet(f"""
+ QPushButton {{
+ background-color: transparent;
+ border: 1px solid transparent;
+ border-radius: 8px;
+ padding: 6px 10px;
+ color: #4a5568;
+ font-size: 12px;
+ font-weight: 500;
+ text-align: center;
+ }}
+ QPushButton:hover {{
+ background-color: #f7fafc;
+ border: 1px solid #e2e8f0;
+ }}
+ QPushButton:pressed {{
+ background-color: #edf2f7;
+ border: 1px solid #cbd5e0;
+ }}
+ """)
class WordStatusBar(QStatusBar):
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
+ # 连接主题切换信号
+ theme_manager.theme_changed.connect(self.apply_theme)
+ # 应用初始主题
+ self.apply_theme()
def setup_ui(self):
- """设置状态栏"""
- self.setStyleSheet("""
- QStatusBar {
- background-color: #f3f2f1;
- border-top: 1px solid #d0d0d0;
- font-size: 11px;
- color: #333333;
- }
- """)
-
+ """设置状态栏 - 现代极简主义风格"""
# 添加状态栏项目
self.page_label = QLabel("第 1 页,共 1 页")
self.words_label = QLabel("字数: 0")
@@ -787,12 +1226,61 @@ class WordStatusBar(QStatusBar):
self.addPermanentWidget(self.words_label)
self.addPermanentWidget(self.language_label)
self.addPermanentWidget(self.input_mode_label)
+
+ def apply_theme(self):
+ """应用当前主题到状态栏"""
+ is_dark = theme_manager.is_dark_theme()
+ colors = theme_manager.get_current_theme_colors()
+
+ if is_dark:
+ self.setStyleSheet(f"""
+ QStatusBar {{
+ background-color: {colors['surface']};
+ border-top: 1px solid {colors['border']};
+ font-size: 12px;
+ color: {colors['text_secondary']};
+ font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ padding: 8px;
+ }}
+ QStatusBar QLabel {{
+ background-color: transparent;
+ padding: 4px 8px;
+ border-radius: 4px;
+ color: {colors['text_secondary']};
+ }}
+ QStatusBar QLabel:hover {{
+ background-color: {colors['surface_hover']};
+ }}
+ """)
+ else:
+ self.setStyleSheet("""
+ QStatusBar {
+ background-color: #ffffff;
+ border-top: 1px solid #e2e8f0;
+ font-size: 12px;
+ color: #718096;
+ font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ padding: 8px;
+ }
+ QStatusBar QLabel {
+ background-color: transparent;
+ padding: 4px 8px;
+ border-radius: 4px;
+ }
+ QStatusBar QLabel:hover {
+ background-color: #f7fafc;
+ }
+ """)
class WordTextEdit(QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.input_processor = None # 输入处理器引用
+ # 连接主题切换信号
+ theme_manager.theme_changed.connect(self.apply_theme)
+ # 应用初始主题
+ self.apply_theme()
def set_input_processor(self, processor):
"""设置输入处理器"""
@@ -872,19 +1360,7 @@ class WordTextEdit(QTextEdit):
return super().inputMethodQuery(query)
def setup_ui(self):
- """设置文本编辑区域样式"""
- self.setStyleSheet("""
- QTextEdit {
- background-color: #ffffff;
- border: 1px solid #d0d0d0;
- font-family: 'Calibri', 'Microsoft YaHei', '微软雅黑', sans-serif;
- font-size: 12pt;
- color: #000000;
- padding: 20px;
- line-height: 1.5;
- }
- """)
-
+ """设置文本编辑区域样式 - 现代极简主义风格"""
# 设置页面边距和背景
self.setViewportMargins(50, 50, 50, 50)
@@ -898,6 +1374,46 @@ class WordTextEdit(QTextEdit):
# 设置光标宽度
self.setCursorWidth(2)
+
+ def apply_theme(self):
+ """应用当前主题到文本编辑区域"""
+ is_dark = theme_manager.is_dark_theme()
+ colors = theme_manager.get_current_theme_colors()
+
+ if is_dark:
+ self.setStyleSheet(f"""
+ QTextEdit {{
+ background-color: {colors['surface']};
+ border: none;
+ font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ font-size: 15px;
+ color: {colors['text']};
+ padding: 40px;
+ line-height: 1.7;
+ selection-background-color: rgba(66, 153, 225, 0.3);
+ selection-color: {colors['text']};
+ }}
+ QTextEdit:focus {{
+ outline: none;
+ }}
+ """)
+ else:
+ self.setStyleSheet("""
+ QTextEdit {
+ background-color: #ffffff;
+ border: none;
+ font-family: 'Inter', 'SF Pro Display', 'Microsoft YaHei', '微软雅黑', sans-serif;
+ font-size: 15px;
+ color: #2d3748;
+ padding: 40px;
+ line-height: 1.7;
+ selection-background-color: rgba(66, 153, 225, 0.2);
+ selection-color: #2d3748;
+ }
+ QTextEdit:focus {
+ outline: none;
+ }
+ """)
class WeatherAPI:
def __init__(self):
@@ -1016,14 +1532,17 @@ class WeatherAPI:
# 使用免费的天气API
url = f"http://t.weather.sojson.com/api/weather/city/{city_id}"
print(f"获取当前天气: {url}")
- response = requests.get(url, timeout=10)
+ response = requests.get(url, timeout=5) # 减少超时时间
response.raise_for_status()
data = response.json()
- print(f"当前天气响应: {data}")
- if data['status'] == 200:
- city_info = data['cityInfo']
- current_data = data['data']
+ if data.get('status') == 200:
+ city_info = data.get('cityInfo', {})
+ current_data = data.get('data', {})
+
+ if not current_data:
+ print("天气数据为空")
+ return None
# 获取生活提示信息
life_tips = []
@@ -1047,20 +1566,35 @@ class WeatherAPI:
while len(life_tips) < 3 and default_tips:
life_tips.append(default_tips.pop(0))
+ # 安全获取天气数据
+ wendu = current_data.get('wendu', 'N/A')
+ shidu = current_data.get('shidu', 'N/A')
+ first_forecast = forecast[0] if forecast else {}
+
weather_info = {
- 'temp': current_data['wendu'],
- 'feels_like': current_data['wendu'], # 没有体感温度,用实际温度代替
- 'weather': current_data['forecast'][0]['type'],
- 'humidity': current_data['shidu'].replace('%', ''),
- 'wind_dir': current_data['forecast'][0]['fx'],
+ 'temp': wendu,
+ 'feels_like': wendu, # 没有体感温度,用实际温度代替
+ 'weather': first_forecast.get('type', '晴'), # 默认晴天
+ 'humidity': shidu.replace('%', '') if shidu != 'N/A' else '50',
+ 'wind_dir': first_forecast.get('fx', '无风'),
'wind_scale': '1', # 没有风力等级,用默认值
- 'vis': current_data['forecast'][0]['high'], # 用最高温作为可见度
+ 'vis': first_forecast.get('high', '高温 15℃'), # 用最高温作为可见度
'pressure': '1013', # 没有气压,用默认值
'life_tips': life_tips # 添加生活提示信息
}
print(f"解析后的天气信息: {weather_info}")
return weather_info
- print(f"获取天气失败,状态码: {data.get('status')}")
+ else:
+ print(f"获取天气失败,状态码: {data.get('status')}")
+ return None
+ except requests.exceptions.Timeout:
+ print("获取天气超时")
+ return None
+ except requests.exceptions.RequestException as e:
+ print(f"网络请求失败: {e}")
+ return None
+ except json.JSONDecodeError as e:
+ print(f"JSON解析失败: {e}")
return None
except Exception as e:
print(f"获取当前天气失败: {e}")
@@ -1123,27 +1657,150 @@ class WeatherAPI:
def get_isp_info(self):
"""获取ISP信息"""
try:
- url = "http://ip-api.com/json/"
- response = requests.get(url, timeout=5)
- response.raise_for_status()
+ # 尝试多个ISP信息服务以提高成功率
+
+ # 方法1: 使用ip-api.com接口
+ try:
+ url = "http://ip-api.com/json/"
+ headers = {'User-Agent': 'MagicWord/1.0'}
+ response = requests.get(url, timeout=5, headers=headers)
+ response.raise_for_status()
+
+ data = response.json()
+ if data.get('status') == 'success':
+ isp = data.get('isp', '')
+ org = data.get('org', '')
+ as_info = data.get('as', '')
+ country = data.get('country', '')
+ return f"{isp} {org} {as_info} {country}".strip()
+ except Exception as e:
+ print(f"ip-api ISP信息获取失败: {e}")
+ pass
+
+ # 方法2: 使用ipinfo.io接口
+ try:
+ url = "https://ipinfo.io/json"
+ headers = {'User-Agent': 'MagicWord/1.0'}
+ response = requests.get(url, timeout=5, headers=headers)
+ response.raise_for_status()
+
+ data = response.json()
+ if 'org' in data:
+ org = data.get('org', '')
+ country = data.get('country', '')
+ return f"{org} {country}".strip()
+ except Exception as e:
+ print(f"ipinfo ISP信息获取失败: {e}")
+ pass
+
+ # 方法3: 使用httpbin.org获取基础信息
+ try:
+ url = "https://httpbin.org/ip"
+ headers = {'User-Agent': 'MagicWord/1.0'}
+ response = requests.get(url, timeout=5, headers=headers)
+ response.raise_for_status()
+ # 这个接口主要用于获取IP,不是ISP信息,但可以作为备选
+ data = response.json()
+ origin = data.get('origin', '')
+ if origin:
+ return f"IP: {origin}"
+ except Exception as e:
+ print(f"httpbin ISP信息获取失败: {e}")
+ pass
- data = response.json()
- if data.get('status') == 'success':
- isp = data.get('isp', '')
- org = data.get('org', '')
- as_info = data.get('as', '')
- return f"{isp} {org} {as_info}".strip()
return None
except Exception as e:
- print(f"获取ISP信息失败: {e}")
+ print(f"获取ISP信息总体失败: {e}")
return None
def get_location_by_ip(self):
"""通过IP地址获取用户位置"""
try:
+ # 首先获取公网IP地址
+ ip_address = None
+ try:
+ # 使用多个IP获取服务确保能获取到公网IP
+ ip_services = [
+ "https://api.ipify.org",
+ "https://icanhazip.com",
+ "https://ident.me",
+ "https://ipecho.net/plain",
+ "https://myexternalip.com/raw"
+ ]
+
+ for service in ip_services:
+ try:
+ response = requests.get(service, timeout=3)
+ if response.status_code == 200:
+ ip_address = response.text.strip()
+ # 验证是否为有效的IPv4地址
+ import re
+ if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', ip_address):
+ print(f"获取到公网IP: {ip_address}")
+ break
+ else:
+ ip_address = None
+ except:
+ continue
+
+ if not ip_address:
+ print("无法获取公网IP地址")
+ except Exception as e:
+ print(f"获取IP地址失败: {e}")
+
# 尝试多个免费的IP地理位置API
- # API 1: 搜狐IP接口(HTTP,无SSL问题)
+ # API 1: 使用ip-api.com接口(更稳定的免费服务,支持HTTPS)
+ try:
+ if ip_address:
+ url = f"https://ip-api.com/json/{ip_address}"
+ else:
+ url = "https://ip-api.com/json/"
+ headers = {'User-Agent': 'MagicWord/1.0'}
+ response = requests.get(url, timeout=5, headers=headers)
+ response.raise_for_status()
+
+ data = response.json()
+ if data.get('status') == 'success':
+ city = data.get('city', '')
+ region = data.get('regionName', '')
+ country = data.get('country', '')
+ if city and city not in ['null', 'None', '']:
+ print(f"ip-api定位成功: {city}, {region}, {country}")
+ # 如果城市信息不完整,尝试用地区信息补充
+ if len(city) < 2 and region:
+ city = region
+ return city
+ except Exception as e:
+ print(f"ip-api接口失败: {e}")
+ pass
+
+ # API 2: 使用ipinfo.io接口(需要处理免费版限制)
+ try:
+ if ip_address:
+ url = f"https://ipinfo.io/{ip_address}/json"
+ else:
+ url = "https://ipinfo.io/json"
+ headers = {'User-Agent': 'MagicWord/1.0'}
+ response = requests.get(url, timeout=5, headers=headers)
+ response.raise_for_status()
+
+ data = response.json()
+ if 'city' in data:
+ city = data.get('city', '')
+ region = data.get('region', '')
+ country = data.get('country', '')
+ if city and city not in ['null', 'None', '']:
+ print(f"ipinfo定位成功: {city}, {region}, {country}")
+ # 如果城市信息不完整,尝试用地区信息补充
+ if len(city) < 2 and region:
+ city = region
+ return city
+ except Exception as e:
+ print(f"ipinfo接口失败: {e}")
+ pass
+
+ # API 3: 搜狐IP接口(HTTP,无SSL问题)
try:
url = "http://pv.sohu.com/cityjson?ie=utf-8"
response = requests.get(url, timeout=5)
@@ -1168,7 +1825,7 @@ class WeatherAPI:
print(f"搜狐IP接口失败: {e}")
pass
- # API 2: 使用pconline接口(HTTP)
+ # API 4: 使用pconline接口(HTTP)
try:
url = "http://whois.pconline.com.cn/ipJson.jsp"
response = requests.get(url, timeout=5)
@@ -1191,26 +1848,14 @@ class WeatherAPI:
print(f"pconline接口失败: {e}")
pass
- # API 3: 使用ip-api.com接口(更稳定的免费服务)
- try:
- url = "http://ip-api.com/json/"
- response = requests.get(url, timeout=5)
- response.raise_for_status()
-
- data = response.json()
- if data.get('status') == 'success':
- city = data.get('city', '')
- if city:
- print(f"ip-api定位成功: {city}")
- return city
- except Exception as e:
- print(f"ip-api接口失败: {e}")
- pass
-
- # API 4: 使用淘宝IP接口
+ # API 5: 使用淘宝IP接口
try:
- url = "http://ip.taobao.com/outGetIpInfo"
- params = {'ip': '', 'accessKey': 'alibaba-inc'}
+ if ip_address:
+ url = "http://ip.taobao.com/outGetIpInfo"
+ params = {'ip': ip_address, 'accessKey': 'alibaba-inc'}
+ else:
+ url = "http://ip.taobao.com/outGetIpInfo"
+ params = {'ip': '', 'accessKey': 'alibaba-inc'}
response = requests.get(url, params=params, timeout=5)
response.raise_for_status()
@@ -1229,25 +1874,42 @@ class WeatherAPI:
return None
except Exception as e:
print(f"IP定位总体失败: {e}")
- return None
+ # 返回默认城市而不是None,确保天气功能仍然可用
+ return "北京"
def get_current_location(self):
"""获取当前位置信息"""
try:
# 首先尝试通过IP获取位置
- city = self.get_location_by_ip()
- if city:
- print(f"通过IP定位成功: {city}")
+ location_result = self.get_location_by_ip()
+
+ # 检查是否是默认城市(表示IP定位失败)
+ if location_result == "北京":
+ print("IP定位失败,使用默认城市")
+ print("自动定位失败,建议手动选择城市")
+ return None
+
+ if location_result:
+ print(f"通过IP定位成功: {location_result}")
# 检查是否是教育网或特殊网络环境
isp_info = self.get_isp_info()
- if isp_info and ('教育网' in isp_info or 'CERNET' in isp_info or 'University' in isp_info):
+ if isp_info and ('教育网' in isp_info or 'CERNET' in isp_info or 'University' in isp_info or '大学' in isp_info):
print(f"检测到教育网环境: {isp_info}")
print("教育网IP定位可能不准确,建议手动选择城市")
# 教育网环境下,如果定位到北京,可能是IP分配问题
- if city.lower() in ['beijing', '北京', 'haidian', '海淀']:
+ if isinstance(location_result, str) and location_result.lower() in ['beijing', '北京', 'haidian', '海淀']:
print("提示:教育网环境下北京定位可能是网络出口导致的")
- return {'city': city, 'note': '教育网环境,定位可能不准确', 'isp': isp_info}
+ return {'city': location_result, 'note': '教育网环境,定位可能不准确', 'isp': isp_info}
+
+ # 处理返回结果格式
+ city = None
+ if isinstance(location_result, dict) and 'city' in location_result:
+ city = location_result['city']
+ elif isinstance(location_result, str):
+ city = location_result
+ else:
+ city = str(location_result)
# 智能处理 - 如果是区级单位,映射到市级城市
district_to_city_map = {
@@ -1395,6 +2057,7 @@ class WeatherAPI:
except Exception as e:
print(f"获取当前位置失败: {e}")
+ # 即使出现异常也返回None而不是抛出异常,确保程序继续运行
return None
def get_city_weather_by_name(self, city_name):
diff --git a/src/word_main_window.py b/src/word_main_window.py
index 2a7f7da..28973f1 100644
--- a/src/word_main_window.py
+++ b/src/word_main_window.py
@@ -1,13 +1,15 @@
# word_main_window.py
import sys
import os
+import platform
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QLabel, QSplitter, QFrame, QMenuBar,
QAction, QFileDialog, QMessageBox, QApplication,
QDialog, QLineEdit, QCheckBox, QPushButton, QListWidget,
- QListWidgetItem, QScrollArea)
+ QListWidgetItem, QScrollArea, QSizePolicy,
+ QGraphicsScene, QGraphicsView)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect, QByteArray, QBuffer, QIODevice
-from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextDocument, QImage, QTextImageFormat, QTextFormat
+from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor, QTextDocument, QImage, QTextImageFormat, QTextFormat, QTextBlockFormat
from ui.word_style_ui import (WordRibbon, WordStatusBar, WordTextEdit,
)
@@ -16,6 +18,10 @@ from typing_logic import TypingLogic
from ui.word_style_ui import WeatherAPI
from file_parser import FileParser
from input_handler.input_processor import InputProcessor
+from ui.calendar_widget import CalendarWidget
+from ui.weather_floating_widget import WeatherFloatingWidget
+from ui.quote_floating_widget import QuoteFloatingWidget
+from ui.calendar_floating_widget import CalendarFloatingWidget
# 导入主题管理器
from ui.theme_manager import theme_manager
@@ -83,6 +89,13 @@ class WordStyleMainWindow(QMainWindow):
self.learning_text = "" # 学习模式下的文本内容
self.cursor_position = 0 # 光标位置
+ # 学习模式窗口引用和同步标记
+ self.learning_window = None # 学习模式窗口引用
+ self.sync_from_learning = False # 从学习模式同步内容的标记
+
+ # DeepSeek对话窗口引用
+ self.deepseek_dialog = None # DeepSeek对话窗口引用
+
# 统一文档内容管理
self.unified_document_content = "" # 统一文档内容
self.last_edit_mode = "typing" # 上次编辑模式
@@ -97,6 +110,27 @@ class WordStyleMainWindow(QMainWindow):
self.network_service = NetworkService()
self.weather_api = WeatherAPI()
+ # 初始化日历组件
+ self.calendar_widget = CalendarWidget(self)
+ self.calendar_widget.hide() # 默认隐藏
+
+ # 初始化天气悬浮窗口
+ self.weather_floating_widget = WeatherFloatingWidget(self)
+ self.weather_floating_widget.hide() # 默认隐藏
+ self.weather_floating_widget.closed.connect(self.on_weather_floating_closed)
+ self.weather_floating_widget.refresh_requested.connect(self.refresh_weather)
+
+ self.quote_floating_widget = QuoteFloatingWidget(self)
+ self.quote_floating_widget.hide() # 默认隐藏
+ self.quote_floating_widget.closed.connect(self.on_quote_floating_closed)
+ self.quote_floating_widget.refresh_requested.connect(self.refresh_daily_quote)
+ self.quote_floating_widget.insert_requested.connect(self.insert_quote_to_cursor)
+
+ self.calendar_floating_widget = CalendarFloatingWidget(self)
+ self.calendar_floating_widget.hide() # 默认隐藏
+ self.calendar_floating_widget.closed.connect(self.on_calendar_floating_closed)
+ self.calendar_floating_widget.date_selected.connect(self.insert_date_to_cursor)
+
# 设置窗口属性
self.setWindowTitle("文档1 - MagicWord")
self.setGeometry(100, 100, 1200, 800)
@@ -123,6 +157,14 @@ class WordStyleMainWindow(QMainWindow):
self.ribbon.on_refresh_weather = self.refresh_weather
self.ribbon.on_city_changed = self.on_city_changed
+ # 连接Ribbon的悬浮窗口按钮信号
+ if hasattr(self.ribbon, 'floating_weather_btn'):
+ self.ribbon.floating_weather_btn.clicked.connect(self.toggle_floating_weather)
+ if hasattr(self.ribbon, 'floating_quote_btn'):
+ self.ribbon.floating_quote_btn.clicked.connect(self.toggle_floating_quote)
+ if hasattr(self.ribbon, 'floating_calendar_btn'):
+ self.ribbon.floating_calendar_btn.clicked.connect(self.toggle_floating_calendar)
+
# 初始化时刷新天气
self.refresh_weather()
@@ -131,9 +173,8 @@ class WordStyleMainWindow(QMainWindow):
# 连接主题切换信号
theme_manager.theme_changed.connect(self.on_theme_changed)
- # 设置默认为白色模式(禁用自动检测)
- theme_manager.enable_auto_detection(False)
- theme_manager.set_dark_theme(False)
+ # 启用系统主题自动检测
+ theme_manager.enable_auto_detection(True)
# 应用当前主题
self.apply_theme()
@@ -314,6 +355,11 @@ class WordStyleMainWindow(QMainWindow):
# 更新功能区下拉框样式
if hasattr(self, 'ribbon'):
self.update_ribbon_styles(is_dark)
+
+ # 更新日历组件样式
+ if hasattr(self, 'calendar_widget') and self.calendar_widget is not None:
+ # 日历组件有自己的主题管理机制,只需触发其主题更新
+ self.calendar_widget.apply_theme()
def update_ribbon_styles(self, is_dark):
"""更新功能区样式"""
@@ -425,8 +471,14 @@ class WordStyleMainWindow(QMainWindow):
print(f"获取到天气数据: {weather_data}")
# 直接传递原始数据,update_weather_display会处理嵌套结构
self.update_weather_display(weather_data)
+ # 同步更新天气悬浮窗口
+ if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible():
+ self.weather_floating_widget.update_weather(weather_data)
else:
print(f"无法获取城市 {city} 的天气数据")
+ # 显示错误信息到天气悬浮窗口
+ if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible():
+ self.weather_floating_widget.update_weather({'error': '无法获取天气数据'})
def refresh_weather(self):
"""刷新天气"""
@@ -504,6 +556,14 @@ class WordStyleMainWindow(QMainWindow):
background-color: #f3f2f1;
}
""")
+
+ # 创建日历组件并添加到窗口中(默认隐藏)
+ try:
+ from ui.calendar_widget import CalendarWidget
+ self.calendar_widget = CalendarWidget(self)
+ self.calendar_widget.hide() # 默认隐藏
+ except Exception as e:
+ print(f"创建日历组件失败: {e}")
def create_menu_bar(self):
"""创建菜单栏"""
@@ -633,6 +693,8 @@ class WordStyleMainWindow(QMainWindow):
self.learning_mode_action = QAction('学习模式', self)
self.learning_mode_action.setCheckable(True)
self.learning_mode_action.setChecked(False)
+ # 设置学习模式快捷键 (Qt会自动在macOS上映射Ctrl为Cmd)
+ self.learning_mode_action.setShortcut('Ctrl+L')
self.learning_mode_action.triggered.connect(lambda: self.set_view_mode("learning"))
view_mode_menu.addAction(self.learning_mode_action)
@@ -640,22 +702,7 @@ class WordStyleMainWindow(QMainWindow):
# 附加工具功能
weather_menu = view_menu.addMenu('附加工具')
-
- # 显示天气工具组
- self.show_weather_tools_action = QAction('显示天气工具', self)
- self.show_weather_tools_action.setCheckable(True)
- self.show_weather_tools_action.setChecked(False) # 默认不显示
- self.show_weather_tools_action.triggered.connect(self.toggle_weather_tools)
- weather_menu.addAction(self.show_weather_tools_action)
-
- # 显示每日一言工具组
- self.show_quote_tools_action = QAction('显示每日一言工具', self)
- self.show_quote_tools_action.setCheckable(True)
- self.show_quote_tools_action.setChecked(False) # 默认不显示
- self.show_quote_tools_action.triggered.connect(self.toggle_quote_tools)
- weather_menu.addAction(self.show_quote_tools_action)
-
- weather_menu.addSeparator()
+
# 刷新天气
refresh_weather_action = QAction('刷新天气', self)
@@ -667,21 +714,90 @@ class WordStyleMainWindow(QMainWindow):
show_weather_action = QAction('显示详细天气', self)
show_weather_action.triggered.connect(self.show_detailed_weather)
weather_menu.addAction(show_weather_action)
+
+ # 天气悬浮窗口
+ toggle_floating_weather_action = QAction('天气悬浮窗口', self)
+ toggle_floating_weather_action.triggered.connect(self.toggle_floating_weather)
+ weather_menu.addAction(toggle_floating_weather_action)
+
+ # 每日谏言悬浮窗口切换动作
+ toggle_floating_quote_action = QAction('每日谏言悬浮窗口', self)
+ toggle_floating_quote_action.triggered.connect(self.toggle_floating_quote)
+ weather_menu.addAction(toggle_floating_quote_action)
+
+ # 日历悬浮窗口切换动作
+ toggle_floating_calendar_action = QAction('日历悬浮窗口', self)
+ toggle_floating_calendar_action.triggered.connect(self.toggle_floating_calendar)
+ weather_menu.addAction(toggle_floating_calendar_action)
# 插入菜单
insert_menu = menubar.addMenu('插入(I)')
+ # 插入图片功能
+ insert_image_action = QAction('插入图片', self)
+ insert_image_action.triggered.connect(self.insert_image_in_typing_mode)
+ insert_menu.addAction(insert_image_action)
+
+ # 插入天气信息功能
+ insert_weather_action = QAction('插入天气信息', self)
+ insert_weather_action.triggered.connect(self.insert_weather_info)
+ insert_menu.addAction(insert_weather_action)
+
+ # 插入每日一句名言功能
+ insert_quote_action = QAction('插入每日一句名言', self)
+ insert_quote_action.triggered.connect(self.insert_daily_quote)
+ insert_menu.addAction(insert_quote_action)
+
+ # 插入古诗词功能
+ insert_poetry_action = QAction('插入古诗词', self)
+ insert_poetry_action.triggered.connect(self.insert_chinese_poetry)
+ insert_menu.addAction(insert_poetry_action)
+
# 绘图菜单
paint_menu = menubar.addMenu('绘图(D)')
+ # 添加日历按钮
+ calendar_action = QAction('日历', self)
+ calendar_action.triggered.connect(self.toggle_calendar)
+ paint_menu.addAction(calendar_action)
+
# 设计菜单
design_menu = menubar.addMenu('设计(G)')
+ # 导出子菜单
+ export_menu = design_menu.addMenu('导出')
+
+ # 导出为HTML
+ export_html_action = QAction('导出为HTML', self)
+ export_html_action.triggered.connect(self.export_as_html)
+ export_menu.addAction(export_html_action)
+
+ # 导出为PDF
+ export_pdf_action = QAction('导出为PDF', self)
+ export_pdf_action.triggered.connect(self.export_as_pdf)
+ export_menu.addAction(export_pdf_action)
+
+ # 导出为TXT
+ export_txt_action = QAction('导出为TXT', self)
+ export_txt_action.triggered.connect(self.export_as_txt)
+ export_menu.addAction(export_txt_action)
+
+ # 导出为DOCX
+ export_docx_action = QAction('导出为DOCX', self)
+ export_docx_action.triggered.connect(self.export_as_docx)
+ export_menu.addAction(export_docx_action)
+
# 布局菜单
layout_menu = menubar.addMenu('布局(L)')
# 引用菜单
reference_menu = menubar.addMenu('引用(R)')
+ # DeepSeek AI对话功能
+ self.deepseek_dialog_action = QAction('DeepSeek AI对话', self)
+ self.deepseek_dialog_action.setShortcut('Ctrl+D')
+ self.deepseek_dialog_action.triggered.connect(self.open_deepseek_dialog)
+ reference_menu.addAction(self.deepseek_dialog_action)
+
# 邮件菜单
mail_menu = menubar.addMenu('邮件(M)')
@@ -691,6 +807,17 @@ class WordStyleMainWindow(QMainWindow):
# 开发工具菜单
developer_menu = menubar.addMenu('开发工具(Q)')
+ # 应用选项菜单
+ app_menu = menubar.addMenu('应用选项(O)')
+
+ # 小游戏子菜单
+ games_menu = app_menu.addMenu('小游戏')
+
+ # 贪吃蛇游戏
+ snake_game_action = QAction('贪吃蛇', self)
+ snake_game_action.triggered.connect(self.launch_snake_game)
+ games_menu.addAction(snake_game_action)
+
# 帮助菜单
help_menu = menubar.addMenu('帮助(H)')
@@ -805,6 +932,9 @@ class WordStyleMainWindow(QMainWindow):
# 文本变化信号
self.text_edit.textChanged.connect(self.on_text_changed)
+ # 光标位置变化信号,用于更新按钮状态
+ self.text_edit.cursorPositionChanged.connect(self.update_format_buttons)
+
# Ribbon按钮信号
# 标签栏已删除,相关代码已移除
@@ -817,6 +947,18 @@ class WordStyleMainWindow(QMainWindow):
self.ribbon.underline_btn.clicked.connect(self.on_underline_clicked)
self.ribbon.color_btn.clicked.connect(self.on_color_clicked)
+ # 样式按钮信号
+ if hasattr(self.ribbon, 'heading1_btn'):
+ self.ribbon.heading1_btn.clicked.connect(self.on_heading1_clicked)
+ if hasattr(self.ribbon, 'heading2_btn'):
+ self.ribbon.heading2_btn.clicked.connect(self.on_heading2_clicked)
+ if hasattr(self.ribbon, 'heading3_btn'):
+ self.ribbon.heading3_btn.clicked.connect(self.on_heading3_clicked)
+ if hasattr(self.ribbon, 'heading4_btn'):
+ self.ribbon.heading4_btn.clicked.connect(self.on_heading4_clicked)
+ if hasattr(self.ribbon, 'body_text_btn'):
+ self.ribbon.body_text_btn.clicked.connect(self.on_body_text_clicked)
+
# 查找和替换按钮信号
if hasattr(self.ribbon, 'find_btn'):
self.ribbon.find_btn.clicked.connect(self.show_find_dialog)
@@ -825,17 +967,39 @@ class WordStyleMainWindow(QMainWindow):
# 页面布局信号已在菜单中直接连接,无需在此重复连接
+ # 段落对齐按钮信号
+ if hasattr(self.ribbon, 'align_left_btn'):
+ self.ribbon.align_left_btn.clicked.connect(self.on_align_left_clicked)
+ if hasattr(self.ribbon, 'align_center_btn'):
+ self.ribbon.align_center_btn.clicked.connect(self.on_align_center_clicked)
+ if hasattr(self.ribbon, 'align_right_btn'):
+ self.ribbon.align_right_btn.clicked.connect(self.on_align_right_clicked)
+ if hasattr(self.ribbon, 'align_justify_btn'):
+ self.ribbon.align_justify_btn.clicked.connect(self.on_align_justify_clicked)
+
# 天气功能信号
if hasattr(self.ribbon, 'city_combo'):
self.ribbon.city_combo.currentTextChanged.connect(self.on_city_changed)
if hasattr(self.ribbon, 'refresh_weather_btn'):
self.ribbon.refresh_weather_btn.clicked.connect(self.refresh_weather)
+ if hasattr(self.ribbon, 'floating_weather_btn'):
+ self.ribbon.floating_weather_btn.clicked.connect(self.toggle_floating_weather)
+ if hasattr(self.ribbon, 'floating_quote_btn'):
+ self.ribbon.floating_quote_btn.clicked.connect(self.toggle_floating_quote)
+
+ # 日历组件信号
+ if hasattr(self, 'calendar_widget'):
+ self.calendar_widget.date_selected.connect(self.insert_date_to_editor)
def on_text_changed(self):
"""文本变化处理 - 根据视图模式处理文本变化"""
# 如果正在加载文件,跳过处理
if self.is_loading_file:
return
+
+ # 检查是否是从学习模式同步内容,避免递归调用
+ if hasattr(self, 'sync_from_learning') and self.sync_from_learning:
+ return
# 根据当前视图模式处理
if self.view_mode == "learning":
@@ -852,8 +1016,9 @@ class WordStyleMainWindow(QMainWindow):
self.handle_learning_mode_typing()
elif self.view_mode == "typing":
- # 打字模式:可以自由打字
- self.handle_typing_mode_typing()
+ # 打字模式:可以自由打字,不自动处理内容
+ # 只在用户主动操作时处理,避免内容被覆盖
+ pass
# 标记文档为已修改
if not self.is_modified:
@@ -1282,10 +1447,162 @@ class WordStyleMainWindow(QMainWindow):
if cursor.hasSelection():
self.status_bar.showMessage("字体颜色已设置,新输入的文本将使用该颜色", 2000)
+ def on_heading1_clicked(self):
+ """一级标题按钮点击处理"""
+ self.apply_heading_style(1)
+
+ def on_heading2_clicked(self):
+ """二级标题按钮点击处理"""
+ self.apply_heading_style(2)
+
+ def on_heading3_clicked(self):
+ """三级标题按钮点击处理"""
+ self.apply_heading_style(3)
+
+ def on_heading4_clicked(self):
+ """四级标题按钮点击处理"""
+ self.apply_heading_style(4)
+
+ def on_body_text_clicked(self):
+ """正文按钮点击处理"""
+ self.apply_body_text_style()
+
+ def on_align_left_clicked(self):
+ """左对齐按钮点击处理"""
+ self.apply_alignment(Qt.AlignLeft)
+
+ def on_align_center_clicked(self):
+ """居中对齐按钮点击处理"""
+ self.apply_alignment(Qt.AlignCenter)
+
+ def on_align_right_clicked(self):
+ """右对齐按钮点击处理"""
+ self.apply_alignment(Qt.AlignRight)
+
+ def on_align_justify_clicked(self):
+ """两端对齐按钮点击处理"""
+ self.apply_alignment(Qt.AlignJustify)
+
+ def insert_date_to_editor(self, date_str):
+ """将选中的日期插入到编辑器中"""
+ # 获取当前光标位置
+ cursor = self.text_edit.textCursor()
+
+ # 在光标位置插入日期字符串
+ cursor.insertText(date_str)
+
+ # 更新文本编辑器的光标
+ self.text_edit.setTextCursor(cursor)
+
+ # 隐藏日历组件
+ if hasattr(self, 'calendar_widget'):
+ self.calendar_widget.hide()
+
+ # 更新状态栏提示
+ self.status_bar.showMessage(f"已插入日期: {date_str}", 2000)
+
+ def apply_heading_style(self, level):
+ """应用标题样式"""
+ cursor = self.text_edit.textCursor()
+
+ # 创建字符格式
+ char_format = QTextCharFormat()
+
+ # 创建块格式(段落格式)
+ block_format = QTextBlockFormat()
+ block_format.setTopMargin(12)
+ block_format.setBottomMargin(6)
+
+ # 根据标题级别设置样式
+ if level == 1:
+ # 一级标题:24pt, 加粗
+ char_format.setFontPointSize(24)
+ char_format.setFontWeight(QFont.Bold)
+ elif level == 2:
+ # 二级标题:18pt, 加粗
+ char_format.setFontPointSize(18)
+ char_format.setFontWeight(QFont.Bold)
+ elif level == 3:
+ # 三级标题:16pt, 加粗
+ char_format.setFontPointSize(16)
+ char_format.setFontWeight(QFont.Bold)
+ elif level == 4:
+ # 四级标题:14pt, 加粗
+ char_format.setFontPointSize(14)
+ char_format.setFontWeight(QFont.Bold)
+
+ # 应用格式
+ if cursor.hasSelection():
+ # 如果有选中文本,只更改选中文本的格式
+ cursor.mergeCharFormat(char_format)
+ else:
+ # 如果没有选中文本,更改当前段落的格式
+ cursor.setBlockFormat(block_format)
+ cursor.mergeCharFormat(char_format)
+ # 将光标移动到段落末尾并添加换行
+ cursor.movePosition(QTextCursor.EndOfBlock)
+ cursor.insertText("\n")
+
+ # 设置文本编辑器的默认格式
+ self.text_edit.setCurrentCharFormat(char_format)
+ self.text_edit.textCursor().setBlockFormat(block_format)
+
+ def apply_body_text_style(self):
+ """应用正文样式"""
+ cursor = self.text_edit.textCursor()
+
+ # 创建字符格式
+ char_format = QTextCharFormat()
+ char_format.setFontPointSize(12) # 正文字号
+ char_format.setFontWeight(QFont.Normal) # 正常粗细
+
+ # 创建块格式(段落格式)
+ block_format = QTextBlockFormat()
+ block_format.setTopMargin(0)
+ block_format.setBottomMargin(6)
+
+ # 应用格式
+ if cursor.hasSelection():
+ # 如果有选中文本,只更改选中文本的格式
+ cursor.mergeCharFormat(char_format)
+ else:
+ # 如果没有选中文本,更改当前段落的格式
+ cursor.setBlockFormat(block_format)
+ cursor.mergeCharFormat(char_format)
+
+ # 设置文本编辑器的默认格式
+ self.text_edit.setCurrentCharFormat(char_format)
+ self.text_edit.textCursor().setBlockFormat(block_format)
+
+ def apply_alignment(self, alignment):
+ """应用段落对齐方式"""
+ cursor = self.text_edit.textCursor()
+
+ # 创建块格式(段落格式)
+ block_format = QTextBlockFormat()
+ block_format.setAlignment(alignment)
+
+ # 应用格式
+ if cursor.hasSelection():
+ # 如果有选中文本,更改选中文本所在段落的对齐方式
+ cursor.mergeBlockFormat(block_format)
+ else:
+ # 如果没有选中文本,更改当前段落的对齐方式
+ cursor.setBlockFormat(block_format)
+
+ # 更新文本编辑器的默认段落格式
+ self.text_edit.textCursor().setBlockFormat(block_format)
+
def update_weather_display(self, weather_data):
"""更新天气显示"""
if 'error' in weather_data:
self.status_bar.showMessage(f"天气数据获取失败: {weather_data['error']}", 3000)
+ # 更新工具栏天气显示为错误状态
+ if hasattr(self, 'ribbon'):
+ if hasattr(self.ribbon, 'weather_icon_label'):
+ self.ribbon.weather_icon_label.setText("❓")
+ if hasattr(self.ribbon, 'weather_temp_label'):
+ self.ribbon.weather_temp_label.setText("--°C")
else:
# 处理嵌套的天气数据结构
city = weather_data.get('city', '未知城市')
@@ -1309,6 +1626,32 @@ class WordStyleMainWindow(QMainWindow):
weather_message = f"{city}: {desc}, {temp}°C{temp_range}"
self.status_bar.showMessage(weather_message, 5000)
+ # 更新工具栏天气图标和温度显示
+ if hasattr(self, 'ribbon'):
+ # 更新天气图标
+ if hasattr(self.ribbon, 'weather_icon_label') and desc != 'N/A':
+ emoji = self.ribbon.get_weather_emoji(desc)
+ self.ribbon.weather_icon_label.setText(emoji)
+
+ # 更新温度显示
+ if hasattr(self.ribbon, 'weather_temp_label') and temp != 'N/A':
+ # 计算平均温度(使用最高温和最低温的平均值)
+ avg_temp = temp
+ if 'forecast' in weather_data and weather_data['forecast']:
+ forecast_data = weather_data['forecast'][0]
+ if isinstance(forecast_data, dict):
+ temp_max = forecast_data.get('temp_max', 'N/A')
+ temp_min = forecast_data.get('temp_min', 'N/A')
+ if temp_max != 'N/A' and temp_min != 'N/A':
+ try:
+ avg_temp = (float(temp_max) + float(temp_min)) / 2
+ avg_temp = round(avg_temp, 1)
+ except (ValueError, TypeError):
+ avg_temp = temp
+
+ temp_str = f"{avg_temp}°C" if isinstance(avg_temp, (int, float)) else f"{temp}°C"
+ self.ribbon.weather_temp_label.setText(temp_str)
+
# 存储天气数据供其他功能使用(确保包含生活提示)
self.current_weather_data = weather_data
print(f"update_weather_display - 存储的current_weather_data包含life_tips: {self.current_weather_data.get('life_tips', [])}")
@@ -1317,7 +1660,18 @@ class WordStyleMainWindow(QMainWindow):
"""手动刷新天气信息"""
try:
# 获取当前选择的城市
- current_city = self.ribbon.city_combo.currentText()
+ current_city = "自动定位" # 默认值
+
+ # 安全地获取当前城市选择
+ if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'city_combo'):
+ current_city = self.ribbon.city_combo.currentText()
+ elif hasattr(self, 'weather_floating_widget') and hasattr(self.weather_floating_widget, 'city_combo'):
+ # 如果Ribbon中的city_combo不可用,尝试从天气悬浮窗口获取
+ current_city = self.weather_floating_widget.city_combo.currentText()
+ else:
+ # 如果都没有,使用默认值
+ current_city = "自动定位"
+
print(f"刷新天气 - 当前选择的城市: {current_city}")
if current_city == '自动定位':
@@ -1338,9 +1692,15 @@ class WordStyleMainWindow(QMainWindow):
print(f"refresh_weather - 原始数据包含life_tips: {weather_data.get('life_tips', [])}")
print(f"refresh_weather - formatted_data包含life_tips: {formatted_data.get('life_tips', [])}")
self.update_weather_display(formatted_data)
+ # 同步更新天气悬浮窗口
+ if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible():
+ self.weather_floating_widget.update_weather(formatted_data)
self.status_bar.showMessage("天气数据已刷新", 2000)
else:
self.status_bar.showMessage("天气数据刷新失败,请检查API密钥", 3000)
+ # 显示错误信息到天气悬浮窗口
+ if hasattr(self, 'weather_floating_widget') and self.weather_floating_widget.isVisible():
+ self.weather_floating_widget.update_weather({'error': '天气数据刷新失败'})
except Exception as e:
self.status_bar.showMessage(f"天气刷新失败: {str(e)}", 3000)
@@ -1436,6 +1796,87 @@ class WordStyleMainWindow(QMainWindow):
self.refresh_weather()
dialog.close()
+ def toggle_floating_weather(self):
+ """切换天气悬浮窗口显示/隐藏"""
+ if hasattr(self, 'weather_floating_widget'):
+ if self.weather_floating_widget.isVisible():
+ self.weather_floating_widget.hide()
+ self.status_bar.showMessage("天气悬浮窗口已隐藏", 2000)
+ else:
+ self.weather_floating_widget.show()
+ # 确保窗口在屏幕内
+ self.weather_floating_widget.move(100, 100)
+ self.status_bar.showMessage("天气悬浮窗口已显示", 2000)
+ # 同步当前城市选择
+ if hasattr(self, 'ribbon') and hasattr(self.ribbon, 'city_combo'):
+ current_city = self.ribbon.city_combo.currentText()
+ self.weather_floating_widget.set_current_city(current_city)
+ # 如果有天气数据,更新显示
+ if hasattr(self, 'current_weather_data') and self.current_weather_data:
+ self.weather_floating_widget.update_weather(self.current_weather_data)
+
+ def on_weather_floating_closed(self):
+ """天气悬浮窗口关闭时的处理"""
+ self.status_bar.showMessage("天气悬浮窗口已关闭", 2000)
+
+ def toggle_floating_quote(self):
+ """切换每日谏言悬浮窗口显示/隐藏"""
+ if hasattr(self, 'quote_floating_widget'):
+ if self.quote_floating_widget.isVisible():
+ self.quote_floating_widget.hide()
+ self.status_bar.showMessage("每日谏言悬浮窗口已隐藏", 2000)
+ else:
+ self.quote_floating_widget.show()
+ # 确保窗口在屏幕内
+ self.quote_floating_widget.move(100, 100)
+ self.status_bar.showMessage("每日谏言悬浮窗口已显示", 2000)
+
+ def on_quote_floating_closed(self):
+ """每日谏言悬浮窗口关闭时的处理"""
+ self.status_bar.showMessage("每日谏言悬浮窗口已关闭", 2000)
+
+ def toggle_floating_calendar(self):
+ """切换日历悬浮窗口的显示/隐藏状态"""
+ if hasattr(self, 'calendar_floating_widget'):
+ if self.calendar_floating_widget.isVisible():
+ self.calendar_floating_widget.hide()
+ self.status_bar.showMessage("日历悬浮窗口已隐藏", 2000)
+ else:
+ self.calendar_floating_widget.show()
+ # 确保窗口在屏幕内
+ self.calendar_floating_widget.move(100, 100)
+ self.status_bar.showMessage("日历悬浮窗口已显示", 2000)
+
+ def on_calendar_floating_closed(self):
+ """日历悬浮窗口关闭事件"""
+ self.status_bar.showMessage("日历悬浮窗口已关闭", 2000)
+
+ def insert_quote_to_cursor(self, quote_text):
+ """将古诗句插入到光标位置"""
+ if hasattr(self, 'text_edit'):
+ # 获取当前光标位置
+ cursor = self.text_edit.textCursor()
+
+ # 在光标位置插入文本
+ cursor.insertText(quote_text + "\n")
+
+ # 更新状态栏提示
+ # 从文本中提取诗句部分用于显示
+ quote_only = quote_text.split(" —— ")[0] if " —— " in quote_text else quote_text
+ self.status_bar.showMessage(f"已插入古诗句: {quote_only}", 3000)
+
+ def insert_date_to_cursor(self, date_str):
+ """在光标位置插入日期"""
+ try:
+ # 在光标位置插入日期
+ cursor = self.text_edit.textCursor()
+ cursor.insertText(date_str)
+
+ # 更新状态栏
+ self.status_bar.showMessage(f"已插入日期: {date_str}", 2000)
+ except Exception as e:
+ print(f"插入日期时出错: {e}")
+
def toggle_weather_tools(self, checked):
"""切换天气工具组显示"""
if checked:
@@ -1470,6 +1911,12 @@ class WordStyleMainWindow(QMainWindow):
if hasattr(self, 'ribbon'):
# 直接调用WordRibbon中的刷新方法
self.ribbon.on_refresh_quote()
+
+ # 同时更新浮动窗口中的内容(如果浮动窗口存在且可见)
+ if hasattr(self, 'quote_floating_widget') and self.quote_floating_widget.isVisible():
+ # 调用浮动窗口的获取新内容方法
+ if hasattr(self.quote_floating_widget, 'fetch_and_update_quote'):
+ self.quote_floating_widget.fetch_and_update_quote()
def on_quote_fetched(self, quote_data):
"""处理名言获取成功"""
@@ -1569,10 +2016,10 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage("新建文档 - 打字模式,可以自由开始打字", 3000)
def import_file(self):
- """导入文件 - 仅在导入时存储内容,不立即显示"""
+ """导入文件 - 根据模式决定是否立即显示"""
file_path, _ = QFileDialog.getOpenFileName(
self, "导入文件", "",
- "文档文件 (*.docx *.txt *.pdf);;所有文件 (*.*)"
+ "文档文件 (*.docx *.txt *.pdf *.html);;所有文件 (*.*)"
)
if file_path:
@@ -1590,7 +2037,7 @@ class WordStyleMainWindow(QMainWindow):
if result.get('is_temp_file', False):
self.temp_files.append(txt_path)
- # 存储完整内容但不立即显示
+ # 存储完整内容
self.imported_content = content
self.displayed_chars = 0
@@ -1632,12 +2079,13 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},开始打字逐步显示学习内容!", 5000)
else:
- # 打字模式:不显示导入内容,保持当前内容
- self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)},切换到学习模式查看内容", 5000)
+ # 打字模式:直接显示完整内容
+ self.text_edit.setPlainText(content)
+ self.status_bar.showMessage(f"已导入: {os.path.basename(txt_path)}", 5000)
# 提取并显示图片(如果有)
if images:
- self.extract_and_display_images(content, images)
+ self.extract_and_display_images(file_path=None, images=images)
else:
# 转换失败,显示错误信息
@@ -1650,7 +2098,7 @@ class WordStyleMainWindow(QMainWindow):
content = parser.parse_file(file_path)
if content:
- # 存储完整内容但不立即显示
+ # 存储完整内容
self.imported_content = content
self.displayed_chars = 0
@@ -1665,8 +2113,9 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)},开始打字逐步显示学习内容!", 5000)
else:
- # 打字模式:不显示导入内容,保持当前内容
- self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)},切换到学习模式查看内容", 5000)
+ # 打字模式:直接显示完整内容
+ self.text_edit.setPlainText(content)
+ self.status_bar.showMessage(f"已导入: {os.path.basename(file_path)}", 5000)
except Exception as fallback_e:
QMessageBox.critical(self, "错误", f"无法导入文件:\n{str(e)}\n\n回退方法也失败:\n{str(fallback_e)}")
@@ -1691,7 +2140,7 @@ class WordStyleMainWindow(QMainWindow):
# 设置文件加载标志
self.is_loading_file = True
- # 存储完整内容但不立即显示
+ # 存储完整内容
self.imported_content = content
self.displayed_chars = 0
@@ -1856,9 +2305,32 @@ class WordStyleMainWindow(QMainWindow):
imported_content = self.imported_content
if hasattr(self, 'learning_progress') and self.learning_progress > 0:
current_position = self.learning_progress
+ else:
+ # 如果没有导入内容,检查当前打字模式的内容
+ current_text = self.text_edit.toPlainText()
+ if current_text and current_text != "在此输入您的内容...":
+ # 将打字模式的内容作为学习模式的导入内容
+ imported_content = current_text
+ current_position = 0
+ self.imported_content = current_text
+
+ # 准备图片数据
+ image_data = None
+ image_positions = None
+ if hasattr(self, 'typing_logic') and self.typing_logic:
+ if hasattr(self.typing_logic, 'image_data'):
+ image_data = self.typing_logic.image_data
+ if hasattr(self.typing_logic, 'image_positions'):
+ image_positions = self.typing_logic.image_positions
- # 创建学习模式窗口,直接传递导入内容
- self.learning_window = LearningModeWindow(self, imported_content, current_position)
+ # 创建学习模式窗口,传递导入内容和图片数据
+ self.learning_window = LearningModeWindow(self, imported_content, current_position, image_data, image_positions)
+
+ # 连接学习模式窗口的内容变化信号
+ self.learning_window.content_changed.connect(self.on_learning_content_changed)
+
+ # 连接学习模式窗口的关闭信号
+ self.learning_window.closed.connect(self.on_learning_mode_closed)
# 显示学习模式窗口
self.learning_window.show()
@@ -1890,8 +2362,86 @@ class WordStyleMainWindow(QMainWindow):
self.learning_mode_action.setChecked(False)
self.typing_mode_action.setChecked(True)
self.view_mode = "typing"
+
+ # 清除学习窗口引用
+ self.learning_window = None
+
self.status_bar.showMessage("学习模式窗口已关闭", 2000)
+ def open_deepseek_dialog(self):
+ """打开DeepSeek AI对话窗口"""
+ try:
+ from deepseek_dialog_window import DeepSeekDialogWindow
+
+ # 检查是否已存在对话窗口,如果存在则激活
+ if self.deepseek_dialog and self.deepseek_dialog.isVisible():
+ self.deepseek_dialog.activateWindow()
+ self.deepseek_dialog.raise_()
+ return
+
+ # 创建DeepSeek对话窗口
+ self.deepseek_dialog = DeepSeekDialogWindow(self)
+
+ # 连接对话窗口的关闭信号
+ self.deepseek_dialog.closed.connect(self.on_deepseek_dialog_closed)
+
+ # 显示对话窗口
+ self.deepseek_dialog.show()
+
+ # 更新菜单状态
+ self.deepseek_dialog_action.setChecked(True)
+
+ self.status_bar.showMessage("DeepSeek AI对话窗口已打开", 3000)
+
+ except ImportError as e:
+ QMessageBox.critical(self, "错误", f"无法加载DeepSeek对话窗口:\n{str(e)}")
+ except Exception as e:
+ QMessageBox.critical(self, "错误", f"打开DeepSeek对话窗口时出错:\n{str(e)}")
+
+ def on_deepseek_dialog_closed(self):
+ """DeepSeek对话窗口关闭时的回调"""
+ # 重置菜单状态
+ self.deepseek_dialog_action.setChecked(False)
+
+ # 清除对话窗口引用
+ self.deepseek_dialog = None
+
+ self.status_bar.showMessage("DeepSeek AI对话窗口已关闭", 2000)
+
+ def on_learning_content_changed(self, new_content, position):
+ """学习模式内容变化时的回调 - 只在末尾追加新内容"""
+ # 设置同步标记,防止递归调用
+ self.sync_from_learning = True
+
+ try:
+ # 只在末尾追加新输入的内容,不修改已有内容
+ if new_content:
+ # 直接在末尾追加新内容
+ cursor = self.text_edit.textCursor()
+ cursor.movePosition(QTextCursor.End)
+ cursor.insertText(new_content)
+
+ # 更新导入内容(但不覆盖用户额外输入的内容)
+ if self.imported_content:
+ self.imported_content += new_content
+ else:
+ self.imported_content = new_content
+
+ self.learning_progress = len(self.imported_content) if self.imported_content else 0
+
+ # 重置打字逻辑(不追踪进度)
+ if self.typing_logic:
+ self.typing_logic.imported_content = self.imported_content
+ self.typing_logic.current_index = self.learning_progress
+ self.typing_logic.typed_chars = self.learning_progress
+
+ # 更新状态栏
+ self.status_bar.showMessage(f"从学习模式同步新内容: {new_content}", 3000)
+
+ finally:
+ # 重置同步标记
+ self.sync_from_learning = False
+
def set_page_color(self, color):
"""设置页面颜色"""
color_map = {
@@ -2205,6 +2755,21 @@ class WordStyleMainWindow(QMainWindow):
# 显示替换结果
QMessageBox.information(self, "替换", f"已完成 {count} 处替换。")
+ def launch_snake_game(self):
+ """启动贪吃蛇游戏"""
+ try:
+ from ui.snake_game import SnakeGameWindow
+
+ # 创建游戏窗口
+ self.snake_game_window = SnakeGameWindow(self)
+ self.snake_game_window.show()
+ except Exception as e:
+ QMessageBox.critical(
+ self,
+ "错误",
+ f"无法启动贪吃蛇游戏:{str(e)}"
+ )
+
def show_about(self):
"""显示关于对话框"""
# 创建自定义对话框
@@ -2402,46 +2967,37 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage("已切换到黑色模式", 2000)
def show_image_viewer(self, filename, image_data):
- """显示图片查看器 - 图片真正铺满整个窗口上方"""
+ """显示图片查看器 - 支持缩放功能"""
try:
# 创建自定义图片查看窗口
viewer = QDialog(self)
viewer.setWindowTitle(f"图片查看 - {filename}")
viewer.setModal(False)
- # 移除窗口边框和标题栏装饰,设置为工具窗口样式
- viewer.setWindowFlags(Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
+ # 设置窗口标志,保留标题栏以便用户可以移动和调整大小
+ viewer.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint)
- # 设置窗口背景为黑色,完全无边距
+ # 设置窗口背景为黑色
viewer.setStyleSheet("""
QDialog {
background-color: #000000;
- border: none;
- margin: 0px;
- padding: 0px;
}
""")
- # 创建布局,完全移除边距
- layout = QVBoxLayout()
- layout.setContentsMargins(0, 0, 0, 0) # 移除布局边距
- layout.setSpacing(0) # 移除组件间距
- layout.setAlignment(Qt.AlignCenter) # 布局居中对齐
+ # 创建场景和视图
+ scene = QGraphicsScene(viewer)
+ view = QGraphicsView(scene)
+ view.setStyleSheet("border: none;") # 移除视图边框
- # 创建图片标签,设置为完全填充模式
- image_label = QLabel()
- image_label.setAlignment(Qt.AlignCenter)
- image_label.setScaledContents(True) # 关键:允许图片缩放以填充标签
- image_label.setMinimumSize(1, 1) # 设置最小尺寸
- image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # 设置大小策略为扩展
- image_label.setStyleSheet("""
- QLabel {
- border: none;
- margin: 0px;
- padding: 0px;
- background-color: #000000;
- }
- """)
+ # 设置视图为可交互的,并启用滚动条
+ view.setDragMode(QGraphicsView.ScrollHandDrag)
+ view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
+
+ # 创建布局
+ layout = QVBoxLayout()
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(view)
+ viewer.setLayout(layout)
# 加载图片
pixmap = QPixmap()
@@ -2449,10 +3005,10 @@ class WordStyleMainWindow(QMainWindow):
self.status_bar.showMessage(f"加载图片失败: {filename}", 3000)
return
- layout.addWidget(image_label)
- viewer.setLayout(layout)
+ # 将图片添加到场景
+ scene.addPixmap(pixmap)
- # 计算位置和大小
+ # 设置视图大小和位置
if self:
parent_geometry = self.geometry()
screen_geometry = QApplication.primaryScreen().geometry()
@@ -2477,42 +3033,24 @@ class WordStyleMainWindow(QMainWindow):
viewer.show()
- # 关键:强制图片立即填充整个标签区域
- def force_image_fill():
- try:
- if pixmap and not pixmap.isNull():
- # 获取标签的实际大小
- label_size = image_label.size()
- if label_size.width() > 10 and label_size.height() > 10: # 确保尺寸有效
- # 完全填充,忽略宽高比,真正铺满
- scaled_pixmap = pixmap.scaled(
- label_size,
- Qt.IgnoreAspectRatio, # 关键:忽略宽高比,强制填充
- Qt.SmoothTransformation
- )
- image_label.setPixmap(scaled_pixmap)
- print(f"图片已强制缩放至 {label_size.width()}x{label_size.height()}")
-
- # 确保标签完全填充布局
- image_label.setMinimumSize(label_size)
- except Exception as e:
- print(f"图片缩放失败: {e}")
+ # 设置视图适应图片大小
+ view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
- # 使用多个定时器确保图片正确填充
- from PyQt5.QtCore import QTimer
- QTimer.singleShot(50, force_image_fill) # 50毫秒后执行
- QTimer.singleShot(200, force_image_fill) # 200毫秒后执行
- QTimer.singleShot(1000, force_image_fill) # 1000毫秒后再执行一次
+ # 重写视图的滚轮事件以支持缩放
+ def wheelEvent(event):
+ factor = 1.2
+ if event.angleDelta().y() > 0:
+ view.scale(factor, factor)
+ else:
+ view.scale(1.0/factor, 1.0/factor)
- # 连接窗口大小变化事件
- viewer.resizeEvent = lambda event: force_image_fill()
+ view.wheelEvent = wheelEvent
- # 添加点击关闭功能
- def close_viewer():
- viewer.close()
+ # 添加双击重置视图功能
+ def mouseDoubleClickEvent(event):
+ view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
- image_label.mousePressEvent = lambda event: close_viewer()
- viewer.mousePressEvent = lambda event: close_viewer()
+ view.mouseDoubleClickEvent = mouseDoubleClickEvent
except Exception as e:
self.status_bar.showMessage(f"创建图片查看器失败: {str(e)}", 3000)
@@ -2636,6 +3174,79 @@ class WordStyleMainWindow(QMainWindow):
# 这个方法现在不需要了,因为图片会直接插入到文本中
pass
+ def insert_image_in_typing_mode(self):
+ """在打字模式下插入图片"""
+ try:
+ # 检查当前是否在打字模式下
+ if self.view_mode != "typing":
+ self.status_bar.showMessage("请在打字模式下使用插入图片功能", 3000)
+ return
+
+ # 打开文件对话框选择图片
+ from PyQt5.QtWidgets import QFileDialog
+ file_path, _ = QFileDialog.getOpenFileName(
+ self,
+ "选择图片文件",
+ "",
+ "图片文件 (*.png *.jpg *.jpeg *.bmp *.gif *.ico)"
+ )
+
+ if not file_path:
+ return
+
+ # 加载图片文件
+ pixmap = QPixmap(file_path)
+ if pixmap.isNull():
+ self.status_bar.showMessage("无法加载图片文件", 3000)
+ return
+
+ # 获取当前光标位置
+ cursor = self.text_edit.textCursor()
+
+ # 创建图片格式
+ image_format = QTextImageFormat()
+
+ # 调整图片大小
+ scaled_pixmap = pixmap.scaled(200, 150, Qt.KeepAspectRatio, Qt.SmoothTransformation)
+
+ # 将图片保存到临时文件
+ import tempfile
+ import os
+ temp_dir = tempfile.gettempdir()
+ filename = os.path.basename(file_path)
+ safe_filename = "".join(c for c in filename if c.isalnum() or c in ('.', '_', '-'))
+ temp_file = os.path.join(temp_dir, safe_filename)
+
+ if scaled_pixmap.save(temp_file):
+ # 设置图片格式
+ image_format.setName(temp_file)
+ image_format.setWidth(200)
+ image_format.setHeight(150)
+
+ # 在光标位置插入图片
+ cursor.insertImage(image_format)
+
+ # 在图片后插入一个空格,让文字继续
+ cursor.insertText(" ")
+
+ # 标记文档为已修改
+ if not self.is_modified:
+ self.is_modified = True
+ self.update_window_title()
+
+ # 显示成功消息
+ self.status_bar.showMessage(f"图片已插入: {filename}", 3000)
+
+ # 添加到临时文件列表以便清理
+ self.temp_files.append(temp_file)
+ else:
+ self.status_bar.showMessage("保存临时图片文件失败", 3000)
+
+ except Exception as e:
+ self.status_bar.showMessage(f"插入图片失败: {str(e)}", 3000)
+ import traceback
+ traceback.print_exc()
+
def closeEvent(self, event):
"""关闭事件处理"""
# 清理临时文件
@@ -2670,11 +3281,307 @@ class WordStyleMainWindow(QMainWindow):
print(f"删除临时文件失败 {temp_file}: {str(e)}")
self.temp_files.clear()
- def extract_and_display_images(self, file_path):
+ def export_as_html(self):
+ """导出为HTML"""
+ file_path, _ = QFileDialog.getSaveFileName(
+ self, "导出为HTML", "", "HTML文件 (*.html);;所有文件 (*.*)"
+ )
+
+ if file_path:
+ try:
+ # 获取当前文本内容
+ content = self.text_edit.toPlainText()
+
+ # 处理图片标签
+ html_body = ""
+ lines = content.split('\n')
+
+ for line in lines:
+ if line.strip().startswith('[图片:') and line.strip().endswith(']'):
+ # 提取图片文件名
+ img_name = line.strip()[4:-1].strip()
+
+ # 查找对应的图片数据
+ img_data = None
+ for filename, image_data in self.extracted_images:
+ if filename == img_name:
+ img_data = image_data
+ break
+
+ if img_data:
+ # 创建图片的base64编码
+ import base64
+ img_base64 = base64.b64encode(img_data).decode('utf-8')
+
+ # 检测图片类型
+ if img_name.lower().endswith('.png'):
+ img_type = 'png'
+ elif img_name.lower().endswith(('.jpg', '.jpeg')):
+ img_type = 'jpeg'
+ elif img_name.lower().endswith('.gif'):
+ img_type = 'gif'
+ else:
+ img_type = 'png' # 默认
+
+ html_body += f'
\n'
+ html_body += f'

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