教學 8 - 使其順~暢¶
到目前為止,我們的應用程式相對簡單 - 顯示 GUI widget、呼叫一個簡單的第三方函式庫,以及在對話框中顯示輸出。所有這些操作都發生得非常快,而且我們的應用程式仍然反應迅速。
但是,在現實世界的應用程式中,我們需要執行複雜的任務或計算,這些任務或計算可能需要一段時間才能完成 - 而在執行這些任務時,我們希望應用程式能保持反應迅速。讓我們對應用程式做一個可能需要花點時間才能完成的變更,看看需要做哪些變更來適應這種行為。
存取 API¶
應用程式需要執行的一項常見耗時任務是向 Web API 提出擷取資料的請求,並將資料顯示給使用者。Web API 有時需要一、兩秒的時間來回應,因此如果我們要呼叫這樣的 API,就必須確保在等待 Web API 回覆時,應用程式不會變得毫無反應。
這是一個玩具應用程式,因此我們沒有真正的 API 可供使用,所以將採用一個範例 API 端點作為資料來源。若您在瀏覽器中開啟
https://tutorial.beeware.org/tutorial/message.json,便會取得包含訊息的
JSON 載荷。
Python 標準函式庫包含了存取 API 所需的所有工具。然而,內建的 API 是非常低階的。它們是 HTTP 通訊協定的良好實作 - 但需要使用者管理許多低階的細節,像是 URL 重定向、會話、驗證和有效載荷編碼。身為「一般瀏覽器使用者」,您可能習慣將這些細節視為理所當然,因為瀏覽器會替您管理這些細節。
因此,人們開發了第三方函式庫來包裝內建的 API,並提供更簡單的 API,更貼近日常的瀏覽器體驗。我們要使用其中一個函式庫來存取 {JSON}占位符 API -
一个名为 httpx 的库。Briefcase 內部使用了
httpx,所以它已經在您的本機環境中了 - 您不需要另外安裝就可以在這裡使用它。
讓我們在應用程式中加入 httpx API 呼叫。修改我們的 pyproject.toml 中的 requires 設定,以包含新的需求:
requires = [
"faker",
"httpx",
]
在 app.py 的頂端加入 import 來匯入 httpx:
import httpx
要使我們的教程異步,請修改 say_hello() 事件處理程序,使其如下所示:
async def say_hello(self, widget):
fake = faker.Faker()
with httpx.Client() as client:
response = client.get("https://tutorial.beeware.org/tutorial/message.json")
payload = response.json()
await self.main_window.dialog(
toga.InfoDialog(
greeting(self.name_input.value),
f"A message from {fake.name()}: {payload['body']}",
)
)
這將會變更「say_hello()」回呼,因此當它被呼叫時,它將會:
- 在 JSON 占位符 API 上進行 GET 請求,以取得 Post 42;
- 將回應解碼成 JSON;
- 提取文章的正文;以及
- 包含該文章的正文作為「訊息」對話方塊的文字,以取代 Faker 產生的文字。
讓我們在 Briefcase 開發者模式中執行更新後的應用程式,檢查我們的變更是否成功。由於我們新增了新的需求,我們需要使用 -r
參數告訴開發人員模式重新安裝需求:
(beeware-venv) $ briefcase dev -r
[helloworld] Installing requirements...
...
[helloworld] Starting in dev mode...
===========================================================================
當您輸入名稱並按下按鈕時,您應該會看到一個類似以下內容的對話框:
。
(beeware-venv) $ briefcase dev -r
[helloworld] Installing requirements...
...
[helloworld] Starting in dev mode...
===========================================================================
當您輸入名稱並按下按鈕時,您應該會看到一個類似以下內容的對話框:

(beeware-venv) C:\...>briefcase dev -r
[helloworld] Installing requirements...
...
[helloworld] Starting in dev mode...
===========================================================================
當您輸入名稱並按下按鈕時,您應該會看到一個類似以下內容的對話框:
。
您無法在開發者模式下執行 Android 應用程式 - 請使用您所選擇的桌面平台說明。
您無法在開發者模式下執行 iOS 應用程式 - 請使用您所選擇的桌面平台說明。
除非您的網路連線速度非常快,否則您可能會發現按下按鈕時,應用程式的圖形使用者介面會稍微鎖定一下。作業系統甚至可能會以「beachball」或「spinner」游標來表示應用程式反應不良。
除非您擁有 真正 快速的網路連接,否則您可能會注意到,當您按下按鈕時,應用程式的 GUI 會鎖定一點。這是因為我們發出的 Web 請求是 同步 的。當我們的應用程式發出 Web 請求時,它會等待 API 回傳回應,然後再繼續。在等待時,它 不允許 應用程式重繪 - 結果,應用程式停止回應。
GUI 事件循環(Event loop)¶
為了理解為什麼會發生這種情況,我們需要深入研究 GUI 應用程式如何運作的細節。具體情況因平台而異;但無論您使用什麼平台或 GUI 環境,概念都是相同的。
從根本上來說,GUI 應用程式是一個看起來像這樣的循環:
while not app.quit_requested():
app.process_events()
app.redraw()
此循環稱為 事件循環 。 (這些不是實際的方法名稱 - 它是 偽代碼 中發生的情況的說明)。
當您按一下按鈕、拖曳捲軸或按下按鍵時,代表你產生一個 事件 。該 事件
被放入佇列中,應用程式將在下次有機會處理事件佇列時處理該事件。響應事件而觸發的程式碼稱為 event handler 。這些事件處理程序作為
process_events() 呼叫的一部分被呼叫。
一旦應用程式處理完所有可用事件,它將 redraw() GUI。這考慮了事件對應用程式顯示造成的任何變化,以及作業系統中發生的任何其他變化 -
例如,另一個應用程式的視窗可能會遮蓋或顯示我們應用程式視窗的一部分,我們的應用程式的重繪需要反映目前可見的視窗部分。
需要注意的重要細節:當應用程式正在處理事件時, 它無法重繪 ,並且 它無法處理其他事件 。
這意味著事件處理程序中包含的任何使用者邏輯都需要快速完成。使用者將觀察到完成事件處理程序的任何延遲,因為 GUI
更新速度會減慢(或停止)。如果延遲足夠長,您的作業系統可能會將此報告為問題 - macOS beachball 和 Windows spinner
圖示是作業系統告訴您您的應用程式在事件處理程序中花費的時間太長。
更新標籤 或 重新計算輸入總數
等簡單操作很容易快速完成。然而,有許多操作無法快速完成。如果您正在執行複雜的數學計算,或對檔案系統上的所有檔案進行索引,或執行網路請求,則您無法 快速完成
- 那些操作本質上很慢。
那麼,我們如何在 GUI 應用程式中執行耗時的操作呢?
非同步程式設計¶
我們需要的是一種方法讓耗時的event handler執行時告訴應用程序,只要可以從中斷的地方恢復,就可以暫時將控制權釋放回事件循環。由應用程式決定何時釋放它;但如果應用程式定期釋放對事件循環的控制,我們就可以擁有一個長時間運行的事件處理程序 並 維護一個響應式 UI。
我們可以透過使用 非同步程式設計 來做到這一點。非同步程式設計是一種描述程式的方式,允許解釋器同時運行多個函數,在所有並發運行的函數之間共用資源。
非同步函數(稱為 協程 )需要明確宣告為非同步。他們還需要在內部聲明何時存在將上下文更改為另一個協程的機會。
在Python中,非同步程式設計是使用 async 和 await 關鍵字以及
asyncio 中的模組來實現的。標準庫。 async
關鍵字允許我們宣告函數是非同步協同例程。 await 關鍵字提供了一種聲明何時存在將上下文更改為另一個協同例程的機會的方法。
asyncio
模組為非同步程式設計提供了一些其他有用的工具和語法。
使教學異步¶
要使我們的教程異步,請修改 say_hello() 事件處理程序,使其如下所示:
async def say_hello(self, widget):
fake = faker.Faker()
async with httpx.AsyncClient() as client:
response = await client.get("https://jsonplaceholder.typicode.com/posts/42")
payload = response.json()
await self.main_window.dialog(
toga.InfoDialog(
greeting(self.name_input.value),
f"A message from {fake.name()}: {payload['body']}",
)
)
與先前的版本相比,此程式碼僅發生了 4 處變更:
- 建立的客戶端是異步
AsyncClient(),而不是同步Client()。這告訴httpx它應該以非同步模式運行,而不是同步模式。 - 用於建立客戶端的上下文管理器被標記為
async。這告訴Python,當進入和退出上下文管理器時,有機會釋放控制權。 - 在
get呼叫中使用了await關鍵字。這會指示應用程式,當我們等待網路的回應時,應用程式可以釋放控制權給事件循環。我們以前見過這個關鍵字 - 當顯示對話方塊時,我們也使用了
await。使用這個關鍵字的原因與 HTTP 請求相同 - 我們需要告訴應用程式,當顯示對話方塊,並且我們正在等待使用者按下按鈕的時候,可以將控制權釋放回事件循環。
同樣重要的是,處理程式本身被定義為 async def,而不只是 def。這會告訴 Python 這個方法是一個異步的 coroutine。我們在教學
3 加入對話方塊時做了這個改變。您只能在宣告為 async def 的方法裡面使用 await 語句。
Toga 允許您使用常規方法或非同步協同例程作為處理程序; Toga 管理幕後的一切,以確保根據需要呼叫或等待處理程序。
如果您儲存這些變更並重新執行應用程式(在開發模式下使用 briefcase dev
,或透過更新並重新執行打包的應用程式),應用程式不會有任何明顯的變更。但是,當您單擊按鈕觸發對話框時,您可能會注意到一些細微的改進:
- 該按鈕返回到
未單擊狀態,而不是停留在單擊狀態。 - 不會出現「沙灘球」/「旋轉器」圖示。
- 如果您在等待對話方塊出現時移動/調整應用程式視窗的大小,則該視窗將會重新繪製。
- 如果您嘗試開啟應用程式選單,該選單將立即出現。
現在我們可以執行完整的應用程式了。然而,由於我們增加了一個額外的需求 (httpx),我們也需要更新應用程式的需求;我們可以將 -r 傳給
briefcase run 來做到這一點。這將更新應用程式的需求,然後重新建立應用程式,再啟動應用程式:
(beeware-venv) $ briefcase run -r
(beeware-venv) $ briefcase run -r
(beeware-venv) C:\...>briefcase run -r
(beeware-venv) $ briefcase run android -r
(beeware-venv) $ briefcase run iOS -r
您應該會看到您的應用程式正在執行,並且在您按下按鈕及擷取網路內容時保持回應。
下一步¶
本教學讓您了解 BeeWare 專案所提供的工具。在本教程中,您將:
- 建立一個新的 GUI 應用程式專案;
- 在開發者模式下運行應用程式
- 將應用程式建立為桌上型電腦作業系統的獨立二進位檔案;
- 將該專案打包分發給其他人;
- 在行動模擬器和/或裝置上執行應用程式;
- 以 Web 應用程式的方式執行應用程式;
- 為您的應用程式新增第三方依賴;以及
- 修改應用程式,使其保持反應靈敏。
那麼–何去何從?
- 如果您想深入瞭解,還有一些額外的 主題教學,會詳細介紹應用程式開發的特定方面。
- 如果您想進一步瞭解如何使用 Toga 建立複雜的使用者介面,您可以深入閱讀 Toga 的說明文件。Toga 也有自己的教學 示範如何使用 widget 工具套件的各種功能。
- 如果您想瞭解更多關於 Briefcase 的功能,可以深入閱讀 Briefcase 的說明文件。