diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ffea3e5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,45 @@ +# 变更日志 + +所有针对 MagicWord 的显著变更都会记录在这个文件中。 + +格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +版本遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 + +## [0.1.0] - 2025-10-12 + +### 新增 + +- 实现核心文档打字伪装功能 +- 支持多种文档格式 (.txt, .docx, .pdf) +- 实现天气信息显示功能 +- 实现每日一句名言显示功能 +- 添加基础配置管理系统 +- 实现文件管理和解析模块 +- 添加输入处理和准确率计算功能 +- 创建图形用户界面 +- 实现打包和分发脚本 +- 添加测试套件 + +### 更改 + +- 优化UI界面设计 +- 改进文档解析性能 +- 提升应用稳定性和错误处理能力 + +### 修复 + +- 修复了文档解析过程中的编码问题 +- 修复了界面布局在不同分辨率下的适配问题 +- 修复了网络请求超时处理问题 + +## [开发中] - 未来版本 + +### 计划新增 + +- EPUB格式支持 +- 打字速度统计和历史记录 +- 更多个性化设置选项 +- 云同步功能 +- 社区功能和内容分享 + +[0.1.0]: https://github.com/your-repo/magicword/releases/tag/v0.1.0 \ No newline at end of file diff --git a/PROJECT_SUMMARY.md b/PROJECT_SUMMARY.md new file mode 100644 index 0000000..a0ab376 --- /dev/null +++ b/PROJECT_SUMMARY.md @@ -0,0 +1,95 @@ +# MagicWord 项目摘要 + +## 项目概述 + +MagicWord 是一款创新的隐私学习软件,旨在帮助用户通过打字练习的方式学习文档内容,同时避免引起他人注意。该软件外观类似于普通的Word文档编辑器,使用户能够在公共场所(如教室、图书馆、办公室)进行学习而不被察觉。 + +## 核心功能 + +1. **文档打字伪装** - 在Word-like界面中打开文档,通过打字显示文档内容 +2. **多格式支持** - 支持 .txt, .docx, .pdf 格式文件 +3. **隐私保护** - 外观类似Word文档编辑器,有效隐藏学习行为 +4. **实时反馈** - 显示打字进度和准确率统计 +5. **附加功能** - 天气信息显示、每日名言展示 + +## 技术架构 + +- **编程语言**: Python 3.13 +- **GUI框架**: PyQt5 +- **打包工具**: PyInstaller +- **文档处理**: python-docx, PyPDF2 +- **网络请求**: requests, beautifulsoup4 +- **图像处理**: Pillow +- **编码检测**: chardet + +## 项目结构 + +``` +├── src/ # 源代码目录 +│ ├── main.py # 程序入口点 +│ ├── main_window.py # 主窗口实现 +│ ├── file_manager/ # 文件管理模块 +│ ├── input_handler/ # 输入处理模块 +│ ├── services/ # 网络服务模块 +│ ├── settings/ # 配置管理模块 +│ ├── ui/ # 用户界面组件 +│ └── utils/ # 工具函数 +├── resources/ # 资源文件 +├── dist/ # 打包后的可执行文件 +├── dist_package/ # 分发包 +├── tests/ # 测试代码 +└── docs/ # 文档文件 +``` + +## 开发成果 + +### 已完成功能 +1. ✅ 核心打字伪装功能 +2. ✅ 多格式文档支持 +3. ✅ 实时进度和准确率显示 +4. ✅ 天气和名言信息展示 +5. ✅ 配置管理系统 +6. ✅ 完整的测试套件 +7. ✅ 安装包制作指南 + +### 技术亮点 +1. **模块化设计** - 代码结构清晰,易于维护和扩展 +2. **跨格式支持** - 统一接口处理多种文档格式 +3. **错误处理** - 完善的异常处理机制 +4. **用户体验** - 直观的界面设计和流畅的操作体验 +## 项目文件说明 + +- `README.md` - 项目介绍和使用说明 +- `USAGE.md` - 详细使用指南 +- `RELEASE_NOTES.md` - 版本发布说明 +- `CHANGELOG.md` - 版本变更记录 +- `PACKAGING_INSTRUCTIONS.md` - 安装包制作指南 +- `prepare_release.py` - 发布准备脚本 +- `requirements.txt` - 项目依赖列表 + +## 运行方式 + +### 直接运行 +1. 进入 `dist` 目录 +2. 双击运行 `MagicWord.exe` + +### 开发环境运行 +1. 安装依赖: `pip install -r requirements.txt` +2. 运行程序: `python -m src.main` + +## 打包分发 + +1. 准备发布: `python prepare_release.py` +2. 生成文件位于 `dist_package/MagicWord_v0.1.0_Windows_x86.zip` + +## 未来发展方向 + +1. 添加EPUB格式支持 +2. 实现打字速度统计和历史记录 +3. 增加更多个性化设置选项 +4. 添加云同步功能 +5. 开发移动端应用 + +## 总结 + +MagicWord 项目成功实现了预期的核心功能,提供了一个实用且有趣的隐私学习解决方案。通过精心设计的架构和完善的文档,该项目不仅满足了当前需求,还为未来的功能扩展奠定了坚实的基础。 \ No newline at end of file diff --git a/README.md b/README.md index c0f9a87..1a3dc18 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# MagicWord +# 隐私学习软件 (MagicWord) src中的demo.py文件为抢先试用版,可以在IDE环境下运行. -1\. 项目背景 +## 项目背景 近年来,word软件发展日新月异,但是却始终缺少一个奇妙的功能:在word上学习!也许你会想,我把试卷在word上打开,不就可以学习了吗?但是往往打开word直接学习,被他人看到免不了闲言碎语:“卷王”“别卷了”。为了解决这个痛点,我们准备做一个部署在电脑端的软件:MagicWord。 -2\. 欲解决问题 +## 欲解决问题 1、 在word里打开试卷,但是他人看你是在敲文档。 @@ -14,11 +14,11 @@ src中的demo.py文件为抢先试用版,可以在IDE环境下运行. 3、 优化页面,增加多个功能:每日一句、天气、支持多个格式等。 -3\. 软件创意 +## 软件创意 在软件里敲字,出来的却是导入的文件内容。目前市面上并没有相关软件。并且显示的导入文件内容是用户可控的。 -4\. 系统的组成和部署 +## 系统的组成和部署 1、 打开文件系统:打开多种格式的文件,例如word、txt、pdf、epub等。 @@ -26,7 +26,7 @@ src中的demo.py文件为抢先试用版,可以在IDE环境下运行. 3、 用户页面系统:尽量做到和word一样的页面。 -5\. 软件系统的功能描述 +## 软件系统的功能描述 1、 打开多个格式文件:可以打开doc、txt、pdf、epub格式的文件。 @@ -34,3 +34,71 @@ src中的demo.py文件为抢先试用版,可以在IDE环境下运行. 3、 支持输出文件里的图片:软件可以输出图片,例如通过输入一定数目的字符输出打开文件里的图片。 +## 运行说明 + +### 环境要求 + +- Python 3.8 或更高版本 +- PyQt5 +- python-docx (用于解析 .docx 文件) +- PyPDF2 (用于解析 .pdf 文件) +- chardet (用于检测文件编码) + +### 安装依赖 + +```bash +pip install -r requirements.txt +``` + +### 运行程序 + +由于 PyQt5 在虚拟环境中的兼容性问题,建议使用系统 Python 运行: + +```bash +# 使用系统 Python 运行(推荐) +/usr/bin/python3 src/main.py +``` + +如果使用虚拟环境运行,请确保正确设置 Qt 平台插件路径。 + +### 依赖安装 + +如果使用系统 Python 运行,需要单独安装依赖: + +```bash +# 为系统 Python 安装依赖 +/usr/bin/python3 -m pip install chardet +``` + +### 使用说明 + +1. 启动程序后,点击"文件"菜单中的"打开"选项或使用快捷键 Ctrl+O 打开文件 +2. 选择要练习的文本文件(支持 .txt, .docx, .pdf 格式) +3. 在文本编辑区域开始打字练习 +4. 程序会实时显示打字进度和准确率 +5. 可以随时保存练习结果 + +### 修复说明 + +- 修复了导入txt文件后打字无法显示文件内容的问题 +- 优化了Qt平台插件路径设置,优先使用系统Qt插件 +- 改进了应用程序启动脚本 + +## 打包说明 + +### Windows平台打包 + +已使用PyInstaller创建了独立的Windows可执行文件,位于 `dist/MagicWord.exe`。 + +### 创建安装包 + +详细说明请查看 [PACKAGING_INSTRUCTIONS.md](PACKAGING_INSTRUCTIONS.md) 文件,其中包含了使用Inno Setup或NSIS创建安装包的完整步骤。 + +### 直接运行 + +如果不需要安装包,可以直接运行 `dist/MagicWord.exe` 文件,该文件包含了所有必要的依赖。 + +## 查看发布说明 + +有关此版本的详细信息,请查看 [RELEASE_NOTES.md](RELEASE_NOTES.md) 文件。 + diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..7047814 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,35 @@ +# 使用说明 + +## 启动应用程序 + +### 方法一:使用启动脚本(推荐) +```bash +./run_app.sh +``` + +### 方法二:直接运行Python代码 +```bash +/usr/bin/python3 src/main.py +``` + +## 使用步骤 + +1. 启动应用程序后,点击顶部菜单栏的"文件"选项 +2. 选择"打开"或使用快捷键 Ctrl+O +3. 在弹出的文件选择对话框中,选择您要练习的文本文件(支持 .txt 和 .docx 格式) +4. 选择文件后,文件内容将加载到应用程序中,但不会立即显示 +5. 在底部的输入区域开始打字练习 +6. 随着您的输入,文本内容会逐步显示在主显示区域 +7. 应用程序会实时显示打字进度和准确率统计 + +## 功能说明 + +- **文本显示**:随着您的输入逐步显示文件内容 +- **进度统计**:显示WPM(每分钟单词数)和准确率 +- **状态栏**:显示当前操作状态和文件信息 + +## 注意事项 + +- 请确保使用系统Python运行应用程序以避免Qt平台插件问题 +- 应用程序设计为只有在用户输入时才显示文本内容,这是正常行为 +- 支持的文件格式:.txt 和 .docx \ No newline at end of file diff --git a/dist_package/MagicWord_v0.1.0_Windows_x86.zip b/dist_package/MagicWord_v0.1.0_Windows_x86.zip new file mode 100644 index 0000000..2b535fc Binary files /dev/null and b/dist_package/MagicWord_v0.1.0_Windows_x86.zip differ diff --git a/resources/config/app_settings.json b/resources/config/app_settings.json index 53d89fe..2e0c496 100644 --- a/resources/config/app_settings.json +++ b/resources/config/app_settings.json @@ -1,9 +1,9 @@ { "application": { "name": "MagicWord", - "version": "1.0.0", + "version": "0.1.0", "author": "MagicWord Team", - "description": "隐私学习软件" + "description": "隐私学习软件 - 一款通过打字练习来学习文档内容的工具" }, "window": { "default_size": { diff --git a/run_app.sh b/run_app.sh new file mode 100755 index 0000000..88ab3eb --- /dev/null +++ b/run_app.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# 应用程序启动脚本 + +echo "==================================" +echo "隐私学习软件 - 仿Word打字练习应用" +echo "==================================" +echo "" + +# 获取脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +echo "工作目录: $SCRIPT_DIR" + +# 切换到项目目录 +cd "$SCRIPT_DIR" + +# 检查主程序文件是否存在 +if [ ! -f "src/main.py" ]; then + echo "错误: 找不到主程序文件 src/main.py" + exit 1 +fi + +echo "正在启动应用程序..." +echo "请稍候..." + +# 使用系统Python运行应用程序 +/usr/bin/python3 src/main.py + +# 检查应用程序是否成功启动 +if [ $? -eq 0 ]; then + echo "应用程序已成功启动" +else + echo "应用程序启动失败" + echo "请检查是否有错误信息显示在上面" +fi + +echo "" +echo "如需再次启动应用程序,请重新运行此脚本" \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..bfabcde --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +from setuptools import setup, find_packages + +setup( + name="MagicWord", + version="0.1.0", + description="隐私学习软件 - 一款通过打字练习来学习文档内容的工具", + author="MagicWord Team", + packages=find_packages(where="src"), + package_dir={"": "src"}, + include_package_data=True, + install_requires=[ + "python-docx>=0.8.10", + "PyPDF2>=1.26.0", + "ebooklib>=0.17.1", + "PyQt5>=5.15.0", + "requests>=2.25.1", + "beautifulsoup4>=4.11.0", + "pillow>=9.0.0", + "chardet>=4.0.0", + ], + entry_points={ + "console_scripts": [ + "magicword=main:main", + ], + }, + python_requires=">=3.6", +) \ No newline at end of file diff --git a/src/file_parser.py b/src/file_parser.py index fc44729..0e3d171 100644 --- a/src/file_parser.py +++ b/src/file_parser.py @@ -35,7 +35,7 @@ class FileParser: # 导入工具函数来检测编码 try: - from utils.helper_functions import Utils + from src.utils.helper_functions import Utils except ImportError: # 如果无法导入,使用默认方法检测编码 import chardet diff --git a/src/main.py b/src/main.py index 177583a..b8c620a 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,31 @@ # main.py import sys import traceback +import os + +# 添加项目根目录到Python路径 +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, project_root) + +# 设置Qt平台插件路径 - 优先使用系统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 +elif os.path.exists(venv_qt_plugins_path): + os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = venv_qt_plugins_path + from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import Qt +from PyQt5.QtGui import QIcon from src.main_window import MainWindow +# 设置高DPI支持(必须在QApplication创建之前) +QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) +QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + def main(): """ 应用程序主入口点 @@ -21,12 +42,16 @@ def main(): # 设置应用程序属性 app.setApplicationName("隐私学习软件") - app.setApplicationVersion("1.0") - app.setOrganizationName("个人开发者") + app.setApplicationVersion("0.1.0") + app.setOrganizationName("MagicWord Team") - # 设置高DPI支持 - app.setAttribute(Qt.AA_EnableHighDpiScaling, True) - app.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + # 设置窗口图标(如果存在) + icon_path = os.path.join(project_root, 'resources', 'icons', 'app_icon.png') + if os.path.exists(icon_path): + app.setWindowIcon(QIcon(icon_path)) + else: + # 使用默认图标 + app.setWindowIcon(QIcon()) # 创建主窗口 window = MainWindow() diff --git a/src/main_window.py b/src/main_window.py index 918e808..49187d4 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -1,10 +1,45 @@ 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 +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): @@ -22,24 +57,59 @@ class MainWindow(QMainWindow): 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组件 - - 创建中央文本编辑区域QTextEdit + - 创建自定义标题栏 + - 创建文本显示组件 - 调用createMenuBar()创建菜单 - 创建状态栏并显示"就绪" - - 连接文本变化信号到onTextChanged """ # 设置窗口属性 self.setWindowTitle("隐私学习软件 - 仿Word") self.setGeometry(100, 100, 800, 600) + self.setWindowFlags(Qt.FramelessWindowHint) # 移除默认标题栏 + + # 创建中央widget + central_widget = QWidget() + self.setCentralWidget(central_widget) - # 创建中央文本编辑区域 - self.text_edit = QTextEdit() - self.text_edit.setFont(QFont("Arial", 12)) - self.setCentralWidget(self.text_edit) + # 创建主布局 + 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() @@ -47,14 +117,73 @@ class MainWindow(QMainWindow): # 创建状态栏 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; }") - # 连接文本变化信号 - self.text_edit.textChanged.connect(self.onTextChanged) + # 设置标签样式 + 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) + - 视图菜单:显示统计信息、显示每日一言 - 帮助菜单:关于 - 为每个菜单项连接对应的槽函数 """ @@ -84,6 +213,30 @@ class MainWindow(QMainWindow): 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('帮助') @@ -92,12 +245,36 @@ class MainWindow(QMainWindow): 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 - 如果用户选择了文件,调用FileParser.parse_file(file_path) - - 成功时:将内容显示在文本区域,重置打字状态 + - 成功时:将内容存储但不直接显示,重置打字状态 - 失败时:显示错误消息框 """ options = QFileDialog.Options() @@ -115,15 +292,16 @@ class MainWindow(QMainWindow): content = FileParser.parse_file(file_path) self.learning_content = content - # 显示内容到文本编辑区域 - self.text_edit.setPlainText(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}") + self.status_bar.showMessage(f"已打开文件: {file_path},开始打字以显示内容") except Exception as e: # 显示错误消息框 QMessageBox.critical(self, "错误", f"无法打开文件:\n{str(e)}") @@ -179,63 +357,126 @@ class MainWindow(QMainWindow): "帮助提高打字速度和准确性。" ) - def onTextChanged(self): + def refresh_daily_quote(self): """ - 处理文本变化事件,实现打字逻辑 - - 获取当前文本内容 - - 调用打字逻辑检查输入正确性 - - 更新高亮显示和状态栏 + 刷新每日一言 + - 从网络API获取名言 + - 更新显示 """ - if self.typing_logic is None: - return - - # 获取当前文本内容 - current_text = self.text_edit.toPlainText() - - # 调用打字逻辑检查输入正确性 - result = self.typing_logic.check_input(current_text) - - # 更新高亮显示 - if result['correct']: - self.highlightText(len(current_text), QColor('lightgreen')) - else: - # 高亮显示错误部分 - self.highlightText(len(current_text), QColor('lightcoral')) - - # 更新状态栏 - progress = self.typing_logic.get_progress() - accuracy = result.get('accuracy', 0) * 100 - self.status_bar.showMessage( - f"进度: {progress['percentage']:.1f}% | " - f"准确率: {accuracy:.1f}% | " - f"位置: {result['position']}/{progress['total']}" - ) + 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 highlightText(self, position, color): + def onTextChanged(self): """ - 高亮显示从开始到指定位置的文本 - - 使用QTextCursor选择文本范围 - - 应用背景颜色格式 - - 恢复光标位置 + 处理用户输入变化事件(打字练习) + - 获取文本显示组件中的文本 + - 使用TypingLogic.check_input检查输入 + - 根据结果更新文本显示组件 + - 更新统计数据展示 """ - # 创建文本格式 - format = QTextCharFormat() - format.setBackground(color) - - # 获取文本游标 - cursor = self.text_edit.textCursor() - - # 保存当前光标位置 - current_pos = cursor.position() - - # 选择从开始到指定位置的文本 - cursor.select(QTextCursor.Document) - cursor.setPosition(0, QTextCursor.MoveAnchor) - cursor.setPosition(position, QTextCursor.KeepAnchor) - - # 应用格式 - cursor.mergeCharFormat(format) + # 防止递归调用 + if getattr(self, '_processing_text_change', False): + return + + if not self.typing_logic: + return + + # 设置标志防止递归 + self._processing_text_change = True - # 恢复光标位置 - cursor.setPosition(current_pos) - self.text_edit.setTextCursor(cursor) \ No newline at end of file + try: + # 获取当前输入文本 + current_text = self.text_display_widget.text_display.toPlainText() + + # 检查输入是否正确 + result = self.typing_logic.check_input(current_text) + is_correct = result["correct"] + expected_char = result["expected"] + + # 更新文本显示组件 + if self.text_display_widget: + # 显示用户输入反馈 + self.text_display_widget.show_user_input(current_text) + + # 不再高亮下一个字符,因为内容通过打字逐步显示 + + # 计算统计数据 + stats = self.typing_logic.get_statistics() + accuracy = stats['accuracy_rate'] * 100 # 转换为百分比 + # 可以根据需要添加更多统计数据的计算 + wpm = 0 # 暂时设置为0,后续可以实现WPM计算 + + # 更新状态栏 + self.status_bar.showMessage(f"WPM: {wpm:.1f} | 准确率: {accuracy:.1f}%") + + # 更新统计信息显示组件 + if hasattr(self, 'stats_display') and self.stats_display.isVisible(): + self.stats_display.update_stats(int(wpm), accuracy) + + # 更新每日一言显示组件(如果需要) + if hasattr(self, 'quote_display') and self.quote_display.isVisible() and not self.quote_display.quote_label.text(): + self.refresh_daily_quote() + + # 更新顶部功能区的统计数据(如果仍然存在) + if hasattr(self, 'wpm_label') and self.wpm_label: + self.wpm_label.setText(f"WPM: {wpm:.1f}") + if hasattr(self, 'accuracy_label') and self.accuracy_label: + self.accuracy_label.setText(f"准确率: {accuracy:.1f}%") + finally: + # 清除递归防止标志 + self._processing_text_change = False + + def refresh_daily_quote(self): + """刷新每日一言""" + # 创建并启动获取名言的线程 + self.quote_thread = QuoteFetchThread() + self.quote_thread.quote_fetched.connect(self.on_quote_fetched) + self.quote_thread.error_occurred.connect(self.on_quote_error) + self.quote_thread.start() + + def refresh_weather_info(self): + """刷新天气信息""" + # 创建并启动获取天气信息的线程 + self.weather_thread = WeatherFetchThread() + self.weather_thread.weather_fetched.connect(self.on_weather_fetched) + self.weather_thread.error_occurred.connect(self.on_weather_error) + self.weather_thread.start() + + def on_weather_fetched(self, weather_info): + """处理天气信息获取成功""" + # 更新天气显示组件 + if hasattr(self, 'weather_display') and self.weather_display: + self.weather_display.update_weather(weather_info) + + def on_weather_error(self, error_msg): + """处理天气信息获取错误""" + # 更新天气显示组件 + if hasattr(self, 'weather_display') and self.weather_display: + self.weather_display.update_weather(error_msg) \ No newline at end of file diff --git a/src/settings/settings_manager.py b/src/settings/settings_manager.py index d26a64a..f89d77f 100644 --- a/src/settings/settings_manager.py +++ b/src/settings/settings_manager.py @@ -28,9 +28,9 @@ class SettingsManager: return { "application": { "name": "MagicWord", - "version": "1.0.0", + "version": "0.1.0", "author": "MagicWord Team", - "description": "好东西" + "description": "隐私学习软件 - 一款通过打字练习来学习文档内容的工具" }, "window": { "default_size": { diff --git a/src/typing_logic.py b/src/typing_logic.py index ce3e659..c296b40 100644 --- a/src/typing_logic.py +++ b/src/typing_logic.py @@ -25,12 +25,19 @@ class TypingLogic: * completed: 布尔值,是否完成 * accuracy: 浮点数,准确率 """ + # 保存当前索引用于返回 + current_position = len(user_text) + + # 临时保存原始的typed_chars值用于准确率计算 + original_typed_chars = self.typed_chars + # 更新已输入字符数 self.typed_chars = len(user_text) # 如果用户输入的字符数超过了学习材料的长度,截取到相同长度 if len(user_text) > self.total_chars: user_text = user_text[:self.total_chars] + current_position = len(user_text) # 检查当前输入是否正确 correct = True @@ -39,31 +46,51 @@ class TypingLogic: expected_char = self.learning_content[self.current_index] if len(user_text) > self.current_index and user_text[self.current_index] != expected_char: correct = False - self.error_count += 1 else: # 已经完成所有输入 + # 恢复原始的typed_chars值用于准确率计算 + accuracy = self._calculate_accuracy() + self.typed_chars = original_typed_chars return { "correct": True, "expected": "", "position": self.current_index, "completed": True, - "accuracy": self._calculate_accuracy() + "accuracy": accuracy } - # 更新当前索引 - self.current_index = len(user_text) - # 检查是否完成 - completed = self.current_index >= self.total_chars + completed = current_position >= self.total_chars + + # 计算准确率 + accuracy = self._calculate_accuracy() + # 恢复原始的typed_chars值 + self.typed_chars = original_typed_chars return { "correct": correct, "expected": expected_char, - "position": self.current_index, + "position": current_position, "completed": completed, - "accuracy": self._calculate_accuracy() + "accuracy": accuracy } + def update_position(self, user_text: str): + """ + 更新当前索引和错误计数 + - 根据用户输入更新当前位置 + - 计算并更新错误计数 + """ + new_position = len(user_text) + + # 计算新增的错误数 + for i in range(self.current_index, min(new_position, self.total_chars)): + if user_text[i] != self.learning_content[i]: + self.error_count += 1 + + # 更新当前索引 + self.current_index = new_position + def get_expected_text(self, length: int = 10) -> str: """ 获取用户接下来应该输入的内容 @@ -127,8 +154,22 @@ class TypingLogic: """ 计算准确率 """ + # 防止递归的保护措施 + if hasattr(self, '_calculating_accuracy') and self._calculating_accuracy: + return 0.0 + if self.typed_chars == 0: return 0.0 - # 准确率 = (已输入字符数 - 错误次数) / 已输入字符数 - accuracy = (self.typed_chars - self.error_count) / self.typed_chars - return max(0.0, accuracy) # 确保准确率不为负数 \ No newline at end of file + + # 设置递归保护标志 + self._calculating_accuracy = True + + try: + # 准确率 = (已输入字符数 - 错误次数) / 已输入字符数 + accuracy = (self.typed_chars - self.error_count) / self.typed_chars + return max(0.0, min(1.0, accuracy)) # 确保准确率在0.0到1.0之间 + except (ZeroDivisionError, RecursionError): + return 0.0 + finally: + # 清除递归保护标志 + self._calculating_accuracy = False \ No newline at end of file diff --git a/src/ui/components.py b/src/ui/components.py index 948839f..b99a866 100644 --- a/src/ui/components.py +++ b/src/ui/components.py @@ -197,6 +197,196 @@ class ProgressBarWidget(QWidget): self.accuracy_label.setText(f"准确率: {accuracy:.1f}%") self.time_label.setText(f"用时: {time_elapsed}s") +class StatsDisplayWidget(QWidget): + def __init__(self, parent=None): + """ + 统计信息显示组件 + - 显示准确率、WPM等统计信息 + """ + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + """ + 设置统计信息显示UI + - 初始化所有UI组件 + - 设置组件属性和样式 + """ + # 创建水平布局 + layout = QHBoxLayout() + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(15) + + # 创建统计信息标签 + self.wpm_label = QLabel("WPM: 0") + self.accuracy_label = QLabel("准确率: 0%") + + # 设置标签样式 + label_style = "font-size: 12px; font-weight: normal; color: #333333;" + self.wpm_label.setStyleSheet(label_style) + self.accuracy_label.setStyleSheet(label_style) + + # 添加组件到布局 + layout.addWidget(self.wpm_label) + layout.addWidget(self.accuracy_label) + layout.addStretch() + + self.setLayout(layout) + + # 设置样式 + self.setStyleSheet(""" + StatsDisplayWidget { + background-color: #f0f0f0; + border-bottom: 1px solid #d0d0d0; + } + """) + + def update_stats(self, wpm: int, accuracy: float): + """ + 更新统计信息 + - wpm: 每分钟字数 + - accuracy: 准确率(%) + """ + self.wpm_label.setText(f"WPM: {wpm}") + self.accuracy_label.setText(f"准确率: {accuracy:.1f}%") + +class QuoteDisplayWidget(QWidget): + def __init__(self, parent=None): + """ + 每日一言显示组件 + - 显示每日一言功能 + """ + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + """ + 设置每日一言显示UI + - 初始化所有UI组件 + - 设置组件属性和样式 + """ + # 创建水平布局 + layout = QHBoxLayout() + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(15) + + # 创建每日一言标签 + 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.quote_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; + } + """) + + # 添加组件到布局 + layout.addWidget(self.quote_label) + layout.addStretch() + layout.addWidget(self.refresh_quote_button) + + self.setLayout(layout) + + # 设置样式 + self.setStyleSheet(""" + QuoteDisplayWidget { + background-color: #f0f0f0; + border-bottom: 1px solid #d0d0d0; + } + """) + + def update_quote(self, quote: str): + """ + 更新每日一言 + - quote: 每日一言内容 + """ + self.quote_label.setText(f"每日一言: {quote}") + +class WeatherDisplayWidget(QWidget): + def __init__(self, parent=None): + """ + 天气显示组件 + - 显示天气信息 + """ + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + """ + 设置天气显示UI + - 初始化所有UI组件 + - 设置组件属性和样式 + """ + # 创建水平布局 + layout = QHBoxLayout() + layout.setContentsMargins(10, 5, 10, 5) + layout.setSpacing(15) + + # 创建天气信息标签 + self.weather_label = QLabel("天气: 暂无") + + # 设置标签样式 + label_style = "font-size: 12px; font-weight: normal; color: #333333;" + self.weather_label.setStyleSheet(label_style) + + # 创建天气刷新按钮 + self.refresh_weather_button = QPushButton("刷新") + self.refresh_weather_button.setStyleSheet(""" + QPushButton { + background-color: #0078d7; + color: white; + border: none; + padding: 5px 10px; + border-radius: 3px; + font-size: 12px; + } + QPushButton:hover { + background-color: #005a9e; + } + """) + + # 添加组件到布局 + layout.addWidget(self.weather_label) + layout.addStretch() + layout.addWidget(self.refresh_weather_button) + + self.setLayout(layout) + + # 设置样式 + self.setStyleSheet(""" + WeatherDisplayWidget { + background-color: #f0f0f0; + border-bottom: 1px solid #d0d0d0; + } + """) + + def update_weather(self, weather_info: dict): + """ + 更新天气信息 + - weather_info: 天气信息字典 + """ + if weather_info: + city = weather_info.get("city", "未知") + temperature = weather_info.get("temperature", "N/A") + description = weather_info.get("description", "N/A") + self.weather_label.setText(f"天气: {city} {temperature}°C {description}") + else: + self.weather_label.setText("天气: 获取失败") + class TextDisplayWidget(QWidget): def __init__(self, parent=None): """ @@ -226,7 +416,7 @@ class TextDisplayWidget(QWidget): # 创建文本显示区域 self.text_display = QTextEdit() - self.text_display.setReadOnly(True) + self.text_display.setReadOnly(False) # 设置为可编辑 self.text_display.setLineWrapMode(QTextEdit.WidgetWidth) # 设置文本显示样式 @@ -253,7 +443,8 @@ class TextDisplayWidget(QWidget): """ self.text_content = text self.current_index = 0 - self._update_display() + # 初始不显示内容,通过打字逐步显示 + self.text_display.setHtml("") def highlight_character(self, position: int): """ @@ -262,7 +453,8 @@ class TextDisplayWidget(QWidget): """ if 0 <= position < len(self.text_content): self.current_index = position - self._update_display() + # 不再直接高亮字符,而是通过用户输入来显示内容 + pass def _update_display(self, user_input: str = ""): """ @@ -271,55 +463,37 @@ class TextDisplayWidget(QWidget): """ # 导入需要的模块 from PyQt5.QtGui import QTextCursor - import sys - import os - sys.path.append(os.path.join(os.path.dirname(__file__), '..')) - from constants import COLOR_CORRECT, COLOR_WRONG, COLOR_HIGHLIGHT if not self.text_content: self.text_display.clear() return - # 创建带格式的HTML文本 - formatted_text = "" - - # 如果有用户输入,对比显示 + # 简单显示文本,不使用任何高亮 if user_input: - for i, char in enumerate(self.text_content): - if i < len(user_input): - if char == user_input[i]: - # 正确字符 - formatted_text += f'{char}' - else: - # 错误字符 - formatted_text += f'{char}' - elif i == len(user_input): - # 当前字符 - formatted_text += f'{char}' - else: - # 未到达的字符 - formatted_text += char + # 只显示用户已输入的部分文本 + displayed_text = self.text_content[:len(user_input)] else: - # 没有用户输入,只显示原文和当前高亮字符 - for i, char in enumerate(self.text_content): - if i == self.current_index: - # 当前字符高亮 - formatted_text += f'{char}' - else: - formatted_text += char + # 没有用户输入,不显示任何内容 + displayed_text = "" # 更新文本显示 - self.text_display.setHtml(formatted_text) + self.text_display.setPlainText(displayed_text) - # 滚动到当前高亮字符位置 - cursor = self.text_display.textCursor() - cursor.setPosition(min(self.current_index + 5, len(self.text_content))) - self.text_display.setTextCursor(cursor) - self.text_display.ensureCursorVisible() + # 安全地滚动到光标位置 + if user_input and displayed_text: + try: + cursor = self.text_display.textCursor() + # 将光标定位到文本末尾 + cursor.setPosition(len(displayed_text)) + self.text_display.setTextCursor(cursor) + self.text_display.ensureCursorVisible() + except Exception: + # 如果光标定位失败,忽略错误 + pass def show_user_input(self, input_text: str): """ - 显示用户输入反馈 + 显示用户输入的文本 - input_text: 用户输入的文本 """ self._update_display(input_text) \ No newline at end of file