diff --git a/README.md b/README.md
index c0f9a87..bb5d9e4 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,53 @@ 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插件
+- 改进了应用程序启动脚本
+
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/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/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..95fec00 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():
"""
应用程序主入口点
@@ -24,9 +45,13 @@ def main():
app.setApplicationVersion("1.0")
app.setOrganizationName("个人开发者")
- # 设置高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/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