四、编程向导(4.5事件和属性)

编程向导4.5事件和属性

事件是Kivy编程里面一个重要的部分。对于有GUI开发经验的人来说也许不是那么让人惊奇,但对于初学者是一个重要的概念。一旦你理解了事件如何工作、如何绑定,你将会在Kivy到处发现它们。它们使你想利用Kivy实现任何的行为变得很容易。

下面的插图显示了在Kivy框架中事件如何被处理。

events and properties

一、介绍事件发送

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

运行结果如下:

property_events_binding

我们的定制按钮没有可视的表述,因此显示一个黑块。你能触摸或点击它以在控制台查看输出。

九、混合属性

当定义一个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输入管理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容