Python答题系统简易实现指南
在现代教育和培训中,答题系统是一种常见的工具,用于评估学习者的知识掌握情况。无论是学校教育、职业培训还是在线学习平台,答题系统都扮演着重要的角色。传统的纸质考试逐渐被电子化答题系统取代,这不仅提高了效率,还增强了用户体验。
Python 作为一种简单易学、功能强大的编程语言,非常适合用于开发小型应用程序。通过Python,我们可以快速实现一个简单的答题系统,支持从文件中读取题目、动态加载题目、提交答案、自动校验结果以及保存答题记录等功能。
本文将介绍如何使用 Python 和 Tkinter 库实现一个简单的答题系统,并逐步讲解其核心功能和实现细节。
功能概述
本答题系统的主要功能包括:
-
题目加载:
-
从 Excel 文件中读取题目,支持单选和多选题。
-
动态加载题目和选项,显示在用户界面中。
-
答题功能:
-
用户可以选择答案并提交。
-
支持自动校验答案,显示答题结果。
-
结果保存:
-
将用户的答题记录保存到 Excel 文件中,便于后续分析和存档。
-
查询功能:
-
提供按题目内容和答题状态(正确/错误)查询的功能。
-
用户界面:
-
使用 Tkinter 构建简单的图形用户界面(GUI),提升用户体验。
技术栈
Python:核心编程语言。
Tkinter:用于构建图形用户界面。
Pandas:用于读取和保存 Excel 文件。
msoffcrypto:支持读取加密的文件内容。
实现步骤
环境准备:
安装 Python 3.x。
安装所需的第三方库:pandas
、openpyxl
pip install pandas openpyxl
题目数据准备:
将题目存储在 Excel 文件中,包含以下列:
题目
选项1
选项2
选项3
选项4
答案
类型(单选/多选)
解析
核心功能实现:
-
使用 Tkinter 构建用户界面。
-
使用 Pandas 读取 Excel 文件中的题目。
-
动态加载题目和选项,支持用户选择答案。
-
提交答案后,自动校验并显示结果。
-
将答题记录保存到 Excel 文件中。
-
提供查询功能,按题目内容或答题状态筛选结果。
-
支持题目跳转,重新回顾固定题目。
代码示例
以下是核心代码的简要示例:
import platform
from io import BytesIO
from openpyxl import load_workbook
import tkinter as tk
from tkinter import ttk, messagebox
import pandas as pd
from tkinter import filedialog
import msoffcrypto
class QuizApp:
def __init__(self, root, file_path):
self.root = root
self.file_path = file_path
self.questions = None
self.current_question = 0
self.score = 0
self.user_answers = []
self.auto_check = tk.BooleanVar(value=True) # 默认开启自动校验
# 设置窗口标题
self.root.title("答题系统")
# 设置窗口图标
try:
system = platform.system()
if system == "Windows":
self.root.iconbitmap("que.ico") # Windows 图标
elif system == "Linux":
self.root.iconbitmap("@icon.xbm") # Linux 图标
elif system == "Darwin": # macOS
img = tk.PhotoImage(file="que.png") # macOS 使用 PNG 文件
self.root.tk.call("wm", "iconphoto", self.root._w, img)
except Exception as e:
print(f"无法加载图标文件: {e}") # 如果图标文件加载失败,打印错误信息
# 加载 Excel 文件中的所有 Sheet
self.sheets = self.load_sheets()
# 选择 Sheet 的区域
self.sheet_frame = tk.Frame(root)
self.sheet_frame.pack(pady=10)
self.sheet_label = tk.Label(self.sheet_frame, text="请选择 题库:", font=("Arial", 12))
self.sheet_label.pack(side="left", padx=10)
self.sheet_combobox = ttk.Combobox(self.sheet_frame, values=self.sheets, font=("Arial", 12))
self.sheet_combobox.pack(side="left", padx=10)
self.start_button = tk.Button(self.sheet_frame, text="开始答题", command=self.start_quiz, font=("Arial", 12))
self.start_button.pack(side="left", padx=10)
# 总题数和已答题数显示区域
self.status_frame = tk.Frame(root)
self.status_frame.pack(pady=10)
self.total_questions_label = tk.Label(self.status_frame, text="总题数: 0", font=("Arial", 12))
self.total_questions_label.pack(side="left", padx=10)
self.answered_questions_label = tk.Label(self.status_frame, text="已答题数: 0", font=("Arial", 12))
self.answered_questions_label.pack(side="left", padx=10)
# 配置区域
self.config_frame = tk.Frame(root)
self.config_frame.pack(pady=10)
self.auto_check_button = tk.Checkbutton(
self.config_frame,
text="提交答案后自动校验结果",
variable=self.auto_check,
font=("Arial", 12))
self.auto_check_button.pack()
# 题目显示区域
self.question_label = tk.Label(root, text="", font=("Arial", 14), wraplength=850)
self.question_label.pack(pady=20)
# 选项显示区域
self.option_vars = []
self.option_vars_words = []
self.option_buttons = []
self.option_frame = tk.Frame(root)
self.option_frame.pack()
# 在 __init__ 方法中添加以下代码
self.prev_button = tk.Button(root, text="上一题", command=self.prev_question, font=("Arial", 12))
self.prev_button.pack(pady=10)
self.prev_button.config(state=tk.DISABLED) # 初始禁用上一题按钮
# 在 __init__ 方法中添加以下代码
self.jump_frame = tk.Frame(root)
self.jump_frame.pack(pady=10)
self.jump_label = tk.Label(self.jump_frame, text="跳转到题目:", font=("Arial", 12))
self.jump_label.pack(side="left", padx=10)
self.jump_entry = tk.Entry(self.jump_frame, font=("Arial", 12), width=10)
self.jump_entry.pack(side="left", padx=10)
self.jump_button = tk.Button(self.jump_frame, text="跳转", command=self.jump_to_question, font=("Arial", 12))
self.jump_button.pack(side="left", padx=10)
# 提交按钮
self.submit_button = tk.Button(root, text="提交答案", command=self.submit_answer, font=("Arial", 12))
self.submit_button.pack(pady=20)
self.submit_button.config(state=tk.DISABLED) # 初始禁用提交按钮
def load_sheets(self):
"""加载 Excel 文件中的所有 Sheet 名称"""
try:
#excel无加密
# excel_file = pd.ExcelFile(self.file_path)
# return excel_file.sheet_names
# excel加密
excel_file = self.read_excel_auto(self.file_path, "aura112233")
return excel_file.sheet_names
except Exception as e:
messagebox.showerror("错误", f"加载 Excel 文件失败: {e}")
return []
def read_excel_auto(self,file_path, password=None):
with open(file_path, "rb") as f:
if msoffcrypto.olefile.isOleFile(f): # 检测是否加密
decrypted_stream = BytesIO()
office_file = msoffcrypto.OfficeFile(f)
office_file.load_key(password=password)
office_file.decrypt(decrypted_stream)
decrypted_stream.seek(0)
return pd.ExcelFile(decrypted_stream)
else:
return pd.ExcelFile(f) # 直接加载未加密文件
def read_excel_autoBysheet(self,file_path,selectedsheet, password=None):
with open(file_path, "rb") as f:
if msoffcrypto.olefile.isOleFile(f): # 检测是否加密
decrypted_stream = BytesIO()
office_file = msoffcrypto.OfficeFile(f)
office_file.load_key(password=password)
office_file.decrypt(decrypted_stream)
decrypted_stream.seek(0)
return pd.read_excel(decrypted_stream, sheet_name=selectedsheet)
else:
return pd.read_excel(f, sheet_name=selectedsheet) # 直接加载未加密文件
def start_quiz(self):
"""开始答题"""
selected_sheet = self.sheet_combobox.get()
if not selected_sheet:
messagebox.showwarning("提示", "请选择一个题库!")
return
try:
# 加载选择的 Sheet #excel无加密
# self.questions = pd.read_excel(self.file_path, sheet_name=selected_sheet)
try:
# excel加密
self.questions = self.read_excel_autoBysheet(self.file_path, selected_sheet,"aura112233")
except Exception as e:
self.questions = pd.read_excel(self.file_path, sheet_name=selected_sheet, password="aura112233")
self.current_question = 0
self.score = 0
self.user_answers = []
# 更新总题数显示
self.total_questions_label.config(text=f"总题数: {len(self.questions)}")
# 启用提交按钮
self.submit_button.config(state=tk.NORMAL)
# 加载第一题
self.load_question()
except Exception as e:
messagebox.showerror("错误", f"加载题目失败: {e}")
def jump_to_question(self):
"""跳转到指定题目"""
try:
# 获取用户输入的序号
question_number = int(self.jump_entry.get().strip())
# 检查序号是否有效
if 1 <= question_number <= len(self.questions):
self.current_question = question_number - 1 # 转换为索引
self.load_question() # 加载题目
# 恢复用户之前的选择(如果有)
if self.current_question < len(self.user_answers):
user_answer = self.user_answers[self.current_question]["用户答案"].split(", ")
self.restore_user_answer(user_answer)
# 更新按钮状态
self.update_button_states()
else:
messagebox.showwarning("提示", f"请输入有效的题目序号(1-{len(self.questions)})")
except ValueError:
messagebox.showwarning("提示", "请输入有效的数字!")
def prev_question(self):
"""返回到上一题"""
if self.current_question > 0:
self.current_question -= 1 # 回到上一题
self.load_question() # 加载题目
# 恢复用户之前的选择
if self.current_question < len(self.user_answers):
user_answer = self.user_answers[self.current_question]["用户答案"].split(", ")
self.restore_user_answer(user_answer)
# 更新按钮状态
self.update_button_states()
def restore_user_answer(self, user_answer):
"""恢复用户之前的选择"""
question_data = self.questions.iloc[self.current_question]
if question_data["类型"] == "单选":
for i, var in enumerate(self.option_vars):
if self.option_vars_words[i] in user_answer:
var.set(self.option_vars_words[i])
break
else:
for i, var in enumerate(self.option_vars):
if self.option_vars_words[i] in user_answer:
var.set(1)
else:
var.set(0)
def update_button_states(self):
"""更新上一题和下一题按钮的状态"""
# 更新“上一题”按钮状态
if self.current_question > 0:
self.prev_button.config(state=tk.NORMAL)
else:
self.prev_button.config(state=tk.DISABLED)
# 更新“提交答案”按钮状态
if self.current_question < len(self.questions):
self.submit_button.config(state=tk.NORMAL)
else:
self.submit_button.config(state=tk.DISABLED)
def load_question(self):
"""加载当前题目和选项"""
# 清空选项区域和变量
for widget in self.option_frame.winfo_children():
widget.destroy()
self.option_vars.clear() # 清空变量列表
self.option_vars_words.clear() # 清空变量列表
self.option_buttons.clear() # 清空按钮列表
# 获取当前题目
question_data = self.questions.iloc[self.current_question]
question_index = question_data["序号"]
question_text = question_data["题目"]
options = [question_data[f"选项{i+1}"] for i in range(5) if pd.notna(question_data[f"选项{i+1}"])]
options_words = [question_data[f"选项{i+1}字符"] for i in range(5) if pd.notna(question_data[f"选项{i+1}字符"])]
question_type = question_data["类型"] # 单选或多选
# 显示题目
self.question_label.config(text=f"题目 {self.current_question + 1}: {question_text}")
# 更新跳转输入框的内容
self.jump_entry.delete(0, tk.END)
self.jump_entry.insert(0, str(self.current_question + 1))
# 显示选项
if question_type == "单选":
var = tk.StringVar(value="-1") # 初始值为空,避免默认选中
for i, option in enumerate(options):
rb = tk.Radiobutton(self.option_frame, text=option, variable=var, value=option)
rb.pack(anchor="w", fill=tk.X)
rb.deselect() # 显式取消选中
self.option_vars.append(var) # 添加变量
self.option_vars_words.append(options_words[i]) # 添加变量
self.option_buttons.append(rb) # 添加按钮
else:
for i, option in enumerate(options):
var = tk.IntVar(value=0) # 初始值为0,避免默认选中
cb = tk.Checkbutton(self.option_frame, text=option, variable=var)
cb.pack(anchor="w")
self.option_vars.append(var) # 添加变量
self.option_vars_words.append(options_words[i]) # 添加变量
self.option_buttons.append(cb) # 添加按钮
# 更新已答题数显示
self.answered_questions_label.config(text=f"已答题数: {self.current_question}")
# 更新按钮状态
self.update_button_states()
def submit_answer(self):
"""提交答案并计算得分"""
# 获取用户答案
question_data = self.questions.iloc[self.current_question]
correct_answer = question_data["答案"].split(",") # 多选答案用逗号分隔
user_answer = []
if question_data["类型"] == "单选":
uni=""
for var in self.option_vars:
if var.get() and var.get()!='-1' and uni!=var.get():
user_answer.append(var.get().split(':')[0].strip())
uni = var.get()
else:
for i, var in enumerate(self.option_vars):
if var.get() and var.get()!='-1':
user_answer.append(self.option_buttons[i].cget("text").split(':')[0].strip())
# 检查是否选择了答案
if not user_answer:
messagebox.showwarning("提示", "请选择一个答案!")
return
# 检查答案
is_correct = set(user_answer) == set(correct_answer)
if is_correct:
self.score += 1
# 记录用户答案
self.user_answers.append({
"题目": question_data["题目"],
"用户答案": ", ".join(user_answer),
"正确答案": ", ".join(correct_answer),
"解析": question_data.get("解析", "暂无解析"),
"是否正确": "正确" if is_correct else "错误"
})
# 如果开启自动校验,显示当前题目的对错
if self.auto_check.get():
self.show_question_result(is_correct, user_answer, correct_answer, question_data)
# 下一题或结束
self.current_question += 1
if self.current_question < len(self.questions):
self.load_question()
else:
self.show_final_result()
# 更新按钮状态
self.update_button_states()
def show_question_result(self, is_correct, user_answer, correct_answer, question_data):
"""显示当前题目的答题结果"""
result = "回答正确!" if is_correct else f"回答错误!正确答案是:{', '.join(correct_answer)}"
details = f"\n您的答案: {', '.join(user_answer)}\n"
details += f"正确答案: {', '.join(correct_answer)}\n"
# 显示解析
jiexi = question_data.get("解析", "暂无解析")
details += f"\n解析: {jiexi}\n"
if is_correct:
messagebox.showinfo("答题结果", result + details)
else:
messagebox.showerror("答题结果", result + details)
def show_final_result(self):
"""显示最终答题结果并保存到 Excel"""
# 显示总得分
result = f"答题结束!\n您的得分是:{self.score}/{len(self.questions)}"
messagebox.showinfo("答题结果", result)
# 弹框确认是否保存结果
save_confirmation = messagebox.askyesno("保存结果", "是否将答题结果保存到 Excel 文件?")
if save_confirmation:
self.save_results_to_excel()
# 显示结果列表
self.show_result_list()
def save_results_to_excel(self):
"""将答题结果保存到 Excel 文件"""
# 将用户答案转换为 DataFrame
results_df = pd.DataFrame(self.user_answers)
# 弹出文件保存对话框
file_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel 文件", "*.xlsx")],
title="保存答题结果"
)
if file_path:
try:
# 保存到 Excel
results_df.to_excel(file_path, index=False)
messagebox.showinfo("保存成功", f"答题结果已保存到:{file_path}")
except Exception as e:
messagebox.showerror("保存失败", f"保存文件时出错:{e}")
def show_result_list(self):
"""显示答题结果列表"""
# 创建新窗口
result_window = tk.Toplevel(self.root)
result_window.title("答题结果列表")
# 添加查询框
search_frame = tk.Frame(result_window)
search_frame.pack(pady=10)
self.search_entry = tk.Entry(search_frame, font=("Arial", 12), width=30)
self.search_entry.pack(side="left", padx=10)
self.status_combobox = ttk.Combobox(search_frame, values=["全部", "正确", "错误"], font=("Arial", 12))
self.status_combobox.pack(side="left", padx=10)
self.status_combobox.current(0) # 默认选择“全部”
search_button = tk.Button(search_frame, text="查询", command=self.search_results, font=("Arial", 12))
search_button.pack(side="left")
# 添加 Treeview 组件
columns = ("题目", "用户答案", "正确答案", "解析", "是否正确")
self.tree = ttk.Treeview(result_window, columns=columns, show="headings")
self.tree.pack(fill="both", expand=True)
# 设置列标题
for col in columns:
self.tree.heading(col, text=col)
# 插入数据
for result in self.user_answers:
self.tree.insert("", "end", values=(
result["题目"],
result["用户答案"],
result["正确答案"],
result["解析"],
result["是否正确"]
))
def search_results(self):
"""查询答题结果"""
keyword = self.search_entry.get().strip().lower()
status_filter = self.status_combobox.get()
# 遍历 Treeview,隐藏不匹配的行
for item in self.tree.get_children():
values = self.tree.item(item, "values")
match_keyword = keyword in values[0].lower() # 题目匹配
match_status = (status_filter == "全部") or (status_filter == values[4]) # 状态匹配
if match_keyword and match_status:
self.tree.item(item, tags=("match",))
else:
self.tree.item(item, tags=())
# 高亮匹配的行
self.tree.tag_configure("match", background="yellow")
def load_questions_from_excel(file_path):
"""从Excel加载题目"""
df = pd.read_excel(file_path, engine="openpyxl")
return df
def read_encrypted_xlsx(file_path, password):
wb = load_workbook(file_path, read_only=True, password=password) # 直接指定密码
sheet = wb.active
data = sheet.values
return pd.DataFrame(data, columns=next(data)) # 转换为 DataFrame
if __name__ == "__main__":
# 创建Tkinter窗口
root = tk.Tk()
root.geometry("1000x700")
# Excel 文件路径
file_path = "questions.xlsx"
# 启动答题系统
app = QuizApp(root, file_path)
root.mainloop()
运行效果
动动小手,扫码关注,一起进步。
作者:Felix_Fly