📐 小学数学四则运算题库生成工具 V3.0 — 让出题像呼吸一样简单
一款基于 Python Tkinter 打造的桌面级数学题库生成器,支持加减乘除混合运算、多数值多位数自由组合、括号嵌套、竖式计算、脱式计算等多种题型,可自定义难度参数并一键导出 Word/竖式/脱式格式文档,是小学数学教师备课和家长辅导的高效利器。
✨ 开场白
数学是思维的体操,而四则运算则是这场体操的基本功。对于小学数学教师而言,每天面对的一个现实问题就是:如何快速、高效地生成大量难度适中、格式规范的练习题?手工出题不仅耗时耗力,还容易出现重复或难度失控的问题;而市面上的在线出题工具往往功能单一,无法满足"竖式计算""脱式计算""括号运算"等多样化教学需求。
小学数学四则运算题库生成工具 V3.0 正是为解决这一痛点而设计的专业桌面应用。它完全基于 Python 标准库 Tkinter 构建,无需网络、无需付费、一个文件即可运行,却提供了堪比专业教辅软件的功能深度。
从参数维度来看,本工具支持 2~5 个数值参与运算,每个数值可独立设置位数(一位数到四位数、随机、小数、分数),运算符支持加减乘除任意组合,答案范围从 10 到 9999 可选,还能为连续数值添加括号形成复合运算。高级选项方面,支持允许负数结果、小数答案、除法余数等灵活配置,以及竖式计算(限2个数)和脱式计算(最少3个数)两种专业题型模式。
操作流程上,工具采用"生成→预览→暂存→导出"的工作流:点击生成即可在右侧预览区实时查看题目效果,满意后暂存到题库,最终一键导出为 Word 文档(需 python-docx)、竖式格式或脱式格式的文本文件,直接打印即可用于课堂练习或家庭作业。
整个项目约 500 行精炼代码,涵盖了随机数生成、表达式求值、括号分组算法、多格式导出等核心技术,既是实用的教学工具,也是学习 Python GUI 开发和算法设计的优秀实战项目。
📋 功能特性一览
| |
|---|
| 2~5个数值,每个独立设置位数(1~4位/随机/小数/分数) |
| |
| |
| |
| |
| |
| |
| |
| |
🏗️ 第一部分:架构设计与核心算法
整体架构
程序以 MathQuizApp 单类封装,按职责划分为四大模块:
- UI 层:
_build_left_panel(参数面板)/ _build_right_panel(预览区) - 生成引擎:
_gen_number / _generate_one / _generate - 表达式处理:
_evaluate / _format_expression / _find_bracket_groups - 导出系统:
_export_word / _export_vertical / _export_strip
数值生成策略
根据位数类型生成对应范围的随机数,支持整数、小数和分数:
def_gen_number(self, digit_type, max_val=9999):if digit_type == "一位数":return random.randint(1, 9)elif digit_type == "二位数":return random.randint(10, 99)elif digit_type == "三位数":return random.randint(100, 999)elif digit_type == "小数(2位)":return round(random.uniform(0.01, 99.99), 2)elif digit_type == "分数": denom = random.choice([2, 3, 4, 5, 6, 8, 10]) numer = random.randint(1, denom - 1)return Fraction(numer, denom)
括号分组算法
通过扫描连续勾选的标志位,自动识别需要添加括号的数值组:
def_find_bracket_groups(self, bracket_flags, num_count): groups = [] i = 0while i < num_count:if i < len(bracket_flags) and bracket_flags[i]: start = iwhile i < num_count and i < len(bracket_flags) and bracket_flags[i]: i += 1if i - start >= 2: # 至少2个连续才成组 groups.append((start, i - 1))else: i += 1return groups
例如勾选数值1、2、3 → 生成 (a + b + c);勾选1、2、4、5 → 生成 (a + b) ○ c ○ (d + e)。
🎯 第二部分:题目生成引擎
单题生成流程
_generate_one 方法是核心,采用"生成-验证-重试"策略:
def_generate_one(self):for _ in range(200): # 最多尝试200次 numbers = [self._gen_number(digit_type) for ...] operators = [random.choice(ops) for ...]# 除法整除处理for i, op in enumerate(operators):if op == '÷'andnot allow_dec: numbers[i] = quotient * numbers[i+1] result = self._evaluate(numbers, operators)# 验证约束if abs(result) > answer_range: continueifnot allow_neg and result < 0: continuereturn {"expr": expr, "answer": answer, ...}
表达式求值
利用 Python 的 eval() 进行安全计算(输入完全由程序生成,无注入风险):
def_evaluate(self, numbers, operators): expr = str(numbers[0])for i, op in enumerate(operators):if op == '×': expr += f"*{numbers[i+1]}"elif op == '÷': expr += f"/{numbers[i+1]}"else: expr += f"{op}{numbers[i+1]}"return eval(expr)
🖨️ 第三部分:多格式显示与导出
竖式计算显示
标准竖式排版,对齐数位,预留答案书写区:
1. 45 + 38 ──────
脱式计算显示
逐步展开格式,留出多行计算过程:
1. 25 + 38 × 4 - 12 = ________________ = ________________ = ________________
Word 导出
优先使用 python-docx 库生成标准 Word 文档,包含标题、题目和答案页;若未安装则自动降级为纯文本导出:
try:from docx import Document doc = Document() doc.add_heading("四则运算练习题", level=1)for i, q in enumerate(questions): p = doc.add_paragraph() p.add_run(f"{i+1}. {q['expr']} = ______").font.size = Pt(14) doc.save(path)except ImportError:# 降级为txt导出 self._export_as_txt(questions, path)
💻 完整源代码
"""小学数学四则运算题库生成工具 V3.0功能:随机生成四则运算练习题,支持自定义题目难度与格式,可导出Word/竖式/脱式题目"""import tkinter as tkfrom tkinter import ttk, filedialog, messageboximport randomimport mathfrom fractions import Fractionimport os============ 常量 ============VERSION = "3.0"DIGIT_OPTIONS = ["一位数", "二位数", "三位数", "四位数", "随机", "小数(2位)", "分数"]ANSWER_RANGE_OPTIONS = [10, 20, 50, 100, 200, 300, 500, 999, 5000, 9999]OP_SYMBOLS = {'+': '+', '-': '-', '×': '×', '÷': '÷'}class MathQuizApp:def init(self, root):self.root = rootself.root.title(f"小学数学四则运算题库生成工具 V{VERSION}")self.root.geometry("1350x820")self.root.configure(bg="#f5f5f5")self.root.resizable(True, True) # 暂存题目列表 self.stored_questions = [] # 当前预览题目 self.current_questions = [] self._build_ui()def _build_ui(self): # 顶部标题栏 header = tk.Frame(self.root, bg="#2196F3", height=55) header.pack(fill=tk.X) header.pack_propagate(False) tk.Label(header, text="📐 小学数学四则运算题库生成工具 V3.0", font=("SimHei", 15, "bold"), bg="#2196F3", fg="white" ).pack(side=tk.LEFT, padx=20, pady=12) # 主体 body = tk.Frame(self.root, bg="#f5f5f5") body.pack(fill=tk.BOTH, expand=True, padx=10, pady=8) # 左侧参数面板 self._build_left_panel(body) # 右侧预览面板 self._build_right_panel(body)def _build_left_panel(self, parent): left = tk.Frame(parent, bg="white", width=520, relief=tk.RIDGE, bd=1) left.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 8)) left.pack_propagate(False) # 滚动区域 canvas = tk.Canvas(left, bg="white", highlightthickness=0) sb = ttk.Scrollbar(left, orient="vertical", command=canvas.yview) sf = tk.Frame(canvas, bg="white") sf.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0, 0), window=sf, anchor="nw", width=500) canvas.configure(yscrollcommand=sb.set) sb.pack(side=tk.RIGHT, fill=tk.Y) canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) pad = {"padx": 12, "pady": 4} # === 数值个数 === tk.Label(sf, text="🔢 数值个数", font=("SimHei", 10, "bold"), bg="white" ).pack(anchor="w", **pad) self.num_count_var = tk.IntVar(value=5) nc_frame = tk.Frame(sf, bg="white") nc_frame.pack(anchor="w", **pad) for n in [2, 3, 4, 5]: tk.Radiobutton(nc_frame, text=str(n), variable=self.num_count_var, value=n, bg="white", font=("SimSun", 10)).pack(side=tk.LEFT, padx=8) # === 数值位数设置 === tk.Label(sf, text="📏 数值位数设置", font=("SimHei", 10, "bold"), bg="white" ).pack(anchor="w", **pad) self.digit_vars = [] for i in range(5): frame = tk.Frame(sf, bg="white") frame.pack(fill=tk.X, **pad) tk.Label(frame, text=f"数值{i+1}:", bg="white", font=("SimSun", 10), width=6, anchor="w").pack(side=tk.LEFT) var = tk.StringVar(value="二位数") combo = ttk.Combobox(frame, textvariable=var, values=DIGIT_OPTIONS, width=12, state="readonly", font=("SimSun", 9)) combo.pack(side=tk.LEFT, padx=4) self.digit_vars.append(var) # === 括号设置 === tk.Label(sf, text="🔗 括号设置(勾选连续数值添加括号)", font=("SimHei", 10, "bold"), bg="white").pack(anchor="w", **pad) self.bracket_vars = [] br_frame = tk.Frame(sf, bg="white") br_frame.pack(anchor="w", **pad) for i in range(5): var = tk.BooleanVar(value=False) tk.Checkbutton(br_frame, text=f"数值{i+1}", variable=var, bg="white", font=("SimSun", 9)).pack(side=tk.LEFT, padx=4) self.bracket_vars.append(var) # === 答案范围 === tk.Label(sf, text="🎯 答案范围", font=("SimHei", 10, "bold"), bg="white" ).pack(anchor="w", **pad) self.answer_range_var = tk.IntVar(value=9999) ar_frame = tk.Frame(sf, bg="white") ar_frame.pack(anchor="w", **pad) for i, val in enumerate(ANSWER_RANGE_OPTIONS): tk.Radiobutton(ar_frame, text=str(val), variable=self.answer_range_var, value=val, bg="white", font=("SimSun", 9) ).grid(row=i // 5, column=i % 5, padx=4, pady=2) # === 题目数量 === tk.Label(sf, text="📝 题目数量", font=("SimHei", 10, "bold"), bg="white" ).pack(anchor="w", **pad) qn_frame = tk.Frame(sf, bg="white") qn_frame.pack(anchor="w", **pad) self.question_count_spin = ttk.Spinbox(qn_frame, from_=1, to=200, width=8, font=("SimSun", 11)) self.question_count_spin.set(20) self.question_count_spin.pack(side=tk.LEFT) tk.Label(qn_frame, text=" 道", bg="white", font=("SimSun", 10)).pack(side=tk.LEFT) # === 运算方法 === tk.Label(sf, text="➕ 运算方法(可多选)", font=("SimHei", 10, "bold"), bg="white" ).pack(anchor="w", **pad) self.op_vars = {} op_frame = tk.Frame(sf, bg="white") op_frame.pack(anchor="w", **pad) for op, label in [('+', '加法'), ('-', '减法'), ('×', '乘法'), ('÷', '除法')]: var = tk.BooleanVar(value=True) tk.Checkbutton(op_frame, text=label, variable=var, bg="white", font=("SimSun", 10)).pack(side=tk.LEFT, padx=8) self.op_vars[op] = var # === 高级选项 === tk.Label(sf, text="⚙️ 高级选项", font=("SimHei", 10, "bold"), bg="white" ).pack(anchor="w", **pad) self.allow_negative = tk.BooleanVar(value=False) self.allow_decimal_answer = tk.BooleanVar(value=False) self.allow_remainder = tk.BooleanVar(value=False) self.vertical_calc = tk.BooleanVar(value=False) self.strip_calc = tk.BooleanVar(value=False) adv_options = [ (self.allow_negative, "允许结果为负数"), (self.allow_decimal_answer, "答案允许带小数"), (self.allow_remainder, "除法有余数(限2个数)"), (self.vertical_calc, "竖式计算(限2个数)"), (self.strip_calc, "脱式计算(最少3个数)"), ] for var, text in adv_options: tk.Checkbutton(sf, text=text, variable=var, bg="white", font=("SimSun", 10)).pack(anchor="w", **pad) # === 操作按钮 === tk.Label(sf, text="", bg="white").pack(pady=5) btn_frame = tk.Frame(sf, bg="white") btn_frame.pack(fill=tk.X, padx=12, pady=5) tk.Button(btn_frame, text="🎲 生成题目", command=self._generate, font=("SimHei", 11), bg="#2196F3", fg="white", relief=tk.FLAT, padx=12, pady=5).pack(side=tk.LEFT, padx=4) tk.Button(btn_frame, text="📥 暂存题目", command=self._store, font=("SimHei", 11), bg="#4CAF50", fg="white", relief=tk.FLAT, padx=12, pady=5).pack(side=tk.LEFT, padx=4) btn_frame2 = tk.Frame(sf, bg="white") btn_frame2.pack(fill=tk.X, padx=12, pady=5) tk.Button(btn_frame2, text="🗑 清空暂存", command=self._clear_store, font=("SimHei", 10), bg="#ff9800", fg="white", relief=tk.FLAT, padx=8, pady=4).pack(side=tk.LEFT, padx=3) tk.Button(btn_frame2, text="📄 导出Word", command=self._export_word, font=("SimHei", 10), bg="#9C27B0", fg="white", relief=tk.FLAT, padx=8, pady=4).pack(side=tk.LEFT, padx=3) tk.Button(btn_frame2, text="📐 导出竖式", command=self._export_vertical, font=("SimHei", 10), bg="#607D8B", fg="white", relief=tk.FLAT, padx=8, pady=4).pack(side=tk.LEFT, padx=3) tk.Button(btn_frame2, text="📝 导出脱式", command=self._export_strip, font=("SimHei", 10), bg="#795548", fg="white", relief=tk.FLAT, padx=8, pady=4).pack(side=tk.LEFT, padx=3) # 暂存计数 self.store_label = tk.Label(sf, text="暂存:0 道题", bg="white", fg="#666666", font=("SimSun", 10)) self.store_label.pack(anchor="w", padx=12, pady=5)def _build_right_panel(self, parent): right = tk.Frame(parent, bg="white", relief=tk.RIDGE, bd=1) right.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 顶部栏 top = tk.Frame(right, bg="#f8f9fa", height=40) top.pack(fill=tk.X) top.pack_propagate(False) tk.Label(top, text="📋 题目预览", font=("SimHei", 10, "bold"), bg="#f8f9fa").pack(side=tk.LEFT, padx=10, pady=8) tk.Button(top, text="清空显示", command=self._clear_preview, font=("SimSun", 9), relief=tk.FLAT, bg="#e0e0e0" ).pack(side=tk.RIGHT, padx=10, pady=6) # 文本预览区 text_frame = tk.Frame(right, bg="white") text_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=8) self.preview_text = tk.Text(text_frame, font=("Consolas", 12), wrap=tk.WORD, bg="#fafafa", fg="#333333", relief=tk.SOLID, bd=1, spacing1=4, spacing3=4) scroll = ttk.Scrollbar(text_frame, command=self.preview_text.yview) self.preview_text.configure(yscrollcommand=scroll.set) scroll.pack(side=tk.RIGHT, fill=tk.Y) self.preview_text.pack(fill=tk.BOTH, expand=True)def _clear_preview(self): self.preview_text.delete("1.0", tk.END) self.current_questions = []# ============ 数值生成 ============def _gen_number(self, digit_type, max_val=9999): """根据位数类型生成数值""" if digit_type == "一位数": return random.randint(1, 9) elif digit_type == "二位数": return random.randint(10, 99) elif digit_type == "三位数": return random.randint(100, 999) elif digit_type == "四位数": return random.randint(1000, 9999) elif digit_type == "随机": return random.randint(1, min(max_val, 999)) elif digit_type == "小数(2位)": return round(random.uniform(0.01, min(max_val, 99.99)), 2) elif digit_type == "分数": denom = random.choice([2, 3, 4, 5, 6, 8, 10]) numer = random.randint(1, denom - 1) return Fraction(numer, denom) return random.randint(1, 99)def _get_ops(self): """获取选中的运算符""" ops = [] if self.op_vars['+'].get(): ops.append('+') if self.op_vars['-'].get(): ops.append('-') if self.op_vars['×'].get(): ops.append('×') if self.op_vars['÷'].get(): ops.append('÷') if not ops: ops = ['+'] return opsdef _evaluate(self, numbers, operators): """计算表达式结果(按运算符优先级)""" try: expr = str(numbers[0]) for i, op in enumerate(operators): n = numbers[i + 1] if op == '+': expr += f"+{n}" elif op == '-': expr += f"-{n}" elif op == '×': expr += f"*{n}" elif op == '÷': if n == 0: return None expr += f"/{n}" result = eval(expr) return result except: return Nonedef _format_number(self, n): """格式化数值显示""" if isinstance(n, Fraction): return str(n) elif isinstance(n, float): if n == int(n): return str(int(n)) return str(round(n, 2)) return str(n)def _format_expression(self, numbers, operators, brackets=None): """格式化表达式字符串""" parts = [] for i, n in enumerate(numbers): parts.append(self._format_number(n)) # 应用括号 if brackets: # 找连续勾选的组 groups = self._find_bracket_groups(brackets, len(numbers)) # 构建表达式 expr = "" i = 0 while i < len(numbers): in_group = False for start, end in groups: if i == start: expr += "(" for j in range(start, end + 1): expr += parts[j] if j < end: expr += f" {operators[j]} " expr += ")" if end < len(numbers) - 1: expr += f" {operators[end]} " i = end + 1 in_group = True break if not in_group: expr += parts[i] if i < len(numbers) - 1: expr += f" {operators[i]} " i += 1 return expr else: expr = parts[0] for i, op in enumerate(operators): expr += f" {op}{parts[i+1]}" return exprdef _find_bracket_groups(self, bracket_flags, num_count): """找出连续勾选的括号组""" groups = [] i = 0 while i < num_count: if i < len(bracket_flags) and bracket_flags[i]: start = i while i < num_count and i < len(bracket_flags) and bracket_flags[i]: i += 1 if i - start >= 2: groups.append((start, i - 1)) else: i += 1 return groups# ============ 题目生成 ============def _generate_one(self): """生成一道题""" num_count = self.num_count_var.get() answer_range = self.answer_range_var.get() ops = self._get_ops() allow_neg = self.allow_negative.get() allow_dec = self.allow_decimal_answer.get() allow_rem = self.allow_remainder.get() # 获取括号设置 brackets = [self.bracket_vars[i].get() for i in range(num_count)] max_attempts = 200 for _ in range(max_attempts): # 生成数值 numbers = [] for i in range(num_count): digit_type = self.digit_vars[i].get() if i < len(self.digit_vars) else "二位数" numbers.append(self._gen_number(digit_type, answer_range)) # 生成运算符 operators = [random.choice(ops) for _ in range(num_count - 1)] # 处理除法:确保能整除(除非允许余数/小数) for i, op in enumerate(operators): if op == '÷' and numbers[i + 1] != 0: if not allow_dec and not allow_rem: # 调整被除数使其能整除 divisor = numbers[i + 1] if not isinstance(numbers[i + 1], Fraction) else 1 if divisor != 0 and isinstance(divisor, int): quotient = random.randint(1, max(1, answer_range // max(1, abs(divisor)))) numbers[i] = quotient * divisor elif op == '÷' and numbers[i + 1] == 0: numbers[i + 1] = random.randint(1, 9) # 计算结果 result = self._evaluate(numbers, operators) if result is None: continue # 检查答案范围 if isinstance(result, (int, float)): if abs(result) > answer_range: continue if not allow_neg and result < 0: continue if not allow_dec and isinstance(result, float) and result != int(result): continue # 格式化表达式 expr = self._format_expression(numbers, operators, brackets) answer = self._format_number(result) if not isinstance(result, Fraction) else str(result) if isinstance(result, float) and result == int(result): answer = str(int(result)) return {"expr": expr, "answer": answer, "numbers": numbers, "operators": operators} # 兜底:简单加法 a, b = random.randint(1, 50), random.randint(1, 50) return {"expr": f"{a} + {b}", "answer": str(a + b), "numbers": [a, b], "operators": ['+']}def _generate(self): """生成题目并显示""" count = int(self.question_count_spin.get()) self.current_questions = [] # 特殊模式检查 if self.vertical_calc.get(): self.num_count_var.set(2) if self.strip_calc.get() and self.num_count_var.get() < 3: self.num_count_var.set(3) for i in range(count): q = self._generate_one() self.current_questions.append(q) # 显示到预览区 self.preview_text.delete("1.0", tk.END) self.preview_text.insert(tk.END, f"{'='*50}\n") self.preview_text.insert(tk.END, f" 四则运算练习题(共 {count} 道)\n") self.preview_text.insert(tk.END, f"{'='*50}\n\n") for i, q in enumerate(self.current_questions): if self.vertical_calc.get(): self._display_vertical(i + 1, q) elif self.strip_calc.get(): self._display_strip(i + 1, q) else: line = f" {i+1:>3}. {q['expr']} = ______\n" self.preview_text.insert(tk.END, line) self.preview_text.insert(tk.END, f"\n{'─'*50}\n") self.preview_text.insert(tk.END, f" 参考答案:\n") for i, q in enumerate(self.current_questions): self.preview_text.insert(tk.END, f" {i+1}. {q['answer']} ") if (i + 1) % 5 == 0: self.preview_text.insert(tk.END, "\n") self.preview_text.insert(tk.END, "\n")def _display_vertical(self, idx, q): """竖式显示""" nums = q['numbers'] op = q['operators'][0] if q['operators'] else '+' a = self._format_number(nums[0]) b = self._format_number(nums[1]) if len(nums) > 1 else "0" width = max(len(a), len(b)) + 2 self.preview_text.insert(tk.END, f" {idx}.\n") self.preview_text.insert(tk.END, f" {a:>{width}}\n") self.preview_text.insert(tk.END, f" {op}{b:>{width}}\n") self.preview_text.insert(tk.END, f" {'─' * (width + 2)}\n") self.preview_text.insert(tk.END, f" {'':>{width}}\n\n")def _display_strip(self, idx, q): """脱式显示""" self.preview_text.insert(tk.END, f" {idx}. {q['expr']}\n") self.preview_text.insert(tk.END, f" = ________________\n") self.preview_text.insert(tk.END, f" = ________________\n") self.preview_text.insert(tk.END, f" = ________________\n\n")# ============ 暂存管理 ============def _store(self): """暂存当前题目""" if not self.current_questions: messagebox.showinfo("提示", "请先生成题目") return self.stored_questions.extend(self.current_questions) self.store_label.config(text=f"暂存:{len(self.stored_questions)} 道题") messagebox.showinfo("暂存成功", f"已暂存 {len(self.current_questions)} 道题\n" f"累计暂存:{len(self.stored_questions)} 道")def _clear_store(self): """清空暂存""" if self.stored_questions: if messagebox.askyesno("确认", f"确定清空 {len(self.stored_questions)} 道暂存题目?"): self.stored_questions = [] self.store_label.config(text="暂存:0 道题") else: messagebox.showinfo("提示", "暂存区为空")# ============ 导出功能 ============def _get_export_questions(self): """获取要导出的题目(优先暂存,否则当前)""" if self.stored_questions: return self.stored_questions elif self.current_questions: return self.current_questions else: messagebox.showinfo("提示", "没有可导出的题目,请先生成或暂存题目") return Nonedef _export_word(self): """导出为Word文档(纯文本docx格式)""" questions = self._get_export_questions() if not questions: return path = filedialog.asksaveasfilename(defaultextension=".docx", filetypes=[("Word文档", "*.docx"), ("文本文件", "*.txt")]) if not path: return try: # 尝试使用python-docx try: from docx import Document from docx.shared import Pt, Cm from docx.enum.text import WD_ALIGN_PARAGRAPH doc = Document() # 标题 title = doc.add_heading("四则运算练习题", level=1) title.alignment = WD_ALIGN_PARAGRAPH.CENTER doc.add_paragraph(f"共 {len(questions)} 道题") # 题目 for i, q in enumerate(questions): p = doc.add_paragraph() p.paragraph_format.space_after = Pt(8) p.add_run(f"{i+1}. {q['expr']} = ______").font.size = Pt(14) # 答案页 doc.add_page_break() doc.add_heading("参考答案", level=2) answer_text = "" for i, q in enumerate(questions): answer_text += f"{i+1}.{q['answer']} " if (i + 1) % 8 == 0: answer_text += "\n" doc.add_paragraph(answer_text) doc.save(path) messagebox.showinfo("导出成功", f"Word文档已保存到:\n{path}") except ImportError: # 降级为txt if not path.endswith('.txt'): path = path.replace('.docx', '.txt') self._export_as_txt(questions, path) except Exception as e: messagebox.showerror("导出失败", str(e))def _export_vertical(self): """导出竖式计算""" questions = self._get_export_questions() if not questions: return path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("文本文件", "*.txt")]) if not path: return try: lines = [] lines.append("竖式计算练习题") lines.append("=" * 40) lines.append("") for i, q in enumerate(questions): nums = q['numbers'] op = q['operators'][0] if q['operators'] else '+' a = self._format_number(nums[0]) b = self._format_number(nums[1]) if len(nums) > 1 else "0" width = max(len(a), len(b)) + 2 lines.append(f"{i+1}.") lines.append(f" {a:>{width}}") lines.append(f" {op}{b:>{width}}") lines.append(f" {'─' * (width + 2)}") lines.append(f" {'':>{width}}") lines.append("") lines.append("─" * 40) lines.append("参考答案:") for i, q in enumerate(questions): lines.append(f" {i+1}. {q['answer']}") with open(path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) messagebox.showinfo("导出成功", f"竖式题目已保存到:\n{path}") except Exception as e: messagebox.showerror("导出失败", str(e))def _export_strip(self): """导出脱式计算""" questions = self._get_export_questions() if not questions: return path = filedialog.asksaveasfilename(defaultextension=".txt", filetypes=[("文本文件", "*.txt")]) if not path: return try: lines = [] lines.append("脱式计算练习题") lines.append("=" * 40) lines.append("") for i, q in enumerate(questions): lines.append(f"{i+1}. {q['expr']}") lines.append(f" = ________________") lines.append(f" = ________________") lines.append(f" = ________________") lines.append("") lines.append("─" * 40) lines.append("参考答案:") for i, q in enumerate(questions): lines.append(f" {i+1}. {q['answer']}") with open(path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) messagebox.showinfo("导出成功", f"脱式题目已保存到:\n{path}") except Exception as e: messagebox.showerror("导出失败", str(e))def _export_as_txt(self, questions, path): """降级导出为纯文本""" lines = [] lines.append("四则运算练习题") lines.append(f"共 {len(questions)} 道题") lines.append("=" * 40) lines.append("") for i, q in enumerate(questions): lines.append(f"{i+1}. {q['expr']} = ______") lines.append("") lines.append("─" * 40) lines.append("参考答案:") for i, q in enumerate(questions): lines.append(f" {i+1}. {q['answer']}") with open(path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) messagebox.showinfo("导出成功", f"文本文件已保存到:\n{path}\n(未安装python-docx,已降级为txt)")============ 程序入口 ============if name == "main":root = tk.Tk()app = MathQuizApp(root)root.mainloop()