Kivyの基本部品 EventDispatcher

# EventDispatcher

EventDispatcherはKivyの基本的なeventの仕組みを実装している部品のようです。様々な部品(Widget,Animation,App,Sound等)がこのEventDispatcherを継承していて、「Buttonを押した時に何かをする」や「Animationが完了した時に何かする」、「アプリが中断された時に何かする」、「音が鳴り止んだ時に何かする」等のように何かをきっかけに処理を行う仕組みを用意してくれています。

## `bind()`

それでは実際に使ってみます。eventと言えばButtonが定番だと思うのでまずはそれを使った例から。

```python3
from kivy.app import runTouchApp
from kivy.factory import Factory

button = Factory.Button(
    text='Press Me',
    font_size='80sp',
)

button.bind(on_press=lambda button: print('callback1'))
button.bind(on_press=lambda button: print('callback2'))

runTouchApp(button)
```

![08_00.png](https://qiita-image-store.s3.amazonaws.com/0/144673/a425843f-4cb5-98a2-1ae2-dd8694e9557a.png)

以下がButtonを一回押した時の出力で、渡した関数が渡した順とは逆に呼ばれているのが確認できます。

```text
callback2
callback1
```

もう一つの例も見てみます。

```python
from kivy.app import runTouchApp
from kivy.factory import Factory

button = Factory.Button(
    text='Press Me',
    font_size='80sp',
)
button.bind(
    on_press=lambda button: print('Buttonが押されました'),
    on_release=lambda button: print('Buttonが離されました'),
)

runTouchApp(button)
```

```text
Buttonが押されました
Buttonが離されました
```

このように複数のeventに対して<ruby>纏<rt>まと</rt></ruby>めて関数を渡す事もできます。またwidgetであれば以下のように初期化時にも渡せます。

```python3
from kivy.app import runTouchApp
from kivy.factory import Factory

runTouchApp(Factory.Button(
    text='Press Me',
    font_size='80sp',
    on_press=lambda button: print('Buttonが押されました'),
    on_release=lambda button: print('Buttonが離されました'),
))
```

## `dispatch()`

上の例なのですが一々Buttonを押して動作確認するのが面倒くさいと感じたのなら以下のように書くことができます。


```python3
from kivy.factory import Factory

button = Factory.Button()
button.bind(
    on_press=lambda button: print('Buttonが押されました'),
    on_release=lambda button: print('Buttonが離されました'),
)
button.dispatch('on_press')
button.dispatch('on_release')
```

```text
Buttonが押されました
Buttonが離されました
```

このように書くことで実際にButtonを押さずともeventを発生させられます。実際これはButtonが押された時に自ら呼んでいるmethodです。

## `unbind()`

`bind()`で結びつけた関数は`unbind()`で<ruby>解<rt>ほど</rt></ruby>く事ができます。

```python3
from kivy.factory import Factory

def callback2(button):
    print('callback2')

button = Factory.Button()
button.bind(on_press=lambda button: print('callback1'))
button.bind(on_press=callback2)
button.bind(on_press=lambda button: print('callback3'))

button.dispatch('on_press')
print('-----------------------------------')
button.unbind(on_press=callback2)
button.dispatch('on_press')
```

```text
callback3
callback2
callback1
-----------------------------------
callback3
callback1
```

## 追加の引数

次は別の`EventDispatcher`である`Animation`も見てみます。

```python3
from kivy.app import runTouchApp
from kivy.factory import Factory
from kivy.animation import Animation

# このLabelをAnimationの対象とする
label = Factory.Label(text='Label', font_size='80sp')
# 徐々に透明にするAnimation
anim = Animation(opacity=0, step=0.1, duration=.5)

anim.bind(
    on_start=lambda anim, *args: print('on_start', *args),
    on_complete=lambda anim, *args: print('on_complete', *args),
    on_progress=lambda anim, *args: print('on_progress', *args),
)
anim.start(label)

runTouchApp(label)
```

![08_01_00.png](https://qiita-image-store.s3.amazonaws.com/0/144673/1ef5c301-bc20-2030-c904-5a45226de7f6.png)
![08_01_01.png](https://qiita-image-store.s3.amazonaws.com/0/144673/54e453e0-9b20-c32d-305f-21308d49d3a6.png)
![08_01_02.png](https://qiita-image-store.s3.amazonaws.com/0/144673/95004cd2-9db9-26d4-0f12-d1e02c7a8278.png)

```text
on_start <kivy.uix.label.Label object at 0x7f2ce3e43388>
on_progress <kivy.uix.label.Label object at 0x7f2ce3e43388> 0.0
on_progress <kivy.uix.label.Label object at 0x7f2ce3e43388> 0.19305010401876643
on_progress <kivy.uix.label.Label object at 0x7f2ce3e43388> 0.38978709201910533
on_progress <kivy.uix.label.Label object at 0x7f2ce3e43388> 0.5864729700260796
on_progress <kivy.uix.label.Label object at 0x7f2ce3e43388> 0.7831993760191835
on_progress <kivy.uix.label.Label object at 0x7f2ce3e43388> 0.9799360600009095
on_progress <kivy.uix.label.Label object at 0x7f2ce3e43388> 1.0
on_complete <kivy.uix.label.Label object at 0x7f2ce3e43388>
```

Animationが

- 始まった時に`on_start`eventが一回
- 進行中に`on_progress`eventが複数回
- 終わった時に`on_complete`eventが一回

発生しているのが見て取れます。Buttonのeventの時と違うのはこれら三つのeventが追加の引数を伴って発生している事です。三つともAnimationの対象となったLabelを伴ってますし、`on_progress`に於いてはさらにAnimationの進行度を割合で表した数値を伴ってます。このように各eventによって伴う引数が異なるのでcallback関数は可変長の引数を受け取れるようにしておくといいかもしれません。

## default handlerを使ったやり方

今度は関数を結びつけるのとは別の方法も見ていきます。それはeventと同じ名前のmethodを上書きする方法です。`EventDispatcher`の一つである`App`を使う時はこの方法を使うことが多いと思います。

```python3
from kivy.app import App


class MyApp(App):
    # on_startというeventと同名のmethodを上書き
    def on_start(self):
        print('default handler')


app = MyApp()
app.bind(on_start=lambda button: print('callback'))  # 勿論bind()も可
app.bind(on_start=lambda button: print('callback2'))
app.run()
```

```text
callback2
callback
default handler
```

## 戻り値の効果

戻り値にも触れておきます。次のコードを見てください。

```python3
from kivy.app import App


class MyApp(App):
    def on_start(self):
        print('default handler')

def callback(app):
    print('callback')
    return True

def callback2(app):
    print('callback2')
    # return True  A行

app = MyApp()
app.bind(on_start=callback)
app.bind(on_start=callback2)
print('戻り値', app.dispatch('on_start'))
```

```text
# default handlerが呼ばれていない。
callback2
callback
戻り値 True
```

またA行を有効にすると出力は

```text
# callback1とdefault handlerが呼ばれていない。
callback2
戻り値 True
```

となります。このように結びつけた関数が真を返すとそれ以降の関数が呼ばれなくなり`dispatch()`も真を返すようになります。(`default handler`が真を返した場合も`dispatch()`は真を返します)。ほとんどのeventにおいてはこの事は意識しなくていいのですが`on_touch_xxx`系のeventではこの仕組みが重要なので触れておきました。

## `bind()`の制限

以下のように同じ物を二度登録しようとしても

```python3
from kivy.factory import Factory


def callback(__):
    print('callback')


button = Factory.Button()
button.bind(on_press=callback)
button.bind(on_press=callback)
button.dispatch('on_press')
```

```text:標準出力
callback
```

となります。`bind()`は同じ物を二度登録できない仕様なのです。なのでそのような事がしたいのなら次に示すより高機能版を使わないといけません。

### `fbind()`と`unbind_uid()`

```python3
from kivy.factory import Factory


def callback(__):
    print('callback')


button = Factory.Button()
uid1 = button.fbind('on_press', callback)
button.bind(on_press=lambda __: print('callback2'))
uid2 = button.fbind('on_press', callback)
button.dispatch('on_press')
print('-----------------')
button.unbind_uid('on_press', uid1)
button.dispatch('on_press')
print('-----------------')
button.unbind_uid('on_press', uid2)
button.dispatch('on_press')
```

```text
callback
callback2
callback
-----------------
callback
callback2
-----------------
callback2
```

この高機能版の特徴としては他にも

- 既定ではcallback関数への直接参照が結び付けられる。(`bind()`は条件付きの弱参照)
- `bind()`より効率的
- callback関数へ渡す引数も指定できる

があります。最後の特徴は分かりにくいと思うので次の例を見てください。

```python3
from kivy.factory import Factory


def callback(*args, **kwargs):
    print('callback', args, kwargs)


button = Factory.Button()
button.fbind('on_press', callback, 123, kivy='awesome')
button.dispatch('on_press')
```

```text
callback (123, <kivy.uix.button.Button object at 0x7f5e4e01aa70>) {'kivy': 'awesome'}
```

位置引数`123`が`Button object`の前に来るのが注意点。

## 自分でeventを作ってみる

今度は使うだけではなく自分で作ってみます。

```python
from kivy.event import EventDispatcher


class MyClass(EventDispatcher):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.register_event_type('on_test')  # 1

    def on_test(self, *args, **kwargs):  # 2
        pass
```

作るには

- eventの名前を決めて登録(名前は必ず`on_`から始まらなくてはいけない)  (# 1)
- `default handler`を実装 (# 2)

するだけです。これでこれまでに出てきたeventと同じように扱えるようになっています。

```python3
a = MyClass()
a.bind(on_test=lambda __, *args, **kwargs: print('on_test', args, kwargs))

a.dispatch('on_test')
print('-------------------')
a.dispatch('on_test', 12, 34, kivy='awesome', python='awesome')
```

```text
on_test () {}
-------------------
on_test (12, 34) {'kivy': 'awesome', 'python': 'awesome'}
```

簡単ですね。言い忘れてましたが`dispatch()`に追加の引数を渡すことでcallback関数にそれが渡ります。

### \_\_events\_\_

eventを簡単に実装する方法としてこの`__events__`という物があります。例えば以下の`MyClass`は

```python3
class MyClass(EventDispatcher):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.register_event_type('on_test1')
        self.register_event_type('on_test2')

    def on_test1(self, *args, **kwargs):
        pass

    def on_test2(self, *args, **kwargs):
        pass
```

`__events__`を使うことで以下のように簡潔に書けます。

```python3
class MyClass(EventDispatcher):

    __events__ = ('on_test1', 'on_test2', )

    def on_test1(self, *args, **kwargs):
        pass

    def on_test2(self, *args, **kwargs):
        pass
```

「最初からこの方法を紹介しろよ」と思うかもしれませんがこれはおそらく内部用の物なので使うか否かは自己責任で。私は使っています。

## Kv言語におけるevent

Kv言語上では以下のように書くことでeventに処理を結びつけられます。

```python3
from kivy.lang import Builder


button = Builder.load_string('''
Button:
    on_press: print('callback1')
    on_press:  # 複数の文も書ける
        msg = 'callback2'
        print(msg)
''')
button.dispatch('on_press')
```

```text
callback2
callback1
```

Kv言語で書く場合はreturn文が使えないのが少し不便。