KivyではPythonコードからウィジェットを参照するためのidという概念があります。この記事ではkv言語のidの使い方とidのスコープ(有効範囲)について解説します。
idの基本
idを使用するとkvファイルで定義したウィジェットの情報を参照・取得できます。idはウィジェット共通のプロパティとしてkvファイル内でのみ定義可能で、Pythonコード内では定義できません。
同じkvファイルではidは一意の値にした方がいいです。またidには有効範囲があります。有効範囲は一つのツリー内に限られていて、スコープ内とスコープ外ではidの参照方法が異なります。
idは全てのウィジェットに定義する必要はありません。全てのidに定義すると管理が大変になってしまうので通常は動的に操作するウィジェットのみ定義した方が良いと思います。
kv言語のidの書き方
idを設定するには下記のように定義します。idはプロパティの中で最初に定義します
id: idの名前
idの名前は英単語にします。シングルクォートやダブルクォートで囲む必要はありません。
Label:
id: mylabel
text: "ラベルのidを設定する"
Button:
id: myButton
text: "ボタンのidを設定する"
Kivyのidの仕組み
kvファイルで設定されたidプロパティは、kvファイルが読み込まれると自動的にids辞書に登録されます。Pythonコードではidsの名前空間を辿ることでidで定義されたウィジェットに参照・変更することが可能になっています。
実際のidsは下記のように登録されています
{'test_label': <WeakProxy to <kivy.uix.label.Label object at 0x0000023637A62F20>>, 'status_label': <WeakProxy to <kivy.uix.label.Label object at 0x0000023637AAD550>>}
idsの参照はタイミングによってはエラーが発生することがあるので注意して下さい。これを理解すためには下記の記事を参照してください。
kvファイル内でidを参照する
kvファイル内でもウィジェットを参照・変更することができます。
自身のウィジェットを参照する
自分自身のウィジェットのプロパティを参照することができます。自身を参照する場合はidを指定する必要はなく、selfキーワードに続けてプロパティを指定するだけです。
self.プロパティ名
Label:
id: mylabel
text: self.text # 自分自身のプロパティを参照
on_touch_down: self.text = "クリックされたよ" # 自分自身を更新
上記のコードでは自身を参照する再帰呼び出しをしていて、Labelがクリックされた時にテキストを更新しています。文字列の中で直接self.textを参照できないので注意してください。
kvファイル内の他のウィジェットを参照する
kvファイル内で他のウィジェットを参照するときは、参照するウィジェットのidの後にウィジェットのプロパティ名を書きます。
参照したいウィジェットのid.プロパティ
Label:
id: label1
text: "label1のテキスト"
Label:
id: label2
text: label1.text # id「label1」のウィジェットを設定
on_touch_down: self.text = f"Label2のテキスト:{label1.text}"
上記のコードではlabel2のラベルのtextプロパティにlabel1のテキストを代入しています。またラベル2をクリックするとテキストが変わります。
Pythonでidsを参照する
Pythonコードでidを設定したウィジェットを参照するには下記のように書きます。
キーワード.ids.id名.ウィジェットのプロパティ
キーワードはクラス内から参照する場合はselfになります。
#クラス内で参照する
# 参照する場合
text = self.ids.mylabel.text
#変更する場合
self.ids.mylabel.text = 'changed text'
上記の場合、Labelウィジェットのtextプロパティを参照しています。
Appサブクラスから参照する場合はself.rootになります。
# Appサブクラスから参照する
# 参照する場合
text = self.root.ids.mylabel.text
#変更する場合
self.root.ids.mylabel.text = 'changed text'
Pythonコードで動的に作成されたウィジェット
Pythonコードでは、kvファイルで定義されていないウィジェットをPythonコードで動的に作成した場合はid
を割り当てることはできません。
動的に作成されたウィジェットとは
元々定義されていたウィジェットを操作することで新たにウィジェットを作り出す事を指します。例えば元々定義していたボタンをクリックすると、違うボタンが作成される、文字列が変化するなど色々なことができます。以下に動的に作成されるウィジェットのサンプルを示します。
example_ids1.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.label import Label
class RootWidget(BoxLayout):
def add_label(self):
self.ids.container.add_widget(Label(text="動的に追加されたラベル"))
print(self.ids) # ids辞書を参照する
class ExampleIds1(App):
def build(self):
return RootWidget()
if __name__ == "__main__":
ExampleIds1().run()
exampleids1.kv
<RootWidget>:
BoxLayout:
id: container
orientation: "vertical"
Button:
text: "ラベルを追加する"
on_press: root.add_label()
このコードはボタンを押す度にラベルが作られます。またidsを確認するためコンソール画面にids辞書が表示されます。
{'container': <WeakProxy to <kivy.uix.boxlayout.BoxLayout object at 0x00000299BF212580>>}
idの定義をしていなので当たり前なんですが(Pythonコードでidは定義できない)、kvファイルに定義したid「container」のみ登録されます。動的に追加されたウィジェットはids辞書に登録できません。
動的に作成されたウィジェットを参照するには
動的に作成されたウィジェットはPythonコードで直接操作することができるのでidは必要ありません。add_widget()
で作成したウィジェットのオブジェクトを操作します。
self.label = Label(text="テキスト")
self.add_widget(self.label)
#↑のウィジェットのtextプロパティを変更
self.label.text = 'changed'
idのスコープ(有効範囲)
idのスコープはウィジェットのツリー内に限定されます。対象ツリー以外のルールではスコープ外となり、通常の方法では参照できません。
スコープ内とスコープ外
下記の場合はMyWidgetがルートウィジェットになりその配下にあるウィジェットはスコープの範囲内になります。
<MyWidget> #ルートウィジェット
∟BoxLayout # これより下のウィジェットはスコープ範囲内
∟Label
∟Label
∟Button
∟Button
複数のルールを定義をした場合は、他のツリーはスコープ外になります。MainWidgetとSubWidgetがある場合、それぞれのツリーでスコープが異なります。
<MainWidget> # ルートウィジェット
∟BoxLayout # MainWidgetのツリー内は同一スコープ
∟Button
∟Button
<SubWidget>
∟BoxLayout # SubWidgetのツリー内は同一スコープ
∟Label
ルートウィジェットのツリーを外側のスコープから操作することができません。ルートウィジェットは他のクラスを自身に配置すれることで他のツリーを操作することができます。クラスはインスタンス化されるとオブジェクトになりKivyではそれをウィジェットとして扱います。
下記の場合は、SubWidgetからMainWidgetを操作することはできません。MainWidgetは自身のツリーにSubWidgetを配置することでSubWidgetツリーのウィジェットを操作することができます。SubWidgetはウィジェットとして扱われるのでBoxLayoutの中にカスタムクラスのウィジェットを配置するということになります。この配置をしないとSubWidgetは孤立した状態になり画面には表示されません。またidを定義しないと名前空間を辿れないので注意してください。
<MainWidget> # ルートウィジェット
∟BoxLayout
∟Button
∟Button
∟SubWidget # SubWidgetをルートウィジェット内に配置することでSubWidgetを操作できる
id: sub_id # idを定義しないと参照できない
<SubWidget>
∟BoxLayout # SubWidgetからMainWidgetのウィジェットを操作することはできない
∟Label
参照方法の違い
pythonコードから参照する場合、同一スコープ内では通常の参照方法が使用できますが、スコープ外ではidsの名前空間を遡って指定する必要があります。
スコープ内の参照
# サブクラス内から参照
self.ids.id名.プロパティ名
# Appサブクラスから参照
self.root.id名.プロパティ名
スコープ外の参照
スコープ外のidを参照するには下記のように名前空間を遡る必要があります。クラスインスタンスのid名はルートウィジェットではない他のクラスルール(サブクラス)になります。このように名前空間で使用するためid定義が必須となります。
# サブクラス内から参照
# self.ids.クラスインスタンスのid名.ids.id名.プロパティ名
self.ids.sub_id.ids.sub_label.text
# Appサブクラスから参照
# self.root.ids.クラスインスタンスのid名.ids.id名.プロパティ名
self.root.ids.sub_id.ids.sub_label.text
idのスコープと参照方法のサンプル
idのスコープを理解すためのサンプルを以下に示します
サンプル 1: idの通常のスコープ
idのスコープは同一ツリー内になります。下記のツリー構成の場合、MyWidgetルートウィジェット配下は全て同じスコープになります。参照方法は前章で説明した通常の参照方法なりますがおさらいのためにもう一度確認しましょう。
<MyWidget>
∟BoxLayout
∟Label
∟Label
∟Button
∟Button
example_ids2.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
class MainWidget(BoxLayout):
def change_label(self):
self.ids.label2.text = "rootからlabel2を更新"
class ExampleIds2(App):
def build(self):
return MainWidget()
def change_label(self):
self.root.ids.label1.text = "appからlabel1を更新"
if __name__ == "__main__":
ExampleIds2().run()
exampleids2.kv
<MainWidget>:
orientation: 'vertical'
Label:
id: label1
text: "ここをクリックして"
on_touch_down: self.text += "\n自己参照をしたよ"
Label:
id: label2
text: f"kvファイルから他のウィジェットを参照:\nボタン1のtextは「{button1.text}」"
Button:
id: button1
text: "appから変更"
on_press: app.change_label()
Button:
text: "rootから変更"
on_press: root.change_label()
MainWidgetツリーの配下であればどのidも参照することができます。またAppクラスを継承したサブクラスExampleIds2からでも参照可能です。Appサブクラスから参照する場合はself.root.ids…になります。
サンプル 2: idのスコープ制限(複数のクラスルール)
複数のクラスルールを定義した場合をみてみましょう。この場合は異なるツリーになるので互いのidはスコープ外になるので通常の方法(通常使う名前空間)では参照できません。またSubWidgetからMainWidgetのウィジェットを参照することはできません。
<MainWidget> # このツリーの配下は参照できる
∟BoxLayout
∟SubWidget # SubWidgetを配置
∟Label
∟Button
∟Button
<SubWidget> # このツリーの配下は参照できるがMainWidgetは参照できない
∟BoxLayout
∟Label
∟Button
example_ids3.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
class SubWidget(BoxLayout):
def update_sub_label(self):
# サブウィジェット以外のスコープは参照できない
# self.ids.main_label.text = "SubWidgetからMain_labelを更新" # これはエラー
# main_widget = self.ids.main_widget
# main_widget.ids.main_label.text = "SubWidgetからSub_labelを更新" # これもエラー
self.ids.sub_label.text = "SubWidgetからsub_labelを更新"
print(f"SubWidget.ids:{self.ids}")
class MainWidget(BoxLayout):
def update_sub_label(self):
# サブウィジェット内のidに直接アクセスはできない
# self.ids.sub_label.text = "MainWidgetからsub_labelを更新" # これはエラー
sub_widget = self.ids.sub_widget # 正しい方法
sub_widget.ids.sub_label.text = "MainWidgetからsub_labelを更新"
print(f"MainWidget.ids:{self.ids}")
class ExampleIds3(App):
def build(self):
return MainWidget()
def update_sub_label(self):
# サブウィジェット内のidに直接アクセスはできない
# self.root.ids.sub_label.text = "ExampleIds3からsub_labelを更新" # これはエラー
sub_widget = self.root.ids.sub_widget # 正しい方法
sub_widget.ids.sub_label.text = "ExampleIds3からsub_labelを更新"
print(f"ExampleIds3.ids:{self.root.ids}")
if __name__ == "__main__":
ExampleIds3().run()
exampleids3.kv
<SubWidget>:
BoxLayout:
orientation: "vertical"
Label:
id: sub_label
text: "SubWidgetのラベル"
Button:
text: "SubWidgetからSubWidgetを変更"
on_press: root.update_sub_label()
<MainWidget>:
BoxLayout:
orientation: "vertical"
SubWidget:
id: sub_widget
Label:
id: main_label
text: "MainWidgetのラベル\nこれはスコープ外から操作できない"
Button:
text: "ExampleIds3からSubWidgetを変更"
on_press: app.update_sub_label()
Button:
text: "MainWidgetからSubWidgetを変更"
on_press: root.update_sub_label()
このサンプルはそれぞれのクラスでSubWidget内のsub_label
を参照しています。
sub_labelはSubWidgetツリー内のスコープになるため、MainWidgetクラスやExampleIds3クラスからは直接参照できないので名前空間を遡って参照する必要があります。Appサブクラスから参照する場合はself.root.ids…になります。
sub_widget = self.ids.sub_widget # 正しい方法
sub_widget.ids.sub_label.text = "MainWidgetから更新"
# self.ids.sub_widget.ids.sub_label.textの様に1行で書いても良い
SubWidgetは自身のツリー内のラベル内なので、通常の参照方法が使用できます。
self.ids.sub_label.text = "SubWidgetからsub_labelを更新"
SubWidgetから見るとMainWidgetはスコープ外になるためmain_labelを参照することはできません。下記のように名前空間を遡ってもエラーになります。
Label:
id: main_label
text: "MainWidgetのラベル\nこれはスコープ外から操作できない"
# main_widget = self.ids.main_widget
# main_widget.ids.main_label.text = "SubWidgetからSub_labelを更新" # これもエラー
MainWidgetからSubWidgetを参照できるのは、下記のようにMainWidgetのツリー内にSubWidgetを配置をしたので同じツリーのウィジェットとして扱われるためです。また、ExampleIds3からもSubWidgetを参照することができます。Appサブクラスからはルートウィジェットのツリーを参照できるのでこれが可能になります。
SubWidgetを参照するには、名前空間を辿れるようにするためのidが必要になります。
SubWidget:
id: sub_widget
それぞれのクラスでids辞書を参照しているのでどのようになっているのか見てみましょう。
print(f"MainWidget.ids:{self.ids}")
SubWidget.ids:{'sub_label': <WeakProxy to <kivy.uix.label.Label object at 0x000001E56000DD30>>}
MainWidget.ids:{'sub_widget': <WeakProxy to <__main__.SubWidget object at 0x000001E55FFABB60>>, 'main_label': <WeakProxy to <kivy.uix.label.Label object at 0x000001E56002B690>>}
ExampleIds3.ids:{'sub_widget': <WeakProxy to <__main__.SubWidget object at 0x000001E55FFABB60>>, 'main_label': <WeakProxy to <kivy.uix.label.Label object at 0x000001E56002B690>>}
これらはそれぞれのクラスのidのスコープを示しています。
- SubWidgetのids:sub_label
- MainWidgetのids:sub_widget、main_label
- ExampleIds3のids(MainWidgetのidsと同じ):sub_widget、main_label
重複するidsを使うとどうなるか
同じidを定義してもルール構文上エラーにはなりませんが、想定している挙動と異なるので一意のidにすることをお勧めします。以下に同じid名を使用した場合のサンプルを示します。
example_ids4.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
class MainWidget(BoxLayout):
def change_label(self):
self.ids.same_label.text = "labelを更新"
print(self.ids)
class ExampleIds4(App):
def build(self):
return MainWidget()
if __name__ == "__main__":
ExampleIds4().run()
exampleids4.kv
<MainWidget>:
orientation: 'vertical'
Label:
id: same_label
text: "ラベル1"
Label:
id: same_label
text: "ラベル2"
Button:
text: "押してみて"
on_press: root.change_label()
2つのラベルに同じidを定義しています。このコードの結果は「ラベル2」のみが更新されます。またidsに登録されたidは1つだけになっています。
{'same_label': <WeakProxy to <kivy.uix.label.Label object at 0x000001E169FD4EC0>>}
このように想定しない結果となりますので、idの重複には十分注意してください。
Comment