Python+Kivy入門

kv言語のクラスルールとルールの使い方【Kivy入門】

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

kv言語のクラスルールの使い方についてのいくつかのサンプルとPythonファイルにkv言語を書く書き方やそのほかのルールの使い方について説明しています。

以下が今回紹介するサンプルです。

  • Pythonファイルにkv言語を書くBuilder.load_stringを使う方法
  • 複数のクラスルールを定義する方法
  • その他のルールを活用する方法
  • クラスルールをルートルールでインスタンス生成をする方法
  • kvファイルを分割して書く方法
  • pyファイルを分割して書く方法

Builder.load_stringを使用してPythonファイルにkv言語を書く方法

Pythonファイルにkv言語を書いて読ませるBuilderクラスのload_stringメソッドを使用する方法があります。コード量が少ない時やファイルを2つ作るのが面倒な時は便利です。ただし、クラスルールとルートルールを指定するときで若干書き方が異なります。一応両方紹介しておきます。

クラスルールの場合

Pythonファイルに文字列としてkvコードを記述して、Builderクラスのload_stringメソッドで読ませます。

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout

# Pythonファイル内でkv言語を直接記述
kv_code = Builder.load_string('''
<MyBoxLayout>:
    orientation: 'vertical'
    Label:
        id: my_label
        text: "ここに表示されるよ"
    Button:
        text: "押して?"
        on_press: my_label.text = "ボタンが押されたよ"
''')


class MyBoxLayout(BoxLayout):
    pass

class ExLoadString1(App):
    def build(self):
        return MyBoxLayout()

if __name__ == '__main__':
    ExLoadString1().run()
  1. Builderクラスをインポートする
  2. Public領域にBuilder.load_stringクラスのインスタンスを生成
  3. 引数にコメント文としてkvコードを記述する
  4. buildメソッドのreturn文にはウィジェットを継承したサブクラスを返す
from kivy.lang import Builder

kv_code = Builder.load_string('''
<MyBoxLayout>:
    orientation: 'vertical'
    Label:
        id: my_label
        text: "ここに表示されるよ"
    Button:
        text: "押して?"
        on_press: my_label.text = "ボタンが押されたよ"
''')

kvコードはコメントにします。またインデントの数を守らないとkvファイルで書いた時と同じようにエラーが発生します。

return MyBoxLayout()

buildメソッドの戻り値にはウィジェットを継承したサブクラスを返すようにします。

ルートルールの場合

from kivy.app import App
from kivy.lang import Builder

# Pythonファイル内でkv言語を直接記述
kv_code = Builder.load_string('''
BoxLayout:
    orientation: 'vertical'
    Label:
        id: my_label
        text: "ここに表示されるよ"
    Button:
        text: "押して?"
        on_press: my_label.text = "ボタンが押されたよ"
''')

class ExLoadString1(App):
    def build(self):
        return kv_code

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

ルートルールにウィジェットを指定した場合はreturn文にオブジェクトを返すようにします。

return kv_code

下記のようにルートクラス名を指定した場合は何故かできませんでした。

kv_code = Builder.load_string('''
MyBoxLayout:

class MyBoxLayout(BoxLayout):
    pass

複数のクラス構成の書き方

複数のクラスを定義したサンプルになります。

switch_colors.py

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

class Panel1(BoxLayout):
    pass

class Panel2(BoxLayout):
    pass

class Controller(BoxLayout):
    def switch_color(self):
        # Panel1とPanel2を取得
        panel1 = self.ids.panel1
        panel2 = self.ids.panel2
        
        # 各パネル内のラベルの色を切り替え
        panel1_label = panel1.ids.panel1_label
        panel2_label = panel2.ids.panel2_label

        if panel1_label.color == [1, 0, 0, 1]:  # 赤
            panel1_label.color = [0, 1, 0, 1]  # 緑
            panel2_label.color = [1, 0, 0, 1]  # 赤
        else:
            panel1_label.color = [1, 0, 0, 1]  # 赤
            panel2_label.color = [0, 1, 0, 1]  # 緑

class SwitchColors(App):
    def build(self):
        return Controller()

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

switchcolors.kv

<Panel1>:
    id: panel1
    Label:
        id: panel1_label
        text: "Panel 1"
        color: 1, 0, 0, 1  # 初期色: 赤

<Panel2>:
    id: panel2
    Label:
        id: panel2_label
        text: "Panel 2"
        color: 0, 1, 0, 1  # 初期色: 緑

<Controller>:
    orientation: 'vertical'
    Panel1:
        id: panel1
    Panel2:
        id: panel2
    Button:
        text: "Switch Colors"
        size_hint_y: None
        height: 50
        on_press: root.switch_color()

このコードはボタンを押すとラベルの色が変わります。

Appサブクラスを含めて4つのクラスで成り立っています。ツリー構成にすると下記のようになります。Controllerクラスのボタンでpanel1クラスとpanel2クラスを制御しています。

<Controller>
  ∟BoxLayout
    ∟panel1
      ∟Label
    ∟panel2
      ∟Label
     ∟Button

ルールのオーバーライド

何故Controllerのツリーにpanelがあるのかを説明します。複数のクラスルールを定義する場合、他のクラスのルールをルートウィジェットにオーバーライドして配置する必要があります。他のクラスはカスタムウィジェットになるので「ウィジェット」として扱われるため、ウィジェットツリーにLabelやButtonと同じように配置することができます。

ここではルートウィジェットのControllerにpanel1とpanel2をオーバーライドして配置しています。これをしないとpanel1とpanel2は独立しているためこのウィジェットは表示されません。

    Panel1:
        id: panel1
    Panel2:
        id: panel2

試しにこれをコメントにしてみてください。

    #Panel1:
        #id: panel1
    #Panel2:
        #id: panel2

panel1とpanel2は何も表示されないことが確認できると思います。Pythonプログラミングでクラス定義しただけでインスタンス生成をしていない状況と同じです。クラスのUIとスタイルを定義しただけでオーバーライドされていないのでこれらのウィジェットは表示されません。そして、ボタンを押すとウィジェットが存在しないのでエラーが表示されます。

コード解説

kvファイルでidを定義するとPythonコードでウィジェット情報を参照することができます。idについては本書を読み進めて頂ければと思います。

kvファイル

Labelウィジェットにはcolorプロパティを定義しています。

    Label:
        id: panel1_label
        text: "Panel 1"
        color: 1, 0, 0, 1  # 初期色: 赤

色はRGBAで指定します。色設定は下記のページを参照してください

    Button:
        text: "Switch Colors"
        size_hint_y: None
        height: 50
        on_press: root.switch_color()

size_hint_yは親ウィジェットのスペース対して高さをどれくらいウィジェットに割り当てるかを設定します。ここでいう親はControllerクラス(つまりBoxLayout)になります。Noneにすることで最小の高さを割り当てません。heightを50に設定しているので最小の高さは50固定になります。

on_press: root.switch_color()

on_pressメソッドでControllerクラスのswitch_colorメソッドを呼び出しています。

pyファイル

        # Panel1とPanel2を取得
        panel1 = self.ids.panel1
        panel2 = self.ids.panel2
        
        # 各パネル内のラベルの色を切り替え
        panel1_label = panel1.ids.panel1_label
        panel2_label = panel2.ids.panel2_label

        if panel1_label.color == [1, 0, 0, 1]:  # 赤
            panel1_label.color = [0, 1, 0, 1]  # 緑
            panel2_label.color = [1, 0, 0, 1]  # 赤
        else:
            panel1_label.color = [1, 0, 0, 1]  # 赤
            panel2_label.color = [0, 1, 0, 1]  # 緑

switch_colorメソッドではkvファイルで設定したidを使ってLabelウィジェットを取得して色を変更しています。

その他のルールの使い方

2つのルールの書き方を説明します。

レイアウトのルール

レイアウトのウィジェットを<>で囲むとそのレイアウトのスタイルや設定を定義することができます。

custom_rule1.py

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

class RootWidget(BoxLayout):  
    pass

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

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

customrule1.kv

<BoxLayout>:
    orientation: "vertical"
    
    Label:
        text: "レイアウトの設定をする"
        

使用用途は思いつきませんが、公式サイトでもこのような書き方でサンプルコードが載っています。BoxLayoutを継承したクラスなのでBoxLayoutについてのスタイルや設定を定義することができるんだと思います。

<BoxLayout>:
    orientation: "vertical"
    
    Label:
        text: "レイアウトの設定をする"

<RootWidget>:
    orientation: "vertical"
    Label:
        text: "Label"
    BoxLayout: # オーバーライド

上記のコードはルールをオーバーライドしてみました。試しにコードを実行して確認してみてください。使い道はあるかもしれませんが、ダイナミッククラスを使った方がよさそうです。

同じ設定を複数のクラスに反映させる

同じ設定を違うクラスにも適用させたい時はクラス名をカンマで区切り<>で囲みます。クラスルールを2つ並べてる形です。

custom_rule2.py

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

class SubWidget(BoxLayout):
    pass

class MainWidget(BoxLayout):  
    pass

class CustomRule2(App):
    def build(self):
        return MainWidget()

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

customrule2.kv

<MainWidget,SubWidget>:
    orientation: "vertical"
    Label:
        canvas.before:
            Color:
                rgba: 42/255, 100/255, 89/255, 1 
            Rectangle:
                size: self.size
                pos: self.pos
        text: "レイアウトの設定をする"
        font_size: 20
        
<MainWidget>:
    SubWidget: #オーバーライド
<MainWidget,SubWidget>:

上記は2つのクラスのスタイルを定義しています。このままでは定義しただけになるのでオーバーライドする必要があります。

<MainWidget>:
    SubWidget: #オーバーライド

上記でツリーの構成を決めています。<MainWidget>がルートウィジェットになります。SubWidgetのルールをオーバーライドして配置します。

canvas.before:
            Color:
                rgba: 42/255, 100/255, 89/255, 1 
            Rectangle:
                size: self.size
                pos: self.pos

このコードはラベルに四角形を描画して背景色にしています。canvasを使用した背景色の設定は下記のページをご覧ください。

インスタンス生成にルートルールを使う

あまりお勧めしませんがBuildメソッドを定義しないでルートルールのルートクラス名を定義してインスタンスを生成させる事もできます。SwitchColorsのコードを下記のように変更してください。

# pyファイル
class SwitchColors(App):
    pass

➀buidメソッドを消して空のAppサブクラスにします。

# kvファイル
Controller: # 1行目に追記
<Panel1>:
    id: panel1

②kvファイルの1行目にルートクラス名のControllerを記述します。

この書き方でも実行することが可能ですがクラスルールを定義してbuildメソッドでインスタンス生成する場合と異なる挙動をする場合があるので注意してください。

kvファイルを分割して書く方法

kvファイルをクラス毎に分割するサンプルになります。ファイル構成は下記のようになっています。

split_file1.py
split_main.kv
split_sub.kv

split_file1.py

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.lang import Builder

class MainScreen(BoxLayout):
    pass

class SubScreen(BoxLayout):
    pass

class SplitFile1(App):
    def build(self):
        # kvファイルをロード
        Builder.load_file("split_main1.kv")
        Builder.load_file("split_sub1.kv")
        # 初期画面としてMainScreenを返す
        return MainScreen()

    # Appクラスで画面を制御
    def switch_subscreen(self):
        self.root.clear_widgets()
        self.root.add_widget(SubScreen())

    def switch_mainscreen(self):
        self.root.clear_widgets()
        self.root.add_widget(MainScreen())

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

split_main.kv

<MainScreen>:
    orientation: "vertical"
    Label:
        text: "画面1"
        font_size: "24sp"
        color: 74/255, 99/255, 255/255, 1
    Button:
        text: "画面2へ移動"
        font_size: "20sp"
        on_press: app.switch_subscreen()

split_sub.kv

<SubScreen>:
    orientation: "vertical"
    Label:
        text: "画面2"
        font_size: "24sp"
        color: 255/255, 99/255, 71/255, 1
    Button:
        text: "画面1へ移動"
        font_size: "20sp"
        on_press: app.switch_mainscreen()

このコードはボタンを押すと疑似的な画面1と2を切り替えてラベルの色を変更しています。

コード解説

下記のツリー構成になっていて、AppサブクラスのSplitFile1でMainScreenクラスとSubScreenクラスを制御しています。

SplitFile1(App)
 ∟<MainScreen>
   ∟BoxLayout
     ∟Label
     ∟Button
 ∟SubScreen
   ∟BoxLayout
     ∟Label
     ∟Button

pyファイル

kvファイルが分割されているのでファイルを読み込む処理が必要です。

from kivy.lang import Builder # Builderクラスのインポート
Builder.load_file("split_main1.kv")
Builder.load_file("split_sub1.kv")

Builderクラスのload_fileメソッドでファイルを読み込みます。引数にはファイルのパスを指定します。今回は同一フォルダー内にkvファイルを置いたのでこのような書き方になります。またBuilder.load_fileをbuildメソッド内で定義していますが、Public領域のimport文の下などでも大丈夫です。ただしbuildメソッド以前に読まれるように記述する必要があります。

# Builder.load_fileを書く場所

from kivy.app import App

Builder.load_file("file.kv") # ここでも良い

class MyRoot(BoxLayout):
    # ここはダメ
    pass

class TestApp(App):
    Builder.load_file("file.kv") # ここでも良い

    def build(self):
        pass
return MainScreen()

戻り値にはMainScreenを指定しています。こちらがメインの画面になります。

def switch_subscreen(self):
    self.root.clear_widgets()
    self.root.add_widget(SubScreen())

MainScreenのボタンを押すとswitch_mainscreenメソッドが呼ばれ、SubSubscreenのボタンを押すとswitch_subscreenメソッドが呼ばれます。clear_widgetsで描画されているウィジェットを消去し、add_widgetでウィジェットを追加し再描画して、疑似的に画面を切り替えている仕組みになっています。今回はこのような実装にしましたが、Kivyでは複数の画面を切り替える為のScreenManagerというクラスがあるのでそれを使うと良いと思います。

kvファイル

Button:
    text: "画面1へ移動"
    font_size: "20sp"
    on_press: app.switch_mainscreen()

on_pressメソッドでPythonコードで定義したイベント処理メソッドを呼び出しています。

pyファイルを分割して書く方法

pyファイルを分割するサンプルになります。ファイル構成は下記のようになっていてメインのファイルはsplit_file2_1.pyになっています。実行はこのファイルから行います。

split_file2_1.py ★これを実行する
split_file2_2.py
splitfile2_1.kv

split_file2_1.py

from kivy.app import App
from split_file2_2 import MyRoot

class SplitFile2_1(App):
    def build(self):
        return MyRoot()  

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

split_file2_2.py

from kivy.uix.boxlayout import BoxLayout
from kivy.core.clipboard import Clipboard

class MyRoot(BoxLayout):
    def copy_text(self, text):
        Clipboard.copy(text)

splitfile2_1.kv

<MyRoot>:
    orientation: "horizontal"
    Label:
        id: label
        text: "Copy text"
        font_size: 24

    Button:
        text: "Click Me"
        on_release: root.copy_text(label.text)

このコードはボタンを押すとラベルの文字をコピーします。

コード解説

下記のツリー構成になっています

<MyRoot>
  ∟BoxLayout
    ∟Label
    ∟Button

pyファイル

pyファイルが分割されているので実行ファイル以外はインポートする必要があります。ファイルが分割されているだけでアプリ構造は1つのファイル構成の時と変わりはありません。

from <ファイル名> import <クラスルール>

from split_file2_2 import MyRoot

今回はコピー機能を実装しているのでClipboardクラスを使います。copy_textクラスを実装し、引数にtextを指定しイベントが発生するとlabelのテキストを受け取るようにしています。

from kivy.core.clipboard import Clipboard

    def copy_text(self, text):
        Clipboard.copy(text)

kvファイル

今回はBoxLayoutを「縦」に指定しました。これを指定しなくてもデフォルトは縦並びになります。

orientation: "horizontal"

今回はon_releaseイベントを実装しています。

Button:
    text: "コピーする"
    on_release: root.copy_text(label.text)

これはボタンが押されて離された時にイベントが発生します。on_pressの場合はボタンが押された瞬間にイベントが発生するのでこの2つは微妙にイベントが呼ばれるタイミングが異なります。引数にlabel.textを指定しラベルのテキストを取得しています。

Comment

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