v5.0 #2

Merged
hnu202326010326 merged 2 commits from develop into main 4 months ago

@ -0,0 +1,8 @@
{
"python.linting.flake8Enabled": true,
"python.formatting.provider": "yapf",
"python.linting.flake8Args": ["--max-line-length=248"],
"python.linting.pylintEnabled": false,
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": []
}

Binary file not shown.

@ -1,14 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
单文件桌面应用小初高数学练习含注册/邮箱验证码密码规则题目自动生成
修改点
- 每道题操作数数量为 1-5随机操作数取值 1-100
- 登录界面支持 使用 用户名 邮箱 登录
- 其它功能保留注册邮箱验证码激活并设置密码修改密码选择学段与题量自动生成题目逐题答题评分
注意
- 请替换 SMTP 配置为有效账户或在无法发送邮件时调整 send_verification_email 为测试模式
- Python 3.8+
单文件桌面应用小初高数学练习Tkinter
主要修改按用户要求
- 每道题中出现的数字不超过 5 操作数数目为 1~5
- 小学题不出现负数操作数为正生成表达式尽量避免负结果
- 初中题题目显示使用根号符号 (...)幂显示为 x^2内部仍用 sqrt()/** 计算
- 高中题题目中不出现 pi 字符串三角函数以角度表示 sin(30°)内部计算将角度转为弧度
其它功能保留注册邮箱验证码用户名/邮箱登录修改密码题库自动生成与保存PBKDF2 密码哈希等
注意替换 SMTP 配置为可用的邮箱/授权码或改为测试模式
"""
import tkinter as tk
@ -51,7 +51,7 @@ WINDOW_H = 620
# 题目生成参数
MIN_OPERANDS = 1
MAX_OPERANDS = 5
MAX_OPERANDS = 5 # 至多 5 个操作数(数字)
OPERAND_MIN = 1
OPERAND_MAX = 100
# --------------------------------------
@ -62,11 +62,9 @@ def log(msg: str):
f.write(f"{datetime.utcnow().isoformat()} {msg}\n")
def ensure_files_exist():
# users file
if not os.path.exists(USERS_FILE):
with open(USERS_FILE, "w", encoding="utf-8") as f:
json.dump([], f, ensure_ascii=False, indent=2)
# question files
for k, p in QUESTION_FILES.items():
if not os.path.exists(p):
with open(p, "w", encoding="utf-8") as f:
@ -102,7 +100,6 @@ class UserStore:
return None
def find(self, key: str):
# try email first, then username
u = self.find_by_email(key)
if u:
return u
@ -158,7 +155,6 @@ class VerificationManager:
return False, "验证码已过期"
if not secrets.compare_digest(real, code):
return False, "验证码不匹配"
# 删除以防二次使用
del self.store[email]
return True, "验证成功"
@ -180,7 +176,6 @@ def verify_password_hash(stored_hash, stored_salt, attempt):
# ----------------- SMTP 发送 -----------------
def send_verification_email(target_email: str, code: str):
"""同步发送;上层在线程中调用"""
msg = MIMEText(f"您的注册码6位{code}\n有效期 {CODE_TTL_SECONDS//60} 分钟。")
msg['Subject'] = "注册码"
msg['From'] = SENDER_EMAIL
@ -207,21 +202,20 @@ def send_verification_email(target_email: str, code: str):
log(f"Failed send code to {target_email}: {e}")
return False, str(e)
# ----------------- 题目生成1~5 个操作数,操作数范围 1~100 -----------------
# ----------------- 题目生成相关辅助 -----------------
def gen_id(prefix: str) -> str:
return f"{prefix}_{secrets.token_hex(4)}"
def mk_options_from_number(correct_val, kind="int"):
# produce 4 unique options including correct
opts = []
if kind == "int":
corr = int(round(correct_val))
opts = [str(corr)]
attempts = 0
while len(opts) < 4 and attempts < 100:
delta = random.choice([-10, -6, -3, -2, -1, 1, 2, 3, 5, 8, 10])
while len(opts) < 4 and attempts < 200:
delta = random.choice([-10, -6, -4, -3, -2, -1, 1, 2, 3, 4, 5, 8, 10])
candidate = corr + delta
if str(candidate) not in opts:
if candidate >= -9999 and str(candidate) not in opts:
opts.append(str(candidate))
attempts += 1
while len(opts) < 4:
@ -229,7 +223,6 @@ def mk_options_from_number(correct_val, kind="int"):
random.shuffle(opts)
return opts, opts.index(str(corr))
else:
# float
corr = float(correct_val)
corr_s = f"{corr:.3f}"
opts = [corr_s]
@ -246,180 +239,206 @@ def mk_options_from_number(correct_val, kind="int"):
random.shuffle(opts)
return opts, opts.index(corr_s)
# 安全评估:表达式由程序生成,使用受限 eval 环境评估
# 受限 eval 环境用于计算(内部使用 math 的函数)
SAFE_EVAL_ENV = {"__builtins__": None, "sqrt": math.sqrt, "sin": math.sin, "cos": math.cos, "tan": math.tan, "pi": math.pi}
def safe_eval(expr: str):
# 只包含数字、运算符、sqrt, sin, cos, tan, (, ), ^ 替换为 **
# expr 为 Python 可 eval 的表达式(外层 display 与内部计算分开)
expr = expr.replace("^", "**")
return eval(expr, SAFE_EVAL_ENV, {})
# 小学题1~5 个操作数,运算符 + - * /,可能带一对括号
# ----------------- 题目生成(符合新要求) -----------------
# 小学1~5 个操作数,操作数为正,尽量避免负数结果
def generate_primary_question():
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS) # number of operands
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
# Build expression, maybe with parentheses
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
if n_ops >= 2 and random.choice([True, False]):
idx = random.randint(0, n_ops - 2)
# 生成 1~5 个正整数操作数
n = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)]
# 选择运算符n-1 个)
ops_list = []
for _ in range(max(0, n - 1)):
ops_list.append(random.choice(['+', '-', '*', '/']))
# 为避免出现负数结果,策略:
# - 对于减法,确保左边操作数大于或等于右边(在构建顺序时调整)
# - 对于除法,尽量设置可整除关系(将右边替换为因数)
# 构造表达式逐步调整
def build_expr(nums, ops):
parts = []
for i in range(n_ops):
p = str(nums[i])
if i == idx:
p = "(" + p
if i == idx + 1:
p = p + ")"
parts.append(p)
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
# evaluate and prefer integer
val = None
for _ in range(60):
for i, a in enumerate(nums):
parts.append(str(a))
if i < len(ops):
parts.append(ops[i])
return "".join(parts)
# 调整减法与除法以尽量得到非负整数
for _ in range(100):
nums_copy = nums[:]
ops_copy = ops_list[:]
# adjust for division: make divisor divide dividend when possible
for i, op in enumerate(ops_copy):
if op == '/':
a = nums_copy[i]
b = nums_copy[i+1]
if b == 0:
nums_copy[i+1] = random.randint(OPERAND_MIN, OPERAND_MAX)
# try to set b as a divisor of a by replacing b
divisors = [d for d in range(1, OPERAND_MAX+1) if a % d == 0]
if divisors:
nums_copy[i+1] = random.choice(divisors)
else:
# fallback: change op to + to avoid fractions
ops_copy[i] = random.choice(['+', '-','*'])
# adjust for subtraction to avoid negatives: ensure left >= right when directly subtracting
for i, op in enumerate(ops_copy):
if op == '-':
if nums_copy[i] < nums_copy[i+1]:
# swap operands to attempt non-negative
nums_copy[i], nums_copy[i+1] = nums_copy[i+1], nums_copy[i]
expr = build_expr(nums_copy, ops_copy)
try:
v = safe_eval(expr)
if v is None or isinstance(v, complex):
raise ValueError
if isinstance(v, float):
v = int(round(v))
val = v
break
val = safe_eval(expr)
# if float, round to int
if isinstance(val, float):
if abs(val - round(val)) < 1e-9:
val = int(round(val))
else:
val = int(round(val))
# ensure non-negative (小学要求)
if isinstance(val, (int, float)) and val >= 0:
nums = nums_copy
ops_list = ops_copy
break
except Exception:
# regenerate simpler
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
if val is None:
# regenerate small changes and retry
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n - 1))]
continue
# final expression
expr_display = build_expr(nums, ops_list)
# Evaluate final value
try:
val = safe_eval(expr_display)
if isinstance(val, float):
if abs(val - round(val)) < 1e-9:
val = int(round(val))
else:
val = int(round(val))
except Exception:
val = 0
opts, idx = mk_options_from_number(val, kind="int")
return {"id": gen_id("pri"), "grade": "小学", "stem": f"{expr} = ?", "options": opts, "answer_index": idx}
# 初中题:至少包含平方或开根号 sqrt(...)
# Ensure no negative options for primary (replace negatives)
opts = [o if not (o.startswith("-")) else str(abs(int(o))) for o in opts]
# ensure answer_index updated if changed due to replacement
if str(val) in opts:
answer_index = opts.index(str(val))
else:
# if val changed by making negative->positive, recompute index
answer_index = 0
for i,o in enumerate(opts):
if int(o) == int(val):
answer_index = i
break
return {"id": gen_id("pri"), "grade": "小学", "stem": f"{expr_display} = ?", "options": opts, "answer_index": answer_index}
# 初中1~5 个操作数,题目显示中用 √(...)表示根号,幂显示为 x^2
def generate_middle_question():
n = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n - 1))]
# ensure at least one sqrt or square
use_sqrt = random.choice([True, False])
use_square = not use_sqrt if random.random() < 0.3 else random.choice([True, False])
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
idx = random.randint(0, n_ops - 1)
idx = random.randint(0, n - 1)
expr_parts = []
# prepare a copy for computation (python expression)
comp_nums = [str(x) for x in nums]
if use_sqrt:
# try to use perfect square inside sometimes
inside = random.randint(1, 50)
if random.choice([True, False]):
nums[idx] = f"sqrt({inside*inside})"
else:
nums[idx] = f"sqrt({nums[idx]})"
# prefer perfect square inside sometimes
inner_base = random.randint(1, 50)
inner = inner_base * inner_base if random.choice([True, False]) else nums[idx]*nums[idx]
comp_nums[idx] = f"sqrt({inner})"
display_num = f"√({inner})"
else:
nums[idx] = f"({nums[idx]}**2)"
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
comp_nums[idx] = f"({nums[idx]}**2)"
display_num = f"{nums[idx]}^2"
# build display expression and compute expression separately
display_parts = []
comp_parts = []
for i in range(n):
display_parts.append(display_num if i == idx else str(nums[i]))
comp_parts.append(comp_nums[i])
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
val = None
for _ in range(60):
try:
v = safe_eval(expr)
if v is None or isinstance(v, complex):
raise ValueError
val = v
break
except Exception:
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
idx = random.randint(0, n_ops - 1)
if random.choice([True, False]):
nums[idx] = f"sqrt({nums[idx]*nums[idx]})"
else:
nums[idx] = f"({nums[idx]}**2)"
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
if val is None:
val = 0
display_parts.append(ops_list[i])
comp_parts.append(ops_list[i])
display_expr = "".join(display_parts)
comp_expr = "".join(comp_parts)
# evaluate
try:
val = safe_eval(comp_expr)
except Exception:
# fallback to simpler expression
comp_expr = "1+1"
display_expr = "1+1"
val = 2
# format options
if isinstance(val, float) and abs(val - round(val)) > 1e-9:
opts, idx = mk_options_from_number(val, kind="float")
opts, idx_ans = mk_options_from_number(val, kind="float")
else:
opts, idx = mk_options_from_number(val, kind="int")
return {"id": gen_id("mid"), "grade": "初中", "stem": f"{expr} = ?", "options": opts, "answer_index": idx}
opts, idx_ans = mk_options_from_number(val, kind="int")
return {"id": gen_id("mid"), "grade": "初中", "stem": f"{display_expr} = ?", "options": opts, "answer_index": idx_ans}
# 高中题:至少包含 sin/cos/tan使用角度并可包含 1~5 个操作数
# 高中1~5 个操作数,三角函数以角度表示(如 sin(30°)),题目显示不含 pi
def generate_high_question():
func = random.choice(["sin", "cos", "tan"])
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
idx = random.randint(0, n_ops - 1)
n = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n - 1))]
idx = random.randint(0, n - 1)
# choose angle in degrees (常见角优先)
angle = random.choice([0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330])
nums[idx] = f"{func}(pi*{angle}/180)"
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
# comp expression uses radians: func(pi*angle/180)
comp_nums = [str(x) for x in nums]
comp_nums[idx] = f"{func}(pi*{angle}/180)"
display_nums = [str(x) for x in nums]
display_nums[idx] = f"{func}({angle}°)" # show degrees, no pi
# build expressions
comp_parts = []
display_parts = []
for i in range(n):
comp_parts.append(comp_nums[i])
display_parts.append(display_nums[i])
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
val = None
for _ in range(60):
try:
v = safe_eval(expr)
if v is None or isinstance(v, complex):
raise ValueError
val = v
break
except Exception:
n_ops = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n_ops)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n_ops - 1))]
idx = random.randint(0, n_ops - 1)
angle = random.choice([30,45,60,90,120])
func = random.choice(["sin", "cos", "tan"])
nums[idx] = f"{func}(pi*{angle}/180)"
parts = []
for i in range(n_ops):
parts.append(str(nums[i]))
if i < len(ops_list):
parts.append(ops_list[i])
expr = "".join(parts)
if val is None:
comp_parts.append(ops_list[i])
display_parts.append(ops_list[i])
comp_expr = "".join(comp_parts)
display_expr = "".join(display_parts)
# evaluate
try:
val = safe_eval(comp_expr)
except Exception:
# fallback
comp_expr = "0.0"
display_expr = f"{func}({angle}°)"
val = 0.0
opts, idx = mk_options_from_number(val, kind="float")
stem = f"{expr} = ? (保留3位小数)"
return {"id": gen_id("high"), "grade": "高中", "stem": stem, "options": opts, "answer_index": idx}
# options as float (3 decimals)
opts, idx_ans = mk_options_from_number(val, kind="float")
return {"id": gen_id("high"), "grade": "高中", "stem": f"{display_expr} = ? (保留3位小数)", "options": opts, "answer_index": idx_ans}
# Ensure enough questions in file; auto-generate and append if insufficient
# auto-generate to ensure enough questions
def ensure_questions_for_grade(grade: str, needed: int, qstore: QuestionStore):
qs = qstore.load(grade)
if len(qs) >= needed:
return qs
to_gen = needed - len(qs)
gen = []
new = []
for _ in range(to_gen):
if grade == "小学":
gen.append(generate_primary_question())
new.append(generate_primary_question())
elif grade == "初中":
gen.append(generate_middle_question())
new.append(generate_middle_question())
else:
gen.append(generate_high_question())
qs.extend(gen)
new.append(generate_high_question())
qs.extend(new)
qstore.save(grade, qs)
log(f"Auto-generated {len(gen)} questions for {grade}")
log(f"Auto-generated {len(new)} questions for {grade}")
return qs
# ----------------- 服务逻辑(注册/激活/登录/改密) -----------------
@ -442,7 +461,6 @@ class AuthService:
return False, "邮箱格式不正确"
if not username:
return False, "用户名不能为空"
# check username uniqueness
existing_user = self.store.find_by_username(username)
if existing_user and existing_user.get("email") != email:
return False, "用户名已被占用"
@ -465,7 +483,6 @@ class AuthService:
return True, "注册并设置密码成功"
def login(self, key: str, password: str):
# key can be email or username
u = self.store.find(key)
if not u:
return False, "未找到该用户"
@ -499,7 +516,7 @@ class MathApp:
self.user_store = UserStore()
self.qstore = QuestionStore()
self.auth = AuthService(self.user_store)
self.current_user = None # user dict
self.current_user = None
self.current_exam = None
self.build_main()
@ -516,54 +533,44 @@ class MathApp:
tk.Button(frm, text="登录", width=40, command=self.ui_login).pack(pady=6)
tk.Button(frm, text="退出", width=40, command=self.root.quit).pack(pady=6)
# ----- 注册流程 -----
# 注册流程
def ui_register_flow(self):
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
tk.Label(fr, text="邮箱(注册邮箱):").grid(row=0, column=0, sticky="e")
email_var = tk.StringVar()
email_entry = tk.Entry(fr, textvariable=email_var, width=36)
email_entry.grid(row=0, column=1, padx=6)
send_btn = tk.Button(fr, text="发送验证码")
send_btn.grid(row=0, column=2, padx=6)
tk.Label(fr, text="验证码:").grid(row=1, column=0, sticky="e")
code_var = tk.StringVar()
code_entry = tk.Entry(fr, textvariable=code_var, width=20)
code_entry.grid(row=1, column=1, padx=6)
tk.Label(fr, text="用户名:").grid(row=2, column=0, sticky="e")
username_var = tk.StringVar()
username_entry = tk.Entry(fr, textvariable=username_var, width=30)
username_entry.grid(row=2, column=1, padx=6)
tk.Label(fr, text="密码:").grid(row=3, column=0, sticky="e")
pw_var = tk.StringVar()
pw_entry = tk.Entry(fr, textvariable=pw_var, show="*", width=30)
pw_entry.grid(row=3, column=1, padx=6)
tk.Label(fr, text="确认密码:").grid(row=4, column=0, sticky="e")
pw2_var = tk.StringVar()
pw2_entry = tk.Entry(fr, textvariable=pw2_var, show="*", width=30)
pw2_entry.grid(row=4, column=1, padx=6)
btn_fr = tk.Frame(fr)
btn_fr.grid(row=6, column=0, columnspan=3, pady=18)
tk.Button(btn_fr, text="确定", width=12, command=lambda: on_confirm()).pack(side="left", padx=8)
tk.Button(btn_fr, text="返回", width=12, command=self.build_main).pack(side="left", padx=8)
fr.grid_columnconfigure(1, weight=1)
cooldown = {"left": 0, "timer": None}
def update_send_btn():
if cooldown["left"] > 0:
send_btn.config(text=f"重新发送({cooldown['left']})", state="disabled")
else:
send_btn.config(text="发送验证码", state="normal")
def countdown_tick():
if cooldown["left"] <= 0:
update_send_btn()
@ -571,13 +578,11 @@ class MathApp:
cooldown["left"] -= 1
update_send_btn()
cooldown["timer"] = self.root.after(1000, countdown_tick)
def on_send():
email = email_var.get().strip()
if not is_valid_email(email):
messagebox.showwarning("邮箱错误", "请输入正确的邮箱地址", parent=self.root)
return
# generate and send code asynchronously
def worker():
send_btn.config(state="disabled", text="发送中...")
ok, msg = self.auth.send_code(email)
@ -589,9 +594,7 @@ class MathApp:
messagebox.showerror("发送失败", f"发送失败: {msg}", parent=self.root)
update_send_btn()
threading.Thread(target=worker, daemon=True).start()
send_btn.config(command=on_send)
def on_confirm():
email = email_var.get().strip()
code = code_var.get().strip()
@ -614,11 +617,10 @@ class MathApp:
if not ok:
messagebox.showerror("失败", msg, parent=self.root); return
messagebox.showinfo("成功", "注册并设置密码成功,跳转到选择界面", parent=self.root)
# login and go to choice screen
self.current_user = self.user_store.find_by_email(email)
self.build_choice_screen()
# ----- 登录界面(用户名/邮箱登录) -----
# 登录(用户名或邮箱)
def ui_login(self):
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
@ -639,14 +641,13 @@ class MathApp:
ok, res = self.auth.login(key, pw)
if not ok:
messagebox.showerror("登录失败", res, parent=self.root); return
# res is user dict
self.current_user = res
messagebox.showinfo("登录成功", f"欢迎,{self.current_user.get('username')}", parent=self.root)
self.build_choice_screen()
tk.Button(btn_fr, text="登录", width=12, command=do_login).pack(side="left", padx=6)
tk.Button(btn_fr, text="返回", width=12, command=self.build_main).pack(side="left", padx=6)
# ----- 选择学段与题量 -----
# 选择学段与题量
def build_choice_screen(self):
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
@ -656,7 +657,7 @@ class MathApp:
tk.Label(fr, text=f"已登录: {uname} ({email})", font=("Arial", 12)).pack(anchor="w")
tk.Button(fr, text="修改密码", command=self.ui_change_password).pack(anchor="w", pady=4)
tk.Button(fr, text="注销", command=self.logout).pack(anchor="w", pady=4)
tk.Label(fr, text="请选择学段并输入题目数量(默认3上限20:", font=("Arial", 12)).pack(pady=8, anchor="w")
tk.Label(fr, text="请选择学段并输入题目数量(数量最高为20:", font=("Arial", 12)).pack(pady=8, anchor="w")
grade_var = tk.StringVar(value="小学")
for g in QUESTION_FILES.keys():
tk.Radiobutton(fr, text=g, variable=grade_var, value=g).pack(anchor="w")
@ -705,7 +706,7 @@ class MathApp:
messagebox.showerror("失败", msg, parent=self.root); return
messagebox.showinfo("成功", "密码修改成功", parent=self.root)
# ----- 题目界面 -----
# 题目界面
def build_question_screen(self):
self.clear()
exam = self.current_exam
@ -740,7 +741,7 @@ class MathApp:
tk.Button(nav, text="下一题/提交", command=next_or_finish).pack(side="left", padx=6)
tk.Button(nav, text="放弃并返回", command=self.build_choice_screen).pack(side="left", padx=6)
# ----- 结果界面 -----
# 结果界面
def show_result_screen(self):
exam = self.current_exam
total = len(exam["questions"])
@ -779,7 +780,7 @@ class MathApp:
self.current_exam = None
self.build_main()
# ----------------- 初始化 -----------------
# ----------------- main -----------------
def main():
ensure_files_exist()
root = tk.Tk()

Loading…
Cancel
Save