在像VB.net,C#这样的语言中,都有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、在BoxLayoutTest的on_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
全文字,可能不便于理解,但是整个流程是讲清楚了,有机会在用图来总结下。