Python+Kivy入門

Kivyアプリの内部がどのように実行されているかライフサイクルを確認する

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

Kivyアプリケーションの内部ではどのように実行されているのかを説明します。またライフサイクルを確認するサンプルコードをルールごとに紹介しているのでidsの生成タイミングやウィジェットツリーがいつ生成されるかの内部の動きを確認することができます。

Kivyアプリケーションのライフサイクル

どの処理でも開始から終了までのプロセスがあります。その流れをライフサイクルと言っています。Kivyのアプリケーション開始から終了までのライフサイクルを理解するとコードの流れが掴めるかと思います。内部の動きは実際にはもっと複雑になっています。モバイルアプリでは若干プロセスが異なります。

Kivyアプリのライフサイクル
引用:Kivy公式サイト
  1. Python開始/実行(run()が呼ばれる)
  2. build()が呼ばれる
  3. on_start()が呼ばれる
  4. イベントループが開始される
  5. 外部アプリ、OS、内部関数がアプリケーションを一時停止する。
  6. 主にスマホアプリではon_pause()やon_resume()が呼ばれて再開する→④に戻るもしくは⑧のPythonが停止する。
  7. on_stop()が呼ばれる
  8. kivyのウィンドウが破棄される
  9. Pythonが停止する

アプリが起動するまでのKivy内部の流れ

アプリがどの様に起動されるのかを説明します。 Python言語のみで書かれた場合とkvファイルを使用して書いた場合では若干動作が異なります。特に書き方によってはids辞書の生成タイミングとウィジェットツリー構築の順番が異なるのでプログラムの動作が異なる場合があります。(実際はもっと複雑で、順番が違うかもしれませんがご容赦ください。)

この続きではkv言語のルールについて理解する必要があります。

Pythonで書いた場合

  1. run()が呼ばれる
  2. APPクラスとそれ継承したサブクラスがを初期化される
  3. メインループ開始
  4. build()が呼ばれる
  5. レイアウトを継承したサブクラスが呼ばれ初期化される
  6. on_startが呼ばれる
  7. ウィジェットツリーが構築される
  8. 画面に描画される
  9. イベントループでイベントを監視
  10. 画面を閉じるとon_stopが呼ばれる。

kvファイルでクラスルールを定義した場合

  1. run()が呼ばれる
  2. KVファイルのファイル名とクラス名がマッチするか検証する
  3. APPクラスとそれ継承したサブクラスがを初期化される
  4. メインループ開始
  5. build()が呼ばれる
  6. レイアウトを継承したサブクラスが呼ばれ初期化される
  7. コンストラクタ内でKVファイルが読み込まれる
  8. on_startが呼ばれる
  9. idsの辞書が生成される
  10. ウィジェットツリーが構築される
  11. 画面に描画される
  12. イベントループでイベントを監視
  13. 画面を閉じるとon_stopが呼ばれる。

kvファイルでルートルール(ルートクラス名)を定義した場合

  1. run()が呼ばれる
  2. KVファイルのファイル名とクラス名がマッチするか検証する
  3. APPクラスとそれ継承したサブクラスがを初期化される
  4. メインループ開始
  5. レイアウトを継承したサブクラスが呼ばれ初期化される
  6. コンストラクタ内もしくはその外側でKVファイルが読み込まれる
  7. 自動的にbuild()が呼ばれる
  8. build()内でids辞書が生成される
  9. build()内でウィジェットツリーが構築される
  10. on_startが呼ばれる
  11. 画面に描画される
  12. イベントループでイベントを監視
  13. 画面を閉じるとon_stopが呼ばれる。

kvファイルでルートルール(UIウィジェット)を定義した場合

  1. run()が呼ばれる
  2. KVファイルのファイル名とクラス名がマッチするか検証する
  3. APPクラスとそれ継承したサブクラスがを初期化される
  4. メインループ開始
  5. 自動的にbuild()が呼ばれる
  6. コンストラクタ内でKVファイルが読み込まれる(未確定)
  7. build()内でidsの辞書が生成される
  8. build()内でウィジェットツリーが構築される
  9. on_startが呼ばれる
  10. 画面に描画される
  11. イベントループでイベントを監視
  12. 画面を閉じるとon_stopが呼ばれる。

プロセスの説明

上記のプロセス毎に説明します。

  • run()が呼ばれる

    アプリケーションが起動するとrun()が呼び出されます。

    if __name__ == "__main__":
        MyApp().run()
  • KVファイルの検証が行われる

    kvファイルを使用している場合は、ファイル名と同じ名前のサブクラスがあるか検索しています。もしマッチしなければ黒い画面で何も表示されません。

  • APPクラスが初期化される

    Appクラスとそれを継承したサブクラスが初期化されてインスタンスの生成が行われます。

    class MyApp(App):
  • メインループ

    アプリが起動中からアプリを終了するまでメインループと呼ばれるループ処理が走っています。これはイベントの監視を行ったり、適切な箇所でコールバック関数を呼び出しています。

  • build()メソッドが呼ばれる

    ここでウィジェットツリーのルートウィジェットが生成されます。 通常、このメソッドで最初の画面に表示されるルートウィジェットが指定されます。

    kvファイルでルートルールを定義して書いた場合は、レイアウトを継承したサブクラスの初期化の後に呼んでいて自動でbuildメソッドが呼ばれます。またこのタイミングでids辞書の生成やウィジェットツリーの構築が行われます。

    def build(self):
        return MyRootWidget()
  • レイアウトウィジェットの初期化

    Pythonで書かれたコードやKVファイル内でクラスルール、ルートルールを使用した場合はレイアウトを継承したサブクラスの初期化を行いインスタンスが生成されます。

    class MyRootWidget(BoxLayout):
  • kvファイルの読み込み

    kvファイルを定義してる場合はコンストラクタ内もしくはその外側でkvファイルの読み込みが行われ、kvファイルの内容が解析されてPythonで定義されたクラスやウィジェットと紐付けされます。これによってファイル内でkv宣言されたウィジェットがPython側で利用可能になります。プロパティやイベントのバインディングも自動的に行われ、kvファイル内の記述とPython側のクラスが統合されます。

  • on_startが呼ばれる

    on_start関数を呼び出します。このタイミングでイベントの紐づけが行われてイベントリスナーが監視しています。またPythonで書かれたコードとkvファイルをクラスルールで定義した場合は、ここでids辞書の生成とウィジェットツリーの構築が行われます。

  • ラベル
    画面に描画される

    ウィジェットツリーの構造に基づいて描画します。Kivyの描画エンジンはOpenGLを使用して実際のUI要素を画面上に描画します。
    ※OpenGL:Khronos Groupが提供しているグラフィックスAPI

  • ラベル
    on_stopが呼ばれる

    アプリケーションを閉じるとアプリが終了されてこのon_stop関数が呼ばれます。ここでメインループが終了しKivyはリソースの開放や画面破棄などを行います。

ライフサイクルを確認するサンプルコード

これらの流れを理解するための簡単なサンプルコードを作りました。idsがいつ作成されるのかとウィジェットツリーがいつ構成されるのかを確認することができると思います。実行するとコンソール画面にメッセージを表示します。画面はダミーになります。

pythonコードのみのライフサイクルサンプル

Pythonコードのみで書いたコードのサンプルになります。

life_cycle_check_class.py

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

class RootLayout(BoxLayout):
    def __init__(self, **kwargs):
        app_obj = LifeCycleCheckPython
        
        print("5-1. RootLayout: __init__が呼ばれました")
        app_obj.ids_check(self,'5-1.')
        app_obj.log_widget_tree(self,'5-1.')

        super().__init__(**kwargs)

        self.orientation = 'vertical'
        self.add_widget(Label(text='hello world'))

        app_obj.ids_check(self,'5-2.')
        app_obj.log_widget_tree(self,'5-2.')


    def on_kv_post(self, base_widget):
        print("--------KVファイルはありません--------")

class LifeCycleCheckPython(App):
    def __init__(self, **kwargs):
        print("2. APPの生成を確認: __init__が呼ばれました")
        self.ids_check('2-1.')
        self.log_widget_tree('2-1.')

        super().__init__(**kwargs)
        self.ids_check('2-2.')
        self.log_widget_tree('2-2.')

    def build(self):
        print("4. buildの実行を確認")
        self.ids_check('6. build後')
        self.log_widget_tree("6. build後")
        
        return RootLayout()

    def on_start(self):
        print("7. on_startの確認")
        print("7. イベントループが待機中")
        self.ids_check('7.')
        self.log_widget_tree("7. on_start中")
        

    def on_stop(self):
        print("8. アプリ終了時の確認: on_stopが呼ばれました")

    def on_pause(self):
        print("9. アプリ一時停止時の確認: on_pauseが呼ばれました")
        return True

    def on_resume(self):
        print("10. アプリ再開時の確認: on_resumeが呼ばれました")

    def ids_check(self,prefix):
        try:
            if self.root.ids:
                self.ids_status = "idsが生成されました"
                print(f"{prefix} ids : {self.root.ids}")
        except AttributeError:
            self.ids_status = "idsは生成されません"
        return print(f"{prefix} idsの状態: {self.ids_status}")

    def log_widget_tree(self, prefix):
        #ウィジェットツリーをログ出力する
        try:
            if self.root:
                print(f"{prefix} 現在のウィジェットツリー")
                self._print_widget_tree(self.root)
        except AttributeError:
            print(f"{prefix} ウィジェットツリーはまだ生成されていません")

    def _print_widget_tree(self, widget, level=0):
        #再帰的にウィジェットツリーを出力
        indent = "  " * level
        print(f"{indent}- {widget.__class__.__name__}")
        for child in widget.children:
            self._print_widget_tree(child, level + 1)      

if __name__ == '__main__':
    print("1. runを確認")
    app = LifeCycleCheckPython()
    print("3. メインループの開始の確認")
    app.run()

load_string(クラスルール)を使用したライフサイクルサンプル

Builder.load_stringを使用してクラスルールを指定したしたサンプル

life_cycle_check_load_string1.py

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

kv_code = '''
<RootLayout>:
    orientation: 'vertical'

    Label:
        id: label
        text: 'コンソール画面に結果が表示されます'
'''

Builder.load_string(kv_code)

class RootLayout(BoxLayout):
    def __init__(self, **kwargs):
        app_obj = life_cycle_check_load_string1
        
        print("5-1. RootLayout: __init__が呼ばれました")
        app_obj.ids_check(self,'5-1.')
        app_obj.log_widget_tree(self,'5-1.')

        super().__init__(**kwargs)
        app_obj.ids_check(self,'5-2.')
        app_obj.log_widget_tree(self,'5-2.')

    def on_kv_post(self, base_widget):
        print("--------KVファイルはありません。--------")

class life_cycle_check_load_string1(App):
    def __init__(self, **kwargs):
        print("2. APPの生成を確認: __init__が呼ばれました")
        self.ids_check('2-1.')
        self.log_widget_tree('2-1.')

        super().__init__(**kwargs)
        self.ids_check('2-2.')
        self.log_widget_tree('2-2.')

    def build(self):
        print("4. buildの実行を確認")
        self.ids_check('6. build後')
        self.log_widget_tree("6. build後")
        return RootLayout()

    def on_start(self):
        print("7. on_startの確認")
        print("7. イベントループが待機中")
        self.ids_check('7.')
        self.log_widget_tree("7. on_start中")
        

    def on_stop(self):
        print("8. アプリ終了時の確認: on_stopが呼ばれました")

    def on_pause(self):
        print("9. アプリ一時停止時の確認: on_pauseが呼ばれました")
        return True

    def on_resume(self):
        print("10. アプリ再開時の確認: on_resumeが呼ばれました")

    def ids_check(self,prefix):
        try:
            if self.root.ids:
                self.ids_status = "idsが生成されました"
                print(f"{prefix} ids : {self.root.ids}")
        except AttributeError:
            self.ids_status = "idsはまだ生成されていません"
        return print(f"{prefix} idsの状態: {self.ids_status}")

    def log_widget_tree(self, prefix):
        #ウィジェットツリーをログ出力する
        try:
            if self.root:
                print(f"{prefix} 現在のウィジェットツリー")
                self._print_widget_tree(self.root)
        except AttributeError:
            print(f"{prefix} ウィジェットツリーはまだ生成されていません")

    def _print_widget_tree(self, widget, level=0):
        #再帰的にウィジェットツリーを出力
        indent = "  " * level
        print(f"{indent}- {widget.__class__.__name__}")
        for child in widget.children:
            self._print_widget_tree(child, level + 1)      

if __name__ == '__main__':
    print("1. runを確認")
    app = life_cycle_check_load_string1()
    print("3. メインループの開始の確認")
    app.run()

load_string(ルートルール)を使用したライフサイクルサンプル

Builder.load_stringを使用してルートルールを指定したサンプルになります。

life_cycle_check_load_string2.py

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

kv_code = Builder.load_string('''
BoxLayout:
    orientation: 'vertical'

    Label:
        id: label
        text: 'コンソール画面に結果が表示されます'
''')

class LifeCycleCheckLoadString2(App):
    def __init__(self, **kwargs):
        print("2. APPの生成を確認: __init__が呼ばれました")
        self.ids_check('2-1.')
        self.log_widget_tree('2-1.')

        super().__init__(**kwargs)
        self.ids_check('2-2.')
        self.log_widget_tree('2-2.')

    def build(self):
        print("4. buildの実行を確認")
        self.ids_check('6. build後')
        self.log_widget_tree("6. build後")
        return kv_code

    def on_start(self):
        print("7. on_startの確認")
        print("7. イベントループが待機中")
        self.ids_check('7.')
        self.log_widget_tree("7. on_start中")
        

    def on_stop(self):
        print("8. アプリ終了時の確認: on_stopが呼ばれました")

    def on_pause(self):
        print("9. アプリ一時停止時の確認: on_pauseが呼ばれました")
        return True

    def on_resume(self):
        print("10. アプリ再開時の確認: on_resumeが呼ばれました")

    def on_kv_post(self, base_widget):
        print("--------KVファイルはありません--------")

    def ids_check(self,prefix):
        try:
            if self.root.ids:
                self.ids_status = "idsが生成されました"
                print(f"{prefix} ids : {self.root.ids}")
        except AttributeError:
            self.ids_status = "idsはありません"
        return print(f"{prefix} idsの状態: {self.ids_status}")

    def log_widget_tree(self, prefix):
        #ウィジェットツリーをログ出力する
        try:
            if self.root:
                print(f"{prefix} 現在のウィジェットツリー")
                self._print_widget_tree(self.root)
        except AttributeError:
            print(f"{prefix} ウィジェットツリーはまだ生成されていません")

    def _print_widget_tree(self, widget, level=0):
        #再帰的にウィジェットツリーを出力
        indent = "  " * level
        print(f"{indent}- {widget.__class__.__name__}")
        for child in widget.children:
            self._print_widget_tree(child, level + 1)      

if __name__ == '__main__':
    print("1. runを確認")
    app = LifeCycleCheckLoadString2()
    print("3. メインループの開始の確認")
    app.run()

クラスルールを使用したライフサイクルサンプル

クラスルールを指定したサンプルになります。

life_cycle_check_class.py

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

class RootLayout(BoxLayout):
    def __init__(self, **kwargs):
        app_obj = LifeCycleCheckClass
        
        print("5-1. RootLayout: __init__が呼ばれました")
        app_obj.ids_check(self,'5-1.')
        app_obj.log_widget_tree(self,'5-1.')

        super().__init__(**kwargs)
        app_obj.ids_check(self,'5-2.')
        app_obj.log_widget_tree(self,'5-2.')

    def on_kv_post(self, base_widget):
        print("--------KVファイルの読み込みが終わりました。--------")

class LifeCycleCheckClass(App):
    def __init__(self, **kwargs):
        print("2. APPの生成を確認: __init__が呼ばれました")
        self.ids_check('2-1.')
        self.log_widget_tree('2-1.')

        super().__init__(**kwargs)
        self.ids_check('2-2.')
        self.log_widget_tree('2-2.')

    def build(self):
        print("4. buildの実行を確認")
        self.ids_check('6. build後')
        self.log_widget_tree("6. build後")
        return RootLayout()

    def on_start(self):
        print("7. on_startの確認")
        print("7. イベントループが待機中")
        self.ids_check('7.')
        self.log_widget_tree("7. on_start中")
        

    def on_stop(self):
        print("8. アプリ終了時の確認: on_stopが呼ばれました")

    def on_pause(self):
        print("9. アプリ一時停止時の確認: on_pauseが呼ばれました")
        return True

    def on_resume(self):
        print("10. アプリ再開時の確認: on_resumeが呼ばれました")

    def ids_check(self,prefix):
        try:
            if self.root.ids:
                self.ids_status = "idsが生成されました"
                print(f"{prefix} ids : {self.root.ids}")
        except AttributeError:
            self.ids_status = "idsはまだ生成されていません"
        return print(f"{prefix} idsの状態: {self.ids_status}")

    def log_widget_tree(self, prefix):
        #ウィジェットツリーをログ出力する
        try:
            if self.root:
                print(f"{prefix} 現在のウィジェットツリー")
                self._print_widget_tree(self.root)
        except AttributeError:
            print(f"{prefix} ウィジェットツリーはまだ生成されていません")

    def _print_widget_tree(self, widget, level=0):
        #再帰的にウィジェットツリーを出力
        indent = "  " * level
        print(f"{indent}- {widget.__class__.__name__}")
        for child in widget.children:
            self._print_widget_tree(child, level + 1)      

if __name__ == '__main__':
    print("1. runを確認")
    app = LifeCycleCheckClass()
    print("3. メインループの開始の確認")
    app.run()

lifecyclecheckclass.kv

<RootLayout>:
    orientation: 'vertical'
    Label:
        id: label1
        text: "Kivyライフサイクル確認"
    Label:
        id: label2
        text: "コンソール画面に結果が表示されます"

ルートルール(ルートクラス名)を使用したライフサイクルサンプル

ルートルールのルートクラス名を指定したサンプルになります。

life_cycle_check_root_class.py

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

class RootLayout(BoxLayout):
    def __init__(self, **kwargs):
        app_obj = LifeCycleCheckRootClass
        
        print("5-1. RootLayout: __init__が呼ばれました")
        app_obj.ids_check(self,'5-1.')
        app_obj.log_widget_tree(self,'5-1.')

        super().__init__(**kwargs)
        app_obj.ids_check(self,'5-2.')
        app_obj.log_widget_tree(self,'5-2.')

    def on_kv_post(self, base_widget):
        print("--------KVファイルの読み込みが終わりました--------")

class LifeCycleCheckRootClass(App):
    def __init__(self, **kwargs):
        print("2. APPの生成を確認: __init__が呼ばれました")
        self.ids_check('2-1.')
        self.log_widget_tree('2-1.')

        super().__init__(**kwargs)
        self.ids_check('2-2.')
        self.log_widget_tree('2-2.')

    def build(self):
        print("4. buildの実行を確認")
        self.ids_check('6. build後')
        self.log_widget_tree("6. build後")
        
    
    def on_start(self):
        print("7. on_startの確認")
        print("7. イベントループが待機中")
        self.ids_check('7.')
        self.log_widget_tree("7. on_start後")
        

    def on_stop(self):
        print("8. アプリ終了時の確認: on_stopが呼ばれました")

    def on_pause(self):
        print("9. アプリ一時停止時の確認: on_pauseが呼ばれました")
        return True

    def on_resume(self):
        print("10. アプリ再開時の確認: on_resumeが呼ばれました")

    def ids_check(self,prefix):
        try:
            if self.root.ids:
                self.ids_status = "idsが生成されました"
                print(f"{prefix} ids : {self.root.ids}")
        except AttributeError:
            self.ids_status = "idsはまだ生成されていません"
        return print(f"{prefix} idsの状態: {self.ids_status}")

    def log_widget_tree(self, prefix):
        #ウィジェットツリーをログ出力する
        try:
            if self.root:
                print(f"{prefix} 現在のウィジェットツリー")
                self._print_widget_tree(self.root)
        except AttributeError:
            print(f"{prefix} ウィジェットツリーはまだ生成されていません")

    def _print_widget_tree(self, widget, level=0):
        #再帰的にウィジェットツリーを出力
        indent = "  " * level
        print(f"{indent}- {widget.__class__.__name__}")
        for child in widget.children:
            self._print_widget_tree(child, level + 1)      

if __name__ == '__main__':
    print("1. runを確認")
    app = LifeCycleCheckRootClass()
    print("3. メインループの開始の確認")
    app.run()

lifecyclecheckrootclass.kv

RootLayout:
    orientation: 'vertical'
    Label:
        id: label1
        text: "Kivyライフサイクル確認"
    Label:
        id: label2
        text: "コンソール画面に結果が表示されます"

ルートルール(ウィジェット)を使用したライフサイクルサンプル

ルートルールのウィジェットを指定したサンプルになります。

life_cycle_check_ui.py

from kivy.app import App

class LifeCycleCheckUi(App):
    def __init__(self, **kwargs):
        print("2. APPの生成を確認: __init__が呼ばれました")
        self.ids_check('2-1.')
        self.log_widget_tree('2-1.')

        super().__init__(**kwargs)
        self.ids_check('2-2.')
        self.log_widget_tree('2-2.')

    def build(self):
        print("4. buildの実行を確認")
        self.ids_check('6. build後')
        self.log_widget_tree("6. build後")

    def on_start(self):
        print("7. on_startの確認")
        print("7. イベントループが待機中")
        self.ids_check('7.')
        self.log_widget_tree("7. on_start中")
        

    def on_stop(self):
        print("8. アプリ終了時の確認: on_stopが呼ばれました")

    def on_pause(self):
        print("9. アプリ一時停止時の確認: on_pauseが呼ばれました")
        return True

    def on_resume(self):
        print("10. アプリ再開時の確認: on_resumeが呼ばれました")

    def on_kv_post(self, base_widget):
        print("--------KVファイルの読み込みが終わりました--------")

    def ids_check(self,prefix):
        try:
            if self.root.ids:
                self.ids_status = "idsが生成されました"
                print(f"{prefix} ids : {self.root.ids}")
        except AttributeError:
            self.ids_status = "idsはまだ生成されていません"
        return print(f"{prefix} idsの状態: {self.ids_status}")

    def log_widget_tree(self, prefix):
        #ウィジェットツリーをログ出力する
        try:
            if self.root:
                print(f"{prefix} 現在のウィジェットツリー")
                self._print_widget_tree(self.root)
        except AttributeError:
            print(f"{prefix} ウィジェットツリーはまだ生成されていません")

    def _print_widget_tree(self, widget, level=0):
        #再帰的にウィジェットツリーを出力
        indent = "  " * level
        print(f"{indent}- {widget.__class__.__name__}")
        for child in widget.children:
            self._print_widget_tree(child, level + 1)      

if __name__ == '__main__':
    print("1. runを確認")
    app = LifeCycleCheckUi()
    print("3. メインループの開始の確認")
    app.run()

lifecyclecheckui.kv

BoxLayout:
    orientation: 'vertical'
    Label:
        id: label1
        text: "Kivyライフサイクル確認"
    Label:
        id: label2
        text: "コンソール画面に結果が表示されます"

まとめ

アプリの内部で何が行われているかを説明しました。ルートウィジェットに何を指定したかによってkvファイルの読み込みのタイミングやidsの生成タイミングが異なるため挙動が異なる場合があるので注意してください。

Comment

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