四則運算自動生成器(python wxpython GUI)
1、項目概述
1.1 項目要求
·自動生成四則運算題目,實時判斷正誤
·支持真分數的四則運算
1.2 GUI界面呈現
本文借鑒一名作者(昵稱:步平凡)的項目代碼【博客園、GitHub】。具體的解決思路和設計過程,在其GitHub上有詳細介紹。以下是部分運行結果截圖:

可以看見,該作者的輸入和輸出均在命令行實現。所以,小編嘗試將該程序用GUI界面呈現。
1.3 界面設計效果圖
對程序進行修改后,以下為修改后的運行截圖。【使用wxpython庫】
主要分為兩個Frame,“四則運算生成器” 和 “計算界面”。
大體每個Frame所包含的插件如下表:
| 四則運算生成器 | 5個靜態文本(顯示相關需求參數) |
| 5個文本框(獲得需求參數) | |
| 1個按鈕(響應“計算界面”事件) | |
| 計算界面 | 靜態文本(顯示題目及顯示用戶答題結果) |
| 文本框(獲得用戶答案,回車響應) |


2、GUI界面設計(main.py)及對四則運算功能的設計(formula.py)
2.1 main.py
詳細代碼
# -*- coding: utf-8 -*- ''' * @Author : 玩的三立方 * @Date : 2021-09-20 * @Description : 生成算式 * @LastEditTime : * ''' # 導入wx模塊 import wx from wx.core import CLEAR from formula import OPT, GeneralFormular, ComputeFormular class MyApp(wx.App): def OnInit(self): frame = wx.Frame(parent=None, title="四則運算生成器") # 新建框架 panel = wx.Panel(frame, -1) # 生成面板 Text = wx.StaticText(panel,-1,"請輸入相關需求",pos=(150,2)) label1=wx.StaticText(panel,-1,"生成算式數量(>=1)",pos=(10,30)) #標簽 text1=wx.TextCtrl(panel,-1,pos=(200,30),size=(180,20), #輸入框 style=wx.TE_MULTILINE) label2=wx.StaticText(panel,-1,"每個數值的上限(>=10)",pos=(10,60)) text2=wx.TextCtrl(panel,-1,pos=(200,60),size=(180,20), style=wx.TE_MULTILINE) label3=wx.StaticText(panel,-1,"操作數個數(>=2)",pos=(10,90)) text3=wx.TextCtrl(panel,-1,pos=(200,90),size=(180,20), style=wx.TE_MULTILINE) label4=wx.StaticText(panel,-1,"運算符種數(1~4)",pos=(10,120)) text4=wx.TextCtrl(panel,-1,pos=(200,120),size=(180,20), style=wx.TE_MULTILINE) label5=wx.StaticText(panel,-1,"是否要包含分數(0,1)",pos=(10,150)) text5=wx.TextCtrl(panel,-1,pos=(200,150),size=(180,20), style=wx.TE_MULTILINE) button = wx.Button(panel,-1, '確定', pos=(150, 180)) # 確定按鈕位置 self.text1=text1 #方便跨函數調用 self.text2=text2 self.text3=text3 self.text4=text4 self.text5=text5 self.button1 = button self.Bind(wx.EVT_BUTTON, # 綁定事件,如果是按鈕被點擊 self.GetInPut, # 激發的按鈕事件 self.button1) # 激發的按鈕 frame.Center() frame.Show() # 顯示 return True def GetInPut(self,event): #事件的激發函數,獲得輸入 while True: n = self.text1.GetValue() try: n = abs(int(n)) if n < 1: wx.MessageBox("生成算式數量-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break except Exception as e: wx.MessageBox("生成算式數量-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break up_limit = self.text2.GetValue() try: up_limit = abs(int(up_limit)) if up_limit < 10: wx.MessageBox("每個數值的上限-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break except Exception as e: wx.MessageBox("每個數值的上限-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break oper_num = self.text3.GetValue() try: oper_num = abs(int(oper_num)) if oper_num < 2: wx.MessageBox("操作數個數-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break except Exception as e: wx.MessageBox("操作數個數-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break oper_variety = self.text4.GetValue() try: oper_variety = abs(int(oper_variety)) if oper_variety < 1 or oper_variety > 4: wx.MessageBox("運算符種數-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break except Exception as e: wx.MessageBox("運算符種數-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break has_fraction = self.text5.GetValue() try: has_fraction = abs(int(has_fraction)) if has_fraction != 0 and has_fraction != 1: wx.MessageBox("是否要包含分數-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break except Exception as e: wx.MessageBox("是否要包含分數-[Eror]: Input illegal! Please input again... "\ , "INFO", wx.OK|wx.ICON_INFORMATION) break self.n = int(n) self.up_limit = int(up_limit) self.oper_num = int(oper_num) self.oper_variety = int(oper_variety) self.has_fraction = int(has_fraction) self.solveFormular() break def solveFormular(self): opt = OPT(self.up_limit, self.oper_num, self.oper_variety, self.has_fraction) gf = GeneralFormular(opt) cf = ComputeFormular() formulars = [] results = [] for i in range(int(self.n)): f = gf.solve() formulars.append(" ".join(i for i in f) + " = ") results.append(cf.solve(f)) self.formulars=formulars self.results=results self.displayFormular() def displayFormular(self): frame = wx.Frame(parent=None, title="計算界面", size=(500,500)) self.panel = wx.Panel(frame, -1) # 生成面板 self.Column = 30 self.Row = 10 self.N =len(self.formulars) self.i = 0 self.GetResult() frame.Center() frame.Show() # 顯示 return True def GetResult(self): #顯示題目和答題文本框 if self.Column >= 800: self.Row += 500 self.Column = 30 T_label=wx.StaticText(self.panel,-1,"第{}題:".format(self.i+1) + self.formulars[self.i],pos=(self.Row,self.Column)) #標簽 T_text=wx.TextCtrl(self.panel,-1,pos=(self.Row+340,self.Column),size=(100,20), #輸入框 style=wx.TE_PROCESS_ENTER) self.T_text = T_text self.Bind(wx.EVT_TEXT_ENTER, self.judgeResult, T_text) def judgeResult(self,event): #判斷正誤 self.result = self.T_text.GetValue() self.Column += 30 if self.result == self.results[self.i]: flag = "正確 √ √ √" else: flag = "錯誤❌❌❌" T_label=wx.StaticText(self.panel,-1," 正確答案:{} 回答{}"\ .format(self.results[self.i], flag),pos=(self.Row,self.Column)) self.i += 1 try: if self.i <= self.N-1: self.Column = self.Column+30 self.GetResult() else: End_label=wx.StaticText(self.panel,-1," ---------答題結束--------- ",pos=(self.Row, self.Column+50)) End_label.SetFont(wx.Font(12, wx.SWISS)) except Exception as e: return True if __name__ == '__main__': app = MyApp() # 啟動 app.MainLoop() # 進入消息循環
2.2 formula.py
詳細代碼
import random import datetime import argparse import re from fractions import Fraction def OPT(up_limit=10, oper_num=2, oper_variety=4, has_fraction=True, be_negetive=False): ''' * 設置參數 * @param up_limit {int} 操作數數值上限 * @param oper_num {int} 操作數個數 * @param oper_variety {int} 運算符種類 * @param has_fraction {bool} 是否帶有分數 * @param be_negative {bool} 可否存在負數 ''' parse = argparse.ArgumentParser() # 操作數數值上限 parse.add_argument('--up_limit', type=int, default=up_limit) # 操作數個數 parse.add_argument('--oper_num', type=int, default=oper_num) # 運算符種類 parse.add_argument('--oper_variety', type=int, default=oper_variety) # 是否帶有分數 parse.add_argument('--has_fraction', type=bool, default=has_fraction) # 可否存在負數 parse.add_argument('--be_negative', type=bool, default=be_negetive) return parse.parse_args(args=[]) class GeneralFormular: ''' * 生成算式 * @param opt {OPT} 參數 ''' def __init__(self, opt): self.opt = opt # 定義運算符 self.operator = ['+', '-', '×', '÷'] # @profile def catFormula(self, operand1, operator, operand2): ''' * 連接算式 * @param operand1 {str} 操作數1 * @param opertor {str} 運算符 * @param operand2 {str} 操作數2 * @return {str} ''' return "{}#{}#{}".format(operand1, operator, operand2) # @profile def getRandomIntOperand(self): ''' * 返回隨機整數操作數 * @return {int} ''' return random.randint(0, self.opt.up_limit) # @profile def getRandomFractionOperand(self): ''' * 返回隨機分數操作數 * @return {str} ''' # 生成兩個整數,一個作為分子,一個作為分母 num01 = self.getRandomIntOperand() num02 = self.getRandomIntOperand() while num01 == num02 or num02==0: num02 = self.getRandomIntOperand() while num01 == 0: num01 = self.getRandomIntOperand() # 保證分數為真分數, 化簡 if num01 < num02: return Fraction(num01, num02).__str__() else: return Fraction(num02, num01).__str__() # @profile def getRandomOperator(self): ''' * 返回隨機運算符 * @return {str} ''' index = random.randint(0, self.opt.oper_variety-1) return self.operator[index] # @profile def getOriginFormular(self): ''' * 生成整數源算式 * @return {list} ''' # 通過self.opt.oper_num控制操作數個數,循環調用catFormula()方法構造算式 formular = self.getRandomIntOperand() for i in range(self.opt.oper_num-1): formular = self.catFormula(formular, self.getRandomOperator(), self.getRandomIntOperand()) # 去掉'÷0' while(True): if '÷#0' in formular: formular = formular.replace('÷#0', '÷#' + str(self.getRandomIntOperand())) else: break # 通過'#'分割生成列表 formular_list = formular.split('#') return formular_list # @profile def insertBracket(self, formular_list): ''' * 插入括號 * @param formular_list {list} 源算式列表 * @return {list} ''' # print(formular) # 若只包含+號 或 只有兩個操作數 則不用加括號 if self.opt.oper_variety <= 2 or self.opt.oper_num == 2: return formular_list # 若不包含×÷ 則不用加括號 if '×' not in formular_list and '÷' not in formular_list: return formular_list # 存儲添加括號的算式 new_formular_list = [] # flag表示是否已存在左括號,作用在於構造出一對括號 flag = 0 # 添加括號 while(len(formular_list) > 1): oper = formular_list.pop(1) # 若下一個符號為 + or - , 則插入括號 if oper == '-' or oper == '+': if flag == 0: new_formular_list.append("(") flag = 1 new_formular_list.append(formular_list.pop(0)) new_formular_list.append(oper) else: new_formular_list.append(formular_list.pop(0)) if flag == 1: new_formular_list.append(")") flag = 0 new_formular_list.append(oper) # print(operand_list, operator_list, new_formular) new_formular_list.append(formular_list.pop(0)) if flag == 1: new_formular_list.append(")") return new_formular_list # @profile def replaceFraction(self, formular_list): ''' * 帶入分數 * @param formular_list {list} 源算式列表,可能包含括號 * @return {list} ''' # 帶入分數個數 fraction_num = 1 if self.opt.oper_num > 2: fraction_num = (self.opt.oper_num - 1) / 2 index = random.randint(0, len(formular_list)-1) interval = 1 while True: if formular_list[index].isdigit(): break elif formular_list[index - interval].isdigit(): index -= interval break elif formular_list[index + interval].isdigit(): index += interval break else: interval += 1 formular_list[index] = self.getRandomFractionOperand() return formular_list # @profile def solve(self): ''' * 整合生成算式的后綴表達式,帶括號 * @return {list} ''' # 生成原生算式 ori_formular = self.getOriginFormular() # 添加括號 bra_formular = self.insertBracket(ori_formular) # 帶入分數 if self.opt.has_fraction: bra_formular = self.replaceFraction(bra_formular) return bra_formular class ComputeFormular: ''' * 計算算式的值 ''' def __init__(self): pass # @profile def getPostFormular(self, formular_list): ''' * 中綴表達式轉為后綴表達式 * @param formular_list {list} 中綴表達式 * @return {list} ''' # 運算符優先級 priority = {'×': 3, '÷': 3, '+': 2, '-': 2, '(': 1} # 運算符棧 operator_stack = [] # 后綴表達式 post_formular_list = [] # 中綴表達式轉為后綴表達式 while formular_list: char = formular_list.pop(0) if char == '(': operator_stack.append(char) elif char == ')': oper_char = operator_stack.pop() while oper_char != '(': post_formular_list.append(oper_char) oper_char = operator_stack.pop() elif char in '+-×÷': while operator_stack and priority[operator_stack[-1]] >= priority[char]: post_formular_list.append(operator_stack.pop()) operator_stack.append(char) else: post_formular_list.append(char) # 若符號棧不為空則循環 while operator_stack: post_formular_list.append(operator_stack.pop()) # print(post_formular) return post_formular_list # @profile def compute(self, char, num01, num02): ''' * 計算算式的值 * @param char {str} 運算符 * @param num01 {str} 第二個數字,可能為分數 * @param num02 {str} 第二個數字,可能為分數 * @return {str} ''' if char == '+': return (Fraction(num02) + Fraction(num01)).__str__() elif char == '-': return (Fraction(num02) - Fraction(num01)).__str__() elif char == '×': return (Fraction(num02) * Fraction(num01)).__str__() elif char == '÷': try: return (Fraction(num02) / Fraction(num01)).__str__() except Exception as e: # print("Error: ", e) return "NaN" # @profile def calcFormular(self, post_formular_list): ''' * 計算算式的值 * @param post_formular_list {list} 后綴表達式 * @return {str} ''' # 操作數棧 operand_stack = [] while post_formular_list: char = post_formular_list.pop(0) if char in '+-×÷': result = self.compute(char, operand_stack.pop(), operand_stack.pop()) if result == "NaN": return result operand_stack.append(result) else: operand_stack.append(char) return operand_stack.pop() # @profile def solve(self, formular): ''' * 整合計算中綴表達式的值 * @param formular {list} 后綴表達式 * @return {str} ''' # 轉為后綴表達式 post_formular = self.getPostFormular(formular) # 計算值 value = self.calcFormular(post_formular) return value
3、改進與總結
界面可以進行進一步地美化,比如可以從字體、文本框、背景進行改進。該項目GUI的設計難點在於計算界面中,N條算式的輸出和判斷,這是一個相對動態的過程:輸出一個算式——>用戶輸入答案,回車確定——>輸出計算結果——>輸出下一條算式,直到輸出完用戶指定的題目數。另外,由於“計算界面”輸出的均是靜態文本,界面范圍限制了最大的可輸出題目。可進行進一步改進,比如設計成可滑動界面。
