Kivy A to Z -- Kivy之Properties

时间:2022-11-18 15:57:40

在像VB.netC#这样的语言中,都有Property的概念,可以通过get获取属性的值,也可以通过set来设置一个属性的值,Kivy也提供了类似的功能。在Kivy里,提供了以下的属性类:

• StringProperty

• NumericProperty

• BoundedNumericProperty

• ObjectProperty

• DictProperty

• ListProperty

• OptionProperty

• AliasProperty

• BooleanProperty

• ReferenceListProperty

 

看名字应该就能对这些属性有一个初步的印象。

 

先来看下Property是怎样使用的。

 

import kivy

kivy.require('1.8.0')

 

from kivy.app import App

from kivy.properties import BooleanProperty

from kivy.uix.button import Button

from kivy.uix.boxlayout import BoxLayout

 

class MyButton(Button):

    focus = BooleanProperty(False)

    def __init__(self,**kwargs):

        super(MyButton,self).__init__(**kwargs)

    def set_focus(self):

        self.focus = True

    def on_focus(self,instance,focused):

        print '++++++++++++++on_focus:',focused

 

class BoxLayoutTest(BoxLayout):

    def __init__(self,**kargs):

        super(BoxLayoutTest,self).__init__(**kargs)

        self.btn = MyButton(text='press me',on_press=self.on_press)

        self.add_widget(self.btn)

 

    def on_press(self,control):

        self.btn.set_focus()

 

class MyApp(App):

    def build(self):

        return BoxLayoutTest()

    def on_pause(self):

        return True

 

if __name__ == '__main__':

    MyApp().run()

 

 

运行这个例子,连续按下Button,将会得到下面的输出:

++++++++++++++on_focus:True

 

并且只会在第一次按下Button的时候输出。

 

再来分析下代码:

1、在MyButton里定义了一个为BooleanProperty的类变量:

focus = BooleanProperty(False)

2、然后定义了on_focus方法。

3、在BoxLayoutTeston_press方法中调用了MyButton.set_focus,给MyButton.focus赋值为True,这将会触发MyButton.on_focus方法

 

如果对Python没有比较深入的了解,可能会产生这样的疑问:怎么给一个变量赋值能够触发一个方法呢。其实这里的focus就是类似VB.net,C#里的属性了。为了解析这个现象,我们

 

来看下下面的例子:

Test.py

 

class Prop(object):
    def __init__(self):
        self._name = ''
    def __set__(self, obj, val):
        print '__set__'
        self._name = val
    def __get__(self, obj, objtype):
        print '__get__'
        if obj is None:
            return None
        return self._name
 
class Widget(object):
    p = Prop()
    
w = Widget()
w.p = 'abc'
print '--------------------------'
print w.p


 

运行这个例子将会得到下面的输出:

__set__
--------------------------
__get__
abc


 

从这个例子来看,Python是完全支持类似C#的属性的,可以把实现一个“属性类”。但是,这里要注意下:

1、属性类必须继承自object,否则将不会触发__get__,__set__

2、属性类必须实现__get__和__set__方法。

 

了解了属性的应用和基本的实现原理,接下来深入分析下Kivy里的属性是怎么实现的,以解开上面的疑问:怎么给一个变量赋值就能够触发一个已经定义好的方法呢?

 

我们找到kivy/properties.pyx,代码是用Cython写的,所有的Property都在这个文件中实现。我们重点关注Property类:这是所有Property的基类。

这里,先了解二个事实:

1、首先,Property都是在Widget中使用的。

2、第二,在Cython里,自定义的类都是默认继承自object的。

 

接下来,先看下_event.py中的EventDispatcher.__cinit__方法的实现:

        cdef dict cp = cache_properties

...

        if __cls__ not in cp: #查找类的Property是否已经在缓存里了

            attrs_found = cp[__cls__] = {}

            attrs = dir(__cls__)

            for k in attrs:

                uattr = getattr(__cls__, k, None)

                if not isinstance(uattr, Property):

                    continue

                if k == 'touch_down' or k == 'touch_move' or k == 'touch_up':

                    raise Exception('The property <%s> have a forbidden name' % k)

                attrs_found[k] = uattr

        else:

            attrs_found = cp[__cls__]

 

        # First loop, link all the properties storage to our instance

        for k in attrs_found:

            attr = attrs_found[k]

            attr.link(self, k)

 

        # Second loop, resolve all the reference

        for k in attrs_found:

            attr = attrs_found[k]

            attr.link_deps(self, k)

 

        self.__properties = attrs_found

 

首先,查找类的Property是否已经在缓存里了,如果是,直接取缓存,否则查找类的所有Property,保存到attrs_found

接下来很重要的一步:调用attr.link(self, k)将属性绑定到类的实例。

 for k in attrs_found:

            attr = attrs_found[k]

            attr.link(self, k)

 

按下来看下Property.link的实现:

    cpdef link(self, EventDispatcher obj, str name):

        cdef PropertyStorage d = PropertyStorage()

        self._name = name

        obj.__storage[name] = d

        self.init_storage(obj, d)

 

    cdef init_storage(self, EventDispatcher obj, PropertyStorage storage):

        storage.value = self.convert(obj, self.defaultvalue)

        storage.observers = []

 

这里创建了一个PropertyStorage 对象,并对其进行初始化,PropertyStorage 用于保存Property的值以及Property触发时要调用的方法。这里的name即是Property的变量名称,是一个字符串类型,比如上面的例子中的focus。

接下来把这个PropertyStorage 对象保存到EventDispatcher 实例的dict类型的__storage中。PropertyStorage.observers用于保存当值改变是要调用的方法,因为我们看到这个类定义了两个方法:

    def __set__(self, EventDispatcher obj, val):

        self.set(obj, val)

    def __get__(self, EventDispatcher obj, objtype):

        if obj is None:

            return self

        return self.get(obj)

这样,在对Property赋值时将会调用Property.set方法:

cpdef set(self, EventDispatcher obj, value):

        '''Set a new value for the property.

        '''

        cdef PropertyStorage ps = obj.__storage[self._name]

        value = self.convert(obj, value)

        realvalue = ps.value

        if self.compare_value(realvalue, value):

            return False

 

        try:

            self.check(obj, value)

        except ValueError as e:

            if self.errorvalue_set == 1:

                value = self.errorvalue

                self.check(obj, value)

            elif self.errorhandler is not None:

                value = self.errorhandler(value)

                self.check(obj, value)

            else:

                raise e

 

        ps.value = value

        self.dispatch(obj)

        return True

 

1、第一行代码取出保存在__storage中的PropertyStorage,也就是在link时创建的:

cdef PropertyStorage ps = obj.__storage[self._name]

 

2、接下来这个方法通过调用compare_value检测新值是否与之前的相同,如果相同,直接返回。

3、然后,将值保存到PropertyStorage中:ps.value = value

4、最后,调用self.dispatch,这将会调用保存在PropertyStorage.observers中的所有方法。

 

 

到这里,我们已经把整个流程梳理了一遍,但是我们还是没有看到on_focus是怎么被调用的,在init_storage时,storage.observers = []

而on_focus方法其实是通过Property.bind方法来添加到observers 中去的,请看下面的bind的实现:

    cpdef bind(self, EventDispatcher obj, observer):

        '''Add a new observer to be called only when the value is changed.

        '''

        cdef PropertyStorage ps = obj.__storage[self._name]

        if observer not in ps.observers:

            ps.observers.append(observer)

 

但是我们并没有看到在哪里有调用Property.bind方法。那么bind方法是在什么地方调用的呢?

我们来到EventDispatcher.__init__这个类的初始化函数:

 

   __cls__ = self.__class__

        if __cls__ not in cache_events_handlers:

            event_handlers = []

            for func in dir(self):

                if func[:3] != 'on_':

                    continue

                name = func[3:]

                if name in properties:

                    event_handlers.append(func)

            cache_events_handlers[__cls__] = event_handlers

        else:

            event_handlers = cache_events_handlers[__cls__]

        for func in event_handlers:

            self.bind(**{func[3:]: getattr(self, func)})

这个函数将会查找所有的以on_开头的方法,并对其调用bind方法:
            self.bind(**{func[3:]: getattr(self, func)})

再来看下bind的实现:

        cdef Property prop

        for key, value in kwargs.iteritems():

            if key[:3] == 'on_':

                if key not in self.__event_stack:

                    continue

                # convert the handler to a weak method

                handler = WeakMethod(value)

                self.__event_stack[key].append(handler)

            else:

                prop = self.__properties[key]

                prop.bind(self, value)

 

看下红字部分,这里就是调用Property.bind的代码了。

 

OK,that’s all

 

全文字,可能不便于理解,但是整个流程是讲清楚了,有机会在用图来总结下。