目录
KV 语言¶
语言背后的概念¶
随着您的应用变得越来越复杂,构建控件树和显式声明绑定通常会变得冗长且难以维护。KV 语言正是为了克服这些不足而设计的。
`KV`语言,有时也被称为kvlang或kivy语言,允许您以声明式的方式创建控件树,并自然地绑定控件属性到彼此或回调函数。它支持快速原型制作和UI的敏捷更改,同时也有助于分离应用程序的逻辑与其用户界面。
如何加载KV?¶
有两种方式可以将Kv代码加载到你的应用程序中:
按照命名惯例:
Kivy 会查找与你的 App 类同名的小写 Kv 文件,如果类名以 'App' 结尾,则去掉 'App',例如:
MyApp -> my.kv
如果该文件定义了一个`Root Widget`,它将被附加到App的`root`属性上,并作为应用程序控件树的基础。
:
Builder:您可以告诉 Kivy 直接加载一个字符串或文件。如果该字符串或文件定义了一个根部件,该方法将返回它,如下所示::Builder.load_file('path/to/file.kv')
或:
Builder.load_string(kv_string)
规则上下文¶
Kv 源由用于描述 Widget 内容的 规则 组成。您可以有一个 根 规则,以及任意数量的 类 或 动态类 规则。
root 规则通过声明根部件的类来定义,无需缩进,后跟 :,并将被设置为 App 实例的 root 属性::
Widget:
由 < > 中的控件类名称声明,后跟 : 的 class 规则,定义了该类任何实例的外观和行为:
<MyWidget>:
规则使用缩进进行分隔,与Python类似。每级缩进应为四个空格,遵循Python风格指南`推荐 <https://www.python.org/dev/peps/pep-0008/#indentation>`_。
Kv 语言中有三个特定的关键字:
app:始终指代你的应用程序实例。
root:指当前规则中的基础控件。
self:始终指代当前部件
特殊语法¶
有一种特殊语法,用于为整个Kv上下文定义值。
要从kv中访问Python模块和类,请使用 #:import
#: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
在Python中。
要设置全局值,请使用 #:set
#:set name value
等同于::
name = value
在Python中。
实例化子组件¶
要声明某个类的widget实例作为子widget,只需在规则中声明该子widget即可:
MyRootWidget:
BoxLayout:
Button:
Button:
上面的例子定义了我们根部件(一个`MyRootWidget`的实例)有一个子部件,它是:class:`~kivy.uix.boxlayout.BoxLayout`的一个实例,并且该BoxLayout进一步有两个子部件,它们是:class:`~kivy.uix.button.Button`类的实例。
这段代码的Python等效写法可能是:
root = MyRootWidget()
box = BoxLayout()
box.add_widget(Button())
box.add_widget(Button())
root.add_widget(box)
你可能会觉得,无论是阅读还是编写,都不那么令人愉快。
当然,在Python中,你可以在创建控件时通过关键字参数来指定它们的行为。例如,要设置:mod:`~kivy.uix.gridlayout`的列数,我们可以这样做:
grid = GridLayout(cols=3)
在kv中实现相同操作,可以直接在规则中设置子部件的属性:
GridLayout:
cols: 3
该值会被作为Python表达式求值,并且表达式中使用的所有属性都会被观察,这意味着如果你在Python中有类似这样的代码(这里假设`self`是一个带有`data` :class:`~kivy.property.ListProperty`属性的控件):
grid = GridLayout(cols=len(self.data))
self.bind(data=grid.setter('cols'))
为了让您的显示在数据变化时更新,现在只需:
GridLayout:
cols: len(root.data)
备注
控件名称应以大写字母开头,而属性名称应以小写字母开头。建议遵循 PEP8 命名规范。
事件绑定¶
在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_``事件。例如,TextInput类有一个``focus``属性,其自动生成的``on_focus``事件可以在kv语言中这样访问:
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`。
引用控件(Widgets)¶
在控件树中,经常需要访问或引用其他控件。Kv语言提供了一种通过使用id来实现这一目的的方法。可以将它们视为只能在Kv语言中使用的类级别变量。考虑以下示例:
<MyFirstWidget>:
Button:
id: f_but
TextInput:
text: f_but.state
<MySecondWidget>:
Button:
id: s_but
TextInput:
text: s_but.state
id 的作用域仅限于其声明所在的规则内,因此在上面的代码中,s_but 无法在 <MySecondWidget> 规则之外被访问。
警告
给``id``赋值时,请记住该值不是字符串。不要加引号:正确写法 -> id: value,错误写法 -> id: 'value'
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'
尽管在``MyWidget``中存储了对``label_widget``的引用,但这并不足以在其他引用被移除后保持对象存活,因为它只是一个弱引用(weakref)。因此,在点击移除按钮(这会移除对该小部件的任何直接引用)并调整窗口大小(这会调用垃圾回收器,导致``label_widget``被删除)之后,当点击添加按钮将该小部件重新添加时,将会抛出``ReferenceError: weakly-referenced object no longer exists``错误。
为了保持控件的存活,必须保留对``label_widget``控件的直接引用。在这种情况下,可以通过使用``id.__self__``或``label_widget.__self__``来实现。正确的做法如下:
<MyWidget>:
label_widget: label_widget.__self__
在您的Python代码中访问在Kv语言中定义的Widget¶
请考虑以下位于 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)
在我的app.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,并初始化为 None。:
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 传递给需要使用它的函数,如上述代码中 f_but 的情况。
在Kv中使用`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标签的widget,并将它们放入这个`self.ids`字典类型的属性中。这意味着你也可以遍历这些widget,并以字典方式访问它们:
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:
与其在每个按钮上重复相同的值,不如直接使用动态类,如下所示:
<MyBigButton@Button>:
text_size: self.size
font_size: '25sp'
markup: True
<MyWidget>:
MyBigButton:
text: "Hello world, watch this text wrap inside the button"
MyBigButton:
text: "Even absolute is relative to itself"
MyBigButton:
text: "repeating the same thing over and over in a comp = fail"
MyBigButton:
该类仅通过声明此规则即可创建,它继承自Button类,并允许我们更改默认值以及为其所有实例创建绑定,而无需在Python端添加任何新代码。
在多个部件中重用样式¶
请考虑以下位于 my.kv 中的代码:
<MyFirstWidget>:
Button:
on_press: root.text(txt_inpt.text)
TextInput:
id: txt_inpt
<MySecondWidget>:
Button:
on_press: root.text(txt_inpt.text)
TextInput:
id: txt_inpt
在我的app.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样式,如果我们为两个控件复用该样式,这种设计可以简化。你可以在.kv文件中按如下方式实现。在my.kv中:
<MyFirstWidget,MySecondWidget>:
Button:
on_press: root.text(txt_inpt.text)
TextInput:
id: txt_inpt
通过在声明中用逗号分隔类名,所列出的所有类将共享相同的kv属性。
使用Kivy语言进行设计¶
Kivy 语言的目标之一是`分离关注点 <https://en.wikipedia.org/wiki/Separation_of_concerns>`_,即呈现与逻辑的分离。呈现(布局)方面由您的 .kv 文件处理,而逻辑则由您的 .py 文件处理。
代码位于py文件中。¶
让我们从一个简单的例子开始:一个名为 main.py 的 Python 文件:
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()
在这个例子中,我们创建了一个包含2个属性的Controller类:
info用于接收一些文本。
label_wid用于接收标签控件。
此外,我们正在创建一个``do_action()``方法,该方法将使用这两个属性。它将更改``info``文本,并更改``label_wid``小部件中的文本。
布局位于controller.kv文件中。¶
在没有对应的 .kv 文件的情况下运行此应用程序可以工作,但屏幕上不会显示任何内容。这是预期的,因为 Controller 类中没有小部件,它只是一个 FloatLayout。我们可以在名为 controller.kv 的文件中围绕 Controller 类创建用户界面,该文件将在我们运行 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``中有一个标签和一个按钮。看起来非常简单。这里涉及三件事:
使用来自
Controller的数据。一旦控制器中的info属性发生变化,表达式text: 'My controller info is: ' + root.info将自动重新求值,从而改变Button中的文本。向
Controller提供数据。表达式id: my_custom_label将创建的Label分配了标识符my_custom_label。然后,在表达式label_wid: my_custom_label中使用my_custom_label,将那个Label部件的实例提供给你的Controller。使用``Controller``的``on_press``方法在``Button``中创建自定义回调。
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 会被加载,这样 Button 和 Label 就会显示出来,并对我们的触摸事件作出响应。
更多文档。¶
关于`KV`语言不同组件的完整描述、高级用法及限制,请参阅:mod:`~kivy.lang`的文档。