Kivy中文編程指南:事件和屬性
譯者前言
這一章節是我有史以來翻譯質量的低谷,一來是我自己也是剛學,半懂不懂,二來本身語言基礎各方面也薄弱,三來是筆記本壞掉了,搞個ChromeOS折騰中。
大家湊合看看,看不下去給指出來一下比較不好理解和繞的地方,以及錯誤的地方,我一定即時修改。
簡要介紹
在Kivy開發過程中,事件是最重要的一部分了。如果之前有過GUI圖形界面開發的經驗的話,你可能對此習以為常了,但對新手來說,這個概念很重要。一旦你理解了事件的應用和搭配,你就會發現在Kivy開發的過程中,事件是無處不在的。有了各種事件的搭配,你就可以用Kivy來搭建你想要的各種功能了。
下面這幅圖展示了Kivy框架中事件的處理過程:
事件分派器
事件分派器EventDispatcher c是Kivy框架中最重要的基類之一。通過這個類,用戶可以註冊各種事件,然後分發給對應的部件(一般情況下是其他的事件分派器)。控制項類Widget, 動畫類Animation 以及時間類Clock 都屬於事件分派器。
事件分派器對象要在整個程序的循環流程的基礎上來生成和處理各種事件。
主流程
上文的簡介概括起來是說,Kivy有一個主循環體。這個循環體會在Kivy應用的整個生命周期中一直運行,直到退出應用的時候才結束。
在循環內部,每一步迭代都伴隨有各種事件生成,這些事件可以來自用戶輸入、硬體感測器,或者是其他的各種來源,然後一幀一幀地渲染到屏幕上。
你寫的應用程序將要指定好各種由主循環進行調用而產生的回調(callback,稍後再詳細介紹相關內容)。如果一次回調花費很長時間或者根本不退出,主循環就被打破了,你的應用也就不能正常工作了。
在Kivy應用裡面,一定要避免用特別長的循環、無限循環,或者休眠。下面這個代碼就同時是死循環+休眠,就是一個反例了:
while True:n animate_something()n time.sleep(.10)n
如果把上面這段代碼拿去運行,那程序就會無法退出循環了,就讓Kivy卡住了,什麼後續步驟都不能進行了。用戶就只能看到一個黑色的窗口,什麼操作都沒有響應。不能死循環也不能休眠,所以就得想其他辦法,比如有計劃地重複調用對animate_something()這樣的函數。(animate_something 的意思是讓某個東西動起來,作者是用來指代類似的這種需要時不時重複調用的函數。)
計劃周期事件
利用schedule_interval()這個函數,你就可以每秒對某個函數或者方法進行指定次數的調用了。下面就是一個例子,在這段代碼的第三行,實現了每秒鐘調用my_callback(dt)函數三十次:
def my_callback(dt):n print My callback is called, dtnevent = Clock.schedule_interval(my_callback, 1 / 30.)n#譯者註:這裡的1/30明顯就是頻率的倒數了,如果是每秒鐘50次就應該是1/50,以此類推了,大家可以自己修改試試看。n
要取消之前的計劃事件有多種方法。可以用cancel(),也可以用 unschedule():
event.cancel()nn#或者用下面這種方法nnClock.unschedule(event)n
再有一種方法,就是在回調的時候返回False,這樣這個事件就會被自動取消計劃,不再重複:
count = 0ndef my_callback(dt):n global countn count += 1n if count == 10:n print Last call of my callback, bye bye !n return Falsen print My callback is callednClock.schedule_interval(my_callback, 1 / 30.)n
計劃一次性事件
使用schedule_once()函數,可以對一個函數稍後調用的效果,可以是在下一幀,也可以是在指定時間X之後:
def my_callback(dt):n print My callback is called !nClock.schedule_once(my_callback, 1)n
上面這段代碼會在一秒鐘後再對my_callback(dt)進行調用。schedule_once()函數的第二個變數X就是延遲調用的時間,以秒為單位。具體這個變數的用法有以下三種:
- 若X大於零,則作為時間長度的秒數,延遲X秒之後進行下一次調用
- 若X等於零,則在下一幀進行調用
- 若X為-1,調用則發生在下一幀渲染之前
假如你已經有了一個計劃事件,但又想要在下一幀渲染之前計劃一次調用,這種情況就適合使用-1這種用法。
這裡就有了一種衍生出來的重複調用某個函數的方法,就是在函數體內放一個schedule_once(),然後在第二次調用該函數的時候,函數內的schedule_once()就會繼續對本身進行調用了:
def my_callback(dt):n print My callback is called !n Clock.schedule_once(my_callback, 1)nClock.schedule_once(my_callback, 1)n
主循環會一直按照代碼的要求來保持各種計劃調用的實現,但一次計劃調用發生的具體時間是具有一些不確定性的。有時候其他的調用或者應用中的其他任務可能會比想像中執行得更久一些,這時候用計時的方法制定計劃就不太合適了。
後面介紹的這種用內置schedule_once()來進行重複回調問題的解決方案中,在最後一次迭代結束後,下一次迭代將至少要一秒之後才能被調用。 而使用schedule_interval()這種方法就可以每秒都進行回調。
觸發事件
有時候可能一個函數只需要計劃在下一幀調用一次,而不允許重複調用。這時候就可以用下面這樣的思路來實現:
#首先是用schedule_once()計劃調用一次nevent = Clock.schedule_once(my_callback, 0)nn#然後在另外一個位置,就用unschedule()取消計劃調用,這樣就能避免重複調用。接下來就是再次用schedule_once()進行計劃調用。nClock.unschedule(event)nevent = Clock.schedule_once(my_callback, 0)nn#譯者註:我這部分理解的也不夠深,翻譯得很生硬,我的大概理解就是這樣可以精確控制調用次數,避免一次計劃調用之後發生的調用次數不可控。n
上面這種方法構建觸發器可謂費時費力,因為你得經常用到unschedule,即使一個事件已經結束。此外,每次還都產生新事件。所以可以用下面這個trigger()來作為觸發器:
trigger = Clock.create_trigger(my_callback)n# laterntrigger()n
這樣你每次調用trigger()就可以了,這個觸發器會對你的my_callback回調進行單次計劃調用。如果之前存在計劃調用了,則不重新產生計劃調用。
控制項事件
控制項有兩種默認事件:
屬性事件:比如你的控制項改變了位置或者大小,就會觸發一個事件。
控制項定義的事件:比如一個Button按鈕控制項被按下或者鬆開,也會觸發一個事件。
關於控制項的Touch事件的管理和傳播,可以參考API文檔中這部分相關內容。
創建自定義事件
要使用自定義事件創建事件分派器,需要首先在類中註冊事件名稱,然後創建同名的方法。
例如下面這段代碼所示:
class MyEventDispatcher(EventDispatcher):n def __init__(self, **kwargs):n self.register_event_type(on_test)n super(MyEventDispatcher, self).__init__(**kwargs)nn def do_something(self, value):n # when do_something is called, the on_test event will ben # dispatched with the valuen self.dispatch(on_test, value)nn def on_test(self, *args):n print "I am dispatched", argsn
附加回調
要利用一個事件,必須要對其綁定回調。當事件被分派的時候,該特定事件相關的參數將被用於調用回調。
回調可以使Python中能進行調用的任意內容,函數或者方法都可以,但一定要確保回調要接收事件發出的參數。最安全的常規做法是接收* args參數,將所有參數都存放成一個參數列表。
例如:
def my_callback(value, *args):n print "Hello, I got an event!", argsnnev = MyEventDispatcher()nev.bind(on_test=my_callback)nev.do_something(test)n
請閱讀參考 kivy.event.EventDispatcher.bind() 方法的文檔來查看更多附加調用相關的樣例。
簡介屬性Properties
屬性(Properties)是定義和綁定事件的一種很贊的辦法。關鍵就是屬性能生成事件,這樣當你的某個對象中有一個屬性(attribute)發生改變的時候,所有引用該屬性(attribute)的屬性(Properties)都會被自動更新
譯者註:我漢語辭彙量匱乏了,很痛苦,這裡說不明白了,所以就放了原文的單詞作為對比,避免混淆了。
針對你要處理的數據類型,存在很多種不同類型的屬性(Properties):
- 字元串屬性StringProperty
- 數值屬性NumericProperty
- 有界數值屬性BoundedNumericProperty
- 對象屬性ObjectProperty
- 詞典屬性DictProperty
- 列表屬性ListProperty
- 選項屬性OptionProperty
- 別名屬性AliasProperty
- 布爾屬性BooleanProperty
- 引用屬性ReferenceListProperty
屬性聲明
要聲明屬性(Properties),必須要在類的層次上進行聲明。接下來這個類才能在你創建對象的時候對真是的屬性(attributes)進行實例化。此屬性(Properties)非彼屬性(attributes),Properties是根據你的attributes來創建事件的機制,例如:
class MyWidget(Widget):n text = StringProperty()n
譯者註:我這段翻譯的很垃圾,一來是自己水平問題,二來是自己的語言能力問題。
當覆蓋初始化方法__init__的時候,一定要接收**kwargs做參數,並且一定要用super()來調用基類的初始化方法__init__,傳遞自定義類的實例過去:
def __init__(self, **kwargs):n super(MyWidget, self).__init__(**kwargs)n
分派屬性事件
Kivy的屬性Property,默認提供了一個on_事件。在屬性被改變的時候,就會調用這個事件了。
特別注意
如果屬性的新值與當前已有的值相等,那麼on_事件就不會被調用了。
例如下面這段代碼:
class CustomBtn(Widget):nn pressed = ListProperty([0, 0])nn def on_touch_down(self, touch):n if self.collide_point(*touch.pos):n self.pressed = touch.posn return Truen return super(CustomBtn, self).on_touch_down(touch)nn def on_pressed(self, instance, pos):n print (pressed at {pos}.format(pos=pos))n
上面代碼的第三行中:
pressed = ListProperty([0, 0])n
這一句中,基於ListProperty定義了一個pressed按下的屬性,默認值是[0,0]。從這往後,只要這個屬性被改變了,on_presses事件就會被調用。
第五行有如下代碼:
def on_touch_down(self, touch):n if self.collide_point(*touch.pos):n self.pressed = touch.posn return Truen return super(CustomBtn, self).on_touch_down(touch)n
這部分代碼覆蓋了控制項類的_touch_down()方法。這段代碼中,用控制項對touch觸碰的位置進行了檢測。
如果touch的位置在控制項範圍內,就把pressed的值改變成touch.pos這個值,然後返回True,這表明程序已經處理好了這個touch了,就把這個touch消耗掉了,不用再傳播了。
如果touch的位置在控制項外部,就通過super(...)了調用原始事件,並返回結果。這就和常規情況一樣了,touch事件會被繼續傳遞下去。
在第十一行:
def on_pressed(self, instance, pos):n print (pressed at {pos}.format(pos=pos))n
這裡定義了一個on_pressed函數,只要屬性值發生改變了,這個函數就會被調用。
特別注意
這個on_事件是在類內定義屬性的位置被調用。在定義該屬性的類之外,若要監控/觀察一個屬性的任何變動,就必須丟這個屬性進行bind綁定操作。
只能讀取到一個控制項實例的時候,要怎麼去監控屬性的變化呢?這時候用bind綁定一下屬性就行了:
your_widget_instance.bind(property_name=function_name)n
例如下面這段代碼:
class RootWidget(BoxLayout):nn def __init__(self, **kwargs):n super(RootWidget, self).__init__(**kwargs)n self.add_widget(Button(text=btn 1))n cb = CustomBtn()n cb.bind(pressed=self.btn_pressed)n self.add_widget(cb)n self.add_widget(Button(text=btn 2))nn def btn_pressed(self, instance, pos):n print (pos: printed from root widget: {pos}.format(pos=.pos))n
如果運行上面這段代碼,會發現有兩次print輸出語句出現在控制台中。第一個是來自_pressed事件,在CustomBtn類內部調用;另外一次print是來自我們用bind綁定到了屬性變化上的btn_pressed函數。
(譯者註:在用bind綁定了之後,屬性的變化都會通過bind的函數被看到了。)
兩個函數都被調用的原因很簡單。Bind綁定操作並不意味著覆蓋。這兩個函數同時保留就冗餘了,所以一般情況你只選擇一個來對屬性變化進行監聽/反應就行了。
你還得注意一下傳遞給on_事件或者綁定到屬性的函數的參數。
def btn_pressed(self, instance, pos):n
第一個參數是self,這就是該函數所在類本身的一個實例。也可以用內聯函數,如下代碼所示:
cb = CustomBtn()nn def _local_func(instance, pos):n print (pos: printed from root widget: {pos}.format(pos=pos))nn cb.bind(pressed=_local_func)n self.add_widget(cb)n
第一個參數是定義屬性的類的實例。第二個參數是一個值,這個值是屬性的新值。 上面那一段只是代碼片段,下面這一段代碼是完整的樣例了,可以複製粘貼到編輯器裡面然後測試一下:
from kivy.app import Appn from kivy.uix.widget import Widgetn from kivy.uix.button import Buttonn from kivy.uix.boxlayout import BoxLayoutn from kivy.properties import ListPropertynn class RootWidget(BoxLayout):nn def __init__(self, **kwargs):n super(RootWidget, self).__init__(**kwargs)n self.add_widget(Button(text=btn 1))n cb = CustomBtn()n cb.bind(pressed=self.btn_pressed)n self.add_widget(cb)n self.add_widget(Button(text=btn 2))nn def btn_pressed(self, instance, pos):n print (pos: printed from root widget: {pos}.format(pos=pos))nn class CustomBtn(Widget):nn pressed = ListProperty([0, 0])nn def on_touch_down(self, touch):n if self.collide_point(*touch.pos):n self.pressed = touch.posn # we consumed the touch. return False here to propagaten # the touch further to the children.n return Truen return super(CustomBtn, self).on_touch_down(touch)nn def on_pressed(self, instance, pos):n print (pressed at {pos}.format(pos=pos))nn class TestApp(App):nn def build(self):n return RootWidget()nn if __name__ == __main__:n TestApp().run()n
運行上面這段完整的樣例代碼,會得到如下圖所示的輸出:
咱們這個CustomBtn沒有做任何視覺上的調整,所以就是個大黑塊。你可以觸摸/點擊這個黑色的區域,來看看控制台裡面的輸出。
複合屬性
定義一個別名屬性AliasProperty的時候,通常要定義一個getter函數和一個setter函數,前者用來讀取值,後者用來設定值。這時候,你就得通過bind綁定參數來確定好getter和setter函數的調用時間。
例如下面這段代碼:
cursor_pos = AliasProperty(_get_cursor_pos, None, bind=(n cursor, padding, pos, size, focus,n scroll_x, scroll_y))n Current position of the cursor, in (x, y).nn :attr:`cursor_pos` is a :class:`~kivy.properties.AliasProperty`, read-only.n n
這裡的cursor_pos(游標位置的意思)就是一個別名屬性AliasProperty,它有一個getter函數,名為_get_cursor_pos(),然後沒有設置setter函數,這就說明這個屬性是只讀的。
最末尾那一段的bind參數的意思是,當在bind=這個等號後括弧內的屬性中有任意的一個發生變化,都會分派on_cursor_pos事件。
推薦閱讀:
※分享一個簡單的多進程小爬蟲
※第一章 | 使用python機器學習
※Python數據分析及可視化實例之Flask Web開發
※lasagne,keras,pylearn2,nolearn深度學習庫,到底哪家強?
※[requests,pyquery]爬取獵聘網職位信息