Edited at

Kivyの基本 Property


KivyのProperty

 Kivyのpropertyは一言で表すなら監視の仕組みを持った属性だと思います。あらかじめ関数を結び付けておくと、属性が変化した時にその関数を呼び出してくれるのです。Kivyのwidget群の色んな属性(大きさ,位置,テキスト,他色々)がこれで実装されているので、いつでも自由に関数を引っ掛けて属性の変化を察知することができるようになってます。


変化を監視

監視の仕方はEventDispatcherのeventへの関数の結びつけと似ていて、プロパティの持ち主.bind(監視したいプロパティの名前=プロパティが変化した時に呼びたい関数)とする事でできます。(勿論fbind()も可)

from kivy.factory import Factory

button = Factory.Button(text='Press Me')
button.bind(
text=lambda button, value: print(f'textの値が{value}に変わりました'),
x=lambda button, value: print(f'xの値が{value}に変わりました'),
)

print('現在のtext:', button.text)
print('現在のx:', button.x)
print('-------------')
button.text = 'Hoge'
button.x = 100
button.x = 0

現在のtext: Press Me

現在のx: 0
-------------
textの値がHogeに変わりました
xの値が100に変わりました
xの値が0に変わりました

propertyを書き換えた時に関数が呼び出されてるのが確認できます。


実践例

少し実用的な例を見てみます。以下のようにLabelとSliderがあって

09_00.png

Sliderを動かす事で文字の大きさを変えられるようにしたいとします。

09_01.png

これを行うコードが以下で

from kivy.factory import Factory

from kivy.app import runTouchApp

label = Factory.Label(text='Hello Kivy')
slider = Factory.Slider(value=15, min=5, max=200, size_hint_y=.3)
slider.bind(value=label.setter('font_size')) # A
root = Factory.BoxLayout(orientation='vertical')
root.add_widget(label)
root.add_widget(slider)

runTouchApp(root)

Sliderはvalueというpropertyを持っていて、ユーザーがSliderを動かした時にこの値が変わるようになっています。なのでこのpropertyを監視して、変化が起きた時にLabelのfont_sizeを書き換えてやればSliderと文字の大きさを連動させられます(A行)。setter()ですがこれは関数を簡単に用意するための物で、もしA行をsetter()を使わずに書くなら以下のようになります。

slider.bind(value=lambda slider, value: setattr(label, 'font_size', value))

またKv言語を用いた場合は以下のようになります。

from kivy.lang import Builder

from kivy.app import runTouchApp

root = Builder.load_string('''
BoxLayout:
orientation: 'vertical'
Label:
text: 'Hello Kivy'
font_size: slider.value # B
Slider:
id: slider
value: 15
min: 5
max: 200
size_hint_y: .3
'''
)

runTouchApp(root)

Kivyのpropertyが本領を発揮するのは実はKv言語を用いた時で、B行のように書いただけでbind()に相当するものが裏で勝手に行われます。Kv言語を用いない場合と比べてどちらの方が読みやすいコードか、訊くまでもないでしょう。


default handler

EventDispatcherのeventのdefault handlerに相当する物もあります。

from kivy.factory import Factory

class MyButton(Factory.Button):
def on_text(self, button, value):
print(f'textの値が {value} に変わりました')

print('--------')
button = MyButton(text='初期値')
print('--------')
button.text = '書き換え後'

--------

textの値が 初期値 に変わりました
--------
textの値が 書き換え後 に変わりました

bind()の時とは違いon_が付き、また初期化時にも呼び出されるのが注意点。


同じ値を書き込んだ時は関数は呼ばれない

また以下のように現在の値と同じ値を書き込んだ場合は関数が呼ばれていない事も分かります。

from kivy.factory import Factory

button = Factory.Button()
button.bind(x=lambda button, value: print(f'xの値が{value}に変わりました'))
button.x = 100
print('--------')
button.x = 100 # 同じ値を書き込む

xの値が100に変わりました

--------

これは賢い設計で例え以下のような循環依存があっても関数の無限呼び出しにはおちいらない事を意味します。

from kivy.factory import Factory

button1 = Factory.Button()
button2 = Factory.Button()
button1.bind(x=button2.setter('x')) # A
button2.bind(x=button1.setter('x')) # B

button1.x = 100

A行はbutton1xが変化した時にbutton2xを同じ値に書き換えるようにしていて、B行はその逆です。なのでbutton1.xの変化がbutton2.xの変化を引き起こし、button2.xの変化がbutton1.xの変化を引き起こし、それがまたbutton2.xの変化を引き起こし...無限循環におちいるかに思えますが、上記に書いた賢い設計のおかげでそうはなりません。


propertyを作る

今度は自分でpropertyを作ってみます。

from kivy.event import EventDispatcher

from kivy.properties import StringProperty, NumericProperty

class Person(EventDispatcher):

name = StringProperty()
age = NumericProperty()

Kivyのpropertyは必ずEventDispatcherの派生classのclass属性の位置で作らないといけません。(widgetもEventDispatcherの派生class)。class属性の位置で作ってはいますが、instance毎の属性として振る舞います。以前そのままclass属性として扱おうとして失敗している人をよく見かけたので注意が必要と思います。(もしどのような仕組みで動いているか気になるのなら、python ディスクリプタでググるといいと思います)。またpropertyの名前は必ずアンダーバー(_)か小文字で始め、かつon_で始まらないようにした方が良いです。

これで以下のように利用できるようになります。

from kivy.event import EventDispatcher

from kivy.properties import StringProperty, NumericProperty

class Person(EventDispatcher):

name = StringProperty()
age = NumericProperty()

def on_name(self, __, value):
print(f'nameの値が{value}に書き換えられました')

person = Person(name='Bob', age=20)
person.bind(age=lambda __, value: print(f'ageの値が{value}に書き換えられました'))
print('--------')
person.name = 'Ema'
person.age = 30

nameの値がBobに書き換えられました

--------
nameの値がEmaに書き換えられました
ageの値が30に書き換えられました


propertyオブジェクトそのものを扱う

時折propertyを書き換えずに結び付けられた関数を呼びたい事もあります。そんな時は

from kivy.uix.button import Button

button = Button(text='Hello Kivy')
button.bind(text=lambda button, value: print(f'textの値が {value} に書き換えられました'))

prop = Button.text # A
prop.dispatch(button)

textの値が Hello Kivy に書き換えられました

とします。ここでA行で得ている物は propertyを制御する為のオブジェクト であって、button.textという式で得られる propertyに入っている値 とは別物です。

from kivy.uix.button import Button

button = Button(text='Hello Kivy')

print(button.text)
print(type(button.text))
print(Button.text)
print(type(Button.text))

Hello Kivy

<class 'str'>
<StringProperty name=text>
<class 'kivy.properties.StringProperty'>

またpropertyを制御する為のオブジェクトはinstance経由でも得られ、class経由で得たものと同一です。

button = Button(text='Hello Kivy')

prop = Button.text # class経由で得る
prop2 = button.property('text') # instance経由で得る
print(prop is prop2)

True

なのでこのオブジェクトは特定のinstanceとの結びつきを持たないといえます。その為このオブジェクトを使ってButtonのinstanceを操作する際には一々methodにinstanceを渡さないといけません。

button = Button(text='Hello Kivy')

prop = button.property('text')
print(prop.get(button)) # 値を読み込む
prop.set(button, 'New Value') # 値を書き込む
print(button.text)

Hello Kivy

New Value

また、特定のinstanceとの結びつきを持たないという事はどのinstanceの操作をしてもいいはずです。

button1 = Button(text='A')

button2 = Button(text='B')
prop = button1.property('text')
print(prop.get(button1))
print(prop.get(button2))

A

B