Day.11
2020.02.29
今天主要是对tkinter和pygame的初步认识,为了锻炼自己的实际操作能力,接下来几天的任务是制作一个简单的计算器。
1.tkinter
首先需要知道一个概念:GUI(Graphical User Interface,图形用户界面),以下是百度百科对于图形用户界面的定义:
图形用户界面是一种人与计算机通信的界面显示格式,允许用户使用鼠标等输入设备操纵屏幕上的图标或菜单选项,以选择命令、调用文件、启动程序或执行其它一些日常任务。与通过键盘输入文本或字符命令来完成例行任务的字符界面相比,图形用户界面有许多优点。图形用户界面由窗口、下拉菜单、对话框及其相应的控制机制构成,在各种新式应用程序中都是标准化的,即相同的操作总是以同样的方式来完成,在图形用户界面,用户看到和操作的都是图形对象,应用的是计算机图形学的技术。
简单的来说,就是将我们所写的程序放在了一个窗口,通过用户的鼠标、键盘等等操作执行,而不是通过在命令指示符的终端输入数字运行。可以说,GUI和用户的交互性更强,也更便于用户的使用。
而tkinter则是python中默认包含的一个,可以让我们用来开发GUI应用的模块(当然,Tk并不是最新和最好的选择,也没有功能特别强大的GUI控件,事实上,开发GUI应用并不是Python最擅长的工作,如果真的需要使用Python开发GUI应用,wxPython、PyQt、PyGTK等模块都是不错的选择)。
基本上使用tkinter来开发GUI应用需要以下5个步骤:
- 导入tkinter模块中我们需要的东西
- 创建一个顶层窗口对象并用它来承载整个GUI应用
- 在顶层窗口对象上添加GUI组件
- 通过代码将这些GUI组件的功能组织起来
- 进入主事件循环(main loop)
在这里我把tkinter模块的元素作个简要说明:
tkinter类 | 元素 | 简要说明 |
---|---|---|
Button | 按钮 | 类似标签,但提供额外的功能,点击时执行一个动作,例如鼠标掠过、按下、释放以及键盘操作/事件 |
Canvas | 画布 | 提供绘图功能(直线、椭圆、多边形、矩形)可以包含图形或位图 |
Checkbutton | 复选框 | 允许用户选择或反选一个选项,一组方框,可以选择其中的任意个(类似HTML中的checkbox) |
Entry | 单行文本框 | 单行文字域,显示一行文本,用来收集键盘输入(类似HTML中的text) |
Frame | 框架 | 用来承载放置其他GUI元素,就是一个容器 |
Label | 标签 | 用于显示不可编辑的文本或图标 |
LabelFrame | 容器控件 | 是一个简单的容器控件,常用于复杂的窗口布局 |
Listbox | 列表框 | 一个选项列表,用户可以从中选择 |
Menu | 菜单 | 点下菜单按钮后弹出的一个选项列表,用户可以从中选择 |
Menubutton | 菜单按钮 | 用来包含菜单的组件(有下拉式、层叠式等等) |
Message | 消息框 | 类似于标签,但可以显示多行文本 |
OptionMenu | 选择菜单 | 下拉菜单的一个改版,弥补了Listbox无法下拉列表框的遗憾 |
PanedWindow | 窗口布局管理 | 是一个窗口布局管理的插件,可以包含一个或多个子控件 |
Radiobutton | 单选框 | 允许用户从多个选项中选取一个,一组按钮,其中只有一个可被“按下”(类似于HTML中的radio) |
Scale | 进度条 | 线性“滑块”组件,可设定起始值和结束值,会显示当前位置的精确值 |
Scrollbar | 滚动条 | 对其支持的组件(文本域、画布、列表框、文本框)提供滚动功能 |
Spinbox | 输入控件 | 与Entry类似,但是可以指定输入范围值 |
Text | 多行文本框 | 多行文字区域,显示多行文本,可用来收集(或显示)用户输入的文字(类似HTML中的textarea) |
Toplevel | 顶层 | 类似框架,为其他的控件提供单独的容器 |
messageBox | 消息框 | 用于显示你应用程序的消息框(Python2中为tkMessagebox) |
2.pygame
首先,Pygame是一个开源的Python模块,专门用于多媒体应用(如电子游戏)的开发,其中包含对图像、声音、视频、事件、碰撞等的支持。Pygame建立在SDL的基础上,SDL是一套跨平台的多媒体开发库,用C语言实现,被广泛的应用于游戏、模拟器、播放器等的开发。而Pygame让游戏开发者不再被底层语言束缚,可以更多的关注游戏的功能和逻辑。
如果说,tkinter是用来开发GUI应用的,那么pygame则更偏向于游戏方面的开发。当然,pygame的方法也有其局限性,如果想要进行3D游戏的开发,可以选择Panda3D。
今天主要是利用tkinter进行简单应用程序的开发,将来如果有时间利用pygame进行游戏开发的话我会在这里作补充。
3.今日总结
其实今天最主要的内容是了解tkinter各个类的作用和使用方法,如果想详细了解利用tkinter进行GUI开发的话,请阅读下文:
Python GUI之tkinter窗口视窗教程大集合(看这篇就够了)
目前我已经通过阅读此文章学习,能够初步实现了简易计算器的功能(两个个位数之间的加减乘除算法以及清零操作),但是参照windows自带的计算器,还有许多其他地方需要完善。我会在接下来的空余时间里一点点进行修改,如果功能较为完善的话,我会把最终的代码上传以供参考。
Day.12
2020.03.01
1.基于Tkinter的简易计算器项目
从昨天下午开始写到今天下午,基本上已经把简易计算器该有的功能都已经实现,以及可能出现的问题也都解决了(我能想到的一些问题用特殊数据测试的方法都已通过)。这里主要说一下简易计算器实现的思路:
首先,计算器的底层代码是非常简单的,一个显示屏,然后是一些数字键、取反键、取百分号键和运算符键。显示屏通过Label功能实现(Label显示的文本不可编辑,正好可以用来作为计算器的屏幕),而按键通过Button功能实现。在这里,我建议把其他键和运算符键的回调函数区分开来,因为其他键的主要功能仅仅只是输出在显示屏上(就像使用print函数打印一样),而运算符键的功能主要包括两个方面:一,把屏幕上用户输入的数字存储下来用来准备计算;二,判断和前一级运算符优先级大小的关系,从而决定是否压栈(这一点我会在之后讲到)。否则两者混淆在一起的话,写起代码来会比较混乱,思路也不会清晰。
前面讲到了运算符键的回调函数需要判断和前一级运算符优先级大小的关系,这里涉及到了数据结构的知识(我还没有学习到python的数据结构知识,这是大二时上C++的数据结构课时讲过的),虽然python中没有像C++一样自带栈stack(C++中直接#include即可使用),但是如果仔细观察的话会发现,python中的列表其实就是很好的一个栈容器:压栈是append()方法,而取出栈顶元素是pop()方法(当然,pop()中可以指定元素的位置取出某一具体位置的元素,如果括号中不填则默认最后一个元素,此外,pop()是有返回值的,返回值就是被取出的那个元素),那么就意味着,python完全可以像C++一样,通过建立数字栈和运算符栈来完成一系列的计算(特别是先加减后乘除的判断)。
我在这里简述一下如何通过两个栈来实现计算,如果想详细了解的话可以参考严蔚敏的《数据结构C语言版》:
假如说我要计算:1+2×3-4,那么从左到右开始压栈——刚开始时,数字栈和运算符栈都是空的,于是把数字“1”压入数字栈,然后继续往右读,紧接着把运算符“+”压入运算符栈,接着把数字“2”压入数字栈,下一个是运算符“×”,注意,这个时候就要开始判断了:如果即将压入的运算符的优先级大于栈顶运算符的优先级,则直接压入,不发生任何操作,反之,则先将栈顶运算符取出,和数字栈的两个数字(当然,数字栈的这两个数字也是要取出来的)进行运算,然后将运算得到的结果压入数字栈,再将即将压入的运算符压进运算符栈。以这题为例,运算符“×”是即将压入的运算符,这个时候需要先进行判断,显然,它比“+”的优先级大,因此直接压入,不产生任何操作,然后继续将数字“3”压入数字栈。
我们来回顾一下,此时,数字栈中有[1, 2, 3](这里我用Python的列表形式给出了,如果想好看一点可以将手机或者电脑逆时针旋转90度),运算符栈中有[’+’, ‘×’],即将压入的是’-’,因为’-‘的优先级小于’×’,所以我们需要从数字栈中取出2和3,从运算符栈中取出’×’,进行一次运算,得到6,然后把结果6压入数字栈中。此时,数字栈中有[1,6],而运算符栈中有[’+’],而即将压入的’-‘号优先级和栈顶的’+‘号一样(优先级小于等于栈顶运算符,都要进行运算),因此,还需要进行一次运算,取出1和6,再取出’+’,得到结果7,压入数字栈。此时,数字栈中有[7],而运算符栈是空栈[],然后再压入’-‘和4。需要指出的是,如果是减号或者除号操作,一定是数字栈倒数第二者减去(或除去)倒数第一者,此外,’='的优先级比上述任何运算符都要小,但是在计算器中,我们是通过按等号键来进行操作的,因此,需要在等号的回调函数中,我们还需要进行计算操作,并且要把运算符栈清空。
如果能够理解这个问题,那么计算器的大致功能就非常好实现了。如果想要规定运算符和其优先级大小的关系的话,我这里推荐使用字典,因为其键和值正好也是一对一的,键可以是运算符,而值可以是自己规定的数字。
还有一些细枝末节的东西我也就不多说了,修改起来也非常简单。
以下是我的代码:
#简易计算器
#作者:囷囷
import tkinter as tk
window = tk.Tk()
window.title('简易计算器')
window.geometry('300x200')
var = tk.StringVar()
var.set(0)
screen = tk.Label(window, textvariable=var, bg='white', fg='black', font=('Arial', 15), width=32, height=2)
screen.pack()
sum, temp1, temp2 = 0, -1, -1
yunsuanfu = {'+': 1, '-': 1, '×': 2, '÷': 2, '^': 3} # 用字典的键和值对应运算符和优先级
shuzi_stack = [] # 通过堆栈的方法实现多个不同符号时运算优先级的判断和计算:如果压入的运算符比栈顶运算符的优先级大,则压栈,
# 否则先运算,然后去除栈顶运算符,再压栈 栈顶运算符:yunsaunfu_stack[len(yunsaunfu_stack)-1]
yunsuanfu_stack = []
tempstr = ''
tempyunsuanfu = ''
def jia():
global yunsuanfu_stack, shuzi_stack, tempstr
if tempstr:
shuzi_stack.append(float(tempstr))
tempstr=''
if yunsuanfu_stack: # 当运算符栈不为空时
dengyu()
yunsuanfu_stack.append('+') # 先计算,再压栈
else: # 当运算符栈为空时
yunsuanfu_stack.append('+') # 压栈
else:
shuzi_stack.append(sum)
if yunsuanfu_stack: # 当运算符栈不为空时
dengyu()
yunsuanfu_stack.append('+') # 先计算,再压栈
else: # 当运算符栈为空时
yunsuanfu_stack.append('+') # 压栈
def jian():
global yunsuanfu_stack, shuzi_stack, tempstr
if tempstr:
shuzi_stack.append(float(tempstr))
tempstr = ''
if yunsuanfu_stack: # 当运算符栈不为空时
dengyu()
yunsuanfu_stack.append('-') # 先计算,再压栈
else: # 当运算符栈为空时
yunsuanfu_stack.append('-') # 压栈
else:
shuzi_stack.append(sum)
if yunsuanfu_stack: # 当运算符栈不为空时
dengyu()
yunsuanfu_stack.append('-') # 先计算,再压栈
else: # 当运算符栈为空时
yunsuanfu_stack.append('-') # 压栈
def cheng():
global yunsuanfu_stack, shuzi_stack, tempstr
if tempstr:
shuzi_stack.append(float(tempstr))
tempstr = ''
if yunsuanfu_stack == [] or yunsuanfu['×'] > yunsuanfu[yunsuanfu_stack[len(yunsuanfu_stack)-1]]: # 优先级大,直接压栈
yunsuanfu_stack.append('×')
else:
dengyu()
yunsuanfu_stack.append('×') # 先计算,再压栈
else:
shuzi_stack.append(sum)
if yunsuanfu_stack == [] or yunsuanfu['×'] > yunsuanfu[yunsuanfu_stack[len(yunsuanfu_stack) - 1]]: # 优先级大,直接压栈
yunsuanfu_stack.append('×')
else:
dengyu()
yunsuanfu_stack.append('×') # 先计算,再压栈
def chu():
global yunsuanfu_stack, shuzi_stack, tempstr
if tempstr:
shuzi_stack.append(float(tempstr))
tempstr = ''
if yunsuanfu_stack == [] or yunsuanfu['÷'] > yunsuanfu[yunsuanfu_stack[len(yunsuanfu_stack) - 1]]: # 优先级大,直接压栈
yunsuanfu_stack.append('÷')
else:
dengyu()
yunsuanfu_stack.append('÷') # 先计算,再压栈
else:
shuzi_stack.append(sum)
if yunsuanfu_stack == [] or yunsuanfu['÷'] > yunsuanfu[yunsuanfu_stack[len(yunsuanfu_stack) - 1]]: # 优先级大,直接压栈
yunsuanfu_stack.append('÷')
else:
dengyu()
yunsuanfu_stack.append('÷') # 先计算,再压栈
def miyunsuan():
global yunsuanfu_stack, shuzi_stack, tempstr
if tempstr:
shuzi_stack.append(float(tempstr))
tempstr = ''
if yunsuanfu_stack == [] or yunsuanfu['^'] > yunsuanfu[yunsuanfu_stack[len(yunsuanfu_stack) - 1]]: # 优先级大,直接压栈
yunsuanfu_stack.append('^')
else:
dengyu()
yunsuanfu_stack.append('^') # 先计算,再压栈
else:
shuzi_stack.append(sum)
if yunsuanfu_stack == [] or yunsuanfu['^'] > yunsuanfu[yunsuanfu_stack[len(yunsuanfu_stack) - 1]]: # 优先级大,直接压栈
yunsuanfu_stack.append('^')
else:
dengyu()
yunsuanfu_stack.append('^') # 先计算,再压栈
def display0():
global tempstr
tempstr += '0'
var.set(tempstr)
def display1():
global tempstr
tempstr += '1'
var.set(tempstr)
def display2():
global tempstr
tempstr += '2'
var.set(tempstr)
def display3():
global tempstr
tempstr += '3'
var.set(tempstr)
def display4():
global tempstr
tempstr += '4'
var.set(tempstr)
def display5():
global tempstr
tempstr += '5'
var.set(tempstr)
def display6():
global tempstr
tempstr += '6'
var.set(tempstr)
def display7():
global tempstr
tempstr += '7'
var.set(tempstr)
def display8():
global tempstr
tempstr += '8'
var.set(tempstr)
def display9():
global tempstr
tempstr += '9'
var.set(tempstr)
def display_qufan():
global tempstr, shuzi_stack
if tempstr[0] != '-':
temp = tempstr
tempstr = '-' + temp
var.set(tempstr)
else:
temp = tempstr
tempstr = temp[1:]
var.set(tempstr)
def dengyu():
global temp1, temp2, tempstr, sum, yunsuanfu_stack, shuzi_stack, tempyunsuanfu
if tempstr:
shuzi_stack.append(float(tempstr))
tempstr = ''
else:
pass
if yunsuanfu_stack:
while True:
if yunsuanfu_stack[len(yunsuanfu_stack)-1] == '+':
tempyunsuanfu = yunsuanfu_stack.pop()
temp2 = shuzi_stack.pop()
temp1 = shuzi_stack.pop()
sum = temp1 + temp2
shuzi_stack.append(sum)
if yunsuanfu_stack == []:
break
elif yunsuanfu_stack[len(yunsuanfu_stack)-1] == '-':
tempyunsuanfu = yunsuanfu_stack.pop()
temp2 = shuzi_stack.pop()
temp1 = shuzi_stack.pop()
sum = temp1 - temp2
shuzi_stack.append(sum)
if yunsuanfu_stack == []:
break
elif yunsuanfu_stack[len(yunsuanfu_stack)-1] == '×':
tempyunsuanfu = yunsuanfu_stack.pop()
temp2 = shuzi_stack.pop()
temp1 = shuzi_stack.pop()
sum = temp1 * temp2
shuzi_stack.append(sum)
if yunsuanfu_stack == []:
break
elif yunsuanfu_stack[len(yunsuanfu_stack)-1] == '÷':
tempyunsuanfu = yunsuanfu_stack.pop()
temp2 = shuzi_stack.pop()
temp1 = shuzi_stack.pop()
sum = temp1 / temp2
shuzi_stack.append(sum)
if yunsuanfu_stack == []:
break
else:
tempyunsuanfu = yunsuanfu_stack.pop()
temp2 = shuzi_stack.pop()
temp1 = shuzi_stack.pop()
sum = temp1 ** temp2
shuzi_stack.append(sum)
if yunsuanfu_stack == []:
break
var.set('%.3f' % sum)
else:
if tempyunsuanfu:
yunsuanfu_stack.append(tempyunsuanfu)
shuzi_stack.append(temp2)
tempyunsuanfu = ''
dengyu()
else:
sum = shuzi_stack[0]
var.set('%.3f' % sum)
def display_xiaoshudian():
global tempstr
if tempstr == '':
tempstr = '0'
tempstr += '.'
var.set(tempstr)
def display_baifenhao():
global tempstr
temp = tempstr
tempstr = float(temp)/100
var.set(tempstr)
def clear():
global sum, temp1, temp2, yunsuanfu_stack, shuzi_stack, tempstr, tempyunsuanfu
sum, temp1, temp2 = 0, -1, -1
tempstr, tempyunsuanfu = '', ''
shuzi_stack, yunsuanfu_stack = [], []
var.set(0)
Baifenhao = tk.Button(window, text='%', font=('Arial', 12), width=5, height=1, command=display_baifenhao)
Xiaoshudian = tk.Button(window, text='.', font=('Arial', 12), width=5, height=1, command=display_xiaoshudian)
Qufan = tk.Button(window, text='+/-', font=('Arial', 12), width=5, height=1, command=display_qufan)
Ling = tk.Button(window, text='0', font=('Arial', 12), width=5, height=1, command=display0)
Yi = tk.Button(window, text='1', font=('Arial', 12), width=5, height=1, command=display1)
Er = tk.Button(window, text='2', font=('Arial', 12), width=5, height=1, command=display2)
San = tk.Button(window, text='3', font=('Arial', 12), width=5, height=1, command=display3)
Si = tk.Button(window, text='4', font=('Arial', 12), width=5, height=1, command=display4)
Wu = tk.Button(window, text='5', font=('Arial', 12), width=5, height=1, command=display5)
Liu = tk.Button(window, text='6', font=('Arial', 12), width=5, height=1, command=display6)
Qi = tk.Button(window, text='7', font=('Arial', 12), width=5, height=1, command=display7)
Ba = tk.Button(window, text='8', font=('Arial', 12), width=5, height=1, command=display8)
Jiu = tk.Button(window, text='9', font=('Arial', 12), width=5, height=1, command=display9)
Miyunsuan = tk.Button(window, text='x^y', font=('Arial', 12), width=5, height=1, command=miyunsuan)
Qingling = tk.Button(window, text='C', font=('Arial', 12), width=5, height=1, command=clear)
Chuhao = tk.Button(window, text='÷', font=('Arial', 12), width=5, height=1, command=chu)
Chenghao = tk.Button(window, text='×', font=('Arial', 12), width=5, height=1, command=cheng)
Jianhao = tk.Button(window, text='-', font=('Arial', 12), width=5, height=1, command=jian)
Jiahao = tk.Button(window, text='+', font=('Arial', 12), width=5, height=1, command=jia)
Denghao = tk.Button(window, text='=', font=('Arial', 12), width=5, height=1, command=dengyu)
Baifenhao.place(x=240, y=150)
Xiaoshudian.place(x=120, y=150)
Qufan.place(x=0, y=150)
Ling.place(x=60, y=150)
Yi.place(x=0, y=115)
Er.place(x=60, y=115)
San.place(x=120, y=115)
Si.place(x=0, y=80)
Wu.place(x=60, y=80)
Liu.place(x=120, y=80)
Qi.place(x=0, y=45)
Ba.place(x=60, y=45)
Jiu.place(x=120, y=45)
Miyunsuan.place(x=240, y=45)
Qingling.place(x=180, y=45)
Chuhao.place(x=240, y=80)
Chenghao.place(x=240, y=115)
Jianhao.place(x=180, y=80)
Jiahao.place(x=180, y=115)
Denghao.place(x=180, y=150)
window.mainloop()
如果需要将.py文件转成.exe文件的话就要安装一个pyinstaller的包,我在这里直接用anaconda的prompt输入指令pip install pyinstaller(注意需要用管理员运行,否则会报错)。然后再通过cd指令转到py文件所在的目录,输入pyinstaller -F -w calculator.py即可进行转换,这里-F表示打包,-w表示不需要dos窗口(如果不加-w的话,打开.exe文件会弹出一个dos窗口和程序本身)。
补充:上述代码在百分号和小数点的功能方面还是需要完善的。
- 安装pyinstaller的过程:
- 安装好之后的exe文件:
- 计算器的界面:
(还需要改善的地方有:百分号和小数点的后续功能、浮点数和整数计算时显示位数的问题)
2.今日总结
总的来说,花的时间并不多吧,从昨天下午到今天下午,算上一直在写程序的时间其实也就没几个小时(大部分时间在睡觉),而且最难的也只是通过栈解决运算符优先级大小的问题,如果想到这一方面的话,做起来会更快,我主要是一开始没往这方面想(怕难),然后今天早上又重新起来全部改了一遍代码,最后调试成功了。通过Tkinter写一个简易计算器是一次非常好的编程体验,虽然中间会遇到许多许多莫名其妙的问题(比如空列表无法判断、空字符串无法强制类型转换等等),但是一一去解决这些困难,到最后完成这个项目,依然会有一种很大的成就感。
补充:刚才发现了python内置有eval()这个函数,可以用来执行字符串表达式并返回表达式的值,只能说,真是相恨见晚呐。所以我们在学习的时候一定要多看看程序语言内置的函数和标准库,在实际解决问题的时候会方便不少。
下一个项目是通过pygame编写简单的贪吃蛇程序,但是明天要正式开学了,可能每天分配的时间不会很多。