python的GUI之计算器学习目标：学会利用python的GUI做界面布局
手写计算器代码熟悉控件的使用方法
优化计算器代码，解决获取按钮文本的方法
了解lambda函数的传参优点和局限
打包生成自己的计算器软件，并独立运行
学习内容：手写一个简单计算器
1、计算器目标图

目标计算器设计分为三个部分
背景部分
根：Tk()
展示部分:上方展示算式，下方展示计算结果

按钮部分

2、 计算器计算功能
1. 加减乘除，放在=，按钮上
2. 回退，放在<-按钮上
3. 清除，放在MC按钮上
3、 代码实现
1. 外观布局：
首先引入包：from tkinter import *
根据目标图片设计布局按钮：定义计算器类，初始化界面控件。
class Calculator: def __init__(self, master): self.master = master self.master.title("Calculator") self.master.resizable(0, 0) # 设置窗口不可拉伸 self.master.geometry('320x420') # 设置主窗口的初始尺寸 self.result = StringVar() # 用于显示结果的可变文本 self.equation = StringVar() # 显示计算方程 self.result.set(' ') self.equation.set('0') # 显示框 self.show_result_eq = Label(self.master, bg='white', fg='black', font=('Arail', '16'), bd='0', textvariable=self.equation, anchor='se') self.show_result = Label(self.master, bg='white', fg='black', font=('Arail', '20'), bd='0', textvariable=self.result, anchor='se') # 按钮 self.button_back = Button(self.master, text='<-', bg='DarkGray', command=self.back) # 返回 self.button_lbracket = Button(self.master, text='(', bg='DarkGray', command=lambda: self.getNum('(')) # 左括号 self.button_rbracket = Button(self.master, text=')', bg='DarkGray', command=lambda: self.getNum(')')) # 左括号 self.button_division = Button(self.master, text='÷', bg='DarkGray', command=lambda: self.getNum('÷')) # 除号 # 7 8 9 4 5 6 1 2 3 self.button_7 = Button(self.master, text='7', bg='DarkGray', command=lambda: self.getNum('7')) # 7号 self.button_8 = Button(self.master, text='8', bg='DarkGray', command=lambda: self.getNum('8')) # 8号 self.button_9 = Button(self.master, text='9', bg='DarkGray', command=lambda: self.getNum('9')) # 9号 self.button_multiplication = Button(self.master, text='*', bg='DarkGray', command=lambda: self.getNum('*')) # 乘号 # 按钮的command参数,是回调函数。lambda函数是为了可以传参数给回调函数 self.button_4 = Button(self.master, text='4', bg='DarkGray', command=lambda: self.getNum('4')) # 4号 self.button_5 = Button(self.master, text='5', bg='DarkGray', command=lambda: self.getNum('5')) # 5号 self.button_6 = Button(self.master, text='6', bg='DarkGray', command=lambda: self.getNum('6')) # 6号 self.button_minus = Button(self.master, text='-', bg='DarkGray', command=lambda: self.getNum('-')) # -号 self.button_1 = Button(self.master, text='1', bg='DarkGray', command=lambda: self.getNum('1')) # 1号 self.button_2 = Button(self.master, text='2', bg='DarkGray', command=lambda: self.getNum('2')) # 2号 self.button_3 = Button(self.master, text='3', bg='DarkGray', command=lambda: self.getNum('3')) # 3号 self.button_plus = Button(self.master, text='+', bg='DarkGray', command=lambda: self.getNum('+')) # +号 # 控制按钮 0 . self.button_MC = Button(self.master, text='MC', bg='DarkGray', command=self.clear) # MC self.button_0 = Button(self.master, text='0', bg='DarkGray', command=lambda: self.getNum('0')) # 0 self.button_dot = Button(self.master, text='.', bg='DarkGray', command=lambda: self.getNum('.')) # . self.button_eq = Button(self.master, text='=', bg='DarkGray', command=self.run) # = # Layout布局 self.show_result_eq.place(x='10', y='10', width='300', height='50') self.show_result.place(x='10', y='60', width='300', height='50') self.button_back.place(x='10', y='150', width='60', height='40') self.button_lbracket.place(x='90', y='150', width='60', height='40') self.button_rbracket.place(x='170', y='150', width='60', height='40') self.button_division.place(x='250', y='150', width='60', height='40') self.button_7.place(x='10', y='205', width='60', height='40') self.button_8.place(x='90', y='205', width='60', height='40') self.button_9.place(x='170', y='205', width='60', height='40') self.button_multiplication.place(x='250', y='205', width='60', height='40') self.button_4.place(x='10', y='260', width='60', height='40') self.button_5.place(x='90', y='260', width='60', height='40') self.button_6.place(x='170', y='260', width='60', height='40') self.button_minus.place(x='250', y='260', width='60', height='40') self.button_1.place(x='10', y='315', width='60', height='40') self.button_2.place(x='90', y='315', width='60', height='40') self.button_3.place(x='170', y='315', width='60', height='40') self.button_plus.place(x='250', y='315', width='60', height='40') self.button_MC.place(x='10', y='370', width='60', height='40') self.button_0.place(x='90', y='370', width='60', height='40') self.button_dot.place(x='170', y='370', width='60', height='40') self.button_eq.place(x='250', y='370', width='60', height='40')
重点说明
按钮的command参数,是回调函数。lambda函数是为了可以传参数给回调函数。
lambda匿名函数的使用：command=lambda: self.getNum('3'))
注意这里传的参数是字符串：'3'。每一个按钮点击想要获取的文本值不同，所以对应的参数各不相同。
从而，也导致初始化界面代码看起来太冗长了。
功能布局
设置回退back，符号获取getNum，清除clear，计算run方法。
def back(self): temp_equ = self.equation.get() self.equation.set(temp_equ[:-1]) # 一个一个删 def getNum(self, arg): temp_equ = self.equation.get() # 输入算式 temp_result = self.result.get() # 判断基本语法错误 if temp_result != ' ': # 计算器输入前还没有结果，那么结果区域应该设置为空。 self.result.set(' ') if temp_equ == '0' and (arg not in ['.', '+', '-', '*', '÷']): # 如果首次输入为0，则紧跟则不能是数字，只是小数点或运算符 temp_equ = '' if len(temp_equ) > 2 and temp_equ[-1] == '0': # 运算符后面也不能出现0+数字的情形03，09，x if (temp_equ[-2] in ['+', '-', '*', '÷']) and ( arg in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '(']): temp_equ = temp_equ[:-1] temp_equ = temp_equ + arg self.equation.set(temp_equ) def clear(self): self.equation.set('0') self.result.set(' ') def run(self): temp_equ = self.equation.get() temp_equ = temp_equ.replace('÷', '/') if temp_equ[0] in ['+', '-', '*', '÷']: temp_equ = '0' + temp_equ print(temp_equ) try: answer = '%.4f' % eval(temp_equ) # 保留两位小数 self.result.set(str(answer)) except (ZeroDivisionError, SyntaxError): # 其他除0错误，或语法错误返回Error self.result.set(str('Error'))
3、 测试实验与总结
1. 测试
if __name__ == "__main__": root = Tk() my_cal = Calculator(root) root.mainloop()

2. 总结
1. 按钮的command参数设置的是回调函数，点击按钮后的操作由函数完成。
2.将回调函数设置成匿名函数， lambda函数是可以传参数给回调函数。这个参数是在调用函数的时候才传入，不会在定义的时候保存。
3. 关于控件的类型，控件的属性，控件的放置方式
控件类型，可以通过多dir函数查看：比如dir(tkinter)，找到控件
控件：19 'Button','Canvas'画布,'Text'文本,'Checkbutton'复选按钮,'Radiobutton'单选按钮,'Frame'框架,'Message'消息, 'Entry'条目实体,'Label'标签,'LabelFrame'标签框架, 'Listbox'列表框,'Menu'菜单, 'Menubutton'菜单按钮, 'Scale'缩放,'Scrollbar'滚动条,'Toplevel'顶级,'Spinbox'旋转框,'PanedWindow'窗格窗口,'tkMessageBox'消息框
控件属性查看：dir(Button)把用得到的控件都查一遍，就清楚怎么用它们了。
放置方式–集合布局：当定义一个控件，均需要让它布局到窗口上，用到三个函数：pack,grid,place
pack的参数side,可设置上下左右布局;
grid的参数row,column，可设置行列网格布局；
place的参数x,y,可设置坐标布局。
同一块画布不能混合使用。
关于如何避免同志们乱写算式，使软件奔溃的问题。
本计算器计算功能是通过获取将输入表达式，利用eval()函数来执行python代码字符串的。

那么就要杜绝不合理的输入表达式，不能阻止人家乱点，可以在程序中设置try...except...else...finally语句捕捉异常，设置合理的的响应。

学习优化：学会优化冗于代码
优化方向：
1、 初始化布局页面代码冗余；
2、 lambda匿名函数包裹回调函数时传参的问题
代码实现：这里主要优化初始化函数，其他函数采用继承。
优化计算器类Calc继承Calculator的back,clear,run,getNum。增加initPage来定义页面控件布局。
# 计算器，优化程序 class Calc(Calculator): def __init__(self, master): self.master = master self.master.title("Calculator") self.master.resizable(0, 0) # 设置窗口不可拉伸 self.master.geometry('320x420') # 设置主窗口的初始尺寸 self.result = StringVar() # 用于显示结果的可变文本 self.equation = StringVar() # 显示计算方程 self.result.set(' ') self.equation.set('0') self.labels = ['<-', '(', ')', '÷', '7', '8', '9', '*', '4', '5', '6', '-', '1', '2', '3', '+', 'MC', '0', '.', '=', ] # 显示框 self.show_result_eq = Label(self.master, bg='white', fg='black', font=('Arail', '16'), bd='0', textvariable=self.equation, anchor='se') self.show_result = Label(self.master, bg='white', fg='black', font=('Arail', '20'), bd='0', textvariable=self.result, anchor='se') # 按钮 # self.button_dict = {} # Layout布局 self.show_result_eq.place(x='10', y='10', width='300', height='50') self.show_result.place(x='10', y='60', width='300', height='50') self.initPage() def initPage(self): X = ['10', '90', '170', '250'] Y = ['150', '205', '260', '315', '370'] lengths = len(self.labels) # 20 y_ = -1 # 设置按钮并布局 for label in self.labels: print(label) index = self.labels.index(label) x_ = index % 4 if x_ == 0: y_ += 1 if label == '<-': button = Button(self.master, text=label, bg='DarkGray', command=self.back) button.place(x=X[x_], y=Y[y_], width='60', height='40') elif label == '=': button = Button(self.master, text=label, bg='DarkGray', command=self.run) button.place(x=X[x_], y=Y[y_], width='60', height='40') elif label == 'MC': button = Button(self.master, text=label, bg='DarkGray', command=self.clear) button.place(x=X[x_], y=Y[y_], width='60', height='40') else: # 因为lambda函数有传参功能，但只有调用的时候才传参，所以数字按钮永远会调用最后一个label值。解决方案是自己洗一个button来保存label值 button = NumButton(self.master, text=label, bg='DarkGray', fun=self.getNum) button.btn.place(x=X[x_], y=Y[y_], width='60', height='40') # self.button_dict[label] = button
重点：以上代码，倒数第二行，我采用自定义的NumButton,而不是原有的Butoon。
因为即使匿名函数lambda函数有传参功能，但只有调用的时候才传参，所以for循环到最后，label变量的值永远为列表最后一个=等于符号，一度让人无解，只能自定义一个按钮类型NumButton，来保存中间值。使按钮们都有自己的文本值，并且command回调的时候能够准确传参。
class NumButton(): def __init__(self, frame, text, fun, **kwargs): # side = kwargs.get('side') if 'side' in kwargs else () # 此处没用上 self.btn = Button( frame, text=text, activeforeground="blue", activebackground="pink", bg='DarkGray', command=lambda: fun(text) )
注意
形式参数frame, text, fun
frame是根部件。
text按钮的标签文本，
fun函数对象，就是待回调的函数。
测试总结
if __name__ == "__main__": root = Tk() # my_cal = Calculator(root) my_cal = Calc(root) root.mainloop()

自定义的NumButton设置了按钮激活时背景和字体的颜色变化，所以有点颜色。Button自己也可以设置的。
测试没有问题，就要开始打包。
1.确定安装pyinstaller包，没有可以在环境下安装：conda install pyinstaller或pip install pyinstaller
2. 打包：pyinstaller -F calc.py -w
3. 打包生成可执行exe文件不了解请参考博文。