基于PyQt5的视频答题竞赛系统设计与实现

项目概述

视频答题竞赛系统是一个基于PyQt5框架开发的桌面应用程序,旨在为用户提供一个在观看视频时能够随时暂停并记录问题的平台。该系统集成了视频播放、答题记录、截图保存和报告生成等多项功能,适用于教育、培训和竞赛等多种场景。

系统架构

1. 技术栈

  • 前端框架: PyQt5 (Qt for Python)

  • 视频处理: OpenCV (cv2)

  • 数据库: SQLite

  • 数据处理: Pandas

  • 日志系统: Python logging模块

  • 打包工具: PyInstaller (支持)

2. 系统模块设计

复制代码
VideoQuizSystem/
├── 用户界面层 (GUI)
│   ├── 主窗口 (VideoQuizSystem)
│   ├── 姓名输入对话框 (NameInputDialog)
│   └── 答题对话框 (AnswerDialog)
├── 业务逻辑层
│   ├── 视频播放控制
│   ├── 答题逻辑处理
│   └── 报告生成引擎
├── 数据持久层
│   ├── SQLite数据库管理
│   └── 本地文件存储
└── 工具支持层
    ├── 日志记录器
    ├── 异常处理
    └── 资源管理

核心功能详解

1. 用户身份认证

  • 启动时强制用户输入姓名

  • 支持中文姓名输入

  • 姓名将用于报告生成和记录追踪

python 复制代码
class NameInputDialog(QDialog):
    """强制用户输入姓名的对话框"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("请输入姓名")
        self.setFixedSize(400, 150)
        # ... 界面初始化代码

2. 视频播放与管理

2.1 视频播放器功能
  • 支持多种视频格式(MP4、AVI、MOV、MKV等)

  • 完整的播放控制:播放、暂停、跳转

  • 实时进度显示和剩余时间计算

  • 全屏播放支持

2.2 视频选择机制
python 复制代码
def select_video(self):
    """选择视频文件并进行状态检查"""
    # 检查当前是否有活跃视频
    if self.current_video_active and not self.video_completed:
        QMessageBox.warning(self, "操作不允许", 
                          "请先完成当前视频再选择新视频。")
        return
    
    # 打开文件选择对话框
    file_path, _ = QFileDialog.getOpenFileName(
        self, "选择视频文件", "", 
        "视频文件 (*.mp4 *.avi *.mov *.mkv *.flv *.wmv)")
    
    # 重置并初始化新视频
    if file_path:
        self.reset_video_state()
        self.video_path = file_path
        self.media_player.setMedia(QMediaContent(QUrl.fromLocalFile(file_path)))

3. 答题功能设计

3.1 答题流程
  1. 暂停视频: 用户点击"暂停答题"按钮

  2. 捕获截图: 自动捕获当前视频帧

  3. 记录时间戳: 精确记录答题时间点

  4. 输入问题: 弹出对话框供用户输入问题描述

  5. 保存记录: 将答题信息保存到数据库

3.2 答题次数限制
  • 每个视频最多允许10次答题

  • 实时显示剩余答题次数

  • 次数用完后自动生成报告

python 复制代码
def pause_video(self):
    """处理视频暂停答题逻辑"""
    # 检查答题次数
    if self.remaining_answers <= 0:
        QMessageBox.warning(self, "提示", "答题次数已用完!")
        return
    
    # 暂停视频并捕获截图
    self.media_player.pause()
    timestamp = self.media_player.position() / 1000.0
    
    # 创建答题对话框
    self.current_dialog = AnswerDialog(screenshot, timestamp, 
                                     self.video_duration, self)
    self.current_dialog.show()

4. 数据存储与管理

4.1 数据库设计
sql 复制代码
-- 答题记录表
CREATE TABLE answers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    video_path TEXT,          -- 视频路径
    timestamp REAL,           -- 时间戳(秒)
    question TEXT,            -- 问题描述
    screenshot BLOB           -- 截图二进制数据
);

-- 视频统计表
CREATE TABLE video_stats (
    video_path TEXT PRIMARY KEY,
    remaining_answers INTEGER,   -- 剩余答题次数
    report_generated INTEGER,    -- 报告是否已生成
    start_time REAL,             -- 开始时间戳
    end_time REAL               -- 结束时间戳
);
4.2 数据持久化策略
  • 自动保存: 每次答题后立即保存到数据库

  • 状态恢复: 重启程序后可恢复视频状态

  • 防数据丢失: 使用事务确保数据完整性

5. 报告生成系统

5.1 报告类型
  • 自动生成报告: 答题次数用完或视频播放完成时自动生成

  • 手动生成报告: 用户主动触发生成报告

  • 批量生成报告: 为所有视频生成汇总报告

5.2 HTML报告设计
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>视频答题报告</title>
    <style>
        /* 现代化的CSS样式 */
        .answer-card {
            background-color: white;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            padding: 20px;
            margin-bottom: 30px;
        }
        .screenshot img {
            max-width: 100%;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <!-- 报告内容 -->
</body>
</html>
5.3 报告内容
  1. 用户信息: 姓名、报告生成时间

  2. 视频信息: 视频名称、时长、答题次数

  3. 时间统计: 开始时间、结束时间、答题用时

  4. 答题记录: 每个答题点的时间戳、截图和问题描述

  5. 摘要信息: 答题数量、生成方式

6. 用户界面设计

6.1 主界面布局
python 复制代码
def init_ui(self):
    """初始化用户界面"""
    main_layout = QVBoxLayout()
    
    # 控制栏
    control_layout = QHBoxLayout()
    control_layout.addWidget(self.select_btn)   # 选择视频
    control_layout.addWidget(self.play_btn)     # 播放
    control_layout.addWidget(self.pause_btn)    # 暂停答题
    control_layout.addWidget(self.finish_btn)   # 交卷
    control_layout.addWidget(self.report_btn)   # 生成报告
    
    # 视频播放区域
    self.video_widget = QVideoWidget()
    main_layout.addWidget(self.video_widget, 1)
    
    # 进度条
    progress_layout = QHBoxLayout()
    progress_layout.addWidget(self.current_time_label)
    progress_layout.addWidget(self.progress_slider)
    progress_layout.addWidget(self.total_time_label)
    
    # 状态信息
    main_layout.addWidget(self.time_label)
    main_layout.addWidget(self.status_label)
    main_layout.addWidget(self.name_label)
6.2 界面特点
  • 响应式设计: 适应不同屏幕尺寸

  • 大字体支持: 便于观看

  • 直观操作: 按钮布局合理,操作流程清晰

  • 状态反馈: 实时显示系统状态和剩余时间

7. 错误处理与日志系统

7.1 异常处理策略
python 复制代码
try:
    # 执行核心操作
    ret, frame = cv2.VideoCapture(video_path).read()
except Exception as e:
    # 记录错误日志
    logging.error(f"视频读取失败: {str(e)}")
    # 用户友好提示
    QMessageBox.critical(self, "错误", f"视频读取失败: {str(e)}")
7.2 日志配置
python 复制代码
logging.basicConfig(
    filename='app.log',
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

关键技术实现

1. PyQt5与OpenCV集成

python 复制代码
# 视频截图捕获
def capture_screenshot(self, timestamp):
    cap = cv2.VideoCapture(self.video_path)
    cap.set(cv2.CAP_PROP_POS_MSEC, timestamp * 1000)
    ret, frame = cap.read()
    
    if ret:
        # 转换BGR到RGB
        rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        # 创建QImage
        qimg = QImage(rgb_frame.data, 
                     rgb_frame.shape[1], 
                     rgb_frame.shape[0], 
                     QImage.Format_RGB888)
        return qimg
    return None

2. 多媒体播放控制

python 复制代码
class VideoQuizSystem(QMainWindow):
    def __init__(self):
        # 初始化媒体播放器
        self.media_player = QMediaPlayer()
        self.audio_output = QAudioOutput()
        self.media_player.setAudioOutput(self.audio_output)
        self.media_player.setVideoOutput(self.video_widget)
        
        # 连接信号与槽
        self.media_player.stateChanged.connect(self.handle_state_changed)
        self.media_player.positionChanged.connect(self.position_changed)
        self.media_player.durationChanged.connect(self.duration_changed)

3. 时间处理优化

python 复制代码
def format_time(seconds: float) -> str:
    """格式化时间显示(替代字符串截断方式)"""
    seconds = int(seconds)
    hours = seconds // 3600
    minutes = (seconds % 3600) // 60
    secs = seconds % 60
    return f"{hours:02d}:{minutes:02d}:{secs:02d}"

系统部署与使用

1. 环境要求

bash 复制代码
# 安装依赖
pip install PyQt5 opencv-python pandas
# 或使用requirements.txt
pip install -r requirements.txt

2. 运行程序

bash 复制代码
python VideoQuizSystem.py

3. 打包部署

bash 复制代码
# 使用PyInstaller打包
pyinstaller --onefile --windowed --add-data "*.dll;." VideoQuizSystem.py

# 包含额外资源
pyinstaller --onefile --windowed \
    --hidden-import=numpy \
    --hidden-import=opencv-python \
    --add-binary "path/to/opencv;opencv" \
    VideoQuizSystem.py

项目优势与创新点

1. 技术优势

  • 跨平台兼容: 基于Qt框架,支持Windows、macOS、Linux

  • 高性能: 使用OpenCV进行视频处理,效率高

  • 可扩展性: 模块化设计,便于功能扩展

  • 稳定性: 完善的异常处理和日志系统

2. 用户体验

  • 直观操作: 流程化的界面设计

  • 实时反馈: 即时显示操作结果和系统状态

  • 数据安全: 自动保存,防止数据丢失

  • 美观报告: 生成专业美观的HTML报告

3. 应用场景

  • 在线教育: 视频课程中的互动答题

  • 培训考核: 培训视频的知识点测试

  • 竞赛系统: 视频观看过程中的问题发现

  • 研究记录: 学术研究中的观察记录

遇到的问题与解决方案

1. NumPy DLL加载问题

bash 复制代码
# 特殊修复:确保NumPy的DLL可以加载
if getattr(sys, 'frozen', False):
    base_path = sys._MEIPASS
    numpy_core_path = os.path.join(base_path, 'numpy', 'core')
    if sys.platform.startswith('win'):
        os.add_dll_directory(numpy_core_path)

2. 跨平台兼容性

  • 使用Qt的多媒体框架替代特定平台的API

  • 统一路径处理方式

  • 适配不同操作系统的文件系统差异

3. 内存管理

  • 及时释放视频捕获对象

  • 使用适当的图像压缩策略

  • 清理临时文件

未来改进方向

1. 功能扩展

  • 云端同步: 支持数据云端存储和同步

  • 多人协作: 支持多人同时观看和答题

  • AI辅助: 集成AI进行问题分析和答案评估

  • 移动端支持: 开发移动端应用

2. 性能优化

  • 视频流处理: 支持网络视频流

  • 批量处理: 支持批量视频处理

  • 缓存策略: 优化视频加载和缓存

3. 用户体验

  • 主题切换: 支持深色/浅色主题

  • 快捷键支持: 添加常用快捷键

  • 多语言支持: 国际化支持

总结

视频答题竞赛系统是一个功能完整、设计精良的桌面应用程序,它成功地将视频播放、答题记录和报告生成等功能集成在一个统一的平台中。系统采用现代化的技术栈,具备良好的用户体验和可扩展性,适用于多种教育和培训场景。

项目的核心价值在于:

  1. 技术创新: 结合多种技术解决实际问题

  2. 实用性: 解决用户在视频学习中的真实需求

  3. 专业性: 生成高质量的答题报告

  4. 可靠性: 稳定的数据存储和错误处理机制

通过这个项目,不仅实现了功能需求,也展示了PyQt5在构建复杂桌面应用程序方面的强大能力,为类似项目的开发提供了宝贵的参考经验。


项目源码:

python 复制代码
# -*- coding: utf-8 -*-
import ctypes
import sys
import cv2
import sqlite3
import pandas as pd
import os
import re
from datetime import timedelta, datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
                             QHBoxLayout, QPushButton, QLabel, QFileDialog,
                             QDialog, QTextEdit, QGroupBox, QMessageBox,
                             QLineEdit, QFormLayout, QDialogButtonBox, QSlider,
                             QSizePolicy)
from PyQt5.QtGui import QImage, QPixmap, QFont
from PyQt5.QtCore import Qt, pyqtSignal, QUrl, QBuffer
from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent, QAudioOutput
from PyQt5.QtMultimediaWidgets import QVideoWidget

import logging

# 配置日志
logging.basicConfig(
    filename='app.log',
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

# 时间格式化函数(替代 [:7] 截断方式)
def format_time(seconds: float) -> str:
    seconds = int(seconds)
    hours = seconds // 3600
    minutes = (seconds % 3600) // 60
    secs = seconds % 60
    return f"{hours:02d}:{minutes:02d}:{secs:02d}"


# 特殊修复:确保 NumPy 的 DLL 可以加载
if getattr(sys, 'frozen', False):
    # 获取临时解压目录
    base_path = sys._MEIPASS

    # 添加 NumPy 核心路径到 DLL 搜索路径
    numpy_core_path = os.path.join(base_path, 'numpy', 'core')
    if os.path.exists(numpy_core_path):
        # Windows 需要将路径添加到 DLL 搜索路径
        if sys.platform.startswith('win'):
            os.add_dll_directory(numpy_core_path)
            ctypes.windll.kernel32.SetDllDirectoryW(numpy_core_path)

        # 添加 Python 模块搜索路径
        sys.path.append(numpy_core_path)
        logging.info(f"Added numpy.core path: {numpy_core_path}")

    # 强制加载关键模块
    try:
        import numpy.core.multiarray

        logging.info("Successfully imported numpy.core.multiarray")
    except ImportError as e:
        logging.error(f"Failed to import numpy.core.multiarray: {str(e)}")
        # 尝试直接加载 DLL
        try:
            dll_path = os.path.join(numpy_core_path, 'multiarray.dll')
            if os.path.exists(dll_path):
                ctypes.CDLL(dll_path)
                logging.info(f"Loaded multiarray.dll from {dll_path}")
        except Exception as dll_error:
            logging.error(f"Failed to load multiarray.dll: {str(dll_error)}")

    try:
        import numpy.core.umath

        logging.info("Successfully imported numpy.core.umath")
    except ImportError:
        pass

if getattr(sys, 'frozen', False):
    # 添加 OpenCV 路径
    cv2_path = os.path.join(sys._MEIPASS, 'cv2')
    if os.path.exists(cv2_path):
        sys.path.append(cv2_path)
        logging.info(f"Added cv2 path: {cv2_path}")
    else:
        logging.error(f"cv2 path not found: {cv2_path}")
        # 尝试备用路径
        alt_cv2_path = os.path.join(sys._MEIPASS, 'opencv_python.libs')
        if os.path.exists(alt_cv2_path):
            sys.path.append(alt_cv2_path)
            logging.info(f"Using alternative cv2 path: {alt_cv2_path}")

    # 添加 PyQt5 插件路径
    pyqt5_plugin_path = os.path.join(sys._MEIPASS, 'PyQt5', 'Qt', 'plugins')
    if os.path.exists(pyqt5_plugin_path):
        os.environ['QT_PLUGIN_PATH'] = pyqt5_plugin_path
        logging.info(f"Set QT_PLUGIN_PATH: {pyqt5_plugin_path}")
    else:
        # 尝试备用路径
        alt_plugin_path = os.path.join(sys._MEIPASS, 'plugins')
        if os.path.exists(alt_plugin_path):
            os.environ['QT_PLUGIN_PATH'] = alt_plugin_path
            logging.info(f"Set QT_PLUGIN_PATH (alt): {alt_plugin_path}")

    # 添加 DLL 路径
    os.environ['PATH'] = sys._MEIPASS + os.pathsep + os.environ['PATH']
    logging.info(f"Updated PATH with: {sys._MEIPASS}")




class NameInputDialog(QDialog):
    """姓名输入对话框 - 强制用户输入姓名"""

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("请输入姓名")
        self.setFixedSize(400, 150)
        self.setWindowModality(Qt.ApplicationModal)

        layout = QVBoxLayout()
        self.setLayout(layout)

        form_layout = QFormLayout()
        self.name_input = QLineEdit()
        self.name_input.setFont(QFont("Arial", 14))
        self.name_input.setPlaceholderText("请输入您的姓名")
        form_layout.addRow("姓名:", self.name_input)
        layout.addLayout(form_layout)

        button_box = QDialogButtonBox(QDialogButtonBox.Ok)
        button_box.accepted.connect(self.accept)
        layout.addWidget(button_box)

        self.setWindowFlags(self.windowFlags() & ~Qt.WindowCloseButtonHint)

    def accept(self):
        if not self.name_input.text().strip():
            QMessageBox.warning(self, "输入错误", "姓名不能为空!")
            return
        super().accept()

    def get_name(self):
        return self.name_input.text().strip()


class AnswerDialog(QDialog):
    answerSubmitted = pyqtSignal(str, float)
    dialogClosed = pyqtSignal()

    def __init__(self, screenshot, timestamp, video_duration, parent=None):
        super().__init__(parent)
        self.setWindowTitle("提交问题")
        self.setWindowModality(Qt.ApplicationModal)
        self.setFixedSize(parent.width(), parent.height())
        self.timestamp = timestamp

        main_layout = QVBoxLayout()
        self.setLayout(main_layout)

        time_layout = QHBoxLayout()
        elapsed_label = QLabel(f"已播放时间: {format_time(timestamp)}")
        elapsed_label.setFont(QFont("Arial", 14))
        time_layout.addWidget(elapsed_label)

        remaining = max(0, video_duration - timestamp)
        remaining_label = QLabel(f"剩余时间: {format_time(remaining)}")
        remaining_label.setFont(QFont("Arial", 14))
        time_layout.addWidget(remaining_label)
        main_layout.addLayout(time_layout)

        screenshot_label = QLabel()
        screenshot_label.setPixmap(QPixmap.fromImage(screenshot).scaled(
            1600, 800, Qt.KeepAspectRatio, Qt.SmoothTransformation
        ))
        screenshot_label.setAlignment(Qt.AlignCenter)
        screenshot_label.setMinimumSize(1600, 800)
        main_layout.addWidget(screenshot_label, 1)

        question_group = QGroupBox("问题描述")
        q_layout = QVBoxLayout()
        self.question_input = QTextEdit()
        self.question_input.setFont(QFont("Arial", 14))
        self.question_input.setPlaceholderText("请输入问题描述...")
        self.question_input.setFixedHeight(100)
        q_layout.addWidget(self.question_input)
        question_group.setLayout(q_layout)
        main_layout.addWidget(question_group)

        btn_layout = QHBoxLayout()
        submit_btn = QPushButton("提交")
        submit_btn.setFont(QFont("Arial", 14))
        submit_btn.setFixedHeight(50)
        submit_btn.clicked.connect(self.on_submit)
        cancel_btn = QPushButton("取消")
        cancel_btn.setFont(QFont("Arial", 14))
        cancel_btn.setFixedHeight(50)
        cancel_btn.clicked.connect(self.on_cancel)
        btn_layout.addWidget(submit_btn)
        btn_layout.addWidget(cancel_btn)
        main_layout.addLayout(btn_layout)

    def on_submit(self):
        question = self.question_input.toPlainText()
        self.answerSubmitted.emit(question, self.timestamp)
        self.dialogClosed.emit()
        self.accept()

    def on_cancel(self):
        self.dialogClosed.emit()
        self.reject()

    def closeEvent(self, event):
        self.dialogClosed.emit()
        super().closeEvent(event)


class VideoQuizSystem(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("视频答题竞赛系统")

        self.user_name = self.get_user_name()
        if not self.user_name:
            sys.exit(0)

        self.big_font = QFont("Arial", 14)
        QApplication.instance().setFont(self.big_font)

        self.video_path = ""
        self.max_answers = 10
        self.remaining_answers = 10
        self.answers = []
        self.video_duration = 0
        self.report_generated = False
        self.video_completed = False
        self.current_video_active = False
        self.pending_answer_timestamp = None
        self.pending_answer_screenshot = None
        self.current_dialog = None
        self.start_time = None
        self.end_time = None
        self.report_generation_type = ""

        self.init_db()
        self.init_ui()

        self.setMinimumSize(1280, 720)
        screen_geometry = QApplication.primaryScreen().geometry()
        self.resize(int(screen_geometry.width() * 0.8), int(screen_geometry.height() * 0.8))

    def on_slider_released(self):
        """处理滑块释放事件"""
        self.slider_pressed = False
        # 更新位置(确保滑块位置与视频位置同步)
        current_position = self.media_player.position()
        self.progress_slider.setValue(current_position)

    def get_user_name(self):
        dialog = NameInputDialog()
        if dialog.exec_() == QDialog.Accepted:
            return dialog.get_name()
        return None

    def init_db(self):
        self.conn = sqlite3.connect('video_quiz.db')
        self.cursor = self.conn.cursor()
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS answers (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                video_path TEXT,
                timestamp REAL,
                question TEXT,
                screenshot BLOB
            )
        ''')
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS video_stats (
                video_path TEXT PRIMARY KEY,
                remaining_answers INTEGER,
                report_generated INTEGER DEFAULT 0,
                start_time REAL,
                end_time REAL
            )
        ''')
        self.conn.commit()

    def init_ui(self):
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QVBoxLayout(main_widget)

        control_layout = QHBoxLayout()
        self.select_btn = QPushButton("选择视频")
        self.select_btn.setFont(self.big_font)
        self.select_btn.setFixedHeight(50)
        self.select_btn.clicked.connect(self.select_video)
        control_layout.addWidget(self.select_btn)

        self.play_btn = QPushButton("播放")
        self.play_btn.setFont(self.big_font)
        self.play_btn.setFixedHeight(50)
        self.play_btn.clicked.connect(self.play_video)
        self.play_btn.setEnabled(False)
        control_layout.addWidget(self.play_btn)

        self.pause_btn = QPushButton("暂停答题")
        self.pause_btn.setFont(self.big_font)
        self.pause_btn.setFixedHeight(50)
        self.pause_btn.clicked.connect(self.pause_video)
        self.pause_btn.setEnabled(False)
        control_layout.addWidget(self.pause_btn)

        self.finish_btn = QPushButton("交卷")
        self.finish_btn.setFont(self.big_font)
        self.finish_btn.setFixedHeight(50)
        self.finish_btn.clicked.connect(self.finish_exam)
        self.finish_btn.setEnabled(False)
        control_layout.addWidget(self.finish_btn)

        self.report_btn = QPushButton("生成报告")
        self.report_btn.setFont(self.big_font)
        self.report_btn.setFixedHeight(50)
        self.report_btn.clicked.connect(self.generate_report_manually)
        self.report_btn.setEnabled(False)
        control_layout.addWidget(self.report_btn)

        self.count_label = QLabel(f"剩余答题次数: {self.max_answers}/{self.max_answers}")
        self.count_label.setFont(QFont("Arial", 14))
        control_layout.addWidget(self.count_label)

        main_layout.addLayout(control_layout)

        # === 媒体播放器 ===
        self.media_player = QMediaPlayer()

        self.video_widget = QVideoWidget()
        main_layout.addWidget(self.video_widget, 1)
        self.media_player.setVideoOutput(self.video_widget)

        progress_layout = QHBoxLayout()
        self.current_time_label = QLabel("00:00:00")
        progress_layout.addWidget(self.current_time_label)

        self.progress_slider = QSlider(Qt.Horizontal)
        self.progress_slider.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        self.progress_slider.setRange(0, 0)
        self.progress_slider.sliderMoved.connect(self.seek_video)
        # 添加新连接
        self.progress_slider.sliderReleased.connect(self.on_slider_released)
        progress_layout.addWidget(self.progress_slider)

        self.total_time_label = QLabel("00:00:00")
        progress_layout.addWidget(self.total_time_label)
        main_layout.addLayout(progress_layout)

        self.time_label = QLabel("剩余时间: 00:00:00")
        self.time_label.setFont(QFont("Arial", 14))
        self.time_label.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(self.time_label)

        self.status_label = QLabel("请选择视频开始")
        self.status_label.setFont(QFont("Arial", 12))
        self.status_label.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(self.status_label)

        self.name_label = QLabel(f"姓名: {self.user_name}")
        self.name_label.setFont(QFont("Arial", 12))
        self.name_label.setAlignment(Qt.AlignRight)
        main_layout.addWidget(self.name_label)

        self.media_player.stateChanged.connect(self.handle_state_changed)
        self.media_player.positionChanged.connect(self.position_changed)
        self.media_player.durationChanged.connect(self.duration_changed)
        self.media_player.mediaStatusChanged.connect(self.handle_media_status)

        self.slider_pressed = False

    def get_video_stats(self, video_path):
        self.cursor.execute(
            "SELECT remaining_answers, report_generated, start_time, end_time FROM video_stats WHERE video_path=?",
            (video_path,))
        result = self.cursor.fetchone()
        if result:
            return result[0], bool(result[1]), result[2], result[3]
        return self.max_answers, False, None, None

    def update_video_stats(self, video_path, remaining, report_generated=False, start_time=None, end_time=None):
        # 获取当前时间戳
        current_time = datetime.now().timestamp()

        # 如果没有提供开始时间,但需要设置开始时间,则使用当前时间
        if start_time is None and hasattr(self, 'start_time') and self.start_time is not None:
            start_time = self.start_time
        elif start_time is None:
            start_time = current_time

        # 如果没有提供结束时间,但需要设置结束时间,则使用当前时间
        if end_time is None and hasattr(self, 'end_time') and self.end_time is not None:
            end_time = self.end_time
        elif end_time is None and report_generated:
            end_time = current_time

        self.cursor.execute(
            "INSERT OR REPLACE INTO video_stats (video_path, remaining_answers, report_generated, start_time, end_time) VALUES (?, ?, ?, ?, ?)",
            (video_path, remaining, 1 if report_generated else 0, start_time, end_time)
        )
        self.conn.commit()

    def reset_video_state(self):
        self.media_player.stop()
        self.video_path = ""
        self.remaining_answers = self.max_answers
        self.answers = []
        self.video_duration = 0
        self.report_generated = False
        self.video_completed = False
        self.current_video_active = False
        self.pending_answer_timestamp = None
        self.pending_answer_screenshot = None
        self.start_time = None
        self.end_time = None
        self.count_label.setText(f"剩余答题次数: {self.max_answers}/{self.max_answers}")
        self.time_label.setText("剩余时间: 00:00:00")
        self.play_btn.setEnabled(False)
        self.pause_btn.setEnabled(False)
        self.finish_btn.setEnabled(False)
        self.report_btn.setEnabled(False)
        self.status_label.setText("请选择视频开始")
        self.progress_slider.setValue(0)
        self.current_time_label.setText("00:00:00")
        self.total_time_label.setText("00:00:00")

    def select_video(self):
        # 检查媒体播放器状态:如果正在播放或暂停,则不允许选择新视频
        if (self.media_player.state() in (
                QMediaPlayer.PlayingState, QMediaPlayer.PausedState)) and not self.report_generated:
            QMessageBox.warning(self, "操作不允许", "请先完成当前视频再选择新视频。")
            return

        # if self.report_generated:
        #     QMessageBox.warning(self, "提示", "当前视频已完成答题, 只能生成报告, 不能继续答题!")

        # 只检查当前是否有活跃视频且未完成
        if (self.current_video_active and not self.video_completed and self.remaining_answers > 0) \
                and not (self.report_generated and len(self.answers) >= 0):
            QMessageBox.warning(self, "操作不允许", "请先完成当前视频(播放完毕或答题次数用完)再选择新视频。")
            print(self.report_generated)
            print(len(self.answers))
            return

        file_path, _ = QFileDialog.getOpenFileName(
            self, "选择视频文件", "", "视频文件 (*.mp4 *.avi *.mov *.mkv *.flv *.wmv)")
        if not file_path:
            return

        remaining, report_generated, start_time, end_time = self.get_video_stats(file_path)

        # 重置视频状态
        self.reset_video_state()

        self.video_path = file_path
        self.current_video_active = True
        self.remaining_answers = remaining
        self.report_generated = report_generated
        self.video_completed = False

        # 设置开始时间
        if start_time is None:
            self.start_time = datetime.now().timestamp()
        else:
            self.start_time = start_time

        self.media_player.setMedia(QMediaContent(QUrl.fromLocalFile(file_path)))
        # 设置按钮状态
        self.play_btn.setEnabled(not report_generated)  # 如果已生成报告,则禁用播放
        self.pause_btn.setEnabled(False)  # 暂停按钮在未播放时总是禁用
        self.finish_btn.setEnabled(not report_generated)  # 如果已生成报告,则禁交卷
        self.report_btn.setEnabled(True)  # 报告按钮总是可用

        self.update_count_label()
        self.status_label.setText(f"已选择视频: {os.path.basename(file_path)}")

        # 更新数据库中的开始时间
        self.update_video_stats(self.video_path, self.remaining_answers, report_generated, self.start_time, end_time)

    def play_video(self):
        if self.media_player.state() != QMediaPlayer.PlayingState:
            self.media_player.play()
            self.play_btn.setEnabled(False)
            self.pause_btn.setEnabled(True)
            self.finish_btn.setEnabled(True)
            self.report_btn.setEnabled(True)

    def pause_video(self):
        if not self.video_path or self.report_generated or self.remaining_answers <= 0:
            QMessageBox.warning(self, "提示", "当前视频不允许继续答题。")
            return

        if self.media_player.state() == QMediaPlayer.PlayingState:
            self.media_player.pause()
            self.play_btn.setEnabled(True)
            self.pause_btn.setEnabled(False)

        timestamp = self.media_player.position() / 1000.0
        self.pending_answer_timestamp = timestamp

        cap = cv2.VideoCapture(self.video_path)
        cap.set(cv2.CAP_PROP_POS_MSEC, self.media_player.position())
        ret, frame = cap.read()
        cap.release()

        if ret:
            rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            h, w, ch = rgb_frame.shape
            bytes_per_line = ch * w
            qimg = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format_RGB888)
            screenshot = qimg.copy()
            self.pending_answer_screenshot = screenshot
        else:
            pixmap = self.video_widget.grab()
            screenshot = pixmap.toImage() if not pixmap.isNull() else QImage()
            self.pending_answer_screenshot = screenshot

        self.current_dialog = AnswerDialog(screenshot, timestamp, self.video_duration, self)
        self.current_dialog.answerSubmitted.connect(self.save_answer)
        self.current_dialog.dialogClosed.connect(self.on_dialog_closed)
        self.current_dialog.finished.connect(lambda: self.play_video())
        self.current_dialog.show()

    def on_dialog_closed(self):
        self.remaining_answers -= 1
        self.update_video_stats(self.video_path, self.remaining_answers, False, self.start_time, None)
        self.update_count_label()

        if self.remaining_answers <= 0:
            self.pause_btn.setEnabled(False)
            self.status_label.setText("答题次数已用完,正在生成报告...")
            self.report_generation_type = "自动"
            self.generate_report()

    def save_answer(self, question, timestamp):
        try:
            cap = cv2.VideoCapture(self.video_path)
            cap.set(cv2.CAP_PROP_POS_MSEC, timestamp * 1000)
            ret, frame = cap.read()
            cap.release()

            if ret:
                _, buffer = cv2.imencode('.png', frame)
                screenshot_blob = buffer.tobytes()
            else:
                if self.pending_answer_screenshot:
                    buffer = QBuffer()
                    buffer.open(QBuffer.ReadWrite)
                    self.pending_answer_screenshot.save(buffer, "PNG")
                    screenshot_blob = buffer.data()
                    buffer.close()
                else:
                    screenshot_blob = b''

            self.cursor.execute('''
                INSERT INTO answers (video_path, timestamp, question, screenshot)
                VALUES (?, ?, ?, ?)
            ''', (self.video_path, timestamp, question, screenshot_blob))
            self.conn.commit()

            self.answers.append({
                'timestamp': timestamp,
                'question': question,
                'screenshot': screenshot_blob
            })

        except Exception as e:
            QMessageBox.critical(self, "错误", f"保存答案时出错: {str(e)}")

    def generate_report(self, is_manual=False):
        # 确保使用相同的开始和结束时间
        # 如果尚未设置结束时间,则设置为当前时间
        if self.end_time is None:
            self.end_time = datetime.now().timestamp()

        # 设置报告生成类型
        if is_manual:
            self.report_generation_type = "手动"
        else:
            self.report_generation_type = "自动"

        # 更新数据库状态
        self.update_video_stats(self.video_path, self.remaining_answers, True, self.start_time, self.end_time)
        self.report_generated = True

        # 禁用答题相关按钮
        self.pause_btn.setEnabled(False)
        self.finish_btn.setEnabled(False)
        # self.report_btn.setEnabled(False)  # 生成报告后禁用生成报告按钮

        self.cursor.execute("SELECT * FROM answers WHERE video_path=?", (self.video_path,))
        answers = self.cursor.fetchall()

        if not answers and not self.answers:
            self.status_label.setText("没有答题记录可生成报告")
            QMessageBox.information(self, "提示", "没有答题记录可生成报告")
            return

        if not answers:
            answers = self.answers
        else:
            answers = [{
                'timestamp': row[2],
                'question': row[3],
                'screenshot': row[4]
            } for row in answers]

        try:
            # 生成HTML报告
            html_file_path = self.generate_html_report(answers, is_manual)

            # 更新状态
            self.status_label.setText(f"报告已生成: {html_file_path}")
            QMessageBox.information(self, "报告生成成功", f"HTML报告已保存至: {html_file_path}")
            self.play_btn.setEnabled(False)
            self.pause_btn.setEnabled(False)

            # 标记视频已完成
            self.video_completed = True
            self.report_generated = True

        except Exception as e:
            self.status_label.setText(f"生成报告时出错: {str(e)}")
            QMessageBox.critical(self, "错误", f"生成报告时出错: {str(e)}")

    def generate_report_manually(self):
        """手动生成报告 - 为所有视频生成报告"""

        # 确认用户是否生成报告
        reply = QMessageBox.question(
            self, "确认生成报告",
            "确定要生成报告吗?生成报告后当前视频无法继续答题。",
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No
        )

        if reply == QMessageBox.No:
            return

        # 暂停视频播放
        if self.media_player.state() in (QMediaPlayer.PlayingState, QMediaPlayer.PausedState):
            self.media_player.pause()
            # self.play_btn.setEnabled(True)
            # self.pause_btn.setEnabled(False)

        # 禁用所有操作按钮
        self.play_btn.setEnabled(False)
        self.pause_btn.setEnabled(False)
        self.finish_btn.setEnabled(False)
        self.report_btn.setEnabled(False)

        if self.video_path:
            self.cursor.execute("UPDATE video_stats set report_generated = 1 where video_path=?", (self.video_path,))
            self.conn.commit()
            self.report_generated = True

        # 从数据库中获取所有不同的视频路径
        self.cursor.execute("SELECT DISTINCT video_path FROM answers")
        video_paths = [row[0] for row in self.cursor.fetchall()]

        if not video_paths:
            QMessageBox.information(self, "提示", "数据库中没有答题记录,无法生成报告。")
            self.play_btn.setEnabled(True)
            return

        # 创建报告保存目录
        documents_dir = os.path.join(os.path.expanduser('~'), 'Documents')
        save_dir = os.path.join(documents_dir, '视频答题报告', 'HTML报告')
        os.makedirs(save_dir, exist_ok=True)

        # 弹出选择保存路径对话框
        save_dir = QFileDialog.getExistingDirectory(
            self,
            "选择报告保存目录",
            save_dir,
            QFileDialog.ShowDirsOnly
        )

        # 如果用户取消选择,则不保存
        if not save_dir:
            # 重新启用报告按钮
            self.report_btn.setEnabled(True)
            self.status_label.setText("用户取消保存报告")
            return

        report_count = 0
        for video_path in video_paths:
            try:
                # 获取视频的答题记录
                self.cursor.execute("SELECT * FROM answers WHERE video_path=?", (video_path,))
                answers = self.cursor.fetchall()
                if not answers:
                    continue

                # 获取视频信息
                self.cursor.execute(
                    "SELECT start_time, end_time FROM video_stats WHERE video_path=?",
                    (video_path,))
                result = self.cursor.fetchone()
                if result:
                    start_time = result[0]
                    end_time = result[1]
                else:
                    start_time = None
                    end_time = None

                # 获取视频时长
                cap = cv2.VideoCapture(video_path)
                if cap.isOpened():
                    fps = cap.get(cv2.CAP_PROP_FPS)
                    frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
                    video_duration = frame_count / fps if fps > 0 else 0
                    cap.release()
                else:
                    video_duration = 0

                # 生成报告
                video_name = os.path.splitext(os.path.basename(video_path))[0]
                video_name = re.sub(r'[<>:"/\\|?*]', '', video_name)
                timestamp_str = pd.Timestamp.now().strftime("%Y%m%d_%H%M%S")

                # 创建图片目录
                images_dir = os.path.join(save_dir, f'images_{video_name}_{self.user_name}_{timestamp_str}_手动')
                os.makedirs(images_dir, exist_ok=True)

                # HTML文件路径
                file_path = os.path.join(save_dir, f"{video_name}_{self.user_name}_{timestamp_str}_手动.html")

                # 计算答题用时
                if start_time and end_time:
                    start_dt = datetime.fromtimestamp(start_time)
                    end_dt = datetime.fromtimestamp(end_time)
                    duration = end_dt - start_dt
                    total_seconds = duration.total_seconds()
                    hours = int(total_seconds // 3600)
                    minutes = int((total_seconds % 3600) // 60)
                    seconds = total_seconds % 60
                    milliseconds = int((seconds - int(seconds)) * 1000)
                    seconds = int(seconds)
                    duration_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}"
                else:
                    start_dt = end_dt = None
                    duration_str = "未知"

                start_time_str = start_dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] if start_dt else "未知"
                end_time_str = end_dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] if end_dt else "未知"

                # 创建HTML内容
                html_content = f"""
                <!DOCTYPE html>
                <html lang="zh-CN">
                <head>
                    <meta charset="UTF-8">
                    <meta name="viewport" content="width=device-width, initial-scale=1.0">
                    <title>视频答题报告 - {video_name}</title>
                    <style>
                        body {{
                            font-family: 'Arial', sans-serif;
                            line-height: 1.6;
                            color: #333;
                            max-width: 1200px;
                            margin: 0 auto;
                            padding: 20px;
                            background-color: #f9f9f9;
                        }}
                        .header {{
                            text-align: center;
                            padding: 20px 0;
                            background-color: #2c3e50;
                            color: white;
                            border-radius: 5px;
                            margin-bottom: 30px;
                        }}
                        .header h1 {{
                            margin: 0;
                            font-size: 28px;
                        }}
                        .user-info {{
                            font-size: 18px;
                            margin-top: 10px;
                        }}
                        .answer-card {{
                            background-color: white;
                            border-radius: 8px;
                            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
                            padding: 20px;
                            margin-bottom: 30px;
                            transition: transform 0.3s ease;
                        }}
                        .answer-card:hover {{
                            transform: translateY(-5px);
                            box-shadow: 0 5px 15px rgba(0,0,0,0.1);
                        }}
                        .timestamp {{
                            font-size: 18px;
                            font-weight: bold;
                            color: #2c3e50;
                            margin-bottom: 10px;
                        }}
                        .question {{
                            font-size: 16px;
                            margin-bottom: 15px;
                            color: #34495e;
                        }}
                        .screenshot {{
                            text-align: center;
                            margin: 20px 0;
                        }}
                        .screenshot img {{
                            max-width: 100%;
                            border-radius: 5px;
                            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
                        }}
                        .remaining-time {{
                            font-style: italic;
                            color: #7f8c8d;
                            margin-top: 10px;
                        }}
                        .section-title {{
                            font-size: 24px;
                            color: #2c3e50;
                            border-bottom: 2px solid #3498db;
                            padding-bottom: 10px;
                            margin: 40px 0 20px;
                        }}
                        .summary {{
                            background-color: #e8f4f8;
                            border-left: 4px solid #3498db;
                            padding: 15px;
                            margin: 20px 0;
                            border-radius: 0 5px 5px 0;
                        }}
                        .summary p {{
                            margin: 5px 0;
                        }}
                        .time-info {{
                            display: grid;
                            grid-template-columns: repeat(2, 1fr);
                            gap: 10px;
                            margin-bottom: 15px;
                        }}
                        .time-item {{
                            background-color: #f0f7ff;
                            padding: 10px;
                            border-radius: 5px;
                            border-left: 3px solid #3498db;
                        }}
                        .time-label {{
                            font-weight: bold;
                            margin-bottom: 5px;
                        }}
                    </style>
                </head>
                <body>
                    <div class="header">
                        <h1>视频答题报告</h1>
                        <div class="user-info">
                            <p>姓名: {self.user_name} | 视频: {video_name}</p>
                        </div>
                    </div>

                    <div class="summary">
                        <p><strong>报告摘要</strong></p>
                        <p>视频时长: {str(timedelta(seconds=int(video_duration)))}</p>
                        <p>答题数量: {len(answers)}</p>
                        <p>生成时间: {pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]}</p>
                        <p>报告生成方式: 手动</p>

                        <div class="time-info">
                            <div class="time-item">
                                <div class="time-label">开始时间</div>
                                <div>{start_time_str}</div>
                            </div>
                            <div class="time-item">
                                <div class="time-label">结束时间</div>
                                <div>{end_time_str}</div>
                            </div>
                            <div class="time-item">
                                <div class="time-label">答题用时</div>
                                <div>{duration_str}</div>
                            </div>
                            <div class="time-item">
                                <div class="time-label">生成方式</div>
                                <div>手动</div>
                            </div>
                        </div>
                    </div>

                    <h2 class="section-title">答题记录</h2>
                """

                # 添加每个答案的卡片
                for i, answer in enumerate(answers):
                    # 保存截图到文件
                    img_path = os.path.join(images_dir, f"screenshot_{i + 1}.png")
                    with open(img_path, 'wb') as img_file:
                        img_file.write(answer[4])  # 截图在数据库行的第5列

                    # 使用相对路径引用图片
                    img_relative_path = os.path.relpath(img_path, os.path.dirname(file_path))
                    html_content += f"""
                    <div class="answer-card">
                        <div class="timestamp">记录 #{i + 1} | 时间点: {str(timedelta(seconds=answer[2]))}</div>
                        <div class="remaining-time">剩余时间: {str(timedelta(seconds=int(video_duration - answer[2])))}</div>

                        <div class="screenshot">
                            <img src="{img_relative_path}" alt="视频截图">
                        </div>

                        <div class="question">
                            <strong>问题描述:</strong>
                            <p>{answer[3]}</p>
                        </div>
                    </div>
                    """
                # 添加页脚
                html_content += """
                    <footer style="text-align: center; margin-top: 40px; padding: 20px; color: #7f8c8d;">
                        <p>视频答题系统自动生成报告</p>
                        <p>生成时间: """ + pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + """</p>
                    </footer>
                </body>
                </html>
                """

                # 保存HTML文件
                with open(file_path, 'w', encoding='utf-8') as f:
                    f.write(html_content)

                report_count += 1
            except Exception as e:
                print(f"生成报告时出错 (视频: {video_path}): {str(e)}")

        if report_count > 0:
            self.status_label.setText(f"已生成{report_count}份报告")
            QMessageBox.information(self, "报告生成成功", f"已生成{report_count}份报告,保存至: {save_dir}")
        else:
            self.status_label.setText("没有生成任何报告")
            QMessageBox.warning(self, "报告生成失败", "没有找到可生成报告的答题记录")

        # 重新启用报告按钮
        self.report_btn.setEnabled(True)

    def generate_html_report(self, answers, is_manual=False):
        """生成美观的HTML报告"""
        # 安全生成文件名
        video_name = os.path.splitext(os.path.basename(self.video_path))[0]
        video_name = re.sub(r'[<>:"/\\|?*]', '', video_name)
        timestamp_str = pd.Timestamp.now().strftime("%Y%m%d_%H%M%S")

        # 自动生成报告使用默认路径,手动生成报告让用户选择路径
        if is_manual:
            # 让用户选择报告保存目录
            save_dir = QFileDialog.getExistingDirectory(
                self,
                "选择报告保存目录",
                os.path.join(os.path.expanduser('~'), 'Documents', '视频答题报告', 'HTML报告')
            )

            if not save_dir:
                return None  # 用户取消了选择
        else:
            # 自动生成报告使用默认路径
            documents_dir = os.path.join(os.path.expanduser('~'), 'Documents')
            save_dir = os.path.join(documents_dir, '视频答题报告', 'HTML报告')
            os.makedirs(save_dir, exist_ok=True)

        # 创建保存目录
        os.makedirs(save_dir, exist_ok=True)

        # 创建图片目录 - 添加时间戳避免覆盖
        images_dir = os.path.join(save_dir, f'images_{self.user_name}_{timestamp_str}_自动')
        os.makedirs(images_dir, exist_ok=True)

        # HTML文件路径
        file_path = os.path.join(save_dir, f"{video_name}_{self.user_name}_{timestamp_str}_自动.html")

        # 计算答题用时(精确到毫秒)
        if self.start_time and self.end_time:
            # 转换为datetime对象(精确到微秒)
            start_dt = datetime.fromtimestamp(self.start_time)
            end_dt = datetime.fromtimestamp(self.end_time)

            # 计算时间差(精确到微秒)
            duration = end_dt - start_dt
            total_seconds = duration.total_seconds()

            # 计算各部分时间(精确到毫秒)
            hours = int(total_seconds // 3600)
            minutes = int((total_seconds % 3600) // 60)
            seconds = total_seconds % 60
            milliseconds = int((seconds - int(seconds)) * 1000)
            seconds = int(seconds)

            # 格式化时间差(小时:分钟:秒.毫秒)
            duration_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}"
        else:
            start_dt = end_dt = None
            duration_str = "未知"

        # 格式化时间(精确到毫秒)
        start_time_str = start_dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] if start_dt else "未知"
        end_time_str = end_dt.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] if end_dt else "未知"

        # 创建HTML内容
        html_content = f"""
        <!DOCTYPE html>
        <html lang="zh-CN">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>视频答题报告 - {video_name}</title>
            <style>
                body {{
                    font-family: 'Arial', sans-serif;
                    line-height: 1.6;
                    color: #333;
                    max-width: 1200px;
                    margin: 0 auto;
                    padding: 20px;
                    background-color: #f9f9f9;
                }}
                .header {{
                    text-align: center;
                    padding: 20px 0;
                    background-color: #2c3e50;
                    color: white;
                    border-radius: 5px;
                    margin-bottom: 30px;
                }}
                .header h1 {{
                    margin: 0;
                    font-size: 28px;
                }}
                .user-info {{
                    font-size: 18px;
                    margin-top: 10px;
                }}
                .answer-card {{
                    background-color: white;
                    border-radius: 8px;
                    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
                    padding: 20px;
                    margin-bottom: 30px;
                    transition: transform 0.3s ease;
                }}
                .answer-card:hover {{
                    transform: translateY(-5px);
                    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
                }}
                .timestamp {{
                    font-size: 18px;
                    font-weight: bold;
                    color: #2c3e50;
                    margin-bottom: 10px;
                }}
                .question {{
                    font-size: 16px;
                    margin-bottom: 15px;
                    color: #34495e;
                }}
                .screenshot {{
                    text-align: center;
                    margin: 20px 0;
                }}
                .screenshot img {{
                    max-width: 100%;
                    border-radius: 5px;
                    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
                }}
                .remaining-time {{
                    font-style: italic;
                    color: #7f8c8d;
                    margin-top: 10px;
                }}
                .section-title {{
                    font-size: 24px;
                    color: #2c3e50;
                    border-bottom: 2px solid #3498db;
                    padding-bottom: 10px;
                    margin: 40px 0 20px;
                }}
                .summary {{
                    background-color: #e8f4f8;
                    border-left: 4px solid #3498db;
                    padding: 15px;
                    margin: 20px 0;
                    border-radius: 0 5px 5px 0;
                }}
                .summary p {{
                    margin: 5px 0;
                }}
                .time-info {{
                    display: grid;
                    grid-template-columns: repeat(2, 1fr);
                    gap: 10px;
                    margin-bottom: 15px;
                }}
                .time-item {{
                    background-color: #f0f7ff;
                    padding: 10px;
                    border-radius: 5px;
                    border-left: 3px solid #3498db;
                }}
                .time-label {{
                    font-weight: bold;
                    margin-bottom: 5px;
                }}
            </style>
        </head>
        <body>
            <div class="header">
                <h1>视频答题报告</h1>
                <div class="user-info">
                    <p>姓名: {self.user_name} | 视频: {video_name}</p>
                </div>
            </div>

            <div class="summary">
                <p><strong>报告摘要</strong></p>
                <p>视频时长: {str(timedelta(seconds=int(self.video_duration)))}</p>
                <p>答题数量: {len(answers)}</p>
                <p>生成时间: {pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]}</p>
                <p>报告生成方式: {self.report_generation_type}</p>

                <div class="time-info">
                    <div class="time-item">
                        <div class="time-label">开始时间</div>
                        <div>{start_time_str}</div>
                    </div>
                    <div class="time-item">
                        <div class="time-label">结束时间</div>
                        <div>{end_time_str}</div>
                    </div>
                    <div class="time-item">
                        <div class="time-label">答题用时</div>
                        <div>{duration_str}</div>
                    </div>
                    <div class="time-item">
                        <div class="time-label">生成方式</div>
                        <div>{self.report_generation_type}</div>
                    </div>
                </div>
            </div>

            <h2 class="section-title">答题记录</h2>
        """

        # 添加每个答案的卡片
        for i, answer in enumerate(answers):
            # 保存截图到文件
            img_path = os.path.join(images_dir, f"screenshot_{i + 1}.png")
            with open(img_path, 'wb') as img_file:
                img_file.write(answer['screenshot'])

            # 使用相对路径引用图片
            img_relative_path = os.path.relpath(img_path, os.path.dirname(file_path))
            html_content += f"""
            <div class="answer-card">
                <div class="timestamp">记录 #{i + 1} | 时间点: {str(timedelta(seconds=answer['timestamp']))}</div>
                <div class="remaining-time">剩余时间: {str(timedelta(seconds=int(self.video_duration - answer['timestamp'])))}</div>

                <div class="screenshot">
                    <img src="{img_relative_path}" alt="视频截图">
                </div>

                <div class="question">
                    <strong>问题描述:</strong>
                    <p>{answer['question']}</p>
                </div>
            </div>
            """

        # 添加页脚
        html_content += """
            <footer style="text-align: center; margin-top: 40px; padding: 20px; color: #7f8c8d;">
                <p>视频答题系统自动生成报告</p>
                <p>生成时间: """ + pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + """</p>
            </footer>
        </body>
        </html>
        """

        # 保存HTML文件
        with open(file_path, 'w', encoding='utf-8') as f:
            f.write(html_content)

        return file_path

    def update_count_label(self):
        self.count_label.setText(f"剩余答题次数: {self.remaining_answers}/{self.max_answers}")

    def handle_state_changed(self, state):
        if state == QMediaPlayer.StoppedState:
            if not self.report_generated and self.video_completed:
                self.status_label.setText("视频播放完成,正在生成报告...")
                self.report_generation_type = "自动"
                self.generate_report()

    def handle_media_status(self, status):
        if status == QMediaPlayer.EndOfMedia:
            self.video_completed = True
            if not self.report_generated:
                self.status_label.setText("视频播放完成,正在生成报告...")
                self.report_generation_type = "自动"
                self.generate_report()

    # === 核心:修复时间显示 ===
    def position_changed(self, position):
        if self.video_duration > 0:
            current_time = position / 1000.0
            remaining = max(0, self.video_duration - current_time)
            self.time_label.setText(f"剩余时间: {format_time(remaining)}")
            self.current_time_label.setText(format_time(current_time))
            if not self.slider_pressed:
                self.progress_slider.setValue(position)

    def duration_changed(self, duration):
        if duration > 0:
            self.video_duration = duration / 1000.0
            self.time_label.setText(f"剩余时间: {format_time(self.video_duration)}")
            self.progress_slider.setRange(0, duration)
            self.total_time_label.setText(format_time(self.video_duration))

    def seek_video(self, position):
        self.media_player.setPosition(position)
        self.slider_pressed = True

    # def mousePressEvent(self, event):
    #     if self.progress_slider.underMouse():
    #         self.slider_pressed = True
    #     super().mousePressEvent(event)

    # def mouseReleaseEvent(self, event):
    #     self.slider_pressed = False
    #     super().mouseReleaseEvent(event)

    def finish_exam(self):
        """处理交卷按钮点击事件"""
        if self.report_generated:
            QMessageBox.warning(self, "提示", "当前视频已交卷不允许再次交卷。")
            return

        # 确认用户是否要提前交卷
        reply = QMessageBox.question(
            self, "确认交卷",
            "确定要提前交卷吗?交卷后将无法继续答题。",
            QMessageBox.Yes | QMessageBox.No,
            QMessageBox.No
        )

        if reply == QMessageBox.No:
            return

        # 停止视频播放
        if self.media_player.state() in (QMediaPlayer.PlayingState, QMediaPlayer.PausedState):
            self.media_player.stop()

        # 如果有打开的答题对话框,关闭它
        if self.current_dialog and self.current_dialog.isVisible():
            self.current_dialog.close()

        # 设置结束时间
        self.end_time = datetime.now().timestamp()

        # 生成报告
        self.status_label.setText("正在生成报告...")
        self.report_generation_type = "自动"
        self.generate_report()

        # 禁用所有操作按钮
        self.play_btn.setEnabled(False)
        self.pause_btn.setEnabled(False)
        self.finish_btn.setEnabled(False)

    def closeEvent(self, event):
        # 不再自动生成报告
        self.media_player.stop()
        if self.conn:
            self.conn.close()
        # 清理临时文件
        for i in range(50):
            temp_path = f"temp_{i}.png"
            if os.path.exists(temp_path):
                try:
                    os.remove(temp_path)
                except:
                    pass
        event.accept()



if __name__ == "__main__":
    app = QApplication(sys.argv)
    font = QFont()
    font.setPointSize(14)
    app.setFont(font)
    window = VideoQuizSystem()
    window.show()
    sys.exit(app.exec_())
相关推荐
岱宗夫up4 小时前
Python 数据分析入门
开发语言·python·数据分析
码界筑梦坊4 小时前
325-基于Python的校园卡消费行为数据可视化分析系统
开发语言·python·信息可视化·django·毕业设计
多恩Stone5 小时前
【RoPE】Flux 中的 Image Tokenization
开发语言·人工智能·python
李日灐5 小时前
C++进阶必备:红黑树从 0 到 1: 手撕底层,带你搞懂平衡二叉树的平衡逻辑与黑高检验
开发语言·数据结构·c++·后端·面试·红黑树·自平衡二叉搜索树
Risehuxyc5 小时前
备份三个PHP程序
android·开发语言·php
byte轻骑兵5 小时前
从HCI报文透视LE Audio重连流程(3):音频流建立、同步与终止
音视频·蓝牙·le audio·cig/cis·广播音频
lly2024065 小时前
PHP Error: 常见错误及其解决方法
开发语言
网安墨雨5 小时前
Python自动化一------pytes与allure结合生成测试报告
开发语言·自动化测试·软件测试·python·职场和发展·自动化
毕设源码李师姐5 小时前
计算机毕设 java 基于 java 的图书馆借阅系统 智能图书馆借阅综合管理平台 基于 Java 的图书借阅与信息管理系统
java·开发语言·课程设计