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.xml

      
        cgi  
        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/*  
     
    #passShellEnvironment: 与Python解析器解析CGI脚本有关,但是一定要配置好Python的环境变量;
    #cgiPathPrefix:  配置访问的脚本目录
    #executable: (本地python的安装路径)配置Python的解析器

    ② 修改context.xml
    在标签中加入:
    由于上面web.xml是这样配置的:
    Python权威指南的10个项目(6~10)
    所以在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是:
    Python权威指南的10个项目(6~10)
    ③ 重启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))

效果:
Python权威指南的10个项目(6~10)_第1张图片
当在输入框中编辑然后提交后,内容会更新到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


File name:
''')

   文本框名为filename,这确保其内容将通过CGI参数filename提供给脚本edit.cgi,如果在文本框中输入文件名,再 单击Open按钮,将运行脚本edit.cgi。
② 编写编辑器脚本
  脚本edit.cgi显示的页面应包含一个文本区域和一个文本框,其中前者包含当前编辑的文件的 内容,而后者用于输入密码。这个脚本需要的唯一输入是文件名,它是从index.html中的表单中获得的。然而,可在不提交index.html中表单的情况下直接运行脚本edit.py。在这种情况下, cgi.FieldStorage的字段将是未设置的。因此,你需要检查是否获得了文件名;如果获得了,就 打开指定目录中的这个文件。我们将这个目录命名为data(当然,你必须创建这个目录)。
③ 编写保存脚本
  这个简单系统的最后一部分是执行保存的脚本。它接收文件名、密码和一些文本,并检查密 码是否正确;如果正确,就将这些文本存储到指定的文件中。我们将使用模块sha来处理密码。

(5) 结果展示

Python权威指南的10个项目(6~10)_第2张图片

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) 结果展示

    Python权威指南的10个项目(6~10)_第3张图片

    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中出现:
Python权威指南的10个项目(6~10)
而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

Python权威指南的10个项目(6~10)
它访问file2下的文件:
Python权威指南的10个项目(6~10)
然后创建otherpeer:
Python权威指南的10个项目(6~10)
它访问file1下的文件:
Python权威指南的10个项目(6~10)
把mypeer介绍给otherpeer:
Python权威指南的10个项目(6~10)
然后在访问file1下的文件:
Python权威指南的10个项目(6~10)
通过otherpeer拉去file1下的文件:
Python权威指南的10个项目(6~10)
发现file2下有test1.txt:
Python权威指南的10个项目(6~10)

  有朋友想问,这个有啥用啊,我们不妨拓展一下思维,如果这里是一个集群,我们用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,大概是这样的:
Python权威指南的10个项目(6~10)
在Miller2>后面输入,定义的do_xxx的方法中的xxx,它就会执行相应方法中的内容。
最后小编在这里说一下这个项目的运行规则:
运行:python client.py urls.txt directory http://servername.com:16888
Python权威指南的10个项目(6~10)
执行错误操作:
Python权威指南的10个项目(6~10)_第4张图片
首先查询所有的能关联的所有的urls列表,没有的返回Couldn't find the file 123。
输入exit 或者EOF会退出程序:
Python权威指南的10个项目(6~10)_第5张图片

9. 项目9:使用GUI共享文件

这个项目较小,你将看到给既 有Python程序添加GUI非常容易。

(1) 问题描述

  开发的文件共享系统:添加GUI客户端,让它使用起来更 容易。这意味着可能有更多的人选择使用它,他实现的需求是:

  • 允许用户输入文件名,并将其提交给服务器的方法fecth
  • 列出服务器的文件目录当前包含哪些文件

    (2) 工作准备

      开发这个项目时,最好是将项目8做一遍之后,然后在熟悉熟悉GUI的工具包的使用。

    (3) 初次实现

    由于这里是结合着项目8的代码,这里小编给出运行结果:
    Python权威指南的10个项目(6~10)

    (4) 再次实现

      第一个原型非常简单,它确实实现了文件共享功能,但对用户不太友好。如果用户能够知道 有哪些文件可用(这些文件可能是程序启动时就位于文件目录中,也可能是后来从其他节点那里 下载的),将大有裨益。再次实现将实现这种列出文件的功能,要获取节点包含的文件的列表。

    (5) 结果展示

    Python权威指南的10个项目(6~10)_第6张图片

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和其他几种文件格式存储的图像。
    两张图片:
    Python权威指南的10个项目(6~10)Python权威指南的10个项目(6~10)

(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) 最终展示

开始界面:
Python权威指南的10个项目(6~10)_第7张图片
游戏界面:
Python权威指南的10个项目(6~10)_第8张图片
闯关界面:
Python权威指南的10个项目(6~10)_第9张图片
结束界面:
Python权威指南的10个项目(6~10)_第10张图片

到此所有的项目都已结束!!!
所有的代码 小编都上传到gitlab上:

https://gitlab.com/ZZY478086819/actualcombatproject