抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

说在前面

我于繁忙学业中和比赛中挤出时间总结记录了这次的开发经历,如有纰漏,请见谅。

下文中,许多时候将以 pgz 指代 Pygame

你可能疑惑,为什么我需要使用 Pygame Zreo 进行开发?的确,我不是三岁大的小孩,但可惜我们学校要求我们几个用 pgz 开发一两个 GUI 程序,没办法,那就用呗。不过,使用 pgz,确实有点拿手指推汽车的意思在里面,所以本次记录纯当是个人为巩固和学习 Python 基础的学习笔记就好,当然,如果能给你些许帮助,那当然更好。

本文将以实现 多页面管理 这一 GUI 程序常见的问题展开,阐述一些我的感触和经验,以及一些其他功能的实现,希望你看的愉快。

Pygame Zero:精简版中的精简版

常有耳闻:

人生苦短,我用 Python

的确,Python 以其 简明、优雅、强大 的宗旨而著称,可见,排在前头的固然是简明。个人认为,这里的简明指的是 简单优雅,显然,简单又排在优雅之前。

相信我要表达的意思很明确,Python 是一款简单的编程语言,而 Pygame 是 Python 著名的 2D 游戏开发库,相较于市面上各大游戏开发所使用的语言,其最大的不同依旧是简单,如果你也想用 Python 实现简单的游戏和 GUI ,这一定是个不赖的选择。

而这篇文章的主题,Pygame Zero,或许在座的各位甚至于没有听闻过其名,先看看它的官方文档对其的定义?

Pygame Zero是一个基于Pygame的简化版,让你可以用Python编写2D游戏,而不需要担心复杂的设置和代码。

没错,这个库确实是为 三岁大的小孩 而生的,本质上,它是一种进化的少儿编程,它二次封装了 Pygame 的一些复杂的实现,使得开发者可以更快速、更容易地开发游戏,而不必关心 Pygame 的一些底层细节。

但截至到本文发布时,我不推荐使用其进行开发,原因如下:

  1. 技术问题频出。这点的确使人很恼火,以至于有时甚至需要开发者本末倒置,调用 Pygame API 来帮助 pgz 修复一些奇怪的问题;
  2. 严格限制了代码书写和文件命名。在你的开发中,你需要严格按格式书写某些代码,例如一个 pgz 文件中一定存在 draw()update() 函数,与 Pygame 不同,pgz 程序的主循环写死在了其源码中,本质上,在其主循环中,只会调用这两个函数,这实际上也是低自定义的体现;
  3. 反直觉的对象调用方法。

说了这么多,其实从 pgz 之初衷来看,真正的问题只是第一条,剩下的其实都是我们早已习惯精细化操控的缘故,如果你能够适应,或许你能使用它进行项目的前期验证之类的工作,这是个好方法。

问题引入

话已至此,我们还没有指出在标题中的所谓 多页面管理 是什么。简单点说,就是从一个页面到另一个页面,或许你可以理解为 PPT 中的翻页操作?

有些同学肯定要说,这个我知道,直接 import 即可。的确,在 Python 中,import 事实上就是运行你所 import 的 module,但生产环境中问题真有这么简单吗?试试就知道了。

方案一:直接 import page

不多废话,让我们从头开始书写一个 pgz 项目。这是该项目的目录结构:

1
2
3
4
5
E:.
+---blog_pgz_test
| main.py
| page1.py
| tools.py

简单对其作一些解释。

main.py 程序入口

page1.py 将要被引导的页面

tools.py 模拟一个项目中存放一些常用操作的库


首先,完成 main.py 内代码:

1
2
3
4
5
6
7
8
9
import pgzrun

def draw():
screen.fill((255,255,255))

def on_mouse_down():
import page1

pgzrun.go()

你肯定很疑惑 screen 是从哪冒出来的,并且怀疑是不是我粗心大意提供了错误的代码,你别说,还真不是。

完成 page1.py 内代码:

1
2
3
4
5
6
import pgzrun

def draw():
screen.fill((255,0,0))

pgzrun.go()

代码很简单,不需要我解释,不过在运行之前,我们先要明确我们想要的效果。

当 程序入口 被调用时,弹出 pgz 游戏窗口,背景被填充为白色。

当 窗口内被点击后,窗口被填充为红色。

实际效果如何呢?

第一步的确没有任何问题,弹出了一个背景被填充为白色的窗口:

可当 user 点击窗口内部时,竟没有任何反应,关掉窗口程序又抛出一个错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Traceback (most recent call last):
File "e:\Can1425\TK\blog_pgz_test\main.py", line 9, in <module>
pgzrun.go()
File "E:\Can1425\Python312\Lib\site-packages\pgzrun.py", line 31, in go
run_mod(mod)
File "E:\Can1425\Python312\Lib\site-packages\pgzero\runner.py", line 113, in run_mod
PGZeroGame(mod).run()
File "E:\Can1425\Python312\Lib\site-packages\pgzero\game.py", line 217, in run
self.mainloop()
File "E:\Can1425\Python312\Lib\site-packages\pgzero\game.py", line 256, in mainloop
draw()
File "e:\Can1425\TK\blog_pgz_test\main.py", line 4, in draw
screen.fill((255,255,255))
File "E:\Can1425\Python312\Lib\site-packages\pgzero\screen.py", line 81, in fill
self.surface.fill(make_color(color))
pygame.error: display Surface quit

方案2:避免主循环冲突

有些有经验的同学可能已经发现了:

你这一看就不对啊,在 mian.pypage1.py 中都有相当于 Pygame 中的主循环的 pgzrun.go()

这个观点是正确的,不过是否可行呢?

分析代码可得,我们的 page1.py 中的 draw() 函数就是书写界面绘制逻辑的地方。那我们可用 main.py 中的主循环来直接运行它们。

修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
import pgzrun

def draw():
screen.fill((255,255,255))

def on_mouse_down():
import page1
page1.draw()

pgzrun.go()
1
2
3
4
5
import pgzrun, tools

def draw():
screen.fill((0,0,0))

能跑吗?不能跑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Traceback (most recent call last):
File "e:\Can1425\TK\blog_pgz_test\main.py", line 10, in <module>
pgzrun.go()
File "E:\Can1425\Python312\Lib\site-packages\pgzrun.py", line 31, in go
run_mod(mod)
File "E:\Can1425\Python312\Lib\site-packages\pgzero\runner.py", line 113, in run_mod
PGZeroGame(mod).run()
File "E:\Can1425\Python312\Lib\site-packages\pgzero\game.py", line 217, in run
self.mainloop()
File "E:\Can1425\Python312\Lib\site-packages\pgzero\game.py", line 247, in mainloop
self.dispatch_event(event)
File "E:\Can1425\Python312\Lib\site-packages\pgzero\game.py", line 172, in dispatch_event
handler(event)
File "E:\Can1425\Python312\Lib\site-packages\pgzero\game.py", line 164, in new_handler
return handler(**prepped)
^^^^^^^^^^^^^^^^^^
File "e:\Can1425\TK\blog_pgz_test\main.py", line 8, in on_mouse_down
page1.draw()
File "e:\Can1425\TK\blog_pgz_test\page1.py", line 4, in draw
screen.fill((0,0,0))
^^^^^^
NameError: name 'screen' is not defined

你一定会感到匪夷所思,不是说 pgz 准备好了 screen 对象吗?这点确实没错,因为当你给 page1.py 加上主循环单独运行,一点问题都没有:

那我们使用 Pygame 的方法来初始化 screen?

1
2
import pygame
screen = pygame.display.set_mode((100,100))

然而到 pgz 这儿是这样:

1
AttributeError: 'pygame.surface.Surface' object has no attribute 'surface'

其实,这个方法并不是行不通,我们可以换一种写法,直接使用 pgzero.screen 中的 Screen 类来初始化 screen 对象:

1
2
3
4
5
import pgzero.screen
import pgzrun, pygame

screen = pgzero.screen.Screen(pygame.Surface((0,0)))
# 注:事实上,这里 Surface 的大小给多少好像都行

单独运行依旧正常,可通过 main.py 引导却又是:

1
pygame.error: display Surface quit

显然,一步步看到这里,你已经陷入了死循环。

update() 函数与页面索引机制

其实,在上文所提的方案2中,我们距离解决问题的正确方案已经很接近了:你已经意识到了最本质的问题。既然无法实现,显然是姿势不对。

关于 update()draw()

pgz 程序有个可选函数叫做 update(),顾名思义,就是更新窗口所用。这个函数默认 1s 被 pgz 的主循环调用 60 次,即每秒 60 帧。

插播一个消息,事实上,Pygame Zero所有设置都是默认可选,所以一个空的文件也是一个合法可以运行的 Pygame Zero 脚本。

从理论上来说,draw() 函数只会被执行一次

然而事实似乎并不如此

所以,我们是否应该引入 update() 来实现我们的效果呢?

让我们做个简单尝试。

修改 main.py 的代码如下:

1
2
3
4
5
6
7
8
9
10
import pgzrun
import page1

def draw():
screen.fill((255,255,255))

def update():
page1.draw()

pgzrun.go()

修改 page1.py 的代码如下:

1
2
3
4
5
6
7
8
import pgzero.screen
import pygame

screen = pgzero.screen.Screen(pygame.Surface((0,0)))
# 必须初始化 screen,否则将有 NameError: name 'screen' is not defined

def draw():
screen.fill((255,0,0))

再次运行:

显然,程序入口的 update() 函数成功的调用了 page1.pydraw()函数,将 screen 填充为白色。

如果我们把程序入口处的 update() 函数改为一个 draw() 函数会发生什么呢?

有同学肯定要急了:

你这不是自己打自己脸吗?

实则不然,原因如下:

  1. draw() 将造成性能衰减
  2. draw() 将在窗口移动时出现渲染问题
  3. 我本身就想打我自己的脸

我们一条条来说。

首先是第一条,还记得我说 从理论上来说,draw() 函数只会被执行一次 吗?

从理论上 才是整句话的重心所在。

每当需要刷新(重绘)窗口的时候,draw()也会被调用,但与 update() 不同的是,它的帧率被砍了半。我想,造成的影响就不用我多解释了。

第二条很好说,你可以看看之前所有没有使用 update() 的代码运行情况的截图,结果就是你会发现窗口地下有一部分都没被渲染上,至于为什么,我卖个关子,可以自行尝试研究。

第三条又是什么鬼?事实上,上文的那个 简单尝试 不过是个幌子。

不会真有人试了吧

如果你硬是要让我解释做这个尝试有何目的,实际上我也说得出来:

  1. 打我自己的脸
  2. 浪费读者的阅读兴趣
  3. 引出下文内容

事实上,我认为 draw() 函数没有其存在的必要,在我使用 pgz 编写 GUI 程序时,我几乎不使用 draw()

页面索引机制

索引,一个很生动的词。当你带着查阅某个字词的目的拿起一本字典,你要做的肯定是先通过各种方式找到这个字或词,而不是把书翻来覆去。

当你把书翻来覆去的时候,这个动作在本质上就不存在了意义,你所带有的目的也成了空谈

在字典中,找到一个字或词的方法叫做索引

在书写程序时,其实索引的意义大同小异:

1
2
words = ['apple', 'bed', 'can', 'dentist']
print(words[2])

这是一个很生动的索引例子,第一行定义了一个“字典”,第二行则是取出了这个“字典”中偏移量为 2 的单词 can。

我们今天的目的,是实现多页面管理,索引在此将会变得非常实用。

无意义的“翻来覆去”

我们保持原有目录架构不变的同时,新建一个 .py 文件,名为 page2.py

我们在 page2.py 中书写如下代码:

1
2
3
4
5
6
7
8
import pgzero.screen
import pygame

screen = pgzero.screen.Screen(pygame.Surface((0,0)))
# 必须初始化 screen,否则将有 NameError: name 'screen' is not defined

def draw():
screen.fill((0,255,0))

显然,如果将其放入程序入口处的主循环中,screen 将被填充为绿色。

为了使接下来的操作更加方便且易于理解,我们对当前所有代码做一个小小的重构:

page1.py

1
2
def draw(screen):
screen.fill((255,0,0))

page2.py

1
2
def draw(screen):
screen.fill((0,255,0))

main.py

1
2
3
4
5
6
7
8
9
10
11
import pgzrun
import page1, page2
import pgzero.screen
import pygame

screen = pgzero.screen.Screen(pygame.Surface((0,0)))

def update():
page1.draw(screen)

pgzrun.go()

改动其实很简单,只不过是将 screen 交给了程序入口统一管理,避免了接下来一个页面一个 screen 的惨状,相信这样做你可以更好的助力你理解。

接着,在程序入口的 update() 中添加一行:

1
page2.draw(screen)

如果你在生产环境中这样写代码,没有人敢肯定将会发生什么,因为这就是在无意义的“翻来覆去”

况且,这与我们实现管理相差甚远。

正确实现

还记得最开始我所举的例子吗?索引索引,首先要有东西才能索引,在这里,我们需要创建一个列表:

在程序入口的 update() 函数前写入:

1
2
3
4
PAGE_LIST = [
page1.draw,
page2.draw,
]

让我们设置索引的目的,创建一个变量,我们将通过它决定我们索引的内容,它的值对应在列表中的偏移量:

1
now_page = 0

删除 update() 中原有的内容,修改后如下:

1
2
def update():
PAGE_LIST[now_page](screen)

效果看起来不错!接下来如何实现与用户交互相结合呢?

我们在项目根目录下新建一个文件夹,应 pgz 特性要求,必须命名为 images

在文件夹内,放入我自行制作的两个按钮样式:

next

back

page1.py

1
2
3
4
5
6
7
8
import pgzero.actor
Actor = pgzero.actor.Actor

button = Actor('next', (600,400))

def draw(screen):
screen.fill((255,0,0))
button.draw()

page2.py

1
2
3
4
5
6
7
8
import pgzero.actor
Actor = pgzero.actor.Actor

button = Actor('back', (200,400))

def draw(screen):
screen.fill((0,255,0))
button.draw()

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pgzrun
import page1, page2
import pgzero.screen
import pygame

PAGE_LIST = [
page1.draw,
page2.draw,
]

now_page = 0

screen = pgzero.screen.Screen(pygame.Surface((0,0)))

def update():
PAGE_LIST[now_page](screen)

def on_mouse_down(pos):
global now_page

if page1.button.collidepoint(pos):
now_page += 1

if page2.button.collidepoint(pos):
now_page -= 1

pgzrun.go()

无关紧要的话

在过去整整一年的时间里,我的博客一直处于荒废状态,文章时间轴也停留在了 2024.3。

在过去的整整一年时间里,我还算是顺利的开始接触真正的开发,不怎么扎实的学习了 Python,用 Flutter 写出了一个牵挂了五年的应用,就是这样。

也没什么好写的了,作为我的第一篇技术文章,小站重建后的第一篇文章,一定会有很多错误,如果你有所发现,欢迎通过各种方式联系我,当然,能在评论区留言固然更好。总之,祝你读的开心。

心若向阳,无畏冰霜。