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を書き換えた時に関数が呼び出されてるのが確認できます。
実用例1
少し実用的な例を見てみます。以下のようにLabelとSliderがあって
Sliderを動かす事で文字の大きさを変えられるようにしたいとします。
これを行うコードが以下で
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言語を用いない場合と比べてどちらの方が読みやすいコードか訊くまでもないでしょう。
実用例2
次は画面の縦横比によって柔軟にwidgetの配置を変えてみます。具体的には画面が 縦長なら縦に 横長なら横に widgetを並べます。
from kivy.app import runTouchApp
from kivy.lang import Builder
root = Builder.load_string('''
BoxLayout:
orientation: 'vertical' if self.width < self.height else 'horizontal'
Label:
text: 'A'
font_size: 100
Label:
text: 'B'
font_size: 100
''')
runTouchApp(root)
Kv言語を用いない場合
from kivy.factory import Factory
from kivy.app import runTouchApp
label_a = Factory.Label(text='A', font_size=100)
label_b = Factory.Label(text='B', font_size=100)
root = Factory.BoxLayout()
root.add_widget(label_a)
root.add_widget(label_b)
def update_orientation(boxlayout, size):
boxlayout.orientation = 'vertical' if size[0] < size[1] else 'horizontal'
root.bind(size=update_orientation)
runTouchApp(root)
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行はbutton1
のx
が変化した時にbutton2
のx
を同じ値に書き換えるようにしていて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 descriptor
でググるといいと思います)。また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を制御する為のオブジェクト(descriptor) であって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
eventとの違い
- eventの場合は結び付けられた関数が真を返すと後続の関数が呼ばれなくなるのでした。しかしながらpropertyではそのようなことは起きません。
- eventの場合は結び付けられた順とは逆に関数が呼ばれますがproeprtyでは結びつけられた順に呼ばれます。