Kivy中文編程指南:KV 語言
這一章翻譯的一般,對不住大家了,希望大家多多提出修改建議。
英文原文
語言背後的概念
隨著你的應用程序越寫越複雜,就往往會發現控制項樹的結構/各種綁定的聲明等等,都越來越繁瑣複雜了,維護起來也很費力氣。KV 語言就是為了解決這個問題而設計出來的。
(譯者註:這種情況在 GUI 界面的 APP 開發中很常見,比如在 Android 開發的過程中,就用到了 xml 來定義界面元素的關係等等。)
KV 語言(英文縮寫也叫 kvlang 或者 kivy 語言),可以讓開發者用描述的方式來創建控制項樹,以及綁定控制項對應的屬性,以實現一種自然地調用。這一設計可以允許用戶能夠快速建立應用雛形,然後對界面進行靈活調整。此外,這樣的設計還使得運行邏輯與用戶界面相互分離不干擾。
載入 KV 的方法
通過以下兩種方法都可以在你的應用程序中載入 KV 代碼:
通過同名文件查找: Kivy 會找跟 App 類同名的小寫字母的 Kv 擴展名的文件,如果你的應用類尾部有 App 字樣,查找的時候會找去掉這個 App 字樣的文件,例如: MyApp -> my.kv 如果這個文件定義了一個根控制項,這個文件就會被添加到應用的根屬性中去,然後用作整個程序的控制項樹基礎。
Builder: 也可以直接指定讓 Kivy 去載入某個字元串或者文件。如果這個字元串或者文件定義了一個根控制項,就會被下面這個方法返回:Builder.load_file(path/to/file.kv)或者Builder.load_string(kv_string)
規則語義
Kv 源文件包含有各種規則,這些規則是用來描述控制項的環境設定的,可以有一個根規則,然後其他的各種類的或者模板的規則就都不限制數量來。
根規則是用來描述你的根控制項類的,不能有任何縮進,跟著一個英文冒號:,在應用程序的實例當中這就會被設置成根屬性:
Widget:
類規則,聲明方式為將控制項類的名字用尖括弧括起來的,然後跟著一個英文冒號:,這類規則用來定義這個類的實例圖形化呈現的方式:
<MyWidget>:
Kv 文件中各種規則都用縮進來進行區塊劃分,就像 Python 裡面一樣,這些縮進得是四個空格作為一層縮進,就跟 Python 裡面推薦的做法是一樣的。
以下是三個 Kv 語言的關鍵詞:
app: 指向你應用程序的實例。n nroot: 指向當前規則中的基礎控制項或者基礎模板。n nself: 指向當前的控制項。n
特殊語法
有兩種特殊的語法,能定義整個 Kv 環境下的各種值:
讀取 Kv 中的 Python 模塊和各種類:
#:import name x.y.zn#:import isdir os.path.isdirn#:import np numpyn
等價於 Python 中的:
from x.y import z as namenfrom os.path import isdirnimport numpy as npn
設置各種全局變數:
#:set name valuen
等價於 Python 中的:
name = valuen
子對象實例化
To declare the widget has a child widget, instance of some class, just declare this child inside the rule:
給一個控制項聲明子控制項,比如某個類的實例,只要在規則內部聲明一下這個子對象就可以類:
MyRootWidget:n BoxLayout:n Button:n Button:n
上面的樣例代碼定義了根控制項,是一個 MyRootWidget 的實例,它有一個子控制項,是一個 BoxLayout 實例。這個 BoxLayout 還有自己的兩個子對向,是兩個 Button 類的實例。
與上面代碼等價的 Python 代碼大致如下:
root = MyRootWidget()nbox = BoxLayout()nbox.add_widget(Button())nbox.add_widget(Button())nroot.add_widget(box)n
你可能會發現直接用 Python 來實現的代碼不那麼好閱讀,寫起來也不那麼簡便。
用 Python 可以在創建控制項的時候傳遞關鍵詞參數過去,來指定這些控制項的行為。例如下面的這個代碼就是設定一個 gridlayout 中的欄目數:
grid = GridLayout(cols=3)n
同樣目的也可以用 Kv 來實現,可以直接在規則內指定好子控制項的屬性:
GridLayout:n cols: 3n
這個值會作為一個 Python 表達式來進行計算,然後所有在表達式中用到的屬性都是可見的,就好比下面的 Python 代碼一樣(這裡假設 self 是一個有 ListProperty 數據的控制項):
grid = GridLayout(cols=len(self.data))nself.bind(data=grid.setter(cols))n
如果想要在數據修改的時候就更新顯示,可以用如下方法實現:
GridLayout:n cols: len(root.data)n
特別注意
Widget names should start with upper case letters while property names should start with lower case ones. Following the PEP8 Naming Conventions is encouraged.
控制項的名字一定要用大寫字母打頭,而屬性的名字一定要用小寫的。推薦遵循PEP8 命名慣例。
事件綁定
在 Kv 中,使用英文冒號 ":" 就可以來進行事件綁定,也就是將某個回調和一個事件聯繫起來:
Widget:n on_size: my_callback()n
使用 args 關鍵詞的信號,就能把分派來的值傳遞過去類:
TextInput:n on_text: app.search(args[1])n
還可以用更加複雜的表達式,例如:
pos: self.center_x - self.texture_size[0] / 2., self.center_y - self.texture_size[1] / 2.n
上面這段表達式中,監聽了center_x, center_y, texture_size這三個屬性的變化。只要其中有一個發生了變化,表達式就會重新計算來更新 pos 的區域。
你還看以在 kv 語言中處理 on_ 事件。例如 TextInput 這個類就有一個 focus 屬性,這個數行自動生成的 on_focus 事件可在 kv 語言內進行讀取:
TextInput:n on_focus: print(args)n
擴展畫布
Kv 語言也已用來定義你控制項的畫布,如下所示:
MyWidget:n canvas:n Color:n rgba: 1, .3, .8, .5n Line:n points: zip(self.data.x, self.data.y)n
當屬性發生變化的時候,這些畫布就會更新。當然也可以用 canvas.before 和 canvas.after。
定位控制項
在一個控制項樹當中,經常會需要去讀取或者定位其他的控制項。 Kv 語言提供了一種快速的方法來實現這一目的,就是使用 id。(譯者註:在 Android 的開發中就是這樣的。) 可以把這些控制項當作是類這以層次的變數,只能在 Kv 語言中使用。例如下面的:
<MyFirstWidget>:n Button:n id: f_butn TextInput:n text: f_but.statenn<MySecondWidget>:n Button:n id: s_butn TextInput:n text: s_but.staten
An id is limited in scope to the rule it is declared in, so in the code above s_but can not be accessed outside the <MySecondWidget> rule.
id 只能再所處的規則內使用,也就是聲明它的位置,所以在上面的代碼中,s_but 就不能在 <MySecondWidget> 規則外被讀取到。
特別注意
給一個 id 賦值的時候,一定要記住這個值不能是字元串。所以不能有引號:這是正確的 -> id: value, 這樣就不對 -> id: value id 是對空間的一個 weakref (弱引用),而不是控制項本身。所以,在垃圾回收的時候要保存控制項,就不能僅僅保存 id。
下面的代碼中:
<MyWidget>:n label_widget: label_widgetn Button:n text: Add Buttonn on_press: root.add_widget(label_widget)n Button:n text: Remove Buttonn on_press: root.remove_widget(label_widget)n Label:n id: label_widgetn text: widgetn
雖然 MyWidget 中已經存儲了一個對 label_widget 的引用,但是這個只是一個弱引用,其他引用被移除的時候還不足以保證對象依然可用。因此,在移除按鈕被點擊(這時候也就是移除類所有對這個控制項的引用)之後,或者窗口大小被調整了(這會調用垃圾回收器,導致刪掉 label_widget),這時候如果點擊添加按鈕來重新把控制項增加回來的話,就會有一個引用錯誤(ReferenceError)被拋出來:因為弱引用的對象已經不存在類。
<MyWidget>:n label_widget: label_widget.__self__n
Python 代碼讀取在 Kv 中定義的控制項
假如在 my.kv 文件中有如下的代碼:
<MyFirstWidget>::n # both these variables can be the same name and this doesnt lead ton # an issue with uniqueness as the id is only accessible in kv.n txt_inpt: txt_inptn Button:n id: f_butn TextInput:n id: txt_inptn text: f_but.staten on_text: root.check_status(f_but)n
在 myapp.py 這個文件中:
...nclass MyFirstWidget(BoxLayout):nn txt_inpt = ObjectProperty(None)nn def check_status(self, btn):n print(button state is: {state}.format(state=btn.state))n print(text input text is: {txt}.format(txt=self.txt_inpt))n...n
txt_inpt 是一個ObjectProperty對象,在類內被初始化為 None。
txt_inpt = ObjectProperty(None)n
目前位置,這個 self.txt_inpt 還是 None。在 Kv 語言中,這個屬性會進行更新,保存 txt_input 這個 id 所引用的 TextInput 實例:
txt_inpt: txt_inptn
從這以後,self.txt_inpt 久保存了一個到控制項的引用,通過 txt_input 這個 id 來識別,可以在類內的各個地方來使用,就跟在 check_status 函數里一樣。當然也可以不這麼做,可以把 id 傳給需要用到它的函數,例如上面代碼中那個 f_but 這個例子。
通過 id 標籤,在 kv 語言中可以查找對象,這是一種更簡單的讀取對象的方法。比如下面這段代碼:
<Marvel>n Label:n id: lokin text: loki: I AM YOUR GOD!n Button:n id: hulkn text: "press to smash loki"n on_release: root.hulk_smash()n
在你的 Python 代碼中:
class Marvel(BoxLayout):n def hulk_smash(self):n self.ids.hulk.text = "hulk: puny god!"n self.ids["loki"].text = "loki: >_ # alternative syntaxn
當你的 kv 文件被解析的時候,kivy 會選中所有帶有標籤 id 的控制項,然後把它們放到 self.ids 這樣一個辭典類型的屬性裡面去。所以你就可以對這些控制項進行遍歷,然後像是辭典數據一樣來進行讀取類:
for key, val in self.ids.items():n print("key={0}, val={1}".format(key, val))n
特別注意
雖然這種 self.ids 方法非常簡便,但通常最推薦的還是用對象屬性。這樣會創建一個直接的引用,能提供更快地讀取速度,並且也更加準確可靠。
動態類型
參考下面的代碼:
<MyWidget>:n Button:n text: "Hello world, watch this text wrap inside the button"n text_size: self.sizen font_size: 25spn markup: Truen Button:n text: "Even absolute is relative to itself"n text_size: self.sizen font_size: 25spn markup: Truen Button:n text: "Repeating the same thing over and over in a comp = fail"n text_size: self.sizen font_size: 25spn markup: Truen Button:n
如果這裡使用一個模板,就不用那麼麻煩地去重複對每一個按鈕進行設置了,例如下面這樣就可以了:
<MyBigButt@Button>:n text_size: self.sizen font_size: 25spn markup: Truenn<MyWidget>:n MyBigButt:n text: "Hello world, watch this text wrap inside the button"n MyBigButt:n text: "Even absolute is relative to itself"n MyBigButt:n text: "repeating the same thing over and over in a comp = fail"n MyBigButt:n
上面這個類是在這個規則的聲明內建立的,繼承了按鈕類 Button,然後我們就可以用這個類來對默認值進行修改,並且建立所有實例的鏈接綁定,而不用在 Python 弄一大堆新代碼了。
在多個控制項中復用樣式
參考下面的代碼,在 my.kv 文件中:
<MyFirstWidget>:n Button:n on_press: root.text(txt_inpt.text)n TextInput:n id: txt_inptnn<MySecondWidget>:n Button:n on_press: root.text(txt_inpt.text)n TextInput:n id: txt_inptn
在 myapp.py 這個文件中:
class MyFirstWidget(BoxLayout):n def text(self, val):n print(text input text is: {txt}.format(txt=val))nnclass MySecondWidget(BoxLayout):n writing = StringProperty()n def text(self, val):n self.writing = valn
好多類都要用到同樣的 .kv 樣式文件,這樣就可以通過讓所有控制項都對樣式進行復用,就能簡化一下設計。這就可以在 kv文件中來實現。例如下面這個就是 my.kv 文件中的代碼:
<MyFirstWidget,MySecondWidget>:n Button:n on_press: self.text(txt_inpt.text)n TextInput:n id: txt_inptn
只要把各個類的名字用英文冒號:分隔開,聲明當中包含的類就都會使用相同的 kv 屬性了。
使用 Kivy 語言來設計
Kivy 語言的設計目標之一就是希望能夠做好分工,把界面和內部邏輯相互分離。輸出的樣式由你的 kv 文件來確定,運行邏輯靠你的 python 代碼來執行。
Python 文件中的代碼
來一個簡單的小例子。首先要有一個名字為 main.py 的 Python 源文件:
import kivynkivy.require(1.0.5)nnfrom kivy.uix.floatlayout import FloatLayoutnfrom kivy.app import Appnfrom kivy.properties import ObjectProperty, StringPropertynnclass Controller(FloatLayout):nn創建一個控制器,從 kv 文件中接收一個定製的控制項。n增加一個由 kv 文件進行調用的動作。nn label_wid = ObjectProperty()n info = StringProperty()n def do_action(self):n self.label_wid.text = My label after button pressn self.info = New info textnnclass ControllerApp(App):n def build(self):n return Controller(info=Hello world)nnif __name__ == __main__:n ControllerApp().run()n
剛剛這個代碼樣例中,我們創建了一個有兩個屬性的控制器:
- info 該屬性用於接收文本
- label_wid 該屬性用於接收文本標籤控制項
此外還創建了一個 do_action() 方法,這個方法會使用上面的屬性。這個方法會改變 info裡面的文本,以及 label_wid 控制項中的文本。
controller.kv 文件中的布局樣式
沒有對應的 kv 文件,應用程序也能運行,只不過是屏幕上不會有任何顯示輸出。這個是符合情理的,因為畢竟控制器 Controller 這個類是沒有任何控制項的,就只是一個FloatLayout(浮動布局)咱們可以創建一個名字為 controller.kv 的文件,來圍繞著這個控制器類 Controller 搭建 UI (用戶界面),這個文件會在運行 ControllerApp 的時候被載入。這個具體怎麼實現的,以及載入類哪些文件,可以參考 kivy.app.App.load_kv() 方法里的描述。
#:kivy 1.0n<Controller>:n label_wid: my_custom_labelnn BoxLayout:n orientation: verticaln padding: 20nn Button:n text: My controller info is: + root.infon on_press: root.do_action()nn Label:n id: my_custom_labeln text: My label before button pressn
上面的代碼中就是一個豎直的箱式布局(BoxLayout)。看著就挺簡單的。這個代碼主要功能有以下三個:
1 使用來自控制器類 Controller 的數據。只要 controller 中的 info 屬性發生了改變,表達式 My controller info is: + http://root.info 就會自動重新計算,改變按鈕(Button)上面的值。
2 傳遞數據給控制器類 Controller。my_custom_label 這個 id 會賦值給 id 為 my_custom_label 的新建文本標籤(Label)。此後,使用 label_wid:my_custom_label 中的 my_custom_label 就能得到一個控制器中的 Label 控制項實例了。
3 在按鈕 Button 中使用控制器類 Controller 的 on_press 方法來創建一個自定義回調。 root 和 self 都是保留關鍵詞,任何地方都不可見的。root 代表的是規則中的頂層控制項,self 表示的是當前控制項。 你可以在當前規則中使用任意的已經聲明過的 id, root 和 self 也可以這樣來用。比如下面就是在 on_press() 裡面使用 root:
Button:n on_press: root.do_action(); my_custom_label.font_size = 18n
就這麼多了。現在當我們再次運行 main.py的時候,controller.kv 就會被載入,然後 Button 按鈕和 Label 文本標籤就會出現,並且根據觸摸事件進行響應了。
推薦閱讀: