跳转至

教程 8 - 使其光滑

到目前为止,我们的应用程序相对简单——显示图形用户界面控件、调用一个简单的第三方库,以及在对话框中显示输出。所有这些操作都非常迅速,我们的应用程序始终保持响应。

但是,在实际的应用中,我们将需要进行会需要一会儿才能完成的复杂任务。当这些任务进行时,我们想让我们的应用保持响应。让我们更改我们的应用程序,添加一个需要一些时间才能完成的任务,并看看需要怎样更改应用程序,适应这个新的行为。

访问 API

应用程序需要进行的一个常见的耗时任务时在一个网络 API 上请求取回数据,并将数据显示给用户。网络 API 有时需要一两秒才能回应,所以如果我们将要访问这种 API,我们需要确保我们的应用在等待网络 API 返回答案时保持响应。

这是一个玩具应用,因此我们没有真正的API可供使用,所以将使用一个示例API端点作为数据源。若在浏览器中打开https://tutorial.beeware.org/tutorial/message.json,您将获得包含消息的JSON有效负载。

Python 标准库包含了需要访问一个 API 的所有工具,但这些内置 API 非常低级别。它们良好的实现了 HTTP 协议,但它们要求用户管理 URL 重定向、会话、身份验证、数据编码等许多低级别细节。作为一个 “普通浏览器用户”,您可能会把这些细节的管理是为理所当然的,因为浏览器帮您管理它们。

因此,人们开发了第三方库来封装内置 API,并提供更简单的 API,使其更符合日常的浏览器体验。我们将使用其中一个库来访问 {JSON}占位符 API - 一个名为 httpx 的库。公文包在内部使用 httpx,因此它已经存在于本地环境中,无需单独安装即可在此使用。

让我们为应用程序添加一个 httpx API 调用。修改我们的 pyproject.toml 中的 requires 设置,以包含新需求:

requires = [
    "faker",
    "httpx",
]

app.py 顶部添加导入,以导入 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 请求,以获取职位 42;
  • 将响应解码为 JSON 格式;
  • 提取帖子正文;以及
  • 在 "消息 "对话框中加入该帖子的正文,取代 Faker 生成的文本。

让我们在 Briefcase 开发人员模式下运行更新后的应用程序,检查我们的更改是否有效。由于我们添加了一个新需求,因此需要使用 -r 参数告诉开发者模式重新安装需求:

(beeware-venv) $ briefcase dev -r

[helloworld] Installing requirements...
...
[helloworld] Starting in dev mode...
===========================================================================

输入名称并按下按钮后,您会看到一个类似如下的对话框:

《Hello World》教程 8 对话框,在 macOS 上

(beeware-venv) $ briefcase dev -r

[helloworld] Installing requirements...
...
[helloworld] Starting in dev mode...
===========================================================================

输入名称并按下按钮后,您会看到一个类似如下的对话框:

Hello World 教程 8 对话框,在 Linux 上!

(beeware-venv) C:\...>briefcase dev -r

[helloworld] Installing requirements...
...
[helloworld] Starting in dev mode...
===========================================================================

输入名称并按下按钮后,您会看到一个类似如下的对话框:

Hello World 教程 8 对话框,在 Windows 上!

您无法在开发者模式下运行 Android 应用程序,请使用所选桌面平台的说明。

您无法在开发者模式下运行 iOS 应用程序,请使用所选桌面平台的说明。

除非你的网络连接速度非常快,否则你可能会发现,当你按下按钮时,应用程序的图形用户界面会锁定一小会儿。操作系统甚至会用 "沙滩球 "或 "旋转器 "光标来表示应用程序反应迟钝。

除非您的网络连接速度非常快,否则您可能会注意到,当您按下按钮时,应用程序的图形用户界面会锁定一会儿。这是因为我们发出的网络请求是\ 同步\ 的。当我们的应用程序发出网络请求时,它会等待应用程序接口返回响应,然后再继续。在等待的过程中,应用程序允许重新绘制,结果导致应用程序锁定。

图形用户界面事件循环

要理解为什么会出现这种情况,我们需要深入了解图形用户界面应用程序的工作原理。具体细节因平台而异,但无论使用何种平台或图形用户界面环境,高层概念都是相同的。

从根本上说,图形用户界面应用程序就是一个单一的循环,看起来就像:

while not app.quit_requested():
    app.process_events()
    app.redraw()

这个循环被称为 事件循环。(这些并不是实际的方法名称,而是 "伪代码 "的说明)。

当你点击一个按钮、拖动一个滚动条或输入一个键时,你就产生了一个 "事件"。该 "事件 "被放入一个队列,应用程序将在下一次有机会时处理队列中的事件。为响应事件而触发的用户代码称为事件处理程序。这些事件处理程序作为process_events()调用的一部分被调用。

应用程序处理完所有可用事件后,就会 重绘 (redraw())图形用户界面。这将考虑到事件对应用程序显示所造成的任何变化,以及操作系统中发生的任何其他情况,例如,其他应用程序的窗口可能会遮挡或显示我们应用程序的部分窗口,而我们应用程序的重绘将需要反映当前可见的窗口部分。

需要注意的重要细节是:当应用程序在处理事件时,不能重绘也不能处理其他事件

这意味着事件处理程序中包含的任何用户逻辑都需要快速完成。完成事件处理程序的任何延迟都会被用户观察到,表现为图形用户界面更新的减慢(或停止)。如果延迟时间足够长,操作系统可能会将此报告为问题–macOS 的 "沙滩球 "和 Windows 的 "旋转器 "图标就是操作系统在告诉你,你的应用程序在事件处理程序中耗时过长。

像 "更新标签 "或 "重新计算输入总和 "这样的简单操作很容易快速完成。然而,有很多操作是无法快速完成的。如果要执行复杂的数学计算,或为文件系统中的所有文件编制索引,或执行大型网络请求,就不能 "快速完成"–这些操作本身就很慢。

那么,我们如何在图形用户界面应用程序中执行长期操作呢?

异步编程

我们需要的是一种方法,让处于长期事件处理程序中间的应用程序知道,只要我们能从上次中断的地方继续运行,就可以暂时将控制权释放回事件循环。应用程序可以自行决定何时释放控制权;但如果应用程序定期向事件循环释放控制权,我们就可以拥有一个长期运行的事件处理程序,并**保持响应式用户界面。

我们可以通过使用异步编程来实现这一点。异步编程是一种描述程序的方法,它允许解释器同时运行多个函数,在所有并发运行的函数之间共享资源。

异步函数(称为 * 协同例程*)需要明确声明为异步函数。它们还需要在内部声明何时有机会将上下文切换到另一个共同例程。

在 Python 中,异步编程是通过 asyncawait 关键字以及标准库中的 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 处改动:

  1. 创建的客户端是异步的 AsyncClient() 而不是同步的 Client()[。这就告诉httpx` 应在异步模式而非同步模式下运行。
  2. 用于创建客户端的上下文管理器被标记为 async。这就告诉 Python,当上下文管理器进入和退出时,有机会释放控制。
  3. get "调用带有 "等待 "关键字。这指示应用程序在等待网络响应时,可以将控制权释放给事件循环。我们以前见过这个关键字–在显示对话框时我们也使用了await。使用该关键字的原因与 HTTP 请求相同–我们需要告诉应用程序,在显示对话框并等待用户按下按钮时,可以将控制权释放回事件循环。

还需要注意的是,处理程序本身被定义为 async def,而不仅仅是 def。这告诉 Python 该方法是一个异步的例程。我们在教程 3 中添加对话框时做了这一更改。您只能在声明为 async def 的方法中使用 await 语句。

Toga 允许你使用常规方法或异步协程作为处理程序;Toga 在幕后管理一切,确保处理程序按要求被调用或等待。

如果保存这些更改并重新运行应用程序(在开发模式下使用 briefcase dev 或更新并重新运行打包的应用程序),应用程序不会有任何明显的变化。不过,当您点击按钮触发对话框时,您可能会注意到一些细微的改进:

  • 按钮会返回到 "未点击 "状态,而不是停留在 "已点击 "状态。
  • 沙滩球"/"旋转器 "图标不会出现。
  • 如果在等待对话框出现时移动或调整应用程序窗口的大小,窗口将重新绘制。
  • 如果尝试打开应用程序菜单,菜单会立即出现。

现在我们可以运行完整的应用程序了。不过,由于我们添加了一个额外的需求(httpx),因此还需要更新应用程序的需求;我们可以通过向 briefcase run 传递 -r 来做到这一点。这将更新应用程序的需求,然后重新构建应用程序,最后启动应用程序:

(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 项目提供的工具可以做些什么。在本教程中,您可以

  • 创建一个新的图形用户界面应用程序项目;
  • 在开发者模式下运行应用程序
  • 将应用程序作为桌面操作系统的独立二进制文件构建;
  • 将该项目打包分发给其他人;
  • 在移动模拟器和/或设备上运行应用程序;
  • 将应用程序作为网络应用程序运行;
  • 在应用程序中添加了第三方依赖;以及
  • 修改应用程序,使其保持响应速度。

那么–何去何从?