软工博客28
关于创建标准化考试系统:
import json
import time
import os
import sqlite3
import shutil
from datetime import datetime
from tkinter import *
from tkinter import ttk, messagebox, filedialog
from PIL import Image, ImageTk
数据库操作类
class ExamDatabase:
def init(self, db_file="exam_system.db"):
self.db_file = db_file
self.conn = None
self.connect()
self.create_tables()
def connect(self):
"""连接到数据库"""
self.conn = sqlite3.connect(self.db_file)
self.conn.row_factory = sqlite3.Row
def create_tables(self):
"""创建数据库表"""
cursor = self.conn.cursor()
# 考试表
cursor.execute('''
CREATE TABLE IF NOT EXISTS exams (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL UNIQUE,
duration INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# 问题表
cursor.execute('''
CREATE TABLE IF NOT EXISTS questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
exam_id INTEGER NOT NULL,
question_text TEXT NOT NULL,
question_type TEXT NOT NULL,
options TEXT, -- JSON格式存储选项
correct_answer TEXT NOT NULL,
FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE
)
''')
# 学生答案表
cursor.execute('''
CREATE TABLE IF NOT EXISTS student_answers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
exam_record_id INTEGER NOT NULL,
question_id INTEGER NOT NULL,
answer TEXT,
is_correct INTEGER DEFAULT 0,
answered_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (exam_record_id) REFERENCES exam_records(id) ON DELETE CASCADE,
FOREIGN KEY (question_id) REFERENCES questions(id) ON DELETE CASCADE
)
''')
# 考试记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS exam_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
exam_id INTEGER NOT NULL,
student_name TEXT,
start_time TIMESTAMP,
end_time TIMESTAMP,
score INTEGER,
total_questions INTEGER,
FOREIGN KEY (exam_id) REFERENCES exams(id) ON DELETE CASCADE
)
''')
self.conn.commit()
def add_exam(self, title, duration):
"""添加新考试"""
try:
cursor = self.conn.cursor()
cursor.execute("INSERT INTO exams (title, duration) VALUES (?, ?)",
(title, duration))
self.conn.commit()
return cursor.lastrowid
except sqlite3.IntegrityError:
return None
def get_exam(self, exam_id=None, title=None):
"""获取考试信息"""
cursor = self.conn.cursor()
if exam_id:
cursor.execute("SELECT * FROM exams WHERE id=?", (exam_id,))
elif title:
cursor.execute("SELECT * FROM exams WHERE title=?", (title,))
else:
return None
return cursor.fetchone()
def get_all_exams(self):
"""获取所有考试"""
cursor = self.conn.cursor()
cursor.execute("SELECT * FROM exams ORDER BY created_at DESC")
return cursor.fetchall()
def add_question(self, exam_id, question_text, question_type, options, correct_answer):
"""添加问题"""
cursor = self.conn.cursor()
options_json = json.dumps(options) if options else None
cursor.execute(
"INSERT INTO questions (exam_id, question_text, question_type, options, correct_answer) VALUES (?, ?, ?, ?, ?)",
(exam_id, question_text, question_type, options_json, correct_answer)
)
self.conn.commit()
return cursor.lastrowid
def get_questions(self, exam_id):
"""获取考试的所有问题"""
cursor = self.conn.cursor()
cursor.execute("SELECT * FROM questions WHERE exam_id=? ORDER BY id", (exam_id,))
return cursor.fetchall()
def start_exam(self, exam_id, student_name):
"""开始考试并创建记录"""
cursor = self.conn.cursor()
start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
cursor.execute(
"INSERT INTO exam_records (exam_id, student_name, start_time) VALUES (?, ?, ?)",
(exam_id, student_name, start_time)
)
self.conn.commit()
return cursor.lastrowid
def submit_answer(self, record_id, question_id, answer, is_correct):
"""提交答案"""
cursor = self.conn.cursor()
# 检查是否已有答案,避免重复
cursor.execute(
"SELECT id FROM student_answers WHERE exam_record_id=? AND question_id=?",
(record_id, question_id)
)
existing = cursor.fetchone()
if existing:
cursor.execute(
"UPDATE student_answers SET answer=?, is_correct=? WHERE id=?",
(answer, is_correct, existing['id'])
)
else:
cursor.execute(
"INSERT INTO student_answers (exam_record_id, question_id, answer, is_correct) VALUES (?, ?, ?, ?)",
(record_id, question_id, answer, is_correct)
)
self.conn.commit()
def finish_exam(self, record_id, score, total_questions):
"""完成考试并记录分数"""
cursor = self.conn.cursor()
end_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
cursor.execute(
"UPDATE exam_records SET end_time=?, score=?, total_questions=? WHERE id=?",
(end_time, score, total_questions, record_id)
)
self.conn.commit()
def get_exam_results(self, record_id):
"""获取考试结果"""
cursor = self.conn.cursor()
cursor.execute(
"SELECT er.*, e.title, e.duration FROM exam_records er JOIN exams e ON er.exam_id=e.id WHERE er.id=?",
(record_id,)
)
record = cursor.fetchone()
if record:
cursor.execute(
"SELECT q.*, sa.answer, sa.is_correct FROM student_answers sa "
"JOIN questions q ON sa.question_id=q.id "
"WHERE sa.exam_record_id=? ORDER BY q.id",
(record_id,)
)
questions = cursor.fetchall()
return record, questions
return None, None
def get_all_results(self):
"""获取所有考试结果"""
cursor = self.conn.cursor()
cursor.execute(
"SELECT er.id, e.title, er.student_name, er.score, er.total_questions, "
"er.start_time, er.end_time FROM exam_records er JOIN exams e ON er.exam_id=e.id "
"WHERE er.end_time IS NOT NULL ORDER BY er.end_time DESC"
)
return cursor.fetchall()
def delete_exam(self, exam_id):
"""删除考试及关联数据"""
cursor = self.conn.cursor()
cursor.execute("DELETE FROM exams WHERE id=?", (exam_id,))
self.conn.commit()
def close(self):
"""关闭数据库连接"""
if self.conn:
self.conn.close()
考试系统核心逻辑
class ExamSystem:
def init(self, db):
self.db = db
self.current_exam = None
self.current_record = None
self.student_name = None
def create_exam(self, title, duration):
"""创建新考试"""
return self.db.add_exam(title, duration)
def add_question(self, exam_id, question_text, question_type, options, correct_answer):
"""添加问题到考试"""
return self.db.add_question(exam_id, question_text, question_type, options, correct_answer)
def get_exam_list(self):
"""获取考试列表"""
return self.db.get_all_exams()
def start_exam(self, exam_id, student_name):
"""开始考试"""
self.current_exam = self.db.get_exam(exam_id=exam_id)
self.student_name = student_name
self.current_record = self.db.start_exam(exam_id, student_name)
return self.current_record
def get_exam_questions(self, exam_id):
"""获取考试问题"""
return self.db.get_questions(exam_id)
def submit_answer(self, question_id, answer, is_correct):
"""提交答案"""
if self.current_record:
self.db.submit_answer(self.current_record, question_id, answer, is_correct)
def finish_exam(self, score, total_questions):
"""完成考试"""
if self.current_record:
self.db.finish_exam(self.current_record, score, total_questions)
self.current_exam = None
self.current_record = None
def get_exam_result(self, record_id):
"""获取考试结果"""
return self.db.get_exam_results(record_id)
def get_all_results(self):
"""获取所有考试结果"""
return self.db.get_all_results()
图形界面
class ExamSystemGUI:
def init(self, root):
self.root = root
self.root.title("标准化考试系统")
self.root.geometry("1000x700")
# 初始化数据库和系统
self.db = ExamDatabase()
self.system = ExamSystem(self.db)
# 设置样式
self.setup_styles()
# 创建主界面
self.create_main_frame()
# 加载图标
self.load_icons()
# 显示主菜单
self.show_main_menu()
def setup_styles(self):
"""设置界面样式"""
style = ttk.Style()
style.configure("TFrame", background="#f0f0f0")
style.configure("TButton", font=('Microsoft YaHei', 10), padding=5)
style.configure("Title.TLabel", font=('Microsoft YaHei', 16, 'bold'), background="#f0f0f0")
style.configure("Header.TLabel", font=('Microsoft YaHei', 12, 'bold'), background="#f0f0f0")
style.configure("Normal.TLabel", font=('Microsoft YaHei', 10), background="#f0f0f0")
style.configure("Question.TLabel", font=('Microsoft YaHei', 11), background="#f0f0f0", wraplength=600)
style.configure("Option.TRadiobutton", font=('Microsoft YaHei', 10), background="#f0f0f0")
style.configure("TEntry", font=('Microsoft YaHei', 10))
style.configure("TCombobox", font=('Microsoft YaHei', 10))
def load_icons(self):
"""加载图标资源"""
try:
self.icons = {
"exam": self.load_image("exam_icon.png", (64, 64)),
"question": self.load_image("question_icon.png", (64, 64)),
"result": self.load_image("result_icon.png", (64, 64)),
"back": self.load_image("back_icon.png", (32, 32)),
"next": self.load_image("next_icon.png", (32, 32)),
"submit": self.load_image("submit_icon.png", (32, 32))
}
except:
# 如果图标加载失败,使用文本代替
self.icons = None
def load_image(self, filename, size):
"""加载并调整图片大小"""
img = Image.open(filename)
img = img.resize(size, Image.Resampling.LANCZOS)
return ImageTk.PhotoImage(img)
def create_main_frame(self):
"""创建主框架"""
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(fill=BOTH, expand=True, padx=20, pady=20)
# 标题区域
self.title_frame = ttk.Frame(self.main_frame)
self.title_frame.pack(fill=X, pady=(0, 20))
self.title_label = ttk.Label(self.title_frame, text="标准化考试系统", style="Title.TLabel")
self.title_label.pack(side=LEFT)
# 内容区域
self.content_frame = ttk.Frame(self.main_frame)
self.content_frame.pack(fill=BOTH, expand=True)
# 状态栏
self.status_var = StringVar()
self.status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=SUNKEN, anchor=W)
self.status_bar.pack(side=BOTTOM, fill=X)
def clear_content(self):
"""清除内容区域"""
for widget in self.content_frame.winfo_children():
widget.destroy()
def show_main_menu(self):
"""显示主菜单"""
self.clear_content()
self.status_var.set("就绪")
# 创建菜单按钮
menu_frame = ttk.Frame(self.content_frame)
menu_frame.pack(expand=True, pady=50)
# 教师功能
teacher_frame = ttk.LabelFrame(menu_frame, text="教师功能", padding=10)
teacher_frame.grid(row=0, column=0, padx=20, pady=10, sticky="nsew")
ttk.Button(teacher_frame, text="创建新考试",
command=self.show_create_exam, width=20).pack(pady=5)
ttk.Button(teacher_frame, text="管理考试题目",
command=self.show_manage_exams, width=20).pack(pady=5)
# 学生功能
student_frame = ttk.LabelFrame(menu_frame, text="学生功能", padding=10)
student_frame.grid(row=0, column=1, padx=20, pady=10, sticky="nsew")
ttk.Button(student_frame, text="参加考试",
command=self.show_take_exam, width=20).pack(pady=5)
ttk.Button(student_frame, text="查看考试结果",
command=self.show_exam_results, width=20).pack(pady=5)
# 系统功能
sys_frame = ttk.LabelFrame(menu_frame, text="系统功能", padding=10)
sys_frame.grid(row=0, column=2, padx=20, pady=10, sticky="nsew")
ttk.Button(sys_frame, text="数据备份",
command=self.backup_data, width=20).pack(pady=5)
ttk.Button(sys_frame, text="退出系统",
command=self.root.quit, width=20).pack(pady=5)
menu_frame.columnconfigure(0, weight=1)
menu_frame.columnconfigure(1, weight=1)
menu_frame.columnconfigure(2, weight=1)
def show_create_exam(self):
"""显示创建考试界面"""
self.clear_content()
self.status_var.set("创建新考试")
# 返回按钮
ttk.Button(self.content_frame, text="返回", command=self.show_main_menu).pack(anchor=NW, pady=5)
# 表单框架
form_frame = ttk.Frame(self.content_frame)
form_frame.pack(fill=BOTH, expand=True, padx=50, pady=20)
# 考试标题
ttk.Label(form_frame, text="考试标题:").grid(row=0, column=0, sticky=W, pady=5)
self.exam_title_entry = ttk.Entry(form_frame, width=40)
self.exam_title_entry.grid(row=0, column=1, sticky=W, pady=5)
# 考试时长
ttk.Label(form_frame, text="考试时长(分钟):").grid(row=1, column=0, sticky=W, pady=5)
self.exam_duration_spinbox = Spinbox(form_frame, from_=1, to=300, width=5)
self.exam_duration_spinbox.grid(row=1, column=1, sticky=W, pady=5)
# 创建按钮
ttk.Button(form_frame, text="创建考试", command=self.create_exam).grid(row=2, column=1, sticky=E, pady=20)
def create_exam(self):
"""创建新考试"""
title = self.exam_title_entry.get().strip()
duration = self.exam_duration_spinbox.get()
if not title:
messagebox.showerror("错误", "请输入考试标题!")
return
try:
duration = int(duration)
if duration <= 0:
raise ValueError
except ValueError:
messagebox.showerror("错误", "请输入有效的考试时长!")
return
exam_id = self.system.create_exam(title, duration)
if exam_id:
messagebox.showinfo("成功", f"考试 '{title}' 创建成功!")
self.show_add_questions(exam_id, title)
else:
messagebox.showerror("错误", f"考试 '{title}' 已存在!")
def show_add_questions(self, exam_id, exam_title):
"""显示添加问题界面"""
self.clear_content()
self.status_var.set(f"添加问题 - {exam_title}")
# 当前问题列表
self.questions = []
self.current_question = 0
# 返回按钮
ttk.Button(self.content_frame, text="返回", command=self.show_main_menu).pack(anchor=NW, pady=5)
# 标题
ttk.Label(self.content_frame, text=f"为考试 '{exam_title}' 添加问题",
style="Header.TLabel").pack(pady=10)
# 问题表单框架
self.question_form_frame = ttk.Frame(self.content_frame)
self.question_form_frame.pack(fill=BOTH, expand=True, padx=50, pady=10)
# 问题类型
ttk.Label(self.question_form_frame, text="问题类型:").grid(row=0, column=0, sticky=W, pady=5)
self.question_type_var = StringVar(value="single")
ttk.Combobox(self.question_form_frame, textvariable=self.question_type_var,
values=["single", "multiple", "essay"], state="readonly", width=10).grid(row=0, column=1, sticky=W, pady=5)
# 问题文本
ttk.Label(self.question_form_frame, text="问题内容:").grid(row=1, column=0, sticky=NW, pady=5)
self.question_text_entry = Text(self.question_form_frame, width=50, height=3, wrap=WORD)
self.question_text_entry.grid(row=1, column=1, sticky=W, pady=5)
# 选项框架 (仅对选择题显示)
self.options_frame = ttk.LabelFrame(self.question_form_frame, text="选项", padding=10)
self.options_frame.grid(row=2, column=0, columnspan=2, sticky="we", pady=5)
# 动态添加选项控件
self.option_entries = []
self.add_option_entry()
# 添加选项按钮
ttk.Button(self.options_frame, text="添加选项", command=self.add_option_entry).pack(side=LEFT, padx=5)
# 正确答案
ttk.Label(self.question_form_frame, text="正确答案:").grid(row=3, column=0, sticky=W, pady=5)
self.correct_answer_entry = ttk.Entry(self.question_form_frame, width=40)
self.correct_answer_entry.grid(row=3, column=1, sticky=W, pady=5)
# 按钮框架
button_frame = ttk.Frame(self.question_form_frame)
button_frame.grid(row=4, column=0, columnspan=2, pady=20)
ttk.Button(button_frame, text="保存问题", command=lambda: self.save_question(exam_id)).pack(side=LEFT, padx=10)
ttk.Button(button_frame, text="完成", command=self.show_main_menu).pack(side=LEFT, padx=10)
# 绑定问题类型变化事件
self.question_type_var.trace_add("write", self.update_question_form)
def add_option_entry(self):
"""添加一个选项输入框"""
frame = ttk.Frame(self.options_frame)
frame.pack(fill=X, pady=2)
var = StringVar()
entry = ttk.Entry(frame, textvariable=var, width=40)
entry.pack(side=LEFT, padx=5)
# 删除按钮
ttk.Button(frame, text="×", width=2, command=lambda: frame.destroy()).pack(side=LEFT)
self.option_entries.append((var, entry))
def update_question_form(self, *args):
"""根据问题类型更新表单"""
q_type = self.question_type_var.get()
if q_type in ["single", "multiple"]:
self.options_frame.grid()
else:
self.options_frame.grid_remove()
def save_question(self, exam_id):
"""保存问题到数据库"""
q_type = self.question_type_var.get()
q_text = self.question_text_entry.get("1.0", END).strip()
correct_answer = self.correct_answer_entry.get().strip()
if not q_text:
messagebox.showerror("错误", "请输入问题内容!")
return
options = []
if q_type in ["single", "multiple"]:
for var, entry in self.option_entries:
option_text = var.get().strip()
if option_text:
options.append(option_text)
if not options:
messagebox.showerror("错误", "请至少添加一个选项!")
return
if not correct_answer:
messagebox.showerror("错误", "请输入正确答案!")
return
# 保存到数据库
question_id = self.system.add_question(exam_id, q_text, q_type, options, correct_answer)
if question_id:
messagebox.showinfo("成功", "问题添加成功!")
self.clear_question_form()
else:
messagebox.showerror("错误", "问题添加失败!")
def clear_question_form(self):
"""清除问题表单"""
self.question_text_entry.delete("1.0", END)
self.correct_answer_entry.delete(0, END)
# 清除所有选项
for child in self.options_frame.winfo_children():
child.destroy()
# 重新添加一个空选项
self.option_entries = []
self.add_option_entry()
def show_manage_exams(self):
"""显示管理考试界面"""
self.clear_content()
self.status_var.set("管理考试")
# 返回按钮
ttk.Button(self.content_frame, text="返回", command=self.show_main_menu).pack(anchor=NW, pady=5)
# 标题
ttk.Label(self.content_frame, text="管理考试", style="Header.TLabel").pack(pady=10)
# 考试列表
exams = self.system.get_exam_list()
if not exams:
ttk.Label(self.content_frame, text="没有可用的考试").pack(pady=20)
return
# 创建Treeview显示考试列表
tree_frame = ttk.Frame(self.content_frame)
tree_frame.pack(fill=BOTH, expand=True, padx=20, pady=10)
columns = ("id", "title", "duration", "created_at")
self.exam_tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="browse")
# 设置列标题
self.exam_tree.heading("id", text="ID")
self.exam_tree.heading("title", text="考试标题")
self.exam_tree.heading("duration", text="时长(分钟)")
self.exam_tree.heading("created_at", text="创建时间")
# 设置列宽
self.exam_tree.column("id", width=50, anchor=CENTER)
self.exam_tree.column("title", width=200)
self.exam_tree.column("duration", width=80, anchor=CENTER)
self.exam_tree.column("created_at", width=150, anchor=CENTER)
# 添加滚动条
scrollbar = ttk.Scrollbar(tree_frame, orient=VERTICAL, command=self.exam_tree.yview)
self.exam_tree.configure(yscrollcommand=scrollbar.set)
self.exam_tree.pack(side=LEFT, fill=BOTH, expand=True)
scrollbar.pack(side=RIGHT, fill=Y)
# 添加考试数据
for exam in exams:
self.exam_tree.insert("", END, values=(
exam["id"],
exam["title"],
exam["duration"],
exam["created_at"]
))
# 按钮框架
button_frame = ttk.Frame(self.content_frame)
button_frame.pack(pady=10)
ttk.Button(button_frame, text="查看问题", command=self.view_exam_questions).pack(side=LEFT, padx=5)
ttk.Button(button_frame, text="删除考试", command=self.delete_exam).pack(side=LEFT, padx=5)
def view_exam_questions(self):
"""查看考试的问题"""
selected = self.exam_tree.selection()
if not selected:
messagebox.showwarning("警告", "请先选择一个考试!")
return
item = self.exam_tree.item(selected[0])
exam_id, exam_title = item["values"][0], item["values"][1]
self.clear_content()
self.status_var.set(f"查看问题 - {exam_title}")
# 返回按钮
ttk.Button(self.content_frame, text="返回", command=self.show_manage_exams).pack(anchor=NW, pady=5)
# 标题
ttk.Label(self.content_frame, text=f"考试 '{exam_title}' 的问题",
style="Header.TLabel").pack(pady=10)
# 获取问题列表
questions = self.system.get_exam_questions(exam_id)
if not questions:
ttk.Label(self.content_frame, text="这个考试还没有问题").pack(pady=20)
return
# 创建问题显示框架
canvas = Canvas(self.content_frame)
scrollbar = ttk.Scrollbar(self.content_frame, orient=VERTICAL, command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(
scrollregion=canvas.bbox("all")
)
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side=LEFT, fill=BOTH, expand=True)
scrollbar.pack(side=RIGHT, fill=Y)
# 显示每个问题
for idx, question in enumerate(questions, 1):
q_frame = ttk.LabelFrame(scrollable_frame, text=f"问题 {idx}", padding=10)
q_frame.pack(fill=X, padx=5, pady=5)
# 问题内容
ttk.Label(q_frame, text=question["question_text"],
style="Question.TLabel").pack(anchor=W)
# 问题类型
ttk.Label(q_frame, text=f"类型: {question['question_type']}").pack(anchor=W)
# 显示选项
if question["options"]:
options = json.loads(question["options"])
ttk.Label(q_frame, text="选项:").pack(anchor=W)
for opt in options:
ttk.Label(q_frame, text=f"- {opt}").pack(anchor=W, padx=20)
# 正确答案
ttk.Label(q_frame, text=f"正确答案: {question['correct_answer']}").pack(anchor=W)
def delete_exam(self):
"""删除考试"""
selected = self.exam_tree.selection()
if not selected:
messagebox.showwarning("警告", "请先选择一个考试!")
return
item = self.exam_tree.item(selected[0])
exam_id, exam_title = item["values"][0], item["values"][1]
confirm = messagebox.askyesno("确认", f"确定要删除考试 '{exam_title}' 吗?")
if confirm:
self.db.delete_exam(exam_id)
messagebox.showinfo("成功", f"考试 '{exam_title}' 已删除!")
self.show_manage_exams()
def show_take_exam(self):
"""显示参加考试界面"""
self.clear_content()
self.status_var.set("参加考试")
# 返回按钮
ttk.Button(self.content_frame, text="返回", command=self.show_main_menu).pack(anchor=NW, pady=5)
# 标题
ttk.Label(self.content_frame, text="选择考试", style="Header.TLabel").pack(pady=10)
# 学生姓名输入
name_frame = ttk.Frame(self.content_frame)
name_frame.pack(pady=10)
ttk.Label(name_frame, text="您的姓名:").pack(side=LEFT, padx=5)
self.student_name_entry = ttk.Entry(name_frame, width=20)
self.student_name_entry.pack(side=LEFT, padx=5)
# 考试列表
exams = self.system.get_exam_list()
if not exams:
ttk.Label(self.content_frame, text="没有可用的考试").pack(pady=20)
return
# 创建Treeview显示考试列表
tree_frame = ttk.Frame(self.content_frame)
tree_frame.pack(fill=BOTH, expand=True, padx=20, pady=10)
columns = ("id", "title", "duration")
self.exam_tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="browse")
# 设置列标题
self.exam_tree.heading("id", text="ID")
self.exam_tree.heading("title", text="考试标题")
self.exam_tree.heading("duration", text="时长(分钟)")
# 设置列宽
self.exam_tree.column("id", width=50, anchor=CENTER)
self.exam_tree.column("title", width=300)
self.exam_tree.column("duration", width=100, anchor=CENTER)
# 添加滚动条
scrollbar = ttk.Scrollbar(tree_frame, orient=VERTICAL, command=self.exam_tree.yview)
self.exam_tree.configure(yscrollcommand=scrollbar.set)
self.exam_tree.pack(side=LEFT, fill=BOTH, expand=True)
scrollbar.pack(side=RIGHT, fill=Y)
# 添加考试数据
for exam in exams:
self.exam_tree.insert("", END, values=(
exam["id"],
exam["title"],
exam["duration"]
))
# 开始考试按钮
ttk.Button(self.content_frame, text="开始考试", command=self.start_selected_exam).pack(pady=10)
def start_selected_exam(self):
"""开始选择的考试"""
selected = self.exam_tree.selection()
if not selected:
messagebox.showwarning("警告", "请先选择一个考试!")
return
student_name = self.student_name_entry.get().strip()
if not student_name:
messagebox.showwarning("警告", "请输入您的姓名!")
return
item = self.exam_tree.item(selected[0])
exam_id, exam_title, duration = item["values"][0], item["values"][1], item["values"][2]
# 开始考试
record_id = self.system.start_exam(exam_id, student_name)
if record_id:
self.show_exam_questions(exam_id, exam_title, duration, record_id)
else:
messagebox.showerror("错误", "无法开始考试!")
def show_exam_questions(self, exam_id, exam_title, duration, record_id):
"""显示考试问题界面"""
self.clear_content()
self.status_var.set(f"正在考试: {exam_title}")
# 考试信息
info_frame = ttk.Frame(self.content_frame)
info_frame.pack(fill=X, padx=10, pady=10)
ttk.Label(info_frame, text=f"考试: {exam_title}", style="Header.TLabel").pack(side=LEFT)
self.time_label = ttk.Label(info_frame, text=f"剩余时间: {duration}:00")
self.time_label.pack(side=RIGHT, padx=10)
# 获取问题
questions = self.system.get_exam_questions(exam_id)
if not questions:
messagebox.showerror("错误", "这个考试没有问题!")
self.show_main_menu()
return
# 创建问题导航
self.current_question_index = 0
self.questions = questions
self.record_id = record_id
self.exam_duration = int(duration) * 60 # 转换为秒
self.start_time = time.time()
self.end_time = self.start_time + self.exam_duration
# 创建问题显示区域
self.question_display_frame = ttk.Frame(self.content_frame)
self.question_display_frame.pack(fill=BOTH, expand=True, padx=20, pady=10)
# 创建导航按钮
nav_frame = ttk.Frame(self.content_frame)
nav_frame.pack(fill=X, pady=10)
ttk.Button(nav_frame, text="上一题", command=self.prev_question).pack(side=LEFT, padx=10)
ttk.Button(nav_frame, text="下一题", command=self.next_question).pack(side=LEFT, padx=10)
ttk.Button(nav_frame, text="提交考试", command=self.submit_exam).pack(side=RIGHT, padx=10)
# 显示第一个问题
self.display_current_question()
# 启动计时器
self.update_timer()
def display_current_question(self):
"""显示当前问题"""
# 清除之前的显示
for widget in self.question_display_frame.winfo_children():
widget.destroy()
question = self.questions[self.current_question_index]
# 问题编号
ttk.Label(self.question_display_frame,
text=f"问题 {self.current_question_index + 1}/{len(self.questions)}",
style="Header.TLabel").pack(anchor=W, pady=5)
# 问题内容
ttk.Label(self.question_display_frame,
text=question["question_text"],
style="Question.TLabel").pack(anchor=W, pady=10)
# 根据问题类型显示不同输入方式
if question["question_type"] in ["single", "multiple"]:
self.display_options_question(question)
else:
self.display_essay_question(question)
def display_options_question(self, question):
"""显示选择题"""
options = json.loads(question["options"])
self.answer_var = StringVar()
for idx, option in enumerate(options):
rb = ttk.Radiobutton(
self.question_display_frame,
text=option,
variable=self.answer_var,
value=chr(65 + idx), # A, B, C, ...
style="Option.TRadiobutton"
)
rb.pack(anchor=W, pady=2)
def display_essay_question(self, question):
"""显示问答题"""
self.essay_answer = Text(self.question_display_frame, width=60, height=10, wrap=WORD)
self.essay_answer.pack(fill=BOTH, expand=True, pady=10)
def prev_question(self):
"""显示上一题"""
if self.current_question_index > 0:
self.save_current_answer()
self.current_question_index -= 1
self.display_current_question()
def next_question(self):
"""显示下一题"""
if self.current_question_index < len(self.questions) - 1:
self.save_current_answer()
self.current_question_index += 1
self.display_current_question()
def save_current_answer(self):
"""保存当前问题的答案"""
question = self.questions[self.current_question_index]
q_type = question["question_type"]
answer = None
is_correct = 0 # 0-错误,1-正确(客观题自动判断)
if q_type in ["single", "multiple"]:
# 选择题答案
selected = self.answer_var.get()
answer = selected or ""
if answer:
# 自动判断是否正确(仅单选题)
if q_type == "single" and answer == question["correct_answer"]:
is_correct = 1
else:
# 问答题答案
answer = self.essay_answer.get("1.0", END).strip()
# 主观题需人工判分,此处暂不判断正确性
if answer or q_type == "essay": # 允许空答案(如未作答)
self.system.submit_answer(
question_id=question["id"],
answer=answer,
is_correct=is_correct
)
def update_timer(self):
"""更新剩余时间显示"""
elapsed = time.time() - self.start_time
remaining = max(0, self.exam_duration - int(elapsed))
if remaining <= 0:
self.submit_exam()
return
# 当剩余时间少于5分钟时显示警告颜色
if remaining <= 300: # 300秒 = 5分钟
self.time_label.configure(foreground="red")
else:
self.time_label.configure(foreground="black")
minutes, seconds = divmod(remaining, 60)
self.time_label.config(text=f"剩余时间: {int(minutes):02d}:{int(seconds):02d}")
self.root.after(1000, self.update_timer) # 每秒更新
def submit_exam(self):
"""提交考试"""
# 保存最后一题答案
self.save_current_answer()
# 计算得分
total = len(self.questions)
cursor = self.db.conn.cursor()
cursor.execute(
"SELECT is_correct FROM student_answers WHERE exam_record_id=?",
(self.record_id,)
)
correct = sum(row[0] for row in cursor.fetchall())
score = int((correct / total) * 100) if total else 0
# 完成考试
self.system.finish_exam(score, total)
# 显示结果
self.show_exam_result(self.record_id)
def show_exam_result(self, record_id):
"""显示考试结果"""
self.clear_content()
record, questions = self.system.get_exam_result(record_id)
if not record:
messagebox.showerror("错误", "未找到考试记录!")
self.show_main_menu()
return
# 结果标题
ttk.Label(self.content_frame, text="考试结果", style="Header.TLabel").pack(pady=20)
# 基本信息
info_frame = ttk.Frame(self.content_frame)
info_frame.pack(pady=10)
ttk.Label(info_frame, text=f"考试名称: {record['title']}").pack(side=LEFT, padx=10)
ttk.Label(info_frame, text=f"考生姓名: {record['student_name']}").pack(side=LEFT, padx=10)
ttk.Label(info_frame, text=f"得分: {record['score']} / {record['total_questions']}").pack(side=LEFT, padx=10)
# 问题详情
for idx, q in enumerate(questions, 1):
frame = ttk.LabelFrame(self.content_frame, text=f"问题 {idx}", padding=10)
frame.pack(fill=X, padx=5, pady=5)
# 问题内容
ttk.Label(frame, text=q["question_text"], style="Question.TLabel").pack(anchor=W)
# 考生答案
if q["question_type"] in ["single", "multiple"]:
answer = q["answer"] or "未作答"
ttk.Label(frame, text=f"你的答案: {answer}").pack(anchor=W)
else:
answer = q["answer"] or "未作答"
ttk.Label(frame, text=f"你的答案:\n{answer}", wraplength=500).pack(anchor=W)
# 正确性判断
correct = "正确" if q["is_correct"] else "错误"
ttk.Label(frame, text=f"正确性: {correct}",
foreground="green" if q["is_correct"] else "red").pack(anchor=W)
# 正确答案
if q["question_type"] != "essay":
ttk.Label(frame, text=f"正确答案: {q['correct_answer']}").pack(anchor=W)
# 返回按钮
ttk.Button(self.content_frame, text="返回主菜单", command=self.show_main_menu).pack(pady=20)
def show_exam_results(self):
"""显示所有考试结果"""
self.clear_content()
self.status_var.set("查看考试结果")
# 返回按钮
ttk.Button(self.content_frame, text="返回", command=self.show_main_menu).pack(anchor=NW, pady=5)
# 标题
ttk.Label(self.content_frame, text="考试结果列表", style="Header.TLabel").pack(pady=10)
# 获取结果数据
results = self.system.get_all_results()
if not results:
ttk.Label(self.content_frame, text="没有已完成的考试结果").pack(pady=20)
return
# 创建Treeview显示结果
tree_frame = ttk.Frame(self.content_frame)
tree_frame.pack(fill=BOTH, expand=True, padx=20, pady=10)
columns = ("id", "考试名称", "考生姓名", "得分", "总分", "开始时间", "结束时间")
self.result_tree = ttk.Treeview(tree_frame, columns=columns, show="headings", selectmode="browse")
# 设置列标题
self.result_tree.heading("id", text="ID")
self.result_tree.heading("考试名称", text="考试名称")
self.result_tree.heading("考生姓名", text="考生姓名")
self.result_tree.heading("得分", text="得分")
self.result_tree.heading("总分", text="总分")
self.result_tree.heading("开始时间", text="开始时间")
self.result_tree.heading("结束时间", text="结束时间")
# 设置列宽
self.result_tree.column("id", width=50, anchor=CENTER)
self.result_tree.column("考试名称", width=200)
self.result_tree.column("考生姓名", width=150)
self.result_tree.column("得分", width=80, anchor=CENTER)
self.result_tree.column("总分", width=80, anchor=CENTER)
self.result_tree.column("开始时间", width=150)
self.result_tree.column("结束时间", width=150)
# 添加滚动条
scrollbar = ttk.Scrollbar(tree_frame, orient=VERTICAL, command=self.result_tree.yview)
self.result_tree.configure(yscrollcommand=scrollbar.set)
self.result_tree.pack(side=LEFT, fill=BOTH, expand=True)
scrollbar.pack(side=RIGHT, fill=Y)
# 添加数据
for res in results:
self.result_tree.insert("", END, values=(
res["id"],
res["title"],
res["student_name"],
res["score"],
res["total_questions"],
res["start_time"],
res["end_time"]
))
# 查看详情按钮
ttk.Button(self.content_frame, text="查看详情", command=self.view_result_details).pack(pady=10)
def view_result_details(self):
"""查看考试结果详情"""
selected = self.result_tree.selection()
if not selected:
messagebox.showwarning("警告", "请先选择一个考试结果!")
return
item = self.result_tree.item(selected[0])
record_id = item["values"][0]
self.show_exam_result(record_id)
def backup_data(self):
"""数据备份"""
file_path = filedialog.asksaveasfilename(
defaultextension=".db",
filetypes=[("SQLite数据库", "*.db"), ("所有文件", "*.*")]
)
if not file_path:
return
try:
# 复制数据库文件
shutil.copyfile(self.db.db_file, file_path)
messagebox.showinfo("成功", "数据备份完成!")
except Exception as e:
messagebox.showerror("错误", f"备份失败: {str(e)}")
if name == "main":
root = Tk()
app = ExamSystemGUI(root)
root.mainloop()

浙公网安备 33010602011771号