### 編程向導: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個事情將被做:
1. 從Controller使用數據。一旦在controller中info屬性被改變,表達式text:'My Controller info is:' + root.info將會自動更新。
2. 傳遞數據到Controller。表達式id:my_custom_label被賦值給id為my_custom_label的標簽。于是,在表達式label_wid:my_custom_label中使用my_custom_label傳遞部件Label的實例到你的Controller。
3. 使用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將會被自動加載,按鈕和標簽也將顯示并響應你的觸摸事件。
### 下節預告:編程向導4.10集成其他框架