原文:
zh.annas-archive.org/md5/9c3f4e93a52e8d38197e54db421b1d0d译者:飞龙
前言
移动应用早已不再是“新热点”,如今用户通常期望新的软件——无论是视频游戏还是社交网络——都有一个移动版本。类似的趋势也影响了桌面操作系统;编写跨平台软件,曾经是不常见的,迅速成为了一种规范。即使是通常仅限于桌面 Microsoft 操作系统的游戏开发者,现在也可以看到他们在为许多新游戏(例如,在撰写本文时,Steam 托管了超过一百款在 Mac 上运行的游戏,以及超过五十款在 Linux 上运行的游戏)开发 Mac 和 Linux 版本。
这对于初创公司和独立开发者来说尤其有价值:构建真正跨平台的软件可以扩大潜在受众,从而增加销量,并在过程中可能创造良好的舆论。
然而,编写可移植的软件可能是一个非常资源密集的过程,这对小型开发者的影响远大于大型企业。
尤其是许多平台都有一个首选的编程语言和软件开发工具包(SDK):iOS 应用大多是用 Objective-C 和 Swift 编写的,Android 推荐使用不太理想的 Java 编程语言,而微软则推广使用.NET 框架,特别是 C#,来构建 Windows 软件。
使用这些工具允许你利用操作系统的原生用户界面和底层功能,但它也自动防止了代码重用。这意味着即使你对所有涉及的编程语言和接口都同样精通,移植代码可能仍然需要相当多的时间,并引入新的错误。
编写一次,运行任何地方
这种整个情况产生了对一种通用、多平台编程方式的需求。这个问题并不是全新的:1995 年由 Sun 提出的解决方案之一是 Java 编程语言。它的营销承诺——“编写一次,运行任何地方”——从未实现,而且该语言本身使用起来非常繁琐。这导致了众多对该口号的嘲讽变体,最终以“编写一次,逃离一次”为高潮,指的是许多开发者放弃了 Java,转而使用更好的编程语言,包括 Python。
顺便提一下,Kivy——本书的主要主题——是一个图形用户界面库,它简化了多平台 Python 应用的创建。Kivy 工具包的主要特性如下:
-
兼容性:基于 Kivy 的应用在 Linux、Mac OS X、Windows、Android 和 iOS 上都能运行——所有这些都来自同一个代码库。
-
自然用户界面:Kivy 弥合了不同输入方法之间的差距,允许你使用类似的代码处理多种可能的用户交互,无论是鼠标事件还是多点触控手势。
-
快速硬件加速图形:OpenGL 渲染使 Kivy 适合创建图形密集型应用,如视频游戏,同时也通过平滑的过渡改善了用户体验。
-
Python 的使用:Kivy 应用是用 Python 编写的,Python 是一种较好的通用编程语言。除了本身具有可移植性、表达性和可读性之外,Python 还具有有用的标准库和丰富的第三方包生态系统,即 Python 包索引(PyPI)。
谈到第三方包,Kivy 可以被视为许多经过实战检验的组件的超集:其功能的大部分依赖于知名的库,如 Pygame、SDL 和 GStreamer。然而,Kivy 提供的 API 非常高级且统一。
值得一提的是,Kivy 是一款免费且开源的 MIT 许可软件。在实践中,这意味着您可以在不支付许可费用的情况下将其用于商业用途。它的完整源代码托管在 GitHub 上,因此您也可以修复错误或为其添加新功能。
本书涵盖的内容
第一章,《构建时钟应用》为使用 Kivy 编写应用提供了一个温和的介绍。它涵盖了 Kivy 语言、布局、小部件和计时器。到本章结束时,我们将构建一个简单的时钟应用,类似于您手机中的时钟应用。
第二章,《构建绘图应用》进一步探讨了 Kivy 框架的组件和功能。生成的绘图应用展示了内置小部件的定制、在画布上绘制任意形状以及处理多点触控事件。
第三章,《Android 声音录制器》作为编写基于 Kivy 的 Android 应用的示例。它展示了如何使用 Pyjnius 互操作性层将 Java 类加载到 Python 中,这使得我们可以将 Android API 调用与基于 Kivy 的用户界面混合使用。
第四章,《Kivy 网络编程》是一本从零开始构建网络应用的实战指南。它涵盖了从创建简单协议到用 Python 编写服务器和客户端软件的多个主题,并以 Kivy 聊天应用作为总结。
第五章,《制作远程桌面应用》展示了编写客户端-服务器应用的另一种方式。本章的程序基于 HTTP 协议——互联网背后的协议。我们首先开发了一个命令行 HTTP 服务器,然后使用 Kivy 构建远程桌面客户端应用。
第六章,《制作 2048 游戏》将指导您构建一个可玩的游戏副本。我们展示了更复杂的 Kivy 功能,例如创建自定义小部件、使用 Kivy 属性进行数据绑定以及处理触摸屏手势。
第七章,编写 Flappy Bird 克隆介绍了另一个基于 Kivy 的游戏,这次是一个类似于著名 Flappy Bird 标题的街机游戏。在本章中,我们讨论了纹理坐标和音效的使用,实现了街机物理和碰撞检测。
第八章,介绍着色器展示了在 Kivy 应用程序中使用 GLSL 着色器的用法。在本教程中,您将了解 OpenGL 原语,如索引和顶点,然后编写直接在 GPU 上运行的极快级代码。
第九章,制作射击游戏从上一章结束的地方继续:我们使用 GLSL 的知识来构建一个侧滚动射击游戏。在此过程中,开发了一个可重用的粒子系统类。本项目总结了整个系列,并利用了书中解释的许多技术,例如碰撞检测、触摸屏控制、音效等。
附录,Python 生态系统为您提供了更多关于 Python 库和工具的信息。
设置工作环境
本节简要讨论了有效遵循叙述、实现和运行 Kivy 应用程序所需的条件。一台运行现代操作系统的个人电脑——Mac、Linux 或 Windows 计算机——是隐含的。
关于 Python 的说明
Python 是本书中使用的首选编程语言;虽然不是严格必要的,但对其有良好的了解可能会有所帮助。
在撰写本文时,广泛使用中的 Python 有两个不兼容的版本。Python 2.7 非常稳定,但不再积极开发,而 Python 3 是一个较新的、略带争议的版本,为语言带来了许多改进,但偶尔会破坏兼容性。
本书中的代码应该在大致上适用于 Python 的两个版本,但可能需要稍作调整才能完全兼容 Python 3;为了获得最佳效果,建议您使用 Python 2.7 或您系统上可用的最新 Python 2 版本。
注意
在大多数平台上,不需要单独安装 Python 用于 Kivy 开发:它可能预装在系统上(Mac OS X)、与 Kivy 捆绑(MS Windows),或者作为依赖项包含(Linux,尤其是 Ubuntu)。
安装和运行 Kivy
您可以从官方网站(kivy.org/)下载 Kivy;只需选择合适的版本并按照说明操作。整个过程应该相当直接和简单。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_Preface_01.jpg
Kivy 下载
要检查安装是否正常工作,请按照以下说明操作:
-
在 Mac 上:
-
打开Terminal.app。
-
运行
kivy。 -
应该出现 Python 提示符
>>>。输入import kivy。 -
命令应该无错误完成,并打印类似
[INFO] Kivy v1.8.0的消息。
-
-
在 Linux 机器上:
-
打开一个终端。
-
运行
python。 -
应该出现 Python 提示符
>>>。输入import kivy。 -
命令应该打印类似
[INFO] Kivy v1.8.0的消息。
-
-
在 Windows 系统上:
-
双击 Kivy 软件包目录内的 kivy.bat。
-
在命令提示符中输入
python。 -
输入
import kivy。 -
命令应该打印类似
[INFO] Kivy v1.8.0的消息。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_Preface_02.jpg
终端会话
-
运行 Kivy 应用程序(基本上是一个 Python 程序)的方式类似:
-
在 Mac 上,使用
kivy main.py -
在 Linux 上,使用
python main.py -
在 Windows 上,使用
kivy.bat main.py(或将 main.py 文件拖放到 kivy.bat 之上)。
编码注意事项
编程通常涉及大量文本处理;因此,选择一个好的文本编辑器非常重要。这就是为什么我强烈建议在考虑其他选项之前先尝试使用 Vim。
Vim 是目前可用的优秀文本编辑器之一;它高度可配置,专为有效文本编辑而构建(比典型替代品更有效)。Vim 拥有一个充满活力的社区,正在积极维护,并且预安装在许多类 Unix 操作系统中——包括 Mac OS X 和 Linux。已知(至少一些)Kivy 框架的开发者也喜欢使用 Vim。
这里有一些针对 Vim 用户快速了解 Kivy 的技巧:
-
Python-mode (
github.com/klen/python-mode) 对于编写 Python 代码非常出色。它提供了许多额外功能,例如样式和静态检查器、智能完成以及重构支持。 -
GLSL 着色器的源代码可以使用
vim-glsl语法正确高亮显示(github.com/tikhomirov/vim-glsl)。 -
Kivy 纹理映射(即
.atlas文件,在第八章 介绍着色器 中介绍),基本上是 JSON 格式,因此您可以使用,例如 vim-json (github.com/elzr/vim-json),并将以下行添加到您的.vimrc文件中,以创建文件关联:au BufNewFile,BufRead *.atlas set filetype=json -
Kivy 布局文件(
.kv)处理起来稍微复杂一些,因为它们与 Python 类似,但并不真正像 Python 那样解析。Kivy 仓库中有一个不完整的 Vim 插件,但在撰写本文时,Vim 内置的 YAML 支持更好地突出显示这些文件(这显然可能在将来发生变化)。要将.kv文件作为 YAML 加载,请将以下行添加到您的.vimrc文件中:au BufNewFile,BufRead *.kv set filetype=yaml
显然,您并不 必须 使用 Vim 来跟随本书的示例——这只是一个建议。现在让我们写一点代码,怎么样?
Hello, Kivy
当学习一门新的编程语言或技术时,学生通常会首先看到的是一个传统的 “hello, world” 程序。在 Python 中它看起来是这样的:
print('hello, world')
Kivy 的“hello, world”版本稍微长一些,由两个文件组成,即一个 Python 模块和一个 .kv 布局定义。
代码
Kivy 应用程序的入口通常称为 main.py,其内容如下:
from kivy.app import App
class HelloApp(App):
pass
if __name__ == '__main__':
HelloApp().run()
如您所见,这使用了 Kivy 的 App 类,对其没有任何添加,并调用了 run() 方法。
布局
布局文件通常以应用程序类名命名,在本例中为 HelloApp,不带 App 后缀且为小写:hello.kv。它包含以下行:
Label:
text: 'Hello, Kivy'
这是一个非常简单的 Kivy 布局定义,仅包含一个 Label 小部件,其中包含所需文本。布局文件允许以简洁、声明性的方式构建复杂的组件层次结构,这在此处未显示,但将在本书的整个过程中被大量使用。
如果我们现在运行程序(有关详细信息,请参阅安装和运行 Kivy部分),这将是我们得到的结果:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_Preface_03.jpg
我们由 Kivy 驱动的第一个应用程序
现在您已经准备好进入第一章,并开始编写真正的程序。
本书面向的对象
本书旨在为那些熟悉 Python 语言并希望以最小麻烦使用 Python 构建桌面和移动应用程序的程序员编写。虽然了解 Kivy 对您有所帮助,但并非必需——框架的每个方面在首次使用时都会进行描述。
在本书的各个部分,我们将 Kivy 与 Web 开发实践进行类比。然而,对后者的深入了解也不是必需的,以跟随叙述。
规范
在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“Kivy 应用的入口通常称为 main.py。”
代码块设置如下:
from kivy.app import App
class HelloApp(App):
pass
if __name__ == '__main__':
HelloApp().run()
当我们希望将您的注意力引向代码块的一个特定部分时,相关的行或项目将以粗体显示:
# In Python code
LabelBase.register(name="Roboto",
fn_regular="Roboto-Regular.ttf",
fn_bold="Roboto-Bold.ttf",
fn_italic="Roboto-Italic.ttf",
fn_bolditalic="Roboto-BoldItalic.ttf")
任何命令行输入或输出都如下所示:
pip install -U twisted
新术语和重要词汇以粗体显示。屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:“第一个事件处理器是为开始和停止按钮。”
注意
警告或重要注意事项如下所示:
小贴士
小贴士
读者反馈
我们欢迎读者反馈。请告诉我们您对本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中受益的标题。
向我们发送一般反馈,只需发送电子邮件至 <feedback@packtpub.com>,并在邮件主题中提及本书的标题。
如果你在一个领域有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在你已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。
下载示例代码
你可以从你购买的所有 Packt 出版物的账户中下载示例代码文件www.packtpub.com。如果你在其他地方购买了这本书,你可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给你。获取最新源代码的另一种方法是克隆 GitHub 仓库github.com/mvasilkov/kb。
下载本书的彩色图像
我们还为你提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助你更好地理解输出的变化。你可以从以下链接下载此文件:www.packtpub.com/sites/default/files/downloads/7849OS_ColorImages.pdf。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这一点,我们将不胜感激。通过这样做,你可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果你发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择你的书籍,点击勘误提交表单链接,并输入你的勘误详情来报告它们。一旦你的勘误得到验证,你的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
盗版
互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果你在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过发送链接到疑似盗版材料的方式,与我们联系 <copyright@packtpub.com>。
我们感谢你在保护我们的作者和我们提供有价值内容的能力方面的帮助。
问题
如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章. 构建时钟应用
本书将引导你创建九个小 Kivy 程序,每个程序都类似于 Kivy 框架在现实世界中的实际应用案例。在许多情况下,框架将与适合当前任务的 Python 模块一起使用。我们将看到 Kivy 提供了大量的灵活性,使我们能够以干净、简洁的方式解决各种不同的问题。
让我们从简单开始。在本章中,我们将构建一个简单的时钟应用,其概念类似于 iOS 和 Android 中内置的应用程序。在本章的第一部分,我们将创建一个非交互式的数字时钟显示并对其进行样式设计,使我们的程序具有类似 Android 的扁平外观。我们还将简要讨论事件驱动的程序流程和 Kivy 主循环,介绍用于执行重复任务的计时器,例如每帧更新屏幕。
在本章的第二部分,我们将添加计时器显示和控制功能,创建一个适合任何屏幕尺寸和方向的流畅布局。计时器自然需要用户交互,我们将最后实现它。
本章介绍的重要主题如下:
-
Kivy 语言的基礎,这是一个用于布局小部件的内置领域特定语言(DSL)
-
样式(以及最终子类化)内置的 Kivy 组件
-
加载自定义字体和格式化文本
-
调度和监听事件
我们完成的程序,如以下截图所示,将只有大约 60 行长,Python 源代码和 Kivy 语言(.kv)界面定义文件各占一半。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_01_01.jpg
我们将要构建的时钟应用最终外观。
起始点
我们在序言中的“Hello, Kivy”示例是本应用的合适起点。我们只需要添加一个布局容器,BoxLayout,这样我们就可以在屏幕上放置多个小部件。
到目前为止,这是完整的源代码:
# File: main.py
from kivy.app import App
class ClockApp(App):
pass
if __name__ == '__main__':
ClockApp().run()
# File: clock.kv
BoxLayout:
orientation: 'vertical'
Label:
text: '00:00:00'
目前,它的外观和行为与之前看到的“Hello, world”应用完全一样。BoxLayout容器允许两个或更多子小部件并排存在,垂直或水平堆叠。给定一个嵌套小部件,如前面的代码所示,BoxLayout将所有可用的屏幕空间填充为它,因此实际上几乎看不见(就像Label是根小部件一样,接管了应用程序窗口)。我们将在稍后更详细地回顾布局。
注意
注意,虽然我们可以将main.py文件命名为任何我们想要的名称,但clock.kv文件是由 Kivy 自动加载的,因此必须以应用程序类的名称命名。例如,如果我们的应用程序类名为FooBarApp,相应的.kv文件应命名为foobar.kv(类名转换为小写且不带-app后缀)。严格遵循此命名约定可以让我们避免手动加载 Kivy 语言文件,这无疑是件好事——更少的代码行数就能达到相同的结果。
现代用户界面
在撰写本文时,扁平化设计范式在界面设计领域正流行,系统地接管了每一个平台,无论是 Web、移动还是桌面。这种范式转变的突出例子包括 iOS 7 及其后续版本和 Windows 8 及其后续版本。互联网公司随后效仿,在 2014 年 Google I/O 大会上提出了“材料设计原则”,以及许多其他 HTML5 框架,包括那些已经建立起来的,例如 Bootstrap。
方便的是,扁平化设计强调内容而非展示,省略了照片般的阴影和详细的纹理,转而使用纯色和简单的几何形状。这种设计在程序上创建起来比“老式”的拟物化设计要简单得多,后者往往视觉丰富且富有艺术性。
注意
拟物主义是用户界面设计的一种常见方法。其特点是应用程序在视觉上模仿其现实世界的对应物,例如,一个计算器应用程序具有与廉价物理计算器相同的按钮布局和外观感觉。这可能有助于或可能不助于用户体验(取决于你问谁)。
为了更简单、更流畅的界面而放弃视觉细节,这似乎是今天每个人都正在走的方向。另一方面,仅从彩色矩形等元素中构建一个独特、令人难忘的界面自然具有挑战性。这就是为什么扁平化设计通常与良好的排版同义;根据应用的不同,文本几乎总是 UI 的一个重要部分,因此我们希望它看起来很棒。
设计灵感
仿效是最高形式的恭维,我们将仿效来自 Android 4.1 姜饼的时钟设计。这种设计的独特之处在于字体粗细对比。直到 4.4 KitKat 版本中进行了更改,默认时钟曾经看起来是这样的:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_01_02.jpg
在 Android 姜饼版锁屏上看到的时钟。
使用的字体是 Roboto,这是 Google 在 Android 4.0 冰激凌三明治中取代 Droid 字体家族的 Android 字体。
Roboto 可用于商业用途,并受 Apache 许可的宽松许可。可以从 Google Fonts 或来自出色的 Font Squirrel 库www.fontsquirrel.com/fonts/roboto下载。
加载自定义字体
当谈到排版时,Kivy 默认使用 Droid Sans——谷歌早期的字体。用自定义字体替换 Droid 很容易,因为 Kivy 允许我们为文本小部件(在这种情况下,Label)指定font_name属性。
在最简单的情况下,当我们只有一种字体变体时,我们可以在小部件的定义中直接分配.ttf文件名:
Label:
font_name: 'Lobster.ttf'
然而,对于上述设计,我们希望有不同的字体重量,所以这种方法行不通。原因是字体的每个变体(例如,粗体或斜体)通常都生活在单独的文件中,而我们只能将一个文件名分配给font_name属性。
在我们的用例中,涉及到多个.ttf文件,LabelBase.register静态方法提供了更好的解决方案。它接受以下参数(所有参数都是可选的),以 Roboto 字体家族为例:
# In Python code
LabelBase.register(name="Roboto",
fn_regular="Roboto-Regular.ttf",
fn_bold="Roboto-Bold.ttf",
fn_italic="Roboto-Italic.ttf",
fn_bolditalic="Roboto-BoldItalic.ttf")
在调用此方法后,可以设置小部件的font_name属性为之前注册的字体家族名称,在这种情况下是Roboto。
这种方法有两个限制需要注意:
-
Kivy 只接受 TrueType
.ttf字体文件。如果字体打包为 OpenType.otf或.woff这样的网络字体格式,您可能需要先进行转换。这可以通过 FontForge 编辑器轻松完成,该编辑器可以在fontforge.org/找到。 -
每种字体最多有四种可能的样式:正常、斜体、粗体和粗斜体。对于像 Droid Sans 这样的旧字体家族来说,这没问题,但许多现代字体包括从 4 种到 20 种以上的样式,具有不同的字体重量和其他功能。我们即将使用的 Roboto 至少有 12 种样式。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_01_03.jpg
Roboto 字体的六种字体重量
第二点强制我们选择在应用程序中使用的字体样式,因为我们不能随意使用所有 12 种,这本身就是一个糟糕的想法,因为它会导致文件大小大幅增加,例如在 Roboto 字体家族中,可能增加到 1.7 兆字节。
对于这个特定的应用程序,我们只需要两种样式:一种较轻的样式(Roboto-Thin.ttf)和一种较重的样式(Roboto-Medium.ttf),我们分别将它们分配给fn_regular和fn_bold:
from kivy.core.text import LabelBase
LabelBase.register(name='Roboto',
fn_regular='Roboto-Thin.ttf',
fn_bold='Roboto-Medium.ttf')
这段代码应该放在main.py中的__name__ == '__main__'行之后,因为它需要在从 Kivy 语言定义创建界面之前运行。当应用程序类被实例化时,可能已经太晚执行这种基本初始化了。这就是为什么我们必须提前这样做。
现在我们已经设置了自定义字体,接下来要做的就是将其分配给我们的Label小部件。这可以通过以下代码完成:
# In clock.kv
Label:
text: '00:00:00'
font_name: 'Roboto'
font_size: 60
文本格式化
目前最流行且普遍使用的标记语言无疑是 HTML。另一方面,Kivy 实现了一种 BBCode 的变体,这是一种曾经用于在许多论坛上格式化帖子的标记语言。与 HTML 的明显区别是 BBCode 使用方括号作为标签分隔符。
以下标签在 Kivy 中可用:
| BBCode 标签 | 文本效果 |
|---|---|
[b]...[/b] | 加粗 |
[i]...[/i] | 斜体 |
[font=Lobster]...[/font] | 更改字体 |
[color=#FF0000]...[/color] | 使用 CSS 类似的语法设置颜色 |
[sub]...[/sub] | 下标(位于行下方的文本) |
[sup]...[/sup] | 上标(位于行上方的文本) |
[ref=name]...[/ref] | 可点击区域,HTML 中的 <a href="…"> |
[anchor=name] | 命名位置,HTML 中的 <a name="…"> |
提示
这绝对不是一份详尽的参考,因为 Kivy 正在积极开发,自本文编写以来可能已经发布了多个版本,增加了新功能并改进了现有功能。请参考官方网站(kivy.org)上找到的 Kivy 文档,以获取最新的参考手册。
让我们回到我们的项目。为了达到所需的格式(小时加粗,其余文本使用 fn_regular 轻细字体),我们可以使用以下代码:
Label:
text: '[b]00[/b]:00:00'
markup: True
Kivy 的 BBCode 风味只有在我们也设置了小部件的 markup 属性为 True 时才有效,如前述代码所示。否则,你将直接在屏幕上看到字符串 [b]…[/b] 被显示出来,这显然不是我们想要的。
注意,如果我们想使整个文本加粗,没有必要将所有内容都放在 [b]…[/b] 标签内;我们只需将小部件的 bold 属性设置为 True。同样的方法也适用于斜体、颜色、字体名称和大小——几乎所有的配置都可以全局设置,从而影响整个小部件,而不需要修改标记。
更改背景颜色
在本节中,我们将调整窗口的背景颜色。窗口背景(OpenGL 渲染器的“清除颜色”)是全局 Window 对象的一个属性。为了更改它,我们在 main.py 中的 __name__ == '__main__' 行之后添加此代码:
from kivy.core.window import Window
from kivy.utils import get_color_from_hex
Window.clearcolor = get_color_from_hex('#101216')
get_color_from_hex 函数并非严格必需,但使用起来很方便,因为它允许我们使用 CSS 风格的 (#RRGGBB) 颜色代替 (R, G, B) 元组来编写代码。而且使用 CSS 颜色至少有以下两个优点:
-
阅读时的认知负担更小:当你熟悉这种表示法时,
#FF0080值会立即被识别为颜色,而 (255, 0, 128) 只是一组数字,其用途可能因上下文而异。#FF0080的浮点变体 (1.0, 0.0, 0.50196) 甚至更糟。 -
简单且明确的搜索:元组可以任意格式化,而 CSS 类似的颜色表示法是统一的,尽管不区分大小写。在大多数文本编辑器中进行不区分大小写的搜索非常简单,相比之下,在漫长的 Python 列表中定位给定元组的所有实例则可能更具挑战性,这可能需要正则表达式等工具,因为元组的格式不需要保持一致。
提示
关于 #RRGGBB 颜色格式的更多信息可以在 Mozilla 开发者网络中找到:developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_started/Color。
我们将在稍后讨论 Kivy 的设计相关特性。同时,让我们让我们的应用程序真正显示时间。
使时钟滴答
UI 框架大多是事件驱动的,Kivy 也不例外。与“常规”过程代码的区别很简单——事件驱动代码需要经常返回到主循环;否则,它将无法处理来自用户(如指针移动、点击或窗口调整大小)的事件,界面将“冻结”。如果你是长期使用 Microsoft Windows 的用户,你可能熟悉那些经常无响应和冻结的程序。在我们的应用程序中绝对不能让这种情况发生。
实际上,这意味着我们无法在我们的程序中简单地编写一个无限循环:
# Don't do this
while True:
update_time() # some function that displays time
sleep(1)
从技术上讲,这可能可行,但应用程序的 UI 将保持在“未响应”状态,直到用户或操作系统强制停止应用程序。与其采取这种错误的方法,我们应牢记 Kivy 内部有一个主循环正在运行,我们需要通过利用事件和计时器来利用它。
事件驱动架构还意味着在许多地方,我们将监听事件以响应各种条件,无论是用户输入、网络事件还是超时。
许多程序监听的一个常见事件是 App.on_start。如果在这个应用程序类中定义了具有此名称的方法,那么一旦应用程序完全初始化,该方法就会被调用。另一个在许多程序中都会找到的事件示例是 on_press,当用户点击、轻触或以其他方式与按钮交互时,它会触发。
说到时间和计时器,我们可以使用内置的 Clock 类轻松地安排我们的代码在未来运行。它公开以下静态方法:
-
Clock.schedule_once:在超时后运行一次函数 -
Clock.schedule_interval:定期运行函数
注意
任何有 JavaScript 背景的人都会很容易识别这两个函数。它们与 JS 中的 window.setTimeout 和 window.setInterval 完全一样。实际上,尽管 API 完全不同,Kivy 的编程模型与 JavaScript 非常相似。
重要的是要理解,所有源自 Clock 的定时事件都作为 Kivy 主事件循环的一部分运行。这种方法与线程不同,调度这种阻塞函数可能会阻止其他事件及时或根本无法被调用。
更新屏幕上的时间
要访问包含时间的 Label 小部件,我们将给它一个唯一的标识符(id)。稍后,我们可以根据它们的 id 属性轻松查找小部件——这又是一个与网络开发非常相似的概念。
通过添加以下内容修改 clock.kv:
Label:
id: time
就这些!现在我们可以直接使用root.ids.time的表示法(在我们的例子中是BoxLayout)从我们的代码中访问这个Label小部件。
ClockApp类的更新包括添加一个显示时间的update_time方法,如下所示:
def update_time(self, nap):
self.root.ids.time.text = strftime('[b]%H[/b]:%M:%S')
现在让我们安排更新函数在程序启动后每秒运行一次:
def on_start(self):
Clock.schedule_interval(self.update_time, 1)
如果我们现在运行应用程序,我们会看到显示的时间每秒都在更新。用尼尔·阿姆斯特朗的话来说,这是人类迈出的一小步,但对于 Kivy 初学者来说是一大步。
值得注意的是,strftime函数的参数是如何将之前描述的 Kivy 的 BBCode-like 标签与特定函数的 C 样式格式指令结合起来的。对于不熟悉的人来说,这里有一个关于strftime格式化基本内容的快速且不完整的参考:
| 格式字符串(区分大小写) | 结果输出 |
|---|---|
%S | 秒,通常是00到59 |
%M | 分钟,从00到59 |
%H | 按照 24 小时制的时,从00到23 |
%I | 按照 12 小时制的时,从01到12 |
%d | 月份中的天数,从01到31 |
%m | 月份(数字),从01到12 |
%B | 月份(字符串),例如,“十月” |
%Y | 四位数的年份,例如2016 |
小贴士
关于显示时间的最完整和最新的文档,请参阅官方参考手册——在这种情况下,Python 标准库参考,位于docs.python.org/。
使用属性绑定小部件
我们不仅可以通过 Python 代码为每个需要访问的小部件硬编码一个 ID,还可以在 Kivy 语言文件中创建一个属性并为其赋值。这样做的主要动机是DRY原则和更清晰的命名,代价是代码行数稍微多了些。
这样的属性可以这样定义:
# In main.py
from kivy.properties import ObjectProperty
from kivy.uix.boxlayout import BoxLayout
class ClockLayout(BoxLayout):
time_prop = ObjectProperty(None)
在这个代码片段中,我们基于BoxLayout为我们的应用程序创建一个新的根小部件类。它有一个自定义属性,time_prop,它将引用我们需要从 Python 代码中引用的Label。
此外,在 Kivy 语言文件clock.kv中,我们必须将这个属性绑定到一个相应的id上。自定义属性看起来和行为与默认属性没有区别,并且使用完全相同的语法:
ClockLayout:
time_prop: time
Label:
id: time
这段代码通过使用新定义的属性root.time_prop.text = "demo",使Label小部件从 Python 代码中可访问,而无需知道小部件的 ID。
描述的方法比之前展示的方法更便携,并且消除了在重构时需要保持 Kivy 语言文件中的小部件标识符与 Python 代码同步的需求。否则,选择依赖属性还是通过root.ids从 Python 访问小部件,这是一个编码风格的问题。
在本书的后面部分,我们将探讨 Kivy 属性的更高级用法,这有助于几乎无需费力地进行数据绑定。
布局基础
为了在屏幕上排列小部件,Kivy 提供了一系列Layout类。Layout是Widget的子类,用作其他小部件的容器。每个布局都以独特的方式影响其子元素的位置和大小。
对于这个应用,我们不需要任何花哨的东西,因为所需的用户界面相当直观。这是我们想要实现的目标:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_01_04.jpg
完成时钟应用程序界面的布局原型。
为了构建这个,我们将使用BoxLayout,它基本上是一个一维网格。我们已经在clock.kv文件中有了BoxLayout,但由于它只有一个子元素,所以它不会影响任何事情。一个只有一个单元格的矩形网格实际上就是这样一个矩形。
Kivy 布局几乎总是试图填满屏幕,因此我们的应用程序将自动适应任何屏幕大小和方向变化。
如果我们在BoxLayout中添加另一个标签,它将占据一半的屏幕空间,具体取决于方向:垂直盒布局从上到下增长,水平布局从左到右。
你可能已经猜到,为了在垂直布局中创建一排按钮,我们只需将另一个水平盒布局嵌入到第一个布局中即可。布局是小部件,因此它们可以以任意和创造性的方式嵌套,以构建复杂的界面。
完成布局
将三个小部件堆叠到BoxLayout中通常会使每个小部件占据可用大小的三分之一。由于我们不想让按钮与时钟显示相比这么大,我们可以向水平(内部)BoxLayout添加一个height属性,并将其垂直size_hint属性设置为None。
size_hint属性是一个包含两个值的元组,影响小部件的宽度和高度。我们将在下一章讨论size_hint对不同布局的影响;现在,我们只需说,如果我们想为宽度或高度使用绝对数字,我们必须相应地将size_hint设置为None;否则,分配大小将不起作用,因为小部件将继续计算它自己的大小而不是使用我们提供的值。
在更新clock.kv文件以考虑计时器显示和控制后,它应该看起来类似于以下内容(注意布局的层次结构):
BoxLayout:
orientation: 'vertical'
Label:
id: time
text: '[b]00[/b]:00:00'
font_name: 'Roboto'
font_size: 60
markup: True
BoxLayout:
height: 90
orientation: 'horizontal'
padding: 20
spacing: 20
size_hint: (1, None)
Button:
text: 'Start'
font_name: 'Roboto'
font_size: 25
bold: True
Button:
text: 'Reset'
font_name: 'Roboto'
font_size: 25
bold: True
Label:
id: stopwatch
text: '00:00.[size=40]00[/size]'
font_name: 'Roboto'
font_size: 60
markup: True
如果我们现在运行代码,我们会注意到按钮没有填满BoxLayout内部的所有可用空间。这种效果是通过使用布局的padding和spacing属性实现的。Padding 与 CSS 非常相似,将子元素(在我们的例子中是按钮)从布局的边缘推开,而 spacing 控制相邻子元素之间的距离。这两个属性默认为零,旨在达到最大的小部件密度。
减少重复
这个布局是可行的,但有一个严重的问题:代码非常重复。我们可能想要做的每一个更改都必须在文件中的多个地方进行,很容易错过其中一个,从而引入不一致的更改。
注意
为了继续与网络平台的类比,在 CSS(层叠样式表)成为普遍可用之前,样式信息是直接写入围绕文本的标签中的。它看起来像这样:
<p><font face="Helvetica">Part 1</font></p>
<p><font face="Helvetica">Part 2</font></p>
使用这种方法,更改任何单个元素的属性很容易,但调整整个文档外观的属性则需要大量的手动劳动。如果我们想在下一版本的页面上更改字体为 Times,我们就必须搜索并替换 Helvetica 这个词的每个出现,同时确保运行文本中没有这个词,因为它可能偶尔也会被替换。
另一方面,使用样式表,我们将所有的样式信息移动到一个 CSS 规则中:
p {font-family: Helvetica}
现在我们只需要一个地方来处理文档中每个段落的样式;不再需要搜索和替换来更改字体或任何其他视觉属性,如颜色或填充。请注意,我们仍然可以稍微调整单个元素的属性:
<p style="font-family: Times">Part 3</p>
因此,通过实现 CSS,我们没有失去任何东西,实际上没有权衡;这解释了为什么在互联网上采用样式表非常快(特别是考虑到规模)并且非常成功。CSS 到今天仍在广泛使用,没有概念上的变化。
在 Kivy 中,我们不需要为我们的聚合样式或类规则使用不同的文件,就像在网页开发中通常所做的那样。我们只需在 BoxLayout 外部向 clock.kv 文件添加一个如下定义:
<Label>:
font_name: 'Roboto'
font_size: 60
markup: True
这是一个类规则;它的作用类似于之前信息框中描述的 CSS 选择器。每个 Label 都从 <Label> 类规则继承所有属性。(注意尖括号。)
现在,我们可以从每个单独的 Label 中移除 font_name、font_size 和 markup 属性。作为一个一般规则,总是努力将每个重复的定义移动到类中。这是一个众所周知的最佳实践,称为不要重复自己(DRY)。像前一个代码片段中所示的变化,在这样一个玩具项目中可能看起来微不足道,但最终会使我们的代码更加整洁和易于维护。
如果我们想覆盖某个小部件的属性,只需像往常一样添加即可。立即属性比从类定义中继承的属性具有优先级。
小贴士
请记住,类定义与在同一个 .kv 文件中定义的小部件完全不同。虽然语法在很大程度上是相同的,但类只是一个抽象定义;它本身不会创建一个新的小部件。因此,如果我们以后不使用它,添加类定义不会对应用程序引入任何更改。
命名类
之前描述的直接方法中存在的一个明显问题是,我们只能有一个名为Label的类。一旦我们需要将不同的属性集应用于同一种类型的控件,我们就必须为它们定义自己的自定义类。此外,覆盖框架的内置类,如Label或Button,可能会在整个应用程序中产生不希望的结果,例如,如果另一个组件正在使用我们底层更改的控件。
幸运的是,这很容易解决。让我们为按钮创建一个命名类,RobotoButton:
<RobotoButton@Button>:
font_name: 'Roboto'
font_size: 25
bold: True
@符号之前的部分指定了新的类名,后面跟着我们要扩展的控件类型(在 Python 中,我们会说class RobotoButton(Button):),然后可以使用这个结果类代替通用的Button类:
RobotoButton:
text: 'Start'
使用类规则允许我们在clock.kv文件中减少重复行的数量,并提供一种一致的方式来使用类定义调整类似的控件。接下来,让我们使用这个功能来自定义所有按钮。
按钮样式
平面 UI 范式的一个较暗的角落是可点击元素的外观,例如按钮;没有普遍接受的方式来设计它们。
例如,现代 UI 风格(以前称为 Metro,如 Windows 8 所示)非常激进,可点击元素看起来主要是单色矩形,几乎没有或没有明显的图形特征。其他供应商,如苹果,使用鲜艳的渐变;添加圆角的趋势也很明显,尤其是在网页设计中,因为 CSS3 提供了专门用于此的语法。微妙的阴影,虽然有些人认为这是异端,但也不是闻所未闻。
在这方面,Kivy 非常灵活。该框架不对视觉施加任何限制,并提供了许多有用的功能来实现您喜欢的任何设计。我们接下来要讨论的一个实用功能是 9-patch 图像缩放,它用于设计可能具有边框的按钮和类似控件。
9-patch 缩放
一个好的缩放算法的动机很简单:几乎不可能为每个按钮提供像素完美的图形,尤其是对于包含(不同数量的)文本的问题按钮。均匀缩放图像很简单实现,但结果最多是平庸的,部分原因是由于长宽比失真。
另一方面,非均匀的 9-patch 缩放产生了无妥协的质量。想法是将图像分成静态和可缩放的部分。以下图像是一个假设的可缩放按钮。中间部分(以黄色显示)是工作区域,其余部分都是边框:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_01_05.jpg
红色区域可以在一个维度上拉伸,而蓝色区域(角落)始终保持完整。这可以从以下屏幕截图中看出:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_01_06.jpg
蓝色显示的角是完全静态的,可能包含几乎所有内容。红色显示的边框在一维(顶部和底部边可以水平拉伸,左侧和右侧边可以垂直拉伸)上是可伸缩的。唯一将均匀缩放的图像部分是内矩形,即工作区域,黄色显示;因此,通常会用单色来绘制它。如果有的话,它还将包含分配给按钮的文本。
使用 9-patch 图像
对于这个教程,我们将使用一个简单的平面按钮,带有 1 像素的边框。我们可以为所有按钮重用这个纹理,或者选择不同的纹理,例如用于重置按钮。以下是一个正常状态下的按钮纹理,具有平面颜色和 1 像素边框:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_01_07.jpg
对应于按下状态的纹理——即前一个图像的反转——如下所示:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_01_08.jpg
现在,为了应用 9-patch 魔法,我们需要告诉 Kivy 具有限制伸缩性的边框的大小,如前所述(默认情况下,图像将均匀缩放)。让我们回顾一下 clock.kv 文件,并添加以下属性:
<RobotoButton@Button>:
background_normal: 'button_normal.png'
background_down: 'button_down.png'
border: (2, 2, 2, 2)
border 属性的值排序与 CSS 中的顺序相同:上、右、下、左(即从上开始按顺时针方向)。与 CSS 不同,我们不能为所有边提供单个值;至少在当前 Kivy 版本(1.8)中,border: 2 的表示法会导致错误。
小贴士
将所有边框设置为相同值的最短方法是 Python 语法 border: [2] * 4,这意味着取一个包含单个元素 2 的列表,并重复四次。
还要注意,虽然可见的边框只有一像素宽,但我们将自定义按钮的 border 属性分配为 2。这是由于渲染器的纹理拉伸行为:如果“切割线”两边的像素颜色不匹配,结果将是一个渐变,而我们希望是纯色。
在类规则概述中,我们提到在部件实例上声明的属性会优先于具有相同名称的类规则属性。这可以用来选择性地覆盖 background_*、border 或任何其他属性,例如,在重用边框宽度定义的同时分配另一个纹理:
RobotoButton:
text: 'Reset'
background_normal: 'red_button_normal.png'
background_down: 'red_button_down.png'
现在我们的按钮已经具有样式,但它们仍然没有任何功能。我们朝着目标迈出的下一步是使计时器工作。
计时
虽然计时器和常规时钟最终都只是显示时间,但在功能上它们完全不同。墙钟是一个严格递增的单调函数,而计时器时间可以被暂停和重置,减少计数器。更实际地说,区别在于操作系统可以轻松地将其内部墙钟暴露给 Python,无论是直接作为datetime对象,还是在strftime()函数的情况下透明地暴露。后者可以在没有datetime参数的情况下调用,以格式化当前时间,这正是我们需要用于墙钟显示的。
对于创建计时器的任务,我们首先需要构建自己的非单调时间计数器。这很容易实现,因为我们不需要使用 Python 的时间函数,多亏了 Kivy 的Clock.schedule_interval事件处理器,它接受调用之间的时间差作为参数。这正是以下代码中nap参数的作用:
def on_start(self):
Clock.schedule_interval(self.update, 0.016)
def update(self, nap):
pass
时间以秒为单位,也就是说,如果应用程序以 60 fps 运行并且每帧调用我们的函数,平均睡眠时间将是60^(−1)* = 0.016(6)*。
使用此参数后,跟踪经过的时间变得简单,可以通过简单的增量来实现:
class ClockApp(App):
sw_seconds = 0
def update(self, nap):
self.sw_seconds += nap
我们刚刚创建的这个计时器,按照定义并不是一个计时器,因为现在用户实际上无法停止它。然而,让我们首先更新显示以递增的时间,这样我们就可以在实现它们时立即看到控制的效果。
格式化计时器的时间
对于主要的时间显示,格式化很简单,因为标准库函数strftime为我们提供了一系列现成的原语,可以将datetime对象转换为可读的字符串表示,根据提供的格式字符串。
此函数有一些限制:
-
它只接受 Python
datetime对象(而计时器我们只有经过的浮点秒数sw_seconds) -
它没有为秒的小数部分提供格式化指令
前面的datetime限制可以很容易地规避:我们可以将我们的sw_seconds变量转换为datetime。但后者的不足使得这变得不必要,因为我们希望我们的表示以秒的分数结束(精确到 0.01 秒),所以strftime格式化就不够了。因此,我们实现自己的时间格式化。
计算值
首先,我们需要计算必要的值:分钟、秒和秒的分数。数学很简单;以下是计算分钟和秒的一行代码:
minutes, seconds = divmod(self.sw_seconds, 60)
注意使用divmod函数。这是一个简写,相当于以下内容:
minutes = self.sw_seconds / 60
seconds = self.sw_seconds % 60
虽然更简洁,但divmod版本在大多数 Python 解释器上也应该表现得更好,因为它只执行一次除法。在今天的机器上,浮点除法非常有效,但如果我们在每一帧运行大量此类操作,如视频游戏或模拟,CPU 时间将迅速增加。
小贴士
通常,作者倾向于不同意关于过早优化是邪恶的常见格言;许多导致性能不佳和标准不高的不良做法可以并且应该很容易避免,而不会影响代码质量,不这样做无疑是过早的悲观化。
还要注意,minutes 和 seconds 的值仍然是浮点数,因此在我们打印之前需要将它们转换为整数:int(minutes) 和 int(seconds)。
现在只剩下百分之一秒;我们可以这样计算它们:
int(seconds * 100 % 100)
放置停表
我们已经拥有了所有值;让我们将它们组合起来。在 Python 中格式化字符串是一项相当常见的任务,与 Python 的 Zen 命令“应该有一个——最好是只有一个——明显的做法来做这件事”(www.python.org/dev/peps/pep-0020/)相反,存在几种常见的字符串格式化惯用法。我们将使用其中最简单的一种,即操作符 %,它在某种程度上类似于在其他编程语言中常见的 sprintf() 函数:
def update_time(self, nap):
self.sw_seconds += nap
minutes, seconds = divmod(self.sw_seconds, 60)
self.root.ids.stopwatch.text = (
'%02d:%02d.[size=40]%02d[/size]' %
(int(minutes), int(seconds),
int(seconds * 100 % 100)))
由于我们现在有了秒的分数,之前使用的 1 fps 刷新频率已经不再足够。让我们将其设置为 0,这样 update_time 函数就会在每一帧被调用:
Clock.schedule_interval(self.update_time, 0)
小贴士
今天,大多数显示器以 60 fps 的刷新率运行,而我们的值精确到 1/100 秒,即每秒变化 100 次。虽然我们可以尝试以正好 100 fps 的速度运行我们的函数,但完全没有必要这样做:对于用户来说,在常见的硬件上不可能看到差异,因为显示器的更新频率最多也只有每秒 60 次。
话虽如此,大多数时候你的代码应该独立于帧率工作,因为它依赖于用户的硬件,而且无法预测应用程序最终会运行在什么机器上。即使今天的智能手机也有截然不同的系统规格和性能,更不用说笔记本电脑和台式计算机了。
就这样;如果我们现在运行应用程序,我们会看到一个递增的计数器。它目前还没有交互性,这将是我们的下一个目标。
停表控制
通过按钮按下事件控制应用程序非常简单。我们只需要使用以下代码来实现这一点:
def start_stop(self):
self.root.ids.start_stop.text = ('Start'
if self.sw_started else 'Stop')
self.sw_started = not self.sw_started
def reset(self):
if self.sw_started:
self.root.ids.start_stop.text = 'Start'
self.sw_started = False
self.sw_seconds = 0
第一个事件处理程序是为 开始 和 停止 按钮的。它改变状态(sw_started)和按钮标题。第二个处理程序将一切恢复到初始状态。
我们还需要添加状态属性来跟踪停表是正在运行还是暂停:
class ClockApp(App):
sw_started = False
sw_seconds = 0
def update_clock(self, nap):
if self.sw_started:
self.sw_seconds += nap
我们修改 update_clock 函数,使其仅在停表开始时增加 sw_seconds,即 sw_started 被设置为 True。最初,停表没有开始。
在 clock.kv 文件中,我们将这些新方法绑定到 on_press 事件:
RobotoButton:
id: start_stop
text: 'Start'
on_press: app.start_stop()
RobotoButton:
id: reset
text: 'Reset'
on_press: app.reset()
小贴士
在 Kivy 语言中,我们有几个上下文相关的引用可供使用。它们如下:
-
self:这始终指当前小部件; -
root:这是给定作用域的最外层小部件; -
app:这是应用程序类实例。
如您所见,实现按钮的事件处理并不困难。到目前为止,我们的应用程序提供了与计时器的交互,使用户能够启动、停止和重置它。为了本教程的目的,我们已经完成了。
摘要
在本章中,我们构建了一个功能性的 Kivy 应用程序,准备部署到例如 Google Play 或另一个应用商店供公众使用。这需要一些额外的工作,打包过程是平台特定的,但最困难的部分——编程——已经完成。
通过时钟应用程序,我们成功地展示了 Kivy 应用程序开发周期的许多方面,而没有使代码变得不必要地冗长或复杂。保持代码简短和简洁是框架的一个主要特点,因为它使我们能够快速地进行实验和迭代。能够以极少的旧代码阻碍,实现新的功能部分是无价的。Kivy 确实符合其作为快速应用程序开发库的描述。
在本书(以及 Kivy 开发总体上)中,我们将遇到的一个普遍原则是,我们的程序和 Kivy 都不是孤立存在的;我们始终拥有整个平台可供使用,包括丰富的 Python 标准库,以及从 Python“奶酪店”——位于pypi.python.org的Python 包索引(PyPI)以及其他地方可用的许多其他库,以及底层的操作系统服务。
我们还可以轻松地重新配置许多面向 Web 开发的资产,重用来自 CSS 框架(如 Bootstrap)的字体、颜色和形状。并且无论如何,都应该看看谷歌的材料设计原则——这不仅仅是一组设计资产,而是一本完整的指南,它使我们能够在不牺牲应用程序的个性或“性格”的情况下,实现一致且美观的用户界面。
当然,这仅仅是开始。本书中本章简要讨论的许多功能将在后面的章节中更深入地探讨。
第二章:构建绘画应用程序
在第一章《构建时钟应用程序》中,我们使用 Kivy 的标准组件:布局、文本标签和按钮构建了一个应用程序。我们能够在保持非常高层次抽象的同时显著自定义这些组件的外观——使用完整的控件,而不是单个图形原语。这对于某些类型的应用程序来说很方便,但并不总是理想的,并且正如你很快就会看到的,Kivy 框架还提供了用于较低层次抽象的工具:绘制点和线。
我认为,通过构建绘画应用程序来玩自由形式的图形是最好的方式。我们的应用程序完成时,将与 Windows 操作系统捆绑的 MS Paint 应用程序有些相似。
与 Microsoft Paint 不同,我们的 Kivy Paint 应用程序将完全跨平台,包括运行 Android 和 iOS 的移动设备。此外,我们还将故意省略“真实”软件中常见的许多图像处理功能,例如矩形选择、图层和将文件保存到磁盘。实现它们可以成为你的一项良好练习。
小贴士
关于移动设备:虽然使用 Kivy 构建 iOS 应用程序当然可能,但如果你没有 iOS 或 Kivy 开发的经验,这仍然是非平凡的。因此,建议你首先为易于使用的平台编写代码,这样你可以快速更新你的代码并运行应用程序,而无需构建二进制文件等。在这方面,由于 Kivy Launcher,Android 开发要简单得多,它是一个通用的环境,可以在 Android 上运行 Kivy 应用程序。它可在 Google Play 上找到,网址为 play.google.com/store/apps/details?id=org.kivy.pygame。
能够立即启动和测试你的应用程序而不需要编译,这是 Kivy 开发中一个极其重要的方面。这使得程序员能够快速迭代并现场评估可能的解决方案,这对于快速应用开发(RAD)和整体敏捷方法至关重要。
除了窗口大小调整(这在移动设备上不太常用)之外,Kivy 应用程序在各种移动和桌面平台上表现相似。因此,在发布周期后期之前,完全有可能只编写和调试桌面或 Android 版本的程序,然后填补任何兼容性差距。
我们还将探讨 Kivy 应用程序提供的两个独特且几乎相互排斥的功能:多指控制,适用于触摸屏设备,以及在桌面计算机上更改鼠标指针。
坚持其以移动端为先的方法,Kivy 提供了一个多触控输入的模拟层,可以用鼠标使用。它可以通过右键点击触发。然而,这种多触控模拟并不适合任何实际应用,除了调试;当在桌面运行时,它将在应用的生成版本中被关闭。
这就是本章结束时我们的应用将看起来像这样:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_01.jpg
Kivy 绘画应用,绘画工具另行销售
设置舞台
初始时,我们应用的全部表面都被根小部件占据,在这种情况下,那就是用户可以绘画的画布。我们不会在稍后为工具区域分配任何屏幕空间。
如你所知,根小部件是层次结构中最外层的小部件。每个 Kivy 应用都有一个,它可以基本上是任何东西,取决于期望的行为。如第一章中所示,构建时钟应用,BoxLayout是一个合适的选择作为根小部件;由于我们没有对它有额外的要求,布局被设计成作为其他控制容器的功能。
在绘画应用的情况下,我们需要其根小部件满足更多有趣的要求;用户应该能够画线,可能利用多触控功能,如果可用。目前,Kivy 没有内置的控制适合这项任务,因此我们需要自己创建。
构建新的 Kivy 小部件很简单。一旦我们的类从 Kivy 的Widget类继承,我们就可以开始了。所以,最简单的没有特殊功能的自定义小部件,以及使用它的小程序,可以像这样实现:
from kivy.app import App
from kivy.uix.widget import Widget
class CanvasWidget(Widget):
pass
class PaintApp(App):
def build(self):
return CanvasWidget()
if __name__ == '__main__':
PaintApp().run()
这是我们的绘画应用起始点的完整列表,main.py,包括PaintApp类。在未来的章节中,我们将省略像这样的简单样板代码;这个例子提供是为了完整性。
小贴士
Widget类通常作为基类,就像 Python 中的object或 Java 中的Object。虽然可以在应用中使用它“原样”,但Widget本身非常有限。它没有视觉外观,也没有在程序中立即有用的属性。另一方面,继承Widget相当直接且在许多不同场景中很有用。
调整外观
首先,让我们调整我们应用的外观。这并不是一个关键的功能,但请耐心,因为这些定制通常被请求,而且设置起来也很容易。我将简要描述我们在上一章中覆盖的属性,并添加一些新的调整,例如窗口大小和鼠标光标的更改。
视觉外观
我坚信,任何绘图应用程序的背景颜色最初应该是白色的。你可能已经从第一章中熟悉了这个设置。以下是我们在__name__ == '__main__'行之后添加的代码行,以实现所需的效果:
from kivy.core.window import Window
from kivy.utils import get_color_from_hex
Window.clearcolor = get_color_from_hex('#FFFFFF')
你可能希望将大多数import行放在它们通常所在的位置,即在程序文件的开始附近。正如你很快就会学到的,Kivy 中的某些导入实际上是顺序相关的,并且有副作用,最值得注意的是Window对象。在行为良好的 Python 程序中很少出现这种情况,导入语句的副作用通常被认为是不良的应用程序设计。
窗口大小
桌面应用程序的另一个常见调整属性是窗口大小。以下更改对移动设备将完全没有影响。
值得注意的是,默认情况下,桌面上的 Kivy 窗口可以被最终用户调整大小。我们将在稍后学习如何禁用此功能(仅为了完整性;通常这不是一个好主意)。
小贴士
当你针对已知规格的移动设备时,以编程方式设置窗口大小也是一个方便的做法。这允许你使用目标设备的正确屏幕分辨率在桌面上测试应用程序。
要分配初始窗口大小,将下一代码片段插入到读取from kivy.core.window import Window的行之上。在Window对象导入之前应用这些设置至关重要;否则,它们将没有任何效果:
from kivy.config import Config
Config.set('graphics', 'width', '960')
Config.set('graphics', 'height', '540') # 16:9
此外,你可能还想通过添加以下一行来禁用窗口调整大小:
Config.set('graphics', 'resizable', '0')
请除非你有非常充分的理由,否则不要这样做,因为从用户那里移除这些微不足道的定制通常是一个坏主意,并且很容易破坏整体用户体验。仅在一个分辨率下构建像素完美的应用程序很有吸引力,但许多客户(尤其是移动用户)可能不会满意。另一方面,Kivy 布局使得构建可伸缩界面变得可行。
鼠标光标
下一个通常仅适用于桌面应用程序的定制选项是更改鼠标指针。Kivy 没有对此进行抽象,因此我们将在一个较低级别上工作,直接从 Pygame 导入和调用方法,Pygame 是基于 SDL 的窗口和 OpenGL 上下文提供者,通常在桌面平台上被 Kivy 使用。
如果你选择实现此代码,应始终有条件地运行。大多数移动设备和一些桌面应用程序不会有 Pygame 窗口,我们当然希望避免因为鼠标光标这样的琐碎且非必要的事情而导致程序崩溃。
简而言之,这是 Pygame 使用的鼠标指针格式:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_11.jpg
用于描述自定义鼠标指针的 ASCII 格式
此记法中的每个字符代表一个像素:'@' 是黑色,'-' 是白色;其余的都是透明的。所有行都必须具有相同的宽度,且能被八整除(这是底层 SDL 实现强加的限制)。
当在应用程序中使用时,它应该看起来像下一张截图所示(图像被显著放大,显然):
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_02.jpg
Kivy Paint 应用程序的鼠标光标:一个十字准星
注意
然而,在撰写本文时,某些操作系统普遍可用的 Pygame 版本在 pygame.cursors.compile() 函数中存在一个在白色和黑色之间切换的漏洞。检测受影响的 Pygame 版本是不切实际的,所以我们将在代码中包含正确工作的函数,而不会调用可能存在漏洞的同名函数版本。
正确的函数 pygame_compile_cursor(),它将 Pygame 的鼠标光标定义转换为 Simple DirectMedia Layer(SDL)所期望的,Pygame 的后端库,可在网上找到:goo.gl/2KaepD。
现在,为了将生成的光标实际应用到应用程序窗口中,我们将用以下代码替换 PaintApp.build 方法:
from kivy.base import EventLoop
class PaintApp(App):
def build(self):
EventLoop.ensure_window()
if EventLoop.window.__class__.__name__.endswith('Pygame'):
try:
from pygame import mouse
# pygame_compile_cursor is a fixed version of
# pygame.cursors.compile
a, b = pygame_compile_cursor()
mouse.set_cursor((24, 24), (9, 9), a, b)
except:
pass
return CanvasWidget()
代码相当直接,但其中的一些方面可能需要解释。以下是一个快速浏览:
-
EventLoop.ensure_window():此函数调用会阻塞执行,直到我们有了应用程序窗口(EventLoop.window)。 -
if EventLoop.window.__class__.__name__.endswith('Pygame'):此条件检查窗口类名(不是对代码进行断言的最佳方式,但在这个情况下有效)。我们只想为特定的窗口提供者运行我们的鼠标光标定制代码,在这种情况下,是 Pygame。 -
代码的剩余部分,包含在
try ... except块中,是 Pygame 特定的mouse.set_cursor调用。 -
变量
a和b构成了 SDL 使用的鼠标内部表示,即 XOR 和 AND 掩码。它们是二进制的,应被视为 SDL 的一个不透明的实现细节。
注意
如同往常,请参阅官方参考手册以获取完整的 API 规范。Pygame 文档可在 www.pygame.org 找到。
当我们在比 Kivy 更低的抽象级别工作时,这种整个情况并不常见,但无论如何,不要害怕有时深入研究实现细节。因为 Kivy 不提供对这些事物的有意义的抽象,所以可以实现许多只有底层库才能实现的事情。这尤其适用于非跨平台功能,如操作系统依赖的应用程序互操作性、通知服务等等。
再次强调,此图总结了在此特定情况下设置鼠标指针时我们遍历的抽象级别:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_03.jpg
Kivy、Pygame、SDL 和底层操作系统抽象之间的关系
幸运的是,我们不必直接与操作系统交互——跨平台功能可能很难正确实现。这正是 SDL 所做的事情。
注意
虽然我们不直接与 SDL 交互,但你可能仍然想查看位于www.libsdl.org/的文档——这将为你提供关于 Kivy 最终依赖的低级 API 调用的视角。
多指模拟
默认情况下,当在桌面系统上运行时,Kivy 为多指操作提供了一种模拟模式。它通过右键点击激活,并生成永久触摸,以半透明的红色点形式渲染;同时也可以在按住右鼠标按钮的情况下拖动。
此功能对于调试可能很有用,尤其是在你没有真实的多指触摸设备进行测试时;另一方面,用户不会期望将此功能绑定到右键点击。可能最好禁用此功能,以免我们的用户被这种不太有用或明显的模拟模式所困惑。为此,请在初始化序列中添加以下内容:
Config.set('input', 'mouse', 'mouse,disable_multitouch')
如果你在开发过程中实际上使用此功能进行调试,可以在此时将其条件化(或暂时注释掉)。
绘图触摸
为了说明一个可能的触摸输入响应场景,让我们每次用户触摸(或点击)屏幕时都画一个圆。
Widget有一个on_touch_down事件,这对于这项任务非常有用。目前我们只对每个触摸的坐标感兴趣,它们可以通过以下方式访问:
class CanvasWidget(Widget):
def on_touch_down(self, touch):
print(touch.x, touch.y)
此示例在触摸发生时打印触摸的位置。要在屏幕上绘制某些内容,我们将使用Widget.canvas属性。Kivy 的Canvas是一个逻辑可绘制表面,它抽象化了底层的 OpenGL 渲染器。与低级图形 API 不同,画布是状态化的,并保留了添加到其中的绘图指令。
谈及绘图原语,许多都可以从kivy.graphics包中导入。绘图指令的例子包括Color、Line、Rectangle和Bezier等。
对画布的简要介绍
Canvas API 可以直接调用,也可以使用with关键字作为上下文处理器。简单的(直接)调用如下所示:
self.canvas.add(Line(circle=(touch.x, touch.y, 25)))
这会将一个带有参数的Line原语添加到图形指令队列中。
小贴士
如果你想要立即尝试这段代码,请参阅下一节,在屏幕上显示触摸,以获取在 Paint 应用上下文中使用画布指令的更全面的示例。
使用上下文处理器通常看起来更美观,也更简洁,尤其是在应用多个指令时。以下示例展示了这一点,其功能与之前的self.canvas.add()代码片段等效:
with self.canvas:
Line(circle=(touch.x, touch.y, 25))
这可能比直接方法更难理解。选择要使用的代码风格是个人偏好的问题,因为它们达到相同的效果。
注意,如前所述,每个后续调用都会添加到画布中,而不会影响之前应用的指令;在核心上,画布是一个随着每次将表面渲染到屏幕上而增长的指令数组。请记住:我们旨在达到 60 fps 的刷新率,我们当然不希望这个列表无限增长。
例如,一种在即时模式渲染表面(如 HTML5 的<canvas>)上正确工作的编码实践是通过用背景色覆盖来擦除之前绘制的图形。这在浏览器中相当直观且工作正常:
// JavaScript code for clearing the canvas
canvas.rect(0, 0, width, height)
canvas.fillStyle = '#FFFFFF'
canvas.fill()
相反,在 Kivy 中,这种模式仍然只是添加绘图指令;它首先渲染所有现有的原语,然后用矩形覆盖它们。这看起来几乎正确(画布在视觉上是空的),但做的是错误的事情:
# Same code as JavaScript above. This is wrong, don't do it!
with self.canvas:
Color(1, 1, 1)
Rectangle(pos=self.pos, size=self.size)
提示
就像内存泄漏一样,这个错误可能长时间不被注意,悄无声息地积累渲染指令并降低性能。多亏了今天设备中强大的显卡,包括智能手机,渲染通常非常快。所以在调试时很难意识到存在开销。
为了在 Kivy 中正确清除画布(即移除所有绘图指令),你应该使用本章后面展示的canvas.clear()方法。
显示屏幕上的触摸
我们将很快实现一个清除屏幕的按钮;在此期间,让我们显示屏幕上的触摸。我们移除了对print()的调用,并在CanvasWidget类定义中添加了以下方法:
class CanvasWidget(Widget):
def on_touch_down(self, touch):
with self.canvas:
Color(*get_color_from_hex('#0080FF80'))
Line(circle=(touch.x, touch.y, 25), width=4)
这会在我们的小部件接收到的每个触摸周围绘制一个空心的圆。Color指令设置了以下Line原语的颜色。
注意
注意,颜色格式(此处为#RRGGBBAA)并不严格遵循 CSS 规范,因为它有第四个组成部分,即 alpha 通道(透明度)。这种语法变化应该是显而易见的。它类似于例如在别处常见的rgb()和rgba()表示法。
你可能也注意到了我们在这里如何非常不寻常地使用Line,绘制的是圆而不是直线。许多 Kivy 图形原语都像这样强大。例如,任何画布指令,如Rectangle或Triangle原语,都可以通过source参数渲染背景图像。
如果你正在跟随,到目前为止的结果应该如下所示:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_04.jpg
显示屏幕上的触摸
到目前为止的完整源代码,用于生成前面的演示,如下所示:
# In main.py
from kivy.app import App
from kivy.config import Config
from kivy.graphics import Color, Line
from kivy.uix.widget import Widget
from kivy.utils import get_color_from_hex
class CanvasWidget(Widget):
def on_touch_down(self, touch):
with self.canvas:
Color(*get_color_from_hex('#0080FF80'))
Line(circle=(touch.x, touch.y, 25), width=4)
class PaintApp(App):
def build(self):
return CanvasWidget()
if __name__ == '__main__':
Config.set('graphics', 'width', '400')
Config.set('graphics', 'height', '400')
Config.set('input', 'mouse',
'mouse,disable_multitouch')
from kivy.core.window import Window
Window.clearcolor = get_color_from_hex('#FFFFFF')
PaintApp().run()
为了使示例代码简洁,我们排除了非必要的鼠标光标部分。在此阶段,伴随的 Kivy 语言文件 paint.kv 完全缺失——相反,应用类中的 build() 方法返回根小部件。
注意 import Window 行的异常位置。这是由于前面已经提到的该特定模块的副作用。Config.set() 调用应在此 import 语句之前进行,才能产生任何效果。
接下来,我们将向我们的小程序添加更多功能,使其与期望的绘图应用行为保持一致。
清除屏幕
目前,清除我们小程序屏幕的唯一方法是重新启动它。让我们向我们的 UI 添加一个按钮,用于从画布中删除所有内容,目前这个 UI 非常简约。我们将重用之前应用的按钮外观,因此在主题方面不会有任何新变化;有趣的部分在于定位。
在我们的第一个程序中,第一章 的时钟应用,构建时钟应用,我们没有进行任何显式的定位,因为所有内容都是由嵌套的 BoxLayouts 维持位置的。然而,现在我们的程序没有布局,因为根小部件就是我们的 CanvasWidget,我们没有实现任何定位其子部件的逻辑。
在 Kivy 中,没有显式布局意味着每个小部件都完全控制其位置和大小(这在许多其他 UI 工具包中几乎是默认状态,例如 Delphi、Visual Basic 等)。
要将新创建的删除按钮定位到右上角,我们进行以下操作:
# In paint.kv
<CanvasWidget>:
Button:
text: 'Delete'
right: root.right
top: root.top
width: 80
height: 40
这是一个属性绑定,表示按钮的 right 和 top 属性应与根小部件的属性相应地保持同步。我们也可以在这里进行数学运算,例如 root.top – 20。其余部分相当直接,因为 width 和 height 是绝对值。
还要注意,我们没有为 <CanvasWidget> 定义一个类规则,而没有指定其超类。这是因为这次我们正在扩展之前在 Python 代码中定义的具有相同名称的现有类。Kivy 允许我们增强所有现有的小部件类,包括内置的,如 <Button> 和 <Label>,以及自定义的。
这说明了使用 Kivy 语言描述对象视觉属性的一种常见良好实践。同时,最好将所有程序流程结构,如事件处理程序,保持在 Python 的一侧。这种关注点的分离使得 Python 源代码及其相应的 Kivy 语言对应物都更容易阅读和跟踪。
传递事件
如果你一直跟随着这个教程并且已经尝试点击按钮,你可能已经注意到(甚至猜到)它不起作用。它没有做任何有用的事情显然是因为缺少即将要实现的点击处理器。更有趣的是,点击根本无法穿透,因为没有视觉反馈;相反,通常的半透明圆圈被画在按钮上,这就是全部。
这种奇怪的效果发生是因为我们在 CanvasWidget.on_touch_down 处理器中处理了所有的触摸事件,而没有将它们传递给子元素,因此它们无法做出反应。与 HTML 的 文档对象模型 (DOM) 不同,Kivy 中的事件不是从嵌套元素向上冒泡到其父元素。它们是相反的方向,从父元素向下到子元素,也就是说,如果父元素将它们传递出去,而它没有这么做。
这可以通过明确执行以下类似代码来修复:
# Caution: suboptimal approach!
def on_touch_down(self, touch):
for widget in self.children:
widget.on_touch_down(touch)
实际上,这基本上就是默认行为 (Widget.on_touch_down) 已经做的事情,所以我们不妨调用它,使代码更加简洁,如下所示:
def on_touch_down(self, touch):
if Widget.on_touch_down(self, touch):
return
默认的 on_touch_down 处理器如果事件实际上以有意义的方式被处理,也会返回 True。触摸按钮会返回 True,因为按钮会对其做出反应,至少会改变其外观。这正是我们需要来取消我们自己的事件处理,这在当前情况下相当于绘制圆圈,因此方法中的第二行有 return 语句。
清除画布
现在我们转向最简单也是最实用的 删除 按钮部分——一个触摸处理器,它可以擦除一切。清除画布相当简单,所以为了使这个功能工作,我们需要做的所有事情都在这里。是的,总共只有两行代码:
def clear_canvas(self):
self.canvas.clear()
不要忘记将此方法作为事件处理器添加到 paint.kv 文件中:
Button:
on_release: root.clear_canvas()
它可以工作,但同时也移除了 删除 按钮本身!这是因为按钮是 CanvasWidget 的子元素(自然地,因为 CanvasWidget 是根元素,所有元素都是它的直接或间接子元素)。虽然按钮本身没有被删除(点击它仍然会清除屏幕),但其画布(Button.canvas)从 CanvasWidget.canvas.children 层级中移除,因此不再渲染。
解决这个问题非常直接的方法如下:
def clear_canvas(self):
self.canvas.clear()
self.canvas.children = [widget.canvas
for widget in self.children]
然而,这样做并不好,因为小部件可能会进行自己的初始化并按不同的方式排列。解决这个问题的更好方法是执行以下操作:
-
从“有罪”的元素(在这种情况下是
CanvasWidget)中移除所有子元素。 -
清除画布。
-
最后,重新添加子元素,以便它们可以正确地初始化渲染。
代码的修订版本稍微长一些,但工作正常且更健壮:
class CanvasWidget(Widget):
def clear_canvas(self):
saved = self.children[:] # See below
self.clear_widgets()
self.canvas.clear()
for widget in saved:
self.add_widget(widget)
一条可能需要解释的行是saved = self.children[:]表达式。[:]操作是一个数组复制(字面上,“创建一个包含这些相同元素的新数组”)。如果我们写成saved = self.children,这意味着我们正在复制一个数组的指针;稍后,当我们调用self.clear_widgets()时,它将从self.children和saved中删除所有内容,因为它们在内存中引用的是同一个对象。这就是为什么需要self.children[:]。(我们刚才讨论的行为是 Python 的工作方式,并且与 Kivy 无关。)
注意
如果你对 Python 中的切片语法不熟悉,请参阅 StackOverflow 论坛上的stackoverflow.com/questions/509211以获取示例。
在这个阶段,我们已经在某种程度上可以用蓝色气泡来绘画,如下面的截图所示。这显然不是我们绘画应用的最终行为,所以请继续阅读下一节,我们将使其能够绘制实际的线条。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_05.jpg
删除按钮的全貌及其令人敬畏的荣耀。还有,用圆形“画笔”绘画
连接点
我们的应用程序已经有了清除屏幕的功能,但仍然只绘制圆圈。让我们改变它,以便我们可以绘制线条。
为了跟踪连续的触摸事件(点击并拖动),我们需要添加一个新的事件监听器,on_touch_move。每次回调被调用时,它都会收到事件发生的最新位置。
如果我们每一刻只有一条线(就像在桌面上的典型做法一样,因为无论如何只有一个鼠标指针),我们就可以在self.current_line中保存我们正在绘制的线。但由于我们从一开始就旨在支持多点触控,我们将采取另一种方法,并将每条正在绘制的线存储在相应的touch变量中。
这之所以有效,是因为对于从开始到结束的每一次连续触摸,所有回调都接收相同的touch对象。还有一个touch.ud属性,其类型为dict(其中ud是用户数据的缩写),它专门用于在事件处理程序调用之间保持触摸特定的属性。最初,touch.ud属性是一个空的 Python 字典,{}。
我们接下来要做的就是:
-
在
on_touch_down处理程序中,创建一条新线并将其存储在touch.ud字典中。这次,我们将使用普通的直线而不是我们之前用来说明单个触摸会落在何处的那种花哨的圆形线条。 -
在
on_touch_move中,将一个新的点添加到对应线的末尾。我们正在添加一条直线段,但由于事件处理程序每秒将被调用多次,最终结果将是一系列非常短的段,但看起来仍然相当平滑。
小贴士
更高级的图形程序正在使用复杂的算法来使线条看起来像是绘制在真实的物理表面上。这包括使用贝塞尔曲线使线条即使在高分辨率下也看起来无缝,以及从指针移动的速度或压力中推断线条的粗细。我们在这里不会实现这些,因为它们与 Kivy 无关,但将这些技术添加到最终的 Paint 应用程序中可能对读者来说是一项很好的练习。
代码,正如我们刚才所描述的,列示如下:
from kivy.graphics import Color, Line
class CanvasWidget(Widget):
def on_touch_down(self, touch):
if Widget.on_touch_down(self, touch):
return
with self.canvas:
Color(*get_color_from_hex('#0080FF80'))
touch.ud['current_line'] = Line(
points=(touch.x, touch.y), width=2)
def on_touch_move(self, touch):
if 'current_line' in touch.ud:
touch.ud['current_line'].points += (touch.x, touch.y)
这种简单的方法是有效的,我们能够在画布上绘制无聊的蓝色线条。现在让我们给用户选择颜色的能力,然后我们就更接近一个真正有用的绘画应用程序了。
颜色调色板
每个绘画程序都附带一个调色板来选择颜色,在我们到达本节的结尾时,我们的也不例外,很快就会实现。
从概念上讲,调色板只是可用的颜色列表,以易于选择正确颜色的方式呈现。在一个完整的图像编辑器中,它通常包括系统上可用的所有颜色(通常是完整的 24 位真彩色或 16777216 种独特的颜色)。这种包含所有颜色的调色板的常规表示通常如下所示:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_06.jpg
真彩色调色板窗口的插图
另一方面,如果我们不想与流行的专有图像编辑应用程序竞争,我们不妨提供有限的调色板选择。对于在图形方面几乎没有背景的人来说,这甚至可能构成竞争优势——选择看起来搭配得当的颜色是困难的。正是出于这个原因,互联网上有一些调色板可以普遍用于 UI 和图形设计。
在这个教程中,我们将使用 Flat UI 风格指南(可在designmodo.github.io/Flat-UI/找到),它基于一组精心挑选的颜色,这些颜色搭配在一起效果很好。或者,你也可以自由选择你喜欢的任何其他调色板,这纯粹是审美偏好。
注意
在颜色领域有很多东西要学习,尤其是颜色兼容性和适合特定任务。低对比度的组合可能非常适合装饰元素或大标题,但对于主要文章的文字来说则不够;然而,出人意料的是,非常高的对比度,如白色与黑色,对眼睛来说并不容易,而且很快就会使眼睛疲劳。
因此,关于颜色的一个很好的经验法则是,除非你对你的艺术技能绝对自信,否则最好坚持使用由他人成功使用的既定调色板。一个好的开始是从你最喜欢的操作系统或桌面环境的指南开始。以下是一些例子:
-
广泛用于桌面 Linux 等开源环境中的 Tango 调色板可以在
tango.freedesktop.org/Tango_Icon_Theme_Guidelines找到。 -
2014 年 Google I/O 大会上提出的 Google 材料设计原则,可在
www.google.com/design/material-design.pdf找到。 -
非官方的 iOS 7 颜色样本可以在
ios7colors.com/找到(包括我在内,许多人认为这些颜色有些夸张且过于鲜艳,因此最适合游戏和广告,而不是日常使用的 UI)。
有许多更多适用于各种任务的调色板可供选择——如果您感兴趣,可以检查 Google,或者在您最喜欢的操作系统和程序上使用颜色选择器。
子类化按钮
由于我们正在寻找一个相对较短的固定颜色列表,因此最适合表示此类列表的用户界面控件可能是切换或单选按钮。Kivy 的ToggleButton非常适合这项任务,但它有一个不幸的限制:在切换组中,所有按钮可能一次全部取消选中。这意味着在绘图应用程序的上下文中,没有选择任何颜色。(在这种情况下,一个可能的选项是回退到默认颜色,但这可能会让用户感到惊讶,所以我们不会采取这种方法。)
好消息是,凭借 Python 的OOP(面向对象编程)功能,我们可以轻松地子类化ToggleButton并修改其行为以完成我们需要的任务,即禁止取消选中当前选中的按钮。在此调整之后,将始终只选择一个颜色。
在此情况下,子类化还将实现另一个目标:对于一个调色板,我们希望每个按钮都涂上其独特的颜色。虽然我们当然可以使用之前用于为按钮分配背景图像的技术,但这将需要我们制作大量的不同背景图像。相反,我们将使用背景颜色属性,该属性可以从paint.kv文件中分配。
这种架构允许我们在paint.kv文件中保持调色板定义的非常可读的声明性形式,同时将实现细节从我们的方式中排除在子类中——这正是面向对象程序应有的样子。
取消取消选择的能力
首先,让我们创建不能同时全部取消选中的切换按钮。
为了说明问题(并创建将作为起点的基础实现),让我们使用标准的 Kivy ToggleButton小部件实现所需的 UI。这部分完全是声明性的;让我们只需将以下代码添加到paint.kv文件的<CanvasWidget>部分底部:
BoxLayout:
orientation: 'horizontal'
padding: 3
spacing: 3
x: 0
y: 0
width: root.width
height: 40
ToggleButton:
group: 'color'
text: 'Red'
ToggleButton:
group: 'color'
text: 'Blue'
state: 'down'
我们在这里使用熟悉的 BoxLayout 组件,作为单个颜色按钮的工具栏。布局小部件本身被绝对定位,x 和 y 都设置为 0(即左下角),占据 CanvasWidget 的全部宽度。
每个 ToggleButton 都属于同一个组,'color',这样最多只能同时选中其中一个(state: 'down')。
覆盖标准行为
如前所述,内置的 ToggleButton 行为并不完全是我们需要的单选按钮;如果你点击已选中的按钮,它将被取消选中,整个切换组将没有选中的元素。
为了解决这个问题,让我们按照以下方式子类化 ToggleButton:
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.uix.togglebutton import ToggleButton
class RadioButton(ToggleButton):
def _do_press(self):
if self.state == 'normal':
ToggleButtonBehavior._do_press(self)
就这样。只有当按钮未被选中时(其 state 为 'normal',而不是 'down'),我们才允许按钮像平常一样切换。
现在剩下的只是将 paint.kv 文件中的每个 ToggleButton 实例替换为我们的自定义类 RadioButton 的名称,并立即看到按钮行为的变化。
这是 Kivy 框架的一个主要卖点:仅用几行代码,你就可以覆盖内置的函数和方法,实现几乎无与伦比的灵活性。
小贴士
要在 Kivy 语言中使用,RadioButton 定义应位于 main.py 模块中或导入其作用域。由于我们目前只有一个 Python 文件,这不是问题,但随着你的应用程序的增长,请记住这一点:自定义 Kivy 小部件,就像其他 Python 类或函数一样,在使用之前必须导入。
着色按钮
现在我们按钮的行为已经正确,下一步是着色。我们想要达到的效果如下所示:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_07.jpg
绘图应用的颜色调色板,鲜艳且吸引人
为了实现这一点,我们将使用 background_color 属性。在 Kivy 中,背景色充当色调而不是纯色;我们首先需要准备一个纯白色的背景图像,当它被着色时,将给出我们想要的颜色。这样,我们只需要为任意数量的任意颜色按钮准备两个按钮纹理(正常状态和按下状态)。
我们在这里使用的图像与我们之前为 第一章 中的时钟应用准备的图像没有太大区别,只是现在按钮的主要区域是白色,以便着色,而选中状态具有黑色边框:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_09.jpg
颜色按钮的纹理,其中白色区域将使用背景色属性进行着色
一种新的按钮类型
这次,我们可以在 paint.kv 文件中完成大部分工作,包括创建一个新的按钮类。新类将被命名为 ColorButton:
<ColorButton@RadioButton>:
group: 'color'
on_release: app.canvas_widget.set_color(self.background_color)
background_normal: 'color_button_normal.png'
background_down: 'color_button_down.png'
border: (3, 3, 3, 3)
如您所见,我们将 group 属性移动到这里,以避免在调色板定义中重复 group: 'color' 行。
我们还分配了一个事件处理器on_release,当按下ColorButton时将被调用。每个按钮都将其background_color属性传递给事件处理器,所以剩下的只是将此颜色分配给画布。此事件将由CanvasWidget处理,它需要从PaintApp类中公开,如下所示:
class PaintApp(App):
def build(self):
# The set_color() method will be implemented shortly.
self.canvas_widget = CanvasWidget()
self.canvas_widget.set_color(
get_color_from_hex('#2980B9'))
return self.canvas_widget
这种安排的原因是我们不能在先前的paint.kv类定义中使用root快捷方式;它将指向ColorButton本身(类规则中的根定义确实就是类规则本身,因为它在paint.kv的顶层定义)。我们还可以在此设置默认颜色,如代码片段所示。
当我们在main.py模块中时,让我们也实现CanvasWidget上的set_color()方法,它将作为ColorButton点击的事件处理器。所涉及的方法非常直接:
def set_color(self, new_color):
self.canvas.add(Color(*new_color))
只需设置传递给参数的颜色。就是这样!
定义调色板
接下来是创意部分:定义实际的调色板。在所有基础工作就绪后,让我们从paint.kv中删除旧的RadioButton定义,并重新开始。
要使用熟悉的 CSS 颜色表示法,我们需要将适当的函数导入到paint.kv文件中。是的,它可以导入函数,就像常规 Python 模块一样。
将此行添加到paint.kv的开头:
#:import C kivy.utils.get_color_from_hex
这与以下 Python 代码(为了简洁,使用了较短的别名,因为我们将会大量使用它)完全相同:
from kivy.utils import get_color_from_hex as C
如前所述,我们将使用 Flat UI 颜色为本例,但请随意选择您喜欢的调色板。定义本身看起来是这样的:
BoxLayout:
# ...
ColorButton:
background_color: C('#2980b9')
state: 'down'
ColorButton:
background_color: C('#16A085')
ColorButton:
background_color: C('#27AE60')
这种表示法尽可能清晰。对于每个ColorButton小部件,只需定义一个属性,即其background_color属性。其他所有内容都继承自类定义,包括事件处理器。
这种架构的美丽之处在于,现在我们可以添加任意数量的此类按钮,并且它们将正确对齐并执行。
设置线宽
我们将要实现的最后一个也是最简单的功能是一个简单的线宽选择器。如以下截图所示,我们正在重用之前部分的颜色调色板中的资产和样式。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_02_10.jpg
线宽选择器
这个 UI 使用了另一个RadioButton子类,无创意地命名为LineWidthButton。将以下声明追加到paint.kv文件中:
<LineWidthButton@ColorButton>:
group: 'line_width'
on_release: app.canvas_widget.set_line_width(self.text)
color: C('#2C3E50')
background_color: C('#ECF0F1')
与ColorButton的关键区别在前面代码中已突出显示。这些新按钮属于另一个单选组,并且在交互时触发另一个事件处理器。除此之外,它们非常相似。
布局同样简单,以与调色板相同的方式构建,只是它是垂直的:
BoxLayout:
orientation: 'vertical'
padding: 2
spacing: 2
x: 0
top: root.top
width: 80
height: 110
LineWidthButton:
text: 'Thin'
LineWidthButton:
text: 'Normal'
state: 'down'
LineWidthButton:
text: 'Thick'
注意
注意,我们新的事件监听器 CanvasWidget.set_line_width 将接受被按下的按钮的 text 属性。为了简单起见,它是这样实现的,因为这允许我们为每个小部件定义一个独特的属性。
在现实世界的场景中,这种方法并不是严格禁止的,也不是特别不常见,但仍然有点可疑:当我们决定将应用程序翻译成中文或希伯来语时,这些文本标签会发生什么?
改变线宽
当用户界面的每一部分都到位后,我们最终可以将事件监听器附加到将要应用所选线宽的绘画上。我们将基于提供的内联按钮文本映射,在 CanvasWidget.line_width 中存储线宽的数值,并在开始绘制新线条时在 on_touch_down 处理程序中使用它。简而言之,这些是修订后的 CanvasWidget 类的相关部分:
class CanvasWidget(Widget):
line_width = 2
def on_touch_down(self, touch):
# ...
with self.canvas:
touch.ud['current_line'] = Line(
points=(touch.x, touch.y),
width=self.line_width)
def set_line_width(self, line_width='Normal'):
self.line_width = {
'Thin': 1, 'Normal': 2, 'Thick': 4
}[line_width]
这就结束了 Kivy Paint 应用程序的教程。如果你现在启动程序,你可能会画出一幅美丽的作品。(我做不到,正如你可能从插图中注意到的。)
摘要
在本章中,我们强调了开发基于 Kivy 的应用程序的一些常见实践,例如自定义主窗口、更改鼠标光标、窗口大小和背景颜色、使用画布指令以编程方式绘制自由形式的图形,以及正确处理所有支持平台上的触摸事件,考虑到多点触控。
在构建 Paint 应用程序之后,关于 Kivy 的一个明显的事实是它如何开放和多功能。Kivy 不是提供大量刚性的组件,而是利用简单构建块的组合性:图形原语和行为。这意味着虽然 Kivy 没有捆绑很多有用的现成小部件,但你可以在几行高度可读的 Python 代码中组装出任何你需要的东西。
模块化 API 设计因其几乎无限的灵活性而效果显著。最终结果完美地满足了应用程序的独特需求。客户想要一些令人惊叹的东西,比如一个三角形按钮——当然,你还可以在上面添加纹理,大约只需要三行代码左右。(相比之下,尝试使用 WinAPI 创建一个三角形按钮。那就像凝视深渊,只是不那么富有成效。)
这些自定义 Kivy 组件通常也最终会变得可重用。实际上,你可以轻松地从 main.py 模块中导入 CanvasWidget 并在另一个应用程序中使用它。
自然用户界面
还值得一提的是,我们的第二个应用程序比第一个应用程序互动得多:它不仅对按钮点击做出响应,还对任意的多点触控手势也做出响应。
所有可用的窗口表面都能响应触摸,一旦对最终用户来说变得明显,就没有认知上的负担去绘画,尤其是在触摸屏设备上。你只需用手指在屏幕上画画,就像是在一个物理表面上,而且你的手指足够脏,可以在上面留下痕迹。
这种界面,或者说是没有这种界面,被称为NUI(自然用户界面)。它有一个有趣的特征:NUI 应用程序可以被小孩子甚至宠物使用——那些能够看到和触摸屏幕上图形对象的存在。这实际上是一个自然、直观的界面,一种“无需思考”的事情,与例如 Norton Commander 的界面形成对比,后者在当年被称为直观。让我们面对现实:那是个谎言。直觉在以任何实际方式应用于蓝屏、双面板 ASCII 艺术程序方面是不适用的。
在下一章中,我们将构建另一个基于 Kivy 的程序,这次仅限于 Android 设备。它将展示 Python 代码和 Java 类之间的互操作性,这些 Java 类构成了 Android API。
第三章。Android 录音机
在上一章中,我们简要讨论了 Kivy 应用程序,通常是跨平台的,其部分代码可能在选定的系统上条件性工作,从而增强某些用户的体验并执行其他特定平台的任务。
有时,这几乎是免费的;例如,如果 Kivy 检测到目标系统支持多点触控,多点触控就会正常工作——你不需要编写任何代码来启用它,只需考虑几个指针事件同时触发以供不同触控使用的情况。
其他平台相关任务包括由于各种原因在其他系统上无法运行的代码。还记得 Paint 应用程序中的鼠标光标自定义吗?那段代码使用了 Pygame 提供的低级绑定来调用 SDL 光标例程,只要你有 SDL 和 Pygame 运行,这是完全正常的。因此,为了使我们的应用程序多平台,我们采取了预防措施,避免在不兼容的系统上进入特定的代码路径;否则,它会导致我们的程序崩溃。
否则,Kivy 应用程序通常可以在所有支持的平台上移植——Mac、Windows、Linux、iOS、Android 和 Raspberry Pi——没有显著的问题。直到它们不再如此;我们将在下一节讨论这个原因。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_01.jpg
Kivy 支持广泛的平台
在本章中,我们将涵盖以下主题:
-
使用Pyjnius库实现 Python 和 Java 之间的互操作性
-
在运行 Android 操作系统的设备(或模拟器)上测试 Kivy 应用程序
-
从 Python 中与 Android 的声音 API 协同工作,这允许你录制和播放音频文件
-
制作类似 Windows Phone 概念的拼图用户界面布局
-
使用图标字体通过矢量图标改善应用程序的展示
编写平台相关代码
本书中的大多数项目都是跨平台的,这得益于 Kivy 的极高可移植性。然而,这次我们将有意识地构建一个单平台的应用程序。这无疑是一个严重的限制,会减少我们的潜在用户群;另一方面,这也给了我们依赖特定平台绑定的机会,这些绑定提供了扩展功能。
这种绑定的需求源于 Kivy 力求尽可能实现跨平台,并在它支持的每个系统上提供类似的用户体验。这本身就是一个巨大的特性;作为加分项,我们还有能力编写一次代码,在各个地方运行,几乎不需要任何调整。
然而,跨平台的缺点是,你只能依赖每个系统支持的核心理念功能。这个“最低共同分母”功能集包括在屏幕上渲染图形、如果有声卡则播放声音、接受用户输入以及其他不多的事情。
由于每个 Kivy 应用程序都是用 Python 编写的,因此它也可以访问庞大的 Python 标准库。它促进了网络通信,支持多种应用程序协议,并提供了许多通用算法和实用函数。
然而,“纯 Kivy”程序的输入输出(IO)能力仅限于大多数平台上存在的功能。这仅占一个普通计算机系统(如智能手机或平板电脑)实际能做的极小一部分。
让我们来看看现代移动设备的 API 表面(为了本章的目的,让我们假设它正在运行 Android)。我们将把所有内容分成两部分:由 Python 和/或 Kivy 直接支持的内容,以及不支持的内容。
以下是在 Python 或 Kivy 中直接可用的功能:
-
硬件加速图形
-
带有可选多点触控的触摸屏输入
-
音频播放(在撰写本文时,播放仅支持持久存储中的文件)
-
网络,假设存在互联网连接
以下是不支持或需要外部库的功能:
-
调制解调器,支持语音通话和短信
-
使用内置摄像头录制视频和拍照
-
使用内置麦克风来录制声音
-
与用户账户关联的应用程序数据云存储
-
蓝牙和其他近场网络功能
-
定位服务和 GPS
-
指纹识别和其他生物识别安全
-
运动传感器,即加速度计和陀螺仪
-
屏幕亮度控制
-
震动和其他形式的触觉反馈
-
电池充电水平
注意
对于“不支持”列表中的大多数条目,已经存在不同的 Python 库来填补空白,例如用于低级声音记录的 Audiostream 和用于处理许多平台特定任务的 Plyer。
所以,这些功能并不是完全不可用给你的应用程序;实际上,挑战在于这些功能片段在不同平台(或者甚至是同一平台的连续版本,例如 Android)上极其碎片化;因此,你最终不得不编写特定平台的、不可移植的代码。
如您从前面的比较中可以看到,Android 上提供了许多功能,但只有部分由现有的 Python 或 Kivy API 覆盖。这为在您的应用程序中使用平台特定功能留下了巨大的未开发潜力。这不仅仅是一个限制,而是一个机会。简而言之,您将很快学会如何从 Python 代码中利用任何 Android API,使您的 Kivy 应用程序几乎可以做任何事情。
将您的应用程序的范围缩小到只有一小部分系统的一个优势是,有一些全新的程序类只能在具有合适硬件规格的移动设备上运行(或甚至有意义)。这些包括增强现实应用程序、陀螺仪控制的游戏、全景相机等等。
介绍 Pyjnius
为了充分利用我们选择的平台,我们将使用特定于平台的 API,碰巧这个 API 是 Java,因此主要是面向 Java 的。我们将构建一个录音应用程序,类似于在 Android 和 iOS 中常见的应用程序,尽管更简单。与纯 Kivy 不同,底层的 Android API 确实为我们提供了编程记录声音的方法。
本章的其余部分将贯穿这个小录音程序的开发过程,使用优秀的 Pyjnius 库来展示 Python-Java 的互操作性,这是 Kivy 开发者制作的另一个伟大项目。我们选择的概念——录音和播放——故意很简单,以便在不引起主题的纯粹复杂性和大量实现细节的过多干扰的情况下,概述这种互操作性的功能。
Pyjnius 最有趣的特性是它不提供自己的“覆盖”API 来覆盖 Android 的 API,而是允许你直接从 Python 中使用 Java 类。这意味着你可以完全访问本地的 Android API 和官方的 Android 文档,这对于 Java 开发来说显然更合适,而不是 Python。然而,这仍然比完全没有 API 参考要好。
注意,你不需要在本地安装 Pyjnius 来完成教程,因为我们显然不会在用于开发的机器上运行调用 Android Java 类的代码。
Pyjnius 的源代码、参考手册和一些示例可以在官方仓库github.com/kivy/pyjnius找到。
小贴士
我们将仅在 Android 开发和互操作性的背景下讨论 Pyjnius,但请记住,你也可以用桌面 Java 进行同样的集成。这是一个有趣的特性,因为从 Python 脚本 Java API 的另一个选项是 Jython,它相当慢且不完整。另一方面,Pyjnius 允许你使用官方的 Python 解释器(CPython),以及像 NumPy 这样的众多库,这有助于非常快速的计算。
因此,如果你绝对必须从 Python 调用 Java 库,那么请务必考虑 Pyjnius 作为一个好的互操作变体。
模拟 Android
如前所述,本章的项目仅针对 Android,因此它不会在你的电脑上工作。如果你没有备用 Android 设备,或者如果你不觉得在教程的目的上玩真实的物理设备很舒服,请不要担心。有高质量的 Android 模拟器可以帮助你克服这个小小的障碍,并在你的桌面上玩 Android 操作系统。
目前市面上最好的模拟器之一是 Genymotion(之前称为 AndroVM),它建立在 Oracle 的 VirtualBox 虚拟机之上。你可以从官方网站 www.genymotion.com/ 获取免费副本;在撰写本文时,他们的许可非常宽松,允许几乎无限制的免费个人使用。
VM 软件的安装对于每个模拟器和主机操作系统组合都大不相同,所以我们现在不会提供过于详细的说明。毕竟,这些事情现在应该是用户友好的,包括说明书和图形用户界面。确实,我们已经进入了技术的黄金时代。
即使最后一句话并不完全是讽刺的,但在设置和使用 Android 模拟的虚拟机时,也有一些事情需要考虑:
-
总是使用最新的 Android 版本。向后兼容性或缺乏兼容性可能相当糟糕;调试操作系统级别的错误一点也不有趣。
-
不要犹豫,在网上搜索解决方案。Android 社区非常庞大,如果你有问题,这意味着你很可能并不孤单。
-
Kivy Launcher 应用程序,你可能觉得它非常有用,可以用来测试你自己的程序,可以从官方 Kivy 网站以
.apk文件的形式获取,kivy.org/;这对于没有访问 Google Play 的模拟 Android 设备来说将非常有用。 -
最后,市面上有许多不同质量、兼容性各异的模拟器。如果事情似乎随机崩溃并停止工作,也许你应该尝试另一个虚拟机或 Android 发行版。调整虚拟机的配置也可能有所帮助。
下一个截图展示了运行最新版本 Android 的 Genymotion 虚拟机,并安装了可用的 Kivy Launcher:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_02.jpg
运行 Android 4.4.2 并安装了 Kivy Launcher 的 Genymotion 虚拟机
Metro UI
当我们谈论这个话题时,让我们构建一个类似于 Windows Phone 主屏幕的用户界面。这个概念,基本上是一个各种尺寸的彩色矩形(瓷砖)的网格,在某个时候被称为 Metro UI,但由于商标问题后来更名为 Modern UI。不管叫什么名字,这就是它的样子。这将给你一个大致的想法,了解在应用程序开发过程中我们将要达到的目标:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_03.jpg
设计灵感 - 带有瓷砖的 Windows Phone 主屏幕
显然,我们不会完全复制它,而是制作一个类似于所描述的用户界面。以下列表基本上总结了我们要追求的独特特性:
-
所有的元素都对齐到矩形网格
-
UI 元素具有与 第一章 中讨论的相同扁平外观,构建时钟应用程序(瓷砖使用明亮的纯色,没有阴影或圆角)
-
被认为更有用的(对于“有用”的任意定义)瓷砖更大,因此更容易点击
如果这听起来对你来说很简单,那么你绝对是对的。正如你很快就会看到的,Kivy 实现这样的 UI 非常直接。
按钮们
首先,我们将调整一个 Button 类,就像我们在之前的程序中做的那样。它类似于 Paint 应用程序中的 ColorButton (第二章, 构建 Paint 应用程序):
<Button>:
background_normal: 'button_normal.png'
background_down: 'button_down.png'
background_color: C('#95A5A6')
font_size: 40
我们设置的背景纹理是纯白色,利用了在创建调色板时使用的相同技巧。background_color 属性充当着色色,将一个纯白色纹理分配给它相当于在 background_color 中绘制按钮。这次我们不想有边框。
第二个(按下 background_down)纹理是 25% 透明的白色。与应用程序的纯黑色背景颜色结合,我们得到了按钮分配的相同背景颜色的稍微深一点的色调:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_04.jpg
按钮的正常(左)和按下(右)状态——背景颜色设置为 #0080FF
网格结构
布局构建起来稍微复杂一些。在没有现成的类似现代 UI 的瓷砖布局可用的情况下,我们将使用内置的 GridLayout 小部件来模拟它。它表现得就像我们之前使用的 BoxLayout 小部件一样,只是在两个维度上而不是一个维度上,因此没有 orientation: 'horizontal' 或 'vertical' 属性——GridLayout 小部件同时具备这两个属性。
如果不是最后一个要求,这样一个布局就能满足我们的所有需求:我们想要有大小不同的按钮。目前,GridLayout 不允许合并单元格来创建更大的按钮(如果能有一个类似于 HTML 中的 rowspan 和 colspan 属性的功能那就太好了)。因此,我们将采取相反的方向:从根 GridLayout 开始,使用大单元格,并在一个单元格内添加另一个 GridLayout 来细分它。
由于嵌套布局在 Kivy 中表现良好,我们得到了以下 Kivy 语言结构(让我们将文件命名为 recorder.kv):
#:import C kivy.utils.get_color_from_hex
GridLayout:
padding: 15
Button:
background_color: C('#3498DB')
text: 'aaa'
GridLayout:
Button:
background_color: C('#2ECC71')
text: 'bbb1'
Button:
background_color: C('#1ABC9C')
text: 'bbb2'
Button:
background_color: C('#27AE60')
text: 'bbb3'
Button:
background_color: C('#16A085')
text: 'bbb4'
Button:
background_color: C('#E74C3C')
text: 'ccc'
Button:
background_color: C('#95A5A6')
text: 'ddd'
为了运行此代码,你需要一个标准的 main.py 模板作为应用程序的入口点。尝试自己编写这段代码作为练习。
小贴士
请参考第一章的开头。应用程序的类名将不同,因为它应该反映之前展示的 Kivy 语言文件的名称。
注意嵌套的 GridLayout 小部件与外部的、较大的按钮处于同一级别。如果你查看之前的 WinPhone 主屏幕截图,这应该会很有意义:一组四个较小的按钮占据与一个较大的按钮相同的空间(一个外部网格单元格)。嵌套的 GridLayout 是这些较小按钮的容器。
视觉属性
在外部网格上,padding 提供了一些距离屏幕边缘的空间。其他视觉属性在 GridLayout 实例之间共享,并移动到一个类中,结果在 recorder.kv 内部的代码如下:
<GridLayout>:
cols: 2
spacing: 10
row_default_height:
(0.5 * (self.width - self.spacing[0]) -
self.padding[0])
row_force_default: True
注意
值得注意的是,padding 和 spacing 都实际上是列表,而不是标量。spacing[0] 属性指的是水平间距,然后是垂直间距。然而,我们可以使用前面代码中显示的单个值来初始化 spacing;然后这个值将被用于所有内容。
每个网格由两列和一些间距组成。row_default_height 属性更复杂:我们不能只是说,“让行高等于单元格宽度。”相反,我们手动计算所需的高度,其中 0.5 是因为我们有两个列:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/7849OS_03_06.jpg
如果我们不应用这个调整,网格内的按钮将填充所有可用的垂直空间,这是不希望的,尤其是在按钮不多的情况下(每个按钮最终都会变得太大)。相反,我们希望所有按钮都整齐划一,底部左侧留有空白,嗯,就是空白。
以下是我们应用 “现代 UI” 磁贴的截图,这是前面代码的结果:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_07.jpg
到目前为止的 UI – 可点击的、大小可变的磁贴,与我们设计灵感不太相似
可缩放矢量图标
我们可以应用到应用程序 UI 中的一个很好的收尾细节是使用图标,而不仅仅是文本,在按钮上。当然,我们可以简单地加入一堆图片,但让我们借鉴现代网络开发中的一个有用技术,使用图标字体——正如你很快就会看到的,这些提供了极大的灵活性,而且不花任何成本。
图标字体
图标字体本质上与常规字体类似,只是它们的符号与语言的字母无关。例如,你输入 “P” 时,会渲染出 Python 的标志而不是字母;每个字体都会发明自己的记忆法来分配字母到图标。
这可能是使用图标字体唯一的缺点——使用这种字体大量代码的可读性并不好,因为字符-图标映射几乎不明显。这可以通过使用常量而不是直接输入符号来缓解。
还有不使用英语字母的字体,它们将图标映射到 Unicode 的 “私有用途区域” 字符代码。这是一种技术上正确构建此类字体的方法,但应用程序对这种 Unicode 功能的支持各不相同——不是每个平台在这方面表现都相同,尤其是在移动平台上。我们将为我们的应用使用的字体不分配私有用途字符,而是使用 ASCII(普通英语字母)。
使用图标字体的理由
在网络上,图标字体解决了与(光栅)图像常见的一些问题:
-
首先要考虑的是,位图图像不易缩放,在调整大小时可能会变得模糊——某些算法比其他算法产生更好的结果,但截至目前,“最佳实践”仍然不完美。相比之下,矢量图像按定义是无限可缩放的。
-
包含矢量图形(如图标和 UI 元素)的位图图像文件通常比矢量格式大。这显然不适用于编码为 JPEG 的照片。
-
此外,图标字体通常只是一个文件,包含任意数量的图标,这意味着只需要一次 HTTP 往返。常规图标(图像)通常在单独的文件中,导致显著的 HTTP 开销;有减轻这种影响的方法,例如 CSS 精灵,但它们并不被普遍使用,并且也有它们自己的问题。
-
在图标字体的情况下,颜色更改实际上只需一秒钟——你只需在 CSS 文件中添加
color: red(例如)即可做到这一点。同样,大小、旋转和其他不涉及改变图像几何形状的属性也是如此。实际上,这意味着对图标进行微调不需要图像编辑器,这在处理位图时通常是必需的。
其中一些观点对 Kivy 应用程序来说并不适用,但总的来说,在当代网络开发中使用图标字体被认为是一种良好的实践,特别是由于有许多免费的高质量字体可供选择——这意味着有成百上千的图标可以包含在你的项目中。
小贴士
两个免费字体(包括那些可以免费用于商业用途的字体)的绝佳来源是Font Squirrel(www.fontsquirrel.com)和Google Fonts(www.google.com/fonts)。不要在意这些网站的一般网络开发方向,大多数字体在离线程序中的可用性与在网络上一样,甚至更好。因为浏览器的支持仍然不是理想的。
真正重要的是文件格式:目前 Kivy 只支持 True Type(.ttf)格式。幸运的是,这已经是目前最流行的字体格式。此外,将任何其他格式的字体转换为.ttf格式也是可能的。
在 Kivy 中使用图标字体
在我们的应用程序中,我们将使用由 John Caserta 设计的 Modern Pictograms(版本 1)免费字体。以下是其外观的一瞥:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_08.jpg
Modern Pictograms 图标字体的一小部分图标样本
要将字体加载到我们的 Kivy 程序中,我们将使用在 第一章 中概述的相同过程,构建时钟应用程序。在这种情况下,这并不是严格必要的,因为图标字体很少有不同的字体粗细和样式。然而,通过显示名称(Modern Pictograms)而不是文件名(modernpics.ttf)来访问字体是一个更好的方法。你可以稍后通过只更新路径的一次出现来重命名或移动字体文件,而不必在每个使用字体的地方更新。
到目前为止的代码(在 main.py 中)看起来像这样:
from kivy.app import App
from kivy.core.text import LabelBase
class RecorderApp(App):
pass
if __name__ == '__main__':
LabelBase.register(name='Modern Pictograms',
fn_regular='modernpics.ttf')
RecorderApp().run()
字体的实际使用发生在 recorder.kv 内。首先,我们希望再次更新 Button 类,以便我们可以在文本中使用标记标签来更改字体。这在上面的代码片段中显示:
<Button>:
background_normal: 'button_normal.png'
background_down: 'button_down.png'
font_size: 24
halign: 'center'
markup: True
halign: 'center' 属性意味着我们希望按钮内的每一行文本都居中。markup: True 属性是显而易见的,并且是必需的,因为按钮定制的下一步将严重依赖于标记。
现在我们可以更新按钮定义。以下是一个例子:
Button:
background_color: C('#3498DB')
text:
('[font=Modern Pictograms][size=120]'
'e[/size][/font]\nNew recording')
通常,在 Kivy 语言文件中不需要在字符串周围使用括号;这种语法仅在声明多行时有用。这种表示法实际上等同于在同一行上写一个长字符串。
注意 [font][size] 标签内的字符 'e'。这是图标代码。我们应用程序中的每个按钮都将使用不同的图标,更改图标相当于在 recorder.kv 文件中替换一个字母。Modern Pictograms 字体的代码完整映射可以在其官方网站 modernpictograms.com/ 上找到。
小贴士
为了手动探索图标字体,你需要使用字体查看器。通常,无论操作系统如何,你的机器上都会有一个现成的查看器。
-
字符映射 程序是 Windows 的一部分
-
在 Mac 上,有一个内置的应用程序叫做 Font Book
-
Linux 有多个查看器,取决于你选择的桌面环境,例如,GNOME 中的 gnome-font-viewer
或者,只需在网上搜索。流行的字体通常有一些在线的用户手册,解释字符映射。
简而言之,这就是我们在按钮上添加图标后应用程序的 UI 看起来是什么样子:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_09.jpg
声音录制器应用程序界面 - 一个具有来自 Modern Pictograms 字体的矢量图标的现代 UI
这已经非常接近原始的 Modern UI 外观了。
注意
你可能会想知道顶部右角的小绿色按钮的用途是什么。答案是,目前它们仅仅是为了数量。实际上我们需要实现的三个按钮——录音、播放、删除——不足以说明 Modern UI 的概念,因为它需要更多的多样性才能看起来稍微有趣一些。
在 Android 上进行测试
目前,我们的应用程序还不包含任何不可移植的代码,但让我们逐步转向我们选择的平台,并在 Android 上进行测试。进行此操作的唯一先决条件是安装并运行Kivy Launcher应用程序的 Android 设备,无论是物理的还是虚拟的。
为 Kivy Launcher 打包应用程序几乎微不足道。我们将添加两个文件,android.txt和icon.png,到其他源(在这种情况下,main.py和recorder.kv)所在的同一文件夹,然后将文件夹复制到 Android 设备的 SD 卡上的/Kivy目录下。目录结构应类似于以下内容:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_10.jpg
Kivy Launcher 的 SD 卡目录结构
当您启动 Kivy Launcher 时,它将显示它搜索项目的完整路径。这可能很有用,例如,当您没有 SD 卡时。
android.txt文件的格式相当明显:
title=App Name
author=Your Name
orientation=portrait
标题和作者字段只是显示在应用程序列表中的字符串。方向可以是纵向(垂直,高度 > 宽度)或横向(水平,宽度 > 高度),具体取决于应用程序首选的宽高比。
图标icon.png是可选的,如果省略,则将保持空白。建议添加它,因为根据图标查找应用程序要容易得多,而且如果您计划将生成的应用程序发布到 Google Play 商店,您无论如何都需要一个图标。
注意,图标的文件名不可自定义,main.py的文件名也不可自定义,它必须指定应用程序的入口点;否则,Kivy Launcher 不会启动应用程序。
当所有文件就绪后,您在启动 Kivy Launcher 时应该能在列表中看到您的录音程序:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_11.jpg
Kivy Launcher 应用程序列表,包含我们在本书的整个过程中编写的每个应用程序
小贴士
如果您看到一条包含放置文件指示的消息,请重新检查您的路径——遗憾的是,在撰写本文时,Kivy Launcher 搜索项目所在的目录不容易配置。这可能在未来的版本中得到改善。
现在您可以通过点击列表中的相应条目来启动您的应用程序。这是在 Android 上测试 Kivy 程序的最简单方法——只需复制文件,您就设置好了(与打包.apk文件相比,后者相对简单,但涉及更多步骤)。
使用原生 API
完成了应用的用户界面部分后,我们现在将转向原生 API,并使用合适的 Android Java 类MediaRecorder和MediaPlayer来实现声音录制和播放逻辑。
技术上,Python 和 Java 都是面向对象的,乍一看,这些语言可能看起来相当相似。然而,面向对象原则的应用却有着根本的不同。与 Python 相比,许多 Java API 都存在(或者根据您询问的人不同,可能会非常享受)过度架构和过度使用面向对象范式的问题。所以,不要对其他非常简单的任务可能需要您导入和实例化很多类而感到惊讶。
注意
1913 年,弗拉基米尔·列宁就 Java 架构写道:
要打破这些类的阻力,只有一个方法,那就是在我们周围的社会中找到能够构成扫除旧事物、创造新事物的力量的力量。
那篇论文当时没有提到 Python 或 Pyjnius,但信息很明确——即使在一百年前,在当代社会中过度使用类也不是很受欢迎。
幸运的是,手头的任务相对简单。要使用 Android API 录制声音,我们只需要以下五个 Java 类:
-
android.os.Environment:此类提供了访问许多有用环境变量的权限。我们将使用它来确定 SD 卡挂载的路径,以便我们可以保存录制的音频文件。直接硬编码'/sdcard/'或类似的常量很有诱惑力,但在实践中,每个其他 Android 设备的文件系统布局都不同。所以,即使是为了教程的目的,我们也不应该这样做。 -
android.media.MediaRecorder:此类是我们的主要工作马。它便于捕捉音频和视频并将其保存到文件系统中。 -
android.media.MediaRecorder$AudioSource,android.media.MediaRecorder$AudioEncoder,和android.media.MediaRecorder$OutputFormat:这些是枚举,包含我们需要传递给MediaRecorder各种方法的参数。
提示
Java 类命名方案
类名中的美元符号通常表示该类是内部的。这并不是一个精确的启发式方法,因为您可以在没有任何逻辑的情况下自己声明一个类似的名字——'$'是 Java 变量和类名中可用的字符,与例如 JavaScript 等语言类似。然而,这种非常规的命名是不被提倡的。
加载 Java 类
将上述 Java 类加载到您的 Python 应用程序中的代码如下:
from jnius import autoclass
Environment = autoclass('android.os.Environment')
MediaRecorder = autoclass('android.media.MediaRecorder')
AudioSource = autoclass('android.media.MediaRecorder$AudioSource')
OutputFormat = autoclass('android.media.MediaRecorder$OutputFormat')
AudioEncoder = autoclass('android.media.MediaRecorder$AudioEncoder')
如果您此时尝试运行程序,您将收到一个错误,类似于以下内容:
-
ImportError: 没有名为 jnius 的模块:如果您在机器上没有安装 Pyjnius,您将遇到此错误。
-
jnius.JavaException: 类未找到 ‘android/os/Environment’:如果您安装了 Pyjnius,但尝试加载的 Android 类缺失(例如,在桌面机器上运行时),您将遇到此错误。
这是一种罕见的情况,收到错误意味着我们做的一切都是正确的。从现在开始,我们应该在 Android 设备或模拟器内部进行所有测试,因为代码不再是跨平台的了。它明确依赖于 Android 特定的 Java 功能。
现在我们可以无缝地在我们的 Python 代码中使用 Java 类。
小贴士
请记住,这些类的文档是为与 Java 一起使用而编写的,而不是 Python。你可以在 Google 官方的 Android 开发者门户上查找它 developer.android.com/reference/packages.html——最初将代码示例从 Java 转换为 Python 可能看起来有些令人生畏,但实际上非常简单(如果有些啰嗦)。
查找存储路径
让我们用一个简单的例子来说明实际的多语言 API 使用。在 Java 中,我们会这样做来找出 SD 卡挂载的位置:
import android.os.Environment;
String path = Environment.getExternalStorageDirectory()
.getAbsolutePath();
当转换为 Python 时,此代码的读取方式如下:
Environment = autoclass('android.os.Environment')
path = Environment.getExternalStorageDirectory().getAbsolutePath()
这与之前代码中显示的完全相同,只是用 Python 而不是 Java 编写的。
当我们在做这件事的时候,也让我们记录这个值,这样我们就可以在 Kivy 日志中看到 getAbsolutePath 方法返回给我们的代码的确切路径:
from kivy.logger import Logger
Logger.info('App: storage path == "%s"' % path)
在我的测试设备上,这会在 Kivy 日志中产生以下行:
[INFO] App: storage path == "/storage/sdcard0"
从设备读取日志
当你在开发期间从终端运行 Kivy 应用程序时,日志会立即在同一终端窗口中显示。这个非常有用的功能在应用程序在 Kivy 启动器内部运行时也是可用的,尽管不太容易访问。
要读取 Kivy 日志,导航到设备上你的应用程序所在的文件夹(例如,SD 卡上的 /Kivy/Recorder)。在这个文件夹内部,Kivy 启动器创建了一个名为 .kivy 的另一个目录,其中包含默认配置和一些杂项服务信息。每次应用程序启动时,都会在 .kivy/logs 下创建一个日志文件。
或者,如果你已经安装了 Android SDK,你可以在设备上启用 USB 调试,然后使用 adb logcat 命令在一个地方查看所有 Android 日志,包括 Kivy 日志。这会产生关于设备内部发生的各种内部过程的大量信息,例如各种硬件的激活和去激活、应用程序窗口状态的变化等等。
日志在调试奇怪的程序行为或当应用程序拒绝启动时非常有价值。Kivy 还会在那里打印关于运行时环境的各种警告,例如缺少库或功能、Python 模块加载失败以及其他潜在问题。
录制声音
现在,让我们深入 Android API 的兔子洞,实际上从麦克风录制声音。以下代码基本上是将 Android API 文档翻译成 Python。如果您对这段代码的原始 Java 版本感兴趣,可以在developer.android.com/guide/topics/media/audio-capture.html找到——它太长了,无法在这里包含。
下面的代码是初始化MediaRecorder对象的准备代码:
storage_path = (Environment.getExternalStorageDirectory()
.getAbsolutePath() + '/kivy_recording.3gp')
recorder = MediaRecorder()
def init_recorder():
recorder.setAudioSource(AudioSource.MIC)
recorder.setOutputFormat(OutputFormat.THREE_GPP)
recorder.setAudioEncoder(AudioEncoder.AMR_NB)
recorder.setOutputFile(storage_path)
recorder.prepare()
这就是典型的、直接的、冗长的 Java 初始化方式,用 Python 逐字重写。
您可以在这里调整输出文件格式和编解码器,例如,将AMR_NB(自适应多速率编解码器,针对语音优化,因此在 GSM 和其他移动电话网络中广泛使用)更改为AudioEncoder.AAC(高级音频编解码器标准,是一种更通用的编解码器,类似于 MP3)。这样做可能没有很好的理由,因为内置麦克风的动态范围可能不适合录制音乐,但选择权在您手中。
现在是时候来点乐趣了,“开始/结束录音”按钮。以下代码片段使用了与第一章中实现计时器开始/停止按钮时相同的逻辑:
class RecorderApp(App):
is_recording = False
def begin_end_recording(self):
if (self.is_recording):
recorder.stop()
recorder.reset()
self.is_recording = False
self.root.ids.begin_end_recording.text = \
('[font=Modern Pictograms][size=120]'
'e[/size][/font]\nBegin recording')
return
init_recorder()
recorder.start()
self.is_recording = True
self.root.ids.begin_end_recording.text = \
('[font=Modern Pictograms][size=120]'
'%[/size][/font]\nEnd recording')
如您所见,这里也没有应用任何火箭科学:我们只是存储了当前状态,is_recording,然后根据它采取行动,即:
-
开始或停止
MediaRecorder对象(高亮部分)。 -
翻转
is_recording标志。 -
更新按钮文本,使其反映当前状态(见以下截图)。
需要更新的应用程序的最后部分是recorder.kv文件。我们需要调整“开始/结束录音”按钮,使其调用我们的begin_end_recording()函数:
Button:
id: begin_end_recording
background_color: C('#3498DB')
text:
('[font=Modern Pictograms][size=120]'
'e[/size][/font]\nBegin recording')
on_press: app.begin_end_recording()
就这样!如果您现在运行应用程序,很可能会记录下要存储在 SD 卡上的声音文件。然而,在这样做之前,请先查看下一节。您创建的按钮看起来可能如下所示:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_03_12.jpg
开始录音和结束录音——这个按钮概括了我们的应用程序到目前为止的功能
主要注意事项——权限
编写本文时,默认的 Kivy 启动器应用程序没有记录声音所需的权限,android.permission.RECORD_AUDIO。这导致MediaRecorder实例初始化时立即崩溃。
有许多方法可以减轻这个问题。首先,最简单的一个:为了这个教程,我们已提供修改后的 Kivy Launcher,其中已启用必要的权限。你可以在书籍的源代码存档中找到KivyLauncherMod.apk。该包的最新版本也可在github.com/mvasilkov/kivy_launcher_hack下载。
在安装提供的.apk文件之前,请从你的设备中删除现有版本的应用,如果有的话。
或者,如果你愿意处理为 Google Play 打包 Kivy 应用的繁琐细节,你可以从源代码自行构建 Kivy Launcher。完成这项工作所需的所有信息都可以在官方 Kivy GitHub 账户github.com/kivy中找到。
第三种可行的选择(也可能比前一个更容易)是调整现有的 Kivy Launcher 应用。为此,你可以使用apktool(code.google.com/p/android-apktool/)。你需要采取的确切步骤如下:
-
下载官方的
KivyLauncher.apk文件,并从命令行提取它,假设 apktool 在你的路径中:apktool d -b -s -d KivyLauncher.apk KivyLauncher -
将必要的权限声明添加到
AndroidManifest.xml文件中:<uses-permission android:name="android.permission.RECORD_AUDIO" /> -
以这种方式重新打包
.apk:apktool b KivyLauncher KivyLauncherWithChanges.apk -
使用
jarsigner实用程序对生成的.apk文件进行签名。请查看关于在developer.android.com/tools/publishing/app-signing.html#signing-manually手动签名 Android 包的官方文档。
由于此过程,修改后的 Kivy Launcher 包将能够录制声音。
小贴士
你可以用同样的方式添加各种其他权限,以便在 Python 代码中使用 Pyjnius 利用它们。例如,为了访问 GPS API,你的应用需要android.permission.ACCESS_FINE_LOCATION权限。
所有可用的权限都在 Android 文档developer.android.com/reference/android/Manifest.permission.html中列出。
播放声音
要使声音播放工作变得容易;不需要此权限,API 也更为简洁。我们只需要加载一个额外的类,MediaPlayer:
MediaPlayer = autoclass('android.media.MediaPlayer')
player = MediaPlayer()
以下是在用户按下播放按钮时运行的代码。我们还会在删除文件部分使用reset_player()函数;否则,可能有一个稍微长一点的函数:
def reset_player():
if (player.isPlaying()):
player.stop()
player.reset()
def restart_player():
reset_player()
try:
player.setDataSource(storage_path)
player.prepare()
player.start()
except:
player.reset()
每个 API 调用的详细情况可以在官方文档中找到,但总体来说,这个列表相当直观:将播放器重置到初始状态,加载声音文件,然后播放。文件格式会自动确定,这使得我们的任务变得稍微容易一些。
小贴士
在实践中,这样的代码应该始终被包裹在一个 try ... catch 块中。可能会有太多事情出错:文件可能丢失或以错误的格式创建,SD 卡可能被拔掉或无法读取,以及其他同样可怕的事情,如果有机会,这些事情一定会让你的程序崩溃。在进行输入输出操作时,一个好的经验法则是宁可靠安全,不可靠后悔。
删除文件
这个最后的功能将使用 java.io.File 类,它与 Android 并不严格相关。官方 Android 文档的一个优点是,它还包含了这些核心 Java 类的引用,尽管它们比 Android 操作系统早了十多年。实现文件删除的实际代码正好是一行;它在下述列表中突出显示:
File = autoclass('java.io.File')
class RecorderApp(App):
def delete_file(self):
reset_player()
File(storage_path).delete()
首先,我们通过调用 reset_player() 函数停止播放(如果有),然后删除文件——简单直接。
有趣的是,Java 中的 File.delete() 方法在发生灾难性故障时不会抛出异常,因此在这种情况下不需要执行 try ... catch。一致性,无处不在的一致性。
注意
一个细心的读者会注意到,我们也可以使用 Python 自己的 os.remove() 函数来删除文件。与纯 Python 实现相比,这样做在 Java 中并没有什么特别之处;它也更快。另一方面,作为 Pyjnius 的演示,java.io.File 与其他 Java 类一样有效。
还要注意,这个函数在桌面操作系统上也能完美运行,因为它不是 Android 特有的;你只需要安装 Java 和 Pyjnius,这个功能才能工作。
到目前为止,随着用户界面和所有三个主要功能的完成,我们的应用程序对于本教程的目的来说已经完整了。
摘要
编写不可移植的代码有其优点和缺点,就像任何其他全局架构决策一样。然而,这个特定的选择尤其困难,因为切换到本地 API 通常发生在项目早期,并且在后期阶段可能完全不切实际地撤销。
这种方法的重大优势在本章的开头已经讨论过:使用平台特定代码,你可以几乎做你平台能做的任何事情。没有人为的限制;你的 Python 代码对相同的底层 API 的访问是无限的。
从另一方面来看,依赖于单一平台是有风险的,原因有很多:
-
仅 Android 的市场规模就比 Android 加上 iOS 的市场规模要小(这适用于几乎所有的操作系统组合)。
-
随着你使用的每个平台特定功能,将程序移植到新系统变得更加困难。
-
如果项目只运行在一个平台上,可能只需要一个政治决定就足以将其扼杀。被谷歌封禁的可能性高于同时被 App Store 和 Google Play 踢出的可能性。(同样,这几乎适用于每一组应用程序市场。)
现在你已经充分了解了各种选项,那么在开发每一个应用程序时,做出明智的选择就取决于你了。
关于 UI 的一些话
在任何情况下,你都不应该犹豫去借鉴和重新实现在其他地方看到的思想(以及布局、字体、颜色等等)。归功于巴勃罗·毕加索的这句话,“好艺术家借鉴;伟大的艺术家偷窃”,简洁地总结了今天的网络和应用程序开发。 (“偷窃”部分是比喻性的:请实际上不要真的去偷东西。)
此外,让我们明确一点:仅仅因为微软决定在许多最新的移动和桌面产品中使用“现代 UI”,并不意味着这种设计本身有什么优点。我们确信的是,由于微软操作系统的普及,这种用户界面范式将立即被用户识别出来,无论这种普及是幸运的还是不幸的。
在下一章中,我们将放下 Java,使用流行的 Python 网络框架Twisted构建一个简单的基于客户端-服务器架构的聊天程序。
第四章:Kivy 网络
之前,我们讨论了在扩展功能集的同时缩小应用兼容性的权衡,例如,仅使用原生 API 进行重负载处理的 Android 应用。现在,让我们探索相反的极端,并基于不妥协、普遍可用的功能——网络——来构建一个应用。
在本章中,我们将构建一个聊天应用,其概念与互联网中继聊天(IRC)类似,但更加简单。
虽然我们的小应用当然不能取代像 Skype 这样的企业级巨无霸,但到本章结束时,我们的应用将支持互联网上的多用户消息传递。这对于小型友好群体来说已经足够了。
友好实际上是一个要求,因为我们有意简化事情,没有实现身份验证。这意味着用户可以轻易地模仿彼此。调整应用程序以适应敌对环境和灾难性事件(如政治辩论)的任务留给你去完成,如果你特别有冒险精神的话。
我们还旨在实现尽可能广泛的兼容性,至少在服务器端;你甚至可以使用Telnet发送和接收消息。虽然不如图形 Kivy 应用那么美观,但 Telnet 在 Windows 95 甚至 MS-DOS 上运行得很好。与恐龙聊天吧!
注意
为了更准确地反映历史,Telnet 协议是在 1973 年标准化的,因此它甚至早于 8086 CPU 和 x86 架构。相比之下,MS-DOS 要现代得多,而 Windows 95 几乎可以说是计算的未来。
本章将涵盖以下重要主题:
-
使用 Python 编写和测试自定义服务器,采用Twisted框架
-
在不同抽象级别上开发几个客户端应用,从使用原始套接字的简单终端程序到事件驱动的 Twisted 客户端
-
使用 Kivy
ScreenManager更好地组织应用 UI -
使用
ScrollView容器有效地在屏幕上展示长文本小部件
我们的应用程序将采用集中式、客户端-服务器架构;这种拓扑在互联网上非常常见,许多网站和应用都是这样工作的。您很快就会看到,与去中心化、点对点网络相比,实现起来也相当简单。
注意
为了本章的目的,我们不区分局域网(LAN)和互联网,因为在抽象的这一层,这基本上是不相关的。然而,请注意,如果正确部署您的应用程序以供互联网大规模消费,这需要许多额外的知识,从设置安全的 Web 服务器和配置防火墙到使代码跨多个处理器核心甚至多台物理机器扩展。在实践中,这可能没有听起来那么可怕,但本身仍然是一项非同小可的任务。
编写聊天服务器
让我们从服务器端代码开始开发,这样在我们开始编写客户端之前就有了一个连接的端点。为此,我们将使用一个优秀的Twisted框架,该框架将许多常见的低级别网络任务简化为少量干净、相对高级的 Python 代码。
小贴士
兼容性通知
Twisted 在撰写本文时不支持 Python 3,因此我们假设以下所有 Python 代码都是针对 Python 2.7 编写的。最终应该很容易将其移植到 Python 3,因为没有任何故意的不兼容设计决策。(相关地,我们还将完全忽略与 Unicode 相关的问题,因为正确解决这些问题取决于 Python 版本。)
Twisted 是一个事件驱动的、低级别的服务器框架,与Node.js(实际上,Node.js 的设计受到了 Twisted 的影响)非常相似。与 Kivy 类似,事件驱动的架构意味着我们不会将代码结构化为循环;相反,我们将多个事件监听器绑定到我们认为对我们应用有用的那些事件上。硬核、低级别的网络操作,如处理传入连接和与原始数据包一起工作,由 Twisted 在启动服务器时自动执行。
注意
为了在你的机器上安装 Twisted,请在终端中运行常规命令:
pip install -U twisted
有几点需要注意:
-
很可能,你需要成为 root(管理员或“超级用户”)才能执行系统范围内的安装。如果你使用 Mac OS 或 Linux,当收到访问被拒绝的错误消息时,尝试在命令前加上
sudo。 -
如果你没有安装 pip,请尝试使用easy_install twisted命令(或者easy_install pip)。
-
或者,请遵循官方 pip 安装指南
pip.pypa.io/en/latest/installing.html。这也涵盖了 Windows。
协议定义
让我们讨论我们将要使用的与聊天服务器通信的协议。由于应用程序将非常简单,我们不会使用像 XMPP 这样的完整协议,而是将创建一个仅包含我们需要的位的裸骨协议。
在本教程的上下文中,我们只想在协议级别实现从客户端到服务器的两条消息——连接到服务器(进入聊天室)以及实际上与其他用户交谈。服务器发送回客户端的所有内容都会被渲染;没有服务事件在服务器上发起。
我们的协议将是文本格式,类似于许多其他应用层协议,包括广泛使用的 HTTP。这是一个非常实用的特性,因为它使得调试和相关活动更加容易。与二进制协议相比,文本协议通常被认为更具可扩展性和未来适应性。纯文本的缺点主要是其大小;二进制枚举通常更紧凑。在这种情况下,这基本上是不相关的,而且可以通过压缩轻松缓解(这正是许多服务器在 HTTP 情况下所做的事情)。
现在我们来回顾构成我们应用程序协议的各个消息:
-
连接到服务器不会传达除用户现在在聊天室的事实之外的其他信息,因此我们将每次只发送单词
CONNECT。这条消息没有参数化。 -
在聊天室里说话更有趣。有两个参数:昵称和文本消息本身。让我们定义这种消息的格式为
A:B,其中A是昵称(作为直接后果,昵称不能包含冒号:字符)。
从这个规范中,我们可以推导出一个有效的算法(伪代码):
if ':' not in message
then
// it's a CONNECT message
add this connection to user list
else
// it's a chat message
nickname, text := message.split on ':'
for each user in user list
if not the same user:
send "{nickname} said: {text}"
测试相同用户是为了减少用户自己的消息回传给他们的不必要的传输(回声)。
服务器源代码
在 Twisted 框架的帮助下,我们的伪代码可以几乎直接地翻译成 Python。以下列表包含我们server.py应用程序的完整源代码:
from twisted.internet import protocol, reactor
transports = set()
class Chat(protocol.Protocol):
def dataReceived(self, data):
transports.add(self.transport)
if ':' not in data:
return
user, msg = data.split(':', 1)
for t in transports:
if t is not self.transport:
t.write('{0} says: {1}'.format(user, msg))
class ChatFactory(protocol.Factory):
def buildProtocol(self, addr):
return Chat()
reactor.listenTCP(9096, ChatFactory())
reactor.run()
操作原理
这是帮助你理解我们的服务器是如何工作的控制流程概述:
-
最后一行,
reactor.run(),启动监听端口 9096 的ChatFactory服务器 -
当服务器接收到输入时,它调用
dataReceived()回调 -
dataReceived()方法实现了协议部分的伪代码,根据需要向其他已连接客户端发送消息
客户端连接的集合被称为transports。我们无条件地将当前传输self.transport添加到集合中,因为在现有元素的情况下,这是一个无操作,为什么要费那个劲。
列表中的其余部分严格遵循算法。因此,除了发送原始消息的用户之外,每个已连接的用户都将收到通知,< 用户名 > says: < 消息文本 >.
注意
注意我们实际上并没有检查连接消息是否说CONNECT。这是紧密遵循乔恩·波斯尔在 1980 年 TCP 规范中提出的网络鲁棒性原则的例子:发送时要保守,接受时要宽容。
除了简化本例中的代码外,我们还获得了一个向前兼容性的选项。假设在未来客户端的版本中,我们向协议中添加了一条新消息,即名为WHARRGARBL的虚构消息,根据其名称,它确实做了一些真正令人惊叹的事情。而不是因为收到格式不正确的消息(在这种情况下,因为版本不匹配)而崩溃,旧版本的服务器将简单地忽略这些消息并继续运行。
具体来说,这个方面——版本之间的兼容性——可以通过多种策略轻松处理。然而,在涉及网络,尤其是公共网络时,也存在一些更困难的问题,包括恶意用户试图破坏你的系统并故意使其崩溃。因此,实际上并不存在过度夸大的服务器稳定性。
测试服务器
以通常运行任何 Python 程序的方式运行服务器:
python server.py
此命令不应产生任何可见的输出。服务器只是静静地坐着,等待客户端连接。然而,在已知的宇宙中没有任何客户端程序能够使用这个协议,因为我们大约在一页半之前就编造了它。我们如何确保服务器能正常工作?
幸运的是,这种“鸡生蛋,蛋生鸡”的问题在这个领域非常普遍,因此有许多有用的工具可以做到这一点——向任何服务器发送任意字节,并接收和显示服务器发送回的任意字节。
适用于篡改使用文本协议的服务器的标准程序之一是 Telnet。像许多“老式”Unix 风格的实用程序一样,Telnet 是一个既可以用作交互式程序,也可以作为更大批处理(shell)脚本一部分的命令行程序。
大多数操作系统都预装了telnet命令。如果没有,那么你可能正在使用 Windows 7 或更高版本。在这种情况下,你可以按照以下截图所示,转到控制面板 | 程序和功能 | 启用或关闭 Windows 功能:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_04_02.jpg
然后,确保Telnet 客户端复选框已勾选,如下所示:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_04_03.jpg
Telnet 接受两个参数:要连接的服务器的名称和端口号。为了使用 telnet 连接到聊天服务器,你首先需要启动server.py,然后在另一个终端中运行:
telnet 127.0.0.1 9096
或者,你可以在大多数系统中使用localhost作为主机名,因为这等同于127.0.0.1;两者都表示当前机器。
如果一切顺利,你将打开一个交互式会话,你输入的每一行都会发送到服务器。现在,使用我们之前讨论的聊天协议,你可以与服务器进行通信:
CONNECT
User A:Hello, world!
将不会有输出,因为我们以这种方式编程服务器,使其不会将消息回显给原始作者——这将是一种浪费。所以,让我们再打开另一个终端(以及一个 Telnet 会话),这样我们就有两个同时连接的用户。
当一切正常时,聊天会话看起来是这样的:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_04_04.jpg
网络上的交互式聊天达到最佳状态
注意
如果由于某种原因,无论是技术原因还是其他原因,您无法在您的系统上使用 Telnet,请不要对此感到特别难过,因为这项测试不是成功完成教程所必需的。
然而,这里有一些(非常个人化,甚至可以说是亲密的)建议,这些建议与您的职业比与本书的主题更相关:为自己做点好事,获取一个 Mac OS 或 Linux 系统,或者也许在同一台机器上使用双启动。这些类 Unix 操作系统比 Windows 更适合软件开发,而且生产力的提升完全值得适应新环境的不便。
通过这一点,我们可以得出结论:我们的服务器正在正常工作:两个 Telnet 窗口正在良好地通信。现在后端工作已经完成,让我们构建一个跨平台的 GUI 聊天客户端。
屏幕管理器
让我们从一个新概念开始 UI 开发,即屏幕管理。我们手头的应用程序,即聊天客户端,是一个合适的例子。将会有两个应用程序状态,具有不同的 UI,彼此完全独立:
-
登录屏幕,用户在此输入要连接的主机名和所需的昵称:https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_04_05.jpg
-
聊天室屏幕,实际对话发生的地方:https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_04_06.jpg
从概念上讲,这些都是聊天应用程序前端的应用程序状态。
一种简单的 UI 分离方法将涉及根据某个变量管理可见和隐藏的控件,该变量持有当前所需的 UI 状态。当小部件数量增加时,这会变得非常繁琐,而且样板代码本身就不太有趣。
正因如此,Kivy 框架为我们提供了一个专门针对此任务定制的容器小部件,即ScreenManager。此外,ScreenManager支持短动画来可视化屏幕切换,并提供多种预构建的过渡效果可供选择。它可以完全声明性地从 Kivy 语言文件中使用,而不需要接触 Python 代码。
让我们这样做。在chat.kv文件中添加以下代码:
ScreenManager:
Screen:
name: 'login'
BoxLayout:
# other UI controls -- not shown
Button:
text: 'Connect'
on_press: root.current = 'chatroom'
Screen:
name: 'chatroom'
BoxLayout:
# other UI controls -- not shown
Button:
text: 'Disconnect'
on_press: root.current = 'login'
这是程序的基本结构:我们在根目录有一个ScreenManager,为每个我们想要的 UI 状态(第一个将默认显示)有一个Screen容器。在Screen内部是通常的 UI:布局、按钮以及我们迄今为止看到的一切。我们很快就会接触到它。
我们刚才看到的代码还包括屏幕切换按钮,每个Screen实例一个。为了切换应用程序状态,我们需要将所需屏幕的名称分配给ScreenManager的current属性。
自定义动画
如前所述,当切换屏幕时发生的简短动画可以自定义。Kivy 提供了多种此类动画,位于kivy.uix.screenmanager包中:
| 过渡类名称 | 视觉效果 |
|---|---|
NoTransition | 没有动画,立即显示新屏幕。 |
SlideTransition | 滑动新屏幕。传递'left'(默认)、'right'、'up'或'down'以选择效果的方向。 |
SwapTransition | 理论上,这个类模拟了 iOS 屏幕切换动画。但实际效果与理论相差甚远。 |
FadeTransition | 淡出屏幕,然后淡入。 |
WipeTransition | 使用像素着色器实现的平滑方向过渡。 |
FallOutTransition | 将旧屏幕缩小到窗口中心并使其透明,从而显示新屏幕。 |
RiseInTransition | FallOutTransition的完全相反:从中心生长新屏幕,重叠并隐藏旧的一个。 |
在.kv文件中设置这些内容有一个小问题:默认情况下不会导入过渡动画,因此您需要使用以下语法(在chat.kv的顶部)导入您想要使用的动画:
#:import RiseInTransition kivy.uix.screenmanager.RiseInTransition
现在您可以将其分配给ScreenManager。请注意,这是一个 Python 类实例化,因此结尾的括号是必需的:
ScreenManager:
transition: RiseInTransition()
登录屏幕布局
在登录屏幕内部,布局方面与上一章的录音应用非常相似:一个GridLayout解决了在网格上对齐组件的任务。
本书尚未使用的是TextInput小部件。Kivy 的文本输入几乎与按钮的行为完全相同,唯一的区别是您可以在其中输入文本。默认情况下,TextInput是多行的,因此我们将multiline属性设置为False,因为在应用程序的上下文中,多行文本输入没有太多意义。
当在未连接物理键盘的设备上运行时,Kivy 将回退到虚拟屏幕键盘,就像原生应用一样。
这是实现登录屏幕布局的代码(在同一个 Kivy 语言文件chat.kv中的ScreenManager下):
Screen:
name: 'login'
BoxLayout:
orientation: 'vertical'
GridLayout:
Label:
text: 'Server:'
TextInput:
id: server
text: '127.0.0.1'
Label:
text: 'Nickname:'
TextInput:
id: nickname
text: 'Kivy'
Button:
text: 'Connect'
on_press: root.current = 'chatroom'
在这里,我们添加了两个文本字段,Server和Nickname,以及相应的标签,还有一个连接按钮。按钮的事件处理程序目前与实际的网络无关,只是切换到聊天室,但这种情况将在不久的将来改变。
要制作单行的TextInput,需要一些有趣的样式。除了将其multiline属性设置为False外,我们还想将文本垂直居中(否则,它将粘在控制的顶部,底部留下很大的间隙)。我们可以使用如下方式使用填充属性来实现正确的对齐:
<TextInput>:
multiline: False
padding: [10, 0.5 * (self.height – self.line_height)]
这条padding行将左右填充设置为 10,上下填充计算为*(小部件高度 - 一行文本高度)× 0.5*。
这是最终屏幕的显示效果;它与我们在本书的编写过程中制作的其他应用程序非常相似。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_04_07.jpg
聊天应用登录屏幕
我们现在可以开始编写连接服务器的代码,但首先让我们让主屏幕,即聊天室,工作起来。这将使我们能够立即进行有意义的测试。
聊天室屏幕布局
接下来在我们的列表中是聊天室屏幕。它包含一个用于长篇对话的ScrollView小部件,由于这是第一次在这本书中出现滚动小部件,让我们仔细看看它是如何工作的。
生成滚动小部件最简单的.kv片段如下:
<ChatLabel@Label>:
text_size: (self.width, None) # Step 1
halign: 'left'
valign: 'top'
size_hint: (1, None) # Step 2
height: self.texture_size[1] # Step 3
ScrollView:
ChatLabel:
text: 'Insert very long text with line\nbreaks'
如果您添加足够的文本使其溢出屏幕,它就会开始滚动,类似于您在 iOS 或 Android 中期望的长列表项。
这是这种布局的工作原理:
-
我们将自定义
Label子类的text_size宽度(第一个值)限制为小部件的可用宽度,并通过将第二个值设置为None让它根据其内容选择高度。 -
然后,我们将垂直
size_hint(第二个值)设置为None,以强制小部件的高度独立于其容器计算。否则,它将被父元素限制,因此将没有可滚动的内容。 -
现在,我们可以将小部件的高度设置为等于
texture_size的高度(请注意,索引通常是零基的,所以第二个值确实是texture_size[1])。这将迫使ChatLabel比包含它的ScrollView小部件更大。 -
当
ScrollView检测到其子小部件大于可用屏幕空间时,启用滚动。在移动设备上它按常规工作,并在桌面上添加鼠标滚轮支持。
滚动模式
您还可以自定义ScrollView的滚动回弹效果,以模仿对应平台的原生行为(尽管在概念上相似,但与原生组件相比,仍然看起来明显不同)。截至写作时,Android 风格的边缘发光效果不是默认支持的;可用的选项如下:
-
ScrollEffect:此效果允许您在到达末尾时突然停止滚动。这与桌面程序通常的工作方式相似,因此如果所讨论的应用程序主要针对桌面,则此行为可能是有吸引力的。 -
DampedScrollEffect:这是默认效果。它与 iOS 中找到的回弹效果相似。这可能是移动设备上最好的模式。 -
OpacityScrollEffect:此效果类似于DampedScrollEffect,在滚动过内容边缘时增加了透明度。
要使用这些设置之一,从 kivy.effects 模块导入它,并将其分配给 ScrollView.effect_cls 属性,类似于刚刚讨论的 ScreenManager 过渡。我们不会使用这个,因为 DampedScrollEffect 已经非常适合我们的应用程序。
考虑到所有这些点,这是聊天室屏幕布局的样子(在 chat.kv 中):
Screen:
name: 'chatroom'
BoxLayout:
orientation: 'vertical'
Button:
text: 'Disconnect'
on_press: root.current = 'login'
ScrollView:
ChatLabel:
id: chat_logs
text: 'User says: foo\nUser says: bar'
BoxLayout:
height: 90
orientation: 'horizontal'
padding: 0
size_hint: (1, None)
TextInput:
id: message
Button:
text: 'Send'
size_hint: (0.3, 1)
最后一行,size_hint,将 Button 小部件的横向比例设置为 0.3,低于默认的 1。这使得 发送 按钮比消息输入字段更小。
为了将消息区域的背景设置为白色,我们可以使用以下代码:
<ScrollView>:
canvas.before:
Color:
rgb: 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
这将在每次其他绘图操作之前无条件地绘制一个白色矩形在 ScrollView 后面。别忘了调整 <ChatLabel> 类,将文本颜色设置为在浅色背景上可读:
#:import C kivy.utils.get_color_from_hex
<ChatLabel@Label>:
color: C('#101010')
到目前为止,我们已经有了这些:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_04_08.jpg
没有有意义对话的聊天室屏幕
再次强调,断开连接 按钮只是切换屏幕,而不会在幕后进行任何网络操作。这实际上是下一个主题;正如你很快就会看到的,用 Python 实现简单的网络程序在复杂性方面与用 Kivy 构建简单的用户界面并没有太大区别。
将应用上线
这是最有趣的部分!我们将与服务器建立连接,发送和接收消息,并向用户显示有意义的输出。
但首先,让我们看看聊天客户端的最小、纯 Python 实现,看看发生了什么。这是使用套接字进行通信的低级代码。在实际应用中,使用更高层次的抽象,如 Twisted,几乎总是建议的;但如果你不熟悉底层概念,可能很难理解代码背后的实际发生情况,这使得调试变成了猜测。
构建简单的 Python 客户端
在以下列表中,我们使用内置的 readline() 函数从控制台读取用户输入,并使用 print() 函数显示输出。这意味着使用这个简单的客户端与使用 Telnet 并无太大区别——UI 由终端窗口中的相同纯文本组成——但这次我们是自己从头开始使用套接字实现的。
我们将需要一些 Python 模块,所有这些模块都来自标准库:socket、sys(用于 sys.stdin,标准输入文件描述符)和 select 模块,以实现高效等待数据可用。假设一个新的文件,让我们称它为 client.py:
import select, socket, sys
这个程序根本不需要外部依赖;这是最纯粹的 Python。
注意
注意,在 Windows 上,由于实现细节,select 无法像套接字那样轮询文件描述符,因此我们的代码将无法正确运行。由于这只是一个低级网络演示,而不是最终产品,我们不会将其移植到边缘系统。
现在,我们打开到服务器的连接并执行通常的CONNECT握手:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 9096))
s.send('CONNECT')
下一个有趣的部分:我们等待标准输入(意味着用户输入了某些内容)或s套接字(意味着服务器发送了某些内容给我们)上的数据变得可用。等待是通过使用select.select()调用来实现的:
rlist = (sys.stdin, s)
while 1:
read, write, fail = select.select(rlist, (), ())
for sock in read:
if sock == s: # receive message from server
data = s.recv(4096)
print(data)
else: # send message entered by user
msg = sock.readline()
s.send(msg)
然后,根据新可用数据源的不同,我们要么在收到来自服务器的消息时将其打印到屏幕上,要么将其发送到服务器,如果它是来自本地用户的消息。再次强调,这基本上就是 Telnet 所做的工作,但没有错误检查。
如您所见,在低级网络中,并没有什么本质上不可能或疯狂复杂的事情。但是,就其价值而言,原始套接字仍然相当难以处理,我们很快将展示相同代码的高级方法。然而,这正是任何框架底层的运作方式;最终,总是套接字在承担重活,只是被不同的抽象(API)所呈现。
小贴士
注意,在这个教程中,我们故意没有进行广泛错误检查,因为这会使代码量增加 2-3 倍,并使其难以控制。
在网络上可能会出现很多问题;它比人们通常认为的要脆弱得多。所以如果你计划与 Skype 等软件竞争,准备好进行大量的错误检查和测试:例如,网络问题如数据包丢失和全国范围内的防火墙,肯定会在某个时刻出现。无论你的架构计划多么周密,使网络服务高度可用都是一项艰巨的任务。
Kivy 与 Twisted 的集成
我们的低级客户端代码不适合 Kivy 应用程序的另一个原因是它依赖于自己的主循环(即while 1:部分)。要使这段代码与驱动 Kivy 的事件循环良好地协同工作,需要做一些工作。
相反,让我们利用作为 Kivy 的一部分分发的 Twisted 集成。这也意味着相同的网络库将在客户端和服务器上使用,使代码在整个系统中更加统一。
要使 Kivy 的主循环与 Twisted 良好地协同工作,需要在导入 Twisted 框架之前运行以下代码:
from kivy.support import install_twisted_reactor
install_twisted_reactor()
from twisted.internet import reactor, protocol
代码应该在main.py文件的非常开头部分。这是至关重要的,否则一切都将以一种神秘的方式停止工作。
现在,让我们使用 Twisted 来实现聊天客户端。
ChatClient 和 ChatClientFactory
在 Twisted 方面,实际上要做的事情很少,因为框架负责处理与实际网络相关的一切。这些类主要用于将程序的“移动部件”连接起来。
ClientFactory子类ChatClientFactory在初始化时仅存储 Kivy 应用程序实例,以便我们可以在以后传递事件给它。请看以下代码:
class ChatClientFactory(protocol.ClientFactory):
protocol = ChatClient
def __init__(self, app):
self.app = app
相对应的ChatClient类监听 Twisted 的connectionMade和dataReceived事件,并将它们传递给 Kivy 应用程序:
class ChatClient(protocol.Protocol):
def connectionMade(self):
self.transport.write('CONNECT')
self.factory.app.on_connect(self.transport)
def dataReceived(self, data):
self.factory.app.on_message(data)
注意无处不在的CONNECT握手。
这与使用原始套接字的代码非常不同,对吧?同时,这非常类似于在server.py服务器端发生的事情。但是,我们不是真正处理事件,而是将它们传递给app对象。
UI 集成
为了最终看到整个画面,让我们将网络代码连接到 UI,并编写缺失的 Kivy 应用程序类。以下是对chat.kv文件需要应用的累积更新:
Button: # Connect button, found on login screen
text: 'Connect'
on_press: app.connect()
Button: # Disconnect button, on chatroom screen
text: 'Disconnect'
on_press: app.disconnect()
TextInput: # Message input, on chatroom screen
id: message
on_text_validate: app.send_msg()
Button: # Message send button, on chatroom screen
text: 'Send'
on_press: app.send_msg()
注意按钮不再切换屏幕,而是调用app上的方法,类似于ChatClient事件处理。
在完成这些之后,我们现在需要实现 Kivy 应用程序类中缺失的五个方法:两个用于来自 Twisted 代码的服务器端事件(on_connect和on_message),以及另外三个用于用户界面事件(connect、disconnect和send_msg)。这将使我们的聊天应用程序真正可用。
客户端应用程序逻辑
让我们从大致的生命周期顺序开始编写程序逻辑:从connect()到disconnect()。
在connect()方法中,我们获取用户提供的服务器和昵称字段的值。然后,昵称被存储在self.nick中,Twisted 客户端连接到指定的主机,如下面的代码所示:
class ChatApp(App):
def connect(self):
host = self.root.ids.server.text
self.nick = self.root.ids.nickname.text
reactor.connectTCP(host, 9096,
ChatClientFactory(self))
现在,调用ChatClient.connectionMade()函数,将控制权传递给on_connect()方法。我们将使用这个事件将连接存储在self.conn中并切换屏幕。正如之前讨论的,按钮不再直接切换屏幕;相反,我们依赖于更具体的事件处理器,如这个:
# From here on these are methods of the ChatApp class
def on_connect(self, conn):
self.conn = conn
self.root.current = 'chatroom'
现在是主要部分:发送和接收消息。实际上,这非常直接:要发送消息,我们从TextInput获取消息文本,从self.nick获取我们的昵称,将它们连接起来,然后将生成的行发送到服务器。我们还在屏幕上回显相同的消息并清除消息输入框。代码如下:
def send_msg(self):
msg = self.root.ids.message.text
self.conn.write('%s:%s' % (self.nick, msg))
self.root.ids.chat_logs.text += ('%s says: %s\n' %
(self.nick, msg))
self.root.ids.message.text = ''
接收消息是完全微不足道的;因为我们没有主动跟踪它们,只需将新到达的消息显示在屏幕上,然后换行,就完成了:
def on_message(self, msg):
self.root.ids.chat_logs.text += msg + '\n'
最后剩下的方法是disconnect()。它确实做了它所说的:关闭连接并执行一般清理,以便将事物恢复到程序首次启动时的状态(特别是清空chat_logs小部件)。最后,它将用户送回登录屏幕,以便他们可以跳转到另一个服务器或更改昵称。代码如下:
def disconnect(self):
if self.conn:
self.conn.loseConnection()
del self.conn
self.root.current = 'login'
self.root.ids.chat_logs.text = ''
这样,我们的应用程序终于有了发送和接收聊天消息的能力。
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_04_09.jpg
聊天应用程序运行中
小贴士
注意事项
在测试期间,server.py脚本显然应该始终运行;否则,我们的应用程序将没有连接的端点。目前,这将导致应用程序停留在登录屏幕;如果没有on_connect()调用,用户将无法进入聊天室屏幕。
此外,当在 Android 上进行测试时,请确保输入服务器的正确 IP 地址,因为它将不再是127.0.0.1——那总是本地机器,所以在 Android 设备上这意味着是设备本身而不是你正在工作的电脑。使用ifconfig实用程序(在 Windows 上称为ipconfig以增加混淆)来确定你机器的正确网络地址。
跨应用互操作性
结果应用程序的一个有趣特性(除了它实际上能工作之外)是它与本章中提到的所有客户端都兼容。用户可以使用 Telnet、纯 Python 客户端或 Kivy UI 程序连接到服务器——核心功能对所有用户都是同样可用的。
这与互联网的运作方式非常相似:一旦你有一个定义良好的协议(如 HTTP),许多无关的各方可以开发服务器和客户端,它们最终将是互操作的:Web 服务器、Web 浏览器、搜索引擎爬虫等等。
协议是 API 的一种高级形式,它是语言和系统无关的,就像一个好的基础应该那样。虽然不是很多网络开发者熟悉例如 2007 年发布的微软 Silverlight 的 API,但在这个领域工作的任何人至少都了解 HTTP 的基础,它是在 1991 年记录的。这种普及程度几乎不可能通过库或框架来实现。
增强和视觉享受
现在我们的聊天基本上已经工作,我们可以给它添加一些最后的修饰,例如改进聊天日志的展示。由于客户端已经显示了服务器发送的所有内容,我们可以轻松地使用 Kivy 标记(类似于BBCode的标记语言,在第一章中讨论,构建时钟应用)来样式化对话日志。
要做到这一点,让我们为每个用户分配一个颜色,然后用这个颜色绘制昵称并使其加粗。这将有助于可读性,并且通常比单色的纯文本墙看起来更美观。
我们将使用Flat UI调色板而不是生成纯随机颜色,因为生成看起来搭配在一起时看起来好的显著不同的颜色本身就不是一件容易的事情。
发送的消息(由当前用户发送的消息)不是来自服务器,而是由客户端代码添加到聊天日志中。因此,我们将使用恒定颜色直接在客户端上绘制当前用户的昵称。
在这次更新之后,聊天服务器的最终代码server.py如下所示:
colors = ['7F8C8D', 'C0392B', '2C3E50', '8E44AD', '27AE60']
class Chat(protocol.Protocol):
def connectionMade(self):
self.color = colors.pop()
colors.insert(0, self.color)
给定一个有限的颜色列表,我们从列表的末尾弹出一个颜色,然后将其重新插入到列表的前端,创建一个旋转缓冲区。
小贴士
如果你熟悉标准库中的更高级的itertools模块,你可以像这样重写我们刚才看到的代码:
import itertools
colors = itertools.cycle(('7F8C8D', 'C0392B', '2C3E50', '8E44AD', '27AE60'))
def connectionMade(self):
self.color = colors.next()
# next(colors) in Python 3
现在,我们将讨论将消息传递给客户端的部分。期望效果的标记非常简单:[b][color]Nickname[/color][/b]。利用它的代码同样简单:
for t in transports:
if t is not self.transport:
t.write('[b][color={}]{}:[/color][/b] {}'
.format(self.color, user, msg))
main.py中的客户端也更新以匹配格式,如前所述。这里有一个常量颜色,与服务器分配的不同,这样当前用户总是突出显示。代码如下:
def send_msg(self):
msg = self.root.ids.message.text
self.conn.write('%s:%s' % (self.nick, msg))
self.root.ids.chat_logs.text += (
'[b][color=2980B9]{}:[/color][/b] {}\n'
.format(self.nick, msg))
然后,我们将对话日志小部件ChatLabel的markup属性设置为True,如下面的代码片段所示,我们(几乎)完成了:
<ChatLabel@Label>:
markup: True
然而,在我们用这种方法解决问题之前(实际上这里确实至少有一个严重的问题),这是必须的最终截图。这就是最终对话屏幕的样子:
https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/kivy-bp/img/B01620_04_10.jpg
彩色的聊天记录有助于可读性,并且通常看起来更好,更“精致”
转义特殊语法
如前所述,这个代码的一个缺点是,现在我们在协议中有特殊的语法,在客户端以某种方式解释。用户可以伪造(或者纯粹偶然地使用,纯粹是巧合)BBCode 风格的标记,造成不想要的视觉错误,例如分配非常大的字体大小和难以阅读的颜色。例如,如果某个用户发布了一个未关闭的[i]标签,聊天室中所有随后的文本都将被设置为斜体。这相当糟糕。
为了防止用户以随机的方式突出显示文本,我们需要转义消息中可能存在的所有标记。幸运的是,Kivy 提供了一个函数来完成这项工作,即kivy.utils.escape_markup。不幸的是,这个函数自 2012 年以来就存在 bug。
有很高的可能性,当你阅读这本书的时候,这个函数已经被修复了,但为了完整性,这里有一个可行的实现:
def esc_markup(msg):
return (msg.replace('&', '&')
.replace('[', '&bl;')
.replace(']', '&br;'))
通过这种方式,所有对 Kivy 标记特殊字符都被替换为 HTML 风格的字符实体,因此通过这个函数传递的标记将按原样显示,并且不会以任何方式影响富文本属性。
我们需要在两个地方调用这个函数,在服务器发送消息给客户端时,以及在客户端显示来自 self(当前用户)的消息时。
在server.py中,相关代码如下:
t.write('[b][color={}]{}:[/color][/b] {}'
.format(self.color, user,
esc_markup(msg)))
在main.py中,实现方式类似:
self.root.ids.chat_logs.text += (
'[b][color=2980B9]{}:[/color][/b] {}\n'
.format(self.nick, esc_markup(msg)))
在这里,漏洞已被修复;现在,如果用户选择这样做,他们可以安全地向彼此发送 BBCode 标记。
注意
有趣的是,这种类型的 bug 在互联网应用中也非常普遍。当应用于网站时,它被称为跨站脚本(XSS),它允许造成比仅仅更改字体和颜色更多的损害。
不要忘记在所有可能涉及命令(如标记、内联脚本,甚至是 ANSI 转义码)与数据混合的场景中对所有用户输入进行清理;忽视这一点将是一场灾难,只是等待发生。
下一步
显然,这仅仅是开始。当前的实施方案仍然存在大量的缺陷:用户名没有被强制要求唯一,没有历史记录,也没有支持在其他人离线时获取已发送的消息。因此,网络状况不佳且频繁断开连接将使这个应用程序基本无法使用。
但重要的是,这些问题肯定是可以解决的,我们已经有了一个工作原型。在创业界,拥有一个原型是一个吸引人的特质,尤其是在筹集资金时;如果你主要是为了娱乐而编程,那就更是如此,因为看到一款工作产品是非常有动力的(相比之下,观察一堆尚未运行的代码就没有那么有动力了)。
摘要
正如我们在本章中看到的,客户端-服务器应用程序开发(以及一般而言,应用层面的网络)并不一定本质上复杂。即使是利用套接字的底层代码也是相当容易管理的。
当然,在编写大量使用网络的程序时,有许多灰色区域和难以处理的问题。这些问题的例子包括处理高延迟、恢复中断的连接,以及在大量节点(尤其是点对点或多主节点,当没有任何机器拥有完整数据集时)上进行同步。
另一类相对较新的网络问题是政治问题。最近,不同压迫程度的政府正在实施互联网法规,从相对合理(例如,封锁推广恐怖主义的资源)到完全荒谬(例如,禁止像维基百科、主要新闻网站或视频游戏这样的教育网站)。这种类型的连接问题也以其高附带损害而闻名,例如,如果内容分发网络(CDN)崩溃,那么许多链接到它的网站将无法正常工作。
然而,通过仔细的编程和测试,确实有可能克服每一个障碍,并向用户交付一个质量卓越的产品。丰富的 Python 基础设施为你承担了部分负担,正如我们在聊天程序中所展示的那样:许多底层细节都通过 Kivy 和 Twisted 这两个优秀的 Python 库得到了抽象化。
考虑到普遍的可用性,这个领域的可能性几乎是无尽的。我们将在下一章讨论和实施一个更有趣的网络应用程序用例,所以请继续阅读。
512

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



