Kivy-Python-交互式应用和游戏指南-全-
Kivy Python 交互式应用和游戏指南(全)
原文:
zh.annas-archive.org/md5/6f1edc8ae20cbffd0b2f654cff980f50译者:飞龙
前言
移动设备已经改变了人们对应用程序的看法。它们增加了交互类型;现在用户期望手势、多点触控、动画、响应性、虚拟键盘和魔法笔。此外,如果你想要避免主要操作系统强加的障碍,兼容性变得至关重要。Kivy 是一个开源的 Python 解决方案,它以易于学习和快速开发的方法满足这些市场需求。自 2013 年 9 月本书首次出版以来,Kivy 一直快速发展,并已发布了两个版本。多亏了一个热情的社区,Kivy 正在一个极其竞争激烈的领域里稳步前进,它以其提供跨平台和高效的替代方案而脱颖而出,这些方案既适用于原生开发,也适用于 HTML5。
本书向您介绍了 Kivy 的世界,涵盖了与交互式应用程序和游戏开发相关的大量重要主题。本书中展示的组件是根据它们对开发最先进应用程序的有用性以及作为更广泛 Kivy 功能示例的选择。遵循这种方法,本书涵盖了 Kivy 库的大部分内容。
本书为您提供了示例,以了解它们的使用方法以及如何整合本书附带的三项项目。第一个项目,漫画创作器,展示了如何构建用户界面(第一章, GUI 基础 – 构建界面,介绍了 Kivy 的基本组件和布局以及如何通过 Kivy 语言进行集成。
第二章, 图形 – 画布,解释了画布的使用以及如何在屏幕上绘制矢量图形。
第三章, 小部件事件 – 绑定动作,教授如何通过界面将用户的交互与程序内部的特定代码连接起来。
第四章, 提升用户体验,介绍了一系列有用的组件,以丰富用户与界面之间的交互。
第五章, 入侵者复仇 – 一个交互式多点触控游戏,展示了构建高度交互式应用程序的组件和策略。
第六章, Kivy Player – 一个 TED 视频流媒体,构建了一个响应式且看起来专业的界面来控制视频流服务。
您需要这本书的内容
在开始阅读这本书之前,你需要具备一些编程经验,并且特别需要理解一些软件工程概念,尤其是继承以及类和实例之间的区别。你应该已经熟悉 Python。尽管如此,代码被尽可能地保持简单,并且避免了使用非常具体的 Python 特性,因此任何其他开发者都可以跟随。不需要有 Kivy 的先验经验,尽管对事件处理、调度和用户界面的基本编程知识将有助于你的学习。你还需要安装 Kivy 1.9.0 及其所有需求。安装说明可以在kivy.org/docs/gettingstarted/installation.html找到。
本书面向对象
本书旨在帮助开发者,特别是希望为不同平台创建 UI/UX 应用的 Python 开发者。本书也将对寻求 HTML5 或原生 Android/iOS 开发替代方案的开发者有所帮助,他们期待学习移动开发及其需求(多点触控、手势和动画),或者希望提高他们对面向对象主题的理解,如继承、类和实例以及事件处理。
术语约定
在本书中,你会找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“这就是我们包含on_touch_down事件的原因。”
代码块设置如下:
1\. # File name: hello.py
2\. import kivy
3\. kivy.require('1.9.0')
4\.
5\. from kivy.app import App
6\. from kivy.uix.button import Label
7\.
8\. class HelloApp(App):
9\. def build(self):
10 return Label(text='Hello World!')
11\.
12\. if __name__=="__main__":
13\. HelloApp().run()
每章的开始处重新编号,为每行代码提供一个唯一的标识符。前一章的代码将不会被引用,如果需要,将会重新复制。当我们希望引起你对代码块中特定部分的注意时,相关的行或项目会被加粗,例如,第 10 行。
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“我们需要一个停止视频的替代方法(不同于停止按钮)。”
注意
警告或重要注意事项以这样的框显示。
小贴士
小贴士和技巧看起来像这样。
读者反馈
我们始终欢迎读者的反馈。告诉我们你对这本书的看法——你喜欢什么或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出你真正能从中受益的标题。
要发送给我们一般性的反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及本书的标题。
如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你购买的所有 Packt 出版物的账户中下载示例代码文件。www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。
错误清单
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在我们的一本书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。通过这样做,你可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果你发现任何错误清单,请通过访问www.packtpub.com/submit-errata,选择你的书籍,点击错误清单提交表单链接,并输入你的错误清单详情。一旦你的错误清单得到验证,你的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误清单部分。
要查看之前提交的错误清单,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将在错误清单部分显示。
盗版
在互联网上,版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过<copyright@packtpub.com>联系我们,并提供涉嫌盗版材料的链接。
我们感谢你在保护我们的作者和我们为你提供有价值的内容方面的帮助。
问答
如果你在这本书的任何方面有问题,你可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。
第一章. GUI 基础 – 构建界面
Kivy 是一个免费的开源 Python 库,它允许快速轻松地开发高度交互的多平台应用程序。Kivy 的执行速度与原生移动替代方案相当,例如 Android 的 Java 或 iOS 的 Objective C。此外,Kivy 有一个巨大的优势,即能够在多个平台上运行,就像 HTML5 一样;在这种情况下,Kivy 的性能更好,因为它不依赖于沉重的浏览器,并且许多组件都是使用 Cython 库在 C 中实现的,这样大多数图形处理都直接在 GPU 中运行。Kivy 在各种硬件和软件环境中在性能和可移植性之间取得了很好的平衡。Kivy 以一个简单但雄心勃勃的目标出现:
"…每个平台相同的代码,至少是我们每天使用的:Linux/Windows/Mac OS X/Android/iOS"
Mathieu Virbel (txzone.net/2011/01/kivy-next-pymt-on-android-step-1-done/)
这种支持已经扩展到 Raspberry Pi,这要归功于 Mathieu Virbel 发起的众筹活动,他是 Kivy 的创造者。Kivy 首次在 2011 年的 EuroPython 上推出,作为一个用于创建自然用户界面的 Python 框架。从那时起,它已经变得更大,并吸引了一个热情的社区。
本书需要一些 Python 知识,以及非常基本的终端技能,但它还要求您理解一些面向对象编程(OOP)的概念。特别是,假设您理解了继承的概念以及实例和类之间的区别。参考以下表格来回顾一些这些概念:
在我们开始之前,您需要安装 Kivy。所有不同平台的安装过程都有文档记录,并且定期在 Kivy 网站上更新:kivy.org/docs/installation/installation.html。
注意
本书中的所有代码都已使用 Kivy 1.9.0 以及 Python 2.7 和 Python 3.4(但 3.3 也应该可以正常工作)进行测试。
注意,Python 3.3+版本对移动端的支持尚未完成。目前,如果我们想为 Android 或 iOS 创建移动应用,我们应该使用 Python 2.7。如果您想了解您的 Python 版本,您可以在终端中执行python -V来检查已安装的 Python 版本。
在本章中,我们首先使用 Kivy 最有趣和最有力的组件之一 – Kivy 语言(.kv)来创建用户界面。Kivy 语言将逻辑与表示分离,以保持代码的简单直观;它还将在界面级别链接组件。在未来的章节中,您还将学习如何使用纯 Python 代码和 Kivy 作为库动态构建和修改界面。
这里是您即将学习到的所有技能列表:
-
启动 Kivy 应用程序
-
使用 Kivy 语言
-
通过基本属性和变量实例化和个性化小部件(GUI 组件)
-
区分固定、比例、绝对和相对坐标
-
通过布局创建响应式 GUI
-
在不同的文件中模块化代码
本章涵盖了在 Kivy 中构建图形用户界面(GUI)的所有基础知识。首先,我们将学习运行应用程序的技术以及如何使用和集成小部件。之后,我们将介绍本书的主要项目,即漫画创作者,并编写 GUI 的主要结构,我们将在接下来的两章中继续使用。在本章结束时,您将能够从铅笔和纸张草图开始构建 GUI,并学习一些使 GUI 能够响应窗口大小的技术。
基本界面 – Hello World!
让我们动手编写我们的第一个代码。
小贴士
下载示例代码
您可以从您在www.packtpub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
以下是一个“Hello World”程序:
1\. # File name: hello.py
2\. import kivy
3\. kivy.require('1.9.0')
4\.
5\. from kivy.app import App
6\. from kivy.uix.button import Label
7\.
8\. class HelloApp(App):
9\. def build(self):
10\. return Label(text='Hello World!')
11\.
12\. if __name__=="__main__":
13\. HelloApp().run()
注意
这仅仅是 Python 代码。启动 Kivy 程序与启动任何其他 Python 应用程序没有区别。
为了运行代码,您需要在 Windows 或 Linux 中打开一个终端(命令行或控制台),并指定以下命令:python hello.py --size=150x100(--size是一个用于指定屏幕大小的参数)。
在 Mac 上,您必须在/Applications中安装Kivy.app后输入kivy而不是python。第 2 行和第 3 行验证我们在计算机上安装了适当的 Kivy 版本。
注意
如果您尝试使用比指定版本更旧的 Kivy 版本(比如 1.8.0)启动我们的应用程序,那么第 3 行将引发Exception错误。如果我们有一个更新的版本,则不会引发此Exception错误。
在本书的大多数示例中,我们省略了对kivy.require的调用,但您将在您在线下载的代码中找到它(www.packtpub.com/),并且强烈建议在实际项目中使用它。程序使用 Kivy 库中的两个类(第 5 行和第 6 行) – App和Label。类App是任何 Kivy 应用程序的起点。将App视为我们将添加其他 Kivy 组件的空窗口。
我们通过继承使用App类;App类成为HelloApp子类或子类的基类(第 8 行)。在实践中,这意味着HelloApp类具有App类的所有变量和方法,以及我们在HelloApp类的主体(第 9 行和第 10 行)中定义的任何内容。最重要的是,App是任何 Kivy 应用程序的起点。我们可以看到第 13 行创建了一个HelloApp实例并运行了它。
现在,HelloApp类的主体只是覆盖了现有的App类的一个方法,即build(self)方法。这个方法必须返回窗口内容。在我们的例子中,是一个Label,它包含文本Hello World!(第 10 行)。Label是一个允许你在屏幕上显示一些文本的小部件。
注意
小部件是 Kivy GUI 组件。小部件是我们为了创建用户界面而组合的最小图形单元。
以下截图显示了执行hello.py代码后的结果屏幕:

那么,Kivy 仅仅是另一个 Python 库吗?嗯,是的。但作为库的一部分,Kivy 提供自己的语言,以便将逻辑与表示分离,并将界面元素链接起来。此外,请记住,这个库将允许您将应用程序移植到许多平台。
让我们开始探索 Kivy 语言。我们将把之前的 Python 代码分成两个文件,一个用于表示(界面),另一个用于逻辑。第一个文件包括以下 Python 行:
14\. # File name: hello2.py
15\. from kivy.app import App
16\. from kivy.uix.button import Label
17\.
18\. class Hello2App(App):
19\. def build(self):
20\. return Label()
21\.
22\. if __name__=="__main__":
23\. Hello2App().run()
hello2.py代码与hello.py非常相似。区别在于build(self)方法没有Hello World!消息。相反,消息已被移动到 Kivy 语言文件(hello2.kv)中的text属性。
注意
属性是一个可以用来改变小部件内容、外观或行为的属性。
以下是hello2.kv的代码(规则),它显示了如何使用text属性修改Label内容(第 27 行):
24\. # File name: hello2.kv
25\. #:kivy 1.9.0
26\. <Label>:
27\. text: 'Hello World!'
你可能会想知道 Python 或 Kivy 如何知道这两个文件(hello2.py和hello2.kv)是相关的。这通常在开始时令人困惑。关键是App子类的名称,在这个例子中是HelloApp。
注意
App类子类名称的开头部分必须与 Kivy 文件名相匹配。例如,如果类的定义是class FooApp(App),那么文件名必须是foo.kv,并且位于主文件(执行App的run()方法的文件)所在的同一目录中。
一旦包含了这个考虑,这个例子就可以像我们运行上一个例子那样运行。我们只需确保我们调用的是主文件 - python hello2.py --size=150x100。
这是我们第一次接触 Kivy 语言,因此我们应该深入了解一下。第 25 行(hello2.kv)告诉 Python 应该使用 Kivy 的最小版本。它与hello.py中前两行所做的相同。在 Kivy 语言头部以#:开头的指令被称为指令。我们将在本书的其余部分省略版本指令,但请记住在你的项目中包含它。
<Label>:规则(第 26 行)表示我们将要修改Label类。
注意
Kivy 语言以一系列规则的形式表达。规则是一段代码,它定义了 Kivy 小部件类的内 容、行为和外观。一个规则总是以尖括号内的一个小部件类名开头,后面跟着一个冒号,例如这样,<Widget Class>:
在规则内部,我们使用'Hello World!'(第 27 行)设置了text属性。本节中的代码将生成与之前相同的输出屏幕。一般来说,Kivy 中的所有事情都可以使用纯 Python 和从 Kivy 库中导入必要的类来完成,就像我们在第一个例子(hello.py)中所做的那样。然而,使用 Kivy 语言有许多优点,因此本书解释了所有 Kivy 语言中的展示编程,除非我们需要添加动态组件,在这种情况下,使用 Kivy 作为传统的 Python 库更为合适。
如果你是一位经验丰富的程序员,你可能担心修改Label类会影响我们从Label创建的所有潜在实例,因此它们都将包含相同的Hello World文本。这是真的,我们将在下一节研究一个更好的方法来做这件事。
基本小部件 - 标签和按钮
在最后一节中,我们使用了Label类,这是 Kivy 提供的多个小部件之一。你可以把小部件想象成我们用来设置 GUI 的界面块。Kivy 有一套完整的小部件 - 按钮、标签、复选框、下拉菜单等等。你可以在 Kivy 的 API 中找到它们,位于kivy.uix包下(kivy.org/docs/api-kivy.html)。
我们将学习如何创建我们自己的个性化小部件的基本知识,而不会影响 Kivy 小部件的默认配置。为了做到这一点,我们将在widgets.py文件中使用继承来创建MyWidget类:
28.# File name: widgets.py
29\. from kivy.app import App
30\. from kivy.uix.widget import Widget
31\.
32\. class MyWidget(Widget):
33\. pass
34\.
35\. class WidgetsApp(App):
36\. def build(self):
37\. return MyWidget()
38\.
39\. if __name__=="__main__":
40\. WidgetsApp().run()
在第 32 行,我们继承自基类Widget并创建了子类MyWidget。创建自己的Widget而不是直接使用 Kivy 类是通用的做法,因为我们希望避免将我们的更改应用到所有未来实例的 Kivy Widget类。在我们的前一个例子(hello2.kv)中,修改Label类(第 26 行)将影响其所有未来的实例。在第 37 行,我们直接实例化了MyWidget而不是Label(就像我们在hello2.py中做的那样),因此我们现在可以区分我们的小部件(MyWidget)和 Kivy 小部件(Widget)。其余的代码与之前我们覆盖的内容类似。
以下是对应的 Kivy 语言代码(widgets.kv):
41\. # File name: widgets.kv
42\. <MyWidget>:
43\. Button:
44\. text: 'Hello'
45\. font_size: 32
46\. color: .8,.9,0,1
47\. pos: 0, 100
48\. size: 100, 50
49\. Button:
50\. text: 'World!'
51\. font_size: 32
52\. color: .8,.9,0,1
53\. pos: 100,0
54\. size: 100, 50
注意,现在我们使用的是按钮而不是标签。Kivy 中的大多数基本小部件以类似的方式工作。实际上,Button只是Label的子类,它包含更多的属性,如背景颜色。
将hello2.kv中第 26 行的注释(<Label>:)与前面代码(widgets.kv)中的第 43 行(Button:)进行比较。我们为Label(和MyWidget)类使用了规则类注释(<Class>:),但对于Button使用了不同的注释(Instance:)。这样,我们定义了MyWidget有两个Button实例(第 43 行和第 49 行)。
最后,我们设置了Button实例的属性。font_size属性设置文本的大小。color属性设置文本颜色,并指定为 RGBA 格式(红色、绿色、蓝色和 alpha/透明度)。size和pos属性设置小部件的大小和位置,由一对固定坐标(x 为水平,y 为垂直)组成,即窗口上的确切像素。
小贴士
注意,坐标(0,0)位于左下角,即笛卡尔原点。许多其他语言(包括 CSS)使用左上角作为(0,0)坐标,所以请注意!
以下截图显示了widgets.py和widgets.kv的输出,并带有一些有用的注释:

在之前的代码(widgets.kv)中,有一些可以改进的地方。首先,按钮有一些重复的属性:pos、color和font_size。相反,让我们像创建MyWidget一样创建自己的Button,这样就可以轻松保持按钮设计的统一。其次,固定位置相当烦人,因为当屏幕大小调整时,小部件不会调整。让我们在widgets2.kv文件中使其对屏幕大小做出响应:
55\. # File name: widgets2.kv
56\. <MyButton@Button>:
57\. color: .8,.9,0,1
58\. font_size: 32
59\. size: 100, 50
60\.
61\. <MyWidget>:
62\. MyButton:
63\. text: 'Hello'
64\. pos: root.x, root.top - self.height
65\. MyButton:
66\. text: 'World!'
67\. pos: root.right - self.width, root.y
在此代码(widgets2.kv)中,我们创建并自定义了MyButton类(第 56 至第 59 行)和实例(第 62 至第 67 行)。注意我们定义MyWidget和MyButton的方式之间的差异。
注意
由于我们没有在widgets.py中将MyButton基类定义为MyWidget(widgets.py的第 32 行),我们必须在 Kivy 语言规则(第 56 行)中指定@Class。在MyWidget类的情况下,我们也需要从 Python 端定义它的类,因为我们直接实例化了它(widgets.py的第 37 行)。
在这个例子中,每个Button类的位置都是响应式的,这意味着它们始终位于屏幕的角落,无论窗口大小如何。为了实现这一点,我们需要使用两个内部变量——self和root。你可能对变量self很熟悉。正如你可能猜到的,它只是对Widget本身的引用。例如,self.height(第 64 行)的值为50,因为那是特定MyButton类的高度。变量root是对层次结构顶部的Widget类的引用。例如,root.x(第 64 行)的值为0,因为那是MyWidget实例在widgets.py的第 37 行创建时的 X 轴位置。
MyWidget默认使用整个窗口空间;因此,原点是(0,0)。x、y、width和height也是小部件属性,我们可以使用它们分别断开pos和size。
固定坐标仍然是组织窗口中的小部件和元素的一种费力的方式。让我们转向更智能的方法——布局。
布局
毫无疑问,固定坐标是组织多维空间中元素的最灵活方式;然而,它非常耗时。相反,Kivy 提供了一套布局,将简化组织小部件的工作。Layout是一个实现不同策略来组织嵌入小部件的Widget子类。例如,一种策略可以是按网格(GridLayout)组织小部件。
让我们从简单的FloatLayout例子开始。它的工作方式与我们直接在另一个Widget子类内部组织小部件的方式非常相似,只是现在我们可以使用比例坐标(窗口总大小的“百分比”)而不是固定坐标(精确像素)。
这意味着我们不需要在上一节中使用self和root所做的计算。以下是一个与上一个例子相似的 Python 代码示例:
68\. # File name: floatlayout.py
69\.
70\. from kivy.app import App
71\. from kivy.uix.floatlayout import FloatLayout
72\.
73\. class FloatLayoutApp(App):
74\. def build(self):
75\. return FloatLayout()
76\.
77\. if __name__=="__main__":
78\. FloatLayoutApp().run()
在前面的代码(floatlayout.py)中,并没有真正的新内容,除了使用了FloatLayout(第 75 行)。有趣的部分在于相应的 Kivy 语言(floatlayout.kv)中:
79\. # File name: floatlayout.py
80\. <Button>:
81\. color: .8,.9,0,1
82\. font_size: 32
83\. size_hint: .4, .3
84\.
85\. <FloatLayout>:
86\. Button:
87\. text: 'Hello'
88\. pos_hint: {'x': 0, 'top': 1}
89\. Button:
90\. text: 'World!'
91\. pos_hint: {'right': 1, 'y': 0}
在floatlayout.kv中,我们使用了两个新属性——size_hint(第 83 行)和pos_hint(第 88 行和第 91 行)。它们与size和pos类似,但接收比例坐标,值范围从0到1;(0,0)是左下角,(1,1)是右上角。例如,第 83 行的size_hint属性将宽度设置为窗口宽度的 40%,将高度设置为当前窗口高度的 30%。pos_hint属性(第 88 行和第 91 行)也有类似的情况,但表示方式不同——一个 Python 字典,其中键(例如,'x'或'top')表示引用小部件的哪个部分。例如,'x'是左边界。
注意,我们在第 88 行使用top键代替y键,在第 91 行使用right键代替x键。top和right键分别引用Button的顶部和右侧边缘。在这种情况下,我们也可以使用x和y来表示两个轴;例如,我们可以将第 91 行写成pos_hint: {'x': .85, 'y': 0}。然而,right和top键避免了我们进行一些计算,使代码更清晰。
下一个截图显示了结果,以及pos_hint字典中可用的键:

可用的pos_hint键(x、center_x、right、y、center_y和top)对于对齐边缘或居中很有用。例如,pos_hint: {'center_x':.5, 'center_y':.5}可以使小部件无论窗口大小如何都居中。
我们本可以使用top和right属性与widgets2.kv的固定定位(第 64 行和第 67 行),但请注意pos不接受 Python 字典({'x':0,'y':0}),只接受与(x,y)相对应的值对。因此,我们不应使用pos属性,而应直接使用x、center_x、right、y、center_y和top属性(不是字典键)。例如,我们不应使用pos: root.x, root.top - self.height(第 64 行),而应使用:
x: 0
top: root.height
注意
属性x、center_x、right、y、center_y和top始终指定固定坐标(像素),而不是比例坐标。如果我们想使用比例坐标,我们必须在Layout(或App)内部,并使用pos_hint属性。
我们也可以强制Layout使用固定值,但如果我们不小心处理属性,可能会出现冲突。如果我们使用任何Layout;pos_hint和size_hint具有优先级。如果我们想使用固定定位属性(pos、x、center_x、right、y、center_y、top),我们必须确保我们没有使用pos_hint属性。其次,如果我们想使用size、height或width属性,那么我们需要将size_hint轴的值设置为None,以使用绝对值。例如,size_hint: (None, .10)允许我们使用高度属性,但保持窗口宽度的 10%。
以下表格总结了关于定位和尺寸属性我们所看到的内容。第一列和第二列表示属性的名称及其相应的值。第三列和第四列表示它是否适用于布局和小部件。
| 属性 | 值 | 对于布局 | 对于小部件 |
|---|---|---|---|
size_hint |
一对 w,h:w 和 h 表示比例(从 0 到 1 或 None)。 |
是 | 否 |
size_hint_x size_hint_y |
从 0 到 1 或 None 的比例,表示宽度(size_hint_x)或高度(size_hint_y)。 |
是 | 否 |
pos_hint |
包含一个 x 轴键(x,center_x 或 right)和一个 y 轴键(y,center_y 或 top)的字典。值是从 0 到 1 的比例。 |
是 | 否 |
size |
一对 w,h:w 和 h 表示以像素为单位的固定宽度和高度。 |
是,但需设置 size_hint: (None, None) |
是 |
width |
固定像素数。 | 是,但需设置 size_hint_x: None |
是 |
height |
固定像素数。 | 是,但需设置 size_hint_y: None |
是 |
pos |
一对 x,y:表示像素中的固定坐标(x,y)。 |
是,但不要使用 pos_hint |
是 |
x, right or center_x |
固定像素数。 | 是,但不要在 pos_hint 中使用 x, right 或 center_x |
是 |
y, top or center_y |
固定像素数。 | 是,但不要在 pos_hint 中使用 y, top 或 center_y |
是 |
我们必须小心,因为一些属性的行为取决于我们使用的布局。Kivy 目前有八个不同的布局,以下表格中进行了描述。左侧列显示了 Kivy 布局类的名称。右侧列简要描述了它们的工作方式。
| 布局 | 详情 |
|---|---|
FloatLayout |
通过 size_hint 和 pos_hint 属性以比例坐标组织小部件。值是介于 0 和 1 之间的数字,表示相对于窗口大小的比例。 |
RelativeLayout |
与 FloatLayout 以相同的方式操作,但定位属性(pos,x,center_x,right,y,center_y,top)相对于 Layout 大小而不是窗口大小。 |
GridLayout |
以网格形式组织小部件。您必须指定两个属性中的至少一个——cols(用于列)或 rows(用于行)。 |
BoxLayout |
根据属性 orientation 的值是 horizontal 或 vertical,在一行或一列中组织小部件。 |
StackLayout |
与 BoxLayout 类似,但在空间不足时将移动到下一行或列。在设置 orientation 方面有更多的灵活性。例如,rl-bt 以从右到左、从下到上的顺序组织小部件。允许任何 lr(从左到右)、rl(从右到左)、tb(从上到下)和 bt(从下到上)的组合。 |
ScatterLayout |
与 RelativeLayout 的工作方式类似,但允许多指触控手势进行旋转、缩放和移动。它在实现上略有不同,所以我们稍后会对其进行回顾。 |
PageLayout |
将小部件堆叠在一起,创建一个多页效果,允许使用侧边框翻页。我们经常使用另一个布局来组织每个页面内的元素,这些页面只是小部件。 |
Kivy API (kivy.org/docs/api-kivy.html) 提供了每个布局的详细解释和良好示例。属性的行性行为取决于布局,有时可能会出乎意料。以下是一些有助于我们在 GUI 构建过程中的提示:
-
size_hint、size_hint_x和size_hint_y在所有布局上(除了PageLayout)都起作用,但行为可能不同。例如,GridLayout将尝试取同一行或列上的 x 提示和 y 提示的平均值。 -
你应该使用从 0 到 1 的值来设置
size_hint、size_hint_x和size_hint_y。然而,你可以使用大于 1 的值。根据布局的不同,Kivy 会使小部件比容器大,或者尝试根据同一轴上的提示总和重新计算比例。 -
pos_hint只适用于FloatLayout、RelativeLayout和BoxLayout。在BoxLayout中,只有axis-x键(x、center_x、right)在vertical方向上工作,反之亦然在horizontal方向上。对于固定定位属性(pos、x、center_x、right、y、center_y和top)也适用类似的规则。 -
size_hint、size_hint_x和size_hint_y可以始终设置为None以便使用size、width和height。
每个布局都有更多属性和特性,但有了这些,我们将能够构建几乎任何 GUI。一般来说,建议使用布局的原样,而不是用我们使用的属性强制它,更好的做法是使用更多布局并将它们组合起来以达到我们的目标。下一节将教我们如何嵌入布局,并提供更全面的示例。
嵌入布局
布局是小部件的子类。从开始(第 43 行)我们就已经在小部件内部嵌入小部件了,所以嵌入的小部件是否也是布局并不重要。在本节中,我们将通过一个综合示例来探索上一节讨论的位置属性的效果。这个示例在视觉上可能不太吸引人,但它将有助于说明一些概念,并提供一些你可以用来测试不同属性的代码。以下是为示例编写的 Python 代码 (layouts.py):
92\. # File name: layouts.py
93\. from kivy.app import App
94\. from kivy.uix.gridlayout import GridLayout
95\.
96\. class MyGridLayout(GridLayout):
97\. pass
98\.
99\. class LayoutsApp(App):
100\. def build(self):
101\. return MyGridLayout()
102\.
103\. if __name__=="__main__":
104\. LayoutsApp().run()
上一段代码中没有新内容——我们只是创建了MyGridLayout。最终输出在下一张截图中有展示,其中包含了一些关于不同布局的说明:

嵌入布局
在此截图中,六个不同的 Kivy 布局被嵌入到两行的GridLayout中(第 107 行),以展示不同小部件属性的行为。代码简单明了,尽管内容广泛。因此,我们将分五个片段研究相应的 Kivy 语言代码(layouts.kv)。以下是片段 1:
105\. # File name: layouts.kv (Fragment 1)
106\. <MyGridLayout>:
107\. rows: 2
108\. FloatLayout:
109\. Button:
110\. text: 'F1'
111\. size_hint: .3, .3
112\. pos: 0, 0
113\. RelativeLayout:
114\. Button:
115\. text: 'R1'
116\. size_hint: .3, .3
117\. pos: 0, 0
在此代码中,MyGridLayout通过rows属性(第 107 行)定义了行数。然后我们添加了前两个布局——每个布局都有一个Button的FloatLayout和RelativeLayout。两个按钮都定义了pos: 0, 0属性(第 112 行和第 117 行),但请注意,在前一个截图中的Button F1(第 109 行)位于整个窗口的左下角,而Button R1(第 114 行)位于RelativeLayout的左下角。原因是FloatLayout中的pos坐标不是相对于布局的位置。
注意
注意,无论我们使用哪种布局,pos_hint始终使用相对坐标。换句话说,如果使用pos_hint而不是pos,前面的例子将不会工作。
在片段 2 中,向MyGridLayout添加了一个GridLayout:
118\. # File name: layouts.kv (Fragment 2)
119\. GridLayout:
120\. cols: 2
121\. spacing: 10
122\. Button:
123\. text: 'G1'
124\. size_hint_x: None
125\. width: 50
126\. Button:
127\. text: 'G2'
126\. Button:
128\. text: 'G3'
129\. size_hint_x: None
130\. width: 50
在这种情况下,我们使用cols属性定义了两列(第 120 行),并使用spacing属性将内部小部件彼此之间隔开 10 像素(第 121 行)。注意,在前一个截图中也注意到,第一列比第二列细。我们通过将按钮G1(第 122 行)和G3(第 128 行)的size_hint_x设置为None和width设置为50来实现这一点。
在片段 3 中,添加了一个AnchorLayout:
131\. # File name: layouts.kv (Fragment 3)
132\. AnchorLayout:
133\. anchor_x: 'right'
135\. anchor_y: 'top'
136\. Button:
137\. text: 'A1'
138\. size_hint: .5, .5
139\. Button:
140\. text: 'A2'
141\. size_hint: .2, .2
我们已将anchor_x属性设置为right,将anchor_y属性设置为top(第 134 行和第 135 行),以便将元素排列在窗口的右上角,如前一个截图所示,其中包含两个按钮(第 136 行和第 139 行)。这种布局非常适合在其中嵌入其他布局,例如顶部菜单栏或侧边栏。
在片段 4 中,添加了一个BoxLayout:
142\. # File name: layouts.kv (Fragment 4)
143\. BoxLayout:
144\. orientation: 'horizontal'
145\. Button:
146\. text: 'B1'
147\. Button:
148\. text: 'B2'
149\. size_hint: 2, .3
150\. pos_hint: {'y': .4}
151\. Button:
152\. text: 'B3'
上述代码展示了如何使用orientation属性设置为horizontal的BoxLayout。此外,第 149 行和第 150 行显示了如何使用size_hint和pos_hint将按钮B2向上移动。
最后,片段 5 添加了一个StackLayout:
153\. # File name: layouts.kv (Fragment 5)
154\. StackLayout:
155\. orientation: 'rl-tb'
156\. padding: 10
157\. Button:
158\. text: 'S1'
159\. size_hint: .6, .2
160\. Button:
161\. text: 'S2'
162\. size_hint: .4, .4
163\. Button:
164\. text: 'S3'
165\. size_hint: .3, .2
166\. Button:
167\. text: 'S4'
168\. size_hint: .4, .3
在这种情况下,我们添加了四个不同大小的按钮。注意,理解嵌入布局的规则非常重要,这些规则是通过将orientation属性设置为rl-tb(从右到左,从上到下,第 155 行)来组织小部件的。同时注意,padding属性(第 156 行)在StackLayout的小部件和边框之间增加了 10 像素的空间。
页面布局——滑动页面
PageLayout 的工作方式与其他布局不同。它是一种动态布局,从某种意义上说,它允许通过其边框翻页。其理念是它的组件是堆叠在一起的,我们只能看到最上面的一个。
以下示例说明了其用法,利用了上一节的示例。Python 代码(pagelayout.py)如下所示:
169\. # File name: pagelayout.py
170\. import kivy
171\.
172\. from kivy.app import App
173\. from kivy.uix.pagelayout import PageLayout
174\.
175\. class MyPageLayout(PageLayout):
176\. pass
177\.
178\. class PageLayoutApp(App):
179\. def build(self):
180\. return MyPageLayout()
181\.
182\. if __name__=="__main__":
183\. PageLayoutApp().run()
这段代码中除了使用 PageLayout 类之外没有新内容。对于 Kivy 语言代码(pagelayout.kv),我们将研究 PageLayout 的属性。我们只是简单地修改了上一节中研究的 layouts.kv 文件(第 105 至 107 行),现在称为 pagelayout.kv:
184\. # File name: pagelayout.kv
185\. <Layout>:
186\. canvas:
187\. Color:
188\. rgba: 1, 1, 1, 1
189\. Rectangle:
190\. pos: self.pos
191\. size: self.size
192\.
193\. <MyPageLayout>:
194\. page: 3
195\. border: 120
196\. swipe_threshold: .4
197\. FloatLay...
所有布局都继承自一个名为 Layout 的基类。在第 185 行,我们以与之前修改 Button 类(第 80 行)相同的方式修改了这个基类。
小贴士
如果我们想对具有公共基类(如 Layout)的所有子小部件应用更改,我们可以在基类中引入这些更改。Kivy 将将这些更改应用到所有从它派生的类中。
默认情况下,布局没有背景颜色,这在 PageLayout 将它们堆叠在一起时不太方便,因为我们能看到底层布局的元素。第 186 至 191 行将绘制一个白色(第 187 和 188 行)矩形,其大小(第 190 行)和位置(第 191 行)与 Layout 相对应。为了做到这一点,我们需要使用 canvas,它允许我们在屏幕上直接绘制形状。这个主题将在下一章(第二章, 图形 - 画布)中深入解释。您可以在下面的屏幕截图中看到结果:

如果你在电脑上运行代码,你会注意到它会带你到上一节示例中的 AnchorLayout 对应的页面。原因是我们将 page 属性设置为值 3(第 194 行)。从 0 开始计数,这个属性告诉 Kivy 首先显示哪个页面。border 属性告诉 Kivy 侧边框有多宽(用于滑动到上一个或下一个屏幕)。最后,swipe_threshold 告诉我们需要滑动屏幕的百分比,才能更改页面。下一节将使用到目前为止学到的某些布局和属性来显示一个更专业的屏幕。
我们的项目 – 漫画创作器
现在我们有了所有必要的概念,能够创建我们想要的任何界面。本节描述了我们将通过以下三个章节(漫画创作器)进行改进的项目。该项目的核心思想是一个简单的应用程序,用于绘制一个棍子人。下面的屏幕截图是我们所追求的 GUI 的草图(线框):

我们可以在草图中区分几个区域。首先,我们需要一个绘图空间(右上角)来放置我们的漫画。我们需要一个工具箱(左上角)包含一些绘图工具来绘制我们的图形,还有一些通用选项(从底部向上数第二个)——清除屏幕、删除最后一个元素、组合元素、更改颜色和使用手势模式。最后,有一个状态栏(中心底部)向用户提供一些信息——图形数量和最后执行的操作。根据我们在本章中学到的知识,有多个解决方案来组织这个屏幕。我们将使用以下方案:
-
在左上角的工具箱区域使用
AnchorLayout。在其内部将是一个两列的GridLayout,用于绘图工具。 -
在右上角的绘图空间使用
AnchorLayout。在其内部将是一个RelativeLayout,以便在相对空间中绘制。 -
在底部的通用选项和状态栏区域使用
AnchorLayout。在其内部将是一个垂直方向的BoxLayout,用于在状态栏上方组织通用选项:-
用于通用选项按钮的水平方向的
BoxLayout。 -
用于状态栏标签的水平方向的
BoxLayout。
-
我们将通过为每个区域创建不同的文件来使用这个结构——comiccreator.py、comiccreator.kv、toolbox.kv、generaltools.kv、drawingspace.kv和statusbar.kv。让我们从comiccreator.py开始:
198\. # File name: comiccreator.py
199\. from kivy.app import App
200\. from kivy.lang import Builder
201\. from kivy.uix.anchorlayout import AnchorLayout
200\.
201\. Builder.load_file('toolbox.kv')
202\. Builder.load_file('drawingspace.kv')
203\. Builder.load_file('generaloptions.kv')
204\. Builder.load_file('statusbar.kv')
205\.
206\. class ComicCreator(AnchorLayout):
207\. pass
208\.
209\. class ComicCreatorApp(App):
210\. def build(self):
211\. return ComicCreator()
212\.
213\. if __name__=="__main__":
214\. ComicCreatorApp().run()
注意,我们明确使用Builder.load_file指令(第 203 行到第 206 行)加载一些文件。没有必要明确加载comiccreator.kv,因为 Kivy 会自动通过提取ComicCreatorApp名称的第一部分来加载它。对于ComicCreator,我们选择AnchorLayout。这不是唯一的选择,但它使代码更清晰,因为第二级也由AnchorLayout实例组成。
尽管使用一个简单的部件会更清晰,但这是不可能的,因为Widget类不遵守AnchorLayout内部所需的size_hint和pos_hint属性。
小贴士
记住,只有布局才遵守size_hint和pos_hint属性。
这是comiccreator.kv的代码:
216\. # File name: comiccreator.kv
217\. <ComicCreator>:
218\. AnchorLayout:
219\. anchor_x: 'left'
220\. anchor_y: 'top'
221\. ToolBox:
222\. id: _tool_box
223\. size_hint: None, None
224\. width: 100
225\. AnchorLayout:
226\. anchor_x: 'right'
227\. anchor_y: 'top'
228\. DrawingSpace:
229\. size_hint: None, None
230\. width: root.width - _tool_box.width
231\. height: root.height - _general_options.height - _status_bar.height
232\. AnchorLayout:
233\. anchor_x: 'center'
234\. anchor_y: 'bottom'
235\. BoxLayout:
236\. orientation: 'vertical'
237\. GeneralOptions:
238\. id: _general_options
239\. size_hint: 1,None
240\. height: 48
241\. StatusBar:
242\. id: _status_bar
243\. size_hint: 1,None
244\. height: 24
这段代码遵循之前展示的漫画创作器结构。在第一级基本上有三个AnchorLayout实例(第 219 行、第 226 行和第 233 行)和一个组织通用选项和状态栏的BoxLayout(第 236 行)。
我们将ToolBox的宽度设置为 100 像素,将GeneralOptions和StatusBar的高度分别设置为 48 像素和 24 像素(第 241 行和第 245 行)。这随之带来一个有趣的问题——绘图空间应该使用屏幕剩余的宽度和高度(无论屏幕大小如何)。为了实现这一点,我们将利用 Kivy id(第 223 行、第 239 行和第 243 行),它允许我们在 Kivy 语言中引用其他组件。在第 231 行和第 232 行,我们从root.width(第 231 行)减去tool_box.width,从root.height(第 232 行)减去general_options.height和status_bar.height。
注意
Kivy 的id允许我们在 Kivy 语言规则内创建内部变量,以便访问其他小部件的属性。
现在,让我们继续在toolbox.kv中探索 Kivy 语言:
245\. # File name: toolbox.kv
246\. <ToolButton@ToggleButton>:
247\. size_hint: None, None
248\. size: 48,48
249\. group: 'tool'
250\.
251\. <ToolBox@GridLayout>:
252\. cols: 2
253\. padding: 2
254\. ToolButton:
255\. text: 'O'
256\. ToolButton:
257\. text: '/'
258\. ToolButton:
259\. text: '?'
我们创建了一个ToolButton类,它定义了绘图工具的大小,并引入了一个新的 Kivy 小部件——ToggleButton。与普通Button的区别在于它会在我们再次点击之前保持按下状态。以下是一个带有激活的ToolButton的工具箱示例:

一个ToggleButton实例可以与另一个ToggleButton实例关联,因此一次只能点击其中一个。我们可以通过将相同的group属性(第 250 行)分配给想要一起响应的ToggleButton实例来实现这一点。在这种情况下,我们希望属于同一组的所有ToolButton实例,因为我们只想一次绘制一个图形;我们将其作为类定义的一部分(第 247 行)。
在第 252 行,我们将ToolBox实现为一个GridLayout的子类,并给ToolButton实例添加了一些字符占位符('O'、'/'和'?'),这些占位符将在后续章节中替换为更合适的内容。
以下是generaloptions.kv文件的代码:
260\. # File name: generaloptions.kv
261\. <GeneralOptions@BoxLayout>:
262\. orientation: 'horizontal'
263\. padding: 2
264\. Button:
265\. text: 'Clear'
266\. Button:
267\. text: 'Remove'
268\. ToggleButton:
269\. text: 'Group'
268\. Button:
270\. text: 'Color'
271\. ToggleButton:
272\. text: 'Gestures'
下面是一个如何使用继承来帮助我们分离组件的例子。我们使用了ToggleButton实例(第 269 行和第 273 行),它们不受之前ToolButton实现的影响。此外,我们没有将它们关联到任何group,因此它们彼此独立,只会保持一种模式或状态。代码只定义了GeneralOptions类,遵循我们的初始结构。以下是将得到的截图:

statusbar.kv文件在用法上与BoxLayout非常相似:
274\. # File name: statusbar.kv
275\. <StatusBar@BoxLayout>:
276\. orientation: 'horizontal'
277\. Label:
278\. text: 'Total Figures: ?'
279\. Label:
280\. text: "Kivy started"
不同之处在于它组织的是标签而不是按钮。以下是将得到的截图:

最后,以下是drawingspace.kv文件的代码:
281\. # File name: drawingspace.kv
282\. <DrawingSpace@RelativeLayout>:
283\. Label:
284\. markup: True
285\. text: '[size=32px][color=#3e6643]The[/color] [sub]Comic[/sub] [i][b]Creator[/b][/i][/size]'
除了定义 DrawingSpace 是 RelativeLayout 的子类之外,我们还引入了 Kivy 的 markup,这是一个用于美化 Label 类文本的不错特性。它的工作方式与基于 XML 的语言类似。例如,在 HTML 中,<b>I am bold</b> 会指定加粗文本。首先,您需要激活它(第 285 行),然后只需将您想要美化的文本嵌入 [tag] 和 [/tag] 之间(第 286 行)。您可以在 Kivy API 的文档中找到完整的标签列表和描述,在 Label 的文档中(kivy.org/docs/api-kivy.uix.label.html)。在先前的例子中,size 和 color 是不言自明的;sub 指的是下标文本;b 表示加粗;i 表示斜体。
下面是显示我们项目 GUI 的截图:

在接下来的章节中,我们将为这个目前仅由占位符小部件组成的界面添加相应的功能。然而,仅仅用几行代码就能得到这样的结果,这令人兴奋。我们的 GUI 已经准备就绪,从现在起我们将专注于其逻辑开发。
概述
本章涵盖了所有基础知识,并介绍了一些不那么基本的概念。我们介绍了如何配置类、实例和模板。以下是本章中我们学习使用的 Kivy 元素列表:
-
基本小部件 –
Widget、Button、ToggleButton和Label -
布局 –
FloatLayout、RelativeLayout、BoxLayout、GridLayout、StackLayout、AnchorLayout和PageLayout -
属性 –
pos、x、y、center_x、center_y、top、right、size、height、width、pos_hint、size_hint、group、spacing、padding、color、text、font_size、cols、rows、orientation、anchor_x和anchor_y -
变量 –
self和root -
其他 –
id和标记标签size、color、b、i和sub
我们还可以使用 Kivy 语言中的许多其他元素,但通过本章,我们已经理解了如何组织元素的一般思路。借助 Kivy API,我们应该能够显示大多数用于 GUI 设计的元素。然而,还有一个非常重要的元素需要单独研究——canvas,它允许我们在小部件内部绘制矢量形状,例如在 PageLayout 示例中作为背景绘制的白色矩形。这是 Kivy 中一个非常重要的主题,下一章将完全致力于它。
第二章. 图形 – 画布
任何 Kivy Widget都包含一个Canvas对象。Kivy 的Canvas是一组绘图指令,定义了Widget的图形表示。
小贴士
注意名称,因为它往往容易引起混淆!Canvas对象不是我们绘制的地方(例如,像 HTML5 中那样);它是一组在坐标空间中绘图的指令。
坐标空间指的是我们绘制图形的地方。所有 Kivy 小部件共享相同的坐标空间,以及一个Canvas实例,即绘制在其上的指令。坐标空间不受窗口大小或应用程序屏幕大小的限制,这意味着我们可以在可见区域之外绘制。
我们将讨论如何通过添加到Canvas对象(例如,按钮的画布)的指令来绘制和操作小部件的表示。以下是我们将要涵盖的最重要技能列表:
-
通过顶点指令绘制基本几何形状(直线和曲线线、椭圆和多边形)
-
使用颜色,并通过上下文指令旋转、平移和缩放坐标空间
-
顶点指令和上下文指令之间的区别以及它们如何相互补充
-
我们可以使用来修改图形指令执行顺序的
Canvas的三组不同指令 -
通过
PushMatrix和PopMatrix存储和检索当前坐标空间上下文
使用 Kivy 画布带来了一些技术挑战,因为 Kivy 在考虑效率的同时集成了图形处理。这些挑战最初可能不明显,但如果我们理解了根本问题,它们并没有什么特别困难。这就是为什么下一节将专门介绍我们在使用画布时面临的主要考虑因素。
理解画布
在学习本章的示例之前,回顾以下与图形显示相关的特定性非常重要:
-
坐标空间指的是我们绘制图形的地方,它并不局限于窗口大小
-
Canvas对象是一组在坐标空间中绘图的指令,而不是我们绘制的地方 -
所有
Widget对象都包含它们自己的Canvas(我们稍后会看到),但它们都共享相同的坐标空间,即App对象中的那个。
例如,如果我们向特定的Canvas实例(例如,按钮的画布)添加旋转指令,那么这也会影响所有即将在坐标空间中显示图形的后继图形指令。无论图形属于不同小部件的画布,它们都共享相同的坐标空间。
因此,我们需要学习技术,在用图形指令修改坐标空间后,将其恢复到原始状态。
小贴士
添加到不同Canvas对象的所有图形指令,这些对象同时属于不同的Widget对象,影响相同的坐标空间。我们的任务是确保在用图形指令修改后,坐标空间保持其原始状态。
我们还需要扩展的另一个重要概念是Widget。我们已经知道,部件是允许我们构建界面的块。
注意
Widget也是一个占位符(带有其位置和大小),但不一定是占位符。部件的画布指令不仅限于部件的特定区域,而是整个坐标空间。
这直接增加了之前共享坐标空间的问题。我们不仅需要控制共享坐标空间的事实,而且我们没有对绘制位置的任何限制。一方面,这使得 Kivy 非常高效,并给我们提供了很多灵活性。另一方面,这似乎需要控制很多。幸运的是,Kivy 提供了必要的工具,可以轻松地解决这个问题。
下一个部分将介绍可以添加到画布中以绘制基本形状的可用的图形指令。之后,我们将探索改变坐标空间上下文的图形指令,并举例说明共享坐标空间的问题。最后一部分专注于在漫画创作者中展示所获得的知识,在那里我们学习最常用的技术来掌握画布的使用,考虑到其特性。到本章结束时,我们将完全控制屏幕上显示的图形。
绘制基本形状
在开始之前,让我们介绍本章所有示例中都将重用的 Python 代码:
1\. # File name: drawing.py
2\. from kivy.app import App
3\. from kivy.uix.relativelayout import RelativeLayout
4\.
5\. class DrawingSpace(RelativeLayout):
6\. pass
7\.
8\. class DrawingApp(App):
9\. def build(self):
10\. return DrawingSpace()
11\.
12\. if __name__=="__main__":
13\. DrawingApp().run()
我们从RelativeLayout创建了子类DrawingSpace。它本可以从任何Widget继承,但使用RelativeLayout通常是图形的一个好选择,因为我们通常希望在部件内部绘制,这意味着相对于其位置。
让我们从画布开始。基本上,我们可以添加到画布中的指令有两种:顶点指令和上下文指令。
注意
顶点指令继承自VertexInstruction基类,并允许我们在坐标空间中绘制矢量形状。
上下文指令(Color、Rotate、Translate和Scale)继承自ContextInstruction基类,并允许我们对坐标空间上下文应用变换。通过坐标空间上下文,我们指的是形状(在顶点指令中指定)在坐标空间中绘制的条件。
基本上,顶点指令是我们绘制的,而上下文指令影响我们绘制的位置和方式。以下是本章第一个示例的截图:

在前面的屏幕截图中,灰色网格将简化读取代码中出现的坐标。此外,与每个单元格关联的白色字母将用于引用形状。网格和字母都不是 Kivy 示例的一部分。前面的屏幕截图展示了我们通过顶点指令学习绘制的 10 个基本图形。几乎所有的可用 Kivy 类都包含在这个示例中,我们可以用它们创建任何 2D 几何形状。由于顶点指令使用固定坐标,因此以 500 x 200(python drawing.py --size=500x200)的屏幕尺寸运行此示例非常重要,以便正确地可视化形状。
我们将研究 Kivy 语言 (drawing.kv),并附带与其相关的图形(和坐标)的小段代码,这样会更容易理解。让我们从形状 A(矩形)开始:

以下为形状 A 的代码片段:
14\. # File name: drawing.kv (vertex instructions)
15\. <DrawingSpace>:
16\. canvas:
17\. Rectangle:
18\. pos: self.x+10,self.top-80
19\. size: self.width*0.15, self.height*0.3
矩形 是一个很好的起点,因为它与我们设置小部件属性的方式相似。我们只需设置 pos 和 size 属性。
注意
顶点指令的 pos 和 size 属性与 Widget 的 pos 和 size 属性不同,因为它们属于 VertexInstruction 基类。指定顶点指令属性的所有值都是固定值。
这意味着我们无法像在 第一章 GUI 基础 - 构建界面 中使用小部件那样使用 size_hint 或 pos_hint 属性。然而,我们可以使用 self 的属性来实现类似的结果(第 18 行和第 19 行)。
让我们继续处理形状 B(类似 Pac-Man 的图形):

以下为形状 B 的代码片段:
20\. Ellipse:
21\. angle_start: 120
22\. angle_end: 420
23\. pos: 110, 110
24\. size: 80,80
椭圆 与 Rectangle 非常相似,但它有三个新属性:angle_start、angle_end 和 segments。前两个属性指定椭圆的起始和结束角度。0° 角度是北(或 12 点钟),它们按顺时针方向相加。因此,angle_start 是 120°(90° + 30°),这是类似 Pac-Man 的图形的下颚(第 21 行)。angle_end 的值是 420°(360° + (90°-30°)),它比 angle_start 大,因为我们需要 Kivy 按顺时针方向绘制 Ellipse。如果我们指定一个低于 angle_start 的值,Kivy 将按逆时针方向绘制,绘制 Pac-Man 的嘴巴而不是身体。
让我们继续处理形状 C(三角形):

25\. Ellipse:
26\. segments: 3
27\. pos: 210,110
28\. size: 60,80
形状 C 的三角形实际上是通过 segments 属性(第 26 行)获得的另一个 Ellipse。让我们这样表达:如果您必须用三条线绘制一个椭圆,您最终得到的最好的结果是一个三角形。如果您有四条线,您将得到一个矩形。实际上,您需要无限多条线才能得到完美的 Ellipse,但计算机无法处理这一点(屏幕的分辨率也无法支持这一点),因此我们需要在某个地方停止。默认的 segments 是 180。请注意,如果您有一个圆(即大小:x,x),您将始终得到等边多边形(例如,如果您只指定四个 segments,则得到一个正方形)。
我们可以一起分析形状 D、E、F 和 G:

29\. Triangle:
30\. points: 310,110,340,190,380,130
31\. Quad:
32\. points: 410,110,430,180,470,190,490,120
33\. Line:
34\. points: 10,30, 90,90, 90,10, 10,60
35\. Point:
36\. points: 110,30, 190,90, 190,10, 110,60
37\. pointsize: 3
Triangle(形状 D)、Quad(形状 E)和 Line(形状 F)的工作方式类似。它们的 points 属性(第 30、32 和 34 行)分别表示三角形、四边形和线的角。points 属性是一系列坐标,格式为 (x1, y1, x2, y2)。Point 也与这三个形状类似。它使用 points 属性(第 36 行),但在这个情况下用来表示一系列点(形状 G)。它还使用 pointsize(第 37 行)属性来表示 Points 的大小。
让我们继续探讨形状 H:

38\. Bezier:
39\. points: 210,30, 290,90, 290,10, 210,60
40\. segments: 360
41\. dash_length: 10
42\. dash_offset: 5
贝塞尔 是一条曲线,它使用 points 属性作为曲线线的“吸引点”(贝塞尔曲线背后有一个数学形式,我们在这本书中不会涉及,因为它超出了范围,但您可以在维基百科中找到足够的信息 en.wikipedia.org/wiki/Bézier_curve)。这些点是吸引点,因为线并不触及所有点(只是它们中的第一个和最后一个)。Bezier 的点(第 39 行)彼此之间的距离与 Line 的点(第 34 行)或 Point 的点(第 36 行)之间的距离相同;它们只是向右平移了 100 像素。您可以直观地比较贝塞尔曲线(形状 H)的结果与 Line(形状 G)或 Point(形状 H)的结果。我们还包含了两个其他属性 dash_length(第 41 行),用于表示断续线的长度,以及 dash_offset(第 42 行),用于表示划痕之间的距离。
让我们来探讨最后两个形状 I 和 J:

43\. Mesh:
44\. mode: 'triangle_fan'
45\. vertices: 310,30,0,0, 390,90,0,0, 390,10,0,0, 310,60,0,0
46\. indices: 0,1,2,3
47\. Mesh:
48\. mode: 'triangle_fan'
49\. vertices: 430,90,0,0, 470,90,0,0, 490,70,0,0, 450,10,0,0, 410,70,0,0, 430,90,0,0,
50\. indices: 0,1,2,3,4,5
我们添加了两个Mesh指令(第 43 行和第 47 行)。一个Mesh指令是由三角形组成的复合体,在计算机图形和游戏中有许多应用。本书中没有足够的空间来介绍使用此指令的高级技术,但至少我们将了解其基础知识,并能够绘制平面多边形。mode属性设置为triangle_fan(第 44 行),这意味着网格的三角形被填充了颜色,而不是例如只绘制边界。
vertices属性是一个坐标元组。为了本例的目的,我们将忽略所有的 0。这将使我们剩下 45 行中的四个坐标(或顶点)。这些点与形状F、G和H相对相同。让我们想象一下,当我们从左到右遍历顶点列表时,形状I中的三角形是如何创建的,每次使用三个顶点。形状I由两个三角形组成。第一个三角形使用第一个、第二个和第三个顶点;第二个三角形使用第一个、第三个和第四个顶点。一般来说,如果我们位于列表的第 i 个顶点,则使用第一个顶点、第(i-1)个顶点和第 i 个顶点来绘制一个三角形。最终的网格(形状J)展示了另一个示例。它包含三个被以下截图中的蓝色线条包围的三角形:

indexes属性包含一个与顶点数量相同的列表(不计 0),并指示顶点列表遍历的顺序,从而改变组成网格的三角形。
到目前为止,我们研究过的所有多边形都已经着色完毕。如果我们需要绘制多边形的边界,我们应该使用Line。从原则上讲,对于像三角形这样的基本形状来说这似乎很简单,但如何只用点来画一个圆呢?幸运的是,Line具有使事情变得更容易的适当属性。
下一个示例将展示您如何构建以下截图中的图形:

线条示例
我们保留了灰色坐标和字母来识别截图中的每个单元格。Python 代码应在 400 x 100 的屏幕尺寸下运行:python drawing.py --size=400x100。以下是为前一个截图的drawing.kv代码:
51\. # File name: drawing.kv (Line Examples)
52\. <DrawingSpace>:
53\. canvas:
54\. Line:
55\. ellipse: 10, 20, 80, 60, 120, 420, 180
56\. width: 2
57\. Line:
58\. circle: 150, 50, 40, 0, 360, 180
59\. Line:
60\. rectangle: 210,10,80,80
61\. Line:
62\. points: 310,10,340,90,390,20
63\. close: True
在之前的代码中,我们使用特定的属性添加了四个Line指令。第一个Line指令(第 54 行,形状A)与我们的 Pac-Man(第 20 行)相似。ellipse属性(第 55 行)分别指定了x、y、width、height、angle_start、angle_end和segments。参数的顺序难以记忆,因此我们应该始终将 Kivy API 放在我们身边(kivy.org/docs/api-kivy.graphics.vertex_instructions.html)。我们还设置了Line的width使其更粗(第 56 行)。
第二个Line指令(第 57 行,形状B)引入了一个在顶点指令中没有对应属性的特性:circle。与ellipse属性的区别在于,前三个参数(第 58 行)定义了Circle的中心(150, 50)和半径(40)。其余的保持不变。第三个Line(第 59 行,形状C)由rectangle(第 60 行)定义,参数简单为x、y、width和height。最后一个Line(第 61 行,形状D)是定义多边形最灵活的方式。我们指定了点(第 62 行),数量不限。close属性(第 63 行)连接了第一个和最后一个点。
我们涵盖了与顶点指令相关的多数指令和属性。我们应该能够使用 Kivy 在二维空间中绘制任何几何形状。如果您想了解更多关于每个指令的详细信息,应该访问 Kivy API(kivy.org/docs/api-kivy.graphics.vertex_instructions.html)。现在,轮到上下文指令来装饰这些单调的黑白多边形了。
添加图像、颜色和背景
在本节中,我们将讨论如何将图像和颜色添加到我们的图形中,以及如何控制哪个图形位于哪个图形之上。我们继续使用第一节的相同 Python 代码。这次,我们以 400 x 100 的屏幕尺寸运行它:python drawing.py --size=400x100。以下截图显示了本节的最终结果:

图像和颜色
以下是对应的drawing.kv代码:
64\. # File name: drawing.kv (Images and colors)
65\. <DrawingSpace>:
66\. canvas:
67\. Ellipse:
68\. pos: 10,10
69\. size: 80,80
70\. source: 'kivy.png'
71\. Rectangle:
72\. pos: 110,10
73\. size: 80,80
74\. source: 'kivy.png'
75\. Color:
76\. rgba: 0,0,1,.75
77\. Line:
78\. points: 10,10,390,10
79\. width: 10
80\. cap: 'square'
81\. Color:
82\. rgba: 0,1,0,1
83\. Rectangle:
84\. pos: 210,10
85 size: 80,80
86\. source: 'kivy.png'
87\. Rectangle:
88\. pos: 310,10
89\. size: 80,80
此代码从Ellipse(第 67 行)和Rectangle(第 71 行)开始。我们使用了source属性,它将图像插入到每个多边形中装饰。kivy.png图像是 80 x 80 像素,背景为白色(没有任何 alpha/透明度通道)。结果显示在“图像和颜色”截图的前两列中。
在第 75 行,我们使用了上下文指令Color来改变坐标空间上下文的颜色(使用rgba属性:红色、绿色、蓝色和透明度)。这意味着下一个顶点指令将以rgba改变的颜色绘制。上下文指令基本上是改变当前的坐标空间上下文。在截图(第 77 行)中,你可以看到底部(第 77 行)的细蓝色条(或本书打印版本的非常深灰色条)呈现为透明蓝色(第 76 行),而不是之前示例中的默认白色(1,1,1,1)。我们使用cap属性(第 80 行)设置了线的端点形状为方形。
我们在第 81 行再次改变了颜色。之后,我们绘制了两个更多的矩形,一个带有kivy.png图像,另一个没有。在前面的截图(执行命令:python drawing.py --size=300x100)中,你可以看到图像的白色部分已经变成了绿色,或在本书的打印版本中为浅灰色,就像右侧的基本Rectangle一样。
小贴士
Color指令就像一盏照亮kivy.png图像的光,它不仅仅是在其上绘画。
在截图中有另一个重要的细节需要注意。底部蓝色的线(在打印版本中为深灰色)覆盖了前两个多边形,并在最后两个多边形下方。指令是按顺序执行的,这可能会带来一些不期望的结果。Kivy 提供了一个解决方案,使这种执行更加灵活和结构化,我们将在下一节中介绍。
结构化图形指令
除了canvas实例外,一个 Widget 还包括两个其他画布实例:canvas.before和canvas.after。
注意
Widget类有三个集合的指令(canvas.before、canvas和canvas.after)来组织执行顺序。通过它们,我们可以控制哪些元素将进入背景或保持在前景。
以下drawing.kv文件显示了这三个集合(第 92、98 和 104 行)的指令示例:
90\. # File name: drawing.kv (Before and After Canvas)
91\. <DrawingSpace>:
92\. canvas.before:
93\. Color:
94\. rgba: 1,0,0,1
95\. Rectangle:
96\. pos: 0,0
97\. size: 100,100
98\. canvas:
99\. Color:
100\. rgba: 0,1,0,1
101\. Rectangle:
102\. pos: 100,0
103\. size: 100,100
104\. canvas.after:
105\. Color:
106\. rgba: 0,0,1,1
107\. Rectangle:
108\. pos: 200,0
109\. size: 100,100
110\. Button:
111\. text: 'A very very very long button'
112\. pos_hint: {'center_x': .5, 'center_y': .5}
113\. size_hint: .9,.1
在每个集合中,都绘制了一个不同颜色的矩形(第 95、101 和 107 行)。以下是说明画布执行顺序的图解。每个代码块左上角的数字表示执行顺序:

画布执行顺序
注意我们没有为Button定义任何canvas、canvas.before或canvas.after,但 Kivy 内部确实有。由于Button在屏幕上显示图形(例如,它包含与background_color属性关联的Rectangle),因此它在其画布集合中有指令。最终结果如下截图所示(执行命令:python drawing.py --size=300x100):

画布前后
Button(子元素)的图形被canvas.after中的指令覆盖。很明显,canvas.before和canvas的指令在显示Button之前执行,但它们之间执行了什么?当我们在继承中工作时,我们想在子类中添加应该在基类的canvas指令集之前执行的指令,这是必要的。同时,当我们将 Python 代码和 Kivy 语言规则混合时,这也是一种便利。我们将在本章的最后部分研究一些与漫画创作者相关的实际例子,并在第四章中回顾这个主题,改进用户体验。
目前,理解我们有三组指令(Canvas)提供了一些在屏幕上显示图形时的灵活性就足够了。现在让我们探索一些与顶点指令变换相关的更多上下文指令。
旋转、平移和缩放坐标空间
Rotate、Translate和Scale是应用于顶点指令的上下文指令,这些指令在坐标空间中显示。如果我们忘记坐标空间是所有小部件共享的,并且它占据了窗口的大小(实际上比这还要大,因为坐标没有限制,我们可以在窗口外绘制),它们可能会带来意外的结果。首先,我们将在本节中了解这条指令的行为,在下一节中,我们可以更深入地分析它们带来的问题,并学习使事情变得更容易的技术。
让我们从新的drawing.kv代码开始:
114\. # File name: drawing.kv (Rotate, Translate and Scale)
115\. <DrawingSpace>:
116\. pos_hint: {'x':.5, 'y':.5}
117\. canvas:
118\. Rectangle:
119\. source: 'kivy.png'
120\. Rotate:
121\. angle: 90
122\. axis: 0,0,1
123\. Color:
124\. rgb: 1,0,0 # Red color
125\. Rectangle:
126\. source: 'kivy.png'
127\. Translate:
128\. x: -100
129\. Color:
130\. rgb: 0,1,0 # Green color
131\. Rectangle:
132\. source: 'kivy.png'
133\. Translate:
134\. y: -100
135\. Scale:
136\. xyz:(.5,.5,0)
137\. Color:
138\. rgb: 0,0,1 # Blue color
139\. Rectangle:
140\. source: 'kivy.png'
在此代码中,我们首先做的事情是将DrawingSpace(RelativeLayout)的坐标(0, 0)定位在屏幕中心(第 116 行)。我们创建了一个带有kivi.png图形的Rectangle,我们之前已经修改过它来指示原始x轴和y轴。
结果展示在以下截图的右上角(使用python drawing.py --size=200x200执行):

旋转、平移和缩放
在第 120 行,我们在 z 轴上(第 122 行)应用了 90°的Rotate指令。值是(x, y, z),这意味着我们可以使用 3D 空间中的任何向量。想象一下,这是在DrawingSpace的左下角钉上一个钉子,然后我们逆时针旋转它。
小贴士
默认情况下,旋转的钉子总是坐标(0, 0),但我们可以通过origin属性改变这种行为。
截图的左上部分(“旋转、平移和缩放”)显示了旋转后的结果。我们用红色(使用rgb属性而不是rgba属性)绘制了相同的矩形以突出显示。在向坐标空间上下文添加旋转之后,我们也修改了相对的 X 轴和 Y 轴。第 128 行考虑到轴是旋转的,为了将坐标空间向下平移(通常是 Y 轴),它将-100px 设置到 X 轴上。我们在左下角用绿色Color绘制了相同的Rectangle。请注意,图像仍然在旋转,并且只要我们不将坐标空间上下文恢复到原始角度,它就会继续旋转。
小贴士
上下文指令持续有效,直到我们再次更改它们。另一种避免这种情况的方法是在RelativeLayout内部工作。如果你还记得上一章,它允许我们使用相对于小部件的坐标进行操作。
要缩放或放大图像,我们将坐标空间上下文(第 133 行)平移到截图的右下角。请注意,我们使用 Y 轴而不是 X 轴,因为上下文仍然是旋转的。缩放操作在第 135 行进行,此时图像的宽度和高度将减半。Scale指令朝向(0, 0)坐标缩放,最初位于左下角。然而,在所有这些上下文修改之后,我们需要考虑这个坐标在哪里。首先,我们旋转了轴(第 120 行),使 X 轴垂直,Y 轴水平。然后,将坐标空间向下平移(第 127 行)和向右平移(第 133 行),(0, 0)坐标位于右下角,X 轴是垂直的,Y 轴是水平的。
注意
Scale使用当前坐标空间上下文的尺寸比例,而不是原始尺寸。例如,要恢复原始尺寸,我们应该使用xyz: (2,2,0)而不是仅仅使用xyz: (1,1,0)。
到目前为止,在本章中,我们已经讨论了Canvas实例是一组包含上下文指令和顶点指令的指令集。上下文指令应用于影响顶点指令在坐标空间中显示条件的坐标空间上下文。
我们将在本章的下一部分和最后一部分中,将一些知识应用到我们的项目中,添加* Stickman*。我们将介绍两个重要的上下文指令来处理小部件之间共享相同坐标空间的问题:PushMatrix和PopMatrix。
漫画创作者:PushMatrix 和 PopMatrix
让我们将一些图形插入到我们在第一章开始的项目中,GUI 基础 - 构建界面。在此之前,我们需要回顾本章与坐标空间相关的两个重要课程:
-
坐标空间不受任何位置或大小的限制。它通常以屏幕左下角为原点。为了避免这种情况,我们使用
RelativeLayout,它内部执行了一个平移到Widget位置的变换。 -
一旦坐标空间上下文被任何指令变换,它就会保持这种状态,直到我们指定不同的内容。
RelativeLayout也通过两个上下文指令解决了这个问题,我们将在本节中研究这些指令:PushMatrix和PopMatrix。
在本节中,我们使用 RelativeLayout 来避免共享坐标空间的问题,但当我们处于任何其他类型的 Widget 内部时,我们也会解释它的替代方案。我们将向我们的项目中添加一个新文件(comicwidgets.kv)。在 comicreator.py 中,我们需要将我们的新文件添加到 Builder:
Builder.load_file('comicwidgets.kv')
文件 comicwidgets.kv 将包含特殊的小部件,我们将为项目创建这些小部件。在本章中,我们将添加 StickMan 类:
141\. # File name: comicwidgets.kv
142\. <StickMan@RelativeLayout>:
143\. size_hint: None, None
144\. size: 48,48
145\. canvas:
146\. PushMatrix
147\. Line:
148\. circle: 24,38,5
149\. Line:
150\. points: 24,33,24,15
151\. Line:
152\. points: 14,5,24,15
153\. Line:
154\. points: 34,5,24,15
155\. Translate:
156\. y: 48-8
157\. Rotate:
158\. angle: 180
159\. axis: 1,0,0
160\. Line:
161\. points: 14,5,24,15
162\. Line:
163\. points: 34,5,24,15
164\. PopMatrix
在第 142 行,StickMan 子类从 RelativeLayout 继承,以方便定位和使用上下文指令。我们定义了大小为 48 x 48 的 StickMan。StickMan 由定义头部、身体、左腿、右腿、左臂和右臂的六条线组成(第 147 到 163 行)。您可以在以下屏幕截图中的三个地方看到 StickMan 的结果:

漫画创作者
第一个 StickMan 是最后一个 ToolButton 设计的一部分,而其他两个出现在 绘图空间 中;其中一个是缩放的。请注意,腿部的代码(第 151 到 154 行)与手臂的代码(第 160 到 163 行)完全相同;区别在于我们将坐标空间向上平移(第 155 和 156 行)并在 x 轴上旋转 180°(第 157 到 159 行)。这样,我们就节省了一些绘制 stickman 的数学计算。
我们已经转换并旋转了坐标空间上下文;因此,我们应该撤销这些上下文更改,以便一切都能保持最初的状态。我们不是向 Translate 和 Rotate 指令添加更多指令以返回坐标空间上下文,而是使用了两个方便的 Kivy 指令:PushMatrix 和 PopMatrix。一开始,我们使用了 PushMatrix(第 146 行),这将保存当前的坐标空间上下文,而在最后,我们使用了 PopMatrix(第 164 行)以将上下文恢复到原始状态。
注意
PushMatrix 保存当前的坐标空间上下文,而 PopMatrix 恢复最后保存的坐标空间上下文。因此,被 PushMatrix 和 PopMatrix 包围的变换指令(Scale、Rotate 和 Translate)不会影响界面的其余部分。
我们将扩展这种方法,向 ToolBox 左上角的其他两个 ToolButton 实例(圆形和线条)添加形状。我们在 toolbox.kv 中添加此代码:
165\. # File name: toolbox.kv
166\. <ToolButton@ToggleButton>:
167\. size_hint: None,None
168\. size: 48,48
169\. group: 'tool'
170\. canvas:
171\. PushMatrix:
172\. Translate:
173\. xy: self.x,self.y
174\. canvas.after:
175\. PopMatrix:
176\.
177\. <ToolBox@GridLayout>:
178\. cols: 2
179\. padding: 2
180\. ToolButton:
181\. canvas:
182\. Line:
183\. circle: 24,24,14
184\. ToolButton:
185\. canvas:
186\. Line:
187\. points: 10,10,38,38
188\. ToolButton:
189\. StickMan:
190\. pos_hint: {'center_x':.5,'center_y':.5}
在ToolButton类(第 166 行)中,我们在指令集的canvas中使用了PushMatrix(第 171 行)来保存坐标空间当前状态。然后,Translate(第 172 行)将图形指令移动到ToolButton的位置,这样我们就可以在每个ToolButton上使用相对坐标(第 180 行到第 190 行)。最后,在canvas.after中添加了PopMatrix(第 175 行)以恢复坐标空间。
遵循不同画布(指令集)的执行顺序非常重要。例如,让我们逐步跟随包含圆圈(第 180 行)的ToolButton画布的执行顺序:首先,ToolButton类的canvas具有PushMatrix和Translate(第 170 行);其次,ToolButton实例的canvas包含圆圈(第 181 行),最后,基类的canvas.after具有PopMatrix(第 174 行)。我们只是实现了与RelativeLayout相同的技巧。
注意
RelativeLayout内部包含PushMatrix和PopMatrix。因此,我们可以在其中安全地添加指令,而不会影响界面的其余部分。
让我们通过在绘图空间中缩放我们的stickman来结束这一章,并说明画布执行顺序的另一个特性。以下是drawingspace.kv的代码:
191\. # File name: drawingspace.kv
192\. <DrawingSpace@RelativeLayout>:
193\. StickMan:
194\. pos_hint: {'center_x':.5,'center_y':.5}
195\. canvas.before:
196\. Translate:
197\. xy: -self.width/2, -self.height/2
198\. Scale:
199\. xyz: 2,2,0
200\. StickMan:
第一个StickMan被平移和旋转了(第 193 行到第 199 行),但第二个没有(第 200 行)。我们讨论了上下文指令会影响全局坐标空间,但当我们看到截图(“漫画创作者”)的结果时,我们意识到第二个实例没有被第 196 行和第 198 行的线条进行缩放或平移。发生了什么?答案并不明显。答案与StickMan类画布内的PushMatrix和PopMatrix有关吗?不是的,因为它们都在同一组指令中。
我们实现ToolButton的方式遵循RelativeLayout类的实现方式。StickMan继承自RelativeLayout,因此在StickMan类(从RelativeLayout继承)的canvas.before中实际上还有一个PushMatrix,以及相应的canvas.after中的PopMatrix。从第 196 行到第 199 行的指令是在RelativeLayout的canvas.before中的PopMatrix执行之后执行的,因此上下文在RelativeLayout的相应PushMatrix上得到恢复。
最后,请注意指令必须在canvas.before中,因为它们是在现有指令之前添加的,即那些实际绘制stickman的指令。换句话说,如果我们简单地在画布中添加它们,那么stickman将会在平移和缩放之前被绘制。
漫画创作器(comiccreator.kv),generaloptions.kv和statusbar.kv的其他文件没有修改,因此我们不再展示它们。上下文和顶点指令易于理解。然而,我们必须非常注意执行顺序,并确保在执行所需的顶点指令后,将坐标空间上下文保持在正常状态。最后,请注意,屏幕上显示的所有内容都是由画布内部的指令(或指令集)显示的,包括例如Label文本和Button背景。
概述
本章解释了理解使用画布所必需的概念。我们涵盖了顶点和上下文指令的使用,以及如何操作指令执行的顺序。我们还介绍了如何处理canvas的转换,无论是反转所有转换还是使用RelativeLayout。以下是本章我们学习使用的全部组件:
-
顶点指令(及其许多相关属性):
Rectangle(pos,size),Ellipse(pos,size,angle_start,angle_end,segments),Triangle(points),Quad(points),Point(points,pointsize),Line(points,ellipse,circle,rectangle,width,close,dash_lenght,dash_offset,和cap),Bezier(points,segments,dash_lenght和dash_offset),以及Mesh(mode,vertices,indices) -
适用于所有顶点指令的
source属性 -
三组画布指令:
canvas.before,canvas和canvas.after -
上下文指令(及其一些属性):
Color(rgba,rgb),Rotate(angle,axis,origin),Translate(x,y,xy),Scale(xyz),PushMatrix和PopMatrix
列表相当全面,但当然还有一些剩余的组件可以在 Kivy API 中找到。重要的是我们讨论了使用画布背后的概念。请随意使用提供的示例来加强本章重要概念的理解。你应该感到舒适地将事物组合起来,使你的界面生动起来,这样你实际上可以用它来绘图。下一章将专注于事件处理和直接从 Python 操作 Kivy 对象。
第三章。小部件事件 – 绑定动作
在本章中,你将学习如何将动作集成到 图形用户界面(GUI)组件中;一些动作将与画布相关联,而其他动作将与 Widget 管理相关联。我们将学习如何动态处理事件,以便使应用程序能够响应用户交互。在本章中,你将获得以下技能:
-
通过 ID 和属性引用 GUI 的不同部分
-
覆盖、绑定、解绑和创建 Kivy 事件
-
动态将小部件添加到其他小部件中
-
动态向画布添加顶点和上下文指令
-
在小部件、其父元素和其窗口之间转换相对和绝对坐标
-
使用属性来保持 GUI 与更改同步
这是一个令人兴奋的章节,因为我们的应用程序将开始与用户交互,应用在前两个章节中获得的观念。到本章结束时,我们的 Comic Creator 项目的所有基本功能都将准备就绪。这包括可拖动的形状、可调整大小的圆圈和线条、清除小部件空间、删除最后添加的图形、将多个小部件分组以一起拖动,以及更新 状态栏 以反映用户的最后操作。
属性、ID 和根
在 第一章,GUI 基础 – 构建界面 中,我们区分了 Comic Creator 的四个主要组件:工具箱、绘图空间、常规选项 和 状态栏。在本章中,我们将使这些组件相互交互,因此我们需要向我们在前几章中创建的项目类中添加一些属性。这些属性将引用界面的不同部分,以便它们可以通信。例如,ToolBox 类需要引用 DrawingSpace 实例,以便 ToolButton 实例可以在其中绘制各自的图形。以下图表显示了在 comiccreator.kv 文件中创建的所有关系:

Comic Creator 的内部引用
我们还在 第一章,GUI 基础 – 构建界面 中学习了,ID 允许我们在 Kivy 语言中引用其他小部件。
注意
ID 仅用于 Kivy 语言内部。因此,我们需要创建属性以便在 Python 代码中引用界面内的元素。
以下是对 Comic Creator 项目的 comiccreator.kv 文件进行了一些修改以创建必要的 ID 和属性:
1\. File Name: comiccreator.kv
2\. <ComicCreator>:
3\. AnchorLayout:
4\. anchor_x: 'left'
5\. anchor_y: 'top'
6\. ToolBox:
7\. id: _tool_box
8\. drawing_space: _drawing_space
9\. comic_creator: root
10\. size_hint: None,None
11\. width: 100
12\. AnchorLayout:
13\. anchor_x: 'right'
14\. anchor_y: 'top'
15\. DrawingSpace:
16\. id: _drawing_space
17\. status_bar: _status_bar
18\. general_options: _general_options
19\. tool_box: _tool_box
20\. size_hint: None,None
21\. width: root.width - _tool_box.width
22\. height: root.height - _general_options.height - _status_bar.height
23\. AnchorLayout:
24\. anchor_x: 'center'
25\. anchor_y: 'bottom'
26\. BoxLayout:
27\. orientation: 'vertical'
28\. GeneralOptions:
29\. id: _general_options
30\. drawing_space: _drawing_space
31\. comic_creator: root
32\. size_hint: 1,None
33\. height: 48
34\. StatusBar:
35\. id: _status_bar
36\. size_hint: 1,None
37\. height: 24
第 7、16、29 和 35 行中的 ID 已添加到 comiccreator.kv 中。根据之前的图表(Comic Creator 的内部引用),ID 用于在 8、17、18、19 和 30 行创建属性。
提示
属性和 ID 的名称不必不同。在前面的代码中,我们只是给 ID 添加了'_'来区分它们和属性。也就是说,_status_bar ID 仅在.kv文件中可访问,而status_bar属性则打算在 Python 代码中使用。它们可以具有相同的名称而不会引起任何冲突。
例如,第 8 行创建了属性drawing_space,它引用了DrawingSpace实例。这意味着ToolBox(第 6 行)实例现在可以访问DrawingSpace实例,以便在其上绘制图形。
我们经常想要访问的是规则层次结构中基本小部件(ComicCreator)的基类。第 9 行和第 31 行使用root完成引用,通过comic_creator属性来访问它。
注意
保留的root关键字是 Kivy 内部语言变量,它始终指向规则层次结构中的基小部件。其他两个重要关键字是self和app。关键字self指向当前小部件,而app指向应用程序的实例。
这些都是在漫画创作者项目中创建属性所需的所有更改。我们可以使用 Python comicreator.py正常运行项目,并将获得与第二章,图形 – 画布相同的结果。
我们使用属性创建了界面组件之间的链接。在接下来的章节中,我们将频繁使用创建的属性来访问界面的不同部分。
基本小部件事件 – 拖动 stickman
基本的Widget事件对应于屏幕上的触摸。然而,在 Kivy 中,触摸的概念比直观上可能想象的要广泛。它包括鼠标事件、手指触摸和魔法笔触摸。为了简化,我们将在本章中经常假设我们正在使用鼠标,但实际上如果我们使用触摸屏(以及手指或魔法笔)也不会有任何改变。以下三个基本的Widget事件:
-
on_touch_down:当一个新的触摸开始时,例如,点击鼠标按钮或触摸屏幕的动作。 -
on_touch_move:当触摸移动时,例如,拖动鼠标或手指在屏幕上滑动。 -
on_touch_up:当触摸结束时,例如,释放鼠标按钮或从屏幕上抬起手指。
注意到on_touch_down在每个on_touch_move之前发生,on_touch_up发生;项目符号列表的顺序反映了必要的执行顺序。最后,如果没有移动动作,则on_touch_move根本不会发生。这些事件使我们能够为我们的Stickman添加拖动功能,以便在添加后将其放置在想要的位置。我们按如下方式修改comicwidgets.kv的标题:
38\. # File name: comicwidgets.kv
39\. #:import comicwidgets comicwidgets
40\. <DraggableWidget>:
41\. size_hint: None, None
42.
43\. <StickMan>:
44\. size: 48,48
45\. ...
代码现在包括了一个名为DraggableWidget的新Widget的规则。第 41 行禁用了size_hint,这样我们就可以使用固定大小(例如,第 44 行)。size_hint: None, None指令已从StickMan中删除,因为它将在 Python 代码中继承自DraggableWidget。第 39 行的import指令负责导入相应的comicwidgets.py文件:
46\. # File name: comicwidgets.py
47\. from kivy.uix.relativelayout import RelativeLayout
48\. from kivy.graphics import Line
49.
50\. class DraggableWidget(RelativeLayout):
51\. def __init__(self, **kwargs):
52\. self.selected = None
53\. super(DraggableWidget, self).__init__(**kwargs)
comicwidgets.py文件包含了新的DraggableWidget类。这个类从RelativeLayout继承(第 50 行)。第 52 行的selected属性将指示DraggableWidget实例是否被选中。请注意,selected不是 Kivy 的一部分;它是我们作为DraggableWidget类的一部分创建的属性。
小贴士
Python 中的__init__构造函数是定义类对象属性的合适位置,只需使用self引用而不在类级别声明它们;这常常会让来自其他面向对象语言(如 C++或 Java)的程序员感到困惑。
在comicwidgets.py文件中,我们还需要重写与触摸事件相关的三个方法(on_touch_down、on_touch_move和on_touch_up)。这些方法中的每一个都接收MotionEvent作为参数(touch),它包含与事件相关的许多有用信息,例如触摸坐标、触摸类型、点击次数(或点击)、持续时间、输入设备等等,这些都可以用于高级任务(kivy.org/docs/api-kivy.input.motionevent.html#kivy.input.motionevent.MotionEvent)。
让我们从on_touch_down开始:
54\. def on_touch_down(self, touch):
55\. if self.collide_point(touch.x, touch.y):
56\. self.select()
57\. return True
58\. return super(DraggableWidget, self).on_touch_down(touch)
在第 55 行,我们使用了 Kivy 中最常见的策略来检测触摸是否在某个小部件之上:collide_point方法。它允许我们通过检查触摸的坐标来检测事件是否实际上发生在特定的DraggableWidget内部。
注意
每个活动的Widget都会接收到在应用(坐标空间)内部发生的所有触摸事件(MotionEvent),我们可以使用collide_point方法来检测事件是否发生在任何特定的Widget中。
这意味着程序员需要实现逻辑来区分特定Widget执行某些操作的可能性(在这种情况下,调用第 56 行的select方法)与事件,或者它只是通过调用基类方法(第 58 行)并因此执行默认行为。
处理事件的常见方式是使用collide_point,但也可以使用其他标准。Kivy 在这方面给了我们绝对的自由。第 55 行提供了检查事件是否发生在Widget内部的简单案例。如果事件的坐标实际上在Widget内部,我们将调用select()方法,这将设置图形为选中状态(细节将在本章后面解释)。
理解事件的返回值(第 57 行)以及调用基类方法的意义(第 58 行)非常重要。Kivy GUI 有一个层次结构,所以每个Widget实例都有一个对应的parent Widget(除非Widget实例是层次结构的根)。
触摸事件的返回值告诉parent我们是否处理了事件,通过分别返回True或False。因此,我们需要小心,因为我们完全控制着接收事件的部件。最后,我们还可以使用super(基类引用)的返回值来找出是否已经有子部件处理了事件。
通常,on_touch_down方法覆盖的行 54 到 58 的结构是处理基本事件的最常见方式:
-
确保事件发生在
Widget内部(第 55 行)。 -
执行必须完成的事情(第 56 行)。
-
返回
True表示事件已被处理(第 57 行)。 -
如果事件发生在
Widget外部,则我们将事件传播给子部件并返回结果(第 58 行)。
尽管这是最常见的方式,并且可能对初学者来说也是推荐的,但我们为了达到不同的目标可以偏离这种方式;我们很快会用其他示例来扩展这一点。首先,让我们回顾一下select方法:
59\. def select(self):
60\. if not self.selected:
61\. self.ix = self.center_x
62\. self.iy = self.center_y
63\. with self.canvas:
64\. self.selected = Line(rectangle=(0,0,self.width,self.height), dash_offset=2)
首先,我们需要确保之前没有选择任何内容(第 60 行),使用我们之前创建的select属性(第 52 行)。如果是这种情况,我们保存DraggableWidget的中心坐标(第 61 和 62 行),并在其边界上动态绘制一个矩形(第 63 和 64 行),如下面的截图所示:

第 63 行是基于 Python 的with语句的一个便利方法。它与在add方法中的调用等效,即self.canvas.add(Rectangle(…)),其优点是它允许我们同时添加多个指令。例如,我们可以用它来添加三个指令:
with self.canvas:
Color(rgb=(1,0,0))
Line(points=(0,0,5,5))
Rotate()
...
在第二章,“图形 – 画布”,我们使用 Kivy 语言向canvas添加形状。现在,我们直接使用 Python 代码,而不是 Kivy 语言的语法,尽管 Python 的with语句与其略有相似,并且在 Kivy API 中经常使用。注意,我们在第 64 行的selected属性中保留了Line实例,因为我们需要它来在部件不再被选中时移除矩形。此外,DraggableWidget实例将知道何时被选中,无论是包含引用还是为None。
该条件用于on_touch_move方法:
65\. def on_touch_move(self, touch):
66\. (x,y) = self.parent.to_parent(touch.x, touch.y)
67\. if self.selected and self.parent.collide_point(x - self.width/2, y - self.height/2):
68\. self.translate(touch.x-self.ix,touch.y-self.iy)
69\. return True
70\. return super(DraggableWidget, self).on_touch_move(touch)
在此事件中,我们控制 DraggableWidget 的拖动。在第 67 行,我们确保 DraggableWidget 被选中。在同一行,我们再次使用 collide_point,但这次我们使用 parent(绘图空间)而不是 self。这就是为什么上一行(第 66 行)将小部件坐标转换为相对于相应 parent 的 to_parent 方法中的值。换句话说,我们必须检查 parent(绘图空间),因为 Stickman 可以在整个 绘图空间 内拖动,而不仅仅是 DraggableWidget 本身。下一节将详细解释如何将坐标定位到屏幕的不同部分。
第 67 行的另一个细节是,我们通过从当前触摸(touch.x - self.width/2, touch.y - self.height/2)减去小部件宽度的一半和高度的一半来检查 DraggableWidget 未来位置的左角。这是为了确保我们不将形状拖出 绘图空间,因为我们将从中心拖动它。
如果条件为 True,我们调用 translate 方法:
71\. def translate(self, x, y):
72\. self.center_x = self.ix = self.ix + x
73\. self.center_y = self.iy = self.iy + y
该方法通过为新值分配 center_x 和 center_y 属性(第 72 行和第 73 行)来移动 DraggableWidget(x,y)像素。它还更新了我们在第 61 行和第 62 行之前的 select 方法中创建的 ix 和 iy 属性。
on_touch_move 方法的最后两行(第 69 行和第 70 行)遵循与 on_touch_down 方法(第 57 行和第 58 行)相同的方法,同样也遵循 on_touch_up 方法(第 77 行和第 78 行):
74\. def on_touch_up(self, touch):
75\. if self.selected:
76\. self.unselect()
77\. return True
78\. return super(DraggableWidget, self).on_touch_up(touch)
on_touch_up 事件撤销 on_touch_down 状态。首先,它检查是否使用我们的 selected 属性选中。如果是,那么它调用 unselected() 方法:
79\. def unselect(self):
80\. if self.selected:
81\. self.canvas.remove(self.selected)
82\. self.selected = None
此方法将动态调用 remove 方法来从 canvas(第 81 行)中移除 Line 顶点指令,并将我们的属性 selected 设置为 None(第 82 行),以表示小部件不再被拖动。注意我们添加 Line 顶点指令(第 63 行和第 64 行)和移除它的不同方式(第 81 行)。
在 comicwidgets.py 中还有两行代码:
83\. class StickMan(DraggableWidget):
84\. pass
这些行定义了我们的 StickMan,现在它从 DraggableWidget(第 83 行)继承,而不是从 RelativeLayout 继承。
在 drawingspace.kv 中还需要进行最后的更改,现在看起来如下所示:
85\. # File name: drawingspace.kv
86\. <DrawingSpace@RelativeLayout>:
87\. Canvas.before:
88\. Line:
89\. rectangle: 0, 0, self.width - 4,self.height - 4
90\. StickMan:
我们在 绘图空间 的 canvas.before 中添加了一个边框(第 87 行和第 88 行),这将为我们提供一个参考,以可视化画布的起始或结束位置。我们还保留了一个 StickMan 实例在 绘图空间 中。你可以运行应用程序(python comiccreator.py)并将 StickMan 拖动到 绘图空间 上。

在本节中,你学习了任何 Widget 的三个基本触摸事件。它们与坐标密切相关,因此有必要学习如何正确地操作坐标。我们在 on_touch_move 方法中介绍了这项技术,但它在下一节中将是主要内容,该节将探讨 Kivy 提供的定位坐标的可能方法。
本地化坐标 – 添加人物
在最后一节中,我们使用了 to_parent() 方法(第 66 行)将相对于 DrawingSpace 的坐标转换为它的父级。记住,我们当时在 DraggableWidget 内部,我们收到的坐标是相对于 parent(DrawingSpace)的。
这些坐标对 DraggableWidget 非常方便,因为我们将其定位在父级坐标中。此方法允许我们在父级的 collide_point 中使用坐标。当我们要检查父级的 parent 空间的坐标或需要直接在 Widget 的画布上绘制某些内容时,这就不再方便了。
在学习更多示例之前,让我们回顾一下理论。你了解到 RelativeLayout 非常有用,因为它在约束空间内思考定位我们的对象要简单得多。问题开始于我们需要将坐标转换为另一个 Widget 区域时。让我们考虑以下 Kivy 程序的截图:

生成此示例的代码在此处未显示,因为它非常直接。如果你想测试它,可以在文件夹 04 - Embedding RelativeLayouts/ 下找到代码,并使用 python main.py --size=150x75 运行它。它由三个相互嵌套的 RelativeLayouts 组成。蓝色(较深灰色)是 绿色(浅灰色)的父级,而 绿色 是 红色(中间灰色)的父级。a(在右上角)是一个位于 红色(中间灰色)RelativeLayout 内部位置 (5, 5) 的 Label 实例。蓝色布局(深灰色)是窗口的大小(150 x 75)。其余元素是指标(代码的一部分)以帮助你理解示例。
上一张截图包含一些测量值,有助于解释 Widget 类提供的四种本地化坐标的方法:
-
to_parent(): 此方法将RelativeLayout内部的相对坐标转换为RelativeLayout的父坐标。例如,red.to_parent(a.x, a.y)返回a相对于绿色(浅灰色)布局的坐标,即(50+5, 25+5) = (55, 30)。 -
to_local(): 此方法将RelativeLayout的parent坐标转换为RelativeLayout。例如,red.to_local(55,30)返回(5,5),这是a标签相对于红色布局(中间灰色)的坐标。 -
to_window():此方法将当前Widget的坐标转换为相对于窗口的绝对坐标。例如,a.to_window(a.x, a.y)返回a的绝对坐标,即(100 + 5, 50 + 5) = (105, 55)。 -
to_widget():此方法将绝对坐标转换为当前小部件的父级内的坐标。例如,a.to_widget(105,55)返回(5,5),再次是a相对于red(中间灰色)布局的坐标。
最后两个方法不使用red布局来转换坐标,因为在这种情况下,Kivy 假设坐标总是相对于父级。还有一个Boolean参数(称为relative),它控制坐标是否在Widget内部是相对的。
让我们在Comic Creator项目中研究一个真实示例。我们将向toolbox按钮添加事件,以便我们可以向drawing space添加图形。在这个过程中,我们将遇到一个场景,我们必须使用之前提到的方法来正确地将我们的坐标定位到Widget。
此代码对应于toolbox.py文件的标题:
91\. # File name: toolbox.py
92\. import kivy
93.
94\. import math
95\. from kivy.uix.togglebutton import ToggleButton
96\. from kivy.graphics import Line
97\. from comicwidgets import StickMan, DraggableWidget
98.
99\. class ToolButton(ToggleButton):
100\. def on_touch_down(self, touch):
101\. ds = self.parent.drawing_space
102\. if self.state == 'down' and ds.collide_point(touch.x, touch.y):
103\. (x,y) = ds.to_widget(touch.x, touch.y)
104\. self.draw(ds, x, y)
105\. return True
106\. return super(ToolButton, self).on_touch_down(touch)
107\.
108\. def draw(self, ds, x, y):
109\. pass
第 99 到 106 行的结构已经很熟悉了。第 102 行确保ToolButton处于'down'状态,并且事件发生在DrawingSpace实例(由ds引用)中。记住,ToolButton的父级是ToolBox,我们在本章开头在comiccreator.kv中添加了一个引用DrawingSpace实例的属性。
第 104 行调用了draw方法。它将根据派生类(ToolStickMan、ToolCircle和ToolLine)绘制相应的形状。我们需要确保向draw方法发送正确的坐标。因此,在调用它之前,我们需要使用to_widget事件(第 103 行)将接收到的绝对坐标(在ToolButton的on_touch_down中接收)转换为相对坐标(适用于drawing space)。
小贴士
我们知道我们接收到的坐标(touch.x和touch.y)是绝对的,因为ToolStickman不是RelativeLayout,而DrawingSpace(ds)是。
让我们继续研究toolbox.py文件,看看ToolStickMan实际上是如何添加StickMan的:
110\. class ToolStickman(ToolButton):
111\. def draw(self, ds, x, y):
112\. sm = StickMan(width=48, height=48)
113\. sm.center = (x,y)
114\. ds.add_widget(sm)
我们创建了一个Stickman实例(第 112 行),使用转换后的坐标(第 103 行)来居中Stickman,最后(第 119 行),使用add_widget方法(第 114 行)将其添加到DrawingSpace实例中。我们只需要更新toolbox.kv中的几行,以便运行带有新更改的项目:
115\. # File name: toolbox.kv
116\. #:import toolbox toolbox
117\.
118\. <ToolButton>:
119\. …
120\. <ToolBox@GridLayout>:
121\. …
122\. ToolStickman:
首先,我们需要导入toolbox.py(第 116 行),然后从ToolButton中移除@ToggleButton(第 118 行),因为我们已经在toolbox.py中添加了它,并且最后我们将最后一个ToolButton替换为我们的新ToolStickman小部件(第 122 行)。到这个时候,我们能够向drawing space添加stickmen,并且也可以将它们拖拽到上面。

现在我们已经涵盖了基础知识,让我们学习如何动态地绑定和解绑事件。
绑定和解绑事件 – 调整肢体和头部大小
在前两个部分中,我们覆盖了基本事件以执行我们想要的操作。在本节中,你将学习如何动态地绑定和解绑事件。添加我们的Stickman相当容易,因为它已经是一个Widget了,但图形、圆和矩形怎么办?我们可以为它们创建一些小部件,就像我们对Stickman所做的那样,但在那之前让我们尝试一些更勇敢的事情。不是仅仅点击在绘图空间上,而是拖动鼠标在其边界上以决定圆或线的尺寸:

使用鼠标设置大小
一旦我们完成拖动(并且我们对大小满意),让我们动态创建包含形状的DraggableWidget,这样我们也可以在DrawingSpace实例上拖动它们。以下类图将帮助我们理解toolbox.py文件的整个继承结构:

图表包括在上一节中解释的ToolButton和ToolsStickman,但它还包括三个新类,称为ToolFigure、ToolLine和ToolCircle。
ToolFigure类有六个方法。让我们先快速概述这些方法,然后突出显示重要和新颖的部分:
-
draw:此方法覆盖了ToolButton的draw方法(第 108 和 109 行)。我们触摸下的位置表示图形的起点,对于圆来说是中心,对于线来说是线的端点之一。123\. class ToolFigure(ToolButton): 124\. def draw(self, ds, x, y): 125\. (self.ix, self.iy) = (x,y) 126\. with ds.canvas: 127\. self.figure=self.create_figure(x,y,x+1,y+1) 128\. ds.bind(on_touch_move=self.update_figure) 129\. ds.bind(on_touch_up=self.end_figure) -
update_figure:此方法在拖动时更新图形的终点。要么是线的终点,要么是圆的半径(从起点到终点的距离)。130\. def update_figure(self, ds, touch): 131\. if ds.collide_point(touch.x, touch.y): 132\. (x,y) = ds.to_widget(touch.x, touch.y) 133\. ds.canvas.remove(self.figure) 134\. with ds.canvas: 135\. self.figure = self.create_figure(self.ix, self.iy,x,y) -
end_figure:此方法使用与update_figure中相同的逻辑指示图形的最终终点。我们还将最终图形放入DraggableWidget中(见widgetize)。136\. def end_figure(self, ds, touch): 137\. ds.unbind(on_touch_move=self.update_figure) 138\. ds.unbind(on_touch_up=self.end_figure) 139\. ds.canvas.remove(self.figure) 140\. (fx,fy) = ds.to_widget(touch.x, touch.y) 141\. self.widgetize(ds,self.ix,self.iy,fx,fy) -
widgetize:此方法创建DraggableWidget并将图形放入其中。它使用四个必须通过本地化方法正确本地化的坐标:142\. def widgetize(self,ds,ix,iy,fx,fy): 143\. widget = self.create_widget(ix,iy,fx,fy) 144\. (ix,iy) = widget.to_local(ix,iy,relative=True) 145\. (fx,fy) = widget.to_local(fx,fy,relative=True) 146\. widget.canvas.add( self.create_figure(ix,iy,fx,fy)) 147\. ds.add_widget(widget) -
create_figure:此方法将被ToolLine(第 153 和 154 行)和ToolCircle(第 162 至 163 行)覆盖。它根据四个坐标创建相应的图形:148\. def create_figure(self,ix,iy,fx,fy): 149\. pass -
create_widget:此方法也被ToolLine(第 156 至 159 行)和ToolCircle(第 165 至 169 行)覆盖。它根据四个坐标创建相应位置和大小的DraggableWidget。150\. def create_widget(self,ix,iy,fx,fy): 151\. pass
前面的方法中的大多数语句已经介绍过了。这段代码的新主题是事件的动态bind/unbind。我们需要解决的主要问题是,我们不想on_touch_move和on_touch_up事件始终处于活动状态。我们需要从用户开始绘制(调用draw方法的ToolButton的on_touch_down)的那一刻起激活它们,直到用户决定大小并执行触摸抬起。因此,当调用draw方法时,我们将update_figure和end_figure分别绑定到DrawingSpace的on_touch_move和on_touch_up事件(第 128 行和第 129 行)。此外,当用户在end_figure方法上结束图形时,我们将它们解绑(第 137 行和第 138 行)。请注意,我们可以从on_touch_up事件中解绑正在执行的方法(end_figure)。我们希望避免不必要地调用update_figure和end_figure方法。采用这种方法,它们只会在图形第一次绘制时被调用。
在这段代码中还有一些其他有趣的事情值得注意。在第 125 行,我们创建了两个类属性(self.ix和self.iy)来保存初始触摸的坐标。每次我们更新图形(第 135 行)和将图形放入Widget(第 141 行)时,我们都会使用这些坐标。
我们还使用了之前章节中介绍的一些本地化方法。在第 132 行和第 140 行,我们使用了to_widget将坐标转换到DrawingSpace实例。第 144 行和第 145 行使用to_local将坐标转换到DraggableWidget。
注意
DraggableWidget被指示使用参数relative=True将坐标转换到其内部相对空间,因为DraggableWidget是相对的,我们试图在其中绘制(不是在父级:绘图空间)。
在图形和控件的位置和尺寸计算中涉及一些基本的数学。我们有意将其移动到继承的更深层类中:ToolLine和ToolCircle。以下是它们的代码,toolbox.py的最后部分:
152\. class ToolLine(ToolFigure):
153\. def create_figure(self,ix,iy,fx,fy):
154\. return Line(points=[ix, iy, fx, fy])
155\.
156\. def create_widget(self,ix,iy,fx,fy):
157\. pos = (min(ix, fx), min(iy, fy))
158\. size = (abs(fx-ix), abs(fy-iy))
159\. return DraggableWidget(pos = pos, size = size)
160\.
161\. class ToolCircle(ToolFigure):
162\. def create_figure(self,ix,iy,fx,fy):
163\. return Line(circle=[ix,iy,math.hypot(ix-fx,iy-fy)])
164\.
165\. def create_widget(self,ix,iy,fx,fy):
166\. r = math.hypot(ix-fx, iy-fy)
167\. pos = (ix-r, iy-r)
168\. size = (2*r, 2*r)
169\. return DraggableWidget(pos = pos, size = size)
这里的数学涉及几何概念,超出了本书的范围。重要的是要理解,这个代码段的这些方法将计算适应为创建线或圆。最后,我们在toolbox.kv中的ToolBox类中做了一些更改:
170\. # File name: toolbox.kv
171\. ...
172\.
173\. <ToolBox@GridLayout>:
174\. cols: 2
175\. padding: 2
176\. tool_circle: _tool_circle
177\. tool_line: _tool_line
178\. tool_stickman: _tool_stickman
179\. ToolCircle:
180\. id: _tool_circle
181\. canvas:
182\. Line:
183\. circle: 24,24,14
184\. ToolLine:
185\. id: _tool_line
186\. canvas:
187\. Line:
188\. points: 10,10,38,38
189\. ToolStickman:
190\. id: _tool_stickman
191\. StickMan:
192\. pos_hint: {'center_x':.5,'center_y':.5}
新的类ToolCircle(第 179 行)、ToolLine(第 184 行)和ToolStickMan(第 189 行)已经替换了之前的ToolButton实例。现在,我们也可以将线和圆添加并缩放到绘图空间:

我们还创建了一些属性(第 176 行、第 177 行和第 178 行),这些属性在第四章中将会很有用,即改进用户体验,当我们使用手势创建图形时。
在 Kivy 语言中绑定事件
到目前为止,我们一直在两种方式下处理事件:重写事件方法(例如,on_touch_event)和将自定义方法绑定到相关事件方法(例如,ds.bind(on_touch_move=self.update_figure))。在本节中,我们将讨论另一种方式,即在 Kivy 语言中绑定事件。实际上,我们可以在本章开始与 DraggableWidget 一起工作时就做这件事,但这里有一个区别。如果我们使用 Kivy 语言,我们可以轻松地将事件添加到特定实例,而不是添加到同一类的所有实例。从这个意义上说,它类似于使用 bind 方法动态地将实例绑定到其回调。
我们将专注于 Button 和 ToggleButton 的特定新事件。以下是 generaloption.kv 的代码:
193\. # File name: generaloptions.kv
194\. #:import generaloptions generaloptions
195\. <GeneralOptions>:
196\. orientation: 'horizontal'
197\. padding: 2
198\. Button:
199\. text: 'Clear'
200\. on_press: root.clear(*args)
201\. Button:
202\. text: 'Remove'
203\. on_release: root.remove(*args)
204\. ToggleButton:
205\. text: 'Group'
206\. on_state: root.group(*args)
207\. Button:
208\. text: 'Color'
209\. on_press: root.color(*args)
210\. ToggleButton:
211\. text: 'Gestures'
212\. on_state: root.gestures(*args)
Button 类有两个额外的事件:on_press 和 on_release。前者类似于 on_touch_down,后者类似于 on_touch_up。然而,在这种情况下,我们不需要担心调用 collide_point 方法。我们使用 on_press 为 Clear Button(第 200 行)和 Color Button(第 209 行),以及 on_release 为 Remove Button(第 203 行)来展示这两种方法,但在这个特定情况下,选择哪一个实际上并不重要。on_state 事件已经是 Button 类的一部分,尽管它更常用于 ToggleButton 实例中。每当 ToogleButton 的状态从 'normal' 变为 'down' 或相反时,都会触发 on_state 事件。on_state 事件在第 206 行和第 212 行使用。所有事件都绑定到根目录中的方法,这些方法在 generaloptions.py 文件中定义:
213\. # File name: generaloptions.py
214\. from kivy.uix.boxlayout import BoxLayout
215\. from kivy.properties import NumericProperty, ListProperty
216.
217\. class GeneralOptions(BoxLayout):
218\. group_mode = False
219\. translation = ListProperty(None)
220.
221\. def clear(self, instance):
222\. self.drawing_space.clear_widgets()
223.
224\. def remove(self, instance):
225\. ds = self.drawing_space
226\. if len(ds.children) > 0:
227\. ds.remove_widget(ds.children[0])
228.
229\. def group(self, instance, value):
230\. if value == 'down':
231\. self.group_mode = True
232\. else:
233\. self.group_mode = False
234\. self.unselect_all()
235.
236\. def color(self, instance):
237\. pass
238.
239\. def gestures(self, instance, value):
240\. pass
241.
242\. def unselect_all(self):
243\. for child in self.drawing_space.children:
244\. child.unselect()
245.
246\. def on_translation(self,instance,value):
247\. for child in self.drawing_space.children:
248\. if child.selected:
249\. child.translate(*self.translation)
GeneralOptions 方法展示了 Widget 类的几种其他方法。clear 方法通过 clear_widgets 方法(第 222 行)从 DrawingSpace 实例中删除所有小部件。以下截图显示了点击它的结果:

remove_widget 方法通过访问 children 列表删除最后添加的 Widget 实例(第 227 行)。group 方法根据 'down' 或 'normal' 的 ToggleButton 状态修改第 218 行的 group_mode 属性。color 和 gestures 方法将在 第四章 改进用户体验 中完成。
group mode 将允许用户选择多个 DraggableWidget 实例,以便同时拖动它们。我们根据 ToggleButton 的状态激活或停用 group mode。在下一节中,我们将实际上允许在 DraggableWidget 类中进行多选和拖动。目前,我们只需使用 unselect_all 和 on_translation 方法准备好控件。
当 分组模式 被禁用时,我们通过调用 unselect_all 方法(第 242 行)确保所有选中的小部件都被取消选择。unselect_all 方法遍历子部件列表,调用每个 DraggableWidget 的内部方法 unselect(第 79 行)。
最后,on_translation 方法也会遍历子部件列表,调用每个 DraggableWidget 的内部 translate 方法(第 71 行)。问题是;什么调用了 on_translation 方法?Kivy 提供的最有用功能之一就是回答这个问题;这将在下一节中解释。
创建自己的事件 – 神奇的属性
本节介绍了 Kivy 属性的使用。每次我们修改 Kivy 属性时,它都会触发一个事件。属性类型多种多样,从简单的 NumericProperty 或 StringProperty 到更复杂的版本,如 ListProperty、DictProperty 或 ObjectProperty。例如,如果我们定义一个名为 text 的 StringProperty,那么每次文本被修改时,都会触发一个 on_text 事件。
注意
一旦我们定义了一个 Kivy 属性,Kivy 就会在内部创建与该属性相关联的事件。属性事件通过在属性名称前添加前缀 on_ 来引用。例如,on_translation 方法(第 246 行)与第 219 行的 translation ListProperty 相关联。
所有属性的工作方式相同。例如,我们在 ToogleButton 类中使用的 state 属性实际上是一个创建 on_state 事件的属性。我们已经在第 206 行使用了这个事件。我们定义属性,Kivy 就会为我们创建事件。
注意
在本书的上下文中,一个 属性 总是指 Kivy 属性,不应与 Python 属性混淆,Python 属性是一个不同的概念,本书没有涉及。属性用于描述属于类的变量(引用、对象和实例)。作为一个一般规则,Kivy 属性始终是一个属性,但属性不一定是 Kivy 属性。
在本节中,我们实现了 分组模式,该模式通过按下 分组 按钮(第 204 行)允许同时选择和拖动多个图形(DraggableWidgets 实例)。为了做到这一点,我们可以利用 translation 属性和 on_translation 方法之间的关系。基本上,每次我们修改 translation 属性时,都会触发 on_translation 事件。假设我们同时拖动三个图形(使用 分组模式),如下面的截图所示:

三个图形被选中,但事件由圆圈处理,因为它是唯一一个指针在顶部的图形。圆圈需要告诉线条和人物移动。它不需要调用 on_translation 方法,只需要修改 translation 属性,从而触发 on_translation 事件。让我们将这些更改包含在 comicwidgets.py 中。我们需要进行四个修改。
首先,我们需要在构造函数中添加 touched 属性(第 252 行),以指示哪个选中的图形接收事件(例如,上一张截图中的圆圈)。我们这样做:
250\. def __init__(self, **kwargs):
251\. self.selected = None
252\. self.touched = False
253\. super(DraggableWidget, self).__init__(**kwargs)
第二,当 DraggableWidget 中的一个实例接收事件时,我们需要将 touched 属性设置为 True(第 256 行)。我们在 on_touch_down 方法中这样做:
254\. def on_touch_down(self, touch):
255\. if self.collide_point(touch.x, touch.y):
256\. self.touched = True
257\. self.select()
258\. return True
259\. return super(DraggableWidget, self).on_touch_down(touch)
第三,我们需要检查 DraggableWidget 是否是当前被触摸的图形(之前接收了 on_touch_down 事件)。我们在第 262 行的条件中添加了这个检查。最重要的更改在第 264 行。我们不是直接调用 translate 方法,而是修改 general options(self.parent.general_options)的 translation 属性,将小部件移动到的像素数设置到属性中。这将触发 GeneralOptions 的 on_translation 方法,同时为每个选中的 DraggableWidget 调用 translate 方法。这是 on_touch_move 的结果代码:
260\. def on_touch_move(self, touch):
261\. (x,y) = self.parent.to_parent(touch.x, touch.y)
262\. if self.selected and self.touched and self.parent.collide_point(x - self.width/2, y -self.height/2):
263\. go = self.parent.general_options
264\. go.translation=(touch.x-self.ix,touch.y-self.iy)
265\. return True
266\. return super(DraggableWidget, self).on_touch_move(touch)
第四,我们需要在 on_touch_up 事件中将 touched 属性设置为 False(第 268 行),并且在使用 group_mode(第 270 行)时避免调用 unselect 方法。以下是 on_touch_up 方法的代码:
267\. def on_touch_up(self, touch):
268\. self.touched = False
269\. if self.selected:
270\. if not self.parent.general_options.group_mode:
271\. self.unselect()
272\. return super(DraggableWidget, self).on_touch_up(touch)
这个例子可以被认为是人为的,因为我们理论上可以从一开始就调用 on_translation 方法。然而,属性对于保持变量的内部状态和屏幕显示的一致性至关重要。下一节的示例将提高你对这一点的理解。
Kivy 及其属性
尽管我们在上一节中只简要介绍了属性的解释,但事实是我们从本章开始就已经在使用它们了。Kivy 的内部充满了属性。它们几乎无处不在。例如,当我们实现 DraggableWidget 时,我们只是修改了 center_x 属性(第 72 行和第 73 行),然后整个 Widget 就会保持更新,因为 center_x 的使用涉及到一系列的属性。
本章的最后一个示例说明了 Kivy 属性是多么强大。以下是 statusbar.py 的代码:
273\. # File name: statusbar.py
274\. from kivy.uix.boxlayout import BoxLayout
275\. from kivy.properties import NumericProperty, ObjectProperty
276.
277\. class StatusBar(BoxLayout):
278\. counter = NumericProperty(0)
279\. previous_counter = 0
280.
281\. def on_counter(self, instance, value):
282\. if value == 0:
283\. self.msg_label.text="Drawing space cleared"
284\. elif value - 1 == self.__class__.previous_counter:
285\. self.msg_label.text = "Widget added"
286\. elif value + 1 == StatusBar.previous_counter:
287\. self.msg_label.text = "Widget removed"
288\. self.__class__.previous_counter = value
Kivy 属性的工作方式可能会让一些高级的 Python 或 Java 程序员感到困惑。困惑发生在程序员假设 counter(第 278 行)是 StatusBar 类的静态属性,因为 counter 的定义方式与 Python 的静态属性类似(例如,第 279 行的 previous_counter)。这种假设是不正确的。
注意
Kivy 属性被声明为静态属性类(因为它们属于类),但它们总是内部转换为属性实例。实际上,它们属于对象,就像我们在构造函数中声明的那样。
我们需要区分类的静态属性和类的实例属性。在 Python 中,previous_counter(第 279 行)是StatusBar类的静态属性。这意味着它是所有StatusBar实例共享的,并且可以通过第 284 行和第 286 行所示的方式之一访问(然而,建议使用第 284 行,因为它与类名无关)。相比之下,selected变量(第 251 行)是DraggableWidget实例的属性。这意味着每个StatusBar对象都有一个selected变量。它们之间不共享。它们在构造函数(__init__)被调用之前创建。访问它的唯一方法是obj.selected(第 251 行)。counter属性(第 278 行)在行为上更类似于selected属性,而不是previous_counter静态属性,因为在每个实例中都有一个counter属性和一个selected属性。
既然已经澄清了这一点,我们就可以继续研究示例。counter在第 278 行定义为NumericProperty。它对应于on_counter方法(第 281 行)并修改了在statusbar.kv文件中定义的Label(msg_text)。
289\. # File name: statusbar.kv
290\. #:import statusbar statusbar
291\. <StatusBar>:
292\. msg_text: _msg_label
293\. orientation: 'horizontal'
294\. Label:
295\. text: 'Total Figures: ' + str(root.counter)
296\. Label:
297\. id: _msg_label
298\. text: "Kivy started"
注意,我们再次使用id(第 297 行)来定义msg_text(第 292 行)。我们还使用第 278 行定义的counter来更新第 295 行的**总图数**信息。text的具体部分(str(root.counter))在counter修改时自动更新。
因此,我们只需修改counter属性,界面就会自动更新。让我们在drawingspace.py中更新计数器:
299\. # File name: drawingspace.py
300\. from kivy.uix.relativelayout import RelativeLayout
301\.
302\. class DrawingSpace(RelativeLayout):
303\. def on_children(self, instance, value):
304\. self.status_bar.counter = len(self.children)
在on_children方法中,我们将counter更新为DrawingSpace的children长度。然后,每当我们在DrawingSpace的children列表中添加(第 114 行或第 147 行)或删除(第 222 行或第 227 行)小部件时,都会调用on_children,因为children也是一个 Kivy 属性。
不要忘记将此文件导入drawingspace.py文件中的drawingspace.kv文件,其中我们还在*绘图空间*中移除了边框:
305\. # File name: drawingspace.kv
306\. #:import drawingspace drawingspace
307\. <DrawingSpace@RelativeLayout>:
下图显示了与children属性相关联的元素链(属性、方法和小部件):

重要的是再次比较我们获取counter属性和msg_label属性的方式。我们在StatusBar(第 278 行)中定义了counter属性,并通过root(第 295 行)在Label中使用它。在msg_label的情况下,我们首先定义了id(第 297 行),然后是 Kivy 语言的属性(第 292 行)。然后,我们能够在 Python 代码中使用 msg_label(第 283 行、第 285 行和第 287 行)。
注意
记住,一个属性不一定是一个 Kivy 属性。属性是类的一个元素,而 Kivy 属性还把属性与事件关联起来。
你可以在 Kivy API 中找到完整的属性列表(kivy.org/docs/api-kivy.properties.html)。至少应该提到两个特定的属性:BoundedNumericProperty和AliasProperty。BoundedNumericProperty属性允许设置最大和最小值。如果值超出范围,将抛出异常。AliasProperty属性提供了一种扩展属性的方法;它允许我们在必要的属性不存在的情况下创建自己的属性。
最后需要注意的一点是,当我们使用 Kivy 语言创建顶点指令属性时,这些属性被用作属性。例如,如果我们更改ToolLine内线的位置,它将自动更新。然而,这仅适用于 Kivy 语言内部,不适用于我们动态添加顶点指令的情况,就像我们在toolbox.py中所做的那样。在我们的情况下,每次我们需要更新图形时(第 133 到 135 行),我们必须删除并创建一个新的顶点指令。然而,我们可以创建自己的属性来处理更新。一个例子将在第六章中提供,即当我们向视频中添加字幕时。
让我们再次运行代码,以查看带有状态栏计数图形并指示我们最后操作的最后结果:

摘要
我们在本章中涵盖了与事件处理相关的大部分主题。你学习了如何覆盖不同类型的事件、动态绑定和解绑、在 Kivy 语言中分配事件以及创建自己的事件。你还学习了 Kivy 属性,如何管理坐标在不同小部件中的定位,以及与添加、删除和更新Kivy Widget和canvas对象相关的许多方法。以下是所涵盖的事件、方法、属性和属性:
-
我们所涵盖的事件是
on_touch_up、on_touch_move和on_touch_down(Widget的);on_press和on_release(Button的);以及on_state(ToggleButton的)。 -
我们在本章中讨论的属性包括
MotionEvent(touch)的 x 和 y 坐标;Widget的center_x、center_y、canvas、parent和children,以及ToggleButton的state。 -
Widget的以下方法:-
bind和unbind用于动态附加事件 -
collide_points、to_parent、to_local、to_window和to_widget用于处理坐标 -
add_widget、remove_widget和clear_widgets用于动态修改子小部件 -
canvas的add和remove方法用于动态添加和删除顶点和上下文指令
-
-
Kivy 属性:
NumericProperty和ListProperty
与时钟和键盘相关的重要事件类型有两种。本章主要关注小部件和属性事件,但我们将看到如何在第五章 入侵者复仇 - 一个交互式多点触控游戏 中使用其他事件。下一章将介绍一系列关于 Kivy 的有趣话题,以提高我们 漫画创作者 的用户体验。
第四章. 提升用户体验
本章概述了 Kivy 提供的实用组件,这些组件可以帮助程序员在提升用户体验时更加轻松。本章中回顾的一些 Kivy 组件与具有非常特定功能的控件(例如,调色板)相关;在这种情况下,你将学习控制它们的基本技巧。其他控件将帮助我们扩展画布的使用,例如,改变颜色、旋转和缩放形状,或处理手势。最后,我们将通过一些小技巧快速提升应用程序的外观和感觉。所有章节都旨在提高应用程序的可用性,并且是独立的。以下是本章我们将回顾的主题列表:
-
在不同的屏幕间切换
-
使用 Kivy 调色板控件选择颜色
-
控制画布的可见区域
-
使用多指手势进行旋转和缩放
-
创建单指手势在屏幕上绘制
-
通过一些全局变化增强设计
更重要的是,我们将讨论如何将这些主题融入当前的工作项目中。这将加强你之前获得的知识,并探索一个新的编程场景,在这个场景中我们需要向现有代码中添加功能。在本章结束时,你应该能够舒适地探索 Kivy API 提供的所有不同控件,并快速理解如何将它们集成到你的代码中。
ScreenManager – 为图形选择颜色
ScreenManager类允许我们在同一个窗口中处理不同的屏幕。在 Kivy 中,屏幕比窗口更受欢迎,因为我们正在为具有不同屏幕尺寸的不同设备编程。因此,要正确适应所有设备的窗口是困难的(如果不是不可能的)。只需想象一下,用你的手指在手机上玩弄窗口。
到目前为止,我们所有的图形都是同一种颜色。让我们允许用户添加一些颜色,使“漫画创作者”更加灵活。Kivy 为我们提供了一个名为ColorPicker的Widget,如下面的截图所示:

如您所见,这个Widget需要很大的空间,所以在我们的当前界面中很难容纳。
小贴士
Kivy 1.9.0 版本中存在一个 bug,阻止了ColorPicker在 Python 3 中工作(它已在开发版本 1.9.1-dev 中修复,可在github.com/kivy/kivy/找到)。你可以使用 Python 2,或者可以从 Packt Publishing 网站下载的代码中找到 Python 3 的替代代码。代替ColorPicker,有一个基于GridLayout的控件用于选择一些颜色。本节中讨论的概念也反映在那段代码中。
ScreenManager 类允许我们拥有多个屏幕,而不仅仅是单个 Widget(ComicCreator),并且还允许我们轻松地在屏幕之间切换。以下是一个新的 Kivy 文件(comicscreenmanager.kv),其中包含 ComicScreenManager 类的定义:
1\. # File name: comicscreenmanager.kv
2\. #:import FadeTransition kivy.uix.screenmanager.FadeTransition
3\. <ComicScreenManager>:
4\. transition: FadeTransition()
5\. color_picker: _color_picker
6\. ComicCreator:
7\. Screen:
8\. name: 'colorscreen'
9\. ColorPicker:
10\. id: _color_picker
11\. color: 0,.3,.6,1
12\. Button:
13\. text: "Select"
14\. pos_hint: {'center_x': .75, 'y': .05}
15\. size_hint: None, None
16\. size: 150, 50
17\. on_press: root.current = 'comicscreen'
我们将 ColorPicker 实例嵌入到一个 Screen 小部件中(第 7 行),而不是直接添加到 ComicScreenManager。
备注
ScreenManager 实例必须包含 Screen 基类的小部件。不允许其他类型的 Widget(标签、按钮或布局)。
由于我们还将我们的 ComicCreator 添加到了 ScreenManager(第 6 行),我们需要确保在 comiccreator.kv 文件中,我们的 ComicCreator 继承自 Screen 类,因此我们需要更改文件头:
18\. # File name: comiccreator.kv
19\. <ComicCreator@Screen>:
20\. name: 'comicscreen'
21\. AnchorLayout:…
name 属性(第 20 行)用于通过 ID 识别屏幕,在这种情况下是 comicscreen,并且它通过其 current 属性在 ScreenManeger 的屏幕之间切换。例如,我们添加到 ColorPicker(第 12 行)的 Button 实例使用 name 属性在 on_press 事件(第 17 行)中更改 current 屏幕。root 指的是 ScreenManager 类,而 current 属性告诉它当前活动的 Screen 是什么。在这种情况下是 comicscreen,我们用来识别 ComicCreator 实例的名称。请注意,我们直接添加了 Python 代码(第 17 行),而不是像我们在 第三章 中所做的那样调用方法,小部件事件 – 绑定动作。
我们还给了包含 ColorPicker 实例的屏幕一个名称(colorscreen)。我们将使用此名称在 常规选项 区域中使用 颜色按钮激活 ColorPicker。我们需要修改 generaloptions.py 中的 color 方法:
22\. def color(self, instance):
23\. self.comic_creator.manager.current = 'colorscreen'
颜色按钮现在切换屏幕以显示 ColorPicker 实例。注意我们访问 ScreenManager 的方式(第 23 行)。首先,我们使用 GeneralOptions 类中的 comic_creator 引用来访问 ComicCreator 实例。其次,我们使用 Screen 的 manager 属性来访问其相应的 ScreenManager。最后,我们更改 current Screen,类似于第 17 行。
ComicScreenManager 现在成为 ComicCreator 项目的主体 Widget,因此 comicreator.py 文件必须相应更改:
24\. # File name: comiccreator.py
25\. from kivy.app import App
26\. from kivy.lang import Builder
27\. from kivy.uix.screenmanager import ScreenManager
28\.
29\. Builder.load_file('toolbox.kv')
30\. Builder.load_file('comicwidgets.kv')
31\. Builder.load_file('drawingspace.kv')
32\. Builder.load_file('generaloptions.kv')
33\. Builder.load_file('statusbar.kv')
34\. Builder.load_file('comiccreator.kv')
35\.
36\. class ComicScreenManager(ScreenManager):
37\. pass
38\.
39\. class ComicScreenManagerApp(App):
40\. def build(self):
41\. return ComicScreenManager()
42\.
43\. if __name__=="__main__":
44\. ComicScreenManagerApp().run()
由于我们将应用程序的名称更改为 ComicScreenManagerApp(第 44 行),我们明确加载了 comiccreator.kv 文件(第 34 行)。请记住,由于应用程序的名称现在是 ComicScreenManagerApp,comicscreenmanager.kv 文件将被自动调用。
关于 ScreenManager 的最后一个有趣的事情是我们可以使用 过渡。例如,第 2 行和第 4 行导入并使用了一个简单的 FadeTransition。
备注
Kivy 提供了一套过渡效果(FadeTransition、SwapTransition、SlideTransition和WipeTransition)来在ScreenManager的Screen实例之间切换。有关如何使用不同参数自定义它们的更多信息,请查看 Kivy API:kivy.org/docs/api-kivy.uix.screenmanager.html
在这些更改之后,我们可以通过点击通用选项中的Color按钮或ColorPicker的Select按钮在两个屏幕ColorPicker和ComicCreator之间切换。我们还使用color属性(第 11 行)在ColorPicker实例中设置不同的颜色;然而,颜色的选择对绘图过程仍然没有影响。下一节将介绍如何将选定的颜色设置为我们所绘制的图形。
画布上的颜色控制 – 给图形上色
上一节主要关注从画布中选择颜色,但这个选择实际上还没有产生效果。在本节中,我们将实际使用选定的颜色。如果我们不小心,分配颜色可能会变得复杂。如果你还记得,在第三章,图形 – 画布中,我们使用PushMatrix和PopMatrix来解决类似问题,但它们只适用于变换指令(Translate、Rotate和Scale),因为它们与坐标空间相关(这也解释了指令名称中的矩阵部分:PushMatrix和PopMatrix)。
让我们通过研究一个小例子(来自Comic Creator项目)来更好地理解这个概念:
45\. # File name: color.py
46\. from kivy.app import App
47\. from kivy.uix.gridlayout import GridLayout
48\. from kivy.lang import Builder
49\.
50\. Builder.load_string("""
51\. <GridLayout>:
52\. cols:2
53\. Label:
54\. color: 0.5,0.5,0.5,1
55\. canvas:
56\. Rectangle:
57\. pos: self.x + 10, self.y + 10
58\. size: self.width - 20, self.height - 20
59\. Widget:
60\. canvas:
61\. Rectangle:
62\. pos: self.x + 10, self.y + 10
63\. size: self.width - 20, self.height - 20
64\. """)
65\.
66\. class LabelApp(App):
67\. def build(self):
68\. return GridLayout()
69\.
70\. if __name__=="__main__":
71\. LabelApp().run()
注意
注意到我们使用Builder类的load_string方法而不是使用load_file方法。这个方法允许我们在 Python 代码文件中嵌入 Kivy 语言语句。
Label的一个属性称为color;它改变Label文本的颜色。我们在第一个Label中将color改为灰色(第 54 行),但它并没有清除上下文。请观察以下截图中的结果:

Label的矩形(第 56 行),以及Widget的矩形(第 61 行)都改变了颜色。Kivy 试图尽可能简化所有组件,以避免不必要的指令。我们将遵循这种方法来处理颜色,因此我们不必担心颜色,直到我们需要使用它。其他任何组件都可以自己处理自己的颜色。
现在,我们可以实现Comic Creator中的更改。只有三个方法在绘图空间中绘制(它们都在toolbox.py文件中)。以下是这些方法,其中对应的新行被突出显示:
-
ToolStickman类中的draw方法:72\. def draw(self, ds, x, y): 73\. sm = StickMan(width=48, height=48) 74\. sm.center = (x,y) 75\. screen_manager = self.parent.comic_creator.manager 76\. color_picker = screen_manager.color_picker 77\. sm.canvas.before.add(Color(*color_picker.color)) 78\. ds.add_widget(sm) -
ToolFigure类中的draw方法:79\. def draw(self, ds, x, y): 80\. (self.ix, self.iy) = (x,y) 81\. screen_manager = self.parent.comic_creator.manager 82\. color_picker = screen_manager.color_picker 83\. with ds.canvas: 84\. Color(*color_picker.color) 85\. self.figure=self.create_figure(x,y,x+1,y+1) 86\. ds.bind(on_touch_move=self.update_figure) 87\. ds.bind(on_touch_up=self.end_figure) -
ToolFigure类中的widgetize方法:88\. def widgetize(self,ds,ix,iy,fx,fy): 89\. widget = self.create_widget(ix,iy,fx,fy) 90\. (ix,iy) = widget.to_local(ix,iy,relative=True) 91\. (fx,fy) = widget.to_local(fx,fy,relative=True) 92\. screen_manager = self.parent.comic_creator.manager 93\. color_picker = screen_manager.color_picker 94\. widget.canvas.add(Color(*color_picker.color)) 95\. widget.canvas.add(self.create_figure(ix,iy,fx,fy)) 96\. ds.add_widget(widget)
所有三种方法都有一个共同的特定指令对;你可以在第 75 和 76 行、第 81 和 82 行、第 92 和 93 行找到它们。这些是获取访问ColorPicker实例的参考链。之后,我们只需在画布上添加一个Color指令(正如我们在第二章,图形 – 画布中学到的),使用color_picker中选择的color(第 77、84 和 94 行)。
小贴士
第 77、84 和 94 行上的“splat”运算符(*)在 Python 中用于解包参数列表。在这种情况下,Color构造函数旨在接收三个参数,即红色、绿色和蓝色值,但我们有一个存储在color_picker.color中的列表,例如(1,0,1),因此我们需要解包它以获取三个分离的值1,0,1。
我们还在ToolStickman类的draw方法中使用了canvas.before(第 77 行)。这是用来确保在Stickman的canvas(comicwidgets.kv文件)中添加的指令之前执行Color指令。在其他两种方法中这不是必要的,因为我们完全控制了那些方法内部的画布顺序。
最后,我们必须在文件from kivy.graphics import Line, Color的头部导入Color类。现在我们可以休息一下,享受我们用漫画创作者辛勤工作的成果:

在稍后的某个时间点,我们可以讨论我们的绘图是否只是一个狂热的漫画创作者粉丝,或者是一个穿着超大 T 恤的自恋外星人。现在,学习如何将绘图空间限制在占据窗口的特定区域似乎更有用。
StencilView – 限制绘图空间
在第三章,小部件事件 – 绑定动作中,我们通过使用简单的数学和collide_points来避免在绘图空间外绘制。这远非完美(例如,在组模式或我们调整大小时会失败),而且很繁琐且容易出错。
这对于第一个例子已经足够了,然而,StencilView在这里是一个更简单的方法。StencilView将绘制区域限制在它自己占据的空间内。任何在该区域之外的绘制都将被隐藏。首先,让我们修改drawingspace.py文件,添加以下头部:
97\. # File name: drawingspace.py
98\. from kivy.uix.stencilview import StencilView
99\.
100\. class DrawingSpace(StencilView):
101\. ...
The DrawingSpace实例现在从StencilView继承,而不是RelativeLayout。StencilView类不使用相对坐标(如RelativeLayout类所做的那样),但我们希望保留绘制空间中的相对坐标,因为它们对绘制很有用。为了做到这一点,我们可以修改右上角的AnchorLayout,使DrawingSpace实例位于一个RelativeLayout实例内部。我们在comiccreator.kv文件中这样做:
102\. AnchorLayout:
103\. anchor_x: 'right'
104\. anchor_y: 'top'
105\. RelativeLayout:
106\. size_hint: None,None
107\. width: root.width - _tool_box.width
108\. height: root.height - _general_options.height - _status_bar.height
109\. DrawingSpace:
110\. id: _drawing_space
111\. general_options: _general_options
112\. tool_box: _tool_box
113\. status_bar: _status_bar
当我们将DrawingSpace实例(第 109 行)嵌入到相同大小的RelativeLayout实例(第 105 行)中时(默认情况下,DrawingSpace实例使用size_hint: 1, 1占据RelativeLayout父实例的所有区域),那么DrawingSpace实例内部的坐标相对于RelativeLayout实例。由于它们大小相同,因此坐标也相对于DrawingSpace实例。
我们保留了DrawingSpace ID(第 110 行)和属性(第 111 至 113 行)。由于我们有一个新的缩进级别,并且DrawingSpace类本身不是相对的,这影响了我们在ToolBox实例中定位坐标的方式,具体来说,是在ToolButton类的on_touch_down和ToolFigure类的update_figure和end_figure中。以下是ToolButton类on_touch_down的新代码:
114\. def on_touch_down(self, touch):
115\. ds = self.parent.drawing_space
116\. if self.state == 'down' and\ ds.parent.collide_point(touch.x, touch.y):
117\. (x,y) = ds.to_widget(touch.x, touch.y)
118\. self.draw(ds, x, y)
119\. return True
120\. return super(ToolButton, self).on_touch_down(touch)
由于我们位于ToolButton内部,它不属于任何RelativeLayout实例,所以我们在这个方法中接收绝对坐标。绘制空间也接收绝对坐标,但它将在嵌入的RelativeLayout实例的上下文中解释它们。对于DrawingSpace实例的正确做法是询问它的RelativeLayout父实例,该实例将正确地碰撞(第 116 行)坐标(在ToolButton中接收到的)
以下是ToolFigure类update_figure和end_figure的新代码:
121\. def update_figure(self, ds, touch):
122\. ds.canvas.remove(self.figure)
123\. with ds.canvas:
124\. self.figure = self.create_figure(self.ix, self.iy,touch.x,touch.y)
125\.
126\. def end_figure(self, ds, touch):
127\. ds.unbind(on_touch_move=self.update_figure)
128\. ds.unbind(on_touch_up=self.end_figure)
129\. ds.canvas.remove(self.figure)
130\. self.widgetize(ds,self.ix,self.iy,touch.x,touch.y)
我们删除了一些指令,因为我们不再需要它们。首先,我们不再需要在两个方法中的任何一个中使用to_widget方法,因为我们已经从RelativeLayout父实例中获取了坐标。其次,我们不需要担心在update_figure方法中应用collide_point方法,因为StencilView将负责它;任何在边界之外的绘制都将被丢弃。
只需进行少量更改,我们就确保了不会在绘制空间之外绘制任何内容,并且有了这个保证,我们现在可以讨论如何拖动、旋转和缩放图形。
散点图 – 多点触摸以拖动、旋转和缩放
在上一章(第三章, 小部件事件 – 绑定动作),你学习了如何使用事件来拖动小部件。你学习了如何使用 on_touch_up、on_touch_move 和 on_touch_down 事件。然而,Scatter 类已经提供了该功能,并允许我们使用两个手指进行缩放和旋转,就像在移动或平板屏幕上一样。所有功能都包含在 Scatter 类中;然而,我们需要进行一些更改以保持我们的项目一致性。特别是,我们仍然希望我们的 组模式 能够工作,以便同时进行平移、缩放和旋转。让我们在 comicwidgets.py 文件中分四步实现这些更改:
-
替换
DraggableWidget基类。让我们使用Scatter而不是RelativeLayout(第132行和135行):131\. # File name: comicwidgets.py 132\. from kivy.uix.scatter import Scatter 133\. from kivy.graphics import Line 134\. 135\. class DraggableWidget(Scatter):注意
Scatter和RelativeLayout都使用相对坐标。 -
确保通过调用
super方法(第140行)在return True(第141行)之前将DraggableWidget的on_touch_down事件发送到基类(Scatter)。如果不这样做,Scatter基类将永远不会收到on_touch_down事件,什么也不会发生:136\. def on_touch_down(self, touch): 137\. if self.collide_point(touch.x, touch.y): 138\. self.touched = True 139\. self.select() 140\. super(DraggableWidget, self).on_touch_down(touch) 141\. return True 142\. return super(DraggableWidget, self).on_touch_down(touch)小贴士
super方法对基类(Scatter)很有用,而return语句对父类(DrawingSpace)很有用 -
删除
on_touch_move方法并添加一个on_pos方法,当pos属性被修改时调用。由于Scatter将负责拖动,我们不再需要on_touch_move。相反,我们将使用Scatter修改的pos属性。记住,属性会触发一个事件,该事件将调用on_pos方法:143\. def on_pos(self, instance, value): 144\. if self.selected and self.touched: 145\. go = self.parent.general_options 146\. go.translation = (self.center_x- self.ix, self.center_y - self.iy) 147\. self.ix = self.center_x 148\. self.iy = self.center_y -
Scatter有两个其他属性:rotation和scale。我们可以使用与pos和on_pos相同的想法,并添加on_rotation和on_scale方法:149\. def on_rotation(self, instance, value): 150\. if self.selected and self.touched: 151\. go = self.parent.general_options 152\. go.rotation = value 153\. 154\. def on_scale(self, instance, value): 155\. if self.selected and self.touched: 156\. go = self.parent.general_options 157\. go.scale = value
on_rotation 和 on_scale 方法修改了几个新属性(第 152 行和 157 行),我们需要将这些属性添加到 GeneralOptions 类中。这将帮助我们保持组模式的工作。以下代码是 generaloptions.py 的新头文件,其中包含新属性:
158\. # File name: generaloptions.py
159\. from kivy.uix.boxlayout import BoxLayout
160\. from kivy.properties import NumericProperty, ListProperty
161\.
162\. class GeneralOptions(BoxLayout):
163\. group_mode = False
164\. translation = ListProperty(None)
165\. rotation = NumericProperty(0)
166\. scale = NumericProperty(0)
我们导入 NumericProperty 和 ListProperty(第 160 行);并创建两个缺失的属性:rotation 和 scale(第 165 行和 166 行)。我们还需要添加 on_rotation(第 167 行)和 on_scale(第 172 行)方法(与 rotation 和 scale 属性相关联),这将确保所有 selected 组件通过遍历添加到 绘图空间(第 173 行和 177 行)的子组件列表一次旋转或缩放:
167 def on_rotation(self, instance, value):
168\. for child in self.drawing_space.children:
169\. if child.selected and not child.touched:
170\. child.rotation = value
171\.
172\. def on_scale(self, instance, value):
173\. for child in self.drawing_space.children:
174\. if child.selected and not child.touched:
175\. child.scale = value
需要进行最后的修改。我们需要将on_translation方法修改为检查循环中的当前child是否不是被触摸的那个(如果发生这种情况,请报警!),因为这样可能会引起无限递归,因为我们修改了最初调用此事件的属性。以下是generaloptions.py文件中的新on_translation方法:
176\. def on_translation(self,instance,value):
177\. for child in self.drawing_space.children:
178\. if child.selected and not child.touched:
179\. child.translate(*self.translation)
到目前为止,我们能够用手指来平移、旋转或缩放图形,甚至在分组模式下也是如此。
注意
Kivy 提供了一种使用鼠标模拟多点触控的方法。虽然有限,但你仍然可以用你的单点鼠标笔记本电脑测试这一部分。你所要做的就是右击你想要旋转的图形。屏幕上会出现一个半透明的红色圆圈。然后,你可以使用正常的左键拖动,就像它是第二个手指一样来旋转或缩放。要清除模拟的多点触控,你只需左击红色图标。
下一个截图展示了我们的StickMan在他旁边的线条同时旋转和缩放的情况。右侧的小StickMan只是一个用来比较原始大小的参考。模拟的多点触控手势被应用于右侧的线条上,这就是为什么你可以看到一个红色的(在打印版本中为灰色)点:

在第一章 GUI 基础 – 构建界面 中,我们简要提到了ScatterLayout,但现在ScatterLayout和Scatter之间的区别可能已经清晰。
注意
ScatterLayout是一个继承自Scatter并包含FloatLayout的 Kivy 布局。这允许你在其中添加小部件时使用size_hint和pos_hint属性。ScatterLayout也使用相对坐标。这并不意味着你无法在简单的Scatter中添加其他小部件;它只是意味着Scatter不遵守size_hint或pos_hint。
使用Scatter,我们能够拖动、旋转和缩放我们的图形。这是对我们漫画创作器功能性的巨大改进。现在让我们进一步增强与用户的交互,学习如何创建我们自己的手势,并在我们的项目中使用它们。
记录手势 – 线、圆和十字
用一个手指画画怎么样?我们能识别手势吗?使用 Kivy 是可以做到的。首先,我们需要记录我们想要使用的手势。手势表示为包含屏幕上笔划点的长字符串。以下代码使用 Kivy 的Gesture和GestureDatabase类来记录手势笔划。它可以与 Python gesturerecorder.py一起运行:
180\. # File Name: gesturerecorder.py
181\. from kivy.app import App
182\. from kivy.uix.floatlayout import FloatLayout
183\. from kivy.graphics import Line, Ellipse
184\. from kivy.gesture import Gesture, GestureDatabase
185\.
186\. class GestureRecorder(FloatLayout):
187\.
188\. def on_touch_down(self, touch):
189\. self.points = [touch.pos]
190\. with self.canvas:
191\. Ellipse(pos=(touch.x-5,touch.y-5),size=(10,10))
192\. self.Line = Line(points=(touch.x, touch.y))
193\.
194\. def on_touch_move(self, touch):
195\. self.points += [touch.pos]
196\. self.line.points += [touch.x, touch.y]
197\.
198\. def on_touch_up(self, touch):
199\. self.points += [touch.pos]
200\. gesture = Gesture()
201\. gesture.add_stroke(self.points)
202\. gesture.normalize()
203\. gdb = GestureDatabase()
204\. print ("Gesture:", gdb.gesture_to_str(gesture).decode(encoding='UTF-8'))
205\.
206\. class GestureRecorderApp(App):
207\. def build(self):
208\. return GestureRecorder()
209\.
210\. if __name__=="__main__":
211\. GestureRecorderApp().run()
上一段代码使用Gesture和GestureDatabase类(第 184 行)打印了手势字符串表示。on_touch_down、on_touch_move和on_touch_up方法收集笔触线条的points(第 189 行、第 195 行和第 199 行)。以下截图是使用gesturerecorded.py收集的笔触示例:

前面的图形(第 190 行和第 191 行)中的小圆圈表示起点,线条表示笔触的路径。最相关的部分在 200 到 204 行中编码。我们在第 200 行创建Gesture,使用add_stroke方法(第 201 行)为笔触添加points,normalize到默认的点数(第 202 行),并在第 203 行创建一个GestureDatabase实例,我们在第 204 行使用它来生成字符串(gesture_to_str)并在屏幕上打印。
以下截图显示了笔触线条的终端输出(对应于前面图形集中左边的第一个图形):

在前面的截图上,以 'eNq1Vktu…' 开头的长字符串是手势序列化。我们使用这些长字符串作为 Kivy 理解并使用的手势描述符,以将笔触与我们要执行的动作关联起来。下一节将解释如何实现这一点。
识别手势 – 用手指绘制
上一节解释了如何从手势中获得字符串表示。本节解释了如何使用这些字符串表示来识别手势。Kivy 在手势识别中存在一些容错误差,因此您不必担心重复执行完全相同的笔触。
首先,我们将上一节中从笔触生成的字符串复制到一个名为gestures.py的新文件中。这些字符串分配给不同的变量。以下代码对应于gestures.py:
212\. # File Name: gestures.py
213\. line45_str = 'eNq1VktuI0cM3fdFrM0I...
214\. circle_str = 'eNq1WMtuGzkQvM+P2JcI/Sb5A9rrA...
215\. cross_str = 'eNq1V9tuIzcMfZ8fSV5qiH...
上一段代码只显示了字符串的前几个字符,但您可以从 Packt Publishing 网站下载完整的文件,或者使用上一节生成您自己的字符串。
接下来,我们将在drawingspace.py文件中使用这些字符串。首先,让我们在标题中导入必要的类:
216\. # File name: drawingspace.py
217\. from kivy.uix.stencilview import StencilView
218\. from kivy.gesture import Gesture, GestureDatabase
219\. from gestures import line45_str, circle_str, cross_str
220\.
221\. class DrawingSpace(StencilView):
在前面的代码中,我们导入了Gesture和GestureDatabase类(第 218 行),以及添加到gestures.py中的手势字符串表示(第 219 行)。我们向DrawingSpace类添加了几个方法。让我们快速回顾每个方法,并在最后突出关键部分:
-
__init__:该方法创建类的属性,并使用str_to_gesture将字符串转换为手势,并使用add_gesture将手势添加到数据库中:222\. def __init__(self, *args, **kwargs): 223\. super(DrawingSpace, self).__init__() 224\. self.gdb = GestureDatabase() 225\. self.line45 = self.gdb.str_to_gesture(line45_str) 226\. self.circle = self.gdb.str_to_gesture(circle_str) 227\. self.cross = self.gdb.str_to_gesture(cross_str) 228\. self.line135 = self.line45.rotate(90) 229\. self.line225 = self.line45.rotate(180) 230\. self.line315 = self.line45.rotate(270) 231\. self.gdb.add_gesture(self.line45) 232\. self.gdb.add_gesture(self.line135) 233\. self.gdb.add_gesture(self.line225) 234\. self.gdb.add_gesture(self.line315) 235\. self.gdb.add_gesture(self.circle) 236\. self.gdb.add_gesture(self.cross) -
activate和deactivate:这些方法将方法绑定或解绑到触摸事件上,以便启动手势识别模式。这些方法由通用选项中的手势Button调用:237\. def activate(self): 238\. self.tool_box.disabled = True 239\. self.bind(on_touch_down=self.down, 240\. on_touch_move=self.move, 241\. on_touch_up=self.up) 242\. 243\. def deactivate(self): 244\. self.unbind(on_touch_down=self.down, 245\. on_touch_move=self.move, 246\. on_touch_up=self.up) 247\. self.tool_box.disabled = False -
down,move和ups:这些方法以非常相似的方式记录笔画的点,就像上一节所做的那样:248\. def down(self, ds, touch): 249\. if self.collide_point(*touch.pos): 250\. self.points = [touch.pos] 251\. self.ix = self.fx = touch.x 252\. self.iy = self.fy = touch.y 253\. return True 254\. 255\. def move(self, ds, touch): 256\. if self.collide_point(*touch.pos): 257\. self.points += [touch.pos] 258\. self.min_and_max(touch.x, touch.y) 259\. return True 260\. 261\. def up(self, ds, touch): 262\. if self.collide_point(*touch.pos): 263\. self.points += [touch.pos] 264\. self.min_and_max(touch.x, touch.y) 265\. gesture = self.gesturize() 266\. recognized = self.gdb.find(gesture, minscore=0.50) 267\. if recognized: 268\. self.discriminate(recognized) 269\. return True -
gesturize:这种方法从之前方法中收集的点创建一个Gesture实例:270\. def gesturize(self): 271\. gesture = Gesture() 272\. gesture.add_stroke(self.points) 273\. gesture.normalize() 274\. return gesture -
min_and_max:这种方法跟踪笔画的极值点:275\. def min_and_max(self, x, y): 276\. self.ix = min(self.ix, x) 277\. self.iy = min(self.iy, y) 278\. self.fx = max(self.fx, x) 279\. self.fy = max(self.fy, y) -
Discriminate:这种方法根据识别的手势调用相应的方法:280\. def discriminate(self, recognized): 281\. if recognized[1] == self.cross: 282\. self.add_stickman() 283\. if recognized[1] == self.circle: 284\. self.add_circle() 285\. if recognized[1] == self.line45: 286\. self.add_line(self.ix,self.iy,self.fx,self.fy) 287\. if recognized[1] == self.line135: 288\. self.add_line(self.ix,self.fy,self.fx,self.iy) 289\. if recognized[1] == self.line225: 290\. self.add_line(self.fx,self.fy,self.ix,self.iy) 291\. if recognized[1] == self.line315: 292\. self.add_line(self.fx,self.iy,self.ix,self.fy) -
add_circle、add_Line、add_stickman:这些方法使用ToolBox的相应ToolButton根据识别的手势添加一个图形:293\. def add_circle(self): 294\. cx = (self.ix + self.fx)/2.0 295\. cy = (self.iy + self.fy)/2.0 296\. self.tool_box.tool_circle.widgetize(self, cx, cy, self .fx, self.fy) 297\. 298\. def add_line(self,ix,iy,fx,fy): 299\. self.tool_box.tool_line.widgetize(self,ix,iy,fx,fy) 300\. 301\. def add_stickman(self): 302\. cx = (self.ix + self.fx)/2.0 303\. cy = (self.iy + self.fy)/2.0 304\. self.tool_box.tool_stickman.draw(self,cx,cy) -
on_children:这种方法保持状态栏计数器的更新:305\. def on_children(self, instance, value): 306\. self.status_bar.counter = len(self.children)
现在DrawingSpace类负责在屏幕上捕捉笔画,在手势数据库(包含上一节中的手势)中搜索它们,并根据搜索结果绘制形状。它还提供了激活和停用手势识别的可能性。让我们分四部分来讨论这个问题。
首先,我们需要创建GestureDatabase实例(第 224 行)并使用它从字符串中创建手势(第 225 至 227 行)。我们使用rotate方法将line45手势旋转 90 度(第 228 至 230 行)四次,这样GestureDatabase实例就能识别不同方向上的线条手势。然后,我们使用生成的手势加载GestureDatabase(第 231 至 236 行)。我们将所有这些指令添加到类的构造函数中,即__init__方法(第 222 至 236 行),这样DrawingSpace类就有识别手势的所有元素。
其次,我们需要捕捉手势笔画。为了做到这一点,我们使用触摸事件。我们已经创建了与它们相关的这些方法:down(第 248 行),move(第 255 行),以及up(第 261 行)。这些方法与上一节中的on_touch_down、on_touch_move和on_touch_up方法类似,因为它们注册了笔画的点。然而,它们还跟踪笔画的极端轴,以定义笔画的边界框,如下面的图所示:

这些点用于定义我们将要绘制的形状的大小。up方法首先使用注册的点创建一个Gesture实例(第 265 行),其次使用find方法(第 266 行)对GestureDatabase实例进行查询,然后调用discriminate方法绘制适当的形状(第 280 行)。find方法的minscore参数(第 266 行)用于指示搜索的精度。
小贴士
我们使用低级别(0.50),因为我们知道笔画非常不同,并且在这个应用程序中的错误可以很容易地撤销。
第三,我们实现了discriminate方法(第 280 行),以从我们的工具箱的三个可能形状中区分recognized变量。被识别的变量(由GestureDatabase的find方法返回)是一对,其中第一个元素是识别的分数,第二个元素是实际识别的手势。我们使用第二个值(recognized[1])进行区分过程(第 281 行),然后调用相应的方法(add_stickman、add_line和add_circle)。对于线条,它还决定发送坐标的顺序以匹配方向。
第四,activate和deactivate方法提供了一个接口,以便激活或关闭手势模式(我们可以使用手势的应用模式)。要激活模式,activate方法将on_touch_up、on_touch_move和on_tourch_down事件绑定到相应的up、move和down方法。它还使用disabled属性(第 238 行)在手势模式激活时禁用工具箱小部件。deactivate方法解绑事件并恢复disabled属性。
注意
我们将disabled属性应用于整个ToolBox实例,但它会自动查找属于它的子项并将它们也禁用。基本上,事件永远不会发送到子项。
通过手势切换按钮,从常规选项按钮激活和关闭手势模式。我们需要在generaloptions.py文件中更改gestures方法的定义:
307\. def gestures(self, instance, value):
308\. if value == 'down':
309\. self.drawing_space.activate()
310\. else:
311\. self.drawing_space.deactivate()
当gestures ToggleButton处于down状态时,则手势模式被激活;否则,工具箱的正常功能运行。
在下一课中,我们将学习如何使用行为来增强我们小部件的功能。
行为 – 增强小部件的功能
Behaviors最近在 Kivy 版本 1.8.0 中引入,允许我们增加现有小部件的功能性和灵活性。基本上,它们让我们将某些小部件的经典行为注入到其他行为中。例如,我们可以使用ButtonBehavior来向Label或Image小部件添加on_press和on_release功能。目前,有三种类型的行为(ButtonBehavior、ToggleButtonBehavior和DragBehavior),在下一个 Kivy 版本中还将有更多。
让我们在我们的应用程序中添加一些致谢。我们希望向状态栏添加一些功能,以便当我们点击时,会出现一个Popup并显示一些文本。首先,我们将必要的组件导入到statusbar.py头文件中,并更改StatusBar类的定义:
312\. # File name: statusbar.py
313\. import kivy
314\. from kivy.uix.boxlayout import BoxLayout
315\. from kivy.properties import NumericProperty, ObjectProperty
316\. from kivy.uix.behaviors import ButtonBehavior
317\. from kivy.uix.popup import Popup
318\. from kivy.uix.label import Label
319\.
320\. class StatusBar(ButtonBehavior, BoxLayout):
在之前的代码中,我们添加了ButtonBehavior、Popup和Label类(第 316 行和第 318 行)。此外,我们使用 Python 的多重继承同时让StatusBar从ButtonBehavior和BoxLayout继承。我们可以将行为添加到任何类型的部件中,并记得从第一章,GUI 基础 - 构建界面,中了解到布局也是部件。我们利用ButtonBehavior从StatusBar继承的优势,以便使用on_press方法:
321\. def on_press(self):
322\. the_content = Label(text = "Kivy: Interactive Apps and Games in Python\nRoberto Ulloa, Packt Publishing")
323\. the_content.color = (1,1,1,1)
324\. popup = Popup(title='The Comic Creator', content = the_content, size_hint=(None, None), size=(350, 150))
325\. popup.open()
我们重写了on_press方法,在屏幕上显示一个包含应用程序版权信息的Popup窗口。
注意
注意,行为不会改变部件的外观;只有与用户输入相关的交互处理功能才会发生变化。
在第 322 行和第 323 行,我们创建了一个包含我们想要显示的文本的Label实例,并确保颜色为白色。在第 324 行,我们创建了一个带有标题的Popup实例,并将Label实例作为内容。最后,在第 325 行,我们显示了Popup实例。以下是点击状态栏后我们得到的结果:

理论上,我们可以将行为添加到任何部件。然而,实际限制可能导致意外结果。例如,当我们向ToggleButton添加ButtonBehavior会发生什么?ToggleButton从Button继承,而Button从ButtonBehavior继承。因此,我们继承了相同的方法两次。多重继承有时确实很棘手。这个例子很明显(我们为什么会考虑创建一个从ButtonBehavior和ToggleButton继承的类呢?)。然而,还有许多其他复杂的部件已经包含了触摸事件的功能。
注意
当你向重叠功能相关的部件添加行为时,你应该小心。当前的行为,ButtonBehavior、ToggleButtonBehavior、DragBehavior、CompoundSelectionBehavior和FocusBehavior都与触摸事件相关。
这里的一个特殊例子是Video部件,我们将在第六章,Kivy 播放器 - TED 视频流播放器中对其进行探讨。这个部件有一个名为state的属性,与ToggleButton的状态属性同名。如果我们想从这两个类中同时进行多重继承,这将会引起名称冲突。
你可能已经注意到,我们明确地将标签的Label颜色设置为白色(第 323 行),这本来就是标签的默认颜色。我们这样做是为了让一切准备就绪,以便在下一节中装饰我们的界面。
样式 - 装饰界面
在本节中,我们将重新装饰我们的界面,以改善其外观和感觉。通过一些战略性的少量更改,我们将通过几个步骤完全改变应用程序的外观。让我们从将背景颜色从黑色更改为白色开始。我们将在 comicreator.py 文件中这样做,以下是它的新标题:
326\. # File name: comiccreator.py
327\. import kivy
328\. from kivy.app import App
329\. from kivy.lang import Builder
330\. from kivy.uix.screenmanager import ScreenManager
331\. from kivy.core.window import Window
332\.
333\. Window.clearcolor = (1, 1, 1, 1)
334\.
335\. Builder.load_file('style.kv')
我们导入了管理应用程序窗口配置的 Window 类,并控制一些全局参数和事件,例如键盘事件,这些将在 第五章 入侵者复仇 – 一个交互式多点触控游戏 中介绍。我们使用 Window 类通过 clearcolor 属性(第 333 行)将应用程序的背景颜色更改为白色。最后,我们向 Builder 添加了一个新文件。这个名为 style.kv 的文件如下所示:
336\. # File name: style.kv
337\.
338\. <Label>:
339\. bold: True
340\. color: 0,.3,.6,1
341\.
342\. <Button>:
343\. background_normal: 'normal.png'
344\. background_down: 'down.png'
345\. color: 1,1,1,1
我们需要与刚刚应用于整个窗口的白色背景形成对比的颜色。因此,我们对 Kivy 的两个基本小部件 Label 和 Button 进行了修改,这影响了所有继承自它们的所有组件。我们将 Label 的 bold 属性(第 339 行)设置为 True,并将 color 属性(第 340 行)设置为蓝色(在打印版本中为灰色)。
我们还更改了 Button 类的默认背景,并介绍了如何创建圆角按钮。background_normal 属性(第 343 行)表示 Button 在其正常状态下使用的背景图像,而 background_down 属性(第 344 行)表示 Button 在按下时使用的图像。
最后,我们将 Button 的 color 属性(第 345 行)重置为白色。你可能想知道,如果 Button 类的文本默认颜色是白色,我们为什么要这样做。问题在于我们刚刚更改了 Label 类的颜色,由于 Button 继承自 Label,这种更改也影响了 Button 类。
注意
规则的顺序也很重要。如果我们首先放置 <Button>: 规则,那么它将不再工作,因为 <Label>: 规则将覆盖 <Button>: 规则。
我们可以看到我们装饰过的界面的结果:

新的设计仍有不尽如人意之处。与字体相比,我们的图形线条相当细,而且与白色背景相比,对比度似乎有所丧失。让我们快速学习一种补救方法来更改我们线条的默认属性。
工厂 – 替换顶点指令
本章的最后部分教给我们一个宝贵的技巧,用于更改 顶点指令 的默认属性。我们想要更改界面上所有线条的宽度。这包括圆圈、线条和棍人。当然,我们可以重新访问创建 Line 顶点指令的所有类(记住,圆圈也是 Line 实例,棍人也是由 Line 实例组成的),并在它们中更改宽度属性。不用说,那将是繁琐的。
相反,我们将替换默认的 Line 类。实际上,这与我们在上一节中更改标签和按钮默认属性时所做的是等效的。我们有一个问题,那就是我们无法在 Kivy 语言中创建规则来更改顶点指令。但是,有一个等效的方法可以绕过这个问题,使用名为 style.py 的新文件中的 Python 代码:
346\. # File name: style.py
347\. from kivy.graphics import Line
348\. from kivy.factory import Factory
349\.
350\. class NewLine (Line):
351\. def __init__(self, **kwargs):
352\. if not kwargs.get('width'):
353\. kwargs['width'] = 1.5
354\. Line.__init__(self, **kwargs)
355\.
356\. Factory.unregister('Line')
357\. Factory.register('Line', cls=NewLine)
在这段代码中,我们创建了自己的 NewLine 类,它继承自 Kivy 的 Line 类(第 350 行)。通过一点 Python 小技巧,我们改变了构造方法(__init__)的 kwargs 参数,以便设置不同的默认宽度(第 353 行)。kwargs 参数是一个字典,包含在创建 Line 实例时明确设置的属性。在这种情况下,如果构造函数(第 352 行)中没有指定 width 属性,我们将宽度默认设置为 1.5(第 353 行)。然后,我们使用调整后的 kwargs 调用基类的构造函数(第 354 行)。
现在,是时候用我们的类替换默认的 Kivy Line 类了。我们需要导入 Kivy 的 Factory(第 348 行),我们可以用它来注册或注销类,并在 Kivy 语言中使用它们。首先,我们需要使用 unregister 方法(第 356 行)注销当前的 Line 类。然后,我们需要使用 register 方法(第 357 行)注册我们的 NewLine 类。在这两种方法中,第一个参数代表用于从 Kivy 语言实例化类的名称。由于我们要替换类,我们将使用相同的名称注册 NewLine 类。在 register 方法(第 357 行)中,第二个参数(cls)表示我们注册的类。
注意
我们可以使用 Factory 类在 Kivy 语言中添加我们经常需要使用的不同线条。例如,我们可以用 ThickLine 名称注册我们的新类,然后在 Kivy 语言中实例化它。
我们故意避免这种策略,因为我们实际上想要替换默认的 Line,这样我们就可以直接在 Kivy 语言中影响我们创建的所有 Line 实例。然而,我们不应该忘记使用 NewLine 类来创建用户将动态创建的实例。我们需要从样式文件中导入 NewLine 并设置别名(Line),这样我们就可以使用相同的名称引用该类(第 362 行)。我们还需要从 toolbox.py 文件中移除我们导入的 kivy.graphics(第 362 行),以避免名称冲突:
358\. # File name: toolbox.py
359\. import math
360\. from kivy.uix.togglebutton import ToggleButton
361\. from kivy.graphics import Color
362\. from style import Line
363\. from comicwidgets import StickMan, DraggableWidget
这是我们的 Comic Creator 的最终截图,展示了更粗的线条:

摘要
本章涵盖了一些特定且有用的主题,这些主题可以改善用户体验。我们添加了几个屏幕,并通过 ScreenManager 在它们之间切换。我们学习了如何在画布中使用颜色,现在我们应该对内部工作方式有很好的理解。我们还学习了如何使用 StencilView 限制绘图区域到 drawing space。我们使用 Scatter 为 DraggableWidget 添加旋转和缩放功能,并通过使用属性和相关事件扩展了功能。我们还介绍了使用手势使界面更具动态性的方法。我们介绍了如何使用行为增强小部件。最后,我们学习了如何通过修改默认小部件和顶点指令来改进界面。
这是本章中我们学习使用的所有类的回顾,包括它们各自的方法、属性和属性:
-
ScreenManager:transistion和current属性 -
FadeTransition、SwapTransition、SlideTransition和WipeTransition过渡 -
Screen:name和manager属性 -
ColorPicker:color属性 -
StencilView -
Scatter:rotate和scale属性,以及on_translate、on_rotate和on_scale方法(事件) -
ScatterLayout:size_hint和pos_hint属性 -
Gesture:add_stroke、normalize和rotate方法 -
GestureDatabase:gesture_to_str、str_to_gesture、add_gesture和find方法 -
Widget:disabled属性 -
ButtonBehavior、ToggleBehavior和DragBehavior:on_press方法 -
Popup:title和content属性 -
Window:clearcolor属性 -
Factory:register和unregister方法
这些都是非常有用的组件,帮助我们创建更具吸引力和动态的应用程序。在本章中,我们提供了一个示例,展示了如何展示这些类的能力。尽管我们没有详尽地探索所有选项,但我们应该能够舒适地使用这些组件来增强应用程序。我们始终可以查看 Kivy API 以获取更全面的属性和方法列表。
下一章将介绍个性化多点触控、动画以及时钟和键盘事件。我们将创建一个新的交互式项目,一款类似于街机游戏 太空侵略者 的游戏。
第五章。侵略者复仇 - 一个交互式多点触控游戏
本章介绍了一系列组件和策略,用于制作动画和动态应用程序。其中大部分特别适用于游戏开发。本章充满了如何结合不同的 Kivy 元素以及控制同时发生的多个事件的策略的示例。所有示例都集成在一个全新的项目中,这是一个经典游戏《太空侵略者》的版本(版权©1978 年太田公司,en.wikipedia.org/wiki/Space_Invaders)。以下是我们将在本章中工作的主要组件列表:
-
图集:一个 Kivy 包,允许我们高效地加载图像
-
声音:允许声音管理的类
-
动画:可以应用于小部件的过渡、时间控制、事件和操作
-
时钟:一个允许我们安排事件的类
-
多点触控:一种允许我们根据触摸控制不同动作的策略
-
键盘:Kivy 捕获键盘事件的策略
第一部分介绍了项目概述、GUI 和游戏规则。之后,我们将采用自下而上的方法。解释与游戏各个组件相关的简单类,然后依次介绍本章的其他主题。我们将以对游戏有主要控制权的类结束。到本章结束时,你应该能够开始为你的移动设备实现任何你一直想要实现的游戏应用程序。
侵略者复仇 - 一个动画多点触控游戏
侵略者复仇是我们 Kivy 版本的《太空侵略者》©的名称。以下截图显示了本章我们将构建的游戏:

截图中有几个黄色和青色的标签(或打印版本中的灰色虚线)。它们帮助我们识别游戏的结构;游戏将包括一个射击者(玩家),他向 32(8x4)个侵略者射击(射击),这些侵略者试图用他们的导弹摧毁射击者。侵略者组织在一个舰队(水平移动)中,有时一个单独的侵略者可以突破网格结构,在屏幕上飞来飞去,然后再回到其在舰队中的对应位置(码头)。
屏幕上横跨的青色(打印版本中为灰色)线条表示屏幕内部将屏幕划分为敌对区域和射击区域。这种划分用于区分根据屏幕不同部分发生的触摸而应发生的动作。
游戏的骨架在invasion.kv文件中展示:
1\. # File name: invasion.kv
2\. <Invasion>:
3\. id: _invasion
4\. shooter: _shooter
5\. fleet: _fleet
6\. AnchorLayout:
7\. anchor_y: 'top'
8\. anchor_x: 'center'
9\. FloatLayout:
10\. id: _enemy_area
11\. size_hint: 1, .7
12\. Fleet:
13\. id: _fleet
14\. invasion: _invasion
15\. shooter: _shooter
16\. cols: 8
17\. spacing: 40
18\. size_hint: .5, .4
19\. pos_hint: {'top': .9}
20\. x: root.width/2-root.width/4
21\. AnchorLayout:
22\. anchor_y: 'bottom'
23\. anchor_x: 'center'
24\. FloatLayout:
25\. size_hint: 1, .3
26\. Shooter:
27\. id: _shooter
28\. invasion: _invasion
29\. enemy_area: _enemy_area
有两个AnchorLayout实例。上面的一个是包含舰队的敌对区域,下面的一个是包含射击者的射击区域。
小贴士
敌人区域和射击区域对于游戏的逻辑非常重要,以便区分屏幕上触摸的类型。
我们还创建了一些 ID 和引用,这将允许不同界面实例之间的交互。以下图表总结了这些关系:

Atlas – 高效的图像管理
当涉及到使用许多图像的应用程序时,减少它们的加载时间非常重要,尤其是在它们从远程服务器请求时。
注意
减少加载时间的一种策略是使用Atlas(也称为精灵)。Atlas 将所有应用程序图像组合成一个大的图像,因此减少了必要的操作系统或在线请求的数量。
这里是我们用于“侵略者复仇”游戏的 Atlas 图像:

我们将不再请求五个“侵略者复仇”图像,而是只请求 Atlas 图像。我们还需要一个相关的json文件,它告诉我们图像中每个单元的确切坐标。好消息是,我们不需要手动做这件事。Kivy 提供了一个简单的命令来创建 Atlas 图像和json文件。假设所有图像都在名为img的目录中,我们只需要打开终端,转到img目录(包含单个图像),然后在终端中运行以下命令:
python -m kivy.atlas invasion 100 *.png
注意
为了执行前面的命令,您需要安装Pillow 库(python-pillow.github.io/)。
命令包含三个参数,即basename、size和images list。basename参数是json文件(img/invasion.json)和 Atlas 图像或图像(img/invasion-0.png)的前缀。可能会生成多个 Atlas 图像,在这种情况下,我们会有一系列以basename为前缀并跟随数字标识符的图像,例如,invasion-0.png和invasion-1.png。size参数表示结果 Atlas 图像的像素大小。请确保指定一个比最大的图像更大的大小。image list参数是要添加到 Atlas 的所有图像的列表,我们可以使用*通配符。在我们的情况下,我们将使用它来指示所有具有.png扩展名的文件。
为了在 Kivy 语言中使用 Atlas,我们必须使用以下格式:atlas://path/to/atlas/atlas_name/id。id文件指的是不带扩展名的图像文件名。例如,我们通常会将射击者图像作为源引用:'img/shooter.png'。在生成 Atlas 后,它变为source: 'atlas://images/invasion/shooter'。下面的image.kv文件展示了“侵略者复仇”中所有图像的代码:
30\. # File name: images.kv
31\. <Invader>:
32\. source: 'atlas://img/invasion/invader'
33\. size_hint: None,None
34\. size: 40,40
35\. <Shooter>:
36\. source: 'atlas://img/invasion/shooter'
37\. size_hint: None,None
38\. size: 40,40
39\. pos: self.parent.width/2, 0
40\. <Boom>:
41\. source: 'atlas://img/invasion/boom'
42\. size_hint: None,None
43\. size: 26,30
44\. <Shot>:
45\. source: 'atlas://img/invasion/shot'
46\. size_hint: None,None
47\. size: 12,15
48\. <Missile>:
49\. source: 'atlas://img/invasion/missile'
50\. size_hint: None,None
51\. size: 12,27
本文件中的所有类都是直接或间接地从 Image 类继承而来的。Missile 和 Shot 首先从名为 Ammo 的类继承,该类也继承自 Image。还有一个 Boom 类,当任何 Ammo 被触发时,它将创建爆炸效果。除了 Boom 图像(Atlas 中的星星)外,Boom 类还将与我们在下一节中添加的声音相关联。
爆炸声 – 简单的声音效果
在 Kivy 中添加声音效果非常简单。当创建 Boom 实例时,它会产生声音,并且每次发射 射击 或 导弹 时都会发生这种情况。以下是 boom.py 的代码:
52\. # File name: boom.py
53\. from kivy.uix.image import Image
54\. from kivy.core.audio import SoundLoader
55\.
56\. class Boom(Image):
57\. sound = SoundLoader.load('boom.wav')
58\. def boom(self, **kwargs):
59\. self.__class__.sound.play()
60\. super(Boom, self).__init__(**kwargs)
生成声音涉及使用两个类,Sound 和 SoundLoader(第 54 行)。SoundLoader 加载音频文件(.wav)并返回一个 Sound 实例(第 57 行),我们将其保存在 sound 引用中(Boom 类的静态属性)。每当创建一个新的 Boom 实例时,我们都会播放声音。
Ammo – 简单动画
本节解释了如何对 射击 和 导弹 进行动画处理,它们表现出非常相似的行为。它们从原始位置移动到目的地,不断检查是否击中了目标。以下是对 ammo.py 类的代码:
61\. # File name: ammo.py
62\. from kivy.animation import Animation
63\. from kivy.uix.image import Image
64\. from boom import Boom
65\.
66\. class Ammo(Image):
67\. def shoot(self, tx, ty, target):
68\. self.target = target
69\. self.animation = Animation(x=tx, top=ty)
70\. self.animation.bind(on_start = self.on_start)
71\. self.animation.bind(on_progress = self.on_progress)
72\. self.animation.bind(on_complete = self.on_stop)
73\. self.animation.start(self)
74\.
75\. def on_start(self, instance, value):
76\. self.boom = Boom()
77\. self.boom.center=self.center
78\. self.parent.add_widget(self.boom)
79\.
80\. def on_progress(self, instance, value, progression):
81\. if progression >= .1:
82\. self.parent.remove_widget(self.boom)
83\. if self.target.collide_ammo(self):
84\. self.animation.stop(self)
85\.
86\. def on_stop(self, instance,value):
87\. self.parent.remove_widget(self)
88\.
89\. class Shot(Ammo):
90\. pass
91\. class Missile(Ammo):
92\. pass
对于 Ammo 动画,我们需要一个简单的 Animation(第 69 行)。我们发送 x 和 top 作为参数。
注意
Animation 实例的参数可以是应用动画的部件的任何属性。
在这种情况下,x 和 top 属性属于 Ammo 本身。这足以将 Ammo 的 Animation 从其原始位置设置为 tx,ty。
注意
默认情况下,Animation 的执行周期为 1 秒。
在 Ammo 的轨迹中,我们需要 Ammo 做一些额外的事情。
注意
Animation 类包括三个事件,当动画开始时触发(on_start),在其进行过程中触发(on_progress),以及当它停止时触发(on_stop)。
我们将这些事件(第 70 至 72 行)绑定到我们自己的方法上。on_start 方法(第 75 行)在动画开始时显示一个 Boom 实例(第 76 行)。on_progress(第 80 至 84 行)方法在 10% 的 progression(第 81 和 82 行)后移除 Boom。此外,它还会不断检查 target(第 83 行)。当 target 被击中时,动画停止(第 84 行)。一旦动画结束(或被停止),Ammo 就会从父级中移除(第 82 行)。
第 89 至 92 行定义了两个类,Shot 和 Missile。Shot 和 Missile 类从 Ammo 继承,它们目前唯一的区别是 images.kv 中使用的图像。最终,我们将使用 Shot 实例进行 射击,使用 Missile 实例进行 入侵者。在此之前,让我们给 入侵者 一些自由,这样它们就可以离开它们的 舰队 并执行单独的攻击。
入侵者 – 动画的过渡
前一节使用默认的Animation过渡。这是一个Linear过渡,这意味着Widget实例从一个点移动到另一个点是一条直线。入侵者的轨迹可以更有趣。例如,可能会有加速度或方向变化,如下面的截图所示:

以下为invader.py的代码:
93\. # File name: invader.py
94\. from kivy.core.window import Window
95\. from kivy.uix.image import Image
96\. from kivy.animation import Animation
97\. from random import choice, randint
98\. from ammo import Missile
99\.
100\. class Invader(Image):
101\. pre_fix = ['in_','out_','in_out_']
102\. functions = ['back','bounce','circ','cubic',
103\. 'elastic','expo','quad','quart','quint','sine']
104\. formation = True
105\.
106\. def solo_attack(self):
107\. if self.formation:
108\. self.parent.unbind_invader()
109\. animation = self.trajectory()
110\. animation.bind(on_complete = self.to_dock)
111\. animation.start(self)
112\.
113\. def trajectory(self):
114\. fleet = self.parent.parent
115\. area = fleet.parent
116\. x = choice((-self.width,area.width+self.width))
117\. y = randint(round(area.y), round(fleet.y))
118\. t = choice(self.pre_fix) + choice(self.functions)
119\. return Animation(x=x, y=y,d=randint(2,7),t=t)
120\.
121\. def to_dock(self, instance, value):
122\. self.y = Window.height
123\. self.center_x = Window.width/2
124\. animation = Animation(pos=self.parent.pos, d=2)
125\. animation.bind(on_complete = self.parent.bind_invader)
126\. animation.start(self)
127\.
128\. def drop_missile(self):
129\. missile = Missile()
130\. missile.center = (self.center_x, self.y)
131\. fleet = self.parent.parent
132\. fleet.invasion.add_widget(missile)
133\. missile.shoot(self.center_x,0,fleet.shooter)
这段代码背后的想法是让一个入侵者从舰队中打破队形并进入solo_attack(第 106 至 111 行)方法。入侵者的Animation在trajectory方法(第 113 和 119 行)中创建,通过随机化入侵者轨迹的终点(第 116 和 117 行)。这种随机化将在敌方区域的左右边界上选择两个坐标。此外,我们还随机化transition(第 118 行)和duration(第 119 行)以创建更多样化和不可预测的轨迹。
注意
Kivy 目前包含 31 种过渡。它们由一个字符串表示,例如'in_out_cubic',其中in_out是一个前缀,描述了函数(cubic)的使用方式。有三个可能的前缀(in、out和in_out),以及 10 个函数(第 102 行),例如cubic、exponential、sin、quadratic。请访问 Kivy API 以了解它们的描述(kivy.org/docs/api-kivy.animation.html)。
第 118 行随机选择一个过渡。过渡应用于进度,因此同时应用于x和y,这在轨迹上产生了一个有趣的加速度效果。
当Animation类结束其轨迹(第 110 行)时,to_dock方法(第 121 至 126 行)将入侵者从Window的顶部中心部分返回到其原始位置。我们使用Window类来获取height和width。有时这比遍历父级链以找到根小部件要简单。当入侵者到达停靠点时,它会被绑定回那里(第 125 行)。
最后一个方法(第 128 至 133 行的drop_missile)发射一枚从入侵者底部中心位置(第 130 行)开始垂直向下至屏幕底部的导弹(第 133 行)。记住,Missile类继承自我们在前一节中创建的Ammo类。
我们现在可以让入侵者自由地在敌方区域内移动。然而,我们还想有一种群体移动方式。在下一节中,我们将为每个相应的入侵者创建一个停靠点。这样,入侵者在舰队队形中就有了一个对应的占位符。之后,我们将创建舰队,它将不断移动所有的停靠点。
停靠点 – Kivy 语言中的自动绑定
你可能会从之前的章节中意识到,Kivy 语言不仅仅是将其规则转换为 Python 指令。例如,你可能会看到,当它创建属性时,它也会绑定它们。
注意
当我们在布局内部执行一些常见操作,例如 pos: self.parent.pos 时,父级的属性就会绑定到其子级。当父级移动时,子级总是移动到父级的位置。
这通常是期望的,但并非总是如此。考虑一下入侵者的 solo_attack。我们需要它打破队形,并在屏幕上遵循自由轨迹。当这种情况发生时,整个入侵者队形将继续从右向左和从左向右移动。这意味着入侵者将同时接收到两个命令;一个来自移动的父级,另一个来自轨迹的 Animation。
这意味着我们需要为每个入侵者(invader)提供一个占位符(dock)。这样,当入侵者从单独攻击执行返回时,可以确保其空间。如果没有占位符,舰队(GridLayout,我们将在下一节中看到)的布局将自动重新配置队形,重新分配剩余的入侵者以填充空位。此外,入侵者还需要从父级(dock)中释放自己,以便可以在屏幕上的任何位置漂浮。以下代码(dock.py)使用 Python 而不是 Kivy 语言绑定(第 145 至 147 行)和解除绑定(第 149 至 151 行)入侵者:
134\. # File name: dock.py
135\. from kivy.uix.widget import Widget
136\. from invader import Invader
137\.
138\. class Dock(Widget):
139\. def __init__(self, **kwargs):
140\. super(Dock, self).__init__(**kwargs)
141\. self.invader = Invader()
142\. self.add_widget(self.invader)
143\. self.bind_invader()
144\.
145\. def bind_invader(self, instance=None, value=None):
146\. self.invader.formation = True
147\. self.bind(pos = self.on_pos)
148\.
149\. def unbind_invader(self):
150\. self.invader.formation = False
151\. self.unbind(pos = self.on_pos)
152\.
153\. def on_pos(self, instance, value):
154\. self.invader.pos = self.pos
小贴士
我们使用 第三章 的知识,部件事件 – 绑定动作,来编写此代码,但重要的是我们应用的策略。
有时会希望避免使用 Kivy 语言,因为这更有利于完全控制。
这并不意味着使用 Kivy 语言解决这个问题是不可能的。例如,一种常见的方法是将入侵者的父级(dock)切换到,比如说,应用程序的根 Widget 实例;这将解除入侵者位置与其当前父级的绑定。我们遵循哪种方法并不重要。只要我们理解了机制,我们就能找到优雅的解决方案。
现在既然每个入侵者都有一个确保其在入侵者队形中位置的 dock,我们就准备好向舰队引入一些运动。
舰队 – 无限连接的动画
在本节中,我们将使舰队从右向左和从左向右进行动画处理,以保持持续运动,如以下截图中的箭头所示:

为了做到这一点,我们将学习如何在动画完成之后立即连接另一个动画。实际上,我们将创建一个无限循环的动画,使得舰队处于持续运动状态。
小贴士
我们可以使用 on_complete 事件连接两个动画。
以下为 fleet.py 的代码片段 1(共 2 个),展示了如何连接这些事件:
155\. # File name: fleet.py (Fragment 1)
156\. from kivy.uix.gridlayout import GridLayout
157\. from kivy.properties import ListProperty
158\. from kivy.animation import Animation
159\. from kivy.clock import Clock
160\. from kivy.core.window import Window
161\. from random import randint, random
162\. from dock import Dock
163\.
164\. class Fleet(GridLayout):
165\. survivors = ListProperty(())
166\.
167\. def __init__(self, **kwargs):
168\. super(Fleet, self).__init__(**kwargs)
169\. for x in range(0, 32):
170\. dock = Dock()
171\. self.add_widget(dock)
172\. self.survivors.append(dock)
173\. self.center_x= Window.width/4
174\.
175\. def start_attack(self, instance, value):
176\. self.invasion.remove_widget(value)
177\. self.go_left(instance, value)
178\. self.schedule_events()
179\.
180\. def go_left(self, instance, value):
181\. animation = Animation(x = 0)
182\. animation.bind(on_complete = self.go_right)
183\. animation.start(self)
184\.
185\. def go_right(self, instance, value):
186\. animation = Animation(right=self.parent.width)
187\. animation.bind(on_complete = self.go_left)
188\. animation.start(self)
go_left 方法(第 180 至 183 行)将 Animation 实例的 on_complete(第 182 行)事件绑定到 go_right 方法(第 185 至 188 行)。同样,go_right 方法将另一个 Animation 实例的 on_complete(第 187 行)事件绑定到 go_left 方法。通过这种策略,我们创建了一个两个动画的无穷循环。
fleet.py 类还重载了构造函数,向 Fleet 的子类添加了 32 个 入侵者(第 169 至 173 行)。这些 入侵者 被添加到我们用来跟踪尚未被击落的 入侵者 的 ListProperty 中。start_attack 方法(第 175 至 178 行)通过调用 go_left 方法(第 177 行)和 schedule_events 方法(第 178 行)启动 Fleet 动画。后者使用了 Clock,这将在下一节中解释。
使用时钟安排事件
我们看到 Animation 有一个持续时间参数,它确定了动画应该持续的时间。另一个与时间相关的话题是在特定时间或 n 秒的间隔内安排特定任务。在这些情况下,我们使用 Clock 类。让我们分析以下 fleet.py 的代码片段 2(共 2 个),如下所示:
189\. # File name: fleet.py (Fragment 2)
190\. def schedule_events(self):
191\. Clock.schedule_interval(self.solo_attack, 2)
192\. Clock.schedule_once(self.shoot,random())
193\.
194\. def solo_attack(self, dt):
195\. if len(self.survivors):
196\. rint = randint(0, len(self.survivors) - 1)
197\. child = self.survivors[rint]
198\. child.invader.solo_attack()
199\.
200\. def shoot(self, dt):
201\. if len(self.survivors):
202\. rint = randint(0,len(self.survivors) - 1)
203\. child = self.survivors[rint]
204\. child.invader.drop_missile()
205\. Clock.schedule_once(self.shoot,random())
206\.
207\. def collide_ammo(self, ammo):
208\. for child in self.survivors:
209\. if child.invader.collide_widget(ammo):
210\. child.canvas.clear()
211\. self.survivors.remove(child)
212\. return True
213\. return False
214\.
215\. def on_survivors(self, instance, value):
216\. if len(self.survivors) == 0:
217\. Clock.unschedule(self.solo_attack)
218\. Clock.unschedule(self.shoot)
219\. self.invasion.end_game("You Win!")
schedule_events 方法(第 190 至 192 行)为特定时间安排动作。第 191 行每两秒安排一次 solo_attack 方法。第 192 行随机安排一次 shoot(在 0 和 1 秒之间)。
注意
schedule_interval 方法定期安排动作,而 schedule_once 方法只安排一次动作。
solo_attack 方法(第 194 至 198 行)随机选择一个幸存者执行我们为 入侵者 研究的单独攻击(invader.py 中的第 106 至 111 行)。shoot 方法(第 200 至 205 行)随机选择一个幸存者向 射击者 发射 导弹(第 201 至 204 行)。之后,该方法安排另一个 shoot(第 205 行)。
在 Ammo 类中,我们使用了 collide_ammo 方法来验证 Ammo 实例是否击中了任何 入侵者(ammo.py 中的第 83 行)。现在,在 fleet.py 中,我们实现了这样一个方法(第 207 或 213 行),它将 入侵者 隐藏并从幸存者列表中删除。每当修改幸存者 ListProperty 时,都会触发 on_survivors 事件。当没有幸存者剩下时,我们使用 unscheduled 方法(第 217 和 218 行)取消安排事件,并通过显示 You Win! 消息来结束游戏。
我们完成了射击者敌人的创建。现在,是时候为 射击者 提供躲避 导弹 和 射击 以击中 入侵者 的移动了。
射击者 – 多点触控控制
Kivy 支持多点触控交互。这个特性始终存在,但我们除了在 第四章 改进用户体验 中使用 Scatter 小部件时之外,并没有过多关注它。我们没有明确指出整个屏幕和 GUI 组件已经能够进行多点触控,以及 Kivy 会相应地处理这些事件。
注意
Kivy 在内部处理多点触控动作。这意味着所有 Kivy 小部件和组件都支持多点触控交互;我们不必担心这一点。Kivy 解决了多点触控控制中常见的模糊情况的所有可能冲突,例如,同时触摸两个按钮。
话虽如此,控制特定实现的责任在我们身上。多点触控编程引入了我们需要作为开发者解决的逻辑问题。尽管如此,Kivy 提供了与每个特定触摸相关的数据,因此我们可以处理逻辑。主要问题是我们需要不断区分一个触摸与另一个触摸,然后采取相应的行动。
在《入侵者复仇》中,我们需要区分由相同类型的触摸触发的两种动作。第一种动作是 射手 的水平移动,以避免入侵者的 导弹。第二种是触摸屏幕以射击 入侵者。以下截图通过宽粗箭头(滑动触摸)和虚线细箭头(射击动作)说明了这两种动作:

以下 shooter.py 的代码片段 1(共 2 个),通过使用 敌人区域 和 射手区域 来控制这两种动作:
220\. # File name: shooter.py (Fragment 1)
221\. from kivy.clock import Clock
222\. from kivy.uix.image import Image
223\. from ammo import Shot
224\.
225\. class Shooter(Image):
226\. reloaded = True
227\. alife = False
228\.
229\. def on_touch_down(self, touch):
230\. if self.parent.collide_point(*touch.pos):
231\. self.center_x = touch.x
232\. touch.ud['move'] = True
233\. elif self.enemy_area.collide_point(*touch.pos):
234\. self.shoot(touch.x,touch.y)
235\. touch.ud['shoot'] = True
236\.
237\. def on_touch_move(self, touch):
238\. if self.parent.collide_point(*touch.pos):
239\. self.center_x = touch.x
240\. elif self.enemy_area.collide_point(*touch.pos):
241\. self.shoot(touch.x,touch.y)
242\.
243\. def on_touch_up(self, touch):
244\. if 'shoot' in touch.ud and touch.ud['shoot']:
245\. self.reloaded = True
on_touch_down(第 229 至 235 行)和 on_touch_move(第 237 至 241 行)方法通过分别使用 射手区域(第 230 和 238 行)和 敌人区域(第 233 和 240 行)小部件来区分两种动作,移动 或 射击,以便碰撞事件的坐标。
注意
触摸坐标是最常见的识别特定触摸的策略。然而,触摸有许多其他属性可以帮助区分它们,例如,时间、双击(或三击)或输入设备。您可以通过检查 MotionEvent 类来查看触摸的所有属性(kivy.org/docs/api-kivy.input.motionevent.html#kivy.input.motionevent.MotionEvent)。
与之相反,on_touch_up 方法(第 243 行)采用了一种不同的方法。它使用 MotionEvent 实例(触摸)的 ud 属性(用户数据字典,用于在触摸中存储个性化数据)来确定事件开始时的触摸是否是 移动(在 射手区域)还是 射击(在 敌人区域)。我们之前在 on_touch_down 中设置了 touch.ud(第 232 和 235 行)。
注意
Kivy 将触摸事件与三个基本触摸事件(按下、移动和抬起)关联起来,因此我们获取的on_touch_down、on_touch_move和on_touch_up的触摸引用是相同的,我们可以区分触摸。
现在我们来分析这些事件调用的方法细节。以下是从shooter.py中提取的代码片段 2(共 2 个):
246\. # File name: shooter.py (Fragment 2)
247\. def start(self, instance, value):
248\. self.alife=True
249\.
250\. def shoot(self, fx, fy):
251\. if self.reloaded and self.alife:
252\. self.reloaded = False
253\. Clock.schedule_once(self.reload_gun, .5)
254\. shot = Shot()
255\. shot.center = (self.center_x, self.top)
256\. self.invasion.add_widget(shot)
257\. (fx,fy) = self.project(self.center_x,self.top,fx,fy)
258\. shot.shoot(fx,fy,self.invasion.fleet)
259\.
260\. def reload_gun(self, dt):
261\. self.reloaded = True
262\.
263\. def collide_ammo(self, ammo):
264\. if self.collide_widget(ammo) and self.alife:
265\. self.alife = False
266\. self.color = (0,0,0,0)
267\. self.invasion.end_game("Game Over")
268\. return True
269\. return False
270\.
271\. def project(self,ix,iy,fx,fy):
272\. (w,h) = self.invasion.size
273\. if ix == fx: return (ix, h)
274\. m = (fy-iy) / (fx-ix)
275\. b = iy - m*ix
276\. x = (h-b)/m
277\. if x < 0: return (0, b)
278\. elif x > w: return (w, m*w+b)
279\. return (x, h)
我们首先创建了一个方法来启动射手(第 247 和 248 行),我们将在游戏开始时使用它。然后,我们为on_touch_move方法与shoot方法(第 250 至 258 行)实现了一个有趣的行为。我们不是尽可能快地射击,而是将下一次shoot延迟0.5秒。这个延迟模拟了枪需要重新装填的时间间隔(第 253 行)。否则,如果允许计算机以尽可能快的速度射击,这对入侵者来说是不公平的。相反,当我们使用on_touch_up方法时,枪立即重新装填,因此在这种情况下,这将取决于玩家的技巧,看谁能够通过射门和触摸序列更快地射击。
collide_ammo方法(第 263 至 269 行)几乎等同于Fleet(第 207 至 213 行)中的collide_ammo方法。唯一的区别是只有一个射手而不是一组入侵者。如果射手被击中,则游戏结束,并显示游戏结束的消息。请注意,我们没有移除射手,我们只是将其alife标志设置为False(第 265 行),并通过将颜色设置为黑色(第 266 行)来隐藏它。这样,我们避免了指向不再存在于界面上下文中的实例的引用不一致。
project方法(第 271 至 278 行)将触摸坐标扩展到屏幕边界,因此射击将继续其轨迹直到屏幕的尽头,而不是正好停止在触摸坐标处。数学细节超出了本书的范围,但它是一种简单的线性投影。
应用程序几乎准备好了。只有一个小问题。如果你没有多点触控屏幕,实际上你将无法玩这个游戏。下一节将介绍如何处理键盘事件,以便采用更经典的游戏方式,这种方式结合了键盘和鼠标。
入侵 - 使用键盘移动射手
本节提供了第二种移动射手的方法。如果你没有多点触控设备,你将需要使用其他东西来轻松控制射手的位置,同时你使用鼠标进行射击。以下是从main.py中提取的代码片段 1(共 2 个):
280\. # File name: main.py (Fragment 1)
281\. from kivy.app import App
282\. from kivy.lang import Builder
283\. from kivy.core.window import Window
284\. from kivy.uix.floatlayout import FloatLayout
285\. from kivy.uix.label import Label
286\. from kivy.animation import Animation
287\. from kivy.clock import Clock
288\. from fleet import Fleet
289\. from shooter import Shooter
290\.
291\. Builder.load_file('images.kv')
292\.
293\. class Invasion(FloatLayout):
294\.
295\. def __init__(self, **kwargs):
296\. super(Invasion, self).__init__(**kwargs)
297\. self._keyboard = Window.request_keyboard(self.close, self)
298\. self._keyboard.bind(on_key_down=self.press)
399\. self.start_game()
300\.
301\. def close(self):
302\. self._keyboard.unbind(on_key_down=self.press)
303\. self._keyboard = None
304\.
305\. def press(self, keyboard, keycode, text, modifiers):
306\. if keycode[1] == 'left':
307\. self.shooter.center_x -= 30
308\. elif keycode[1] == 'right':
309\. self.shooter.center_x += 30
310\. return True
311\.
312\. def start_game(self):
313\. label = Label(text='Ready!')
314\. animation = Animation (font_size = 72, d=2)
315\. animation.bind(on_complete=self.fleet.start_attack)
316\. animation.bind(on_complete=self.shooter.start)
317\. self.add_widget(label)
318\. animation.start(label)
上一段代码展示了键盘事件控制。__init__构造函数(第 295 至 299 行)将请求keyboard(第 297 行)到Window,并将on_keyboard_down事件绑定到press方法。Window._request_keyboard方法的一个重要参数是当keyboard关闭时调用的方法(第 301 至 303 行)。键盘可以关闭的原因有很多,包括当另一个小部件请求它时。press方法(第 305 至 310 行)负责处理键盘输入,即按下的键。按下的键保存在keycode参数中,并在第 306 和 308 行使用,以决定射手应该向左还是向右移动。
注意
游戏中的键盘绑定是为了在没有多触控功能的设备上进行测试。如果您想在您的移动设备上尝试它,您应该注释掉第 297 和 298 行以禁用键盘绑定。
第 299 行调用了start_game方法(第 312 至 318 行)。该方法显示带有文本Ready!的Label。请注意,我们在第 314 行将一个Animation实例应用于font_size。到目前为止,我们一直在使用动画通过x、y或pos属性移动小部件。然而,动画可以与任何支持算术运算的属性一起工作(例如,String不支持此类运算;作为一个反例)。例如,我们可以使用它们来动画化Scatter的旋转或缩放。当动画完成后,它将同时启动舰队和射手(第 315 和 316 行)。注意我们如何将两个方法绑定到同一个事件。
小贴士
我们可以将任意数量的方法绑定到事件上。
在下一节中,我们将讨论如何按顺序或同时动画化多个属性。
使用+和&结合动画
您已经了解到,您可以将多个属性添加到同一个动画中,以便它们一起修改(在ammo.py的第 69 行)。
注意
我们可以通过使用+和&运算符来组合动画。+运算符用于创建顺序动画(一个接一个)。&运算符允许我们同时执行两个动画。
以下代码是main.py的片段 2,并展示了这两个运算符的使用:
319\. # File name: main.py (Fragment 2)
320\. def end_game(self, message):
321\. label = Label(markup=True, size_hint = (.2, .1),
322\. pos=(0,self.parent.height/2), text = message)
323\. self.add_widget(label)
324\. self.composed_animation().start(label)
325\.
326\. def composed_animation(self):
327\. animation = Animation (center=self.parent.center)
328\. animation &= Animation (font_size = 72, d=3)
329\. animation += Animation(font_size = 24,y=0,d=2)
330\. return animation
331\.
332\. class InvasionApp(App):
333\. def build(self):
334\. return Invasion()
335\.
336\. if __name__=="__main__":
337\. InvasionApp().run()
end_game方法(第 320 至 324 行)显示一条最终消息,以指示游戏如何结束(在fleet.py的第 219 行显示You Win或在shooter.py的第 267 行显示Game Over)。该方法使用composed_animation方法(第 326 至 330 行)创建一个组合的Animation,在其中我们使用所有可能的组合动画方式。第 327 行是一个简单的Animation,它通过&运算符与另一个不同持续时间的简单Animation同时执行。在第 329 行,一个包含两个属性(font_size和y)的Animation通过+运算符附加到之前的一个上。
生成的动画执行以下操作:将消息从左侧移动到中间需要一秒钟,同时字体大小在增加。当它到达中间时,字体大小的增加持续两秒钟。一旦字体达到完全大小(72 点),消息移动到底部,同时字体大小以相同的速度减小。以下图表说明了整个动画序列:

'+'运算符类似于我们在将Animation on_complete事件绑定到创建另一个Animation的方法时所做的操作:animation.bind(on_complete = self.to_dock)(invader.py的第 110 行)。区别在于,当我们使用'+'运算符时,没有机会创建无限循环,就像我们在*fleet*中做的那样,或者在进行另一个动画之前更改Widget属性。例如,在*invader*的情况下,我们在将其带回到*dock*的动画之前(第 124 到 126 行),将*invader*移动到屏幕的顶部中心(第 122 和 123 行):
121\. def to_dock(self, instance, value):
122\. self.y = Window.height
123\. self.center_x = Window.width/2
124\. animation = Animation(pos=self.parent.pos, d=2)
125\. animation.bind(on_complete = self.parent.bind_invader)
126\. animation.start(self)
&运算符类似于将两个属性作为Animation的参数发送,就像我们在第 69 行所做的那样:self.animation = Animation(x=tx, top=ty)。将两个属性作为参数发送的区别在于,它们共享相同的持续时间和过渡效果,而在第 328 行,我们改变了第二个属性的持续时间。
这里有一张最后的截图,展示了入侵者最终复仇的场景:

概述
本章涵盖了整个交互式和动画应用程序的构建过程。你学习了如何集成各种 Kivy 组件,现在你应该能够轻松地构建一个 2D 动画游戏。
让我们回顾一下本章中使用的所有新类和组件:
-
Atlas -
Image:source属性 -
SoundLoader和Sound: 分别是load和play方法 -
Window:高度和宽度属性,以及request_keyboard、remove_widget和add_widget方法 -
Animation: 作为参数的属性;d和t参数;start、stop和bind方法;on_start、on_progress和on_complete事件;以及'+'和'&'运算符 -
Touch:ud属性 -
Clock:schedule_interval和schedule_once方法 -
Keyboard:bind和unbind方法,on_key_down事件
本章包含的信息提供了你可以用来开发高度交互式应用程序的工具和策略。通过将前几章的信息与本章对属性、绑定事件和 Kivy 语言进一步理解的洞察相结合,你应该能够快速开始使用 Kivy API 的所有其他组件(kivy.org/docs/api-kivy.html)。
最后一章,第六章, Kivy Player – TED 视频流媒体,本书将教你如何控制多媒体组件,特别是视频和音频。它将提供一个额外的示例,以便展示更多 Kivy 组件,但更重要的是,它将教你如何构建一个看起来更专业的界面。它还将介绍一些 Kivy 工具来调试我们的应用程序。
第六章. Kivy 播放器 – TED 视频流器
在本章中,我们将学习如何搜索、显示和控制视频。我们将整合前几章的知识,构建一个能够适应不同屏幕并最大化空间使用的响应式应用程序。我们将制作一个带有控件和字幕支持的增强型视频小部件,并学习如何显示来自 TED API 服务的搜索结果查询(developer.ted.com)。以下是本章我们将涵盖的主要主题:
-
控制流媒体视频的播放进度
-
使用视频的进度在正确的时间显示字幕
-
应用策略和组件使我们的应用程序响应
-
显示和导航本地文件目录树
-
使用 Kivy 检查器调试我们的应用程序
-
向从互联网查询得到的列表结果添加滚动功能
本章总结了迄今为止获得的大量知识。我们将回顾和结合使用属性、事件、动画、触摸、行为、布局,甚至图形。同时,我们将介绍新的小部件,这些小部件将补充您的知识,并作为新编程情况的好例子。我们还将回顾 Kivy 检查器,它将帮助我们检测 GUI 错误。在本章结束时,我们将完成一个看起来专业的界面。
视频 – 播放、暂停和停止
在本节中,我们将从简单的代码开始,然后逐步添加功能,直到我们得到一个完整的视频播放器。在本节中,我们将讨论如何使用Video小部件从互联网上流式传输视频。让我们从video.kv文件中的代码开始:
1\. # File name: video.kv
2\. #:set _default_video "http://video.ted.com/talk/stream/2004/None/DanGilbert_2004-180k.mp4"
3\.
4\. <Video>:
5\. allow_stretch: True
6\. color: 0,0,0,0
7\. source: _default_video
在此代码中,我们最初使用set指令创建一个常量值(第 2 行)。此指令允许我们在 Kivy 语言范围内使用全局值。例如,我们使用_default_video常量的值设置Video类的source属性(第 7 行)。
我们为Video类设置了三个属性。allow_stretch属性(第 5 行)允许视频根据可用的屏幕大小进行拉伸。color属性(第 6 行)将使视频变黑,当视频未播放时用作前景(以及封面图像的背景)。source属性(第 7 行)包含我们想要播放的视频的 URL(或文件名)。这三个属性实际上属于Image小部件,它是Video的基础类。如果我们把视频看作是一系列图像(伴随声音),这就有意义了。
小贴士
为了测试目的,如果您想避免不断从互联网下载视频(或者如果 URL 已经不可用),您可以将default_video中的 URL 替换为与代码一起提供的示例文件:samples/BigBuckBunny.ogg。
我们将使用 Factory 类来使用我们在 第四章 中学到的技术,改进用户体验。当时,我们使用 Factory 类来替换 Line 顶点指令,使用我们的个性化实现,一个滚动的 Line。
注意
Factory 类遵循一种面向对象的软件设计模式,称为工厂模式。工厂模式根据调用标识符(通常是方法)返回默认的新对象(实例)的子集类。在 Kivy 语言的情况下,我们只使用一个名称。(en.wikipedia.org/wiki/Factory_%28object-oriented_programming%29)。
我们现在将做类似的事情,但这次我们将个性化我们的 Video 小部件:
8\. # File name: video.py
9\. from kivy.uix.video import Video as KivyVideo
10\.
11\. from kivy.factory import Factory
12\. from kivy.lang import Builder
13\.
14\. Builder.load_file('video.kv')
15\.
16\. class Video(KivyVideo):
17\.
18\. def on_state(self, instance, value):
19\. if self.state == 'stop':
20\. self.seek(0)
21\. return super(self.__class__, self).on_state(instance, value)
22\.
23\. def on_eos(self, instance, value):
24\. if value:
25\. self.state = 'stop'
26\.
27\. def _on_load(self, *largs):
28\. super(self.__class__, self)._on_load(largs)
29\. self.color = (1,1,1,1)
30\.
31\. def on_source(self, instance, value):
32\. self.color = (0, 0, 0, 0)
33\.
34\. Factory.unregister('Video')
35\. Factory.register('Video', cls=Video)
video.py 文件将导入 Kivy 的 Video 小部件,并使用别名 KivyVideo(第 9 行)。现在我们将能够使用 Video 类名称(而不是不那么吸引人的替代名称,如 MyVideo)创建我们的个性化小部件(第 16 行至 32 行)。在文件末尾,我们将默认的 Video 小部件替换为我们的个性化 Video 并将其添加到 Factory 中(第 34 行和 35 行)。从现在起,Kivy 语言中引用的 Video 类将对应于我们在此文件中的实现。
我们在 Video 类中创建了四个方法(on_state、on_eos、_on_load 和 on_source)。所有这些都对应于事件:
-
当视频在其三种可能状态(播放
'play')、暂停 ('pause') 或停止 ('stop') 之间改变状态时,会调用on_state方法(第 18 行)。我们确保当视频停止时,使用seek方法(第 20 行)将其重新定位到开始位置。 -
当达到 流结束(EOS)时,将调用
on_eos方法(第 23 行)。当发生这种情况时,我们将确保将状态设置为stop(第 19 行)。 -
我们还需要记住,我们使用 Kivy 语言的
color属性(第 6 行)将视频染成了黑色。因此,我们需要在视频上放置亮光(1,1,1,1)才能看到它(第 29 行)。当视频被加载到内存中并准备好播放时,会调用_on_load方法(第 27 行)。我们使用此方法来设置适当的(以及原始 Kivy 默认的)color属性。注意
记住 第二章,图形 – 画布 中,
Image小部件(Video类的基类)的color属性在显示上充当着染色或光照。对于Video小部件,也会发生同样的效果。 -
最后,从
Image类继承的on_source方法将在更改视频源时在视频上方恢复黑色染色。
让我们继续创建一个 kivyplayer.py 文件来执行我们的应用程序,并播放、暂停和停止我们的视频:
36\. # File name: kivyplayer.py
37\. from kivy.app import App
38\.
39\. from video import Video
40\.
41\. class KivyPlayerApp(App):
42\.
43\. def build(self):
44\. self.video = Video()
45\. self.video.bind(on_touch_down=self.touch_down)
46\. return self.video
47\.
48\. def touch_down(self, instance, touch):
49\. if self.video.state == 'play':
50\. self.video.state = 'pause'
51\. else:
52\. self.video.state = 'play'
53\. if touch.is_double_tap:
54\. self.video.state = 'stop'
55\.
56\. if __name__ == "__main__":
57\. KivyPlayerApp().run()
目前,我们将通过触摸来控制视频。在build方法(第 43 行)中,我们将视频的on_touch_down事件(第 45 行)绑定到touch_down方法(第 48 至 54 行)。一次触摸将根据视频当前的state属性(第 49 和 52 行)播放或暂停视频。状态属性控制视频是否处于三种可能状态之一。如果它在播放,则将其暂停;否则(暂停或停止),则播放它。我们将使用double_tap键来表示双击(双击或双击),以停止视频。下次我们触摸屏幕时,视频将从开头开始。现在,运行应用程序(Python kivyplayer.py),看看当你点击屏幕时,Kivy 是如何立即开始从 TED 流式传输 Dan Gilbert 的视频《幸福的惊人科学》的(www.ted.com/):

AsyncImage – 为视频创建封面
在本节中,我们将学习如何设置一个将在视频不播放时显示的封面。当视频尚未开始时,此图像将作为装饰,在 TED 视频中,通常涉及演讲者的图像。让我们从在video.kv代码中引入一些更改开始:
58\. # File name: video.kv
59\. ...
60\. #:set _default_image "http://images.ted.com/images/ted/016a827cc0757092a0439ab2a63feca8655b6c29_1600x1200.jpg"
61\.
62\. <Video>:
63\. cover: _cover
64\. image: _default_image
65\. ...
66\. AsyncImage:
67\. id: _cover
68\. source: root.image
69\. size: root.width,root.height
在此代码中,我们使用set指令(第 60 行)创建了一个另一个常量(_default_image),并为Video类创建了一个相关属性(image),该属性引用了常量(第 64 行)。我们还创建了cover属性(第 63 行),以引用我们添加到Video类中的AsyncImage(第 66 行),它将作为视频的封面。
注意
Image和AsyncImage之间的主要区别在于,AsyncImage小部件允许我们在图片加载时继续使用程序,而不是在图片完全下载之前阻塞应用程序。
这很重要,因为我们从互联网上下载图像,它可能是一个大文件。当你运行代码时,你会注意到在图像加载时会出现一个等待的图像:

我们还设置了一些AsyncImage属性。我们使用新属性(root.image)(第 68 行)初始化了source属性,该属性我们在Video小部件中创建,以引用封面图像(第 64 行)。请记住,这将内部绑定属性,这意味着每次我们更改image属性时,source属性都将更新为相同的值。第 69 行重复了相同的思想,以保持封面的size属性与视频的尺寸相等。
小贴士
为了测试目的,您可以将default_image中的 URL 替换为代码中包含的以下示例文件:
samples/BigBuckBunny.png。
我们将对我们的视频小部件进行一些修改,以确保在播放视频时封面被移除(隐藏):
70\. # File name: video.py
71\. ...
72\. from kivy.properties import ObjectProperty
73\. ...
74\. class Video(KivyVideo):
75\. image = ObjectProperty(None)
76\.
77\. def on_state(self, instance, value):
78\. if self.state == 'play':
79\. self.cover.opacity = 0
80\. elif self.state == 'stop':
81\. self.seek(0)
82\. self.cover.opacity = 1
83\. return super(self.__class__, self).on_state(instance, value)
84\.
85\. def on_image(self, instance, value):
86\. self.cover.opacity = 1
87\. ...
我们将 on_state 方法改为在视频播放时(第 79 行)揭示视频,并在视频停止时(第 82 行)再次使用 不透明度 属性覆盖它。
提示
避免移除在 .kv 文件中声明的部件。大多数情况下,这些部件与其他部件(例如,属性边界)有内部边界,并可能导致与缺失内部引用和不一致的边界属性相关的意外运行时错误。
与移除部件相比,有几种替代方案;例如,首先,使用 opacity 属性使部件不可见,其次,使用 size 属性将部件区域设置为零(size = (0,0)),最后,使用 pos 属性将部件放置在一个永远不会显示的位置(pos= (99999,999999))。我们选择了第一种方法;在这种情况下,这是最优雅的。我们将 AsyncImage 的 opacity 属性设置为使其可见(opacity = 1)或不可见(opacity = 0)。
提示
尽管使用不透明度控制覆盖以使其不可见可能是这里最优雅的解决方案,但你必须小心,因为部件仍然存在,占据了屏幕上的空间。根据情况,你可能需要扩展策略。例如,如果部件捕获了一些触摸事件,你可以结合 opacity 和 disabled 属性来隐藏和禁用部件。
我们还创建了 image 属性(第 75 行),并使用其 on_image 关联事件(第 85 行)确保在更改图像时恢复不透明度(第 86 行)。现在,当你运行应用程序时(python kivyplayer.py),将出现丹·吉尔伯特的图像。
字幕 – 跟踪视频进度
让我们在我们的应用程序中添加字幕。我们将通过四个简单的步骤来完成这项工作:
-
创建一个从
Label类派生的Subtitle部件(subtitle.kv),用于显示字幕 -
在视频部件上方放置一个
Subtitle实例(video.kv) -
创建一个
Subtitles类(subtitles.py),用于读取和解析字幕文件 -
跟踪
Video进度(video.py)以显示相应的字幕
步骤 1 包括在 subtitle.kv 文件中创建一个新的部件:
88\. # File name: subtitle.kv
89\. <Subtitle@Label>:
90\. halign: 'center'
91\. font_size: '20px'
92\. size: self.texture_size[0] + 20, self.texture_size[1] + 20
93\. y: 50
94\. bcolor: .1, .1, .1, 0
95\. canvas.before:
96\. Color:
97\. rgba: self.bcolor
98\. Rectangle:
99\. pos: self.pos
100\. size: self.size
这段代码中有两个有趣的元素。第一个是大小属性的定义(第 92 行)。我们将其定义为比 texture_size 宽度和高度大 20 像素。texture_size 属性表示由字体大小和文本确定的文本大小,我们使用它来调整 Subtitles 部件的大小以适应其内容。
注意
texture_size 是一个只读属性,因为它的值是根据其他参数计算的,例如字体大小和文本显示的高度。这意味着我们将从这个属性中读取,但不会在其上写入。
第二个元素是创建bcolor属性(第 94 行)以存储背景颜色,以及如何将矩形的rgba颜色绑定到它(第 97 行)。Label小部件(像许多其他小部件一样)没有背景颜色,创建一个矩形是创建此类功能的常用方法。我们添加bcolor属性是为了从实例外部更改矩形的颜色。
提示
我们不能直接修改顶点指令的参数;然而,我们可以创建控制顶点指令内部参数的属性。
让我们继续之前提到的第 2 步。我们需要在video.kv文件中向当前的Video小部件添加一个Subtitle实例:
101\. # File name: video.kv
102\. ...
103\. #:set _default_surl "http://www.ted.com/talks/subtitles/id/97/lang/en"
104\.
105\. <Video>:
106\. surl: _default_surl
107\. slabel: _slabel
108\. ...
109\.
110\. Subtitle:
111\. id: _slabel
112\. x: (root.width - self.width)/2
我们添加了一个名为_default_surl的另一个常量变量(第 103 行),其中包含对应 TED 视频字幕文件的 URL 链接。我们将此值设置为surl属性(第 106 行),这是我们刚刚创建的用于存储字幕 URL 的属性。我们添加了slabel属性(第 107 行),通过其 ID 引用Subtitle实例(第 111 行)。然后我们确保字幕居中(第 112 行)。
为了开始第 3 步(解析字幕文件),我们需要查看 TED 字幕的格式:
113\. {
114\. "captions": [{
115\. "duration":1976,
116\. "content": "When you have 21 minutes to speak,",
117\. "startOfParagraph":true,
118\. "startTime":0,
119\. }, ...
TED 使用一个非常简单的 JSON 格式(en.wikipedia.org/wiki/JSON),其中包含一个字幕列表。每个caption包含四个键,但我们只会使用duration、content和startTime。我们需要解析这个文件,幸运的是,Kivy 提供了一个UrlRequest类(第 121 行),它将为我们完成大部分工作。以下是创建Subtitles类的subtitles.py代码:
120\. # File name: subtitles.py
121\. from kivy.network.urlrequest import UrlRequest
122\.
123\. class Subtitles:
124\.
125\. def __init__(self, url):
126\. self.subtitles = []
127\. req = UrlRequest(url, self.got_subtitles)
128\.
129\. def got_subtitles(self, req, results):
130 self.subtitles = results['captions']
131\.
132\. def next(self, secs):
133\. for sub in self.subtitles:
134\. ms = secs*1000 - 12000
135\. st = 'startTime'
136\. d = 'duration'
137\. if ms >= sub[st] and ms <= sub[st] + sub[d]:
138\. return sub
139\. return None
Subtitles类的构造函数将接收一个 URL(第 125 行)作为参数。然后,它将发出请求以实例化UrlRequest类(第 127 行)。类实例化的第一个参数是请求数据的 URL,第二个参数是当请求数据返回(下载)时调用的方法。一旦请求返回结果,就会调用got_subtitles方法(第 129 行)。UrlRequest提取 JSON 并将其放置在got_subtitles的第二个参数中。我们只需将字幕放入一个类属性中,我们称之为subtitles(第 130 行)。
next方法(第 132 行)接收秒数(secs)作为参数,并将遍历加载的 JSON 字典以查找对应于该时间的字幕。一旦找到,该方法就返回它。我们减去了12000微秒(第 134 行,ms = secs*1000 - 12000),因为 TED 视频在演讲开始前大约有 12 秒的介绍。
一切都为第 4 步做好了准备,在这一步中,我们将所有部件组合起来以查看字幕是否工作。以下是video.py文件头部的修改:
140\. # File name: video.py
141\. ...
142\. from kivy.properties import StringProperty
143\. ...
144\. from kivy.lang import Builder
145\.
146\. Builder.load_file('subtitle.kv')
147\.
148\. class Video(KivyVideo):
149\. image = ObjectProperty(None)
150\. surl = StringProperty(None)
我们导入了StringProperty并添加了相应的属性(第 142 行)。在本章结束时,我们将使用此属性在 GUI 中切换 TED 演讲。目前,我们只需使用在video.kv中定义的_default_surl(第 150 行)。我们还加载了subtitle.kv文件(第 146 行)。现在,让我们分析对video.py文件的其他更改:
151\. ...
152\. def on_source(self, instance, value):
153\. self.color = (0,0,0,0)
154\. self.subs = Subtitles(name, self.surl)
155\. self.sub = None
156\.
157\. def on_position(self, instance, value):
158\. next = self.subs.next(value)
159\. if next is None:
160\. self.clear_subtitle()
161\. else:
162\. sub = self.sub
163\. st = 'startTime'
164\. if sub is None or sub[st] != next[st]:
165\. self.display_subtitle(next)
166\.
167\. def clear_subtitle(self):
168\. if self.slabel.text != "":
169\. self.sub = None
170\. self.slabel.text = ""
171\. self.slabel.bcolor = (0.1, 0.1, 0.1, 0)
172\.
173\. def display_subtitle(self, sub):
174\. self.sub = sub
175\. self.slabel.text = sub['content']
176\. self.slabel.bcolor = (0.1, 0.1, 0.1, .8)
177\. (...)
我们在on_source方法中添加了一些代码行,以便使用surl属性初始化字幕属性为Subtitles实例(第 154 行),并初始化包含当前显示字幕的sub属性(如果有的话)(第 155 行)。
现在,让我们研究如何跟踪进度以显示相应的字幕。当视频在Video小部件内播放时,每秒都会触发on_position事件。因此,我们在on_position方法(第 157 至 165 行)中实现了显示字幕的逻辑。每次调用on_position方法(每秒),我们都会要求Subtitles实例(第 158 行)提供下一个字幕。如果没有返回任何内容,我们使用clear_subtitle方法(第 160 行)清除字幕。如果当前秒已经有一个字幕(第 161 行),那么我们确保没有字幕正在显示,或者返回的字幕不是我们已显示的字幕(第 164 行)。如果条件满足,我们使用display_subtitle方法(第 165 行)显示字幕。
注意到clear_subtitle(第 167 至 171 行)和display_subtitle(第 173 至 176 行)方法使用bcolor属性来隐藏字幕。这是在不从其父元素中删除的情况下使小部件不可见的另一个技巧。让我们看看以下屏幕截图中我们的视频和字幕的当前结果:

控制栏 – 添加按钮以控制视频
在本节中,我们将处理用户与应用程序的交互。目前,我们通过在屏幕上触摸来控制视频,实现播放、暂停和停止视频。然而,这对我们应用程序的新用户来说并不直观。因此,让我们添加一些按钮来提高我们应用程序的可用性。
我们将使用带有ToggleButtonBehaviour和ToggleBehaviour类的Image小部件来创建播放/暂停按钮和停止按钮。以下是本节中将实现的简单控制栏的截屏:

让我们从定义我们的两个controlbar.kv小部件开始。我们将逐一介绍每个小部件。让我们从文件的标题和ControlBar类定义开始:
178\. # File name: controlbar.kv
179\. <ControlBar@GridLayout>:
180\. rows: 1
181\. size_hint: None, None
182\. pos_hint: {'right': 1}
183\. padding: [10,0,0,0]
184\. play_pause: _play_pause
185\. progress: 0
我们从GridLayout类派生了ControlBar类,并设置了一些熟悉的属性。我们还创建了对播放/暂停按钮的引用,以及一个新属性(progress),它将跟踪视频的进度百分比(从 0 到 1)。让我们继续处理第一个嵌入的小部件,VideoPlayPause:
186\. VideoPlayPause:
187\. id: _play_pause
188\. start: 'atlas://data/images/defaulttheme/media-playback-start'
189\. pause: 'atlas://data/images/defaulttheme/media-playback-pause'
190\. size_hint: [None, None]
191\. width: 44
192\. source: self.start if self.state == 'normal' else self.pause
正如我们在controlbar.py中将要看到的,VideoPlayPause是Image和ToggleButtonBehavior的组合。我们以这种方式实现了source属性(第 192 行),即根据state属性的变化(normal和down)改变小部件的图像。现在让我们看看VideoStop的代码:
193\. VideoStop:
194\. size_hint: [None, None]
195\. width: 44
196\. source: 'atlas://data/images/defaulttheme/media-playback-stop'
197\. on_press: self.stop(root.parent.video, _play_pause)
除了定义一些熟悉的属性外,我们还把事件on_press绑定到了stop方法(第 197 行),这将在相应的controlbar.py文件中展示。请注意,我们假设根的父元素包含对视频的引用(root.parent.video)。我们将在controlbar.py中继续这个假设:
198\. # File name: controlbar.py
199\. from kivy.uix.behaviors import ButtonBehavior, ToggleButtonBehavior
200\. from kivy.uix.image import Image
201\. from kivy.lang import Builder
202\.
203\. Builder.load_file('controlbar.kv')
204\.
205\. class VideoPlayPause(ToggleButtonBehavior, Image):
206\. pass
207\.
208\. class VideoStop(ButtonBehavior, Image):
209\.
210\. def stop(self, video, play_pause):
211\. play_pause.state = 'normal'
212\. video.state = 'stop'
此代码导入了必要的类以及'controlbar.kv'(第 198 到 203 行)。然后,使用多重继承,它将VideoPlayPause和VideoStop类定义为Image类和适当行为的组合(第 205 和 208 行)。VideoStop类包含一个stop方法,当按钮被按下时调用(第 208 行)。这将把播放/暂停按钮状态设置为正常并停止视频(第 212 行)。
我们还将在videocontroller.kv文件中定义一个视频控制器,它将是控制栏和视频的父元素:
213\. # File name: videocontroller.kv
214\. <VideoController >:
215\. video: _video
216\. control_bar: _control_bar
217\. play_pause: _control_bar.play_pause
218\. control_bar_width: self.width
219\. playing: _video.state == 'play'
220\.
221\. Video:
222\. id: _video
223\. state: 'pause' if _control_bar.play_pause.state == 'normal' else 'play'
224\.
225\. ControlBar:
226\. id: _control_bar
227\. width: root.control_bar_width
228\. progress: _video.position / _video.duration
首先,我们为VideoContoller定义了五个属性(第 215 到 219 行):video、control_bar、play_pause、control_bar_width和playing。前三个属性引用界面组件,control_bar_width将用于外部控制控制栏的宽度,而playing属性将指示视频是否正在播放(第 219 行)。
然后,我们添加了一个Video实例(第 221 行),其状态将取决于播放/暂停按钮的状态(第 223 行),以及一个ControlBar实例。控制栏的width属性将由我们之前创建的control_bar_width(第 227 行)控制,而progress属性将以持续时间的百分比表示(第 228 行)。
现在,我们需要在各自的videocontroller.py文件中创建VideoController类:
229\. # File name: videocontroller.py
230\. from kivy.uix.floatlayout import FloatLayout
231\. from kivy.lang import Builder
232\.
233\. import video
234\. import controlbar
235\.
236\. Builder.load_file('videocontroller.kv')
237\.
238\. class VideoController(FloatLayout):
239\. pass
我们只包含了必要的导入,并将VideoController定义为FloatLayout的派生类。kivyplayer.py文件也必须更新,以便显示VideoController实例而不是Video:
240\. # File name: kivyplayer.py
241\. from kivy.app import App
242\. from videocontroller import VideoController
243\.
244\. class KivyPlayerApp(App):
245\. def build(self):
246\. return VideoController()
247\.
248\. if __name__=="__main__":
249\. KivyPlayerApp().run()
随意再次运行应用程序以测试播放/暂停和停止按钮。下一节将向我们的应用程序引入进度条。
滑块 - 包括进度条
在本节中,我们将介绍一个新的小部件,称为 Slider。这个小部件将作为 进度条,同时允许用户快进和倒退视频。我们将 进度条 集成到 控制栏 中,如下面的裁剪截图所示:

如您所见,Slider 出现在 播放/暂停 和 停止 按钮的左侧。让我们将 controlbar.kv 修改为添加 Slider 以反映这种顺序。让我们从文件的标题和 ControlBar 类的定义开始:
250\. # File name: controlbar.kv
251\. <ControlBar@GridLayout>:
252\. ...
253\. VideoSlider:
254\. value: root.progress
255\. max: 1
256\. VideoPlayPause:
257\. ...
VideoSlider 将使用视频的进度来更新其 value 属性。value 属性表示滑块在条上的位置,而 max 属性是它可以取的最大值。在这种情况下,1 是合适的,因为我们用从 0 到 1 的百分比(表示持续时间)来表示进度(第 255 行)。
现在我们将 VideoSlider 的定义添加到 controlbar.py 文件中:
258\. # File name: controlbar.py
259\. ...
260\. class VideoSlider(Slider):
261\.
262\. def on_touch_down(self, touch):
263\. video = self.parent.parent.video
264\. if self.collide_point(*touch.pos):
265\. self.prev_state = video.state
266\. self.prev_touch = touch
267\. video.state = 'pause'
268\. return super(self.__class__, self).on_touch_down(touch)
269\.
270\. def on_touch_up(self, touch):
271\. if self.collide_point(*touch.pos) and \
272\. hasattr(self, 'prev_touch') and \
273\. touch is self.prev_touch:
274\. video = self.parent.parent.video
275\. video.seek(self.value)
276\. if prev_state != 'stop':
277\. video.state = self.prev_state
278\. return super(self.__class__, self).on_touch_up(touch)
使用滑块控制视频的进度很棘手,因为视频和滑块需要不断互相更新。视频通过更新滑块来指示其进度,而当用户想要快进或倒退视频时,滑块会更新视频。这创建了一个复杂的逻辑,我们必须考虑以下因素:
-
我们需要使用触摸事件,因为我们想确保是用户在移动滑块,而不是视频进度。
-
似乎存在一个无限循环;我们更新了滑块,滑块上传了视频,然后视频又更新了滑块。
-
用户可能不仅会点击滑块,还可能拖动它,在拖动过程中,视频会再次更新滑块。
由于这些原因,我们需要执行以下步骤:
-
在更新进度之前暂停视频(第 267 行)。
-
不要直接使用值属性更新滑块,而是使用
seek方法(第 275 行)更新视频进度。 -
使用两个事件
on_touch_down(第 262 行)和on_touch_up(第 270 行),以便安全地更改视频的进度百分比。
在on_touch_down方法(第 262 至 268 行)中,我们存储了视频的当前状态(第 265 行),以及触摸的引用(第 266 行),然后我们暂停了视频(第 267 行)。如果我们不暂停视频,在更新视频到滑块的进度之前,视频的进度可能会影响滑块(记住滑块的value在第 254 行绑定到progression属性)。在on_touch_up事件中,我们确保触摸实例与我们在on_touch_down方法中存储的实例相对应(第 272 和 273 行)。然后,我们使用seek方法(第 275 行)根据滑块的位置设置视频到正确的位置。最后,如果视频的状态与stop不同,我们重新建立视频的先前状态(第 276 和 277 行)。
随意再次运行应用程序。您还可以通过不同的选项和滑块来更新视频进行实验。例如,尝试在拖动滑块通过on_touch_move事件时实时更新。
动画 – 隐藏小部件
在本节中,我们将使控制栏在视频开始播放时消失,以便在没有视觉干扰的情况下观看视频。我们需要更改videocontroller.py文件以动画化ControlBar实例:
279\. # File name: videocontroller.py
280\. from kivy.animation import Animation
281\. from kivy.properties import ObjectProperty
282\. ...
283\. class VideoController(FloatLayout):
284\. playing = ObjectProperty(None)
285\.
286\. def on_playing(self, instance, value):
287\. if value:
288\. self.animationVB = Animation(top=0)
289\. self.control_bar.disabled = True
290\. self.animationVB.start(self.control_bar)
291\. else:
292\. self.play_pause.state = 'normal'
293\. self.control_bar.disabled = False
294\. self.control_bar.y = 0
295\.
296\. def on_touch_down(self, touch):
297\. if self.collide_point(*touch.pos):
298\. if hasattr(self, 'animationVB'):
299\. self.animationVB.cancel(self.control_bar)
300\. self.play_pause.state = 'normal'
301\. return super(self.__class__, self).on_touch_down(touch)
除了文件开头必要的导入(第 280 和 281 行)之外,我们还引入了playing属性(第 284 行)以及与on_playing事件和on_touch_down事件相关的两个方法。playing属性已经在 Kivy 语言中定义(第 219 行),但请记住,由于文件解析顺序,如果我们想在同一类中使用该属性,我们还需要在 Python 语言中定义它。
当playing属性改变时,会触发on_playing事件(第 286 行)。此方法开始一个动画(第 290 行)并在视频播放时禁用控制栏(第 289 行)。动画将隐藏屏幕底部的控制栏。当视频不播放时,on_playing方法也会恢复控制栏(第 292 至 294 行),使其再次可见。
由于控制栏将在视频播放时隐藏,我们需要一种替代方法来停止视频(不同于停止按钮)。这就是为什么我们包括了on_touch_down事件(第 296 行)。一旦我们触摸屏幕,如果存在动画,动画将被取消(第 298 行),并将播放/暂停按钮设置为'normal'(第 300 行)。这将暂停视频并因此触发我们刚刚定义的on_playing事件(在这种情况下,因为它停止了播放)。
您现在可以再次运行应用程序并欣赏当我们按下播放/暂停按钮时,控制栏如何缓慢地消失到屏幕底部。
Kivy 检查器 – 调试界面
有时,我们在实现我们的界面时会遇到问题,特别是当许多部件没有图形显示时,理解出了什么问题可能很困难。在本节中,我们将使用本章中创建的应用程序来介绍 Kivy 检查器,这是一个简单的调试界面的工具。为了启动检查器,您运行以下命令:python kivyplayer.py –m inspector。一开始您可能不会注意到任何区别,但如果您按下Ctrl + E,屏幕底部将出现一个栏,就像以下图像左图中的那样:

如果我们按下移至顶部按钮(从左到右数第一个),则栏将移动到屏幕顶部,正如您在右图中所见,这对于我们的特定应用来说是一个更方便的位置。第二个按钮检查激活或关闭检查器行为。我们现在可以通过点击来高亮显示组件。
例如,如果您点击播放/暂停按钮,视频将不会播放;相反,按钮将以红色高亮显示,正如您在下面的左图中所见:

此外,如果我们想可视化当前高亮的部件,我们只需按下父级按钮(从左到右数第三个)。在右图中,您可以看到控制栏(播放/暂停按钮的父级)被高亮显示。您还应该注意,长按钮(从左到右数第四个)显示了高亮实例所属的类。如果我们点击此按钮,将显示该部件的所有属性列表,如以下左图所示:

最后,当我们选择一个属性时,我们能够修改它。例如,在右图中,我们修改了控制栏的宽度属性,我们可以看到控制栏立即调整到这些变化。
请记住,由于 Kivy 部件尽可能简单,这意味着很多时候它们是不可见的,因为更复杂的图形显示意味着不必要的过载。然而,这种行为使得我们难以在 GUI 中找到错误。所以当我们的界面没有显示我们期望的内容时,检查器就变得非常有用,帮助我们理解 GUI 的底层树结构。
ActionBar – 一个响应式栏
Kivy 1.8.0 引入了一套新的小部件,它们都与 ActionBar 小部件相关。这个小部件类似于 Android 的操作栏。这不仅会给您的应用程序带来现代和专业的外观,而且还包括更多细微的特性,如对小型屏幕的响应性。根据 ActionBar 小部件的层次结构和组件,不同的小部件将折叠以适应设备中可用的屏幕空间。首先,让我们看看我们计划中的 ActionBar 的最终结果:

我们将 Kivy 语言代码添加到新文件 kivyplayer.kv 中,如下所示:
302\. # File name: kivyplayer.kv
303\.
304\. <KivyPlayer>:
305\. list_button: _list_button
306\. action_bar: _action_bar
307\. video_controller: _video_controller
308\.
309\. VideoController:
310\. id: _video_controller
311\. on_playing: root.hide_bars(*args)
312\.
313\. ActionBar:
314\. id: _action_bar
315\. top: root.height
316\. ActionView:
317\. use_separator: True
318\. ActionListButton:
319\. id: _list_button
320\. root: root
321\. title: 'KPlayer'
322\. ActionToggleButton:
323\. text: 'Mute'
324\. on_state: root.toggle_mute(*args)
325\. ActionGroup:
326\. text: 'More Options...'
327\. ActionButton:
328\. text: 'Open List'
329\. on_release: root.show_load_list()
330\. ActionTextInput:
331\. on_text_validate: root.search(self.text)
之前代码的层次结构很复杂,因此它也以以下图的形式呈现:

如您在前面的图中所见,KivyPlayer 包含两个主要组件,我们在上一节中创建的 VideoController 以及 ActionBar。如果您还记得,我们为 VideoController 创建了 playing 属性(第 219 行),并将其关联事件 on_playing 绑定到 hide_bars 方法(第 311 行),该方法将基本隐藏操作栏。现在,让我们将注意力集中在 ActionBar 的层次结构上。
ActionBar 总是包含一个 ActionView。在这种情况下,我们添加了一个包含三个小部件的 ActionView:ActionListButton、ActionToggleButton 和 ActionGroup。所有这些小部件都继承自 ActionItem。
注意
ActionView 应该只包含继承自 ActionItem 的小部件。我们可以通过继承 ActionItem 来创建自己的操作项。
ActionGroup 将 ActionItem 实例分组,以便组织响应式显示。在这种情况下,它包含一个 ActionButton 实例和一个 ActionTextInput 实例。ActionListButton 和 ActionTextInput 是我们需要创建的个性化小部件。ActionListButton 将继承自 ActionPrevious 和 ToggleButtonBehaviour,而 ActionTextInput 继承自 TextInput 和 ActionItem。
在继续之前,代码中有几个新的属性值得解释。ActionView 的 use_separator 属性(第 317 行)表示是否在每个 ActionGroup 前使用分隔符。title 属性(第 321 行),它在 ActionListButton 的组件中显示标题,是从 ActionPrevious 继承的。ActionPrevious 只是一个带有一些额外 GUI 特性(如标题,还可以通过 app_icon 修改 Kivy 图标)的按钮,但更重要的是,它的父级(ActionView)将使用 action_previous 属性保留对其的引用。
现在我们来看看 actiontextinput.kv 文件中 ActionTextInput 的定义:
332\. # File name: actiontextinput.kv
333\. <ActionTextInput@TextInput+ActionItem>
334\. background_color: 0.2,0.2,0.2,1
335\. foreground_color: 1,1,1,1
336\. cursor_color: 1,1,1,1
337\. hint_text: 'search'
338\. multiline: False
339\. padding: 14
340\. size_hint: None, 1
正如我们之前所说,ActionTextInput继承自TextInput和ActionItem,TextInput小部件是一个简单的显示文本输入字段的小部件,用户可以在其中写入。它直接从Widget类和 Kivy 1.9.0 中引入的FocusBehaviour类继承。我们使用的多重继承表示法(第 333 行)对我们来说是新的。
注意
为了在 Kivy 语言中使用多重继承,我们使用表示法<DerivedClass@BaseClass1+BaseClass2>。
TextInput小部件是 Kivy 中最灵活的小部件之一,它包含许多可以用来配置它的属性。我们使用了background_color、foreground_color和cursor_color属性(第 334 至 336 行)来分别设置背景、前景和光标颜色。hint_text属性将显示提示背景文本,当TextInput获得焦点时(例如,当我们点击或触摸它时)将消失。multiline属性将指示TextInput是否接受多行,并且当按下Enter键时,它将激活on_text_validate事件,我们在kivyplayer.kv文件(第 331 行)中使用它。
注意,我们还向KivyPlayer(第 305 至 307 行)添加了一些引用。我们在KivyPlayer的 Python 端使用这些引用,即kivyplayer.py。我们将在三个片段中介绍这段代码:
341\. # File name: kivyplayer.py (Fragment 1 of 3)
342\. from kivy.app import App
343\. from kivy.uix.floatlayout import FloatLayout
344\. from kivy.animation import Animation
345\. from kivy.uix.behaviors import ToggleButtonBehavior
346\. from kivy.uix.actionbar import ActionPrevious
347\.
348\. from kivy.lang import Builder
349\.
350\. import videocontroller
351\.
352\. Builder.load_file('actiontextinput.kv')
353\.
354\.
355\. class ActionListButton(ToggleButtonBehavior, ActionPrevious):
356\. pass
在这个片段中,我们添加了所有必要的代码导入。我们还加载了actiontextinput.kv文件,并定义了从ToggleButtonBehaviour和ActionPrevious继承的ActionListButton类,正如我们之前所指示的。
在kivyplayer.py的片段 2 中,我们添加了所有由ActionItems调用的必要方法:
357\. # File name: kivyplayer.py (Fragment 2 of 3)
358\. class KivyPlayer(FloatLayout):
359\.
360\. def hide_bars(self, instance, playing):
361\. if playing:
362\. self.list_button.state = 'normal'
363\. self.animationAB = Animation(y=self.height)
364\. self.action_bar.disabled = True
365\. self.animationAB.start(self.action_bar)
366\. else:
367\. self.action_bar.disabled = False
368\. self.action_bar.top = self.height
369\. if hasattr(self, 'animationAB'):
370\. self.animationAB.cancel(self.action_bar)
371\.
372\. def toggle_mute(self, instance, state):
373\. if state == 'down':
374\. self.video_controller.video.volume = 0
375\. else:
376\. self.video_controller.video.volume = 1
377\.
378\. def show_load_list(self):
379\. pass
380\.
381\. def search(self, text):
382\. pass
对于本节,我们只是实现了hide_bars和toggle_mute方法。hide_bars方法(第 360 至 371 行)在视频播放时以类似我们之前隐藏控制栏的方式隐藏操作栏。toggle_button方法(第 372 至 382 行)使用volume属性在满音量和静音状态之间切换。代码的片段 3 只包含运行代码的最终命令:
383\. # File name: kivyplayer.py (Fragment 3 of 3)
384\. class KivyPlayerApp(App):
385\. def build(self):
386\. return KivyPlayer()
387\.
388\. if __name__=="__main__":
389\. KivyPlayerApp().run()
您现在可以再次运行应用程序。您可能想要调整窗口大小,看看操作栏如何根据屏幕大小重新组织组件。以下是中等(左侧)和小型(右侧)尺寸的两个示例:

LoadDialog – 显示文件目录
在本节中,我们将讨论如何在 Kivy 中显示目录树以选择文件。首先,我们将在loaddialog.kv中定义界面:
390\. # File name: loaddialog.kv
391\. <LoadDialog>:
392\. BoxLayout:
393\. size: root.size
394\. pos: root.pos
395\. orientation: "vertical"
396\. FileChooserListView:
397\. id: filechooser
398\. path: './'
399\. BoxLayout:
400\. size_hint_y: None
401\. height: 30
402\. Button:
403\. text: "Cancel"
404\. on_release: root.cancel()
405\. Button:
406\. text: "Load"
407\. on_release: root.load(filechooser.path, filechooser.selection)
这段代码中除了使用了FileChooserListView控件之外,没有其他新内容。它将显示文件的目录树。path属性(第 398 行)将指示开始显示文件的基准路径。除此之外,我们还添加了取消(第 402 行)和加载按钮(第 405 行),它们调用定义在loaddialog.py文件中的LoadDialog类的相应函数:
408\. # File name: loaddialog.py
409\.
410\. from kivy.uix.floatlayout import FloatLayout
411\. from kivy.properties import ObjectProperty
412\. from kivy.lang import Builder
413\.
414\. Builder.load_file('loaddialog.kv')
415\.
416\. class LoadDialog(FloatLayout):
417\. load = ObjectProperty(None)
418\. cancel = ObjectProperty(None)
在这个类定义中实际上没有明确定义的参数,只有几个属性。我们将在kivyplayer.py文件中将方法分配给这些属性,Kivy/Python 将分别调用它们:
419\. def show_load_list(self):
420\. content = LoadDialog(load=self.load_list, cancel=self.dismiss_popup)
421\. self._popup = Popup(title="Load a file list", content=content, size_hint=(1, 1))
422\. self._popup.open()
423\.
424\. def load_list(self, path, filename):
425\. pass
426\.
427\. def dismiss_popup(self):
428\. self._popup.dismiss()
如果你记得,ActionBar实例的打开列表按钮调用show_load_list方法(第 329 行)。这个方法将创建一个LoadDialog实例(第 420 行),并将两个其他方法作为构造函数的参数发送:load_list(第 424 行)和dismiss_popup(第 427 行)。这些方法将被分配给load和cancel属性。一旦实例创建完成,我们将在Popup中显示它(实例行 421 和 422)。
现在,当我们在LoadDialog的加载按钮(第 420 行)上点击时,将调用load_list方法,当按下取消按钮时,将调用dismiss_popup方法。不要忘记在kivyplayer.py中添加相应的导入:
429\. from kivy.uix.popup import Popup
430\. from loaddialog import LoadDialog
431\. from sidebar import ListItem
下面是生成的截图,我们可以欣赏到树状目录:

ScrollView – 显示视频列表
在本节中,我们将展示在 TED 视频网站上执行搜索的结果,这些结果将显示在一个可以上下滚动的侧边栏中,如下面的截图所示:

让我们在sidebar.kv文件中开始定义侧边栏的组件:
432\. # File name: sidebar.kv
433\. <ListItem>:
434\. size_hint: [1,None]
435\. height: 70
436\. group: 'listitem'
437\. text_size: [self.width-20, None]
438\.
439\.
440\. <Sidebar@ScrollView>:
441\. playlist: _playlist
442\. size_hint: [None, None]
443\. canvas.before:
444\. Color:
445\. rgba: 0,0,0,.9
446\. Rectangle:
447\. pos: 0,0,
448\. size: self.width,self.height
449\.
450\. GridLayout:
451\. id: _playlist
452\. size_hint_y: None
453\. cols: 1
ListItem类继承自ToggleButton。text_size属性将为文本设置边界。如果视频的标题过长,将使用两行显示。Sidebar类继承自ScrollView,这将允许滚动视频列表,类似于我们在上一节LoadDialog中滚动文件的方式。Sidebar内部的GridLayout实例是实际包含和组织ListItem实例的控件。这通过Sidebar的playlist属性(第 442 行)进行引用。
小贴士
在ScrollView内部的包含元素必须允许比ScrollView更大,以便可以滚动。如果你想添加垂直滚动,将size_hint_y设置为None;如果你想添加水平滚动,将size_hint_x设置为None。
让我们继续在 Python 文件(sidebar.py)中定义侧边栏的定义:
454\. # File name: sidebar.py
455\.
456\. import json
457\.
458\. from kivy.uix.togglebutton import ToggleButton
459\. from kivy.properties import ObjectProperty
460\. from kivy.lang import Builder
461\.
462\. Builder.load_file('sidebar.kv')
463\.
464\. class ListItem(ToggleButton):
465\. video = ObjectProperty(None)
466\.
467\. def __init__(self, video, meta, surl, **kwargs):
468\. super(self.__class__, self).__init__(**kwargs)
469\. self.video = video
470\. self.meta = meta
471\. self.surl = surl
472\.
473\. def on_state(self, instance, value):
474\. if self.state == 'down':
475\. data = json.load(open(self.meta))['talk']
476\. self.video.surl = self.surl
477\. self.video.source = data['media']['internal']['950k']['uri']
478\. self.video.image = data['images'][-1]['image']['url']
此文件提供了ListItem类的实现。构造函数中有三个参数(第 473 行):一个video小部件的实例、包含视频元数据的meta文件名,这些元数据由 TED 视频提供,以及包含字幕 URL 的surl。当ListItem小部件的state属性发生变化时,会调用on_state方法(第 474 行)。此方法将打开 TED 提供的 JSON 格式的文件,并提取更新视频小部件属性所需的信息。我们在本节代码中包含了一个 TED 元数据文件集合,位于结果文件夹中,以便在您包含自己的 API 之前测试代码。例如,results/97.json包含我们迄今为止使用的丹·吉尔伯特视频的元数据。您可以在该字幕文件的第 477 行和第 478 行验证 JSON 结构。
现在,我们需要在kivyplayer.kv文件中将一个Sidebar实例添加到KivyPlayer中:
479\. # File name: kivyplayer.kv
480\. <KivyPlayer>:
481\. list_button: _list_button
482\. action_bar: _action_bar
483\. video_controller: _video_controller
484\. side_bar: _side_bar
485\. playlist: _side_bar.playlist
486\.
487\. VideoController:
488\. id: _video_controller
489\. control_bar_width: root.width - _side_bar.right
490\.
491\. (...)
492\.
493\. Sidebar:
494\. id: _side_bar
495\. width: min(_list_button.width,350)
496\. height: root.height - _action_bar.height
497\. top: root.height - _action_bar.height
498\. x: 0 - self.width if _list_button.state == 'normal' else 0
我们已添加Sidebar实例并基于屏幕上的其他元素定义了一些position属性(第 495 行至第 498 行)。我们还调整了控制栏的width为side_bar(第 480 行)。当Sidebar显示时,则控制栏将自动调整到可用空间。我们使用ActionListButton类(第 512 行)控制侧边栏的显示,我们将在kivyplayer.py中定义此类:
499\. # File name: kivyplayer.py
500\. import json
501\. import os
502\.
503\. (...)
504\.
505\. from sidebar import ListItem
506\.
507\. Builder.load_file('actiontextinput.kv')
508\.
509\. _surl = 'http://www.ted.com/talks/subtitles/id/%s/lang/en'
510\. _meta = 'results/%s.json'
511\.
512\. class ActionListButton(ToggleButtonBehavior, ActionPrevious):
513\. def on_state(self, instance, value):
514\. if self.state == 'normal':
515\. self.animationSB = Animation(right=0)
516\. self.animationSB.start(self.root.side_bar)
517\. else:
518\. self.root.side_bar.x=0
519\.
520\. class KivyPlayer(FloatLayout):
521\.
522\. def __init__(self, **kwargs):
523\. super(self.__class__, self).__init__(**kwargs)
524\. self.playlist.bind(minimum_height= self.playlist.setter('height'))
侧边栏的动画与我们本章中看到的类似。我们还包含了两个全局变量:_surl和_meta(第 509 行和第 510 行)。这些字符串将作为字幕和元数据文件的模板。请注意,字符串中的%s将被替换。我们还向KivyPlayer类定义中引入了一个构造函数(__init__)(第 522 行和第 524 行)。第 524 行是必要的,以确保GridLayout实例(在ScrollView内部)适应其高度,从而允许滚动。
现在,我们需要将ListItem实例添加到Sidebar小部件中。为此,我们将在kivyplayer.py中定义load_list方法(第 525 行)和load_from_json方法(第 532 行):
525\. def load_list(self, path, filename):
526\. json_data=open(os.path.join(path, filename[0]))
527\. data = json.load(json_data)
528\. json_data.close()
529\. self.load_from_json(data)
530\. self.dismiss_popup()
531\.
532\. def load_from_json(self, data):
533\. self.playlist.clear_widgets()
534\. for val in data['results']:
535\. t = val['talk']
536\. video = self.video_controller.video
537\. meta = _meta % t['id']
538\. surl = _surl % t['id']
539\. item = ListItem(video, meta, surl, text=t['name'])
540\. self.playlist.add_widget(item)
541\. self.list_button.state = 'down'
我们包含了一个 results.json 文件,其中包含从 TED 网站获取的示例搜索结果列表。这个结果以 JSON 格式呈现,您可以在文件中查看。我们需要打开这个文件,并在 侧边栏 中显示其内容。为了做到这一点,我们使用 LoadDialog 显示选择 result.json 文件,并使用 打开列表 按钮进行选择。一旦选择,就会调用 load_list 方法。该方法打开数据并加载 JSON 数据(第 527 行)。一旦加载,它就会调用 load_from_json 方法(第 528 行)。在这个方法中,我们为从 TED 网站搜索得到的结果创建一个 ListItem 实例(第 539 行),并将这些实例添加到播放列表中(即 侧边栏 内的 GridLayout 实例,第 451 行)。第 537 行和 538 行是 Python 中连接字符串的常见方式。它将字符串中存在的 %s 替换为 % 后面的相应参数。现在,当我们打开 results.json 文件时,就像本节开头截图所示,我们将在应用程序中以侧边栏列表的形式看到结果。
搜索 – 查询 TED 开发者 API
本节最后将介绍一些代码更改,以便我们能够搜索 TED 网站。
小贴士
您需要做的第一件事是从 TED 网站使用以下链接获取 API 密钥:
developer.ted.com/member/register。
TED API 密钥是一个字母数字编号(例如 '1a3bc2'),它允许您直接查询 TED 网站,并获取我们上一节中使用的 JSON 格式的请求。一旦您在电子邮件账户中收到 API 密钥,您就可以修改 kivyplayer.py 文件,并将其放入 _api 全局变量中。目前,我们可以在 kivyplayer.py 文件中使用如下占位符:
_api = 'YOUR_API_KEY_GOES_HERE'
此外,在 kivyplayer.py 文件中,我们需要引入一个包含搜索模板(search)的全局变量,并替换 _meta 全局变量的内容:
_search = 'https://api.ted.com/v1/search.json?q=%s&categories=talks&api-key=%s'
_meta = 'https://api.ted.com/v1/talks/%s.json?api-key=%s'
注意,现在 _meta 变量有两个 %。因此,我们需要在 load_from_json 方法(第 533 行)中将 meta = meta % t['id'] 代码行替换为 meta = _meta % (t['id'], _api)。另外,由于我们不再打开文件,我们还需要修改 ListItem 类中加载 JSON 的方式,因为我们现在没有文件,而是一个 URL。首先,我们需要在 sidebar.py 文件的开始处导入 URLRequest 类(from kivy.network.urlrequest import UrlRequest),然后修改 on_state 方法以使用 URLRequest 类,就像我们学习字幕时做的那样:
542\. def on_state(self, instance, value):
543\. if self.state == 'down':
544\. req = UrlRequest(self.meta, self.got_meta)
545\.
546\. def got_meta(self, req, results):
547\. data = results['talk']
548\. self.video.surl = self.surl
549\. self.video.source = data['media']['internal']['950k']['uri']
550\. self.video.image = data['images'][-1]['image']['url']
我们还需要在 kivyplayer.py 中导入 URLRequest 类,以便在 KivyPlayer 类定义中实现 search 方法:
551\. def search(self, text):
552\. url = _search % (text, _api)
553\. req = UrlRequest(url, self.got_search)
554\.
555\. def got_search(self, req, results):
556\. self.load_from_json(results)
现在,您可以检查是否收到了 TED API 密钥。一旦您替换了 _api 变量,您将能够使用操作栏中的搜索框查询 TED API。您现在可以使用 ActionTextInput 进行搜索:

小贴士
请记住,您刚刚创建的 API 密钥可以识别您和您的应用程序作为 TED 网站的用户。所有通过该 API 注册的活动都是您的责任。您不应该将此 API 密钥告诉任何人。
控制您的 API 密钥的使用涉及设置自己的服务器,其中 API 密钥被安全存储。该服务器将作为您应用程序的代理 (en.wikipedia.org/wiki/Proxy_server),并应限制查询。例如,它应避免大量查询等滥用行为。
摘要
在本章中,我们创建了一个集成了许多 Kivy 组件的应用程序。我们讨论了如何控制视频以及如何将屏幕的不同元素与之关联。我们探索了不同的 Kivy 小部件,并实现了复杂的交互来显示可滚动的元素列表。以下是本章中我们使用的新类和组件列表:
-
Video: 从Image继承的allow_stretch和source属性;state和progress属性;_on_load、on_eos、on_source和on_state、on_position、seek方法 -
AsyncImage: 从Image继承的source属性;从Widget继承的opacity属性 -
Label:texture_size属性 -
Slider:value和max属性Touch:double_tap键 -
Kivy 检查器类
-
ActionBar、ActionView、ActionItem、ActionPrevious、ActionToggleButton、ActionGroup和ActionButton类,以及ActionView的use_separator属性和ActionPrevious的标题属性 -
Textinput:background_color、foreground_color、cursor_color和multiLine属性 -
FileChooserListView:path属性 -
ScrollView类
作为本章的副产品,我们获得了一个经过优化的 Video 小部件,我们可以在其他应用程序中使用它。这个 Video 小部件将我们以 JSON 格式文件接收的子标题与视频的进度同步,并具有响应式的 控制栏。
我们已经掌握了 Video 小部件的使用。我们学习了如何控制其进度并添加字幕。我们还介绍了如何查询 TED 开发者 API 以获取结果列表,并练习了操作 JSON 格式的技能。我们还学习了如何使用 Kivy 调试器来检测界面中的错误。
我们还努力使我们的 KivyPlayer 应用看起来更专业。我们通过引入隐藏 GUI 组件的动画来优化屏幕的使用,这些动画在不需要时隐藏。在这个过程中,我们使用了许多 Kivy 元素来使我们的小部件保持一致,并审查了诸如行为、工厂、动画、触摸事件以及属性的使用等有趣的主题,以便创建多功能组件。
开始即是结束,因此现在轮到你自己开始自己的应用了。我真心希望,你从这本书中学到的知识能帮助你实现你的想法,并开始你自己的应用。


浙公网安备 33010602011771号