使用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_())
God will send the rain when you are ready.You need to prepare your field to receive it.