news 2026/9/21 1:54:10

Kivy Kv Design Language 入门指南:用声明式 .kv 文件快速构建跨平台 GUI

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kivy Kv Design Language 入门指南:用声明式 .kv 文件快速构建跨平台 GUI

Kivy Kv Design Language 入门指南:用声明式 .kv 文件快速构建跨平台 GUI

【免费下载链接】kivyOpen source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS项目地址: https://gitcode.com/gh_mirrors/ki/kivy

导读

Kv Design Language(简称 Kv 语言)是 Kivy 内置的一套专为 GUI 设计打造的声明式描述语言。它让你可以用一份独立的.kv文件描述界面结构与样式,把「界面设计」和「应用逻辑」彻底分离,从而快速原型化、快速迭代,并让界面代码在大规模应用中保持可维护性。读完本文,你将掌握 Kv 语言的最小语法(规则、缩进、属性赋值)、三种加载方式、动态类复用技巧,以及如何把.kv中的控件接入 Python 代码,直接上手编写自己的第一个 Kivy 应用界面。

本文以 Kivy 官方入门文档 doc/sources/gettingstarted/rules.rst 为骨架展开,并参考完整语法指南 doc/sources/guide/lang.rst 与仓库源码进行深化。

为什么需要一门「设计语言」?

随着应用规模增长,直接用 Python 代码手工构建控件树、显式声明绑定(binding),会变得越来越冗长且难以维护。Kv 语言正是针对这一痛点而生:它允许你以声明式(declarative)的方式创建控件树,并自然地让控件属性之间、控件属性与回调之间产生绑定关系。它同时支持非常快的原型开发和敏捷的 UI 调整,也促进了应用逻辑与用户界面的分离——这正是「关注点分离」(separation of concerns)原则在 GUI 领域的体现。

在 Kivy 中,界面设计与应用逻辑可以放在两个不同文件中:

  • 逻辑(.py:负责数据、业务与事件处理;
  • 界面(.kv:负责布局、样式与声明式绑定。

Kv 语言的最小示例:一个登录界面

Kivy 官方入门文档给出了一个极其精简的示例,完整展示了 Kv 语言的三个核心语法要素:

<LoginScreen>: # 每个类都可以用这样的规则(rule)在 kv 文件中表示 GridLayout: # 这样把你的控件/布局添加到父级(注意缩进) rows: 2 # 这样设置控件/布局的每个属性
  • <LoginScreen>:类规则(class rule),它描述了LoginScreen这个类的所有实例的外观与行为;
  • GridLayout:子控件实例声明,缩进表示它是LoginScreen的子节点;
  • rows: 2属性赋值,右侧的表达式会被 Kivy 持续观察,值变化时自动重新求值。

仅凭这 3 行,你就已经掌握了 Kv 语言的全部核心概念。文档中配套的示意图(doc/sources/images/gs-lang.png)展示了左侧 Kv 代码与右侧真实渲染界面的一一对应关系:

图中代码(扩展版)还展示了更多常用写法:

<LoginScreen>: GridLayout: rows: 2 cols: 1 spacing: 10 padding: 10 Label: text: 'User Name:' TextInput: id: username password: False Label: text: 'Password:' TextInput: id: password password: True

从图中可以直观看到:左侧声明式代码如何映射为右侧的登录界面;GridLayoutrows/cols/spacing/padding如何控制行列排布与间距;LabelTextInput如何成对出现;id如何为控件命名以便引用;password: True如何让输入内容以密文显示。

如何加载 Kv 代码

Kv 代码有两种标准加载方式(源码实现见 kivy/app.py 与 kivy/lang/builder.py)。

方式一:按命名约定自动加载

Kivy 会寻找与你的 App 类同名(转为小写、去掉末尾的App后缀)的.kv文件。例如:

MyApp -> my.kv

App.run()首次运行时,若尚未构建控件树,会调用App.load_kv()自动查找并加载该文件(见 kivy/app.py)。查找规则是:.kv文件必须与 App 类定义所在的.py文件位于同一目录。例如main.py中定义了class ShowcaseApp(App),Kivy 就会在main.py所在目录寻找showcase.kv

如果该文件定义了根控件(root widget),它会被挂到 App 的root属性上,作为应用控件树的根基:

ClassName: # 这是根控件(root widget) ...

需要注意的是(源码注释中明确说明):load_kvrun()调用,因此在run()之前(例如在__init__中)创建的控件不会获得该 kv 文件定义的样式;而build()是在load_kv()之后才被调用的。

方式二:通过 Builder 显式加载

你也可以用kivy.lang.Builder直接加载字符串或文件。若加载内容定义了根控件,方法会将其返回:

from kivy.lang import Builder root_widget = Builder.load_file('path/to/file.kv') # 从文件加载 root_widget = Builder.load_string(kv_string) # 从字符串加载

Builder.load_string()在解析后会把规则注册进全局规则表,并实例化根控件返回(见 kivy/lang/builder.py)。它支持rulesonly=True参数,用于只加载规则、禁止出现根控件;也支持filename参数为字符串指定伪文件名,便于之后用Builder.unload_file()卸载同一批规则。load_file()默认按 UTF-8 编码读取,且从 3.0.0 版本起同时支持strpathlib.Path路径(见 kivy/lang/builder.py)。

规则(Rule)体系:根规则、类规则与动态类规则

一份 Kv 源码由若干「规则」组成,用于描述某个控件的内容。你可以在一个 kv 文件中拥有:

  • 1 个根规则(root rule):直接以控件类名开头(无缩进)后跟冒号,会被设置为 App 实例的root属性:

    Widget:
  • 任意数量的类规则(class rule):以< >包裹类名后跟冒号,定义该类的所有实例的外观与行为:

    <MyWidget>:
  • 任意数量的动态类规则(dynamic class rule)<新类名@基类名>:的形式,见下文「动态类」。

规则使用缩进来界定层级,与 Python 一致;官方建议每个缩进层级使用 4 个空格(遵循 PEP 8 的缩进约定)。Kv 语言内置三个关键字:

关键字含义
app始终指向当前应用实例(App 对象)
root指向当前规则中的根控件
self始终指向当前控件本身

在解析器实现中,app被注册为一个ProxyApp代理对象,按需解析为当前正在运行的应用实例(见 kivy/lang/parser.py)。此外,解析器还在全局命名空间中预置了常用度量单位与工具,例如dpspptinchcmmm以及rgba,因此你可以在 kv 表达式中直接写font_size: '25sp'rgba: 1, .3, .8, .5这样的值(见 kivy/lang/parser.py)。

特殊语法:访问 Python 模块与全局值

Kv 语言提供两条指令,用于为整个 kv 上下文定义值。

#:import导入 Python 模块或类

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

等价于 Python 代码:

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

#:set设置全局值

#:set name value

等价于 Python 代码:

name = value

实例化子控件:声明式控件树

在规则内部声明某个类的实例,即可将其作为子控件加入父控件:

MyRootWidget: BoxLayout: Button: Button:

上面定义:根控件是MyRootWidget的实例,它有一个BoxLayout子控件,该布局下再有两个Button子控件。对应的 Python 等价代码是:

root = MyRootWidget() box = BoxLayout() box.add_widget(Button()) box.add_widget(Button()) root.add_widget(box)

相比之下,Kv 版本显然更易读、易写。

属性赋值:声明式绑定

在 Python 中,你可以通过关键字参数在创建时指定控件行为,例如grid = GridLayout(cols=3)。在 Kv 中,直接在规则内设置属性即可:

GridLayout: cols: 3

这里的值会被当作 Python 表达式求值,且表达式中用到的所有属性都会被观察(observed),从而自动建立绑定。比如 Python 中若要实现「data 变化时自动更新列数」:

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

在 Kv 中只需要一行,并保持自动更新:

GridLayout: cols: len(root.data)

这种自动重新求值机制在底层由Builder的绑定逻辑实现:表达式被编译后,解析器通过正则与 AST 分析出其中引用的key.value形式(watched_keys),当任一被观察属性变化时,call_fn会重新eval该表达式并setattr更新属性(见 kivy/lang/parser.py 与 kivy/lang/builder.py)。对于类似self.a.b.c的链式属性,Builder.update_intermediates还支持在中间属性变化时动态解绑/重绑后续链路(见 kivy/lang/builder.py)。

命名约定:控件类名应以大写字母开头,属性名应以小写字母开头,遵循 PEP 8 命名规范。

事件绑定:on_语法与args

Kv 中使用:语法把回调关联到事件:

Widget: on_size: my_callback()

事件分发的参数可以用args关键字引用:

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

Kv 会自动为控件的每个属性生成对应的on_事件。例如TextInputfocus属性,其自动生成的on_focus事件可以在 kv 中这样处理:

TextInput: on_focus: print(args)

复杂表达式同样受支持,并且其中的依赖属性都会建立绑定:

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

该表达式监听了center_xcenter_ytexture_size,其中任一变化都会触发重新求值并更新pos。在解析器实现中,凡属性名以on_开头的赋值都会被归类为事件处理器(handler)而非普通属性(见 kivy/lang/parser.py),其回调表达式以exec模式编译,并可访问args变量。

扩展 Canvas:在 Kv 中绘制图形指令

Kv 语言可以直接定义控件的 canvas 指令,且当属性值变化时指令会自动更新:

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

你也可以使用canvas.beforecanvas.after来插入绘制顺序不同的指令层。解析器在遇到canvascanvas.beforecanvas.after三个特殊属性时,会递归解析其子指令,并分别存入控件的canvas_rootcanvas_beforecanvas_after(见 kivy/lang/parser.py)。

引用控件:id、weakref 陷阱与 ids 查找

在控件树中,经常需要访问其他控件。Kv 语言通过id提供这一能力——可以把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保存的是控件的弱引用(weakref),因此仅靠存储id并不能防止控件被垃圾回收。官方文档给出了一个会触发ReferenceError: weakly-referenced object no longer exists的典型反例:

<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引用,由于它只是弱引用,一旦移除按钮被点击(移除对该控件的直接引用)、窗口被缩放(触发垃圾回收导致label_widget被销毁),再点击添加按钮就会抛出ReferenceError

正确做法是持有直接引用,即使用id.__self__label_widget.__self__

<MyWidget>: label_widget: label_widget.__self__

在 Python 代码中访问 Kv 控件:ObjectProperty 与 ids

假设my.kv中有:

<MyFirstWidget>: # 两个变量可以同名,因为 id 只在 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(None),此时self.txt_inptNone;Kv 中的txt_inpt: txt_inpt会把该属性更新为idtxt_inptTextInput实例。此后self.txt_inpt在类的任何位置都持有该控件引用,例如在check_status函数中使用。与之相对,也可以像f_but那样把 id 直接传给需要它的函数。

另一种更简洁的方式是使用ids查找字典。Kv 文件解析完成后,Kivy 会把所有带id的控件收集进self.ids字典属性:

<Marvel> Label: id: loki text: 'loki: I AM YOUR GOD!' Button: id: hulk text: "press to smash loki" on_release: root.hulk_smash()
class Marvel(BoxLayout): def hulk_smash(self): self.ids.hulk.text = "hulk: puny god!" self.ids["loki"].text = "loki: >_<!!!" # 等价的下标语法

由于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:

使用动态类可以消除全部重复。只需一行规则新类名@基类名:,即可在 Kv 侧动态创建继承自基类的新类:

<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 侧新增任何代码。在实现上,解析器收集动态类定义后,Builder会通过Factory.register()将新类注册进控件工厂(见 kivy/lang/builder.py)。

多类共享样式:逗号分隔的类规则

当多个类需要同一份 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

可以合并为:

<MyFirstWidget,MySecondWidget>: Button: on_press: root.text(txt_inpt.text) TextInput: id: txt_inpt

只要在声明中用逗号隔开类名,所有列出的类都会获得相同的 Kv 属性。解析器通过正则, *切分类名列表来处理这种声明(见 kivy/lang/parser.py)。

完整实战:用 Kv 语言实现「视图与逻辑分离」

Kivy 语言的目标之一就是分离「表现层」与「逻辑层」:布局由.kv文件负责,逻辑由.py文件负责。仓库中的 examples/guide/designwithkv/main.py 与 examples/guide/designwithkv/controller.kv 提供了一个可直接运行的完整范例。

逻辑放在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()

Controller类定义了两个属性:info(接收文本)与label_wid(接收 Label 控件);同时定义do_action()方法,它会同时修改info文本与label_wid控件中的文本。

布局放在controller.kv

没有对应.kv文件时程序也能运行,但屏幕上什么都看不到——因为Controller只是FloatLayout,自身没有任何子控件。为它创建controller.kv后,ControllerApp运行时就会自动加载它:

#: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'

这个示例包含了三个关键机制:

  1. 从 Controller 读取数据text: 'My controller info is: ' + root.infoinfo属性一旦改变,表达式自动重新求值,Button 文本随之更新。
  2. 向 Controller 注入控件id: my_custom_label给 Label 命名,label_wid: my_custom_label把该 Label 实例交给Controllerlabel_wid属性。
  3. 创建自定义回调on_press: root.do_action()中,rootself是可随处使用的保留关键字——root代表规则内的顶层控件,self代表当前控件。规则内声明的任意 id 也可以像root/self一样直接使用,例如:
Button: on_press: root.do_action(); my_custom_label.font_size = 18

运行main.py后,controller.kv会被加载,Button 与 Label 随即显示并响应触摸事件。这正是「一个标签 + 一个按钮」背后蕴含的完整数据流:属性绑定自动刷新、id 注入实现跨层通信、事件回调驱动逻辑。

小结与延伸阅读

Kv 语言的完整要素可以概括为一张速查表:

要素语法说明
根规则Widget:无缩进的类名 + 冒号,成为 App 的root
类规则<MyWidget>:定义某类所有实例的外观与行为
动态类<MyBtn@Button>:在 Kv 侧动态创建基类的子类
多类共享<A,B>:逗号分隔,多类共享同一份样式
属性绑定cols: 3/text: root.info值作为 Python 表达式求值,依赖属性自动观察
事件绑定on_press:/on_text:args引用事件参数,on_前缀自动生成
特殊关键字app/root/self全局应用实例 / 规则根控件 / 当前控件
导入与全局值#:import/#:set访问 Python 模块、设置全局常量
引用控件id:+ObjectProperty/self.ids注意 id 是 weakref,需用__self__保活
Canvascanvas:/canvas.before/canvas.after声明式绘制指令,属性变化自动更新
度量单位dp/sp/pt/inch/cm/mm/rgba解析器内置,可直接在表达式中使用

至此,你已经完成了 Kivy Kv 语言从最小语法到完整实战的入门。若想深入了解语言各组成部分的完整描述、高级用法与限制,可以参考 Kivy 完整语言指南 doc/sources/guide/lang.rst,以及语言模块源码 kivy/lang/init.py、解析器 kivy/lang/parser.py 和规则构建器 kivy/lang/builder.py。此外,examples/guide/designwithkv/ 中的示例可以直接运行体验,examples/kv/ 目录下还有大量展示各种控件与 canvas 用法的.kv示例文件可供参考。

【免费下载链接】kivyOpen source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS项目地址: https://gitcode.com/gh_mirrors/ki/kivy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 1:50:51

LibreChat:Agent时代的基础设施工具链

1. LibreChat不是另一个ChatGPT前端&#xff0c;而是Agent时代的基础设施探针 LibreChat这个名字&#xff0c;第一眼容易让人误以为是又一个开源版ChatGPT界面——毕竟GitHub上叫“XXXChat”的项目数以百计。但如果你真把它当成UI套壳去跑&#xff0c;十有八九会在第三步卡住&…

作者头像 李华
网站建设 2026/9/21 1:50:46

双4090本地部署Qwen3.6-27B:FP8量化与vLLM多卡推理实战

1. 为什么我选择在两张 4090 上折腾 Qwen3.6-27B先把结论摆在前面&#xff1a;Qwen3.6-27B 这个体量的模型&#xff0c;放在两张 4090 上跑本地推理&#xff0c;是当前消费级硬件里性价比相当高的一套组合&#xff0c;但它绝对不是"插上就能用"的那种省心方案。我从早…

作者头像 李华
网站建设 2026/9/21 1:46:25

Python多模态情感识别:EEG/眼动/GSR融合与CLIP对比学习实战

简介&#xff1a;一套基于Python的多模态情感识别项目源码&#xff0c;融合脑电&#xff08;EEG&#xff09;、眼动追踪与皮肤电&#xff08;GSR&#xff09;生理信号&#xff0c;面向计算机/电子信息类毕业设计、情感计算研究者及人机交互开发者。整套源码覆盖信号预处理、特征…

作者头像 李华