Kivyの基本部品 Property

# KivyのProperty

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

### 変化を監視

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

```python3
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
現在のtext: Press Me
現在のx: 0
-------------
textの値が Hoge に変わりました
xの値が 100 に変わりました
xの値が 0 に変わりました
```

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

### 実用例1

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

![09_00.png](https://qiita-image-store.s3.amazonaws.com/0/144673/f4fdef34-b725-c47f-f27e-461b6d461ecb.png)

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

![09_01.png](https://qiita-image-store.s3.amazonaws.com/0/144673/1321e0ef-e220-16e6-17db-9a5808139d31.png)

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

```python3
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()`を使わずに書くなら以下のようになります。

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


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

```python
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を並べます。

```python
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)
```

![Screenshot_2021-03-18_19-41-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144673/2970a766-e532-6c5d-52b6-bdd0c85ce36a.png)
![Screenshot_2021-03-18_19-41-27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144673/a82a45da-2f76-8edf-e214-a6b71d1ab903.png)

Kv言語を用いない場合

```python
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`に相当する物もあります。

```python
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の値が 初期値 に変わりました
--------
textの値が 書き換え後 に変わりました
```

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

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

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

```python
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  # 同じ値を書き込む
```

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

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

```python
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を作ってみます。

```python
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_`で始まらないようにした方が良いです。

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

```python
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
```

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

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

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

```python
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
textの値が Hello Kivy に書き換えられました
```

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

```python
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))
```

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

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

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

```text
True
```

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

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

```text
Hello Kivy
New Value
```

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

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

```text
A
B
```

### eventとの違い

- eventの場合は結び付けられた関数が真を返すと後続の関数が呼ばれなくなるのでした。しかしながらpropertyではそのようなことは起きません。
- eventの場合は結び付けられた順とは逆に関数が呼ばれますがproeprtyでは結びつけられた順に呼ばれます。