123 #90

Merged
pshjeamgr merged 6 commits from main into llllllllllllllCC 4 months ago

@ -14,6 +14,10 @@ from src.file_parser import FileParser
from src.ui.theme_manager import theme_manager
class LearningModeWindow(QMainWindow):
# 定义内容变化信号
content_changed = pyqtSignal(str, int) # 参数:内容,位置
# 定义关闭信号
closed = pyqtSignal()
def __init__(self, parent=None, imported_content="", current_position=0):
"""
学习模式窗口
@ -39,6 +43,9 @@ class LearningModeWindow(QMainWindow):
# 初始化打字逻辑
self.init_typing_logic()
# 初始化同步位置跟踪
self.last_sync_position = current_position
# 如果有导入内容,初始化显示
if self.imported_content:
self.initialize_with_imported_content()
@ -335,6 +342,7 @@ class LearningModeWindow(QMainWindow):
文本变化处理
- 根据导入的内容逐步显示
- 更新学习进度
- 同步内容到打字模式
"""
# 如果正在加载文件,跳过处理
if self.is_loading_file:
@ -382,6 +390,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 +398,15 @@ 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("恭喜!学习完成!")
@ -417,6 +435,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()

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

@ -83,6 +83,10 @@ class WordStyleMainWindow(QMainWindow):
self.learning_text = "" # 学习模式下的文本内容
self.cursor_position = 0 # 光标位置
# 学习模式窗口引用和同步标记
self.learning_window = None # 学习模式窗口引用
self.sync_from_learning = False # 从学习模式同步内容的标记
# 统一文档内容管理
self.unified_document_content = "" # 统一文档内容
self.last_edit_mode = "typing" # 上次编辑模式
@ -836,6 +840,10 @@ class WordStyleMainWindow(QMainWindow):
# 如果正在加载文件,跳过处理
if self.is_loading_file:
return
# 检查是否是从学习模式同步内容,避免递归调用
if hasattr(self, 'sync_from_learning') and self.sync_from_learning:
return
# 根据当前视图模式处理
if self.view_mode == "learning":
@ -852,8 +860,9 @@ class WordStyleMainWindow(QMainWindow):
self.handle_learning_mode_typing()
elif self.view_mode == "typing":
# 打字模式:可以自由打字
self.handle_typing_mode_typing()
# 打字模式:可以自由打字,不自动处理内容
# 只在用户主动操作时处理,避免内容被覆盖
pass
# 标记文档为已修改
if not self.is_modified:
@ -1856,10 +1865,24 @@ class WordStyleMainWindow(QMainWindow):
imported_content = self.imported_content
if hasattr(self, 'learning_progress') and self.learning_progress > 0:
current_position = self.learning_progress
else:
# 如果没有导入内容,检查当前打字模式的内容
current_text = self.text_edit.toPlainText()
if current_text and current_text != "在此输入您的内容...":
# 将打字模式的内容作为学习模式的导入内容
imported_content = current_text
current_position = 0
self.imported_content = current_text
# 创建学习模式窗口,直接传递导入内容
self.learning_window = LearningModeWindow(self, imported_content, current_position)
# 连接学习模式窗口的内容变化信号
self.learning_window.content_changed.connect(self.on_learning_content_changed)
# 连接学习模式窗口的关闭信号
self.learning_window.closed.connect(self.on_learning_mode_closed)
# 显示学习模式窗口
self.learning_window.show()
@ -1890,8 +1913,46 @@ class WordStyleMainWindow(QMainWindow):
self.learning_mode_action.setChecked(False)
self.typing_mode_action.setChecked(True)
self.view_mode = "typing"
# 清除学习窗口引用
self.learning_window = None
self.status_bar.showMessage("学习模式窗口已关闭", 2000)
def on_learning_content_changed(self, new_content, position):
"""学习模式内容变化时的回调 - 只在末尾追加新内容"""
# 设置同步标记,防止递归调用
self.sync_from_learning = True
try:
# 只在末尾追加新输入的内容,不修改已有内容
if new_content:
# 直接在末尾追加新内容
cursor = self.text_edit.textCursor()
cursor.movePosition(QTextCursor.End)
cursor.insertText(new_content)
# 更新导入内容(但不覆盖用户额外输入的内容)
if self.imported_content:
self.imported_content += new_content
else:
self.imported_content = new_content
self.learning_progress = len(self.imported_content) if self.imported_content else 0
# 重置打字逻辑(不追踪进度)
if self.typing_logic:
self.typing_logic.imported_content = self.imported_content
self.typing_logic.current_index = self.learning_progress
self.typing_logic.typed_chars = self.learning_progress
# 更新状态栏
self.status_bar.showMessage(f"从学习模式同步新内容: {new_content}", 3000)
finally:
# 重置同步标记
self.sync_from_learning = False
def set_page_color(self, color):
"""设置页面颜色"""
color_map = {

Binary file not shown.
Loading…
Cancel
Save