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_inptNone。在 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``中有一个标签和一个按钮。看起来非常简单。这里涉及三件事:

  1. 使用来自 Controller 的数据。一旦控制器中的 info 属性发生变化,表达式 text: 'My controller info is: ' + root.info 将自动重新求值,从而改变 Button 中的文本。

  2. Controller 提供数据。表达式 id: my_custom_label 将创建的 Label 分配了标识符 my_custom_label。然后,在表达式 label_wid: my_custom_label 中使用 my_custom_label,将那个 Label 部件的实例提供给你的 Controller

  3. 使用``Controller``的``on_press``方法在``Button``中创建自定义回调。

    • rootself 是保留关键字,可在任何地方使用。root 表示规则中的顶层部件,self 表示当前部件。

    • 您可以在规则中声明的任何id,与``root``和``self``一样使用。例如,您可以在``on_press()``中这样做:

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

就是这样。现在当我们运行 main.py 时,controller.kv 会被加载,这样 ButtonLabel 就会显示出来,并对我们的触摸事件作出响应。

更多文档。

关于`KV`语言不同组件的完整描述、高级用法及限制,请参阅:mod:`~kivy.lang`的文档。