Kv言語の基本
PythonのGUIライブラリは色々あります。
Tk, Qt, GTK, Pyglet, Pygameなど。
しかしスマホアプリをPythonで作ろうと思った時に選択出来るものとしたらQtくらいしか無い上に、PythonとQtでスマホアプリを作るとなるとなかなか手間です。
そこで手軽にスマホアプリを作ってしまおうというライブラリがKivyというライブラリです。
KivyはクロスプラットフォームなのでWindows上で作ったものも、Andoird上で動きます。
特に今回は
- Windows上で開発
- AndoirdアプリのQPython上で動作確認
(3. 出来ればアプリとして1つにパックしたい)
としていこうと思います。
QPythonはAndroid上でPythonが使用出来るアプリです。デフォルトでKivyがGUIとして利用できます。
- 目的
- KV言語のコンセプト
- KVのロード方法
- KV言語の文法
- 特別なシンタックス
- 子のインスタンス化
- イベントバインディング
- キャンバスの拡張
- ウィジェットの参照
- KV言語で定義されたウィジェットへのPythonからのアクセス方法
- 動的クラス
- 複数のウィジェットでスタイルを使い回す
目的
このページではKivyで使われるKv言語と言われるXMLのようなレイアウトを記述する言語についてまとめていこうと思います。
基本的には ProgrammingGuide>>Kv language を訳していく方向です。
KV言語のコンセプト
アプリケーションが複雑になるにつれて、ウィジェット木の構築とバインディングの明示的な宣言が冗長になり保守が困難になります。KV言語はこれらの短所の克服を試みている言語です。
KV言語(kvlangまたはkivy言語とも呼ばれます)は、宣言的な方法でウィジェットツリーを作成し、自然な形でウィジェットのプロパティを相互にコールバックすることが出来ます。
これによってUIをすぐさま変更したりテストすることが出来ます。さらにアプリのロジックとの部分とUIの部分を簡単に分けて考える事ができるため整理がしやすくなります。
KVのロード方法
アプリにKvコードをロードする方法は2つあります。以下で見ていきましょう。
- 名前による方法
以下の例のようにAppクラスの名前と同じKvファイルを用意することです。
MyApp -> my.kv
このファイル(my.kv)でRootウィジェットが定義されている場合、Rootに継承されているAppのroot属性を参照し、アプリケーションウィジェット木の基礎として使用されます。
- Builderによる方法
Builder関数によって文字列(KV言語の)またはファイルを直接Kivyにロードすることが出来ます。文字列またはフィアルでrootウィジェットが定義されている場合、Builder関数によってrootウィジェットが返されます。返されたrootウィジェットから子ウィジェットをたどる形となります。
[.kvファイルからロード]
Builder.load_file("path/to/file.kv")
[文字列からロード]
Builder.load_string(kv_string)
KV言語の文法
KV言語はウィジェットのコンテンツを描画するのに使用され、必ず1つのrootウィジェットといくつかのクラスとテンプレートから成る構成でなければいけません。
rootウィジェットクラスの宣言によってrootが宣言され、:
に続く形でインデント無しで次のようにroot属性をセットします。
Widget:
<>の間にウィジェットクラスの名前としてクラスが宣言される場合、そのクラスの全てのインスタンスをグラフィカルに表現する方法を定義します。
<MyWidget>:
Pythonと同じく文の区切りのためにインデントを使用し、インデントの深さはそれぞれで4スペース分が良いでしょう。
Kv言語を特徴づける3つキーワードを挙げます。
特別なシンタックス
Kv文の全体で利用出来る値を定義する特別なシンタックスが2つあります。
kvからPythonモジュールとクラスにアクセスする方法
#:import name x.y.z #:import isdir os.path.isdir #:import np numpy
これは以下のPythonコードと等価です。
from x.y import z as name from os.path import isdir import numpy as np
グローバル変数をセットする方法
#:set name value
これは以下のPythonコードと等価です。
name = value
子のインスタンス化
複数のクラスのインスタンスや子ウィジェットを含むウィジェットを宣言する時、子の中に子を宣言するというルールがあります。
MyRootWidget: BoxLayout: Button: Button:
上の例ではrooウィジェットとしてMyRootWidgetのインスタンスを宣言し、その中にBoxLayoutのインスタンスを子として宣言しています。
BoxLayoutはさらに2つの子としてButtonクラスのインスタンスを含んでいます。
では上のKV言語での例をPythonコードで表してみましょう。
root = MyRootWidget() box = BoxLayout() box.add_widget(Button()) box.add_widget(Button()) root.add_widget(box)
もちろんPythonでは、ウィジェットの動作を指定するためにインスタンスを作成するときにキーワード引数を渡すことが出来ます。
例えばGridLayoutの列数(cols)を指定する時には以下のようにします。
grid = GridLayout(cols=3)
KV言語で同じことをしてみましょう。KVでは子ウィジェットのプロパティに直接セットできます。
GridLayout: cols: 3
この値はPythonコードとして評価され、使用されている全てのプロパティは監視されます。これはつまりPythonの中でこのようなウィジェットを生成しているという意味になります(ListPropertyというデータウィジェットとしてselfの中で持つことになります)。
grid = GridLayout(cols=len(self.data)) self.bind(data=grid.setter("cols"))
データを変更した時に画面の更新をするには、次のようにすることで実現出来ます。
GridLayout: cols: len(root.data)
イベントバインディング
:
の構文を用いることで、イベントをコールバックする関数に関連付けることが出来ます。
Widget: on_size: my_callback()
args
キーワードを使ったシグナルによって値を渡すことが出来ます。
TextInput: on_text: app.search(args[1])
以下の様なより複雑な表現も出来ます。
pos: self.center_x - self.texture_size[0]/2.0, self.center_y - self.texture_size[1]/2.0
この表現はcenter_x
,、center_y
、texture_size
が変更されても有効な表現です。
これらの値の変化があった場合、pos
を再評価します。
on_
が頭に付くKV言語のイベントは操作することが出来ます。
例えば、PythonコードにおけるTextInput
クラスはFocus
プロパティを持っています。
KV言語ではon_focus
イベントとしてアクセス出来ます。
TextInput: on_focus: print(args)
キャンバスの拡張
KV言語はキャンバスの構造を定義するのにも使われます。以下の例にようになります。
MyWidget: canvas: Color: rgba: 1, 0.3, 0.8, 0.5 Line: points: zip(self.data.x, self.data.y)
プロパティの値が変更された時自動的に更新されます。
もちろんcanvasの変更前と変更後の値としてcanvas.before
とcanvas.after
を利用することが出来ます。
ウィジェットの参照
ウィジェット木の中で、他のウィジェットにアクセスまたは参照することが必ず必要となります。
KV言語はそれぞれのウィジェットのid
を使うことでその方法を提供しています。
KV言語ではこれらをクラスレベルの変数としてのみ扱っています。まずは次の例を見てみましょう。
<MyFirstWidget>: Button: id: f_but TextInput: text: f_but.state <MySecondWidget>: Button: id: s_but TextInput: text: s_but.state
idは宣言されている範囲でしか使用されません。
つまり上記の場合は、<MyFirstWidget>
のidであるf_butは<MySecondWidget>
の中では使うことが出来ません。
定義されていないためです。
idはウィジェット自体への参照では無く、ウィジェットへの弱い参照として扱われます。
結果として、保存されているidはガーベージコレクトされてからウィジェットを維持するには十分ではありません。
そのデモンストレートとして以下の例を挙げましょう。
<MyWidget>: label_widget: label_widget Button: text: 'Add Button' on_press: root.add_widget(label_widget) Button: text: 'Remove Button' on_press: root.remove_widget(label_widget) Label: id: label_widget text: 'widget'
label_widgetへの参照はMyWidgetに保存されるが、これは弱い参照であるため他の参照が取り除かれた際にオブジェクトを活かしたまま維持することが十分になされません。
したがって、removeボタンが押された後(削除という動作はウィジェットへの直接参照)と、ウィンドウがリサイズされた後(label_widgetの削除の結果としてガーベージコレクタが呼ばれます)、ウィジェットの背景を追加するために追加ボタンが押される時、RefferenceError: weakly-refferenced object no longer existsが表示されます。
生きているウィジェットを維持するために、label_widgetに直接参照するウィジェットが維持されなければいけません。これはid.selfまたはlabel_widget.selfを用いることで実現されます。正しい方法は次のようになります。
<MyWidget>:
label_widget: label_widget.__self__
KV言語で定義されたウィジェットへのPythonからのアクセス方法
my.kvというファイルに次のようなコードが書かれているとしよう。
<MyFirstWidget>: txt_inpt: txt_inpt Button: id: f_but TextInput: id: txt_inpt text: f_but.state on_text: root.check_status(f_but)
次にmyapp.pyに以下のようなコードが書かれているとする。
class MyFirstWidget(BoxLayout): txt_inpt = ObjectProperty(None) def check_status(self, btn): print('button state is: {state}'.format(state=btn.state)) print('text input text is: {txt}'.format(txt=self.txt_inpt))
txt_inptはクラス内部で初期化されたObjectPropertyとして定義されています。
txt_inpt = ObjectProperty(None)
ここでのポイントは、self.txt_inptはNoneであることです。KV言語ではこのプロパティはtxt_inptというidによってTextInputのインスタンスが参照され更新されます。
txt_inpt: txt_inpt
これ以降、self.txt_inptはtxt_inptというidによってウィジェット自身に参照が固定され、クラス内のどこでも使用でき、check_status関数のように扱うことが出来ます。
この方法とは対称的に、上記のf_butの場合のようにidが必要な時に渡すことも出来ます。
KV言語で定義したidのオブジェクトにアクセスするシンプルな方法が以下となります。
[KV言語]
<Marvel> Label: id: loki text: 'loki: I AM YOUR GOD!' Button: id: hulk text: "press to smash loki" on_release: root.hulk_smash()
[Pythonコード]
class Marvel(BoxLayout): def hulk_smash(self): self.ids.hulk.text = "hulk: puny god!" self.ids["loki"].text = "loki: >_<!!!"
KVファイルが解析される時、Kivyはidと場所のタグ付けがされている全てのウィジェットをself.idsという辞書型のプロパティに集約させます。
それが意味することは、これらのウィジェットを上からイテレートでき、さらに辞書スタイルでアクセスすることができるということです。
for key, val in self.ids.items(): print("key={0}, val={1}".format(key, val))
Note
self.idsは非常に簡単な方法ですが、一般にはObjectPropertyを使う方法が最も良いと言われています。self.idsは弱参照ですがObjectPropertyは直接参照を生成するため、より明示的で高速なアクセスを実現するためです。
動的クラス
次のコードを見てください。
<MyWidget>: Button: text: "Hello world, watch this text wrap inside the button" text_size: self.size font_size: '25sp' markup: True Button: text: "Even absolute is relative to itself" text_size: self.size font_size: '25sp' markup: True Button: text: "Repeating the same thing over and over in a comp = fail" text_size: self.size font_size: '25sp' markup: True Button:
全てのボタンを同じ値で繰り返し生成するために、以下の様なテンプレートを使うことが出来ます。
<MyBigButt@Button>: text_size: self.size font_size: '25sp' markup: True <MyWidget>: MyBigButt: text: "Hello world, watch this text wrap inside the button" MyBigButt: text: "Even absolute is relative to itself" MyBigButt: text: "repeating the same thing over and over in a comp = fail" MyBigButt:
このクラスは、この規則の下で宣言することで生成され、ボタンクラスからの継承、デフォルト値の変更ができ、さらにPyhon側で新たなコードを追加せずに全てのインスタンスを生成することが出来ます。
例えば上の例であれば
<MyBigButt@Button> text_size: self.size font_size: markup: True
これはButtonクラスを継承して、MyBigButtというクラスを生成しています。
このデフォルト値をtext_size、font_size、markupというプロパティについて変更しています。
このような記述をすることで新たなクラスウィジェットをPythonで何もすることなしにKV言語のみで生成することが出来ます。
複数のウィジェットでスタイルを使い回す
まずは次のコードを見てください。
[KV言語: my.kv]
<MyFirstWidget>: Button: on_press: self.text(txt_inpt.text) TextInput: id: txt_inpt <MySecondWidget>: Button: on_press: self.text(txt_inpt.text) TextInput: id: txt_inpt
[Pythonコード: myapp.py]
class MyFirstWidget(BoxLayout): def text(self, val): print('text input text is: {txt}'.format(txt=val)) class MySecondWidget(BoxLayout): writing = StringProperty('') def text(self, val): self.writing = val
両方のクラスで同じ.kvスタイルを共有するために、両方のウィジェットでスタイルを使いまわす場合はこのデザインをシンプルにすることが出来ます。
次にようにmy.kvファイルに書いてみましょう。
<MyFirstWidget,MySecondWidget>: Button: on_press: self.text(txt_inpt.text) TextInput: id: txt_inpt
カンマ(,)をクラスの名前の区切りに使うことで、宣言された全てのクラスのリストで同じkvプロパティを設定することが出来ます。