1. AI编程开发教育测验EXE

在教育场景中,交互式测验软件是巩固知识、检验学习成果的高效工具。但对于没有编程基础的教育工作者来说,定制一款属于自己的练习软件往往门槛很高。本文将从零开始,带你用 Python + Tkinter 构建一个带图形界面的教育测验程序,并最终打包成可独立运行的 EXE 文件,方便在任意 Windows 电脑上分发使用。

学完本文后,你将掌握:

  • 使用 Tkinter 快速搭建测验界面;
  • 从题库中随机抽题并实现计时与评分;
  • 用 PyInstaller 将程序打包为单文件 EXE。

2. 环境准备

在开始编码之前,需要先安装 Python 以及所需的第三方库。

2.1 安装 Python

前往 Python 官方网站 下载最新版 Python(推荐 3.9 及以上),安装时务必勾选「Add Python to PATH」选项。安装完成后,在命令行中验证:

python --version

2.2 安装 PyInstaller

Tkinter 是 Python 标准库的一部分,无需额外安装。打包工具 PyInstaller 需要手动安装:

pip install pyinstaller

至此,开发与打包环境准备就绪,新建一个项目文件夹(如 quiz_app),正式开始编码。

3. 设计题库结构

题库是测验程序的核心,我们用 JSON 文件来存储题目,既方便读取又易于扩充。在项目文件夹中新建 quiz_data.json:

{
  "title": "Python 基础测验",
  "time_limit": 600,
  "questions": [
    {
      "id": 1,
      "question": "Python 中用于定义函数的关键字是?",
      "options": ["func", "define", "def", "function"],
      "answer": 2
    },
    {
      "id": 2,
      "question": "以下哪个不是 Python 的合法变量名?",
      "options": ["_name", "2var", "my_var", "data1"],
      "answer": 1
    },
    {
      "id": 3,
      "question": "list(range(5)) 的输出是?",
      "options": ["[1,2,3,4,5]", "[0,1,2,3,4]", "[0,1,2,3,4,5]", "[1,2,3,4]"],
      "answer": 1
    },
    {
      "id": 4,
      "question": "Python 中字符串格式化方法 .format() 属于哪个版本引入?",
      "options": ["Python 2.6", "Python 3.0", "Python 2.0", "Python 3.6"],
      "answer": 1
    },
    {
      "id": 5,
      "question": "以下哪个数据类型是可变的?",
      "options": ["tuple", "str", "int", "list"],
      "answer": 3
    }
  ]
}

每个题目包含题目文本、四个选项以及正确答案的索引(从0开始)。根节点还记录了测验标题和总时间限制(单位:秒),后续界面会用到这些字段。

4. 搭建 Tkinter 测验界面

新建主程序文件 main.py,逐步实现完整的测验功能。以下代码从零搭建界面,包含欢迎页、答题页和结果页三个视图,结构清晰、注释详尽。

4.1 框架与常量

import tkinter as tk
from tkinter import messagebox
import json
import random
import os

class QuizApp:
    def __init__(self, root):
        self.root = root
        self.root.title("AI 编程开发教育测验")
        self.root.geometry("700x520")
        self.root.resizable(False, False)
        # 配色
        self.COLOR_BG = "#f5f7fa"
        self.COLOR_CARD = "#ffffff"
        self.COLOR_PRIMARY = "#4a90d9"
        self.COLOR_TEXT = "#333333"
        self.COLOR_CORRECT = "#27ae60"
        self.COLOR_WRONG = "#e74c3c"
        self.root.configure(bg=self.COLOR_BG)
        # 加载题库
        self.load_questions()
        # 状态变量
        self.current_q = 0
        self.score = 0
        self.user_answers = []
        self.shuffled_indices = []
        self.time_left = 0
        self.timer_id = None
        # 显示欢迎页
        self.show_welcome()

    def load_questions(self):
        try:
            with open("quiz_data.json", "r", encoding="utf-8") as f:
                data = json.load(f)
            self.title_text = data.get("title", "测验")
            self.time_limit = data.get("time_limit", 600)
            self.questions = data.get("questions", [])
        except FileNotFoundError:
            messagebox.showerror("错误", "找不到 quiz_data.json,请确认题库文件存在。")
            self.questions = []

QuizApp 类负责整个程序的逻辑调度。构造函数中设置了窗口属性与配色常量,加载题库后自动展示欢迎页。

4.2 欢迎页

    def show_welcome(self):
        self.clear_frame()
        self.welcome_frame = tk.Frame(self.root, bg=self.COLOR_BG)
        self.welcome_frame.pack(expand=True)
        tk.Label(
            self.welcome_frame, text=self.title_text,
            font=("微软雅黑", 24, "bold"), bg=self.COLOR_BG, fg=self.COLOR_PRIMARY
        ).pack(pady=(60, 20))
        tk.Label(
            self.welcome_frame,
            text=f"共 {len(self.questions)} 道题 · 限时 {self.time_limit // 60} 分钟",
            font=("微软雅黑", 13), bg=self.COLOR_BG, fg="#666666"
        ).pack(pady=(0, 10))
        rules = [
            "· 题目从题库中随机抽取,顺序打乱",
            "· 每题计时,超时将自动跳过",
            "· 作答后不可返回修改",
            "· 提交后即时显示成绩与解析"
        ]
        for rule in rules:
            tk.Label(
                self.welcome_frame, text=rule,
                font=("微软雅黑", 11), bg=self.COLOR_BG, fg=self.COLOR_TEXT
            ).pack(anchor="w", padx=180)
        tk.Button(
            self.welcome_frame, text="开始测验", command=self.start_quiz,
            font=("微软雅黑", 14, "bold"), bg=self.COLOR_PRIMARY, fg="white",
            activebackground="#3a7bc8", activeforeground="white",
            relief="flat", padx=40, pady=10, cursor="hand2"
        ).pack(pady=(30, 0))

    def clear_frame(self):
        for widget in self.root.winfo_children():
            widget.destroy()

欢迎页展示测验标题、题目数量和规则说明,点击「开始测验」按钮后进入答题阶段。

4.3 答题页与计时器

    def start_quiz(self):
        # 打乱题目顺序
        self.shuffled_indices = list(range(len(self.questions)))
        random.shuffle(self.shuffled_indices)
        self.current_q = 0
        self.score = 0
        self.user_answers = []
        self.time_left = self.time_limit
        self.clear_frame()
        self.build_question_ui()
        self.start_timer()

    def build_question_ui(self):
        self.clear_frame()
        if self.current_q >= len(self.shuffled_indices):
            self.show_result()
            return
        idx = self.shuffled_indices[self.current_q]
        q = self.questions[idx]
        # 顶部信息栏
        top_bar = tk.Frame(self.root, bg=self.COLOR_PRIMARY, height=45)
        top_bar.pack(fill="x")
        tk.Label(
            top_bar, text=f"第 {self.current_q + 1} / {len(self.shuffled_indices)} 题",
            font=("微软雅黑", 12, "bold"), bg=self.COLOR_PRIMARY, fg="white"
        ).pack(side="left", padx=20, pady=8)
        self.timer_label = tk.Label(
            top_bar, text="", font=("微软雅黑", 12, "bold"),
            bg=self.COLOR_PRIMARY, fg="white"
        )
        self.timer_label.pack(side="right", padx=20, pady=8)
        # 题目卡片
        card = tk.Frame(self.root, bg=self.COLOR_CARD, bd=1, relief="solid")
        card.pack(fill="both", padx=30, pady=20, expand=True)
        tk.Label(
            card, text=q["question"], wraplength=600, justify="left",
            font=("微软雅黑", 14), bg=self.COLOR_CARD, fg=self.COLOR_TEXT
        ).pack(anchor="w", padx=25, pady=(25, 20))
        # 选项
        self.option_vars = []
        self.option_buttons = []
        options_frame = tk.Frame(card, bg=self.COLOR_CARD)
        options_frame.pack(padx=25, pady=(0, 10))
        for i, opt in enumerate(q["options"]):
            var = tk.IntVar(value=-1)
            self.option_vars.append(var)
            rb = tk.Radiobutton(
                options_frame, text=f"{chr(65+i)}. {opt}", variable=var,
                value=i, font=("微软雅黑", 12), bg=self.COLOR_CARD,
                fg=self.COLOR_TEXT, anchor="w", width=50,
                selectcolor=self.COLOR_CARD,
                activebackground=self.COLOR_CARD
            )
            rb.pack(anchor="w", pady=3)
            self.option_buttons.append(rb)
        # 提交按钮
        tk.Button(
            card, text="提交答案", command=lambda: self.submit_answer(q),
            font=("微软雅黑", 13, "bold"), bg=self.COLOR_PRIMARY, fg="white",
            activebackground="#3a7bc8", activeforeground="white",
            relief="flat", padx=30, pady=8, cursor="hand2"
        ).pack(pady=(15, 25))

    def start_timer(self):
        def update_timer():
            if self.time_left <= 0:
                messagebox.showinfo("时间到", "测验时间已结束,将自动提交。")
                self.show_result()
                return
            mins, secs = divmod(self.time_left, 60)
            self.timer_label.config(text=f"剩余 {mins:02d}:{secs:02d}")
            self.time_left -= 1
            self.timer_id = self.root.after(1000, update_timer)
        update_timer()

    def submit_answer(self, q):
        if self.timer_id:
            self.root.after_cancel(self.timer_id)
        selected = -1
        for var in self.option_vars:
            if var.get() != -1:
                selected = var.get()
                break
        if selected == -1:
            messagebox.showwarning("提示", "请先选择一个选项再提交。")
            self.start_timer()
            return
        correct = q["answer"]
        is_correct = (selected == correct)
        if is_correct:
            self.score += 1
        self.user_answers.append({
            "question": q["question"],
            "user_choice": selected,
            "correct": correct,
            "is_correct": is_correct,
            "options": q["options"]
        })
        self.current_q += 1
        self.build_question_ui()
        if self.current_q < len(self.shuffled_indices):
            self.start_timer()

答题页逐题展示,用户选择后点击提交进入下一题。全局计时器贯穿整个测验,超时自动提交。

4.4 结果页

    def show_result(self):
        if self.timer_id:
            self.root.after_cancel(self.timer_id)
        self.clear_frame()
        total = len(self.shuffled_indices)
        result_frame = tk.Frame(self.root, bg=self.COLOR_BG)
        result_frame.pack(expand=True, fill="both")
        # 成绩展示
        tk.Label(
            result_frame, text="测验完成", font=("微软雅黑", 22, "bold"),
            bg=self.COLOR_BG, fg=self.COLOR_PRIMARY
        ).pack(pady=(30, 10))
        score_color = self.COLOR_CORRECT if self.score >= total * 0.6 else self.COLOR_WRONG
        tk.Label(
            result_frame, text=f"{self.score} / {total}",
            font=("微软雅黑", 36, "bold"), bg=self.COLOR_BG, fg=score_color
        ).pack(pady=(0, 5))
        tk.Label(
            result_frame,
            text=f"正确率 {self.score / total * 100:.1f}%",
            font=("微软雅黑", 13), bg=self.COLOR_BG, fg="#666666"
        ).pack(pady=(0, 20))
        # 解析列表
        canvas = tk.Canvas(result_frame, bg=self.COLOR_BG, highlightthickness=0)
        scrollbar = tk.Scrollbar(result_frame, orient="vertical", command=canvas.yview)
        scroll_frame = tk.Frame(canvas, bg=self.COLOR_BG)
        scroll_frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
        canvas.create_window((0, 0), window=scroll_frame, anchor="nw", tags="frame")
        canvas.configure(yscrollcommand=scrollbar.set)
        canvas.pack(side="left", fill="both", expand=True, padx=(30, 0))
        scrollbar.pack(side="right", fill="y")
        # 绑定鼠标滚轮
        def on_mousewheel(event):
            canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
        canvas.bind_all("", on_mousewheel)
        for i, ans in enumerate(self.user_answers):
            icon = "✓" if ans["is_correct"] else "✗"
            color = self.COLOR_CORRECT if ans["is_correct"] else self.COLOR_WRONG
            item_frame = tk.Frame(scroll_frame, bg=self.COLOR_CARD, bd=1, relief="solid")
            item_frame.pack(fill="x", pady=5, padx=(0, 30))
            tk.Label(
                item_frame, text=f"{icon} 第{i+1}题",
                font=("微软雅黑", 11, "bold"), bg=self.COLOR_CARD, fg=color
            ).pack(anchor="w", padx=12, pady=(8, 2))
            tk.Label(
                item_frame, text=ans["question"], wraplength=550,
                font=("微软雅黑", 11), bg=self.COLOR_CARD, fg=self.COLOR_TEXT,
                justify="left"
            ).pack(anchor="w", padx=12)
            tk.Label(
                item_frame,
                text=f"你的答案:{ans['options'][ans['user_choice']]}  正确答案:{ans['options'][ans['correct']]}",
                font=("微软雅黑", 10), bg=self.COLOR_CARD, fg="#888888"
            ).pack(anchor="w", padx=12, pady=(2, 8))
        # 底部按钮
        btn_frame = tk.Frame(result_frame, bg=self.COLOR_BG)
        btn_frame.pack(pady=15)
        tk.Button(
            btn_frame, text="重新测验", command=self.start_quiz,
            font=("微软雅黑", 12, "bold"), bg=self.COLOR_PRIMARY, fg="white",
            activebackground="#3a7bc8", activeforeground="white",
            relief="flat", padx=25, pady=8, cursor="hand2"
        ).pack(side="left", padx=10)
        tk.Button(
            btn_frame, text="返回首页", command=self.show_welcome,
            font=("微软雅黑", 12), bg="#cccccc", fg=self.COLOR_TEXT,
            activebackground="#bbbbbb", relief="flat", padx=25, pady=8, cursor="hand2"
        ).pack(side="left", padx=10)
        # 解除鼠标滚轮绑定,避免影响其他页面
        canvas.bind_all("", lambda e: None)

if __name__ == "__main__":
    root = tk.Tk()
    app = QuizApp(root)
    root.mainloop()

结果页展示总分、正确率,并以卡片列表形式逐题给出正误判断与正确答案。用户可以选择重新测验或返回首页。

5. 运行与测试

确保 main.py 与 quiz_data.json 在同一目录下,执行以下命令启动程序:

python main.py

程序启动后将看到欢迎页,点击「开始测验」即可逐题作答。测试要点:

  • 验证题目随机打乱是否生效;
  • 确认不选选项时提交会弹出提示;
  • 检查计时器在最后一秒是否触发自动提交;
  • 观察结果页的解析列表是否正确滚动。

6. 打包为 EXE

程序调试无误后,使用 PyInstaller 将其打包成单个 EXE 文件,方便在没有 Python 环境的电脑上直接运行。

6.1 基本打包命令

pyinstaller --onefile --windowed --name "AI编程教育测验" main.py

参数说明:

  • --onefile:将所有依赖打包进单个 EXE,分发更简洁;
  • --windowed:运行时不弹出命令行窗口(纯 GUI 体验);
  • --name:指定生成的 EXE 文件名。

6.2 处理外部数据文件

quiz_data.json 不会被自动打包进 EXE,需要手动指定或运行时动态读取。推荐使用 --add-data 参数:

pyinstaller --onefile --windowed --name "AI编程教育测验" --add-data "quiz_data.json;." main.py

同时修改 main.py 中 load_questions 方法的文件读取路径,使其兼容打包后的运行环境:

def load_questions(self):
    # 兼容 PyInstaller 打包后的资源路径
    if getattr(sys, "frozen", False):
        base_path = sys._MEIPASS
    else:
        base_path = os.path.dirname(os.path.abspath(__file__))
    json_path = os.path.join(base_path, "quiz_data.json")
    try:
        with open(json_path, "r", encoding="utf-8") as f:
            data = json.load(f)
        self.title_text = data.get("title", "测验")
        self.time_limit = data.get("time_limit", 600)
        self.questions = data.get("questions", [])
    except FileNotFoundError:
        messagebox.showerror("错误", f"找不到题库文件:{json_path}")
        self.questions = []

记得在文件顶部添加 import sys。打包完成后,在 dist 文件夹中找到生成的 EXE,双击即可运行。

6.3 自定义图标

如果你希望 EXE 拥有专属图标,准备一个 .ico 格式的图标文件,在打包命令中加入 --icon 参数:

pyinstaller --onefile --windowed --name "AI编程教育测验" --add-data "quiz_data.json;." --icon "icon.ico" main.py

7. 优化建议

以上已经是一个功能完整的测验软件,但你还可以进一步扩展:

  • 多题库切换:在欢迎页增加下拉菜单,让用户选择不同科目的题库;
  • 成绩记录:将每次测验的成绩保存到本地文件或 SQLite 数据库,提供历史趋势图表;
  • 错题本:单独列出做错的题目供复习,支持重做错题模式;
  • 题型扩展:增加填空题、判断题、多选题等多种题型;
  • 界面美化:使用 ttk 主题或引入图片背景,让 UI 更具吸引力。

8. 总结

本文从零开始,带领你完成了一个基于 Python + Tkinter 的教育测验软件。核心内容包括:

  • 用 JSON 管理题库数据,实现题目与代码的解耦;
  • 用 Tkinter 构建欢迎页、答题页、结果页三个模块,并实现随机抽题与全局计时;
  • 用 PyInstaller 将程序打包为独立 EXE,方便分发部署。

整个项目代码量不到 300 行,却覆盖了界面开发、事件处理、数据持久化和程序打包等实用技能。无论你是教育从业者还是编程初学者,都可以在此基础上二次开发,做出满足自己需求的练习软件。动手试试吧!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐