Python+Kivy入門

画面遷移に使える動的クラスルールでテンプレートを使う【Kivy kv言語】

この記事は約21分で読めます。

Kivyのkv言語で使える動的クラスルール(Dynamic class rules)の書き方とサンプルコードを紹介します。動的クラスルールはウィジェットを継承してカスタマイズし、テンプレートとして使うことができます。またウィジェットの多重継承もできるので使い方次第で柔軟にkvファイルを使用できます。

動的クラスルールの書き方

動的クラスルールはウィジェットを継承してカスタムウィジェットを作成できる仕組みです。クラスの継承と同じ考え方です。使い方としては、カスタムウィジェットをテンプレートとして定義したり、動的に生成するウィジェットのテンプレートとして呼び出したりすることができます。

書き方はクラスルールの構文に似ていますが’@’でウィジェット名とベースとなるウィジェット名を区切ります。

<ClassName@BaseWidget>:

ClassName
ここには任意の名前を指定します。クラスの継承と同じなのでクラスの命名規則に従います。
@:区切り文字
BaseWidget
継承元のウィジェット名を指定します。

次にプロパティを定義します。

<CustomButton@Button>:
    font_size: 20
    background_color: (1, 0, 0, 1)
    color: (1, 1, 1, 1)

このままでは定義しただけなので、通常のウィジェットと同じようにClassNameに指定した名前でツリー状に配置します。

動的クラスルールはPythonコードで書くと下記のようになり、クラス定義と同じであることが分かるかと思います。これと同じことをkvファイルで書いています。

class CustomButton(Button):
    pass

class RootWidget(BoxLayout):
    pass

では動的クラスルールが実際にどんな使われ方をするのかサンプルコードをみてみましょう。

テンプレートして動的クラスルールを使う①

単純にウィジェットをカスタマイズする時に使います。複数のウィジェットで同じスタイルを設定をするときに便利です。例えばボタンを継承した場合下記のように書きます。

<CustomButton@Button>:
    font_size: 20
    background_color: (1, 0, 0, 1)
    color: (1, 1, 1, 1)

ここではカスタムウィジェットのCustomButtonを定義しました。これを機能させるには通常のウィジェットと同じようにウィジェットツリーに配置します。

# kvファイル
# 例➀ 同じボタンを並べる
<CustomButton@Button>:
    text: "Button"
    font_size: 20
    background_color: (1, 0, 0, 1)
    color: (1, 1, 1, 1)

<RootWidget>:
    CustomButton:
    CustomButton:
    CustomButton:
    CustomButton:
        

プロパティを追加することも可能です。

# kvファイル
# 例②
<CustomButton@Button>:
    font_size: 20
    background_color: (1, 0, 0, 1)
    color: (1, 1, 1, 1)

<RootWidget>:
    CustomButton:
        text: "CustomButtonを配置したよ"
    CustomButton:
        text: "もう1個CustomButtonを配置したよ"
# pyファイル
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout

class RootWidget(BoxLayout):  
    pass

class MyApp(App):
    def build(self):
        return RootWidget()

if __name__ == "__main__":
    MyApp().run()

テンプレートして動的クラスルールを使う②

同じプロパティを複数のウィジェットに適用する場合にも使えます。

# kvファイル
<CustomLabel@Label>:
    font_name: "../fonts/file.ttf"
<CustomButton@Button>:
    font_name: "../fonts/file.ttf"

<RootWidget>:
    orientation: 'vertical'
    
    CustomLabel:
        text: "動的クラスでフォントを指定"
    CustomButton:
        text: "一括font指定したよ"

動的にウィジェットを生成するテンプレートとして使う

Pythonコードで動的にウィジェットを生成するときのテンプレートとしても使えます。

動的クラスルールは定義しただけなので、インスタンスの生成を行わないと機能しません。そこでFactoryクラスを使います。Factoryはカスタムクラスやカスタムウィジェットを登録し、動的にインスタンスを生成するための仕組みです。通常Factoryは、Factory.register()を使用してクラスやモジュールを登録する必要がありますが、動的クラスの場合はregister()で登録しなくても使用できるようです。

Factory.<動的クラスで指定したClassName

例えばCustomButtonをFactoryでインスタンス生成するには下記のように書くことができます。

from kivy.factory import Factory

button = Factory.CustomButton()

Factoryに登録したら、add_widget()でウィジェットを追加します。

self.add_widget(button)

Factoryを使用した場合のids

Factoryでインスタンス生成した場合、動的クラスルールでidを設定してもidsに登録されないので注意してください。

動的にボタンを追加するサンプルコード➀

dynamic_class1.py

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.factory import Factory

class RootWidget(BoxLayout):
    def add_custom_button(self):
        # Factoryクラスでインスタンスを生成
        button = Factory.CustomButton()
        self.add_widget(button)

class DynamicClass1(App):
    def build(self):
        return RootWidget()

if __name__ == '__main__':
    DynamicClass1().run()

dynamicclass1.kv

<CustomButton@Button>:
    text: "Dynamic Button"
    font_size: 20
    background_color: (0.3, 0.6, 0.9, 1)

<RootWidget>:
    orientation: "vertical"
    Button:
        text: "押して"
        on_press: root.add_custom_button()
        # Factoryクラスでインスタンスを生成
        button = Factory.CustomButton()
        self.add_widget(button)

Factoryでインスタンス生成したButtonウィジェットをadd_widget()で追加します。

button = Factory.CustomButton(text="テキストを変更")

動的にプロパティを変更する事も可能です。

動的クラスルールの多重継承

複数のウィジェットを多重継承することもできます。2つの基底ウィジェットの間に「+」を入れます。ウィジェットは2つ迄しか継承することができないので注意してください。

<ClassName@BaseWidget1+BaseWidget2>

ClassName
ここには任意の名前を指定します。クラスの継承と同じなのでクラスの命名規則に従います。
@:区切り文字
BaseWidget1
継承元のウィジェット名を指定します。
+:継承元ウィジェットの区切り文字
BaseWidget2
継承元のウィジェット名を指定します。

敢えて多重継承する有用性はなくBehavior mixinで使用すると良いと思います。

<MyWidget@ButtonBehavior+Label>:

Behaviorクラスを使用した書き方になります。

<DraggableButton@DragBehavior+Label>
    Label: #これは必要ない

ここにLabelを定義する必要はありません。

<DraggableButton@DragBehavior+Label>
    text:"動的クラスの多重継承"
    font_size: 20

ウィジェットに定義するプロパティを記述します。

下記の記事でBehavior mixinを使用しているので参考にしてみてください。

動的クラスルールのidの設定とイベント

動的クラスルール内でidを設定してそれをPythonで参照する場合や、コールバック関数を呼び出すイベントの場合、複雑なツリー構成によっては難しい場合があります。これはキーワードとidの仕組みとKivyアプリのライフサイクルを理解する必要があります。難しいようなら動的クラスルールを使用しないまたはPythonコードで書くなどの選択肢もあります。

サンプルコード

更にもう少しだけ実用的な2つのサンプルコードを見てみましょう。

  1. ボタンを押すとランダムに色が変わる5つのボタンを表示するサンプルコード。1つのFactoryで複数の動的クラスルールをインスタンス生成する方法が学べます。
  2. レイアウトを継承した動的クラスルールで疑似的に生成した3つの画面を遷移する方法が学べます。
KivyのDynamic classを使用してボタンの色をランダムに変更するアプリ

動的にボタンを追加するサンプルコード②

このサンプルはボタンを押す度に色つきのボタンをランダムに5つ表示します。1つのFactoryで複数の動的クラスルールを生成する方法が学べます。

dynamic_class2.py

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.factory import Factory
from random import choice

class RootWidget(BoxLayout):
    def add_custom_button(self):
        # 最初のボタンを保持するために名前を付ける
        initial_button = self.ids.initial_button
        # 画面上のウィジェットをクリア
        self.clear_widgets()
        # 最初のボタンを再度追加
        self.add_widget(initial_button)
        # 新しいボタンを追加
        for _ in range(5):
            button_class = choice(["RedButton", "GreenButton"])
            button = Factory.get(button_class)()
            self.add_widget(button)

class DynamicClass2(App):
    def build(self):
        return RootWidget()

if __name__ == '__main__':
    DynamicClass2().run()

dynamicclass2.kv

<RedButton@Button>:
    text: "Red Button"
    background_color: (1, 0, 0, 1)

<GreenButton@Button>:
    text: "Green Button"
    background_color: (0, 1, 0, 1)

<RootWidget>:
    Button:
        id: initial_button
        text: "押して"
        on_press: root.add_custom_button()

コード解説

このコードは2つの画面で構成されています。
最初の画面(ボタンのみ)→最初の画面のボタン+5つのボタンを表示する画面

<RootWidget>
    ∟Button

<RedButton@Button>
<GreenButton@Button>
initial_button = self.ids.initial_button

最初の画面を保持するために変数に代入しています。

self.clear_widgets()

疑似的に画面遷移させたように見せるため表示されているウィジェットを削除しています。ここでは最初の画面に表示されているボタンを削除しています。

self.add_widget(initial_button)

最初の画面のボタンを削除したので画面には何も表示されていないので変数に代入しておいたボタンを追加します。

        for _ in range(5):
            button_class = choice(["RedButton", "GreenButton"])
            button = Factory.get(button_class)()
            self.add_widget(button)

ボタンを5つ作成するためのループになります。for文の一時変数を「_」とすることで値を保持しない変数として機能します。

from random import choice

button_class = choice(["RedButton", "GreenButton"])

choice()はPythonのrandomモジュールに含まれているメソッドで、リストの中からランダムに1つの要素を選択します。このコードでは、「RedButton」または「GreenButton」のいずれかをランダムに選択します。この文字列はkvファイルの動的クラスルールのClassNameになります。

button = Factory.get(button_class)()

Factory.getメソッドは、指定されたクラス名に対応するウィジェットを取得します。空のカッコ’()’をつけることで、そのクラスのインスタンスを作成します。例えばFactory.get(“RedButton”)()の場合はRedButtonクラスのウィジェットを取得して新しいインスタンスを生成します。

レイアウトを継承するサンプルコード

KivyのDynamic classを使用して画面遷移するアプリ

レイアウトを継承すると疑似的に画面遷移させるためのテンプレートや、ScreenManager(複数の画面を管理するクラス)での画面のテンプレートとして使えます。ここでは疑似的に画面遷移するサンプルを示します。

ScreenManagerで画面遷移したい場合は下記の記事も合わせてご覧ください。

dynamic_class3.py

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.factory import Factory

class RootWidget(BoxLayout):
    def ScreenTransition(self, screen):
        self.clear_widgets()
        if screen == 1:
            layout = Factory.Screen1()
            layout.root_widget = self  # ルートウィジェットを渡す
            self.add_widget(layout)
        elif screen == 2:
            layout = Factory.Screen2()
            layout.root_widget = self  # ルートウィジェットを渡す
            self.add_widget(layout)
        elif screen == 0:
            initial_layout = Factory.InitialScreen()
            initial_layout.root_widget = self  # ルートウィジェットを渡す
            self.add_widget(initial_layout)

class DynamicClass3(App):
    def build(self):
        root = RootWidget()
        initial_layout = Factory.InitialScreen()
        initial_layout.root_widget = root  # ルートウィジェットを渡す
        root.add_widget(initial_layout)
        return root

if __name__ == '__main__':
    DynamicClass3().run()

dynamicclass3.kv

<Screen1@BoxLayout>:
    orientation: 'horizontal'
    Button:
        text: "Go to Screen 2"
        font_size: 20
        background_color: (0, 1, 0, 1)
        on_press: root.root_widget.ScreenTransition(2)  # ルートウィジェットを参照
    Label:
        text: "Screen 1"
        color: (1, 1, 1, 1)

<Screen2@BoxLayout>:
    orientation: 'horizontal'
    Button:
        text: "Back to Main"
        font_size: 20
        background_color: (1, 0, 0, 1)
        on_press: root.root_widget.ScreenTransition(0)  # ルートウィジェットを参照
    Label:
        text: "Screen 2"
        color: (1, 1, 1, 1)

<InitialScreen@BoxLayout>:
    orientation: 'vertical'
    Button:
        text: "Go to Screen 1"
        on_press: root.root_widget.ScreenTransition(1)  # ルートウィジェットを参照
    Label:
        text: "Main Screen"
        color: (1, 1, 1, 1)

<RootWidget>:
    id: container
    orientation: 'vertical'

コード解説

このコードは疑似的な3つの画面にで構成されています。3つの画面は動的クラスルールでBoxLayoutを継承しています。動的クラスルールは2つまでしか継承できないのでUIウィジェットを継承するとそのウィジェットしか定義することができません。しかし、BoxLayoutなどのレイアウトを継承するとどのUIウィジェットでも定義することができるのでテンプレートとして画面を構成するのに便利です。

<RootWidget>
  ∟BoxLayout

<InitialScreen@BoxLayout>
  ∟Botton
  ∟Label
<Screen2@BoxLayout>
  ∟Botton
  ∟Label
<Screen1@BoxLayout>
  ∟Botton
  ∟Label

それぞれの画面のボタンを押すとInitialScreenScreen1 Screen2と画面遷移します。

それぞれの画面ではルートウィジェットをroot_widgetに代入しRootWidget()のインスタンスを参照できるようにしています。Factoryのインスタンス生成は画面を構成することはできますが、画面遷移する度にウィジェットの削除を行っているので、root_widgetにルートウィジェットを渡してRootWidget()のインスタンスを参照させる必要があります。各画面に対してルートウィジェットを参照させることで、画面間の遷移やデータの受け渡しができるようになります。これをしないと画面遷移が行えずエラーになります。

①InitialScreen → root_widgetにルートウィジェットを渡す
②ウィジェットの削除 → Screen1 → root_widgetにルートウィジェットを渡す
③ウィジェットの削除 → Screen2 → root_widgetにルートウィジェットを渡す
④ウィジェットの削除 → InitialScreen → root_widgetにルートウィジェットを渡す
    def build(self):
        root = RootWidget()
        initial_layout = Factory.InitialScreen()
        initial_layout.root_widget = root  # ルートウィジェットを渡す
        root.add_widget(initial_layout)
        return root

buildメソッドでは立ち上げたときに表示される画面を定義しています。(つまりInitialScreen

  1. ルートウィジェットのRootWidget()rootに代入
  2. Factoryに最初の画面のInitialScreen()を登録し、initial_layoutに代入
  3. initial_layout.root_widgetrootRootWidget()のインスタンス)を代入
  4. Factoryに登録したインスタンスをウィジェットに追加
  5. ルートウィジェットのrootのインスタンスを戻り値にする

kvファイルではのon_press()イベントにScreenTransition()メソッドを呼び出しています。

on_press: root.root_widget.ScreenTransition(2)

rootはルートウィジェットを参照するキーワードになりroot_widgetにはルートウィジェットのインスタンスが代入されています。名前空間のroot_widgetの部分は、通常はこれを記述する必要なく参照できますが、ここではルートウィジェットを参照するためにこのような名前空間になっています。

    ScreenTransition(self, screen):
        if screen == 1: # Screen1
            pass
        elif screen == 2: # Screen2
            pass
        elif screen == 0: # InitialScreen
            pass

ScreenTransition()の引数は画面を分岐させるためのフラグになります。

ScreenTransition(self, screen)が呼び出されるとscreenに渡されたフラグに応じて分岐されます、

self.clear_widgets()

画面に描画されているウィジェットを削除し、何も存在しない画面にします。

        if screen == 1:
            layout = Factory.Screen1()
            layout.root_widget = self  # ルートウィジェットを渡す
            self.add_widget(layout)

FactoryScreen1を登録することでScreen1の動的クラスルールで定義したウィジェットを構築します。root_widgetには自身のScreen1を渡してルートウィジェットに紐づけてる形になります。これでScreen1の画面ではルートウィジェットのRootWidget()のメソッドやプロパティに参照できるようになります。他のScreenも同様のことをしています。

このコードのポイントはルートウィジェットであるRootWidget()のインスタンスに各Screenのインスタンスを紐づけてRootWidget()のメソッドやプロパティを参照できるようにすることで画面遷移が行えるということです。

Comment

タイトルとURLをコピーしました