final #103

Closed
pim4r9h6o wants to merge 44 commits from main into shixinglin

4
.gitignore vendored

@ -198,6 +198,9 @@ temp/
*.orig
# Project specific
# Documentation folder
doc/
dist_package/
dist_package_v0.3/
*.zip
@ -231,6 +234,7 @@ venv/
env/
.venv/
.env/
new_venv/
# IDE
.idea/

@ -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平台插件问题

@ -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)

@ -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"

@ -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)

@ -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)

@ -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())

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

@ -0,0 +1,7 @@
#!/bin/bash
echo "设置Qt调试环境变量..."
export QT_DEBUG_PLUGINS=1
echo "Qt调试模式已启用"
echo ""
echo "运行MagicWord应用程序..."
python src/main.py

@ -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

@ -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

@ -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"

@ -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",
],
)

@ -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()

@ -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:
"""验证文件路径是否有效"""

@ -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)
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()

@ -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")
# 设置窗口图标(如果存在)

@ -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)
self.learning_window = None
self.learning_mode_action.setChecked(False)
self.typing_mode_action.setChecked(True)

@ -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,

@ -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_())

@ -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):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -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

Binary file not shown.

@ -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()

@ -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("所有测试完成!")

@ -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. **兼容性好**:与所有现有功能完美配合
这个新增功能与现有的字体样式工具完美集成,提供了完整且安全的文本格式化能力!
Loading…
Cancel
Save