5. 架构

优质
小牛编辑
132浏览
2023-12-01

英文原文

本章我们将从软件工程的角度,来简单介绍一下Kivy的设计。这对于理解各个部分如何配合工作会有帮助。如果你只关注代码,可能有时候会遇到这样一种情况,就是你已经有了一个初步的想法了,但具体怎么去实现可能还是一头雾水,所以本章就针对这种情况,来更深入地讲解一下Kivy的一些基本思想。你也可以先跳过这一章,等以后再翻回来看,不过我们建议开发者还是先看一下这些内容比较好,起码可以快速略读一下有个印象。

Kivy包含的若干个模块,我们将对这些模块一一进行简要说明。下面这幅图是Kivy整个架构的概括图示:

../_images/architecture.png

核心模块和输入模块

对理解Kivy的设计内涵,模块化和抽象化的思想是至关重要的。我们试图把各种基本的任务进行抽象,比如打开窗口、显示图像和文本、播放音频、从摄像头获取图像、拼写校正等等。我们将这些部分称为核心任务。这样也使得API接口用起来比较简单,扩展起来也容易。更重要的是,这种思路可以让Kivy应用在运行的时候,使用各个运行平台所提供的对应功能的API接口。例如,在苹果的MacOS操作系统、Linux操作系统和Windows操作系统中,就都有各自不同的原生API接口提供各种核心功能。所以就有一部分代码,调用这些不同接口中的某一个,一方面与操作系统进行通信,另一方面与Kivy进行交互,起到中间人的角色,我们称之为核心模块。针对不同的操作系统平台要使用各自对应的核心模块,这样的好处是达到一种均衡状态,既能够充分利用操作系统提供的功能,又能尽量提高开发效率。(译者注:我的理解是这样大家平时不用针对不同操作系统去学习和使用各自的API,而只要专心使用Kivy的核心模块进行调用就行了。)这也允许用户来自由选择,使用Kivy提供的核心模块,或者直接使用各个操作系统的API接口。此外,由于使用了各个平台所提供的链接库文件,我们大大减小了Kivy发型版本的体积,也使得打包发布更加容易。这有助于将Kivy应用移植到其他平台。比如Android平台上的Kivy应用就体现了这一特性的好处了。

在输入模块这部分,我们也遵循了同样的思路。输入模块,是一段代码,用于针对各种输入设备提供支持,比如苹果公司的Trackpad触摸板,TUIO多点触摸设备,或者是鼠标模拟器等等。如果你需要对某一种新的输入设备添加支持,只需要提供一个新的类,用这个类来读取输入设备的数据,然后传递给Kivy基本事件,就可以了。

图形接口

Kivy的图形接口是对OpenGL的抽象。在最底层,Kivy使用OpenGL的命令来进行硬件加速的图形绘制。不过写OpenGL的代码可还是挺复杂的,新手就更难以迅速掌握了。所以我们就提供了一系列的图形接口,利用这些接口可以很简单地进行图形绘制,这些接口中用到了例如画布Canvas、矩形Rectangle等几何概念,比OpenGL里面简单不少。

Kivy自带的所有控件,都使用了这个图形接口;出于性能的考虑,此图形接口是用C语言来实现的。

这个图形接口的另一个好处是可以对你代码中的绘图指令进行自动优化。这个很有用,尤其是在你对OpenGL的优化不太熟悉的情况下。这能让你的绘图代码更高效。

当然了,你也可以坚持使用原生的OpenGL命令。目前Kivy在所有操作系统平台上用的都是是OpenGL 2.0 ES (GLES2),所以如果你希望保持跨平台的兼容性,我们建议你只是用GLES2兼容的函数。

核心模块

核心模块也就是kivy.core,这个包里面提供了常用的各种功能,比如:

  • Clock

    时钟类,可以用于安排计时器事件。同时支持一次性计时和周期性计时。

  • Cache

    If you need to cache something that you use often, you can use our class for that instead of writing your own.

    缓存类,如果有一些经常用到的数据需要缓存,就可以用这个类,而不用自己写了。

  • Gesture Detection

    手势识别,这个可以用来识别各种划动行为,比如画个圆圈或者方块之类的。可以训练来识别你自己设计的图形。

  • Kivy Language

    Kivy语言,这个是用来简洁高效地描述Kivy应用的用户界面的。

  • Properties

    这里这些属性和Python语言中的属性不同。这里是我们专门写的一些类,通过用户界面描述来连接控件代码。

UIX(控件和布局)

UIX用户界面模块,包含了常用的各种控件和布局,可以通过复用来快速构建用户界面。

  • Widgets 控件

    控件是各种用户界面元素,可以添加到程序中来提供各种功能。有的可见,有的不可见。文件浏览器,按钮、滑动页、列表等等,这都属于控件。控件接收动作事件。

  • Layouts 布局

    布局是控件的排列方式。当然,你也可以自己自定义控件的位置,不过从我们提供的预设布局中选择一个来使用,会更方便很多。网格布局、箱式布局等等,都是布局了。你还可以试试复杂的多层网状布局。

模块化

如果你用某一种现代的网络浏览器,并且通过一些附加组件对其进行定制,那么你应该就理解了我们提供的各种模块类的基本思想了。各种模块可以用来向Kivy程序中添加功能,即便原作者没有提供的功能也可以加进去了。

例如,有一个模块就能显示当前应用的FPS(Frame Per Second,每秒帧数,即帧率),然后还能统计一段时间的FPS变化。

你可以自己写各种模块添加到应用中。

输入事件(Touches)

Kivy抽象了各种输入类型和输入设备,比如触控,鼠标按键,多点触摸等等。这些输入类型有一个共同点,就是都可以把各种输入事件映射成屏幕上对应的一种2D形态。(当然了,还有的输入设备就没法用2D形态来表示,比如通过加速度传感器来衡量设备倾斜角度等。这种情况就得另外考虑了。下面我们讨论的只是那些能用2D形态表示的输入事件,复杂的类型以后再说。)

这些输入类型,在Kivy中都用Touch()类的实例来表示。(请注意,这里可不仅仅是针对手指去触摸的那种touch,而是所有可以这样抽象表示的输入事件。这里用Touch只是为了方便而这么简称一下。就想象一下,这些Touches就是在用户界面或者显示屏上面的那些个点击行为。 )Touch的实例或者对象,有三种状态。当这个Touch进入了其中的某一个状态,你的程序就会被告知此事件的发生。Touch的三种状态如下:

  • Down 落下

    处于落下状态,只能有一次,就是在发生Touch事件的初始时刻。

  • Move 移动

    这个状态的时间无上限。在一个Touch的生命周期中可以没有这个状态。移动状态只发生在Touch的2D平面位置发生变化的情况下。

  • Up 抬起

    A touch goes up at most once, or never. In practice you will almost always receive an up event because nobody is going to hold a finger on the screen for all eternity, but it is not guaranteed. If you know the input sources your users will be using, you will know whether or not you can rely on this state being entered.

    一个Touch要么只能抬起一次,要么就不发生。而实际应用中你会经常遇到Up时间,因为没有人会一直把手指按到屏幕上,不过也有未必就绝对不会有这种情况。若事先知道用户用的输入设备,就可以确定能否完全依靠用户的输入状态。

    (译者注:以手指触摸屏幕为例,只有开始接触的时候是Down手指落下这个状态,之后移动就是接下来的Move移动状态,手指抬起来的时候就是Up即抬起状态了;如果以鼠标左键点击为例,按下去左键的时候是Down,按住左键不放进行拖动就是Move,松开左键就是Up了。这段我特别解释一下,因为自己翻译的太生硬了。)

控件和事件调度

在图形化的软件开发语境下,控件这个词经常出现,一般是来描述程序中用于和用户进行交互的组件。在Kivy中,控件是用来接收各种输入事件的。所以并不一定非要在屏幕上能看得到。Kivy当中所有控件都以控件树的形式来管理,学过计算机科学中数据结构相关知识的话,就会意识到这是一种树形结构:一个控件可以有任意多个子控件,也可以没有子控件。根控件就只能有一个,处于树形结构的顶端,根控件不具有父控件,并且所有其他控件都是根控件的直接或者间接子控件(就像树根一样,所以叫根控件)。

当新的输入数据可用的时候,Kivy会针对每一个Touch发出一个事件。控件树种的根控件首先接收到这个事件。Touch的不同状态,on_touch_down, on_touch_move和on_touch_up (Down落下、Move移动和Up),会作为Touch的参数,提供给根控件,根控件会调用对应的事件Handler来作出反应。

包括根控件在内,控件树种的每个控件都可以有两种选择,处理该事件,或者将该事件传递下去。如果一个事件的Handler返回True,就意味着这个事件已经被接收并妥善处理。这个事件就也到此为止了。如果不是这样,事件的Handler会跳过此处的空间,调用父类中的对应事件的Handler实现,传递给该控件的子控件。这样的过程可以一路走到最基础的控件类Widget,在它的Touch事件Handler中,只是把Touch传递给子控件,而不进行其他的操作。

  1. # This is analogous for move/up:
  2. def on_touch_down(self, touch):
  3. for child in self.children[:]:
  4. if child.dispatch('on_touch_down', touch):
  5. return True

说起来挺麻烦,看上去挺复杂,实际上要简单得多。下一章就会讲解如何使用这种特性来快速创建应用了。

经常有一种情况,就是你可能要让一个控件只在屏幕上某个特定的区域来监听Touch事件。这时候就可以使用控件的collide_point()方法来实现此目的。只需要把Touch的位置发给该方法,然后如果此位置位于监听区域则返回True,反之返回False。默认情况下,这个方法会监听屏幕上的一个矩形区域,根据控件的中心坐标(x & y坐标系),以及空间尺寸(宽度和高度),不过你也可以用自己的类覆盖掉这一行为。