You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Curriculum_Design/src/learning_mode_window.py

723 lines
29 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# learning_mode_window.py
import sys
import os
from PyQt5.QtWidgets import (QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QTextEdit, QLabel, QFrame, QMenuBar,
QAction, QFileDialog, QMessageBox, QApplication,
QSplitter, QScrollArea, QStatusBar, QProgressBar, QTextBrowser, QSizePolicy,
QListWidget, QListWidgetItem, QDialog, QGraphicsScene, QGraphicsView)
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QRect
from PyQt5.QtGui import QFont, QPalette, QColor, QIcon, QPixmap, QTextCharFormat, QTextCursor
# 修复导入路径
from ui.components import CustomTitleBar, TextDisplayWidget
from typing_logic import TypingLogic
from file_parser import FileParser
from ui.theme_manager import theme_manager
import tempfile
import hashlib
class LearningModeWindow(QMainWindow):
# 定义内容变化信号
content_changed = pyqtSignal(str, int) # 参数:内容,位置
# 定义关闭信号
closed = pyqtSignal()
def __init__(self, parent=None, imported_content="", current_position=0, image_data=None, image_positions=None):
"""
学习模式窗口
- 顶部显示UI.png图片
- 下方显示输入字符页面
- 输入字符显示导入的文件内容
参数:
parent: 父窗口
imported_content: 从主窗口传递的导入内容
current_position: 当前学习进度位置
image_data: 图片数据字典 {文件名: 二进制数据}
image_positions: 图片位置信息列表
"""
super().__init__(parent)
self.parent_window = parent
self.imported_content = imported_content
self.current_position = current_position
self.image_data = image_data or {}
self.image_positions = image_positions or []
self.typing_logic = None
self.is_loading_file = False
self.extracted_images = [] # 用于存储提取的图片数据
# 初始化UI
self.initUI()
# 初始化打字逻辑
self.init_typing_logic()
# 初始化同步位置跟踪
self.last_sync_position = current_position
# 如果有导入内容,初始化显示
if self.imported_content:
self.initialize_with_imported_content()
def initialize_with_imported_content(self):
"""
使用从主窗口传递的导入内容初始化学习模式窗口
"""
if not self.imported_content:
return
# 重置打字逻辑
if self.typing_logic:
self.typing_logic.reset(self.imported_content)
# 设置图片数据到打字逻辑
if self.image_data:
self.typing_logic.set_image_data(self.image_data)
if self.image_positions:
self.typing_logic.set_image_positions(self.image_positions)
# 显示已学习的内容
display_text = self.imported_content[:self.current_position]
self.text_display_widget.text_display.setPlainText(display_text)
# 更新状态
progress = (self.current_position / len(self.imported_content)) * 100
self.status_label.setText(f"继续学习 - 进度: {self.current_position}/{len(self.imported_content)} 字符")
self.progress_label.setText(f"进度: {progress:.1f}%")
# 设置光标位置到文本末尾
cursor = self.text_display_widget.text_display.textCursor()
cursor.movePosition(cursor.End)
self.text_display_widget.text_display.setTextCursor(cursor)
def initUI(self):
"""
初始化学习模式窗口UI
- 设置窗口标题和大小
- 创建顶部图片区域
- 创建下方输入区域
"""
# 设置窗口属性
self.setWindowTitle("学习模式 - MagicWord")
self.setGeometry(200, 200, 900, 700)
self.setWindowFlags(Qt.Window) # 独立窗口
# 创建中央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.create_top_image_area(main_layout)
# 创建分隔线
separator = QFrame()
separator.setFrameShape(QFrame.HLine)
separator.setFrameShadow(QFrame.Sunken)
separator.setStyleSheet("background-color: #d0d0d0;")
main_layout.addWidget(separator)
# 创建输入区域
self.create_input_area(main_layout)
# 创建菜单栏
self.create_menu_bar()
# 创建状态栏
self.create_status_bar()
# 如果有导入内容,更新状态栏显示
if self.imported_content:
progress = (self.current_position / len(self.imported_content)) * 100
self.status_label.setText(f"继续学习 - 进度: {self.current_position}/{len(self.imported_content)} 字符")
self.progress_label.setText(f"进度: {progress:.1f}%")
def create_top_image_area(self, main_layout):
"""
创建顶部图片区域
- 加载并显示UI.png图片完全铺满窗口上方
- 窗口大小自动适配图片大小
"""
# 创建图片标签
self.image_label = QLabel()
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setStyleSheet("""
QLabel {
background-color: #f8f9fa;
border: none;
margin: 0px;
padding: 0px;
}
""")
# 加载UI.png图片
ui_image_path = os.path.join(os.path.dirname(__file__), 'ui', 'UI.png')
if os.path.exists(ui_image_path):
pixmap = QPixmap(ui_image_path)
if not pixmap.isNull():
# 保存原始图片用于缩放计算
self.original_pixmap = pixmap
# 设置图片到标签
self.image_label.setPixmap(pixmap)
self.image_label.setScaledContents(True) # 关键:让图片缩放填充整个标签
# 设置大小策略
self.image_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
# 计算合适的最小高度(保持图片比例)
window_width = 900 # 默认窗口宽度
original_width = pixmap.width()
original_height = pixmap.height()
if original_width > 0:
min_height = int(window_width * original_height / original_width)
self.image_label.setMinimumHeight(min_height)
else:
self.image_label.setMinimumHeight(200)
else:
self.image_label.setText("UI图片加载失败")
self.image_label.setStyleSheet("""
QLabel {
background-color: #f8f9fa;
color: #666666;
font-size: 14px;
qproperty-alignment: AlignCenter;
}
""")
else:
self.image_label.setText("UI图片未找到")
self.image_label.setStyleSheet("""
QLabel {
background-color: #f8f9fa;
color: #666666;
font-size: 14px;
qproperty-alignment: AlignCenter;
}
""")
# 添加到主布局
main_layout.addWidget(self.image_label)
def resizeEvent(self, event):
"""
窗口大小改变事件
- 动态调整图片显示区域
"""
super().resizeEvent(event)
# 获取窗口宽度
window_width = self.width()
# 如果图片标签存在,调整其大小以适配窗口
if hasattr(self, 'image_label') and self.image_label:
# 设置图片标签的固定宽度为窗口宽度
self.image_label.setFixedWidth(window_width)
# 根据窗口宽度计算合适的高度(保持图片比例)
if hasattr(self, 'original_pixmap') and self.original_pixmap:
original_width = self.original_pixmap.width()
original_height = self.original_pixmap.height()
# 计算保持比例的高度
new_height = int(window_width * original_height / original_width)
self.image_label.setFixedHeight(new_height)
else:
# 如果没有原始图片,使用默认高度
self.image_label.setFixedHeight(300)
def create_input_area(self, main_layout):
"""
创建输入区域
- 创建文本显示组件
- 设置与主系统相同的样式
- 创建图片列表区域
"""
# 创建文本显示组件(复用主系统的组件)
self.text_display_widget = TextDisplayWidget(self)
# 设置样式,使其与主系统保持一致
self.text_display_widget.setStyleSheet("""
TextDisplayWidget {
background-color: white;
border: 1px solid #d0d0d0;
border-radius: 0px;
}
""")
# 连接文本变化信号
self.text_display_widget.text_display.textChanged.connect(self.on_text_changed)
# 创建图片显示区域
self.image_list_widget = QListWidget()
self.image_list_widget.setMaximumHeight(150)
self.image_list_widget.setStyleSheet("""
QListWidget {
background-color: #f8f8f8;
border: 1px solid #d0d0d0;
border-radius: 4px;
font-size: 11px;
}
QListWidget::item {
padding: 5px;
border-bottom: 1px solid #e0e0e0;
}
QListWidget::item:selected {
background-color: #e3f2fd;
color: #1976d2;
}
""")
self.image_list_widget.setVisible(False) # 默认隐藏
self.image_list_widget.itemDoubleClicked.connect(self.on_image_item_double_clicked)
# 创建布局容器
input_container = QWidget()
input_layout = QVBoxLayout()
input_layout.setContentsMargins(0, 0, 0, 0)
input_layout.setSpacing(5)
input_layout.addWidget(self.text_display_widget, 1) # 文本显示区域占据剩余空间
input_layout.addWidget(self.image_list_widget) # 图片列表区域
input_container.setLayout(input_layout)
main_layout.addWidget(input_container, 1) # 占据剩余空间
def create_menu_bar(self):
"""
创建菜单栏
- 文件菜单:导入、退出
- 帮助菜单:关于
"""
menu_bar = self.menuBar()
# 文件菜单
file_menu = menu_bar.addMenu('文件')
# 导入动作
import_action = QAction('导入文件', self)
import_action.setShortcut('Ctrl+O')
import_action.triggered.connect(self.import_file)
file_menu.addAction(import_action)
file_menu.addSeparator()
# 退出动作
exit_action = QAction('退出学习模式', self)
exit_action.setShortcut('Ctrl+Q')
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
# 帮助菜单
help_menu = menu_bar.addMenu('帮助')
about_action = QAction('关于', self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
def create_status_bar(self):
"""
创建状态栏
- 显示当前状态和学习进度
"""
self.status_bar = self.statusBar()
# 创建状态标签
self.status_label = QLabel("就绪 - 请先导入文件开始学习")
self.progress_label = QLabel("进度: 0%")
# 添加到状态栏
self.status_bar.addWidget(self.status_label)
self.status_bar.addPermanentWidget(self.progress_label)
def init_typing_logic(self):
"""
初始化打字逻辑
- 创建打字逻辑实例
- 设置默认内容
"""
# 创建打字逻辑实例
self.typing_logic = TypingLogic("欢迎使用学习模式!\n\n请先导入文件开始打字学习。")
# 设置文本显示组件的打字逻辑
if hasattr(self.text_display_widget, 'set_typing_logic'):
self.text_display_widget.set_typing_logic(self.typing_logic)
def import_file(self):
"""
导入文件
- 打开文件对话框选择文件
- 解析文件内容
- 重置打字逻辑
"""
file_path, _ = QFileDialog.getOpenFileName(
self, "导入学习文件", "",
"文档文件 (*.docx *.txt *.pdf);;所有文件 (*.*)"
)
if file_path:
self.is_loading_file = True
try:
# 获取文件扩展名
_, ext = os.path.splitext(file_path)
ext = ext.lower()
# 对于docx文件直接解析而不转换为txt
if ext == '.docx':
# 直接解析docx文件内容
content = FileParser.parse_docx(file_path)
# 提取图片数据
images = FileParser.extract_images_from_docx(file_path)
else:
# 其他文件类型使用原来的转换方法
result = FileParser.parse_and_convert_to_txt(file_path)
content = result['content']
images = result.get('images', [])
if not content:
QMessageBox.warning(self, "导入失败", "文件内容为空或解析失败!")
return
# 保存导入的内容
self.imported_content = content
self.current_position = 0
# 保存提取的图片数据
self.extracted_images = images
# 设置打字逻辑
if self.typing_logic:
self.typing_logic.reset(content)
# 如果有图片,设置图片数据到打字逻辑
if images:
image_data_dict = {}
image_positions = []
# 为每张图片生成位置信息 - 改进位置计算逻辑
for i, (filename, image_data) in enumerate(images):
image_data_dict[filename] = image_data
# 改进图片位置计算,确保图片能在用户早期打字时显示
content_length = len(content)
if content_length == 0:
content_length = 1000 # 备用长度
if len(images) == 1:
# 只有一张图片放在文档开始位置附近前10%),确保用户能快速看到
image_pos = max(10, content_length // 10)
else:
# 多张图片:前几张放在较前位置,确保用户能看到
if i < 3:
# 前3张图片放在文档前30%
segment = content_length // 3
image_pos = max(10, segment * (i + 1) // 4)
else:
# 其余图片均匀分布
remaining_start = content_length // 2
remaining_index = i - 3
remaining_count = len(images) - 3
if remaining_count > 0:
segment = (content_length - remaining_start) // (remaining_count + 1)
image_pos = remaining_start + segment * (remaining_index + 1)
else:
image_pos = content_length // 2
image_positions.append({
'start_pos': image_pos,
'end_pos': image_pos + 50, # 图片占位符长度
'filename': filename
})
# 设置图片数据到打字逻辑
self.typing_logic.set_image_data(image_data_dict)
self.typing_logic.set_image_positions(image_positions)
# 显示图片列表
self.display_image_list(images)
# 显示初始内容(空)
self.text_display_widget.text_display.clear()
self.status_label.setText("已导入文件,请开始打字学习...")
self.progress_label.setText("进度: 0%")
except Exception as e:
QMessageBox.critical(self, "导入错误", f"导入文件时发生错误:\n{str(e)}")
finally:
self.is_loading_file = False
def on_text_changed(self):
"""
文本变化处理
- 根据导入的内容逐步显示
- 更新学习进度
- 同步内容到打字模式
- 处理图片插入
"""
# 如果正在加载文件,跳过处理
if self.is_loading_file:
return
# 如果没有导入内容,清空文本
if not self.imported_content:
current_text = self.text_display_widget.text_display.toPlainText()
if current_text:
self.text_display_widget.text_display.clear()
self.status_label.setText("请先导入文件开始学习")
return
# 获取当前文本
current_text = self.text_display_widget.text_display.toPlainText()
# 始终确保显示的是导入的文件内容,而不是用户输入的文字
expected_text = self.imported_content[:len(current_text)]
# 如果当前文本与期望文本不一致,强制恢复到期望文本
if current_text != expected_text:
cursor = self.text_display_widget.text_display.textCursor()
self.text_display_widget.text_display.setPlainText(expected_text)
self.text_display_widget.text_display.setTextCursor(cursor)
# 显示错误信息
if len(current_text) > 0:
expected_char = self.imported_content[len(current_text)-1] if len(current_text) <= len(self.imported_content) else ''
self.status_label.setText(f"输入错误!期望字符: '{expected_char}'")
return
# 检查输入是否正确
if self.typing_logic and len(current_text) > 0:
result = self.typing_logic.check_input(current_text)
if not result['correct']:
# 输入错误,恢复到正确的状态
expected_text = self.imported_content[:len(current_text)]
if current_text != expected_text:
cursor = self.text_display_widget.text_display.textCursor()
self.text_display_widget.text_display.setPlainText(expected_text)
self.text_display_widget.text_display.setTextCursor(cursor)
# 显示错误信息
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
self.progress_label.setText(
f"进度: {progress:.1f}% ({self.current_position}/{len(self.imported_content)} 字符)"
)
# 只在用户新输入的字符上同步到打字模式
if self.parent_window:
# 获取用户这一轮新输入的字符(与上一轮相比的新内容)
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("恭喜!学习完成!")
QMessageBox.information(self, "学习完成", "恭喜您完成了所有学习内容!")
else:
self.status_label.setText("继续输入以显示更多内容...")
def show_about(self):
"""
显示关于对话框
"""
QMessageBox.about(self, "关于学习模式",
"MagicWord 学习模式\n\n"
"功能特点:\n"
"• 顶部显示UI界面图片\n"
"• 下方为打字输入区域\n"
"• 导入文件后逐步显示内容\n"
"• 实时显示学习进度\n"
"• 支持图片显示\n\n"
"使用方法:\n"
"1. 点击'文件'->'导入文件'选择学习材料\n"
"2. 在下方文本区域开始打字\n"
"3. 系统会根据您的输入逐步显示内容")
def closeEvent(self, event):
"""
窗口关闭事件
- 通知父窗口学习模式已关闭
"""
# 发射关闭信号
self.closed.emit()
if self.parent_window and hasattr(self.parent_window, 'on_learning_mode_closed'):
self.parent_window.on_learning_mode_closed()
event.accept()
def keyPressEvent(self, event):
"""
按键事件处理
- 处理特殊按键
"""
# 处理Escape键关闭窗口
if event.key() == Qt.Key_Escape:
self.close()
else:
super().keyPressEvent(event)
def display_image_list(self, images):
"""
显示图片列表
"""
try:
# 清空之前的图片列表
self.image_list_widget.clear()
# 如果没有图片,隐藏图片列表区域
if not images:
self.image_list_widget.setVisible(False)
return
# 显示图片列表区域
self.image_list_widget.setVisible(True)
# 添加图片项到列表
for index, (filename, image_data) in enumerate(images):
# 创建缩略图
pixmap = QPixmap()
if pixmap.loadFromData(image_data):
# 创建缩略图
thumbnail = pixmap.scaled(60, 60, Qt.KeepAspectRatio, Qt.SmoothTransformation)
# 创建列表项
item = QListWidgetItem()
item.setIcon(QIcon(thumbnail))
item.setText(f"{filename} ({pixmap.width()}x{pixmap.height()})")
item.setData(Qt.UserRole, index) # 保存图片索引
self.image_list_widget.addItem(item)
else:
# 如果无法加载图片,显示默认文本
item = QListWidgetItem(f"{filename} (无法预览)")
item.setData(Qt.UserRole, index)
self.image_list_widget.addItem(item)
# 更新状态栏
self.status_label.setText(f"已提取 {len(images)} 张图片,双击查看大图")
except Exception as e:
self.status_label.setText(f"显示图片列表失败: {str(e)}")
def on_image_item_double_clicked(self, item):
"""
双击图片项时显示大图
"""
try:
# 获取图片索引
index = item.data(Qt.UserRole)
if 0 <= index < len(self.extracted_images):
image_filename, image_data = self.extracted_images[index]
self.show_image_viewer(image_filename, image_data)
except Exception as e:
self.status_label.setText(f"显示图片失败: {str(e)}")
def show_image_viewer(self, filename, image_data):
"""
显示图片查看器 - 支持缩放功能
"""
try:
# 创建自定义图片查看窗口
viewer = QDialog(self)
viewer.setWindowTitle(f"图片查看 - {filename}")
viewer.setModal(False)
# 设置窗口标志,保留标题栏以便用户可以移动和调整大小
viewer.setWindowFlags(Qt.Tool | Qt.WindowCloseButtonHint | Qt.WindowMinMaxButtonsHint)
# 设置窗口背景为黑色
viewer.setStyleSheet("""
QDialog {
background-color: #000000;
}
""")
# 创建场景和视图
scene = QGraphicsScene(viewer)
view = QGraphicsView(scene)
view.setStyleSheet("border: none;") # 移除视图边框
# 设置视图为可交互的,并启用滚动条
view.setDragMode(QGraphicsView.ScrollHandDrag)
view.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
# 创建布局
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(view)
viewer.setLayout(layout)
# 加载图片
pixmap = QPixmap()
if not pixmap.loadFromData(image_data):
self.status_label.setText(f"加载图片失败: {filename}")
return
# 将图片添加到场景
scene.addPixmap(pixmap)
# 设置视图大小和位置
if self:
parent_geometry = self.geometry()
screen_geometry = QApplication.primaryScreen().geometry()
# 设置窗口宽度与主窗口相同高度为屏幕高度的40%
window_width = parent_geometry.width()
window_height = int(screen_geometry.height() * 0.4)
# 计算位置:显示在主窗口正上方
x = parent_geometry.x()
y = parent_geometry.y() - window_height
# 确保不会超出屏幕边界
if y < screen_geometry.top():
y = parent_geometry.y() + 50 # 如果上方空间不足,显示在下方
# 调整宽度确保不超出屏幕
if x + window_width > screen_geometry.right():
window_width = screen_geometry.right() - x
viewer.setGeometry(x, y, window_width, window_height)
viewer.show()
# 设置视图适应图片大小
view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
# 重写视图的滚轮事件以支持缩放
def wheelEvent(event):
factor = 1.2
if event.angleDelta().y() > 0:
view.scale(factor, factor)
else:
view.scale(1.0/factor, 1.0/factor)
view.wheelEvent = wheelEvent
# 添加双击重置视图功能
def mouseDoubleClickEvent(event):
view.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
view.mouseDoubleClickEvent = mouseDoubleClickEvent
except Exception as e:
self.status_label.setText(f"创建图片查看器失败: {str(e)}")
import traceback
traceback.print_exc()