Kivy中文編程指南:控制項
英文原文
控制項簡介
控制項Widget是 Kivy 圖形界面中的基本元素。控制項提供了一個畫布Canvas,這是用來在屏幕上進行繪製的。控制項接收事件,並且對事件作出反應。想要對 控制項Widget進行更深入的了解,可以去看看這個模塊的文檔。
操作控制項樹
Kivy 以樹的形式來組織控制項。你的應用程序會有一個根控制項,通常會含有若干的子控制項 children,這些子控制項還可以有自己的子控制項。一個控制項的子控制項會以 children屬性的形式表述,這個屬性是 Kivy 中的一個列表屬性 ListProperty
可以用一下方法來操作控制項樹:
- add_widget(): 添加一個控制項作為子控制項;
- remove_widget(): 從子控制項列表中去掉一個控制項;
- clear_widgets(): 清空一個控制項的所有子控制項。
例如下面的代碼,就是在一個盒式布局 BoxLayout 中添加一個按鈕:
layout = BoxLayout(padding=10)nbutton = Button(text=My first button)nlayout.add_widget(button)n
這個按鈕就添加到布局當中去了:按鈕的 parent 屬性會被設置為這個布局;這個按鈕也會被添加到布局中的子控制項列表。要把這個按鈕從這個布局中刪掉也很簡單:
layout.remove_widget(button)n
移除了之後,這個按鈕的 parent 屬性就會被設置為 None,也會被從布局的子控制項列表中移除。
要是想清空一個控制項中的所有自科技,那就用 clear_widgets()方法就可以了:
layout.clear_widgets()n
特別注意
千萬別自己手動去操作子控制項列表,除非你確定自己掌控得非常深入透徹。因為控制項樹是和繪圖樹聯繫在一起的。例如,如果你添加了一個控制項到子控制項列表,但沒有添加這個新子控制項的畫布到繪圖樹上,那麼就會出現這種情況:這個控制項確實成了一個子控制項,但是屏幕上不會顯示出來。此外,如果你後續使用添加、移除、清空控制項這些操作,可能還會遇到問題。
遍歷控制項樹
控制項類實例的子控制項children列表屬性中包含了所有的子控制項。所以可以用如下的方式來進行遍歷:
root = BoxLayout()n# ... add widgets to root ...nfor child in root.children:n print(child)n
然而,這樣的操作可得謹慎使用。如果你要用之前一節中提到的方法來修改這個子控制項列表電話,請一定用下面這種方法來做一下備份:
for child in root.children[:]:n # manipulate the tree. For example here, remove all widgets that have an # widthn if child.width 100:n root.remove_widget(child)n
默認情況下,控制項是不會對子控制項的尺寸/位置進行改變的。位置屬性 pos是屏幕坐標繫上的絕對值(除非你使用相對布局relativelayout,這個以後再說),而尺寸屬性 size就是一個絕對的尺寸大小。
控制項索引Z
控制項繪製的順序,是基於各個控制項在控制項樹中的位置。添加控制項方法 add_widget 可以接收一個索引參數,這樣就能指定該新增控制項在控制項樹中的位置。
root.add_widget(widget, index)n
索引值小的控制項會被繪製在索引值大的控制項之上。一定要記住,默認值是 0 ,所以後添加的控制項總會在所有控制項的最頂層,除非指定了索引值。
整理布局
布局 layout是一種特別的控制項,它會控制自己子控制項的尺寸和位置。有各種不同的布局,這些布局分別為子控制項提供拜託你個的自動組織整理。這些布局使用尺寸推測 size_hint和位置推測 pos_hint這兩個屬性來決定子控制項children的尺寸 size 和 位置pos。
盒式布局 BoxLayout: 所有控制項充滿整個空間,以互相挨著的方塊的方式來分布,橫著或者豎著排列都可以。子控制項的 size_hint 屬性可以用來改變每個子控制項的比例,也可以設置為固定尺寸。
網格布局 GridLayout: 以一張網格的方式來安排控制項。你必須指定好網格的維度,確定好分成多少格,這樣 Kivy 才能計算出每個元素的尺寸並且確定如何安排這些元素的位置。
棧狀布局 StackLayout: 挨著放一個個控制項,彼此鄰近,在某一個維度上有固定大小,而使它們填充整個空間。 這適合用來顯示相同預定義大小的子控制項。
錨式布局 AnchorLayout: 一種非常簡單的布局,只關注子控制項的位置。 將子控制項放在布局的邊界位置。 不支持size_hint。
浮動布局 FloatLayout: 允許放置具任意位置和尺寸的子控制項,可以是絕對尺寸,也可以是相對布局的相對尺寸。 默認的 size_hint(1,1)會讓每個子控制項都與整個布局一樣大,所以如果你多個子控制項就要修改這個值。可以把 size_hint 設置成 (None, None),這樣就可以使用 size 這個絕對尺寸屬性。控制項也支持 pos_hint,這個屬性是一個 dict 詞典,用來設置相對布局的位置。
相對布局 RelativeLayout: 和浮動布局 FloatLayout 差不多,不同之處在於子控制項的位置是相對於布局空間的,而不是相對於屏幕。
想要深入理解各種布局的話,可以仔細閱讀各種文檔。
size_hint 和 pos_hint:
- floatlayout
- boxlayout
- gridlayout
- stacklayout
- relativelayout
- anchorlayout
size_hint 是一個 引用列表屬性 ReferenceListProperty ,包括 size_hint_x 和size_hint_y 兩個變數。接收的變數值是從0到1的各種數值,或者 None, 默認值為 (1, 1)。這表示如果控制項處在布局之內,布局將會在兩個方向分配全部尺寸(相對於布局大小)給該控制項。
舉個例子,設置size_hint 為 (0.5, 0.8),就會給該控制項Widget 分配布局 layout 內50% 寬,80% 高的尺寸。
例如下面這個例子:
BoxLayout:n Button:n text: Button 1n # default size_hint is 1, 1, we dont need to specify it explicitlyn # however its provided here to make things clearn size_hint: 1, 1n
載入 Kivy 目錄:
cd $KIVYDIR/examples/demo/kivycatalognpython main.pyn
把上面代碼中的 $KIVYDIR 替換成你的 Kivy 安裝位置。在左邊點擊標註有 Box Layout 的按鈕。 然後將上面的代碼粘貼到窗口右側的編輯器內。
然後你就可以看到上圖這樣的界面了,這個按鈕 Button 會佔據整個布局尺寸 size的 100%。
修改size_hint_x/size_hint_y 為 .5 這就會把控制項 Widget 調整為布局 layout 的50% 寬度 width/高度 height。
這時候效果如上圖所示,雖然我們已經同時指定了 size_hint_x 和 size_hint_y 為 .5,但似乎只有對 size_hint_x 的修改起作用了。這是因為在盒式布局 boxlayout中,當orientation被設置為豎直方向(vertical) 的時候,size_hint_y 由布局來控制,而如果orientation 被設置為水平方向(horizontal)的時候, size_hint_x 由布局來控制,所以這些情況手動設定就無效了。 這些受控維度的尺寸,是根據子控制項 children 在 盒式布局 boxlayout中的總編號來計算的。在上面的例子中,這個子控制項的size_hint_y 是受控的(.5/.5 = 1)。所以,這裡控制項就佔據了上層布局的整個高度。
接下來咱們再添加一個按鈕 Button到這個 布局 layout看看有什麼效果。
盒式布局 boxlayout 默認對其所有的子控制項 children分配了等大的空間。在咱們這個例子裡面,比例是50-50,因為有兩個子控制項 children。那麼接下來咱們就對其中的一個子控制項設置一下 size_hint,然後看看效果怎麼樣。
從上圖可以看出,如果一個子控制項有了一個指定的 size_hint,這就會決定該控制項 Widget使用盒式布局 boxlayout提供的空間中的多大比例,來作為自己的尺寸 size 。在我們這個例子中,第一個按鈕 Button 的size_hint_x設置為了 .5。那麼這個控制項分配到的空間計算方法如下:
first childs size_hint divided bynfirst childs size_hint + second childs size_hint + ...n(no of children)nn.5/(.5+1) = .333...n
盒式布局 BoxLayout 的剩餘寬度 width會分配給另外的一個子控制項 children。在我們這個例子中,這就意味著第二個按鈕 Button 會佔據整個布局 layout的 66.66% 寬度 width 。
修改 size_hint 探索一下來多適應一下吧。
如果你想要控制一個控制項 Widget的絕對尺寸 size ,可以把size_hint_x/size_hint_y當中的一個或者兩個都設置成 None,這樣的話該控制項的寬度 width 和高度 height的屬性值就會生效了。
pos_hint 是一個詞典 dict,默認值是空。相比於size_hint,布局對pos_hint的處理方式有些不同,不過大體上你還是可以對pos 的各種屬性設定某個值來設定控制項 Widget在父控制項 parent中的相對位置(可以設定的屬性包括:x, y, right, top, center_x, center_y)。
咱們用下面 kivycatalog 中的代碼來可視化地理解一下pos_hint:
FloatLayout:n Button:n text: "We Will"n pos: 100, 100n size_hint: .2, .4n Button:n text: "Wee Wiill"n pos: 200, 200n size_hint: .4, .2nn Button:n text: "ROCK YOU!!"n pos_hint: {x: .3, y: .6}n size_hint: .5, .2n
這份代碼的輸出效果如下圖所示:
說了半天size_hint,你不妨自己試試探索一下 pos_hint,來理解一下這個屬性對控制項位置的效果。
給布局添加背景
關於布局,有一個問題經常被問道:
「怎麼給一個布局添加背景圖片/顏色/視頻/等等......」
本來默認的各種布局都是沒有視覺呈現的:因為布局不像控制項,布局是默認不含有繪圖指令的。不過呢,還是你可以給一個布局實例添上繪圖指令,也就可以添加一個彩色背景了:
在 Python 中的實現方法:
from kivy.graphics import Color, Rectanglennwith layout_instance.canvas.before:n Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255n self.rect = Rectangle(size=layout_instance.size,n pos=layout_instance.pos)n
然而很不幸,這樣只能在布局的初始化位置以布局的初始尺寸繪製一個矩形。所以還要對布局的尺寸和位置變化進行監聽,然後對矩形的尺寸位置進行更新,這樣才能保證這個矩形一直繪製在布局的內部。可以用如下方式實現:
with layout_instance.canvas.before:n Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255n self.rect = Rectangle(size=layout_instance.size,n pos=layout_instance.pos)nndef update_rect(instance, value):n instance.rect.pos = instance.posn instance.rect.size = instance.sizenn# listen to size and position changesnlayout_instance.bind(pos=update_rect, size=update_rect)n
在 kv 文件中:
FloatLayout:n canvas.before:n Color:n rgba: 0, 1, 0, 1n Rectangle:n # self here refers to the widget i.e BoxLayoutn pos: self.posn size: self.sizen
上面的 Kv 文件中的生命,就建立了一個隱含的綁定:上面 Kv 代碼中的最後兩行保證了矩形的位置 pos和尺寸 size會在浮動布局 floatlayout的位置 pos發生變化的時候進行更新。
接下來咱們把上面的代碼片段放進 Kivy 應用裡面。
純 Python 方法:
from kivy.app import Appnfrom kivy.graphics import Color, Rectanglenfrom kivy.uix.floatlayout import FloatLayoutnfrom kivy.uix.button import Buttonnnclass RootWidget(FloatLayout):nn def __init__(self, **kwargs):n # make sure we arent overriding any important functionalityn super(RootWidget, self).__init__(**kwargs)nn # lets add a Widget to this layoutn self.add_widget(n Button(n text="Hello World",n size_hint=(.5, .5),n pos_hint={center_x: .5, center_y: .5}))nnclass MainApp(App):nn def build(self):n self.root = root = RootWidget()n root.bind(size=self._update_rect, pos=self._update_rect)nn with root.canvas.before:n Color(0, 1, 0, 1) # green; colors range from 0-1 not 0-255n self.rect = Rectangle(size=root.size, pos=root.pos)n return rootnn def _update_rect(self, instance, value):n self.rect.pos = instance.posn self.rect.size = instance.sizennif __name__ == __main__:n MainApp().run()n
使用 Kv 語言:
from kivy.app import Appnfrom kivy.lang import Buildernnnroot = Builder.load_string(nFloatLayout:n canvas.before:n Color:n rgba: 0, 1, 0, 1n Rectangle:n # self here refers to the widget i.e FloatLayoutn pos: self.posn size: self.sizen Button:n text: Hello World!!n size_hint: .5, .5n pos_hint: {center_x:.5, center_y: .5}n)nnclass MainApp(App):nn def build(self):n return rootnnif __name__ == __main__:n MainApp().run()n
上面這兩個應用的效果都如下圖所示:
給自定義布局規則/類增加背景色
上面那一段中咱們對布局實例增加背景的方法,如果用到很多歌布局裡面,那就很快變得特別麻煩了。要解決這種需求,就可以基於布局類 Layout 創建一個自定義的布局子類,給自定義的這個類增加一個背景。
使用 Python:
from kivy.app import Appnfrom kivy.graphics import Color, Rectanglenfrom kivy.uix.boxlayout import BoxLayoutnfrom kivy.uix.floatlayout import FloatLayoutnfrom kivy.uix.image import AsyncImagennclass RootWidget(BoxLayout):n passnnclass CustomLayout(FloatLayout):nn def __init__(self, **kwargs):n # make sure we arent overriding any important functionalityn super(CustomLayout, self).__init__(**kwargs)nn with self.canvas.before:n Color(0, 1, 0, 1) # green; colors range from 0-1 instead of 0-255n self.rect = Rectangle(size=self.size, pos=self.pos)nn self.bind(size=self._update_rect, pos=self._update_rect)nn def _update_rect(self, instance, value):n self.rect.pos = instance.posn self.rect.size = instance.sizennclass MainApp(App):nn def build(self):n root = RootWidget()n c = CustomLayout()n root.add_widget(c)n c.add_widget(n AsyncImage(n source="http://www.everythingzoomer.com/wp-content/uploads/2013/01/Monday-joke-289x277.jpg",n size_hint= (1, .5),n pos_hint={center_x:.5, center_y:.5}))n root.add_widget(AsyncImage(source=http://www.stuffistumbledupon.com/wp-content/uploads/2012/05/Have-you-seen-this-dog-because-its-awesome-meme-puppy-doggy.jpg))n c = CustomLayout()n c.add_widget(n AsyncImage(n source="http://www.stuffistumbledupon.com/wp-content/uploads/2012/04/Get-a-Girlfriend-Meme-empty-wallet.jpg",n size_hint= (1, .5),n pos_hint={center_x:.5, center_y:.5}))n root.add_widget(c)n return rootnnif __name__ == __main__:n MainApp().run()n
使用 Kv 語言:
from kivy.app import Appnfrom kivy.uix.floatlayout import FloatLayoutnfrom kivy.uix.boxlayout import BoxLayoutnfrom kivy.lang import BuildernnnBuilder.load_string(n<CustomLayout>n canvas.before:n Color:n rgba: 0, 1, 0, 1n Rectangle:n pos: self.posn size: self.sizenn<RootWidget>n CustomLayout:n AsyncImage:n source: http://www.everythingzoomer.com/wp-content/uploads/2013/01/Monday-joke-289x277.jpgn size_hint: 1, .5n pos_hint: {center_x:.5, center_y: .5}n AsyncImage:n source: http://www.stuffistumbledupon.com/wp-content/uploads/2012/05/Have-you-seen-this-dog-because-its-awesome-meme-puppy-doggy.jpgn CustomLayoutn AsyncImage:n source: http://www.stuffistumbledupon.com/wp-content/uploads/2012/04/Get-a-Girlfriend-Meme-empty-wallet.jpgn size_hint: 1, .5n pos_hint: {center_x:.5, center_y: .5}n)nnclass RootWidget(BoxLayout):n passnnclass CustomLayout(FloatLayout):n passnnclass MainApp(App):nn def build(self):n return RootWidget()nnif __name__ == __main__:n MainApp().run()n
上面這兩個應用的效果都如下圖所示:
在自定義布局類中定義了背景之後,就是要確保在自定義布局的各個實例中使用到這個新特性。
首先,要在全局上增加一個圖形或者顏色給內置的 Kivy 布局的背景,這就需要將所用布局的默認 Kv 規則進行覆蓋。
就拿網格布局 GridLayout 舉例吧:
<GridLayout>n canvas.before:n Color:n rgba: 0, 1, 0, 1n BorderImage:n source: ../examples/widgets/sequenced_images/data/images/button_white.pngn pos: self.posn size: self.sizen
接下來把這段代碼放到一個 Kivy 應用裡面:
from kivy.app import Appnfrom kivy.uix.floatlayout import FloatLayoutnfrom kivy.lang import BuildernnnBuilder.load_string(n<GridLayout>n canvas.before:n BorderImage:n # BorderImage behaves like the CSS BorderImagen border: 10, 10, 10, 10n source: ../examples/widgets/sequenced_images/data/images/button_white.pngn pos: self.posn size: self.sizenn<RootWidget>n GridLayout:n size_hint: .9, .9n pos_hint: {center_x: .5, center_y: .5}n rows:1n Label:n text: "I dont suffer from insanity, I enjoy every minute of it"n text_size: self.width-20, self.height-20n valign: topn Label:n text: "When I was born I was so surprised; I didnt speak for a year and a half."n text_size: self.width-20, self.height-20n valign: middlen halign: centern Label:n text: "A consultant is someone who takes a subject you understand and makes it sound confusing"n text_size: self.width-20, self.height-20n valign: bottomn halign: justifyn)nnclass RootWidget(FloatLayout):n passnnnclass MainApp(App):nn def build(self):n return RootWidget()nnif __name__ == __main__:n MainApp().run()n
效果大概如下圖所示:
我們已經對網格布局 GridLayout 類的規則進行了覆蓋,所以接下來在應用中使用這個類就都會顯示那幅圖片了。
動畫背景怎麼弄呢?
就像在矩形Rectangle/ 邊界圖像BorderImage /橢圓Ellipse/等裡面添加設置繪圖指令一樣,可以用一個特定的紋理屬性 texture :
Rectangle:n texture: reference to a texturen
可以用下面的代碼實現一個動畫背景:
from kivy.app import Appnfrom kivy.uix.floatlayout import FloatLayoutnfrom kivy.uix.gridlayout import GridLayoutnfrom kivy.uix.image import Imagenfrom kivy.properties import ObjectPropertynfrom kivy.lang import BuildernnnBuilder.load_string(n<CustomLayout>n canvas.before:n BorderImage:n # BorderImage behaves like the CSS BorderImagen border: 10, 10, 10, 10n texture: self.background_image.texturen pos: self.posn size: self.sizenn<RootWidget>n CustomLayout:n size_hint: .9, .9n pos_hint: {center_x: .5, center_y: .5}n rows:1n Label:n text: "I dont suffer from insanity, I enjoy every minute of it"n text_size: self.width-20, self.height-20n valign: topn Label:n text: "When I was born I was so surprised; I didnt speak for a year and a half."n text_size: self.width-20, self.height-20n valign: middlen halign: centern Label:n text: "A consultant is someone who takes a subject you understand and makes it sound confusing"n text_size: self.width-20, self.height-20n valign: bottomn halign: justifyn)nnnclass CustomLayout(GridLayout):nn background_image = ObjectProperty(n Image(n source=../examples/widgets/sequenced_images/data/images/button_white_animated.zip,n anim_delay=.1))nnnclass RootWidget(FloatLayout):n passnnnclass MainApp(App):nn def build(self):n return RootWidget()nnif __name__ == __main__:n MainApp().run()n
要理解這裡到底發生了什麼,得從第 13 行開始看:
texture: self.background_image.texturen
這裡是指定讓邊界圖像 BorderImage 的紋理屬性在背景圖像 background_image 的紋理屬性發生更新的時候進行同步更新。背景圖像 background_image 屬性的定義是在第 40 行:
background_image = ObjectProperty(...n
這一句代碼是將背景圖像 background_image 設置成一個對象屬性 ObjectProperty,這樣就可以在其中添加一個圖形控制項 Image。圖像控制項有一個紋理屬性(texture property);在前面的 self.background_image.texture 這句代碼中,就是建立了一個名為 texture 的到這個屬性的引用。圖形控制項 Image 支持動畫(animation):隨著動畫的改變,圖像的紋理會同步更新,在這個過程中,邊界圖像 BorderImage 指令的 texture 紋理屬性也會同步更新。
(譯者註:texture of BorderImage instruction,這裡我對 instruction 的翻譯應該是不太對的,不過我還沒理清楚該怎麼表述。)
也可以直接傳遞自定義數據到紋理屬性 texture。更多細節可以參考紋理 Texture 的文檔。
網狀布局
嗯,看看這個過程如何擴展是很有趣的。
尺寸和位置度量
Kivy 的默認長度單位是像素 pixel,所有的尺寸和位置都用這個單位來表達。你也可以用其他單位來衡量,在跨平台多種設備的時候,這有助於實現更好的連續性體驗(這些設備會把尺寸自動轉換到像素)。
可用單位包括 pt, mm,cm, inch, dp and sp。可以在度量文檔 metrics 中了解更多相關內容。
你還可以探索一下屏幕模塊 screen的用法,這個可以模擬出不同設備的屏幕,供測試應用。
使用屏幕管理器進行屏幕分割
如果你的應用程序要包含多個程序,那可能就需要從一個屏幕 Screen到另一個屏幕 Screen提供一個導航的通道。幸運的是,正好有一個屏幕管理器類ScreenManager,這個類允許你來定義分開的各個屏幕,設置屏幕管理器的TransitionBase就可以實現從一個屏幕到另一個屏幕的跳轉導航。
推薦閱讀: