事件和属性

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

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

事件调度器简介

框架最重要的基类之一是EventDispatcher类。此类允许您注册事件类型,并将它们分派给感兴趣的各方(通常是其他事件分派器)。Widget,AnimationClock 类是事件调度程序的示例。

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

主循环

如上图所示,Kivy 有一个主循环。这个循环在应用程序的整个生命周期中都在运行,只有在退出应用程序时才会退出。

在循环内部,在每次迭代中,事件都是从用户输入、硬件传感器或几个其他来源生成的,并且帧被渲染到显示器上。

您的应用程序将指定回调(稍后会详细介绍),它们由主循环调用。如果回调花费的时间太长或根本没有退出,则主循环被破坏并且您的应用程序不再正常运行。

在 Kivy 应用程序中,你必须避免长/无限循环或休眠。例如,以下代码同时执行了这两种操作:

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

当你运行它时,程序永远不会退出你的循环,阻止 Kivy 做所有其他需要做的事情。结果,您所看到的只是一个您无法与之交互的黑色窗口。相反,您需要“安排”您的animate_something()函数被重复调用。

安排一个重复的事件

您可以使用 . 每秒 X 次调用一个函数或方法 schedule_interval()。以下是每秒调用名为 my_callback 的函数 30 次的示例:

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

您可以通过多种方式取消之前安排的活动。一,是使用cancel()or 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)

这将在一秒钟内调用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 = Clock.create_trigger(my_callback)
# later
trigger()

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

小部件事件

小部件有两种默认类型的事件:

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

  • 小部件定义的事件:例如,当按钮被按下或释放时,将触发一个事件。

有关如何管理和传播小部件触摸事件的讨论,请参阅小部件触摸事件冒泡部分。

创建自定义事件

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

请参阅以下示例:

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()有关如何附加回调的更多示例,请参阅方法文档。

属性介绍

属性是定义事件并绑定到它们的绝佳方式。本质上,它们会产生事件,以便当您的对象的某个属性发生更改时,所有引用该属性的属性都会自动更新。

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

财产声明

要声明属性,您必须在类级别声明它们。然后,该类将在创建对象时执行实例化真实属性的工作。这些属性不是属性:它们是根据您的属性创建事件的机制:

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>事件。

例如,考虑以下代码:

 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])

我们定义type 的pressedListProperty属性,给它一个默认值[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()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)

例如,考虑以下代码:

 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))

如果按原样运行代码,您会注意到控制台中有两条打印语句。一个来自CustomBtn类内部调用的on_pressed事件,另一个来自我们绑定到属性更改的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)

第一个参数是定义属性的类的实例。

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

这是从上面的代码片段派生的完整示例,您可以使用它复制并粘贴到编辑器中进行试验。

 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()

运行上面的代码将为您提供以下输出:

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

复合属性

定义 时AliasProperty,您通常会自己定义一个 getter 和一个 setter 函数。在这里,您需要定义何时使用绑定参数调用 getter 和 setter 函数。

考虑以下代码。

 cursor_pos = AliasProperty(_get_cursor_pos, None,
                            bind=('cursor', 'padding', 'pos', 'size',
                                  'focus', 'scroll_x', 'scroll_y',
                                  'line_height', 'line_spacing'),
                            cache=True)
 '''Current position of the cursor, in (x, y).

 :attr:`cursor_pos` is an :class:`~kivy.properties.AliasProperty`,
 read-only.
 '''

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

末尾的 bind 参数定义了当bind=参数中使用的任何属性更改时调度on_cursor_pos事件。

Last updated