10. 语言

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

英文原文

语言背后的概念

随着你的应用程序越写越复杂,就往往会发现控件树的结构/各种绑定的声明等等,都越来越繁琐复杂了,维护起来也很费力气。KV 语言就是为了解决这个问题而设计出来的。

(译者注:这种情况在 GUI 界面的 APP 开发中很常见,比如在 Android 开发的过程中,就用到了 xml 来定义界面元素的关系等等。)

KV 语言(英文缩写也叫 kvlang 或者 kivy 语言),可以让开发者用描述的方式来创建控件树,以及绑定控件对应的属性,以实现一种自然地调用。这一设计可以允许用户能够快速建立应用雏形,然后对界面进行灵活调整。此外,这样的设计还使得运行逻辑与用户界面相互分离不干扰。

载入 KV 的方法

通过以下两种方法都可以在你的应用程序中载入 KV 代码:

  • 通过同名文件查找:
    Kivy 会找跟 App 类同名的小写字母的 Kv 扩展名的文件,如果你的应用类尾部有 App 字样,查找的时候会找去掉这个 App 字样的文件,例如:
    MyApp -> my.kv
    如果这个文件定义了一个根控件,这个文件就会被添加到应用的根属性中去,然后用作整个程序的控件树基础。

  • Builder: 也可以直接指定让 Kivy 去加载某个字符串或者文件。如果这个字符串或者文件定义了一个根控件,就会被下面这个方法返回:
    Builder.load_file('path/to/file.kv')或者Builder.load_string(kv_string)

规则语义

Kv 源文件包含有各种规则,这些规则是用来描述控件的环境设定的,可以有一个根规则,然后其他的各种类的或者模板的规则就都不限制数量来。

根规则是用来描述你的根控件类的,不能有任何缩进,跟着一个英文冒号:,在应用程序的实例当中这就会被设置成根属性:

Widget:

一类规则,声明方式为将控件类的名字用尖括号括起来的,然后跟着一个英文冒号:,这类规则用来定义这个类的实例图形化呈现的方式:

<MyWidget>:

Kv 文件中各种规则都用缩进来进行区块划分,就像 Python 里面一样,这些缩进得是四个空格作为一层缩进,就跟 Python 里面推荐的做法是一样的。

以下是三个 Kv 语言的关键词:

  • app: 指向你应用程序的实例。
  • root: 指向当前规则中的基础控件或者基础模板。
  • self: 指向当前的控件。

特殊语法

有两种特殊的语法,能定义整个 Kv 环境下的各种值:

读取 Kv 中的 Python 模块和各种类:

  1. #:import name x.y.z
  2. #:import isdir os.path.isdir
  3. #:import np numpy

等价于 Python 中的:

  1. from x.y import z as name
  2. from os.path import isdir
  3. import numpy as np

设置各种全局变量:

  1. #:set name value

等价于 Python 中的:

  1. name = value

子对象实例化

To declare the widget has a child widget, instance of some class, just declare this child inside the rule:

给一个控件声明子控件,比如某个类的实例,只要在规则内部声明一下这个子对象就可以类:

  1. MyRootWidget:
  2. BoxLayout:
  3. Button:
  4. Button:

上面的样例代码定义了根控件,是一个 MyRootWidget 的实例,它有一个子控件,是一个 BoxLayout 实例。这个 BoxLayout 还有自己的两个子对向,是两个 Button 类的实例。

与上面代码等价的 Python 代码大致如下:

  1. root = MyRootWidget()
  2. box = BoxLayout()
  3. box.add_widget(Button())
  4. box.add_widget(Button())
  5. root.add_widget(box)

你可能会发现直接用 Python 来实现的代码不那么好阅读,写起来也不那么简便。

用 Python 可以在创建控件的时候传递关键词参数过去,来指定这些控件的行为。例如下面的这个代码就是设定一个 gridlayout 中的栏目数:

  1. grid = GridLayout(cols=3)

同样目的也可以用 Kv 来实现,可以直接在规则内指定好子控件的属性:

  1. GridLayout:
  2. cols: 3

这个值会作为一个 Python 表达式来进行计算,然后所有在表达式中用到的属性都是可见的,就好比下面的 Python 代码一样(这里假设 self 是一个有 ListProperty 数据的控件):

  1. grid = GridLayout(cols=len(self.data))
  2. self.bind(data=grid.setter('cols'))

如果想要在数据修改的时候就更新显示,可以用如下方法实现:

  1. GridLayout:
  2. cols: len(root.data)

特别注意

控件的名字一定要用大写字母打头,而属性的名字一定要用小写的。推荐遵循PEP8 命名惯例

事件绑定

在 Kv 中,使用英文冒号 “:” 就可以来进行事件绑定,也就是将某个回调和一个事件联系起来:

  1. Widget:
  2. on_size: my_callback()

使用 args 关键词的信号,就能把分派来的值传递过去类:

  1. TextInput:
  2. on_text: app.search(args[1])

还可以用更加复杂的表达式,例如:

  1. pos: self.center_x - self.texture_size[0] / 2., self.center_y - self.texture_size[1] / 2.

上面这段表达式中,监听了center_x, center_y, texture_size这三个属性的变化。只要其中有一个发生了变化,表达式就会重新计算来更新 pos 的区域。

你还看以在 kv 语言中处理 on_ 事件。例如 TextInput 这个类就有一个 focus 属性,这个数行自动生成的 on_focus 事件可在 kv 语言内进行读取:

  1. TextInput:
  2. on_focus: print(args)

扩展画布

Kv 语言也已用来定义你控件的画布,如下所示:

  1. MyWidget:
  2. canvas:
  3. Color:
  4. rgba: 1, .3, .8, .5
  5. Line:
  6. points: zip(self.data.x, self.data.y)

当属性发生变化的时候,这些画布就会更新。当然也可以用 canvas.before 和 canvas.after。

定位控件

在一个控件树当中,经常会需要去读取或者定位其他的控件。 Kv 语言提供了一种快速的方法来实现这一目的,就是使用 id。(译者注:在 Android 的开发中就是这样的。)
可以把这些控件当作是类这以层次的变量,只能在 Kv 语言中使用。例如下面的:

  1. <MyFirstWidget>:
  2. Button:
  3. id: f_but
  4. TextInput:
  5. text: f_but.state
  6. <MySecondWidget>:
  7. Button:
  8. id: s_but
  9. TextInput:
  10. text: s_but.state

An id is limited in scope to the rule it is declared in, so in the code above s_but can not be accessed outside the <MySecondWidget> rule.

id 只能再所处的规则内使用,也就是声明它的位置,所以在上面的代码中,s_but 就不能在 <MySecondWidget> 规则外被读取到。

特别注意

给一个 id 赋值的时候,一定要记住这个值不能是字符串。所以不能有引号:这是正确的 -> id: value, 这样就不对 -> id: 'value'
id 是对空间的一个 weakref (弱引用),而不是控件本身。所以,在垃圾回收的时候要保存控件,就不能仅仅保存 id

下面的代码中:

  1. <MyWidget>:
  2. label_widget: label_widget
  3. Button:
  4. text: 'Add Button'
  5. on_press: root.add_widget(label_widget)
  6. Button:
  7. text: 'Remove Button'
  8. on_press: root.remove_widget(label_widget)
  9. Label:
  10. id: label_widget
  11. text: 'widget'

虽然 MyWidget 中已经存储了一个对 label_widget 的引用,但是这个只是一个弱引用,其他引用被移除的时候还不足以保证对象依然可用。因此,在移除按钮被点击(这时候也就是移除类所有对这个控件的引用)之后,或者窗口大小被调整了(这会调用垃圾回收器,导致删掉 label_widget),这时候如果点击添加按钮来重新把控件增加回来的话,就会有一个引用错误(ReferenceError)被抛出来:因为弱引用的对象已经不存在类。

  1. <MyWidget>:
  2. label_widget: label_widget.__self__

Python 代码读取在 Kv 中定义的控件

假如在 my.kv 文件中有如下的代码:

  1. <MyFirstWidget>::
  2. # both these variables can be the same name and this doesn't lead to
  3. # an issue with uniqueness as the id is only accessible in kv.
  4. txt_inpt: txt_inpt
  5. Button:
  6. id: f_but
  7. TextInput:
  8. id: txt_inpt
  9. text: f_but.state
  10. on_text: root.check_status(f_but)

在 myapp.py 这个文件中:

  1. ...
  2. class MyFirstWidget(BoxLayout):
  3. txt_inpt = ObjectProperty(None)
  4. def check_status(self, btn):
  5. print('button state is: {state}'.format(state=btn.state))
  6. print('text input text is: {txt}'.format(txt=self.txt_inpt))
  7. ...

txt_inpt 是一个ObjectProperty对象,在类内被初始化为 None。

  1. txt_inpt = ObjectProperty(None)

目前位置,这个 self.txt_inpt 还是 None。在 Kv 语言中,这个属性会进行更新,保存 txt_input 这个 id 所引用的 TextInput 实例:

  1. txt_inpt: txt_inpt

从这以后,self.txt_inpt 久保存了一个到控件的引用,通过 txt_input 这个 id 来识别,可以在类内的各个地方来使用,就跟在 check_status 函数里一样。当然也可以不这么做,可以把 id 传给需要用到它的函数,例如上面代码中那个 f_but 这个例子。

通过 id 标签,在 kv 语言中可以查找对象,这是一种更简单的读取对象的方法。比如下面这段代码:

  1. <Marvel>
  2. Label:
  3. id: loki
  4. text: 'loki: I AM YOUR GOD!'
  5. Button:
  6. id: hulk
  7. text: "press to smash loki"
  8. on_release: root.hulk_smash()

在你的 Python 代码中:

  1. class Marvel(BoxLayout):
  2. def hulk_smash(self):
  3. self.ids.hulk.text = "hulk: puny god!"
  4. self.ids["loki"].text = "loki: >_ # alternative syntax

当你的 kv 文件被解析的时候,kivy 会选中所有带有标签 id 的控件,然后把它们放到 self.ids 这样一个辞典类型的属性里面去。所以你就可以对这些控件进行遍历,然后像是辞典数据一样来进行读取类:

  1. for key, val in self.ids.items():
  2. print("key={0}, val={1}".format(key, val))

特别注意

虽然这种 self.ids 方法非常简便,但通常最推荐的还是用对象属性。这样会创建一个直接的引用,能提供更快地读取速度,并且也更加准确可靠。

动态类型

参考下面的代码:

  1. <MyWidget>:
  2. Button:
  3. text: "Hello world, watch this text wrap inside the button"
  4. text_size: self.size
  5. font_size: '25sp'
  6. markup: True
  7. Button:
  8. text: "Even absolute is relative to itself"
  9. text_size: self.size
  10. font_size: '25sp'
  11. markup: True
  12. Button:
  13. text: "Repeating the same thing over and over in a comp = fail"
  14. text_size: self.size
  15. font_size: '25sp'
  16. markup: True
  17. Button:

如果这里使用一个模板,就不用那么麻烦地去重复对每一个按钮进行设置了,例如下面这样就可以了:

  1. <MyBigButt@Button>:
  2. text_size: self.size
  3. font_size: '25sp'
  4. markup: True
  5. <MyWidget>:
  6. MyBigButt:
  7. text: "Hello world, watch this text wrap inside the button"
  8. MyBigButt:
  9. text: "Even absolute is relative to itself"
  10. MyBigButt:
  11. text: "repeating the same thing over and over in a comp = fail"
  12. MyBigButt:

上面这个类是在这个规则的声明内建立的,继承了按钮类 Button,然后我们就可以用这个类来对默认值进行修改,并且建立所有实例的链接绑定,而不用在 Python 弄一大堆新代码了。

在多个控件中复用样式

参考下面的代码,在 my.kv 文件中:

  1. <MyFirstWidget>:
  2. Button:
  3. on_press: root.text(txt_inpt.text)
  4. TextInput:
  5. id: txt_inpt
  6. <MySecondWidget>:
  7. Button:
  8. on_press: root.text(txt_inpt.text)
  9. TextInput:
  10. id: txt_inpt

在 myapp.py 这个文件中:

  1. class MyFirstWidget(BoxLayout):
  2. def text(self, val):
  3. print('text input text is: {txt}'.format(txt=val))
  4. class MySecondWidget(BoxLayout):
  5. writing = StringProperty('')
  6. def text(self, val):
  7. self.writing = val

好多类都要用到同样的 .kv 样式文件,这样就可以通过让所有控件都对样式进行复用,就能简化一下设计。这就可以在 kv文件中来实现。例如下面这个就是 my.kv 文件中的代码:

  1. <MyFirstWidget,MySecondWidget>:
  2. Button:
  3. on_press: self.text(txt_inpt.text)
  4. TextInput:
  5. id: txt_inpt

只要把各个类的名字用英文冒号:分隔开,声明当中包含的类就都会使用相同的 kv 属性了。

使用 Kivy 语言来设计

Kivy 语言的设计目标之一就是希望能够做好分工,把界面和内部逻辑相互分离。输出的样式由你的 kv 文件来确定,运行逻辑靠你的 python 代码来执行。

Python 文件中的代码

来一个简单的小例子。首先要有一个名字为 main.py 的 Python 源文件:

  1. import kivy
  2. kivy.require('1.0.5')
  3. from kivy.uix.floatlayout import FloatLayout
  4. from kivy.app import App
  5. from kivy.properties import ObjectProperty, StringProperty
  6. class Controller(FloatLayout):
  7. '''
  8. 创建一个控制器,从 kv 文件中接收一个定制的控件。
  9. 增加一个由 kv 文件进行调用的动作。
  10. '''
  11. label_wid = ObjectProperty()
  12. info = StringProperty()
  13. def do_action(self):
  14. self.label_wid.text = 'My label after button press'
  15. self.info = 'New info text'
  16. class ControllerApp(App):
  17. def build(self):
  18. return Controller(info='Hello world')
  19. if __name__ == '__main__':
  20. ControllerApp().run()

刚刚这个代码样例中,我们创建了一个有两个属性的控制器:

  • info 该属性用于接收文本
  • label_wid 该属性用于接收文本标签控件

此外还创建了一个 do_action() 方法,这个方法会使用上面的属性。这个方法会改变 info里面的文本,以及 label_wid 控件中的文本。

controller.kv 文件中的布局样式

没有对应的 kv 文件,应用程序也能运行,只不过是屏幕上不会有任何显示输出。这个是符合情理的,因为毕竟控制器 Controller 这个类是没有任何控件的,就只是一个FloatLayout(流动输出?)咱们可以创建一个名字为 controller.kv 的文件,来围绕着这个控制器类 Controller 搭建 UI (用户界面),这个文件会在运行 ControllerApp 的时候被加载。这个具体怎么实现的,以及加载类哪些文件,可以参考 kivy.app.App.load_kv() 方法里的描述。

  1. #:kivy 1.0
  2. <Controller>:
  3. label_wid: my_custom_label
  4. BoxLayout:
  5. orientation: 'vertical'
  6. padding: 20
  7. Button:
  8. text: 'My controller info is: ' + root.info
  9. on_press: root.do_action()
  10. Label:
  11. id: my_custom_label
  12. text: 'My label before button press'

上面的代码中就是一个竖直的箱式布局(BoxLayout)。看着就挺简单的。这个代码主要功能有以下三个:

1 使用来自控制器类 Controller 的数据。只要 controller 中的 info 属性发生了改变,表达式 ‘My controller info is: ‘ + root.info 就会自动重新计算,改变按钮(Button)上面的值。

2 传递数据给控制器类 Controller。my_custom_label 这个 id 会赋值给 id 为 my_custom_label 的新建文本标签(Label)。此后,使用 label_wid:my_custom_label 中的 my_custom_label 就能得到一个控制器中的 Label 控件实例了。

3 在按钮 Button 中使用控制器类 Controller 的 on_press 方法来创建一个自定义回调。
root 和 self 都是保留关键词,任何地方都不可见的。root 代表的是规则中的顶层控件,self 表示的是当前控件。
你可以在当前规则中使用任意的已经声明过的 id, root 和 self 也可以这样来用。比如下面就是在 on_press() 里面使用 root:

  1. Button:
  2. on_press: root.do_action(); my_custom_label.font_size = 18

就这么多了。现在当我们再次运行 main.py的时候,controller.kv 就会被加载,然后 Button 按钮和 Label 文本标签就会出现,并且根据触摸事件进行响应了。