三、图形用户界面和游戏开发
1、基于tkinter模块的GUI
Python默认的GUI开发模块是tkinter(在Python 3以前的版本中名为Tkinter),从这个名字就可以看出它是基于Tk的,Tk是一个工具包,最初是为Tcl设计的,后来被移植到很多其他的脚本语言中,它提供了跨平台的GUI控件。当然Tk并不是最新和最好的选择,也没有功能特别强大的GUI控件,事实上,开发GUI应用并不是Python最擅长的工作,如果真的需要使用Python开发GUI应用,wxPython、PyQt、PyGTK等模块都是不错的选择。
基本上使用tkinter来开发GUI应用需要以下5个步骤:
- 导入tkinter模块中我们需要的东西。
- 创建一个顶层窗口对象并用它来承载整个GUI应用。
- 在顶层窗口对象上添加GUI组件。
- 通过代码将这些GUI组件的功能组织起来。
- 进入主事件循环(main loop)。
下面的代码演示了如何使用tkinter做一个简单的GUI应用。
# Step1、导入tkinter模块中我们需要的东西
import tkinter
import tkinter.messagebox
def main():
flag = True
# 定义按钮触发的事件:修改标签上的文字
def change_label_text():
nonlocal flag
flag = not flag
color, msg = ('red', 'Hello, world!')\
if flag else ('blue', 'Goodbye, world!')
label.config(text=msg, fg=color)
# 定义按钮触发的事件:确认退出
def confirm_to_quit():
if tkinter.messagebox.askokcancel('温馨提示', '确定要退出吗?'):
top.quit() # 退出顶级会话窗口
# Step2、创建一个顶层窗口对象并用它来承载整个GUI应用
top = tkinter.Tk()
# 2.1 设置顶层窗口的大小
top.geometry('240x160') # 小写字母x
# 2.2 设置窗口标题
top.title('小游戏')
# Step3、在顶层窗口对象上添加GUI组件
# 3.1 创建标签对象并添加到顶层窗口
label = tkinter.Label(top, text='Hello, world!', font='Arial -32', fg='red')
label.pack(expand=1)
# 3.2 创建按钮对象
# 3.2.1 创建一个装按钮的容器
panel = tkinter.Frame(top)
# 3.2.2 创建按钮对象:指定添加到哪个容器中,通过command参数绑定事件回调函数
button1 = tkinter.Button(panel, text='修改', command=change_label_text)
button1.pack(side='left') # pack方法用来设置组件放置的位置
button2 = tkinter.Button(panel, text='退出', command=confirm_to_quit)
button2.pack(side='right')
panel.pack(side='bottom')
# Step4、开启主事件循环
tkinter.mainloop()
if __name__ == '__main__':
main()
点击“修改”:
点击“退出”:
需要说明的是,GUI应用通常是事件驱动式的,之所以要进入主事件循环就是要监听鼠标、键盘等各种事件的发生并执行对应的代码对事件进行处理,因为事件会持续的发生,所以需要这样的一个循环一直运行着等待下一个事件的发生。另一方面,Tk为控件的摆放提供了三种布局管理器,通过布局管理器可以对控件进行定位,这三种布局管理器分别是:Placer(开发者提供控件的大小和摆放位置)、Packer(自动将控件填充到合适的位置)和Grid(基于网格坐标来摆放控件),此处不进行赘述。
编写一个文本编辑器
import tkinter
import tkinter.filedialog
import tkinter.colorchooser
import tkinter.messagebox
import tkinter.scrolledtext
# 创建应用程序顶层窗口并设置属性,后面将用它来承载整个GUI应用
app = tkinter.Tk()
app.title('My Notepad')
app['width'] = 800
app['height'] = 600
# 创建菜单
menu = tkinter.Menu(app) #(最顶上的菜单行)
submenu_file = tkinter.Menu(menu, tearoff=0) #(File菜单)
submenu_edit = tkinter.Menu(menu, tearoff=0) #(Edit菜单)
submenu_help = tkinter.Menu(menu, tearoff=0) #(Help菜单)
textChanged = tkinter.IntVar(value=0) # 与是否更改了当前文本关联的变量,默认没有更改
filename = ''# 当前文件名
# --------------------------------设置File菜单------------------------------------
# 在File菜单中添加Open选项,并创建相应的事件处理函数
def Open():
global filename
# 如果内容已改变,先保存
if textChanged.get(): # .get()???
yesno = tkinter.messagebox.askyesno(title="Save or not?", message="Do you want to save?")
if yesno == tkinter.YES:
Save()
filename = tkinter.filedialog.askopenfilename(title="Open file", filetypes=[('Text files', '*.txt')])
if filename:
# 清空内容
txtContent.delete(0.0, tkinter.END) # 0.0 表示lineNumber.Column
fp = open(filename, 'r')
txtContent.insert(tkinter.INSERT, ''.join(fp.readlines()))
fp.close()
# 标记为尚未修改
textChanged.set(0)
# 创建Open选项并绑定菜单事件处理函数
submenu_file.add_command(label='Open', command=Open)
# 在File菜单中添加Save选项,并创建相应的事件处理函数
def Save():
global filename
# 如果是第一次保存新文件,则打开“另存为”窗口
if not filename:
SaveAs()
# 如果不是新文件,而是对已存在的文件进行了修改,则保存
elif textChanged.get():
fp = open(filename, 'w')
fp.write(txtContent.get(0.0, tkinter.END))
fp.close()
textChanged.set(0)
# 创建Save选项并绑定菜单事件处理函数
submenu_file.add_command(label='Save', command=Save)
# 在File菜单中添加SaveAs选项,并创建相应的事件处理函数
def SaveAs():
global filename
# 打开“另存为”窗口
newfilename = tkinter.filedialog.asksaveasfilename(title='Save As', initialdir=r'c:\\', initialfile='new.txt')
# 如果指定了文件名,则保存文件
if newfilename:
fp = open(newfilename, 'w')
fp.write(txtContent.get(0.0, tkinter.END))
fp.close()
filename = newfilename
textChanged.set(0)
# 创建Save As选项并绑定菜单事件处理函数
submenu_file.add_command(label='Save As', command=SaveAs)
# 在File菜单中添加分割线
submenu_file.add_separator()
# 在File菜单中添加Close选项,并创建相应的事件处理函数
def Close():
global filename
Save()
txtContent.delete(0.0, tkinter.END)
# 置空文件名
filename = ''
# 创建Close选项并绑定菜单事件处理函数
submenu_file.add_command(label='Close', command=Close)
# 将子菜单File添加到主菜单(最顶上的菜单行)上
menu.add_cascade(label='File', menu=submenu_file)
# --------------------------------设置Edit菜单------------------------------------
# 撤销最后一次操作
def Undo():
# 启用Undo标志
txtContent['undo'] = True
try:
txtContent.edit_undo()
except Exception as e:
pass
submenu_edit.add_command(label='Ubdo', command=Undo)
# 重做最后一次操作
def Redo():
# 启用Redo标志
txtContent['redo'] = True
try:
txtContent.edit_redo()
except Exception as e:
pass
submenu_edit.add_command(label='Redo', command=Redo)
# 添加分割线
submenu_edit.add_separator()
# 复制
def Copy():
txtContent.clipboard_clear()
txtContent.clipboard_append(txtContent.selection_get())
submenu_edit.add_command(label='Copy', command=Copy)
# 剪切
def Cut():
Copy()
# 删除所选内容
txtContent.delete(tkinter.SEL_FIRST, tkinter.SEL_LAST)
submenu_edit.add_command(label='Cut', command=Cut)
# 粘贴
def Paste():
# 如果没有选中内容,则直接粘贴到鼠标位置
# 如果有所选内容,则先删除再粘贴
try:
txtContent.insert(tkinter.SEL_FIRST, txtContent.cliboard_get())
txtContent.delete(tkinter.SEL_FIRST, tkinter.SEL_LAST)
# 如果粘贴成功就结束本函数,以免异常处理结构执行完成之后再次粘贴
return
except Exception as e:
pass
txtContent.insert(tkinter.INSERT, txtContent.cliboard_get())
submenu_edit.add_command(label='Paste', command=Paste)
# 添加分割线
submenu_edit.add_separator()
# 查找文本
def Search():
# 获取要查找的内容
textToSearch = tkinter.simpledialog.askstring(title='Search', prompt='What to search?')
start = txtContent.search(textToSearch, 0.0, tkinter.END)
if start:
tkinter.messagebox.showinfo(title='Found', message='OK')
submenu_edit.add_command(label='Search', command=Search)
# 将子菜单Edit添加到主菜单(最顶上的菜单行)上
menu.add_cascade(label='Edit', menu=submenu_edit)
# --------------------------------设置Help菜单------------------------------------
def About():
tkinter.messagebox.showinfo(title='About', message='Author: Zhu Junyan')
submenu_help.add_command(label='About', command=About)
menu.add_cascade(label='Help', menu=submenu_help)
#-------------------------------------------------------------------------------
# 将创建的(最顶上的)菜单关联到应用程序窗口
app.config(menu=menu)
# 创建文本编辑组件,并自动适应窗口大小
txtContent = tkinter.scrolledtext.ScrolledText(app, wrap=tkinter.WORD)
txtContent.pack(fill=tkinter.BOTH, expand=tkinter.YES)
def KeyPress(event):
textChanged.set(1)
txtContent.bind('<KeyPress>', KeyPress)
#-------------------------------------------------------------------------------
app.mainloop()
2、使用Pygame进行游戏开发
Pygame是一个开源的Python模块,专门用于多媒体应用(如电子游戏)的开发,其中包含对图像、声音、视频、事件、碰撞等的支持。Pygame建立在SDL的基础上,SDL是一套跨平台的多媒体开发库,用C语言实现,被广泛的应用于游戏、模拟器、播放器等的开发。而Pygame让游戏开发者不再被底层语言束缚,可以更多的关注游戏的功能和逻辑。
下面我们来完成一个简单的小游戏,游戏的名字叫“大球吃小球”,当然完成这个游戏并不是重点,学会使用Pygame也不是重点,最重要的我们要在这个过程中体会如何使用前面讲解的面向对象程序设计,学会用这种编程思想去解决现实中的问题。
注意:开始之前,需要下载Pygame模块,下载步骤请点击这里。
制作游戏窗口
import pygame
def main():
# 初始化导入的pygame中的模块
pygame.init()
# 初始化用于显示的窗口并设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
running = True
# 开启一个事件循环处理发生的事件
while running:
# 从消息队列中获取事件并对事件进行处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if __name__ == '__main__':
main()
在窗口中绘图
可以通过pygame中draw模块的函数在窗口上绘图,可以绘制的图形包括:线条、矩形、多边形、圆、椭圆、圆弧等。需要说明的是,屏幕坐标系是将屏幕左上角设置为坐标原点(0, 0),向右是x轴的正向,向下是y轴的正向,在表示位置或者设置尺寸的时候,我们默认的单位都是像素。所谓像素就是屏幕上的一个点,你可以用浏览图片的软件试着将一张图片放大若干倍,就可以看到这些点。pygame中表示颜色用的是色光三原色表示法,即通过一个元组或列表来指定颜色的RGB值,每个值都在0~255之间,因为是每种原色都用一个8位(bit)的值来表示,三种颜色相当于一共由24位构成,这也就是常说的“24位颜色表示法”。
import pygame
def main():
# 初始化导入的pygame中的模块
pygame.init()
# 初始化用于显示的窗口并设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
### 新增部分START
# 设置窗口的背景色(颜色是由红绿蓝三原色构成的元组)
screen.fill((242, 242, 242))
# 绘制一个圆(参数分别是: 屏幕, 颜色, 圆心位置, 半径, 0表示填充圆)
pygame.draw.circle(screen, (255, 0, 0), (100, 100), 30, 0)
# 刷新当前窗口(渲染窗口将绘制的图像呈现出来)
pygame.display.flip()
### 新增部分END
running = True
# 开启一个事件循环处理发生的事件
while running:
# 从消息队列中获取事件并对事件进行处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if __name__ == '__main__':
main()
加载图像
如果需要直接加载图像到窗口上,可以使用pygame中image模块的函数来加载图像,再通过之前获得的窗口对象的blit方法渲染图像,代码如下所示。
import pygame
def main():
# 初始化导入的pygame中的模块
pygame.init()
# 初始化用于显示的窗口并设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
### 新增部分START
# 设置窗口的背景色(颜色是由红绿蓝三原色构成的元组)
screen.fill((255, 255, 255))
# 通过指定的文件名加载图像
ball_image = pygame.image.load('E:/fdj.png')
# 在窗口上渲染图像
screen.blit(ball_image, (50, 50))
### 新增部分END
# - 绘制一个圆(参数分别是: 屏幕, 颜色, 圆心位置, 半径, 0表示填充圆)
# - pygame.draw.circle(screen, (255, 0, 0), (100, 100), 30, 0)
# 刷新当前窗口(渲染窗口将绘制的图像呈现出来)
pygame.display.flip()
running = True
# 开启一个事件循环处理发生的事件
while running:
# 从消息队列中获取事件并对事件进行处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if __name__ == '__main__':
main()
实现动画效果
说到动画这个词大家都不会陌生,事实上要实现动画效果,本身的原理也非常简单,就是将不连续的图片连续的播放,只要每秒钟达到了一定的帧数,那么就可以做出比较流畅的动画效果。如果要让上面代码中的小球动起来,可以将小球的位置用变量来表示,并在循环中修改小球的位置再刷新整个窗口即可。
import pygame
def main():
# 初始化导入的pygame中的模块
pygame.init()
# 初始化用于显示的窗口并设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
### 增加/修改部分START
# 定义变量来表示小球在屏幕上的位置
x, y = 50, 50
running = True
# 开启一个事件循环处理发生的事件
while running:
# 从消息队列中获取事件并对事件进行处理
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 设置窗口的背景色(颜色是由红绿蓝三原色构成的元组)
screen.fill((255, 255, 255))
# 绘制一个圆(参数分别是: 屏幕, 颜色, 圆心位置, 半径, 0表示填充圆)
pygame.draw.circle(screen, (255, 0, 0,), (x, y), 30, 0)
# 刷新当前窗口(渲染窗口将绘制的图像呈现出来)
pygame.display.flip()
# 每隔50毫秒就改变小球的位置再刷新窗口
pygame.time.delay(50)
x, y = x + 5, y + 5
### 增加/修改部分END
if __name__ == '__main__':
main()
(下面的红色小球是变动着的)
碰撞检测
通常一个游戏中会有很多对象出现,而这些对象之间的“碰撞”在所难免,比如炮弹击中了飞机、箱子撞到了地面等。碰撞检测在绝大多数的游戏中都是一个必须得处理的至关重要的问题,pygame的sprite(动画精灵)模块就提供了对碰撞检测的支持,这里我们暂时不介绍sprite模块提供的功能,因为要检测两个小球有没有碰撞其实非常简单,只需要检查球心的距离有没有小于两个球的半径之和。为了制造出更多的小球,我们可以通过对鼠标事件的处理,在点击鼠标的位置创建颜色、大小和移动速度都随机的小球,当然要做到这一点,我们可以把之前学习到的面向对象的知识应用起来。
from enum import Enum, unique
from math import sqrt
from random import randint
import pygame
@unique
class Color(Enum):
"""颜色"""
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (242, 242, 242)
@staticmethod
def random_color():
"""获得随机颜色"""
r = randint(0, 255)
g = randint(0, 255)
b = randint(0, 255)
return (r, g, b)
class Ball(object):
"""球"""
def __init__(self, x, y, radius, sx, sy, color=Color.RED):
"""初始化方法"""
self.x = x
self.y = y
self.radius = radius
self.sx = sx
self.sy = sy
self.color = color
self.alive = True
def move(self, screen):
"""移动"""
self.x += self.sx
self.y += self.sy
if self.x - self.radius <= 0 or self.x + self.radius >= screen.get_width():
# 超过边界了,所以去“对过”
self.sx = -self.sx
if self.y - self.radius <= 0 or self.y + self.radius >= screen.get_height():
self.sy = -self.sy
def eat(self, other):
"""吃其他球"""
if self.alive and other.alive and self != other:
dx, dy = self.x - other.x, self.y - other.y
distance = sqrt(dx ** 2 + dy ** 2)
if distance < self.radius + other.radius and self.radius > other.radius:
# 检查球心的距离有没有小于两个球的半径之和,如果小于,则没有碰撞;
# 检查本球的半径是否大于其他球的半径,大于的话,才能吃其他球。
other.alive = False
self.radius = self.radius + int(other.radius * 0.146)
def draw(self, screen):
"""在窗口上绘制球"""
pygame.draw.cicle(screen, self.color, (self.x, self.y), self.radius, 0)
事件处理
可以在事件循环中对鼠标事件进行处理,通过事件对象的type属性可以判定事件类型,再通过pos属性就可以获得鼠标点击的位置。如果要处理键盘事件也是在这个地方,做法与处理鼠标事件类似。
from enum import Enum, unique
from math import sqrt
from random import randint
import pygame
@unique
class Color(Enum):
"""颜色"""
# 设置下面这些有何用?
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (242, 242, 242)
@staticmethod
def random_color():
"""获得随机颜色"""
r = randint(0, 255)
g = randint(0, 255)
b = randint(0, 255)
return (r, g, b)
class Ball(object):
"""球"""
def __init__(self, x, y, radius, sx, sy, color=Color.RED):
"""
初始化方法
:x/y: 球的圆心所在的位置的坐标
:sx/sy: 将要在x/y轴上移动的距离
:radius: 圆的半径
:color: 圆的填充色
"""
self.x = x
self.y = y
self.radius = radius
self.sx = sx
self.sy = sy
self.color = color
self.alive = True
def move(self, screen):
"""移动"""
self.x += self.sx
self.y += self.sy
if self.x - self.radius <= 0 or self.x + self.radius >= screen.get_width():
# 超过边界了,所以去“对过”
self.sx = -self.sx
if self.y - self.radius <= 0 or self.y + self.radius >= screen.get_height():
self.sy = -self.sy
def eat(self, other):
"""吃其他球"""
if self.alive and other.alive and self != other:
dx, dy = self.x - other.x, self.y - other.y
distance = sqrt(dx ** 2 + dy ** 2)
if distance < self.radius + other.radius and self.radius > other.radius:
# 检查球心的距离有没有小于两个球的半径之和,如果小于,则没有碰撞;
# 检查本球的半径是否大于其他球的半径,大于的话,才能吃其他球。
other.alive = False
self.radius = self.radius + int(other.radius * 0.146)
def draw(self, screen):
"""在窗口上绘制球"""
pygame.draw.circle(screen, self.color, (self.x, self.y), self.radius, 0)
def main():
# 1、定义用来装有所有球的容器
balls = []
# 2、初始化导入的pygame中的模块
pygame.init()
# 3.1、初始化 用于显示的窗口 并 设置窗口尺寸
screen = pygame.display.set_mode((800, 600))
# 3.2、设置当前窗口的标题
pygame.display.set_caption('大球吃小球')
running = True
# 4、开启一个事件循环处理发生的事件
while running:
# 4.1、从消息队列中获取事件并对事件进行处理——鼠标点击到的位置将创建一个球
for event in pygame.event.get():# ?????如何结束这个for循环呢,这个不断创建小球的for循环??
# 4.1.0、判断用户是否点击了关闭按钮
if event.type == pygame.QUIT:
running = False
# 4.1.1、通过事件对象的type属性可以判定事件类型,下面是处理鼠标事件的代码
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
# 4.1.1.1、通过pos属性获得点击鼠标的位置
x, y = event.pos
# 4.1.1.2、在点击鼠标的位置创建一个球(大小、速度和颜色随机)
radius = randint(10, 100) # 半径
sx, sy = randint(-10, 10), randint(-10, 10)
color = Color.random_color()
ball = Ball(x, y, radius, sx, sy, color)
# 4.1.1.3、将球添加到列表容器中
balls.append(ball)
# 4.2、设置窗口的背景色(颜色是由红绿蓝三原色构成的元组)
screen.fill((255, 255, 255))
# 4.3、取出容器中的球,如果没被吃掉就绘制,被吃掉了就移除
for ball in balls:
if ball.alive:
ball.draw(screen)
else:
balls.remove(ball) # ???????????//没有此方法啊
# 4.4、刷新当前窗口(渲染窗口将绘制的图像呈现出来)
pygame.display.flip()
# 4.5、每隔50毫秒就改变球的位置再刷新窗口
pygame.time.delay(50)
for ball in balls:
ball.move(screen)
# 检查球有没有吃到其他的球
for other in balls:
ball.eat(other)
if __name__ == '__main__':
main()
(PS. 运行过程并不能深入理解,因为还没有学习event事件等相关内容,此处暂时不处理,先继续向下学习。否则自己去找资源学习的话,不一定能学到准确的点上,且目前也不是针对那些的学习。)
运行效果如下:用户可以一直点击屏幕,每点击一个地方就出现一个圆,这些圆不停地移动,当发生碰撞时,大球将消灭掉小球,同时大球的半径会增加。
准确的说它算不上一个游戏,但是做一个小游戏的基本知识我们已经通过这个例子告诉大家了,有了这些知识已经可以开始你的小游戏开发之旅了。其实上面的代码中还有很多值得改进的地方,比如刷新窗口以及让球移动起来的代码并不应该放在事件循环中,等学习了多线程的知识后,用一个后台线程来处理这些事可能是更好的选择。如果希望获得更好的用户体验,我们还可以在游戏中加入背景音乐以及在球与球发生碰撞时播放音效,利用pygame的mixer和music模块,我们可以很容易的做到这一点,大家可以自行了解这方面的知识。事实上,想了解更多的关于pygame的知识,最好的教程是pygame的官方网站,如果英语没毛病就可以赶紧去看看啦。 如果想开发3D游戏,pygame就显得力不从心了,对3D游戏开发如果有兴趣的读者不妨看看Panda3D。