6. 项目6:使用CGI进行远程编辑
这个项目主要用的是的CGI进行远程编辑——在另一台机器上通过Web来编辑 文档。你在一台机器上存储了一个文档,希望能够在另一台机器上通过Web来编辑它。这让多个用 户能够协作编辑一个文档,且无需使用FTP或类似的文件传输技术,也无需操心同步多个副本的 问题。要编辑文件,只要有Web浏览器就行。
(1) 问题描述
- 能够以普通网页的方式显示文档。
- 能够在Web表单的文本区域内显示文档
- 用户能够保存表单中的文本
- 程序应使用密码对文档进行保护
- 程序应易于扩展,以支持对多个文档进行编辑
(2) 工作准备
- 模块cgi以及用于调试的模块cgitb
(3) 初次实现
这个简单的程序的逻辑大概如下:
- 获取CGI参数text(默认为数据文件的当前内容)
- 将text的值保存到数据文件中
- 打印表单,其中的文本区域包含text的值
要让脚本能够写入数据文件,必须先创建这样的文件(如simple_edit.dat)。这个文件可以为 空,也可包含初始文档(纯文本文件,其中可能包含一些标记,如XML或HTML)。
运行之前,首先小编先介绍一下如何在Tomcat中运行Python脚本:
① 修改Tomcat的配置文件:web.xmlcgi org.apache.catalina.servlets.CGIServlet debug 0 cgiPathPrefix WEB-INF/cgi executable C:\Users\Administrator\AppData\Local\Programs\Python\Python36-32\python.exe passShellEnvironment true 5 cgi /cgi-bin/* ② 修改context.xml
在标签中加入:
由于上面web.xml是这样配置的:
所以在Tomcat的webapps中创建一个cgitest,然后在cgitest中创建一个WEB-INF,然后在WEB-INF中创建一个cgi文件夹,将编写的Python脚本放入cgi文件夹中:
脚本:test.py
最终路径:webapps/cgitest/WEB-INF/cgi/test.py
然后访问时:http://localhost:8080/cgitest/cgi-bin/test.py
注意这里的cgi-bin是:
③ 重启Tomcat
#脚本:
#C:\Users\Administrator\AppData\Local\Programs\Python\Python36\python.exe
import cgi
form = cgi.FieldStorage()
text = form.getvalue('text', open('simple_edit.dat').read())
f = open('simple_edit.dat', 'w')
f.write(text)
f.close()
print("Content-type: text/html\n\n")
print("""
A Simple Editor
""".format(text))
效果:
当在输入框中编辑然后提交后,内容会更新到simple_edit.dat中。
###(4) 再次实现
至此,第一个原型已编写好,它还缺什么呢?应让用户能够编辑多个文件,并使用密码保护 这些文件。相比于第一个原型,再次实现的主要不同在于,你将把它分成两个CGI脚本,分别对应于系 统支持的两种操作。新的原型包含如下文件:
- index.py:一个普通网页,包含一个供用户输入文件名的表单,还包含一个触发edit.py 的Open按钮。
- edit.py:在文本区域中显示指定文件的脚本。它还包含一个用于输入密码的文本框以及 一个触发save.py的Save按钮。
- save.py:将收到的文本保存到指定的文件并显示一条简单消息(如The file has been saved) 的脚本。这个脚本还应负责检查密码。
① index.py
#C:\Users\Administrator\AppData\Local\Programs\Python\Python36\python.exe
print('Content-type: text/html\n\n')
import cgi, sys
form = cgi.FieldStorage()
print('''
File Editor
''')
文本框名为filename,这确保其内容将通过CGI参数filename提供给脚本edit.cgi,如果在文本框中输入文件名,再 单击Open按钮,将运行脚本edit.cgi。
② 编写编辑器脚本
脚本edit.cgi显示的页面应包含一个文本区域和一个文本框,其中前者包含当前编辑的文件的 内容,而后者用于输入密码。这个脚本需要的唯一输入是文件名,它是从index.html中的表单中获得的。然而,可在不提交index.html中表单的情况下直接运行脚本edit.py。在这种情况下, cgi.FieldStorage的字段将是未设置的。因此,你需要检查是否获得了文件名;如果获得了,就 打开指定目录中的这个文件。我们将这个目录命名为data(当然,你必须创建这个目录)。
③ 编写保存脚本
这个简单系统的最后一部分是执行保存的脚本。它接收文件名、密码和一些文本,并检查密 码是否正确;如果正确,就将这些文本存储到指定的文件中。我们将使用模块sha来处理密码。
(5) 结果展示
7. 项目7:自建公告板
本项目实现的是基于Web的论坛,虽然其功能与复杂的社交媒体平台相距甚远,但提供了评论系统的基本功能。
(1) 问题描述
在这个项目中,你将创建一个通过Web发布和回复消息的简单系统,它可作为论坛使用。这 个系统非常简单,但提供了基本的功能,并能够处理大量的帖子。
本章介绍的技术不仅可用于开发独立论坛,还可用于实现更通用的协作系统、问题跟踪系统、 带评论功能的博客等。通过将支持在消息下方以缩放的方式显示回复CGI(或类似的技术)和可靠的数据库(这里是SQL数据库)结合 起来使用,可实现非常强大的功能,而且用途非常广泛。
最终这个项目要实现的功能:
- 显示当前所有消息的主题
- 支持在消息下方以缩放的方式显示回复
- 让用户能够查看已有的消息
- 让用户能够回复已有的消息
(2) 工作准备
- Gci+Tomcat
- 单机数据库(MySQL/SQLite)本项目使用的是MySQL。(import pymysql)
① 建表语句:-- 创建MySQL数据库 CREATE TABLE messages ( id INT NOT NULL AUTO_INCREMENT, subject VARCHAR(100) NOT NULL, sender VARCHAR(15) NOT NULL, reply_to INT, text MEDIUMTEXT NOT NULL, PRIMARY KEY(id) );
字段介绍:
- id:用于标识消息。每条消息都会自动获得由数据库管理器提供的独一无二的ID,因此 无需在Python代码中指定这些ID。
- subject:包含消息主题的字符串。
- sender:包含发送者姓名、电子邮箱地址或其他类似信息的字符串。
- reply_to:如果消息是另一条消息的回复,这个字段将包含那条消息的id,否则为空。
- text:包含消息正文的字符串。
(3) 初步实现
#连接数据库脚本:
import pymysql
def create_table(cursor,table_name,create_sql):
# 使用 execute() 方法执行 SQL,如果表存在则删除
drop_table = 'drop table if EXISTS '+table_name
cursor.execute(drop_table)
# 建表
cursor.execute(sql)
def insert(cursor,table_name):
reply_to = input('Reply to: ')
subject = input('Subject: ')
sender = input('Sender: ')
text = input('Text: ')
sql='insert into {}(reply_to, sender, subject, text) values({},"{}","{}","{}")'\
.format(table_name,reply_to, sender, subject, text)
cursor.execute(sql)
if __name__=='__main__':
server_host="localhost"
user="xxxx"
passwd= "xxxx"
db_name="test"
# 打开数据库连接
db = pymysql.connect(server_host, user,passwd,db_name)
# 使用 cursor() 方法创建一个游标对象 cursor
cursor = db.cursor()
#建表
sql = '''
CREATE TABLE messages (
id INT NOT NULL AUTO_INCREMENT,
subject VARCHAR(100) NOT NULL,
sender VARCHAR(15) NOT NULL,
reply_to INT,
text MEDIUMTEXT NOT NULL, PRIMARY KEY(id)
)
'''
create_table(cursor,"messages",sql)
#插入数据
insert(cursor,db_name+".messages")
db.commit()
# 关闭连接
cursor.close()
#cgi脚本:
'''
公告板主页
'''
print('Content-type: text/html\n')
import cgitb;
cgitb.enable()
import pymysql
server_host = "localhost"
user = "root"
passwd = "123456"
db_name = "test"
# 打开数据库连接
db = pymysql.connect(server_host, user, passwd, db_name)
curs = db.cursor()
print("""
The FooBar Bulletin Board
The FooBar Bulletin Board
""")
toplevel = []
children = {}
curs.execute('SELECT * FROM test.messages')
rows = curs.fetchall()
for row in rows:
parent_id = row[3]
if parent_id is None:
toplevel.append(row)
else:
children.setdefault(parent_id, []).append(row)
def format(row):
print(row[1]+'')
try:
kids = children[row[0]]
except KeyError:
pass
else:
print('')
for kid in kids:
format(kid)
print('
')
print('')
for row in toplevel:
format(row)
print("""
""")
(4) 最终版
初次实现的功能很有限,用户甚至不能发布消息。这里我们将内容丰富一些:
- main.py:以层次方式显示所有消息的主题,并将这些主题作为到消息本身的链接。
- view.py:显示一条消息,并提供让用户能够回复的链接。
- edit.py:以可编辑的方式显示一条消息(就像第25章那样使用文本框和文本区域),其中 的Submit按钮链接到脚本save.cgi
- save.py:从edit.cgi那里接收有关消息的信息,并通过在数据库表中插入一个新行来保存 这条消息
脚本的逐个分析:
① main.py
脚本main.cgi很像第一个原型中的脚本simple_main.cgi,主要差别在于加入了链接:每个主题 都链接到相应消息(到view.cgi的链接);同时在页面底部添加让用户能够发布新消息的链接(到 edit.cgi的链接)。
② view.py
脚本view.cgi根据提供给它的CGI参数id从数据库获取一条消息,再使用得到的值来生成一个 简单的HTML页面。这个页面包含一个返回到主页面(main.py)的链接,更有趣的是,它还包 含一个到edit.py的链接,但这里将参数reply_to设置为id的值,以确保新消息是对当前消息的回复。
③ edit.py
脚本edit.cgi实际上承担了双重职责:既用于编辑新消息,也用于编辑回复。这两项功能的差 别并不大:如果在CGI请求中提供了reply_to,就将其存储在编辑表单中一个隐藏的input元素中。 在Web表单中,隐藏的input元素用于临时存储信息。它们不像文本区域等元素那样是用户能够 看到的,但它们的值也将传递给表单的属性action指定的CGI脚本,这让生成表单的脚本能够向 处理该表单的脚本传递信息。另外,默认将主题设置为"Re: parentsubject"(除非主题已经以Re:打头,在这种情况下,不用继续添加Re。
④ save.py
下面来编写最后一个脚本。脚本save.py从edit.py生成的表单那里接收有关一条消息的信息, 并将其存储到数据库中。(5) 结果展示
8. 项目8:使用XML-RPC 共享文件
本项目是一个简单的文件共享应用程序。我们将使用的主要技术是XML-RPC,这是一种远程调用过程(函数)的协议, 这种调用可能是通过网络进行的。如果你愿意,可使用普通的套接字编程轻松地实现这个项目的功能。
(1) 问题描述
我们要创建一个P2P(peer-to-peer)文件共享程序。大致而言,文件共享意味着在运行于不 同计算机上的程序之间交换文件。在P2P交互中,任何对等体(peer)都可连接到其他对等体。在 这样一个由对等体组成的网络中,不存在中央权威,这让网络更健壮,因为除非你关闭大部分对等体,否则这样的网络不可能崩溃。
项目满足条件: - 每个节点都必须跟踪一组已知的节点,以便能够向这些节点寻求帮助。还必须让节点能 够向其他节点介绍自己,从而成为其他节点跟踪的节点集中的一员。
- 节点必须能够通过提供文件名向其他节点请求文件。如果对方有这样的文件,应将其返 回,否则应转而向其邻居请求这个文件(而这些邻居可能转而请其邻居请求该文件)。被 请求的节点如果有这样的文件,就将其返回。
- 为避免循环(A向B请求,B又反过来向A请求),同时避免形成过长的请求链(A向B请求, B向C请求等,直到向Z请求),向节点查询时必须提供历史记录。
- 必须能够连接到其他节点,并将自己标识为可信任方。通过这样做,节点将能够使用不 可信任方(如P2P网络中的其他节点)无法使用的功能。这种功能可能包括请求对方通过 查询从网络中的其他节点下载文件并存储。
- 必须提供这样的用户界面:让用户能够作为可信任方连接到其他节点,并让对方下载文 件。这种界面应该能够轻松地扩展乃至替换。
(2) 工作准备
- 需要的模块:xmlrpc.client和xmlrpc.server
- 模块xmlrpc.client的用法非常简单,你 只需使用服务器的URL创建一个ServerProxy对象,就能够马上访问远程过程。
- 模块xmlrpc.server 使用起来要复杂些
- 为实现这个文件共享程序的界面,我们将使用cmd模块
- 为了并行,我们将使用threading
- 为了提取URL,我们将使用urllib.parse模块
(3) 初次实现
这个RPC简单的说就是远程调用服务端的方法,接下来我们简单是实现以下,看看效果:
#Server:
'''
对下面案例的解释:
方法register_instance注册一个实现了其“远程方法”的实例,也
可使用方法register_function注册各个函数。为运行服务器做好准备(让它能够响应来自外部的
请求)后,调用其方法serve_forever ,方法serve_forever解释器看起来就像“挂起”了一样,但实际上它是在等待RPC请求。
'''
from xmlrpc.server import SimpleXMLRPCServer
s=SimpleXMLRPCServer(("",16888)) #localhost和端口4242
def twice(x):
return x*2
s.register_function(twice) # 给服务器添加功能,也就是客户端调用的函数
s.serve_forever() # 启动服务器
#Server:
'''
对下面案例的解释:
方法register_instance注册一个实现了其“远程方法”的实例,也
可使用方法register_function注册各个函数。为运行服务器做好准备(让它能够响应来自外部的
请求)后,调用其方法serve_forever ,方法serve_forever解释器看起来就像“挂起”了一样,但实际上它是在等待RPC请求。
'''
from xmlrpc.server import SimpleXMLRPCServer
s=SimpleXMLRPCServer(("",16888)) #localhost和端口4242
def twice(x):
return x*2
s.register_function(twice) # 给服务器添加功能,也就是客户端调用的函数
s.serve_forever() # 启动服务器
先运行server在运行client,我们发现:
在server中出现:
而client成功调用了sever的方法,并输出了结果:4。
是不是挺简单的,好像URL请求一样,接下来我们就要根据上面的需求实现相应的功能了:
① 定义node
回顾需求,我们关心的主要有两点:Node 必须存储哪些信息(属性);Node必须能够执行哪些操作(方法)。
所有node需要有以下属性:
- 目录名:让Node知道到哪里去查找文件或将文件存储到哪里。
-密码:供其他节点用来将自己标识为可信任方。
- 一组已知的对等体(URL)。
- URL:可能加入到查询历史记录中或提供给其他节点
Node能执行什么操作呢,必须定义一些方法:
- 用于查询的方法(query)
- 获取和存储文件的方法(fetch)
- 向其他节点介绍自己的方法(hello)
② fetch方法解读
这个方法必须接受参数query和 secret,其中secret是必不可少的, 可避免节点被其他节点随便操纵。调用fetch将导致节 点下载一个文件。如果提供的密码不同于(启动时指定的)self.secret,fetch将直接返回FAIL; 否则它将调 用query来获取指定的文件。调用query时,你希望能够知道查询是否 成功,并在成功时返回指定文件的内容。因此,我们将query的返回值定义为元组(code, data), 其中code的可能取值为OK和FAIL,而data是一个字符串。如果code为OK,这个字符串将包含找到 的文件的内容;否则为一个随意的值,如空字符串。如果查询成功,并且返回OK,则开始创建一个文件并开始向其中写入内容。
③ query方法解读
query的结构:query→self._handle→_broadcast
首先调用_handle方法,它负责查询的内容处理(检查节点是否包含指定的文件,获取数据 等),它像query一样返回一个编码和一些数据。如果code为OK,则直接返回,如果为FAIL,就需要其他节点的帮助因此它需要将self.url 添加到history中,然后通过history向方法_broadcast向所有已知的对等体广播查询,它迭代self.known的副本,如果当前对等体包含在history中, 就使用continue语句跳到下一个对等体,否则创建一个ServerProxy对象,并对其调用方法query。 如果方法query成功,就将其返回值作为_broadcast的返回值。
由于代码比较长这里就不给出了,可以在小编的git中下载。
最后我们演示一定这个初级版的程序如何运行:
mypeer:python xxx.py http://localhost:16888 file1 123456
otherpeer:python xxx.py http://localhost:16889 file2 123456
#创建mypeer
它访问file2下的文件:
然后创建otherpeer:
它访问file1下的文件:
把mypeer介绍给otherpeer:
然后在访问file1下的文件:
通过otherpeer拉去file1下的文件:
发现file2下有test1.txt:
有朋友想问,这个有啥用啊,我们不妨拓展一下思维,如果这里是一个集群,我们用client去访问server中的文件,但是文件分布在server集群的某个节点上,想想我们可不可通过这种方法,把文件传递到访问的节点上,然后从访问节点中拿取,我的天一不小心了解分布式的原理,低调低调。
(4) 最终实现
最终版我们需要在初次实现中修改一些内容:
- 如果你停止并重启一个节点,可能出现错误消息,指出端口被占用。
- 你可能想提供对用户更友好的界面,而不是在交互式Python解释器中使用xmlrpc.client。
- 返回的编码不方便,一种更自然、更符合Python风格的解决方案是,在找不到文件时引发 自定义异常。
- 节点没有检查它返回的文件是否包含在文件目录中。
模块解读:
① 创建客户端界面
客户端界面是使用模块cmd中的Cmd类实现,我们在这里只实现命令fetch(下载文件)和exit(退出程序)。命令fetch调用服务器的方 法fetch,并在文件没有找到时打印一条错误消息。命令exit打印一个空行(这只是出于美观考 虑)并调用sys.exit。
② 引发异常
不返回表示成功还是失败的编码,而是假定肯定会成功,并在失败时引发异常。这里我们使用200正常的失败(请求未得到处理)和500表示 请求被拒绝(拒绝访问)。UNHANDLED = 200 ACCESS_DENIED = 500 class UnhandledQuery(Fault): """ 表示查询未得到处理的异常 """ def __init__(self, message="Couldn't handle the query"): super().__init__(UNHANDLED, message) class AccessDenied(Fault): """ 用户试图访问未获得授权的资源时将引发的异常 """ def __init__(self, message="Access denied"): super().__init__(ACCESS_DENIED, message)
异常是xmlrpc.client.Fault的子类。在服务器中引发的异常将传递到客户端,并保持 faultCode不变。如果在服务器中引发了普通异常(如IOException),也将创建一个Fault类实例, 因此你不能在服务器中随意地使用异常。
③ 验证文件名
检查指定的文件是否包含在指定的目录中。我们这里采用较为简单的方法实现:
根据目录名和文件名创建绝对路径(例如,这将把'/foo/bar/../ baz'转换为'/foo/baz'),将目录名与空文件名合并以确保它以文件分隔符(如'/')结尾,再检 查绝对文件名是否以绝对路径名打头。如果是这样的,就说明指定的文件包含在指定的目录中。
(5) 结果展示
这里在pychram中运行有诸多bug,使用cmd感觉不是那么好使,或者可以在Linux下运行试试,这里小编也给出了一个Cmd运行的dome,大概是这样的:
在Miller2>后面输入,定义的do_xxx的方法中的xxx,它就会执行相应方法中的内容。
最后小编在这里说一下这个项目的运行规则:
运行:python client.py urls.txt directory http://servername.com:16888
执行错误操作:
首先查询所有的能关联的所有的urls列表,没有的返回Couldn't find the file 123。
输入exit 或者EOF会退出程序:
9. 项目9:使用GUI共享文件
这个项目较小,你将看到给既 有Python程序添加GUI非常容易。
(1) 问题描述
开发的文件共享系统:添加GUI客户端,让它使用起来更 容易。这意味着可能有更多的人选择使用它,他实现的需求是:
- 允许用户输入文件名,并将其提交给服务器的方法fecth
- 列出服务器的文件目录当前包含哪些文件
(2) 工作准备
开发这个项目时,最好是将项目8做一遍之后,然后在熟悉熟悉GUI的工具包的使用。
(3) 初次实现
(4) 再次实现
第一个原型非常简单,它确实实现了文件共享功能,但对用户不太友好。如果用户能够知道 有哪些文件可用(这些文件可能是程序启动时就位于文件目录中,也可能是后来从其他节点那里 下载的),将大有裨益。再次实现将实现这种列出文件的功能,要获取节点包含的文件的列表。
(5) 结果展示
10. 项目10:自制街机游戏
到了最后一个项目了,这也是Python权威指南的最后一课了,经过一个多月的学习对Python也有了大致的了解,希望以后可以运用到工作中,至少目前看代码时完全没有问题。
最后一个项目,应该说是所有项目中代码量最多的,而且也比较有趣,这个项目将学习如何使用Pygame,这个扩展让你能够使用Python编写功能齐备的全屏街机游戏。
(1) 问题描述
这个游戏中,我们将让玩家控制一支香蕉。这支香蕉要躲开从天而 降的16吨铅锤,尽力在防御战中活下来。这个项目的目标是围绕着游戏设计展开的。这款游戏必须像设计的那样:香蕉能够移动,16 吨的铅锤从天而降。另外,与往常一样,代码必须是模块化的,且易于扩展。一个重要的需求是, 设计应包含一些游戏状态(如游戏简介、关卡和“游戏结束”状态),同时可轻松地添加新状态。
(2) 工作准备
这个项目需要的工具只有一个,那就是pygame。模块pygame自动导入其他所有的Pygame模块,因此只要在程序开头包含语句import pygame, 就能使用其他模块,如pygame.display和pygame.font。
模块pygame包含函数Surface,它返回一个新的Surface对象。Surface对象其实就是一个指定 尺寸的空图像,可用来绘画和传送。传送(调用Surface对象的方法blit)意味着在Surface之间传输内容。
函数init是Pygame游戏的核心,必须在游戏进入主事件循环前调用。这个函数自动初始化其他所有模块(如font和image)。
下载:pip install pygame
如果要捕获Pygame特有的错误,就需要使用error类。
接下来我们再来了解几个pygame重要的函数:
- pygame.locals:包含你可能在自定义模块的作用域内使用的名称(变量),如事件类型、 键、视频模式等的名称。
- pygame.display:模块pygame.display包含处理内容显示的函数,这些内容可显示在普通窗口中,也可占据整个屏幕,而在这个模块我们具体使用的函数有:
- flip:更新显示。一般而言,分两步来修改当前屏幕。首先,对函数get_surface返回 的Surface对象做必要的修改,然后调用pygame.display.flip来更新显示,反映出所做 的修改。
- update:只想更新屏幕的一部分时,使用这个函数,而不是flip。调用这个函数时,可只 提供一个参数,即RenderUpdates类的方法draw返回的矩形列表
- set_mode:设置显示的尺寸和类型。显示模式有多种,但这里只使用全屏模式和默认模式 “在窗口中显示”
- set_caption:设置Pygame程序的标题。
- get_surface:返回一个Surface对象,你可在其中绘制图形,再调用pygame.display.flip 或pygame.display.blit。
- pygame.font:模块pygame.font包含函数Font。
- pygame.sprite:包含两个非常重要的类:Sprite和Group,Sprite类是所有可见游戏对象(在这个项目中,是香蕉和重16吨的铅锤)的基类。要实现自 定义的游戏对象,可从Sprite派生出子类,并重写构造函数以设置其属性image和rect(这些属性 决定了Sprite的外观和位置),同时重写在Sprite可能需要更新时调用的方法update。
- Group:及其子类的实例用作Sprite对象的容器。一般而言,使用Group是个不错的主意。在简 单的游戏(如本章的项目)中,只需创建一个名为sprites或allsprites之类的Group,并将所有 Sprite都添加到其中。这样,当你调用Group对象的方法update时,将自动调用所有Sprite对象的 方法update。另外,Group对象的方法clear用于清除它包含的所有Sprite对象
- pygame.mouse:只使用模块pygame.mouse来做两件事情:隐藏鼠标以及获取 鼠标的位置
- pygame.event:模块pygame.event跟踪各种事件,如鼠标单击、鼠标移动、按下或松开键等。要获取最近发 生的事件列表,可使用函数pygame.event.get。
- pygame.image:模块pygame.image用于处理图像,如以GIF、PNG、JPEG和其他几种文件格式存储的图像。
两张图片:
(3) 初次实现
在初次实现的时候,我们只实现其中一些简单的功能:
- 使用pygame.init、pygame.display.set_mode和pygame.mouse.set_visible初始化Pygame。 使用pygame.display.get_surface获取屏幕表面,使用方法fill以白色填充屏幕表面,再调用 pygame.display.flip显示所做的修改
- 加载铅锤图像
- 使用这幅图像创建自定义类Weight(Sprite的子类)的一个实例。将这个对象添加到Render Updates编组sprites中。(处理多个Sprite对象时,这样做很有帮助。)
- 使用pygame.event.get获取最近发生的所有事件,并依次检查这些事件。如果发现事件QU IT 或因按下Escape键(K_ESCAPE)而触发的KEYDOWN事件,就退出程序
- 调用编组sprites的方法clear和update。方法clear使用回调函数来清除所有的Sprite对象 (这里是铅锤),而方法update调用Weight实例的方法update
- 调用sprites.draw并将屏幕表面作为参数,以便在当前位置绘制铅锤(每次调用Weight实例 的update方法后,位置都将发生变化
- 调用pygame.display.update,并将sprites.draw返回的矩形列表作为参数,只更新需要更 新的部分。
- 重复第4~7步
# C:\Users\Administrator\AppData\Local\Programs\Python\Python36\python.exe
# -*- coding: utf-8 -*-
'''
简单的“铅锤从天而降”动画
'''
import sys,pygame
from random import randrange
from pygame.locals import *
class Weight(pygame.sprite.Sprite):
def __init__(self,speed):
pygame.sprite.Sprite.__init__(self)
self.speed=speed
#绘制Sprite对象时要用到的图像和矩形:
self.image=weight_image
self.rect=self.image.get_rect()
self.reset()
def reset(self):
"""
将铅锤移到屏幕顶端的一个随机位置
"""
self.rect.top=-self.rect.height
self.rect.centerx=randrange(screen_size[0])
def update(self):
"""
更新下一帧中的铅锤
"""
if self.rect.top> screen_size[1]:
self.reset()
self.rect.top += self.speed
#初始化
pygame.init()
screen_size=1366,768
pygame.display.set_mode(screen_size,FULLSCREEN)
pygame.mouse.set_visible(0)
# 加载铅锤图像
image_path="images/jack.jpg"
weight_image=pygame.image.load(image_path)
weight_image=weight_image.convert() # 以便与显示匹配
# 你可能想设置不同的速度
#speed=5
# 创建一个Sprite对象编组,并在其中添加一个Weight实例
sprites = pygame.sprite.RenderUpdates()
#同时添加三个铅块
sprites.add(Weight(speed=2))
sprites.add(Weight(speed=3))
sprites.add(Weight(speed=5))
# 获取并填充屏幕表面
screen=pygame.display.get_surface()
bg=(255, 255, 255) #白色
#以白色填充屏幕表面
screen.fill(bg)
#显示所做的修改
pygame.display.flip()
clock=pygame.time.Clock() # 设置时钟,限制移动速度
# 用于清除Sprite对象:
def clear_back(surf,rect):
surf.fill(bg, rect)
while True:
clock.tick(60) # 然后在while循环中设置多长时间运行一次,每秒执行60次
# 检查退出事件:
for event in pygame.event.get():
if event.type==QUIT:
sys.exit()
if event.type== KEYDOWN and event.key == K_ESCAPE:
sys.exit()
# 清除以前的位置:
sprites.clear(screen,clear_back)
# 更新所有的Sprite对象:
sprites.update() #方法update调用Weight实例的方法update
# 绘制所有的Sprite对象:
updates=sprites.draw(screen) #调用sprites.draw并将屏幕表面作为参数,以便在当前位置绘制铅锤
# 更新必要的显示部分:
pygame.display.update(updates)
(4) 再次实现
最终我们将实现这样的一个游戏:
- 这个游戏包含5个文件:包含各种配置变量的config.py;包含游戏对象的实现的objects.py; 包含主游戏类和各种游戏状态类的squish.py;游戏使用的图像jack.jpg和banana. jpg
- 矩形的方法clamp确保一个矩形位于另一个矩形内,并在必要时移动这个矩形。这个方法 用于避免香蕉移到屏幕外。
- 矩形的方法inflate调整矩形的尺寸——在水平和垂直方向调整指定数量的像素。这个方 法用于收缩香蕉的边界,从而在香蕉和铅锤重叠到一定程度后,才认为香蕉被砸到
- 这个游戏本身由一个游戏对象和各种状态组成。游戏对象在特定时间点只有一种状态, 而状态负责处理事件并在屏幕上显示自己。状态还能让游戏切换到另一种状态。例如, 状态Level可以让游戏切换到GameOver状态。
这里我们将实现这样几个模块:
config.py:可根据偏好随意修改配置变量,如果游戏的节奏太快或太慢,可尝试修改与速度相关的变量
objects.py:这个模块包含游戏Squish使用的游戏对象
squish.py:这个模块包含游戏Squish的主游戏逻辑
(5) 最终展示
到此所有的项目都已结束!!!
所有的代码 小编都上传到gitlab上: