使用PyQt5设计一个题库答题交互界面

使用PyQt5设计一个题库答题交互界面

使用PyQt5设计一个题库答题交互界面,界面功能包括:题目展示,上下题切换,题目标记,题目删除(逻辑删除),对答案,错题回顾等功能。

import os.path
import sys
import pandas as pd
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QRadioButton, QCheckBox, \
    QPushButton, QMessageBox, QSizePolicy, QSpacerItem
from PyQt5.QtGui import QPalette, QColor, QFont

NUMBER_LETTER_DICT = {0: 'A', 1: 'B', 2: 'C', 3: 'D', 4: 'E', 5: 'F', 6: 'G', 7: 'H', 8: 'I', 9: 'J'}
LETTER_NUMBER_DICT = {'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, 'H': 7, 'I': 8, 'J': 9}


class Exam(QWidget):
    def __init__(self, question_file_path, flag_file_path):
        super().__init__()
        self.question_file_path = question_file_path
        self.flag_file_path = flag_file_path
        self.df_questions = None
        self.df_flags = None
        self.cursor = 0
        self.df_question_index = -1
        self.df_review_cursor_list = []  # 复习回顾(包含错题和标记)的试题索引
        self.row = None

        self.question_type_option_list = []
        self.step_type = 0  # 0-选中题型,1-个性化选择,2-答题环节

        self.font_question = QFont()
        self.font_question.setPointSize(12)
        self.font_question.setFamily('Arial')
        # self.font_question.setBold(True)

        self.font_options = QFont()
        self.font_options.setPointSize(12)
        self.font_options.setFamily('Arial')

        self.info_label = QLabel()
        self.info_label.setFixedHeight(30)
        self.info_label.setAlignment(Qt.AlignHCenter | Qt.AlignTop)

        self.question_label = QLabel()
        self.question_label.setWordWrap(True)
        self.question_label.setFont(self.font_question)

        self.options_layout = QVBoxLayout()
        self.options_layout.setContentsMargins(0, 10, 0, 0)
        self.options_layout.setSpacing(10)

        self.buttons_layout = QHBoxLayout()
        self.buttons_layout.setContentsMargins(0, 30, 0, 0)
        self.buttons_layout.setAlignment(Qt.AlignHCenter)

        self.enter_button = QPushButton('确定')
        self.prev_button = QPushButton('上一题')
        self.check_button = QPushButton('对答案')
        self.mark_button = QPushButton('标记')
        self.delete_button = QPushButton('删除')
        self.review_button = QPushButton('回顾')
        self.next_button = QPushButton('下一题')
        self.enter_button.setMaximumWidth(150)
        self.enter_button.setMinimumHeight(40)
        self.prev_button.setMinimumHeight(40)
        self.check_button.setMinimumHeight(40)
        self.mark_button.setMinimumHeight(40)
        self.delete_button.setMinimumHeight(40)
        self.review_button.setMinimumHeight(40)
        self.next_button.setMinimumHeight(40)
        self.initial_buttons()

        main_layout = QVBoxLayout()
        main_layout.setContentsMargins(30, 10, 20, 10)
        main_layout.setAlignment(Qt.AlignTop)
        main_layout.addWidget(self.info_label)
        main_layout.addWidget(self.question_label)
        main_layout.addLayout(self.options_layout)
        main_layout.addSpacerItem(QSpacerItem(20, 40, QSizePolicy.Expanding, QSizePolicy.Expanding))
        main_layout.addLayout(self.buttons_layout)

        self.setLayout(main_layout)
        self.setFixedWidth(680)
        self.setMinimumHeight(360)
        self.setWindowTitle('百日闯关题库_智能模型开发与应用')
        self.show()

    def closeEvent(self, event):
        reply = QMessageBox.question(self, '消息',
                                     "你确定要关闭窗口吗?",
                                     QMessageBox.Yes | QMessageBox.No,
                                     QMessageBox.No)

        if reply == QMessageBox.Yes:
            if self.step_type == 2:
                # 保存设置
                self.save_cursor()
                self.save_flag_file()
            event.accept()
        else:
            event.ignore()

    def start_exam(self):
        self.select_question_type()

    def initial_options(self, question_type, options_list):
        # 先销毁历史组件
        self.destroy_options()
        # 问题选项
        for option in options_list:
            # if question_type == '单选题' or question_type == '判断题':
            #     radio_button = QRadioButton(option)
            #     radio_button.setFont(self.font_options)
            #     self.options_layout.addWidget(radio_button)
            # else:
            #     check_box = QCheckBox(option)
            #     check_box.setFont(self.font_options)
            #     self.options_layout.addWidget(check_box)
            self.options_layout.addLayout(self.create_single_option(question_type, option))

    def create_single_option(self, question_type, option):
        hbox = QHBoxLayout()
        hbox.setAlignment(Qt.AlignLeft | Qt.AlignTop)

        vbox = QVBoxLayout()
        button = QRadioButton() if question_type == '单选题' or question_type == '判断题' else QCheckBox()
        vbox.setContentsMargins(0, 3.2, 0, 0)  # 设置选择框距上方距离
        vbox.addWidget(button, alignment=Qt.AlignTop)
        hbox.addLayout(vbox)


        label = QLabel(option)
        label.setBuddy(button)
        label.setFixedWidth(600)
        label.setWordWrap(True)
        label.setFont(self.font_options)
        hbox.addWidget(label)

        return hbox

    def destroy_options(self):
        for i in range(self.options_layout.count()):
            # item = self.options_layout.takeAt(0)
            # item_temp = item.takeAt(0)
            # widget = item_temp.widget()
            item = self.options_layout.itemAt(i)
            # item_button = item.itemAt(0)
            # button = item_button.widget()

            item_vbox = item.itemAt(0)
            vbox = item_vbox.layout()
            item_button = item_vbox.itemAt(0)
            button = item_button.widget()

            item_label = item.itemAt(1)
            label = item_label.widget()
            hbox = item.layout()
            if button:
                button.deleteLater()
            if vbox:
                vbox.deleteLater()
            if label:
                label.deleteLater()
            if hbox:
                hbox.deleteLater()

    def get_user_answer(self):
        user_answer = []
        for i in range(self.options_layout.count()):
            item = self.options_layout.itemAt(i)
            item_temp = item.itemAt(0)
            item_temp2 = item_temp.itemAt(0)
            widget = item_temp2.widget()
            if isinstance(widget, QRadioButton) and widget.isChecked():
                user_answer.append(i)
            elif isinstance(widget, QCheckBox) and widget.isChecked():
                user_answer.append(i)
        return user_answer

    def initial_buttons(self):
        self.enter_button.clicked.connect(self.enter)
        self.prev_button.clicked.connect(self.prev_question)
        self.check_button.clicked.connect(self.check_answer)
        self.mark_button.clicked.connect(self.mark_question)
        self.delete_button.clicked.connect(self.delete_question)
        self.review_button.clicked.connect(self.review_question)
        self.next_button.clicked.connect(self.next_question)

        self.buttons_layout.addWidget(self.enter_button)
        self.buttons_layout.addWidget(self.prev_button)
        self.buttons_layout.addWidget(self.review_button)
        self.buttons_layout.addWidget(self.delete_button)
        self.buttons_layout.addWidget(self.mark_button)
        self.buttons_layout.addWidget(self.check_button)
        self.buttons_layout.addWidget(self.next_button)

        self.prev_button.setVisible(False)
        self.check_button.setVisible(False)
        self.mark_button.setVisible(False)
        self.delete_button.setVisible(False)
        self.review_button.setVisible(False)
        self.next_button.setVisible(False)

    def enter(self):
        user_answer_list = self.get_user_answer()
        if len(user_answer_list) == 0:
            QMessageBox.information(self, '提醒', '请选择选项')
            return
        user_answer = user_answer_list[0]
        if self.step_type == 0:
            # 选择题型后按确定
            if user_answer > 0:
                self.df_questions = self.df_questions[self.df_questions['type'] == self.question_type_option_list[user_answer]]
            self.step_type = 1
            self.select_question_customized()
        elif self.step_type == 1:
            # 个性化选择
            df_cp = self.df_questions.copy()
            if user_answer == 1:
                start_index = self.cursor + 1
                df_cp = df_cp.loc[start_index:, :]
            elif user_answer == 2:
                id_set = self.get_flag_ids()
                df_cp = df_cp[df_cp['id'].isin(id_set)]
            elif user_answer == 3:
                id_set = self.get_wrong_ids()
                df_cp = df_cp[df_cp['id'].isin(id_set)]
            elif user_answer == 4:
                id_set = self.get_wrong_ratio_high_ids(0.5)
                df_cp = df_cp[df_cp['id'].isin(id_set)]

            if df_cp.empty:
                QMessageBox.information(self, '提醒', '根据个性化选择未筛选出题库,请重新选择')
                return
            else:
                self.df_questions = df_cp

            self.step_type = 2
            self.df_questions = self.df_questions.reset_index()  # 将原索引作为新列保存,并重置索引
            self.df_questions = self.df_questions.rename(columns={'index': 'cursor'})

            self.enter_button.setVisible(False)
            self.prev_button.setVisible(True)
            self.check_button.setVisible(True)
            self.mark_button.setVisible(True)
            self.delete_button.setVisible(True)
            self.review_button.setVisible(True)
            self.next_button.setVisible(True)
            # 开始答题
            self.next_question()

    def read_question_file(self):
        self.df_questions = pd.read_excel(self.question_file_path, encoding='utf-8')

    def read_flag_file(self):
        self.df_flags = self.df_questions[['id']]
        if os.path.exists(self.flag_file_path):
            df_old = pd.read_excel(self.flag_file_path, encoding='utf-8')
            self.df_flags = pd.merge(self.df_flags, df_old, on='id', how='left')
            self.df_flags.fillna(0)
            self.df_flags['wrong_ratio'] = self.df_flags.apply(self.calc_wrong_ratio, axis=1)
            df_cursor = self.df_flags[self.df_flags['cursor'] == 1]
            index_list = df_cursor.index.tolist()
            self.cursor = index_list[0]
        else:
            self.df_flags.loc[:, 'cursor'] = 0
            self.df_flags.loc[:, 'times'] = 0
            self.df_flags.loc[:, 'times_wrong'] = 0
            self.df_flags.loc[:, 'flag'] = 0
            self.df_flags.loc[:, 'delete'] = 0
            self.df_flags.loc[:, 'wrong_ratio'] = 0

    @staticmethod
    def calc_wrong_ratio(row):
        if row['times'] == 0:
            return 0
        return row['times_wrong'] / row['times']

    def filter_delete(self):
        df_delete = self.df_flags[self.df_flags['delete'] == 1]
        delete_id_set = set(df_delete['id'])
        self.df_questions = self.df_questions[~self.df_questions['id'].isin(delete_id_set)]

    def select_question_type(self):
        self.info_label.setText('[1/2]题库信息配置')
        self.question_label.setText('请选择题型:')
        question_type_se = self.df_questions['type'].value_counts()
        # 题型列表
        options_list = [f'{k}: 共{str(v)}道' for k, v in question_type_se.iteritems()]
        options_list.insert(0, '全部题型')
        self.initial_options('单选题', options_list)
        self.question_type_option_list.append('全部题型')
        self.question_type_option_list.extend(question_type_se.index.tolist())

    def select_question_customized(self):
        self.info_label.setText('[2/2]题库信息配置')
        self.question_label.setText('请选择个性化选项:')
        options_list = ['默认, 从头开始', '接着从上次位置开始', '只看历史标记题', '只看答错次数大于0的题', '只看答错率大于50%的题']
        self.initial_options('单选题', options_list)

    def get_flag_ids(self):
        df_temp = self.df_flags[self.df_flags['flag'] == 1]
        return set(df_temp['id'])

    def get_wrong_ids(self):
        df_temp = self.df_flags[self.df_flags['times_wrong'] > 0]
        return set(df_temp['id'])

    def get_wrong_ratio_high_ids(self, wrong_ratio):
        df_temp = self.df_flags[self.df_flags['wrong_ratio'] >= wrong_ratio]
        return set(df_temp['id'])

    def prev_question(self):
        self.df_question_index -= 1
        if self.df_question_index < 0:
            QMessageBox.information(self, '提醒', '这是第一道题')
            self.df_question_index += 1
            return
        self.show_question()

    def next_question(self):
        self.df_question_index += 1
        if self.df_question_index >= len(self.df_questions):
            QMessageBox.information(self, '提醒', '这是最后一道题')
            self.df_question_index -= 1
            return
        self.show_question()

    def show_question(self):
        self.row = self.df_questions.iloc[self.df_question_index, :]
        self.cursor = self.row['cursor']
        info = '第%d题 - 总%d题' % (self.df_question_index + 1, self.df_questions.shape[0])
        self.info_label.setText(info)
        self.question_label.setText('[%s] %s' % (self.row['type'], self.row['question']))
        options = eval(self.row['options'])
        options_list = [f'{k} {v}' for k, v in options.items()]
        self.initial_options(self.row['type'], options_list)
        self.set_mark_button_bg()
        self.set_delete_button_bg()

    def mark_question(self):
        self.df_review_cursor_list.append(self.cursor)
        self.df_flags.loc[self.cursor, 'flag'] = 1 if self.df_flags.loc[self.cursor, 'flag'] == 0 else 0
        self.set_mark_button_bg()

    def set_mark_button_bg(self):
        if self.df_flags.loc[self.cursor, 'flag'] == 1:
            self.mark_button.setStyleSheet('background-color: #FA6D1D;')
        else:
            self.mark_button.setStyleSheet('')

    def delete_question(self):
        self.df_flags.loc[self.cursor, 'delete'] = 1 if self.df_flags.loc[self.cursor, 'delete'] == 0 else 0
        self.set_delete_button_bg()

    def review_question(self):
        if not self.df_review_cursor_list:
            QMessageBox.information(self, '提醒', '没有错题或标记题')
            return
        self.df_questions = self.df_questions[self.df_questions['cursor'].isin(self.df_review_cursor_list)]
        self.df_question_index = -1
        self.next_question()

    def set_delete_button_bg(self):
        if self.df_flags.loc[self.cursor, 'delete'] == 1:
            self.delete_button.setStyleSheet('background-color: #DA1F18;')
        else:
            self.delete_button.setStyleSheet('')

    def check_answer(self):
        self.df_flags.loc[self.cursor, 'times'] = self.df_flags.loc[self.cursor, 'times'] + 1
        answer = self.get_answer()
        # 正确选项对应索引
        normal_answer = [LETTER_NUMBER_DICT[i] for i in self.row['answer']]
        if answer != self.row['answer']:
            # 选错了
            self.df_review_cursor_list.append(self.cursor)
            self.df_flags.loc[self.cursor, 'times_wrong'] = self.df_flags.loc[self.cursor, 'times_wrong'] + 1
            for i in normal_answer:
                item = self.options_layout.itemAt(i)
                item_label = item.itemAt(1)
                widget = item_label.widget()
                palette = widget.palette()
                palette.setColor(QPalette.Background, QColor('lightcoral'))
                widget.setAutoFillBackground(True)  # 确保背景色生效
                widget.setPalette(palette)
        else:
            # 选对了
            for i in normal_answer:
                item = self.options_layout.itemAt(i)
                item_label = item.itemAt(1)
                widget = item_label.widget()
                palette = widget.palette()
                palette.setColor(QPalette.Background, QColor('lightgreen'))
                widget.setAutoFillBackground(True)  # 确保背景色生效
                widget.setPalette(palette)

    def get_answer(self):
        user_answer = self.get_user_answer()
        answer = [NUMBER_LETTER_DICT[i] for i in user_answer]
        return ''.join(answer)

    @staticmethod
    def number_to_letter(index):
        if index in NUMBER_LETTER_DICT:
            return NUMBER_LETTER_DICT[index]
        else:
            raise Exception(f'index={index}不在字母字典中')

    def save_cursor(self):
        self.df_flags['cursor'] = 0
        self.df_flags.loc[self.cursor, 'cursor'] = 1

    def save_flag_file(self):
        self.df_flags[['id', 'cursor', 'times', 'times_wrong', 'flag', 'delete']].to_excel(self.flag_file_path, index=False)


if __name__ == '__main__':
    questionFileName = '百日闯关题库_智能模型开发与应用'
    questionFilePath = f'./result/{questionFileName}.xlsx'
    flagFilePath = './result/flag.xlsx'

    app = QApplication(sys.argv)
    exam = Exam(questionFilePath, flagFilePath)

    # 1. 读取题库
    exam.read_question_file()
    # 2. 读取标记文件
    exam.read_flag_file()
    # 3. 过滤删除标记题目
    exam.filter_delete()
    # 4. 开始答题
    exam.start_exam()

    sys.exit(app.exec_())

posted @ 2025-04-24 11:11  Steven0325  阅读(42)  评论(0)    收藏  举报