Python答题系统简易实现指南

在现代教育和培训中,答题系统是一种常见的工具,用于评估学习者的知识掌握情况。无论是学校教育、职业培训还是在线学习平台,答题系统都扮演着重要的角色。传统的纸质考试逐渐被电子化答题系统取代,这不仅提高了效率,还增强了用户体验。

Python 作为一种简单易学、功能强大的编程语言,非常适合用于开发小型应用程序。通过Python,我们可以快速实现一个简单的答题系统,支持从文件中读取题目、动态加载题目、提交答案、自动校验结果以及保存答题记录等功能。

本文将介绍如何使用 Python 和 Tkinter 库实现一个简单的答题系统,并逐步讲解其核心功能和实现细节。

功能概述

本答题系统的主要功能包括:

  1. 题目加载

  2. 从 Excel 文件中读取题目,支持单选和多选题。

  3. 动态加载题目和选项,显示在用户界面中。

  4. 答题功能

  5. 用户可以选择答案并提交。

  6. 支持自动校验答案,显示答题结果。

  7. 结果保存

  8. 将用户的答题记录保存到 Excel 文件中,便于后续分析和存档。

  9. 查询功能

  10. 提供按题目内容和答题状态(正确/错误)查询的功能。

  11. 用户界面

  12. 使用 Tkinter 构建简单的图形用户界面(GUI),提升用户体验。

技术栈

  • Python:核心编程语言。

  • Tkinter:用于构建图形用户界面。

  • Pandas:用于读取和保存 Excel 文件。

  • msoffcrypto:支持读取加密的文件内容。

  • 实现步骤

    环境准备

  • 安装 Python 3.x。

  • 安装所需的第三方库:pandasopenpyxl

  • pip install pandas openpyxl

    题目数据准备

  • 将题目存储在 Excel 文件中,包含以下列:

  • 题目

  • 选项1

  • 选项2

  • 选项3

  • 选项4

  • 答案

  • 类型(单选/多选)

  • 解析

  • 核心功能实现

    1. 使用 Tkinter 构建用户界面。

    2. 使用 Pandas 读取 Excel 文件中的题目。

    3. 动态加载题目和选项,支持用户选择答案。

    4. 提交答案后,自动校验并显示结果。

    5. 将答题记录保存到 Excel 文件中。

    6. 提供查询功能,按题目内容或答题状态筛选结果。

    7. 支持题目跳转,重新回顾固定题目。

    代码示例

    以下是核心代码的简要示例:

    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

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python答题系统简易实现指南

    发表回复