Python+Kivy入門

Kivyのイベント3: カスタムイベントの作成方法とEventDispatcherクラス

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

Kivyではビルトインイベントが用意されていますが、自作のイベントを作成する事もできます。この記事ではカスタムイベント作成・登録しdispatch(イベントの発生)する方法を解説します。

EventDispatcherクラスのメソッド

Kivyのイベントの仕組みやビルトインイベント、イベントの伝播については下記の記事をご覧ください。

カスタムイベントを作成するにはEventDispatcherクラスにイベントを登録するためのメソッドが用意されています。

  1. register_event_type(self, event_type): 新しいカスタムイベントを登録。
  2. unregister_event_type(self, event_type): カスタムイベントの登録を解除。
  3. bind(self, **kwargs): イベントを特定のコールバックに関連付け。
  4. fbind(self, name, func, * args, **kwargs): bind()より高速にバインディング。
  5. unbind(self, **kwargs): bund()を使用したイベントとコールバックの関連付けを解除。
  6. funbind(name, func, *args, **kwargs): fbind()を使用したイベントとコールバックの関連付けを解除。
  7. dispatch(self, event_type, *args, **kwargs): イベントをトリガーし、リスナーを呼び出す。
  8. get_property_observers(self, name, args = False ): バインドされているイベントリストを取得。
  9. is_event_type(self, event_type): イベントが登録されているかを調べる。

カスタムイベントを作成する

カスタムイベントを作成・実行するには次の手順で行います。

  1. カスタムイベントを登録する。(register_event_type()__events__を使う)
  2. 必要な場合コールバック関数をバインドする。(bind()fbind()を使う)
  3. イベントをトリガーする。(dispatch()を使う)

サンプルコード

Kivy_events_custom_event1.py

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

class RootWidget(BoxLayout):
    #__events__ = ('on_custom_event',) # (1) カスタムイベントを登録(Static)

    def __init__(self, **kwargs):
        self.register_event_type('on_custom_event') # (1) カスタムイベントを登録(Dynamic)
        self.bind(on_custom_event=self.on_callback) # (2) callbackをバインディング
        super().__init__(**kwargs)

    def on_custom_event(self,*args):
        self.ids.label.text += '\n on_custom_event called'

    def on_callback(self,*args):
        self.ids.label.text += '\n on_callback called'

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

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

Kivyeventscustomevent1.kv

<RootWidget>:
    orientation: 'vertical'
    Label:
        id:label
        text: "Event is dispatched."
        font_size: 30     
    Button:
        text: "Dispatch"
        font_size: 30
        on_press:
            root.dispatch('on_custom_event') # (3) イベントをトリガー

EventDispatcherをインポートする必要がないケース

この例で使用したregister_event_type()bind()dispatch()はEventDispatcherクラスのメソッドですが、これらを使用するためにEventDispatcherをインポートしなくても使用できます。

このコードでは、BoxLayoutを継承したサブクラスを使用しています。BoxLayoutはWidgetクラスに属しています。そして、WidgetクラスはEventDispatcherクラスを継承しているのでインポートしなくてもこれらのメソッドを使用することができる訳です。つまり、UIウィジェット、レイアウトウィジェット、Widgetクラスのどれかを継承したサブクラスの場合はインポートする必要はありません。

下記のようにAppサブクラスの構成の場合はEventDispatcherを継承したサブクラスの定義が必要です。

from kivy.event import EventDispatcher
from kivy.app import App

def CustomEvent(EventDispatcher):
    pass

def CustomEventApp(App):
   pass

Step1:カスタムイベントを登録する

カスタムイベントを登録するには2つの方法があります。

  1. register_event_type()を使う方法
  2. 特殊変数__events__を使う方法

この例で登録するイベント関数は下記になります。引数の*argsは省略可能。

    def on_custom_event(self,*args):
        self.ids.label.text += '\n on_custom_event called'

register_event_typeを使う方法

register_event_type()でカスタムイベントの登録するには、下記の条件を満たす必要があります。

  1. ‘on_’から始まる接頭辞にする。
  2. クラス内でカスタムイベントが定義されていること。

これらの条件を守らないとエラーが発生します。

instance.register_event_type(event_type)

self.register_event_type('on_custom_event') # (1) カスタムイベントを登録(Dynamic)

引数のevent_typeにはカスタムイベントを指定しますが、1つのイベントしか登録できません。複数のイベントを登録したい場合は複数行記述します。

self.register_event_type('on_custom_event1')
self.register_event_type('on_custom_event2')
self.register_event_type('on_custom_event3')

特殊変数__events__を使う方法

特殊変数を使う方法は複数のイベントをリストで指定できます。

__events__ = (‘event_name1‘,’event_name2‘,’event_name3‘,)

リストの最後はカンマが必要です。

__events__ = ('on_custom_event',) # (1) カスタムイベントを登録(Static)

両者の違い

どちらを使っても問題なさそうですが、register_event_type()は動的なイベントを登録する場合や静的なイベントを登録する場合の両方に適しています。一方で__events__は静的なイベントを登録する場合に適しています。動的なイベントを登録する場合はregister_event_type()の一択になります。

静的なイベントとは、最初からイベントが定義されており、クラスのインスタンスが生成された後に処理が変わらない不変のイベントの事を指します。一方で動的なイベントとはクラスのインスタンスが生成された後にイベントを追加するなどの動作をする変動するイベントの事を指します。

Step2:コールバック関数をバインドする

コールバック関数をバインドするにはbind()fbind()を使います。通常はbind()で十分ですが、頻繁にイベントを使用する場合やパフォーマンスを重視する場合はfbind()を使う選択肢もあります。fbind()bind()より高速な処理が可能で高機能なバンディング方式です。fbind()の使い方については割愛させて頂きます。

bind()の書き方

この例でのコールバック関数は下記になります。

    def on_callback(self,*args):
        self.ids.label.text += '\n on_callback called'

*argsを省略した場合、下記のエラーが発生するので注意してください。takes 1 positional argument but 2 were givenのエラーは呼び出し元の引数と定義した引数の数が合っていないと発生するTypeErrorです。

   File "kivy\\_event.pyx", line 727, in kivy._event.EventDispatcher.dispatch
   File "kivy\\_event.pyx", line 1307, in kivy._event.EventObservers.dispatch
   File "kivy\\_event.pyx", line 1231, in kivy._event.EventObservers._dispatch
 TypeError: RootWidget.on_callback() takes 1 positional argument but 2 were given

bind()は下記のように記述します。

instance_name.bind(event_name = callback_function)

self.bind(on_custom_event=self.on_callback)
kv言語を使用する場合

通常はkv言語でバインドすることができますが、カスタムイベントの場合はエラーが発生します。Kivyのビルトインイベントと同じように扱うことはできないみたいです。

# py file
# カスタムイベントを登録
self.register_event_type('on_custom_event')
# kv file
# これはエラーになる
on_custom_event: root.on_callback()

オーバーライドしたKivyのビルトインイベントを登録した場合はエラーになりません。

# py file
# オーバーライドしたビルトインイベントを登録
self.register_event_type('on_press')
# kv file
# これはエラーにならない
on_press: root.on_callback()

Step3:イベントをトリガーする

イベントをトリガーさせるにはdispatch()を使います。

on_press:
    root.dispatch('on_custom_event') # (3) イベントをトリガー

この例ではボタンが押された時にカスタムイベントが実行します。今回はこの場所に記述しましたが、例えば、コンストラクタ内で記述するとアプリが起動した時にイベントを実行させることもできます。

カスタムイベントを操作する

この記事の冒頭でも紹介しましたが、EventDispatcherクラスにはカスタムイベントを操作するためにメソッドが用意されています。以下のメソッドを使ったサンプルコードを紹介します

  1. unregister_event_type(self, event_type): カスタムイベントの登録を解除。
  2. unbind(self, **kwargs): bund()を使用したイベントとコールバックの関連付けを解除。
  3. get_property_observers(self, name, args = False ): バインドされているイベントリストを取得します。
  4. is_event_type(self, event_type): イベントが登録されているかを調べます。

サンプルコード

Kivy_events_custom_event2.py

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

class RootWidget(BoxLayout):
    __events__ = ('on_dynamic_event', 'on_custom_event',)

    def __init__(self, **kwargs):
        self.bind(on_custom_event=self.on_callback)
        super().__init__(**kwargs)
        self.dispatch('on_dynamic_event')

    # カスタムイベント1
    def on_custom_event(self,*args):
        self.ids.label.text += '\n on_custom_eventが呼ばれました'

    # カスタムイベント2
    def on_dynamic_event(self,*args):
        self.ids.label.text += '\n on_dynamic_eventが呼ばれました'

    # コールバック
    def on_callback(self, *args):
        self.ids.label.text += '\n on_callbackが呼ばれました'

    # 登録したイベントを解除する。
    def unregister_event(self):
        if self.is_event_type('on_dynamic_event'):
            self.unregister_event_type('on_dynamic_event')

            self.ids.label.text = 'on_dynamic_eventの登録を解除しました'
            self.ids.button1.text = 'Re-register Dynamic event'
        else:
            self.register_event_type('on_re_register')
            self.dispatch('on_re_register')

    # コールバック関数のbindを解除する。
    def unbind_callback(self):
        if self.is_callback() == None:
            self.ids.label.text = 'Error: リストでNoneが返されました'

        if self.is_callback() == True:
            self.unbind(on_custom_event=self.on_callback)
            self.ids.label.text = 'on_callbackを解除しました'
            self.ids.button2.text = 'Re-bind Callback'
        elif self.is_callback() == False:
            self.register_event_type('on_re_bind')
            self.dispatch('on_re_bind')

    # コールバック関数がバインドされてるか確認する。
    def is_callback(self):
        get_callback = self.get_property_observers('on_custom_event', args = False)
        for observer in get_callback:
            if observer.method_name == 'on_callback':
                return True
            elif observer.method_name == None:
                return None
        return False
    
    # 解除されたカスタムイベントを再登録する。
    def on_re_register(self):
        self.register_event_type('on_dynamic_event')
        self.ids.label.text = 'on_dynamic_eventを登録しました'
        self.ids.button1.text = 'Unregister Dynamic event'
        self.dispatch('on_dynamic_event')
    
    # 解除されたコールバック関数を再度バインドする。
    def on_re_bind(self):
        self.bind(on_custom_event=self.on_callback)
        self.ids.label.text = 'on_callbackをバインドしました'
        self.ids.button2.text = 'Unbind Callback'

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

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

kivyeventscustomevent2.kv

<RootWidget>:
    orientation: 'vertical'
    Label:
        id:label
        text: ""
        font_size: 30
    
    BoxLayout:
        Button:
            id: button1
            text: "Unregister Dynamic event"
            font_size: 30
            on_press: root.unregister_event()
                
        Button:
            id: button2
            text: "Unbind Callback"
            font_size: 30
            on_press: root.unbind_callback()

        Button:
            text: "call Custom event"
            font_size: 30
            on_press: root.dispatch('on_custom_event')

コード解説

このサンプルコードでは下記のメソッドで構成されています。

# カスタムイベント1
def on_custom_event(self,*args):
# カスタムイベント2
def on_dynamic_event(self,*args):
# コールバック
def on_callback(self,*args):
# 登録したイベントを解除する。
def unregister_event(self):
# コールバック関数のbindを解除する。
def unbind_callback(self):
# コールバック関数がバインドされてるか確認する。
def is_callback(self):
# 解除されたカスタムイベントを再登録する。
def on_re_register(self):
# 解除されたコールバック関数を再度バインドする。
def on_re_bind(self):
  1. アプリが起動するとon_dynamic_event()がトリガーします。
  2. Button1は登録したイベントの解除と再登録を繰り返します。
  3. Button2はコールバック関数のバインドとバインドの解除を繰り返します。
  4. Button3on_custom_even()をトリガーします。
カスタムいべんとを操作するアプリの処理フロー。

登録したカスタムイベントを解除する

登録したカスタムイベント解除するにはunregister_event_type()を使用します。またカスタムイベントが存在するかをチェックするためにis_event_type()を使用します。

is_event_type()

is_event_type()はパラメータのevent_typeが登録されている場合はTrueを返し、登録されていない場合はFalseを返します。

instance.is_event_type(self, event_type)

event_typeにはregister_event_type()もしくは__events__で登録したイベント名を指定します。

if self.is_event_type('on_dynamic_event'):
unregister_event_type()

unregister_event_type()register_event_type()もしくは__events__で登録したイベントを解除します。

instance.unregister_event_type(self, event_type)

event_typeにはregister_event_type()もしくは__events__で登録したイベント名を指定します。

self.unregister_event_type('on_dynamic_event')

コールバック関数のbindを解除する

コールバックのバインドを解除するにはunbind()を使用します。またバインドされているかを調べるためにget_property_observers()を使用します。

get_property_observers()

get_property_observers()は、プロパティまたはイベントにバインドされているコールバック関数のリスト返します。

instance.get_property_observers(self, name, args = False)

name: バインド元のプロパティまたはイベントを指定します。

args: バインドされたコールバック関数の引数を返すかどうかをbool値で指定します。デフォルトはFalseです。Falseの場合、コールバック関数のみが返され引数は返されません。Trueの場合、引数も一緒に返されます。また、引数リストの各要素は(callback, largs, kwargs, is_ref, uid)の5要素になります。is_refcallbackがweakref(弱参照)であるかどうかを示し、uidfbind()が使用された場合はuidを示し、bind()が使用された場合はNoneになります。

# Trueの場合に返されるリスト
[(<WeakMethod proxy=<__main__.RootWidget object at 0x00000164BC361240> method=None method_name=on_callback>, (), {}, 1, None)]

# Falseの場合に返されるリスト
[<WeakMethod proxy=<__main__.RootWidget object at 0x00000164BC361240> method=None method_name=on_callback>]

method_nameでコールバック関数名が取得できるので分岐処理をしてフラグを返します。

    # コールバック関数がバインドされてるか確認する。
    def is_callback(self):
        get_callback = self.get_property_observers('on_custom_event', args = False)
        for observer in get_callback:
            if observer.method_name == 'on_callback':
                return True
            elif observer.method_name == None:
                return None
        return False

get_callbackにはget_property_observers()のリストが代入されているのでfor文で展開し、’on_callback‘に一致した場合にTrueを返します。

この例では、引数なしコールバック関数をバインドしてるのでmethod_nameが取得できたんですが、引数付きコールバック関数をバインドした場合、method_nameNoneが入る場合やmethod_nameが取得できない現象が起こりました。これの詳細は下記の記事にまとめました。

unbind()

bind()でイベントに紐づけたコールバック関数を解除します。コールバック関数がイベントに複数回バインドされている場合、最初にバインドしたコールバック関数が解除されます。

self.bind(on_custom_event=self.on_callback_1) # これだけが解除される。
self.bind(on_custom_event=self.on_callback_2)
self.bind(on_custom_event=self.on_callback_3)

fbind()でバインドされた引数なしのコールバック関数であればunbind()が使用できます。引数付きコールバック関数の場合はバインド解除に失敗するので、funbind()を使用します。

instance.unbind(self, **kwargs)

self.unbind(on_custom_event=self.on_callback)

カスタムイベントのPropagation

カスタムイベントの伝播の実装については次の記事で紹介しています。

Comment

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