原文:
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 以获取更全面的属性和方法列表。
下一章将介绍个性化多点触控、动画以及时钟和键盘事件。我们将创建一个新的交互式项目,一款类似于街机游戏 太空侵略者 的游戏。
1306

被折叠的 条评论
为什么被折叠?



