企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
### 编程向导4.5事件和属性 事件是Kivy编程里面一个重要的部分。对于有GUI开发经验的人来说也许不是那么让人惊奇,但对于初学者是一个重要的概念。一旦你理解了事件如何工作、如何绑定,你将会在Kivy到处发现它们。它们使你想利用Kivy实现任何的行为变得很容易。 下面的插图显示了在Kivy框架中事件如何被处理。 ![events and properties](http://ww2.sinaimg.cn/large/577d3ebejw1f0yoh6riihj20m80fcgpc.jpg) #### 一、介绍事件发送 Kivy框架的最重要的基类之一就是EventDispatcher类。这个类允许你注册事件类型并发送它们到感兴趣的地方(通常是其它事件发送者)。[部件](https://kivy.org/docs/api-kivy.uix.widget.html#kivy.uix.widget.Widget)、[动画](https://kivy.org/docs/api-kivy.animation.html#kivy.animation.Animation)、[时钟](https://kivy.org/docs/api-kivy.clock.html#kivy.clock.Clock)类都是事件发送的例子。 EventDispatcher对象依赖主循环生成和处理事件。 #### 二、主循环 在上面插图中,主循环作为轮廓。这个循环运行在应用程序的全部生命周期中,直到应用程序退出时才终止。 在循环里面,每一次迭代,当发生用户输入、传感器或者一些其他资源、画面被渲染显示时,总会有事件生成。 你的应用程序可以指定回调函数,它们在主循环中被调用。如果一个回调函数费时太长或者根本不会退出,则主循环会中断同时你的应用程序无法正常运行。 在Kivy应用程序中,你必须避免使用长循环、死循环或睡眠(sleeping),如下代码是需要避免的: ``` python while True: animate_something() time.sleep(.10) ``` 当你运行上面的代码,则你的程序永远无法退出该循环,要预防Kivy做类似的事情。结果将会看到一个无法交互的黑色的窗口。正确的方式的,你需要*定制(schedule)*你的animate_somthing()函数重复调用。 ##### (一)重复事件 你可以使用schedule_interval()每隔X时间调用一个函数或方法,下面是一个例子,每隔1/30秒调用一次my_callback函数: ```python def my_callback(dt): print 'my callback is called', dt Clock.schedule_interval(my_callback, 1/30.) ``` 你有两种方法来取消前面定制的事件,第一种是: Clock.unschedule(my_callback) 或者你在你的回调函数中返回False,那么你的事件将会自动取消: ```python 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秒后: ```python 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函数中又调用了该回调函数: ```python 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()方法需要迭代时钟的弱引用列表,目的是找到你的回调函数并移除它。替代的方法是使用出发器: ```python trigger = Clock.create_trigger(my_callback) #随后 trigger() ``` 每次你调用trigger,它会为你的回调函数定制一个信号调用,如果已经被定制,则不会重新定制。 #### 三、部件事件 每个部件都有两个默认的事件类型: * **属性事件(Property event)**:如果你的部件改变了位置或尺寸,则事件被触发。 * **部件定义事件(Widget-defined event)**:当一个按钮被按下或释放时,事件被触发。 #### 四、自定义事件 为了创建一个自定义事件,你需要在一个类中注册事件名,并创建一个同名的方法: ```python 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列表中接收到所有的参数。例如: ```python 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()文档](https://kivy.org/docs/api-kivy.event.html#kivy.event.EventDispatcher.bind) #### 六、属性介绍 属性是一个很好的方法用来定义事件并绑定它们。本质上来说,当你的对象的特征值发生变化时,它们创造事件,所有的引用特征值的属性都会自动更新。 有不同类型的属性来描述你想要处理的数据类型。 * StringProperty * NumericProperty * BoundedNumericProperty * ObjectProperty * DictProperty * ListProperty * OptionProperty * AliasProperty * BooleanProperty * ReferenceListProperty #### 七、声明属性 为了声明属性,你必须在类的级别进行。当你的对象被创建时,该类将会进行实际特征值的初始化。特写属性不是特征值,它们是基于你的特征值创建事件的机制。 ```python class MyWidget(Widget): text = StringProperty('') ``` 当重载__init__时,总是接受**kwargs参数并使用super()调用父类的__init__方法: def __init__(self, **kwargs): super(MyWidget, self).__init__(**kwargs) #### 八、发送属性事件 Kivy的属性,默认提供一个on_<property_name>事件。当属性值改变时该事件被调用。 >注意,如果新的属性值等于当前值,该事件不会被调用。 例如: ```python 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行: ```python 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行: ```python 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) 例如,考虑以下代码: ```python 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() ``` 运行结果如下: ![property_events_binding](http://ww1.sinaimg.cn/large/577d3ebejw1f0yohywgvmj20jd095jrh.jpg) 我们的定制按钮没有可视的表述,因此显示一个黑块。你能触摸或点击它以在控制台查看输出。 #### 九、混合属性 当定义一个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事件被发送。 ### 下节预告:编程向导4.6输入管理