main
马子昂 5 months ago
parent 656574bc4d
commit 7c8ae3a2cb

@ -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插件
- 改进了应用程序启动脚本

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

@ -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 "如需再次启动应用程序,请重新运行此脚本"

@ -35,7 +35,7 @@ class FileParser:
# 导入工具函数来检测编码
try:
from utils.helper_functions import Utils
from src.utils.helper_functions import Utils
except ImportError:
# 如果无法导入,使用默认方法检测编码
import chardet

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

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

@ -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) # 确保准确率不为负数
# 设置递归保护标志
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

@ -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'<span style="background-color: {COLOR_CORRECT.name()}; color: black;">{char}</span>'
else:
# 错误字符
formatted_text += f'<span style="background-color: {COLOR_WRONG.name()}; color: white;">{char}</span>'
elif i == len(user_input):
# 当前字符
formatted_text += f'<span style="background-color: {COLOR_HIGHLIGHT.name()}; color: black;">{char}</span>'
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'<span style="background-color: {COLOR_HIGHLIGHT.name()}; color: black;">{char}</span>'
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)
Loading…
Cancel
Save