たけし備忘録

自分の好奇心の赴くままに勉強メモ LL系が大好き Python bash Julia C

Kv言語の基本

PythonGUIライブラリは色々あります。
Tk, Qt, GTK, Pyglet, Pygameなど。
しかしスマホアプリをPythonで作ろうと思った時に選択出来るものとしたらQtくらいしか無い上に、PythonとQtでスマホアプリを作るとなるとなかなか手間です。

そこで手軽にスマホアプリを作ってしまおうというライブラリがKivyというライブラリです。

Kivy公式ホームページ

KivyクロスプラットフォームなのでWindows上で作ったものも、Andoird上で動きます。
特に今回は

  1. Windows上で開発
  2. AndoirdアプリのQPython上で動作確認
    (3. 出来ればアプリとして1つにパックしたい)

としていこうと思います。

QPythonAndroid上でPythonが使用出来るアプリです。デフォルトでKivyがGUIとして利用できます。

QPythonホームページ

目的

このページでは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_ytexture_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.beforecanvas.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プロパティを設定することが出来ます。