编程向导:4.9KV语言
一、语言背后的思想
当你的应用程序变得更复杂时,构建部件树和明确的声明绑定将变得冗长和难以维护。KV语言试图克服这些缺点。
KV语言(有时被叫kvlang,或kivy语言),允许你以声明的方式来创建你的部件树,并以一种自然的方式绑定部件属性或回调函数。针对UI,它支持快速原型和敏捷改动。它也使得逻辑和用户接口能更好的分离。
二、如何加载KV
有两种方式来加载KV代码:
-
通过名字约定
Kivy查找你的应用程序类的小写的同名KV文件,如果它以'App'结尾则去掉它,例如:MyApp -> my.kv
如果这个文件定义了一个根部件,它将会附着到应用程序的根特征值,并用它作为应用程序部件树的根。
-
Builder
你可以告诉Kivy直接加载一个字符串或一个文件。如果这个字符串或文件定义了根部件,它将被返回。Builder.load_file('path/to/file.kv')
或者
Builder.load_string('kv_string')
三、管理上下文
一个KV源构成的规则,用来描述部件的内容。你可以有一个根规则和任何数量的类或模板规则。
根规则通过声明你的根部件类来声明,不需要任何缩进,后面跟着冒号(:),并且被设置为应用程序实例的根特征值。
Widget:
一个类规则,有一对尖括号(<>)包括部件类名组成,后面跟冒号(:),定义类的实例如何被生动地表达:
<MyWidget>:
和Python一样,规则使用缩进进行界定,和良好的Python习惯一样,缩进的每一级别最好是4个空格。
有三个关键字来指定KV语言:
- app:总是引用你的应用程序的实例。
- root:引用当前规则中的根部件/模板。
- self:引用当前部件。
四、特殊的语法
有两个特殊语法来为整个KV上下文定义值:
- 为了从KV中访问Python的模块和类:
#:import name x.y.z
#:import isdir os.path.isdir
#:import np numpy
上面的代码等价于:
from x.y import z as name
from os.path import isdir
import numpy as np
- 为了设置一个全部变量:
#:set name value
等价于:
name = value
五、实例化子部件
为了声明部件的子部件,仅在规则里面声明这些子部件即可:
MyRootWidget:
BoxLayout:
Button:
Button:
上面的例子定义了一个MyRootWidget的实例作为我们的根部件,它有一个子部件是BoxLayout的实例。BoxLayout进一步有两个Button类的子部件。在Python代码中应该是这样:
root = MyRootWidget()
box = BoxLayout()
box.add_widget(Button())
box.add_widget(Button())
root.add_widget(box)
你会发现在KV中,仅用很少的代码,易写并易读。
当然,在Python中,你可以传递关键字参数到你的部件中。例如,设置一个GridLayout的列的数目,我们可以这样写:
grid = GridLayout(cols = 3)
在KV中,你可以直接在规则中设置子部件的属性:
GridLayout:
cols:3
这个值被评估为一个Python表达式,并且表达式中所有的属性值都将被监听。例如在Python中:
grid = GridLayout(cols = len(self.data))
self.bind(data = grid.setter('cols'))
当你的数据变化时,显示跟着更新,在KV中只需这样:
GridLayout:
cols:len(root.data)
注意,当属性名以小写字母开头时,部件名首字母应当大写。遵循PEP8 Naming Conventions是被鼓励的。
六、事件绑定
在KV语言中,你可以使用":"语法来绑定事件:
Widget:
on_size: my_callback()
你也可以使用args关键字传递参数:
TextInput:
on_text:app.search(args[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_事件。例如输入框有一个聚焦(focus)属性,它将自动生成on_focus事件:
TextInput:
on_focus:print(args)
七、扩展画布
KV语言可以这样来定义你的画布指令:
MyWidget:
canvas:
Color:
rgba: 1, .3, .8, .5
Line:
points: zip(self.data.x, self.data.y)
当属性值改变时它们将更新,当然,你也可以使用canvas.before和canvas.after.
八、引用部件
在一个部件树中,经常需要访问/引用其他的部件。KV语言提供了一个使用id's的方法来做这些工作。将它们认为是只能用于Kv语言类级别变量。看下面代码:
<MyFirstWidget>:
Button:
id: f_but
TextInput:
text: f_but.state
<MySecondWidget>:
Button:
id: s_but
TextInput:
text: s_but.state
一个id被限制到它被声明的作用域内,所以在<MySecondWidget>外面s_but不能被访问。
id是一个部件的弱引用(weakref)并且不是部件本身。因此,存储id不能防止部件被垃圾回收。为了证明:
<MyWidget>:
label_widget: label_widget
Button:
text: 'Add Button'
on_press: root.add_widget(label_widget)
Button:
text: 'Remove Button'
on_press: root.remove_widget(label_widget)
Label:
id: label_widget
text: 'widget'
上面的代码中,虽然一个到label_widget的引用被存储到MyWidget中,但是因为它仅仅是一个弱引用,一旦别的引用被移除,它不足以保持对象存活。因此,当移除按钮被点击后(将移除其他的引用)窗口将重新计算尺寸(调用垃圾回收导致重新检测label_widget),当点击添加按钮来添加部件,一个引用错误将发生(ReferenceError:weakly-referenced object no longer exists)
为了保持部件存活,一个对label_widget的引用必须被保持。可以使用id.self或label_widget.self做到。正确的方式如下:
<MyWidget>:
label_widget: label_widget.__self__
九、在Python代码中访问Kv语言定义的部件
考虑以下在my.kv中的代码:
<MyFirstWidget>:
# both these variables can be the same name and this doesn't lead to
# an issue with uniqueness as the id is only accessible in kv.
txt_inpt: txt_inpt
Button:
id: f_but
TextInput:
id: txt_inpt
text: f_but.state
on_text: root.check_status(f_but)
在myapp.py:
...
class MyFirstWidget(BoxLayout):
txt_inpt = ObjectProperty(None)
def check_status(self, btn):
print('button state is: {state}'.format(state=btn.state))
print('text input text is: {txt}'.format(txt=self.txt_inpt))
...
txt_inpt被作为ObjectProperty初始化:
txt_inpt = ObjectProperty(None)
这是效果导致self.txt_inpt是None。在KV语言中,这个属性更新被id:txt_inpt引用的持有TextInput的实例。
txt_inpt:txt_inpt
从这点向上,self.txt_inpt持有一个被id txt_input标识的部件的引用并且能被用在类的任何地方,正如在check_status函数中一样。对照这个函数,你仅仅需要传递id到你想用的地方。
你可以使用ids来访问带id标识的对象,这是一种更简单的方法:
<Marvel>
Label:
id: loki
text: 'loki: I AM YOUR GOD!'
Button:
id: hulk
text: "press to smash loki"
on_release: root.hulk_smash()
在你的Python代码中:
class Marvel(BoxLayout):
def hulk_smash(self):
self.ids.hulk.text = "hulk: puny god!"
self.ids["loki"].text = "loki: >_<!!!" # alternative syntax
当你的kv文件被解析时,kivy收集所有的带id标签的部件,并放置它们到self.ids字典中。这意味着你能以字典的风格来迭代这些部件并访问它们。
for key, val in self.ids.items():
print("key={0}, val={1}".format(key, val))
注意,虽然self.ids很简洁,它被认为是使用ObjectProperty的最佳实践。但是创建一个字典的引用,将会提供更快的访问速度并更加清晰。
十、动态类
考虑下面代码:
<MyWidget>:
Button:
text: "Hello world, watch this text wrap inside the button"
text_size: self.size
font_size: '25sp'
markup: True
Button:
text: "Even absolute is relative to itself"
text_size: self.size
font_size: '25sp'
markup: True
Button:
text: "Repeating the same thing over and over in a comp = fail"
text_size: self.size
font_size: '25sp'
markup: True
Button:
为了替代重复的代码,我们可以使用模板来代替:
<MyBigButt@Button>:
text_size: self.size
font_size: '25sp'
markup: True
<MyWidget>:
MyBigButt:
text: "Hello world, watch this text wrap inside the button"
MyBigButt:
text: "Even absolute is relative to itself"
MyBigButt:
text: "repeating the same thing over and over in a comp = fail"
MyBigButt:
这个被规则声明的类继承自按钮类。它允许我们改变默认值,并为每一个实例创建绑定而不用在Python那边添加任何新的代码。
十一、在多个部件中重用样式
看下面的在my.kv中的代码:
<MyFirstWidget>:
Button:
on_press: self.text(txt_inpt.text)
TextInput:
id: txt_inpt
<MySecondWidget>:
Button:
on_press: self.text(txt_inpt.text)
TextInput:
id: txt_inpt
在myapp.py中
class MyFirstWidget(BoxLayout):
def text(self, val):
print('text input text is: {txt}'.format(txt=val))
class MySecondWidget(BoxLayout):
writing = StringProperty('')
def text(self, val):
self.writing = val
因为两个类共同使用相同的.kv风格。如果我们为两个部件重用风格,这将使得设计简化。你可以在my.kv中这样写代码:
<MyFirstWidget,MySecondWidget>:
Button:
on_press: self.text(txt_inpt.text)
TextInput:
id: txt_inpt
用一个逗号(,)来分离类名,所有的类将都有同样的kv属性。
十二、使用KV语言设计
使用Kivy语言的一个目标就是分离逻辑和表现。表现层使用kv文件来表示,逻辑使用py文件来表示。
(一)py文件中写代码
让我们开始一个小例子,首先,在main.py文件中:
import kivy
kivy.require('1.0.5')
from kivy.uix.floatlayout import FloatLayout
from kivy.app import App
from kivy.properties import ObjectProperty, StringProperty
class Controller(FloatLayout):
'''Create a controller that receives a custom widget from the kv lang file.
Add an action to be called from the kv lang file.
'''
label_wid = ObjectProperty()
info = StringProperty()
def do_action(self):
self.label_wid.text = 'My label after button press'
self.info = 'New info text'
class ControllerApp(App):
def build(self):
return Controller(info='Hello world')
if __name__ == '__main__':
ControllerApp().run()
在这个例子中,我们创建了一个带有两个属性的控制类:
- info:接收一些文本
- label_wid接收标签(label)部件
另外,我们创建了一个do_action()方法来使用这些属性。它将会改变info文本和label_wid部件的文本。
(二)在controller.kv中布局
执行一个没有相应的.kv文件的应用程序可以运行,但是没有任何东西被显示到屏幕上。这是被期望的,因为控制类没有部件在里面,它仅仅是一个FloatLayout。我们能围绕Controller类在一个controller.kv文件中创建UI,当我们运行ControllerApp时它会被加载。这将如何实现及什么文件被加载都在kivy.app.App.load_kv()方法中被描述。
#:kivy 1.0
<Controller>:
label_wid: my_custom_label
BoxLayout:
orientation: 'vertical'
padding: 20
Button:
text: 'My controller info is: ' + root.info
on_press: root.do_action()
Label:
id: my_custom_label
text: 'My label before button press'
在垂直布局的BoxLayout中,有一个标签和一个按钮。看起来很简单,有3个事情将被做:
从Controller使用数据。一旦在controller中info属性被改变,表达式text:'My Controller info is:' + root.info将会自动更新。
传递数据到Controller。表达式id:my_custom_label被赋值给id为my_custom_label的标签。于是,在表达式label_wid:my_custom_label中使用my_custom_label传递部件Label的实例到你的Controller。
-
使用Controller的on_press方法创建一个定制的回调函数。
root和self被保留为关键字,可用在任何地方。root代表规则内的根部件,self代表当前部件。
-
在规则内你可以使用任何id声明,同root和self一样。例如,你可以在on_press()中这样:
Button:
on_press:root.do_action();my_custom_label.font_size = 18
现在,我们运行main.py, controller.kv将会被自动加载,按钮和标签也将显示并响应你的触摸事件。