編程向?qū)?.5事件和屬性
事件是Kivy編程里面一個重要的部分。對于有GUI開發(fā)經(jīng)驗的人來說也許不是那么讓人驚奇,但對于初學(xué)者是一個重要的概念。一旦你理解了事件如何工作、如何綁定,你將會在Kivy到處發(fā)現(xiàn)它們。它們使你想利用Kivy實現(xiàn)任何的行為變得很容易。
下面的插圖顯示了在Kivy框架中事件如何被處理。

一、介紹事件發(fā)送
Kivy框架的最重要的基類之一就是EventDispatcher類。這個類允許你注冊事件類型并發(fā)送它們到感興趣的地方(通常是其它事件發(fā)送者)。部件、動畫、時鐘類都是事件發(fā)送的例子。
EventDispatcher對象依賴主循環(huán)生成和處理事件。
二、主循環(huán)
在上面插圖中,主循環(huán)作為輪廓。這個循環(huán)運行在應(yīng)用程序的全部生命周期中,直到應(yīng)用程序退出時才終止。
在循環(huán)里面,每一次迭代,當(dāng)發(fā)生用戶輸入、傳感器或者一些其他資源、畫面被渲染顯示時,總會有事件生成。
你的應(yīng)用程序可以指定回調(diào)函數(shù),它們在主循環(huán)中被調(diào)用。如果一個回調(diào)函數(shù)費時太長或者根本不會退出,則主循環(huán)會中斷同時你的應(yīng)用程序無法正常運行。
在Kivy應(yīng)用程序中,你必須避免使用長循環(huán)、死循環(huán)或睡眠(sleeping),如下代碼是需要避免的:
while True:
animate_something()
time.sleep(.10)
當(dāng)你運行上面的代碼,則你的程序永遠無法退出該循環(huán),要預(yù)防Kivy做類似的事情。結(jié)果將會看到一個無法交互的黑色的窗口。正確的方式的,你需要定制(schedule)你的animate_somthing()函數(shù)重復(fù)調(diào)用。
(一)重復(fù)事件
你可以使用schedule_interval()每隔X時間調(diào)用一個函數(shù)或方法,下面是一個例子,每隔1/30秒調(diào)用一次my_callback函數(shù):
def my_callback(dt):
print 'my callback is called', dt
Clock.schedule_interval(my_callback, 1/30.)
你有兩種方法來取消前面定制的事件,第一種是:
Clock.unschedule(my_callback)
或者你在你的回調(diào)函數(shù)中返回False,那么你的事件將會自動取消:
count = 0
def my_callback(dt):
global count
count += 1
if count == 10:
print 'Last call of my callback, bye bye!'
return False
print 'My callback is called'
Clock.schedule_interval(my_callback, 1/30.)
(二)單次事件
使用schedule_once(),你可以定制執(zhí)行一次你的回調(diào)函數(shù),比如在下一幀,或X秒后:
def my_callback(dt):
print 'My callback is called!'
Clock.schedule_once(my_callback, 1)
上面的代碼中,my_callback()函數(shù)將會在1秒后執(zhí)行。1秒?yún)?shù)是在執(zhí)行該程序前等待的時間,以秒為單位。但是你可以使用特殊的值作為時間參數(shù)得到一切其它結(jié)果:
- 如果X > 0,則回調(diào)函數(shù)會在X秒后執(zhí)行。
- 如果X = 0, 則回調(diào)函數(shù)會在下一幀執(zhí)行。
- 如果x = -1,則回調(diào)函數(shù)在在下一幀之前執(zhí)行。
其中 x = -1最為常用。
重復(fù)執(zhí)行一個函數(shù)的第二種方法是:一個回調(diào)函數(shù)使用schedule_once遞歸調(diào)用了自己,在外部schedule_once函數(shù)中又調(diào)用了該回調(diào)函數(shù):
def my_callback(dt):
print 'My callback is called !'
Clock.schedule_once(my_callback, 1)
Clock.schedule_once(my_callback, 1)
當(dāng)主循環(huán)嘗試保持定制請求時,當(dāng)恰好一個定制的回調(diào)函數(shù)被調(diào)用時,有一些不確定的情況會發(fā)生。有時另外一些回調(diào)函數(shù)或一些任務(wù)花費了超出預(yù)期的時間,則定時會被延遲。
在第二種解決方案中,在上一次迭代執(zhí)行結(jié)束后,下一次迭代每秒至少會被調(diào)用一次。而使用schedule_interval(),回調(diào)函數(shù)則每秒都會被調(diào)用。
(三)事件跟蹤
如果你想為下一幀定制一個僅執(zhí)行一次的函數(shù),類似一個出發(fā)器,你可能這樣做:
Clock.unschedule(my_callback)
Clock.schedule_once(my_callback, 0)
這種方式的代價是昂貴的,因為你總是調(diào)用unschedule()方法,無論你是否曾經(jīng)定制過它。另外,unschedule()方法需要迭代時鐘的弱引用列表,目的是找到你的回調(diào)函數(shù)并移除它。替代的方法是使用出發(fā)器:
trigger = Clock.create_trigger(my_callback)
#隨后
trigger()
每次你調(diào)用trigger,它會為你的回調(diào)函數(shù)定制一個信號調(diào)用,如果已經(jīng)被定制,則不會重新定制。
三、部件事件
每個部件都有兩個默認的事件類型:
- 屬性事件(Property event):如果你的部件改變了位置或尺寸,則事件被觸發(fā)。
- 部件定義事件(Widget-defined event):當(dāng)一個按鈕被按下或釋放時,事件被觸發(fā)。
四、自定義事件
為了創(chuàng)建一個自定義事件,你需要在一個類中注冊事件名,并創(chuàng)建一個同名的方法:
class MyEventDispatcher(EventDispatcher):
def __init__(self, **kwargs):
self.register_event_type('on_test')
super(MyEventDispatcher, self).__init__(**kwargs)
def do_something(self, value):
#當(dāng)do_something被調(diào)用時,on_test事件將會連同value被發(fā)送
self.dispatch('on_test', value)
def on_test(self, *args):
print "I am dispatched", args
五、附加回調(diào)
為了使用事件,你必須綁定回調(diào)函數(shù)。當(dāng)事件被發(fā)送時,你的回調(diào)函數(shù)將會連同參數(shù)被調(diào)用。
一個回調(diào)函數(shù)可以是任何python函數(shù),但是你必須確保它接受事件發(fā)出的參數(shù)。因此,使用*args的參數(shù)會更安全,這樣將會在args列表中接收到所有的參數(shù)。例如:
def my_callback(value, *args):
print "Hello, I got an event!", args
e = MyEventDispatcher()
e.bind(on_test = my_callback)
e.do_something('test')
有關(guān)附加回調(diào)函數(shù)更多的示例可以參閱kivy.event.EventDispatcher.bind()文檔
六、屬性介紹
屬性是一個很好的方法用來定義事件并綁定它們。本質(zhì)上來說,當(dāng)你的對象的特征值發(fā)生變化時,它們創(chuàng)造事件,所有的引用特征值的屬性都會自動更新。
有不同類型的屬性來描述你想要處理的數(shù)據(jù)類型。
- StringProperty
- NumericProperty
- BoundedNumericProperty
- ObjectProperty
- DictProperty
- ListProperty
- OptionProperty
- AliasProperty
- BooleanProperty
- ReferenceListProperty
七、聲明屬性
為了聲明屬性,你必須在類的級別進行。當(dāng)你的對象被創(chuàng)建時,該類將會進行實際特征值的初始化。特寫屬性不是特征值,它們是基于你的特征值創(chuàng)建事件的機制。
class MyWidget(Widget):
text = StringProperty('')
當(dāng)重載init時,總是接受**kwargs參數(shù)并使用super()調(diào)用父類的init方法:
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
八、發(fā)送屬性事件
Kivy的屬性,默認提供一個on_<property_name>事件。當(dāng)屬性值改變時該事件被調(diào)用。
注意,如果新的屬性值等于當(dāng)前值,該事件不會被調(diào)用。
例如:
class CustomBtn(Widget):
pressed = ListProperty([0, 0])
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.pressed = touch.pos
return True
return super(CustomBtn, self).on_touch_down(touch)
def on_pressed(self, instance, pos):
print('pressed at{pos}'.format(pos=pos))
在第3行:
pressed = ListProperty([0,0])
我們定義了pressed屬性,類型為ListProperty,默認值為[0, 0],當(dāng)屬性值發(fā)生改變時,on_pressed事件被調(diào)用。
在第5行:
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.pressed = touch.pos
return True
return super(CustomBtn, self).on_touch_down(touch)
我們重載了on_touch_down()方法,我們?yōu)槲覀兊牟考隽伺鲎矙z測。
如果觸摸發(fā)生在我們的部件內(nèi)部,我們改變touch.pos按下的值并返回True,表明我們處理了這次觸摸并不想它繼續(xù)傳遞。
最后,如果觸摸發(fā)生在我們的部件外部,我們使用super()調(diào)用原始事件并返回結(jié)果。它允許觸摸事件繼續(xù)傳遞。
最后在11行:
def on_pressed(self, instance, pos):
print ('pressed at {pos}'.format(pos=pos))
我們定義了on_pressed函數(shù),當(dāng)屬性值改變時,該函數(shù)被調(diào)用。
注意當(dāng)屬性值被定義時,on_<prop_name>事件在類內(nèi)部被調(diào)用。為了在類的外部監(jiān)控或觀察任何屬性值的變動,你可以以下面的方式綁定屬性值。
your_widget_instance.bind(property_name=function_name)
例如,考慮以下代碼:
class RootWidget(BoxLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.add_widget(Button(text='btn 1'))
cb = CustomBtn()
cb.bind(pressed=self.btn_pressed)
self.add_widget(cb)
self.add_widget(Button(text='btn 2'))
def btn_pressed(self, instance, pos):
print ('pos: printed from root widget: {pos}'.format(pos=.pos))
如果你運行上面的代碼,你會注意到在控制臺有兩個打印信息。一個來自on_pressed事件,該事件在CustomBtn類內(nèi)部被調(diào)用,另一個來自我們綁定屬性改變的btn_pressed函數(shù)
你也需要注意到傳遞給on_<property_name>事件的參數(shù)及綁定屬性的函數(shù)。
def btn_pressed(self, instance, pos):
第一個參數(shù)是self,是該函數(shù)被定義的類的實例。你可以如下面的方式使用一個內(nèi)聯(lián)函數(shù):
cb = CustomBtn()
def _local_func(instance, pos):
print ('pos: printed from root widget: {pos}'.format(pos=.pos))
cb.bind(pressed=_local_func)
self.add_widget(cb)
第一個參數(shù)是屬性被定義的類的實例。
第二個參數(shù)是屬性的新的值。
下面是一個完整的麗日,你能拷貝下來進行實驗。
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ListProperty
class RootWidget(BoxLayout):
def __init__(self, **kwargs):
super(RootWidget, self).__init__(**kwargs)
self.add_widget(Button(text='btn 1'))
cb = CustomBtn()
cb.bind(pressed=self.btn_pressed)
self.add_widget(cb)
self.add_widget(Button(text='btn 2'))
def btn_pressed(self, instance, pos):
print ('pos: printed from root widget: {pos}'.format(pos=pos))
class CustomBtn(Widget):
pressed = ListProperty([0, 0])
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
self.pressed = touch.pos
# we consumed the touch. return False here to propagate
# the touch further to the children.
return True
return super(CustomBtn, self).on_touch_down(touch)
def on_pressed(self, instance, pos):
print ('pressed at {pos}'.format(pos=pos))
class TestApp(App):
def build(self):
return RootWidget()
if __name__ == '__main__':
TestApp().run()
運行結(jié)果如下:

我們的定制按鈕沒有可視的表述,因此顯示一個黑塊。你能觸摸或點擊它以在控制臺查看輸出。
九、混合屬性
當(dāng)定義一個AliasProperty時,通常的做法是定義getter()和setter函數(shù)。當(dāng)getter()和setter()函數(shù)使用bind被調(diào)用時,它落在了你的肩上??紤]以下代碼:
cursor_pos = AliasProperty(_get_cursor_pos, None, bind=(
'cursor', 'padding', 'pos', 'size', 'focus',
'scroll_x', 'scroll_y'))
'''Current position of the cursor, in (x, y).
:attr:`cursor_pos` is a :class:`~kivy.properties.AliasProperty`, read-only.
'''
這里cursor_pos是一個AliasProperty,它使用_get_cursor_pos作為getter(),并且setter()為None,表明這是一個只讀屬性。
在最后,當(dāng)任何使用bind=argument的屬性改變時,on_cursor_pos事件被發(fā)送。