编程向导4.5事件和属性
事件是Kivy编程里面一个重要的部分。对于有GUI开发经验的人来说也许不是那么让人惊奇,但对于初学者是一个重要的概念。一旦你理解了事件如何工作、如何绑定,你将会在Kivy到处发现它们。它们使你想利用Kivy实现任何的行为变得很容易。
下面的插图显示了在Kivy框架中事件如何被处理。
一、介绍事件发送
Kivy框架的最重要的基类之一就是EventDispatcher类。这个类允许你注册事件类型并发送它们到感兴趣的地方(通常是其它事件发送者)。部件、动画、时钟类都是事件发送的例子。
EventDispatcher对象依赖主循环生成和处理事件。
二、主循环
在上面插图中,主循环作为轮廓。这个循环运行在应用程序的全部生命周期中,直到应用程序退出时才终止。
在循环里面,每一次迭代,当发生用户输入、传感器或者一些其他资源、画面被渲染显示时,总会有事件生成。
你的应用程序可以指定回调函数,它们在主循环中被调用。如果一个回调函数费时太长或者根本不会退出,则主循环会中断同时你的应用程序无法正常运行。
在Kivy应用程序中,你必须避免使用长循环、死循环或睡眠(sleeping),如下代码是需要避免的:
while True:
animate_something()
time.sleep(.10)
当你运行上面的代码,则你的程序永远无法退出该循环,要预防Kivy做类似的事情。结果将会看到一个无法交互的黑色的窗口。正确的方式的,你需要定制(schedule)你的animate_somthing()函数重复调用。
(一)重复事件
你可以使用schedule_interval()每隔X时间调用一个函数或方法,下面是一个例子,每隔1/30秒调用一次my_callback函数:
def my_callback(dt):
print 'my callback is called', dt
Clock.schedule_interval(my_callback, 1/30.)
你有两种方法来取消前面定制的事件,第一种是:
Clock.unschedule(my_callback)
或者你在你的回调函数中返回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(),你可以定制执行一次你的回调函数,比如在下一帧,或X秒后:
def my_callback(dt):
print 'My callback is called!'
Clock.schedule_once(my_callback, 1)
上面的代码中,my_callback()函数将会在1秒后执行。1秒参数是在执行该程序前等待的时间,以秒为单位。但是你可以使用特殊的值作为时间参数得到一切其它结果:
- 如果X > 0,则回调函数会在X秒后执行。
- 如果X = 0, 则回调函数会在下一帧执行。
- 如果x = -1,则回调函数在在下一帧之前执行。
其中 x = -1最为常用。
重复执行一个函数的第二种方法是:一个回调函数使用schedule_once递归调用了自己,在外部schedule_once函数中又调用了该回调函数:
def my_callback(dt):
print 'My callback is called !'
Clock.schedule_once(my_callback, 1)
Clock.schedule_once(my_callback, 1)
当主循环尝试保持定制请求时,当恰好一个定制的回调函数被调用时,有一些不确定的情况会发生。有时另外一些回调函数或一些任务花费了超出预期的时间,则定时会被延迟。
在第二种解决方案中,在上一次迭代执行结束后,下一次迭代每秒至少会被调用一次。而使用schedule_interval(),回调函数则每秒都会被调用。
(三)事件跟踪
如果你想为下一帧定制一个仅执行一次的函数,类似一个出发器,你可能这样做:
Clock.unschedule(my_callback)
Clock.schedule_once(my_callback, 0)
这种方式的代价是昂贵的,因为你总是调用unschedule()方法,无论你是否曾经定制过它。另外,unschedule()方法需要迭代时钟的弱引用列表,目的是找到你的回调函数并移除它。替代的方法是使用出发器:
trigger = Clock.create_trigger(my_callback)
#随后
trigger()
每次你调用trigger,它会为你的回调函数定制一个信号调用,如果已经被定制,则不会重新定制。
三、部件事件
每个部件都有两个默认的事件类型:
- 属性事件(Property event):如果你的部件改变了位置或尺寸,则事件被触发。
- 部件定义事件(Widget-defined event):当一个按钮被按下或释放时,事件被触发。
四、自定义事件
为了创建一个自定义事件,你需要在一个类中注册事件名,并创建一个同名的方法:
class MyEventDispatcher(EventDispatcher):
def __init__(self, **kwargs):
self.register_event_type('on_test')
super(MyEventDispatcher, self).__init__(**kwargs)
def do_something(self, value):
#当do_something被调用时,on_test事件将会连同value被发送
self.dispatch('on_test', value)
def on_test(self, *args):
print "I am dispatched", args
五、附加回调
为了使用事件,你必须绑定回调函数。当事件被发送时,你的回调函数将会连同参数被调用。
一个回调函数可以是任何python函数,但是你必须确保它接受事件发出的参数。因此,使用*args的参数会更安全,这样将会在args列表中接收到所有的参数。例如:
def my_callback(value, *args):
print "Hello, I got an event!", args
e = MyEventDispatcher()
e.bind(on_test = my_callback)
e.do_something('test')
有关附加回调函数更多的示例可以参阅kivy.event.EventDispatcher.bind()文档
六、属性介绍
属性是一个很好的方法用来定义事件并绑定它们。本质上来说,当你的对象的特征值发生变化时,它们创造事件,所有的引用特征值的属性都会自动更新。
有不同类型的属性来描述你想要处理的数据类型。
- StringProperty
- NumericProperty
- BoundedNumericProperty
- ObjectProperty
- DictProperty
- ListProperty
- OptionProperty
- AliasProperty
- BooleanProperty
- ReferenceListProperty
七、声明属性
为了声明属性,你必须在类的级别进行。当你的对象被创建时,该类将会进行实际特征值的初始化。特写属性不是特征值,它们是基于你的特征值创建事件的机制。
class MyWidget(Widget):
text = StringProperty('')
当重载init时,总是接受**kwargs参数并使用super()调用父类的init方法:
def __init__(self, **kwargs):
super(MyWidget, self).__init__(**kwargs)
八、发送属性事件
Kivy的属性,默认提供一个on_<property_name>事件。当属性值改变时该事件被调用。
注意,如果新的属性值等于当前值,该事件不会被调用。
例如:
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],当属性值发生改变时,on_pressed事件被调用。
在第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()方法,我们为我们的部件做了碰撞检测。
如果触摸发生在我们的部件内部,我们改变touch.pos按下的值并返回True,表明我们处理了这次触摸并不想它继续传递。
最后,如果触摸发生在我们的部件外部,我们使用super()调用原始事件并返回结果。它允许触摸事件继续传递。
最后在11行:
def on_pressed(self, instance, pos):
print ('pressed at {pos}'.format(pos=pos))
我们定义了on_pressed函数,当属性值改变时,该函数被调用。
注意当属性值被定义时,on_<prop_name>事件在类内部被调用。为了在类的外部监控或观察任何属性值的变动,你可以以下面的方式绑定属性值。
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类内部被调用,另一个来自我们绑定属性改变的btn_pressed函数
你也需要注意到传递给on_<property_name>事件的参数及绑定属性的函数。
def btn_pressed(self, instance, pos):
第一个参数是self,是该函数被定义的类的实例。你可以如下面的方式使用一个内联函数:
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)
第一个参数是属性被定义的类的实例。
第二个参数是属性的新的值。
下面是一个完整的丽日,你能拷贝下来进行实验。
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()
运行结果如下:
我们的定制按钮没有可视的表述,因此显示一个黑块。你能触摸或点击它以在控制台查看输出。
九、混合属性
当定义一个AliasProperty时,通常的做法是定义getter()和setter函数。当getter()和setter()函数使用bind被调用时,它落在了你的肩上。考虑以下代码:
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,表明这是一个只读属性。
在最后,当任何使用bind=argument的属性改变时,on_cursor_pos事件被发送。