編程向?qū)В?.9KV語言
一养盗、語言背后的思想
當(dāng)你的應(yīng)用程序變得更復(fù)雜時(shí),構(gòu)建部件樹和明確的聲明綁定將變得冗長和難以維護(hù)适篙。KV語言試圖克服這些缺點(diǎn)往核。
KV語言(有時(shí)被叫kvlang,或kivy語言)嚷节,允許你以聲明的方式來創(chuàng)建你的部件樹聂儒,并以一種自然的方式綁定部件屬性或回調(diào)函數(shù)。針對(duì)UI硫痰,它支持快速原型和敏捷改動(dòng)衩婚。它也使得邏輯和用戶接口能更好的分離。
二效斑、如何加載KV
有兩種方式來加載KV代碼:
-
通過名字約定
Kivy查找你的應(yīng)用程序類的小寫的同名KV文件非春,如果它以'App'結(jié)尾則去掉它,例如:MyApp -> my.kv
如果這個(gè)文件定義了一個(gè)根部件鳍悠,它將會(huì)附著到應(yīng)用程序的根特征值税娜,并用它作為應(yīng)用程序部件樹的根。
-
Builder
你可以告訴Kivy直接加載一個(gè)字符串或一個(gè)文件藏研。如果這個(gè)字符串或文件定義了根部件敬矩,它將被返回。Builder.load_file('path/to/file.kv')
或者
Builder.load_string('kv_string')
三蠢挡、管理上下文
一個(gè)KV源構(gòu)成的規(guī)則弧岳,用來描述部件的內(nèi)容。你可以有一個(gè)根規(guī)則和任何數(shù)量的類或模板規(guī)則业踏。
根規(guī)則通過聲明你的根部件類來聲明禽炬,不需要任何縮進(jìn),后面跟著冒號(hào)(:)勤家,并且被設(shè)置為應(yīng)用程序?qū)嵗母卣髦怠?/p>
Widget:
一個(gè)類規(guī)則腹尖,有一對(duì)尖括號(hào)(<>)包括部件類名組成,后面跟冒號(hào)(:)伐脖,定義類的實(shí)例如何被生動(dòng)地表達(dá):
<MyWidget>:
和Python一樣热幔,規(guī)則使用縮進(jìn)進(jìn)行界定乐设,和良好的Python習(xí)慣一樣,縮進(jìn)的每一級(jí)別最好是4個(gè)空格绎巨。
有三個(gè)關(guān)鍵字來指定KV語言:
- app:總是引用你的應(yīng)用程序的實(shí)例近尚。
- root:引用當(dāng)前規(guī)則中的根部件/模板。
- self:引用當(dāng)前部件场勤。
四戈锻、特殊的語法
有兩個(gè)特殊語法來為整個(gè)KV上下文定義值:
- 為了從KV中訪問Python的模塊和類:
#:import name x.y.z
#:import isdir os.path.isdir
#:import np numpy
上面的代碼等價(jià)于:
from x.y import z as name
from os.path import isdir
import numpy as np
- 為了設(shè)置一個(gè)全部變量:
#:set name value
等價(jià)于:
name = value
五、實(shí)例化子部件
為了聲明部件的子部件和媳,僅在規(guī)則里面聲明這些子部件即可:
MyRootWidget:
BoxLayout:
Button:
Button:
上面的例子定義了一個(gè)MyRootWidget的實(shí)例作為我們的根部件格遭,它有一個(gè)子部件是BoxLayout的實(shí)例。BoxLayout進(jìn)一步有兩個(gè)Button類的子部件窗价。在Python代碼中應(yīng)該是這樣:
root = MyRootWidget()
box = BoxLayout()
box.add_widget(Button())
box.add_widget(Button())
root.add_widget(box)
你會(huì)發(fā)現(xiàn)在KV中如庭,僅用很少的代碼,易寫并易讀撼港。
當(dāng)然,在Python中骤竹,你可以傳遞關(guān)鍵字參數(shù)到你的部件中帝牡。例如,設(shè)置一個(gè)GridLayout的列的數(shù)目蒙揣,我們可以這樣寫:
grid = GridLayout(cols = 3)
在KV中靶溜,你可以直接在規(guī)則中設(shè)置子部件的屬性:
GridLayout:
cols:3
這個(gè)值被評(píng)估為一個(gè)Python表達(dá)式,并且表達(dá)式中所有的屬性值都將被監(jiān)聽懒震。例如在Python中:
grid = GridLayout(cols = len(self.data))
self.bind(data = grid.setter('cols'))
當(dāng)你的數(shù)據(jù)變化時(shí)罩息,顯示跟著更新,在KV中只需這樣:
GridLayout:
cols:len(root.data)
注意个扰,當(dāng)屬性名以小寫字母開頭時(shí)瓷炮,部件名首字母應(yīng)當(dāng)大寫。遵循PEP8 Naming Conventions是被鼓勵(lì)的递宅。
六娘香、事件綁定
在KV語言中,你可以使用":"語法來綁定事件:
Widget:
on_size: my_callback()
你也可以使用args關(guān)鍵字傳遞參數(shù):
TextInput:
on_text:app.search(args[1])
更復(fù)雜的表達(dá)式可能類似這樣:
pos:self.center_x - self.texture_size[0] / 2, self.center_y - self.texture_size[1] / 2
這個(gè)表達(dá)式監(jiān)聽center_x, center_y, texture_size的變動(dòng)办龄。如果其中一個(gè)發(fā)生了改變烘绽,表達(dá)式將會(huì)更新pos字段。
你也可以在KV語言中處理on_事件俐填。例如輸入框有一個(gè)聚焦(focus)屬性,它將自動(dòng)生成on_focus事件:
TextInput:
on_focus:print(args)
七安接、擴(kuò)展畫布
KV語言可以這樣來定義你的畫布指令:
MyWidget:
canvas:
Color:
rgba: 1, .3, .8, .5
Line:
points: zip(self.data.x, self.data.y)
當(dāng)屬性值改變時(shí)它們將更新,當(dāng)然英融,你也可以使用canvas.before和canvas.after.
八盏檐、引用部件
在一個(gè)部件樹中呀打,經(jīng)常需要訪問/引用其他的部件。KV語言提供了一個(gè)使用id's的方法來做這些工作糯笙。將它們認(rèn)為是只能用于Kv語言類級(jí)別變量贬丛。看下面代碼:
<MyFirstWidget>:
Button:
id: f_but
TextInput:
text: f_but.state
<MySecondWidget>:
Button:
id: s_but
TextInput:
text: s_but.state
一個(gè)id被限制到它被聲明的作用域內(nèi)给涕,所以在<MySecondWidget>外面s_but不能被訪問豺憔。
id是一個(gè)部件的弱引用(weakref)并且不是部件本身。因此够庙,存儲(chǔ)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'
上面的代碼中,雖然一個(gè)到label_widget的引用被存儲(chǔ)到MyWidget中耘眨,但是因?yàn)樗鼉H僅是一個(gè)弱引用昼榛,一旦別的引用被移除,它不足以保持對(duì)象存活剔难。因此胆屿,當(dāng)移除按鈕被點(diǎn)擊后(將移除其他的引用)窗口將重新計(jì)算尺寸(調(diào)用垃圾回收導(dǎo)致重新檢測(cè)label_widget),當(dāng)點(diǎn)擊添加按鈕來添加部件偶宫,一個(gè)引用錯(cuò)誤將發(fā)生(ReferenceError:weakly-referenced object no longer exists)
為了保持部件存活非迹,一個(gè)對(duì)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)
這是效果導(dǎo)致self.txt_inpt是None吵冒。在KV語言中纯命,這個(gè)屬性更新被id:txt_inpt引用的持有TextInput的實(shí)例。
txt_inpt:txt_inpt
從這點(diǎn)向上痹栖,self.txt_inpt持有一個(gè)被id txt_input標(biāo)識(shí)的部件的引用并且能被用在類的任何地方亿汞,正如在check_status函數(shù)中一樣。對(duì)照這個(gè)函數(shù)结耀,你僅僅需要傳遞id到你想用的地方留夜。
你可以使用ids來訪問帶id標(biāo)識(shí)的對(duì)象,這是一種更簡單的方法:
<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
當(dāng)你的kv文件被解析時(shí)图甜,kivy收集所有的帶id標(biāo)簽的部件碍粥,并放置它們到self.ids字典中。這意味著你能以字典的風(fēng)格來迭代這些部件并訪問它們黑毅。
for key, val in self.ids.items():
print("key={0}, val={1}".format(key, val))
注意嚼摩,雖然self.ids很簡潔,它被認(rèn)為是使用ObjectProperty的最佳實(shí)踐。但是創(chuàng)建一個(gè)字典的引用枕面,將會(huì)提供更快的訪問速度并更加清晰愿卒。
十、動(dòng)態(tài)類
考慮下面代碼:
<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:
為了替代重復(fù)的代碼潮秘,我們可以使用模板來代替:
<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:
這個(gè)被規(guī)則聲明的類繼承自按鈕類琼开。它允許我們改變默認(rèn)值,并為每一個(gè)實(shí)例創(chuàng)建綁定而不用在Python那邊添加任何新的代碼枕荞。
十一柜候、在多個(gè)部件中重用樣式
看下面的在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
因?yàn)閮蓚€(gè)類共同使用相同的.kv風(fēng)格。如果我們?yōu)閮蓚€(gè)部件重用風(fēng)格躏精,這將使得設(shè)計(jì)簡化渣刷。你可以在my.kv中這樣寫代碼:
<MyFirstWidget,MySecondWidget>:
Button:
on_press: self.text(txt_inpt.text)
TextInput:
id: txt_inpt
用一個(gè)逗號(hào)(,)來分離類名,所有的類將都有同樣的kv屬性矗烛。
十二辅柴、使用KV語言設(shè)計(jì)
使用Kivy語言的一個(gè)目標(biāo)就是分離邏輯和表現(xiàn)。表現(xiàn)層使用kv文件來表示瞭吃,邏輯使用py文件來表示碌嘀。
(一)py文件中寫代碼
讓我們開始一個(gè)小例子,首先虱而,在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()
在這個(gè)例子中筏餐,我們創(chuàng)建了一個(gè)帶有兩個(gè)屬性的控制類:
- info:接收一些文本
- label_wid接收標(biāo)簽(label)部件
另外,我們創(chuàng)建了一個(gè)do_action()方法來使用這些屬性牡拇。它將會(huì)改變info文本和label_wid部件的文本。
(二)在controller.kv中布局
執(zhí)行一個(gè)沒有相應(yīng)的.kv文件的應(yīng)用程序可以運(yùn)行穆律,但是沒有任何東西被顯示到屏幕上惠呼。這是被期望的,因?yàn)榭刂祁悰]有部件在里面峦耘,它僅僅是一個(gè)FloatLayout剔蹋。我們能圍繞Controller類在一個(gè)controller.kv文件中創(chuàng)建UI,當(dāng)我們運(yùn)行ControllerApp時(shí)它會(huì)被加載辅髓。這將如何實(shí)現(xiàn)及什么文件被加載都在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中,有一個(gè)標(biāo)簽和一個(gè)按鈕洛口〗酶叮看起來很簡單,有3個(gè)事情將被做:
從Controller使用數(shù)據(jù)第焰。一旦在controller中info屬性被改變买优,表達(dá)式text:'My Controller info is:' + root.info將會(huì)自動(dòng)更新。
傳遞數(shù)據(jù)到Controller。表達(dá)式id:my_custom_label被賦值給id為my_custom_label的標(biāo)簽杀赢。于是烘跺,在表達(dá)式label_wid:my_custom_label中使用my_custom_label傳遞部件Label的實(shí)例到你的Controller。
-
使用Controller的on_press方法創(chuàng)建一個(gè)定制的回調(diào)函數(shù)脂崔。
root和self被保留為關(guān)鍵字滤淳,可用在任何地方。root代表規(guī)則內(nèi)的根部件砌左,self代表當(dāng)前部件脖咐。
-
在規(guī)則內(nèi)你可以使用任何id聲明,同root和self一樣绊困。例如文搂,你可以在on_press()中這樣:
Button:
on_press:root.do_action();my_custom_label.font_size = 18
現(xiàn)在,我們運(yùn)行main.py, controller.kv將會(huì)被自動(dòng)加載秤朗,按鈕和標(biāo)簽也將顯示并響應(yīng)你的觸摸事件煤蹭。