23
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Kivyの基本部品 Property

Last updated at Posted at 2016-10-19

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があって

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言語を用いない場合と比べてどちらの方が読みやすいコードか訊くまでもないでしょう。

実用例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)

Screenshot_2021-03-18_19-41-05.png
Screenshot_2021-03-18_19-41-27.png

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行は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 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では結びつけられた順に呼ばれます。
23
25
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?