说在前面
我于繁忙学业中和比赛中挤出时间总结记录了这次的开发经历,如有纰漏,请见谅。
下文中,许多时候将以 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 的一些底层细节。
但截至到本文发布时,我不推荐使用其进行开发,原因如下:
- 技术问题频出。这点的确使人很恼火,以至于有时甚至需要开发者本末倒置,调用 Pygame API 来帮助 pgz 修复一些奇怪的问题;
- 严格限制了代码书写和文件命名。在你的开发中,你需要严格按格式书写某些代码,例如一个 pgz 文件中一定存在
draw()
和update()
函数,与 Pygame 不同,pgz 程序的主循环写死在了其源码中,本质上,在其主循环中,只会调用这两个函数,这实际上也是低自定义的体现; - 反直觉的对象调用方法。
说了这么多,其实从 pgz 之初衷来看,真正的问题只是第一条,剩下的其实都是我们早已习惯精细化操控的缘故,如果你能够适应,或许你能使用它进行项目的前期验证之类的工作,这是个好方法。
问题引入
话已至此,我们还没有指出在标题中的所谓 多页面管理 是什么。简单点说,就是从一个页面到另一个页面,或许你可以理解为 PPT 中的翻页操作?
有些同学肯定要说,这个我知道,直接 import 即可。的确,在 Python 中,import 事实上就是运行你所 import 的 module,但生产环境中问题真有这么简单吗?试试就知道了。
方案一:直接 import page
不多废话,让我们从头开始书写一个 pgz 项目。这是该项目的目录结构:
1 | E:. |
简单对其作一些解释。
main.py
程序入口
page1.py
将要被引导的页面
tools.py
模拟一个项目中存放一些常用操作的库
首先,完成 main.py
内代码:
1 | import pgzrun |
你肯定很疑惑 screen 是从哪冒出来的,并且怀疑是不是我粗心大意提供了错误的代码,你别说,还真不是。
完成 page1.py
内代码:
1 | import pgzrun |
代码很简单,不需要我解释,不过在运行之前,我们先要明确我们想要的效果。
当 程序入口 被调用时,弹出 pgz 游戏窗口,背景被填充为白色。
当 窗口内被点击后,窗口被填充为红色。
实际效果如何呢?
第一步的确没有任何问题,弹出了一个背景被填充为白色的窗口:
可当 user 点击窗口内部时,竟没有任何反应,关掉窗口程序又抛出一个错误:
1 | Traceback (most recent call last): |
方案2:避免主循环冲突
有些有经验的同学可能已经发现了:
你这一看就不对啊,在
mian.py
和page1.py
中都有相当于 Pygame 中的主循环的pgzrun.go()
这个观点是正确的,不过是否可行呢?
分析代码可得,我们的 page1.py
中的 draw()
函数就是书写界面绘制逻辑的地方。那我们可用 main.py
中的主循环来直接运行它们。
修改后的代码如下:
1 | import pgzrun |
1 | import pgzrun, tools |
能跑吗?不能跑:
1 | Traceback (most recent call last): |
你一定会感到匪夷所思,不是说 pgz 准备好了 screen 对象吗?这点确实没错,因为当你给 page1.py
加上主循环单独运行,一点问题都没有:
那我们使用 Pygame 的方法来初始化 screen?
1 | import pygame |
然而到 pgz 这儿是这样:
1 | AttributeError: 'pygame.surface.Surface' object has no attribute 'surface' |
其实,这个方法并不是行不通,我们可以换一种写法,直接使用 pgzero.screen
中的 Screen
类来初始化 screen 对象:
1 | import pgzero.screen |
单独运行依旧正常,可通过 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 | import pgzrun |
修改 page1.py
的代码如下:
1 | import pgzero.screen |
再次运行:
显然,程序入口的 update()
函数成功的调用了 page1.py
的 draw()
函数,将 screen 填充为白色。
如果我们把程序入口处的 update()
函数改为一个 draw()
函数会发生什么呢?
有同学肯定要急了:
你这不是自己打自己脸吗?
实则不然,原因如下:
draw()
将造成性能衰减draw()
将在窗口移动时出现渲染问题- 我本身就想打我自己的脸
我们一条条来说。
首先是第一条,还记得我说 从理论上来说,draw()
函数只会被执行一次 吗?
从理论上 才是整句话的重心所在。
每当需要刷新(重绘)窗口的时候,draw()
也会被调用,但与 update()
不同的是,它的帧率被砍了半。我想,造成的影响就不用我多解释了。
第二条很好说,你可以看看之前所有没有使用 update()
的代码运行情况的截图,结果就是你会发现窗口地下有一部分都没被渲染上,至于为什么,我卖个关子,可以自行尝试研究。
第三条又是什么鬼?事实上,上文的那个 简单尝试 不过是个幌子。
不会真有人试了吧
如果你硬是要让我解释做这个尝试有何目的,实际上我也说得出来:
- 打我自己的脸
- 浪费读者的阅读兴趣
- 引出下文内容
事实上,我认为 draw()
函数没有其存在的必要,在我使用 pgz 编写 GUI 程序时,我几乎不使用 draw()
。
页面索引机制
索引,一个很生动的词。当你带着查阅某个字词的目的拿起一本字典,你要做的肯定是先通过各种方式找到这个字或词,而不是把书翻来覆去。
当你把书翻来覆去的时候,这个动作在本质上就不存在了意义,你所带有的目的也成了空谈
在字典中,找到一个字或词的方法叫做索引
在书写程序时,其实索引的意义大同小异:
1 | words = ['apple', 'bed', 'can', 'dentist'] |
这是一个很生动的索引例子,第一行定义了一个“字典”,第二行则是取出了这个“字典”中偏移量为 2 的单词 can。
我们今天的目的,是实现多页面管理,索引在此将会变得非常实用。
无意义的“翻来覆去”
我们保持原有目录架构不变的同时,新建一个 .py
文件,名为 page2.py
。
我们在 page2.py
中书写如下代码:
1 | import pgzero.screen |
显然,如果将其放入程序入口处的主循环中,screen 将被填充为绿色。
为了使接下来的操作更加方便且易于理解,我们对当前所有代码做一个小小的重构:
page1.py
:
1 | def draw(screen): |
page2.py
1 | def draw(screen): |
main.py
1 | import pgzrun |
改动其实很简单,只不过是将 screen 交给了程序入口统一管理,避免了接下来一个页面一个 screen 的惨状,相信这样做你可以更好的助力你理解。
接着,在程序入口的 update()
中添加一行:
1 | page2.draw(screen) |
如果你在生产环境中这样写代码,没有人敢肯定将会发生什么,因为这就是在无意义的“翻来覆去”。
况且,这与我们实现管理相差甚远。
正确实现
还记得最开始我所举的例子吗?索引索引,首先要有东西才能索引,在这里,我们需要创建一个列表:
在程序入口的 update()
函数前写入:
1 | PAGE_LIST = [ |
让我们设置索引的目的,创建一个变量,我们将通过它决定我们索引的内容,它的值对应在列表中的偏移量:
1 | now_page = 0 |
删除 update()
中原有的内容,修改后如下:
1 | def update(): |
效果看起来不错!接下来如何实现与用户交互相结合呢?
我们在项目根目录下新建一个文件夹,应 pgz 特性要求,必须命名为 images
在文件夹内,放入我自行制作的两个按钮样式:
page1.py
:
1 | import pgzero.actor |
page2.py
1 | import pgzero.actor |
main.py
1 | import pgzrun |
无关紧要的话
在过去整整一年的时间里,我的博客一直处于荒废状态,文章时间轴也停留在了 2024.3。
在过去的整整一年时间里,我还算是顺利的开始接触真正的开发,不怎么扎实的学习了 Python,用 Flutter 写出了一个牵挂了五年的应用,就是这样。
也没什么好写的了,作为我的第一篇技术文章,小站重建后的第一篇文章,一定会有很多错误,如果你有所发现,欢迎通过各种方式联系我,当然,能在评论区留言固然更好。总之,祝你读的开心。
心若向阳,无畏冰霜。