事件与属性

事件是Kivy编程中的一个重要组成部分。对于有GUI开发经验的人来说,这可能并不令人惊讶,但对于新手来说,这是一个重要的概念。一旦你理解了事件是如何工作的以及如何绑定它们,你就会在Kivy中到处看到它们的身影。它们使得在Kivy中构建你想要的任何行为变得容易。

下图展示了Kivy框架中事件的处理方式。

../_images/Events.png

事件分发器简介

框架中最重要的基类之一是 EventDispatcher 类。该类允许您注册事件类型,并将其分发给感兴趣的各方(通常是其他事件分发器)。 WidgetAnimationClock 类是事件分发器的示例。

EventDispatcher 对象依赖主循环来生成和处理事件。

主循环

如上图所示,Kivy 有一个 主循环。该循环在应用程序的整个生命周期内运行,仅在退出应用程序时结束。

在循环内部,每次迭代时,事件由用户输入、硬件传感器或其他一些来源产生,并将帧渲染到显示器上。

你的应用程序将指定回调函数(稍后会详细讨论),这些回调由主循环调用。如果某个回调执行时间过长或根本不退出,主循环就会被破坏,你的应用将无法正常工作。

在Kivy应用中,必须避免长时间/无限循环或休眠。例如,以下代码同时涉及了这两种情况:

while True:
    animate_something()
    time.sleep(.10)

当你运行这段代码时,程序将永远不会退出你的循环,从而阻止Kivy执行所有其他需要完成的任务。结果,你只会看到一个黑色窗口,并且无法与之交互。相反,你需要“调度”你的``animate_something()``函数,使其被反复调用。

调度重复事件

你可以使用 schedule_interval() 在指定的时间间隔内调用一个函数或方法。以下是一个示例,每1/30秒(即每秒30次)调用名为 my_callback 的函数:

def my_callback(dt):
    print('My callback is called', dt)
event = Clock.schedule_interval(my_callback, 1 / 30)

你有多种方式来取消之前安排的事件。一种是使用 cancel()unschedule():

event.cancel()

或:

Clock.unschedule(event)

或者,您可以在回调中返回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)

这将在1秒后调用``my_callback``。第二个参数是等待调用函数前的时间量,单位为秒。然而,通过为第二个参数设置特殊值,你可以实现其他一些效果:

  • 如果X大于0,回调将在X秒后被调用。

  • 如果X为0,回调将在下一帧之后被调用。

  • 如果 X 为 -1,回调将在下一帧之前被调用。

-1 主要用于你已经在某个计划事件中,并且希望在下一帧发生之前安排一次调用的情况。

重复函数调用的第二种方法是先使用 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() 时,回调会每秒被调用一次。

触发事件

有时您可能希望将某个函数安排为仅在下一帧调用一次,以避免重复调用。您可能会尝试这样做:

# First, schedule once.
event = Clock.schedule_once(my_callback, 0)

# Then, in another place you will have to unschedule first
# to avoid duplicate call. Then you can schedule again.
Clock.unschedule(event)
event = Clock.schedule_once(my_callback, 0)

这种触发器的编程方式成本较高,因为即使事件已经完成,你仍会调用unschedule。此外,每次都会创建一个新事件。建议改用触发器(trigger):

trigger = Clock.create_trigger(my_callback)
# later
trigger()

每次调用trigger()时,它都会安排一次回调的调用。如果该调用已被安排,则不会重新安排。

控件事件

控件有2种默认类型的事件:

  • 属性事件:当您的部件更改其位置或大小时,会触发一个事件。

  • 控件定义的事件:例如,当按钮被按下或释放时,会触发相应的事件。

关于控件触摸事件的管理与传播机制的讨论,请参阅 控件触摸事件冒泡 部分。

创建自定义事件

要创建带有自定义事件的事件分发器,您需要在类中注册事件的名称,然后创建一个同名的方法。

请参考以下示例:

class MyEventDispatcher(EventDispatcher):
    def __init__(self, **kwargs):
        self.register_event_type('on_test')
        super(MyEventDispatcher, self).__init__(**kwargs)

    def do_something(self, value):
        # when do_something is called, the 'on_test' event will be
        # dispatched with the 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)


ev = MyEventDispatcher()
ev.bind(on_test=my_callback)
ev.do_something('test')

请参阅 kivy.event.EventDispatcher.bind() 方法的文档,以获取更多关于如何附加回调的示例。

属性(Properties)简介

属性是定义事件并绑定到它们的绝佳方式。本质上,它们会产生事件,使得当你的对象属性发生变化时,所有引用该属性的属性都会自动更新。

有不同类型的属性来描述你想要处理的数据类型。

属性的声明

要声明属性,你必须在类级别进行声明。类会在你的对象创建时自动实例化真实的属性。这些属性并非普通的属性:它们是基于你的属性创建事件的机制。

class MyWidget(Widget):

    text = StringProperty('')

当重写 __init__ 时,始终 接受 **kwargs,并使用 super() 调用父类的 __init__ 方法,传入你的类实例::

def __init__(self, **kwargs):
    super(MyWidget, self).__init__(**kwargs)

调度属性事件

Kivy 属性默认提供一个 on_<property_name> 事件。当属性的值发生变化时,该事件会被调用。

备注

如果属性的新值与当前值相等,则不会调用 on_<property_name> 事件。

例如,考虑以下代码:

 1 class CustomBtn(Widget):
 2
 3     pressed = ListProperty([0, 0])
 4
 5     def on_touch_down(self, touch):
 6         if self.collide_point(*touch.pos):
 7             self.pressed = touch.pos
 8             return True
 9         return super(CustomBtn, self).on_touch_down(touch)
10
11     def on_pressed(self, instance, pos):
12         print('pressed at {pos}'.format(pos=pos))

在上述代码的第3行:

pressed = ListProperty([0, 0])

我们定义了一个类型为 ListPropertypressed 属性,并为其设置了默认值 [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)

我们重写了Widget类的:meth:`on_touch_down`方法。在这里,我们检查`touch`是否与我们的widget发生碰撞。

如果触摸事件落在我们的控件内部,我们将 pressed 的值改为 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)

例如,考虑以下代码:

 1 class RootWidget(BoxLayout):
 2
 3     def __init__(self, **kwargs):
 4         super(RootWidget, self).__init__(**kwargs)
 5         self.add_widget(Button(text='btn 1'))
 6         cb = CustomBtn()
 7         cb.bind(pressed=self.btn_pressed)
 8         self.add_widget(cb)
 9         self.add_widget(Button(text='btn 2'))
10
11     def btn_pressed(self, instance, pos):
12         print('pos: printed from root widget: {pos}'.format(pos=.pos))

如果你直接运行这段代码,会在控制台中看到两条打印语句。一条来自 CustomBtn 类内部调用的 on_pressed 事件,另一条来自我们绑定到属性变化的 btn_pressed 函数。

两个函数都被调用的原因很简单:绑定并不意味着覆盖。同时拥有这两个函数是多余的,通常你应当只使用其中一种方法来监听/响应属性变化。

你还应注意传递给`on_<property_name>`事件或绑定到该属性的函数的参数。

def btn_pressed(self, instance, pos):

第一个参数是 self,它是定义此函数的类的实例。你可以使用内联函数,如下所示:

1 cb = CustomBtn()
2
3 def _local_func(instance, pos):
4     print('pos: printed from root widget: {pos}'.format(pos=pos))
5
6 cb.bind(pressed=_local_func)
7 self.add_widget(cb)

第一个参数将是定义该属性的类的`instance`。

第二个参数是 value,即属性的新值。

以下是完整的示例,由上述代码片段整合而成,您可以直接复制粘贴到编辑器中尝试运行。

 1 from kivy.app import App
 2 from kivy.uix.widget import Widget
 3 from kivy.uix.button import Button
 4 from kivy.uix.boxlayout import BoxLayout
 5 from kivy.properties import ListProperty
 6
 7 class RootWidget(BoxLayout):
 8
 9     def __init__(self, **kwargs):
10         super(RootWidget, self).__init__(**kwargs)
11         self.add_widget(Button(text='btn 1'))
12         cb = CustomBtn()
13         cb.bind(pressed=self.btn_pressed)
14         self.add_widget(cb)
15         self.add_widget(Button(text='btn 2'))
16
17     def btn_pressed(self, instance, pos):
18         print('pos: printed from root widget: {pos}'.format(pos=pos))
19
20 class CustomBtn(Widget):
21
22     pressed = ListProperty([0, 0])
23
24     def on_touch_down(self, touch):
25         if self.collide_point(*touch.pos):
26             self.pressed = touch.pos
27             # we consumed the touch. return False here to propagate
28             # the touch further to the children.
29             return True
30         return super(CustomBtn, self).on_touch_down(touch)
31
32     def on_pressed(self, instance, pos):
33         print('pressed at {pos}'.format(pos=pos))
34
35 class TestApp(App):
36
37     def build(self):
38         return RootWidget()
39
40
41 if __name__ == '__main__':
42     TestApp().run()

运行上述代码将得到以下输出:

../_images/property_events_binding.png

我们的CustomBtn没有视觉表示,因此显示为黑色。您可以触摸/点击黑色区域,在控制台上查看输出。

复合属性

在定义 AliasProperty 时,通常需要自行定义 getter 和 setter 函数。在这里,你需要通过 bind 参数来决定何时调用这些 getter 和 setter 函数。

考虑以下代码。

 1 cursor_pos = AliasProperty(_get_cursor_pos, None,
 2                            bind=('cursor', 'padding', 'pos', 'size',
 3                                  'focus', 'scroll_x', 'scroll_y',
 4                                  'line_height', 'line_spacing'),
 5                            cache=True)
 6 '''Current position of the cursor, in (x, y).
 7
 8 :attr:`cursor_pos` is an :class:`~kivy.properties.AliasProperty`,
 9 read-only.
10 '''

这里的 cursor_pos 是一个 AliasProperty,它使用 getter _get_cursor_pos,而 setter 部分设置为 None,这意味着这是一个只读属性。

末尾的bind参数定义了当`bind=`参数中使用的任何属性发生变化时,会触发`on_cursor_pos`事件。