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<servlet> <servlet-name>cgi</servlet-name> <servlet-class>org.apache.catalina.servlets.CGIServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>0</param-value> </init-param> <init-param> <param-name>cgiPathPrefix</param-name> <param-value>WEB-INF/cgi</param-value> </init-param> <init-param> <param-name>executable</param-name> <param-value>C:\Users\Administrator\AppData\Local\Programs\Python\Python36-32\python.exe</param-value> </init-param> <init-param> <param-name>passShellEnvironment</param-name> <param-value>true</param-value> </init-param> <load-on-startup>5</load-on-startup> </servlet> <servlet-mapping> <servlet-name>cgi</servlet-name> <url-pattern>/cgi-bin/*</url-pattern> </servlet-mapping> #passShellEnvironment: 與Python解析器解析CGI腳本有關,但是一定要配置好Python的環境變量; #cgiPathPrefix: 配置訪問的腳本目錄 #executable: (本地python的安裝路徑)配置Python的解析器
② 修改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("""
<html>
<head>
<title>A Simple Editor</title>
</head>
<body>
<form action='simple_edit.py' method='POST'>
<textarea rows='10' cols='20' name='text'>{}</textarea><br />
<input type='submit' />
</form>
</body>
</html>
""".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('''
<html>
<head>
<title>File Editor</title>
</head>
<body>
<form action='edit.py' method='POST'>
<b>File name:</b><br/>
<input type='text' name='filename'/>
<input type='submit' value='Open'/>
</form>
</body>
</html>
''')
文本框名爲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("""
<html>
<head>
<title>The FooBar Bulletin Board</title>
</head>
<body>
<h1>The FooBar Bulletin Board</h1>
""")
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]+'</br>')
try:
kids = children[row[0]]
except KeyError:
pass
else:
print('<blockquote>')
for kid in kids:
format(kid)
print('</blockquote>')
print('<p>')
for row in toplevel:
format(row)
print("""
</p>
</body>
</html>
""")
(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) 初次實現
由於這裏是結合着項目8的代碼,這裏小編給出運行結果:
(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上: