PR
Python+Kivy入門

on_touch_downの正しい書き方とイベントの仕組み: Kivyのイベント1

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

Kivyのイベントの仕組みとKivyが提供しているイベントハンドラの使い方を紹介します。この記事ではon_touch_downの書き方を例に解説しています。Kivyでは標準でいくつかのイベント関数が用意されていますが、独自のカスタムイベントを作ることも可能です。

Kivyのイベントの種類

Kivyのイベントは大きく分けると3つの種類に分類されます。この記事ではKivyが提供している標準イベントハンドラの使い方について説明します。

  • Kivy標準イベント
  • カスタムイベント
  • Clock()Property()イベント

Kivyの標準イベント

Kivy標準イベントハンドラの殆どは”on_”から始まる接頭辞の付いた関数です。これらのイベントハンドラはウィジェットに標準で組み込まれていて、on_press()on_release()などいくつかのウィジェットで共通で使えるイベントハンドラと、そのウィジェット特有のイベントハンドラがあります。またウィジェットに関連されたイベントハンドラの他にWindowクラスのイベントハンドラも多数用意されています。ほとんどの場合はこれらのKivy標準イベントと自作コールバック関数を組み合わせることでイベント処理は成り立ちます。以下にイベントハンドラの一部を列挙します。

  • on_press() : マウスクリックが押された時に呼び出されるイベント。
  • on_release() : クリックが離されるときに呼び出されるイベント。
  • on_touch_down() : マウスクリックやタッチ入力が画面に触れたときに呼び出されるイベント。
  • on_touch_move() : タッチやドラッグ操作が動いたときに呼び出されるイベント。
  • on_touch_up() : タッチ操作が離れたときに呼び出されるイベント。
  • on_size() : ウィジェットのサイズが変更されたときに呼び出されるイベント。
  • on_kv_post() : kv言語のロード後に呼び出されるイベント。

カスタムイベント

カスタムイベントは独自にイベント関数を作成して登録し、イベントをDispatch(イベントの発生を管理)することができます。またbind()fbind()を使用してイベントにコールバック関数を紐づけることも可能です。

ClockやPropertyなどのイベント

Clockイベント

KivyのClockイベントは一定の時間の間隔で関数を呼び出したい場合や、一定時間経過後に関数を呼び出した場合など、時間で管理したい関数を扱うときに便利です。

Propertyイベント

Propertyクラスはデータを扱うイベントです。バインドされたPropertyの値が更新されるとKivyのオブザーバー設計により自動的に値が更新されます。これは主にpythonコードで書く場合に使用されます。

Kivyのイベントの仕組み

KivyのイベントはEventDispatcherクラスによって管理されています。EventDispatcher()はイベントの登録、バインド、イベントの発生を検知し、関連するコールバック関数を実行します。

ここでイベントに関連する用語を整理しておきましょう。

  • Dispatch : 待機中のイベントを監視し、イベントハンドラーや対応するコールバック関数の実行を許可する役割を持っています。Kivyではイベントを発生させることを指してる場合があります。
  • Event Listeners: イベントの発生を監視する役割を持っています。
  • Event Handlers: イベントが発生したときに実行される処理を記述する関数やメソッドです。
  • Callback functions :ある 関数から他の関数を呼び出す場合や他の関数に引数として渡される関数のことを指します。例えばA関数からB関数を呼び出す。または、A関数の引数としてB関数を指定する。これらの例ではB関数がコールバック関数にあたります。
  • bind : イベントハンドラーとコールバック関数の紐づけを行います。
KivyのEvent Dispatcherフロー

Kivyで例えると、ユーザーがボタンをクリックするとイベントリスナーによって監視されていたイベントがトリガーされ、Kivy標準イベントハンドラーのon_press()が呼ばれます。そして、コールバック関数としてバインドしていたon_callback()が呼ばれます。これらはEvent Dispatcher()で管理されています。on_press()Buttonのイベントで、ボタンが押された瞬間にトリガーするイベントです。ボタンが押されたらコールバック関数のon_callback()を呼び出すといった形になります。

Kivyイベントのライフサイクル

Kivy全体のイベントのライフサイクルをみてみましょう。Kivyアプリケーションを起動するとkvファイルの読み込みやウィジェットツリーの構築が行われるとイベントを待機するメインループが開始され、このループはアプリケーションが終了されるまで続きます、ユーザーのクリックやタップなどの動作が発生するとイベントを監視するEvent Dispatcher()がイベントの発生を検知し、イベントリスナーを呼び出します。次にイベントに関連したイベントハンドラとコールバック関数を呼び出し実行します。次にWindowクラスのイベントがある場合はそれを実行します。Clockクラスは別のループになっています。

Kivyイベントのライフサイクル

イベントの書き方

Kivy標準イベントハンドラの書き方を説明します。イベントの書き方はPythonコードで書く場合とkvコードで書く場合では書き方が異なります。

Pythonコードのイベントの書き方

この例では、Kivy標準のイベントハンドラを使用する方法とKivy標準イベントハンドラにコールバック関数をバインドして呼び出す方法の2種類のイベントの使い方を書いています。

kivy_events1.py

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

class MyLabel(Label):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self.text = "Label 2 touched!"
            return True

class MyApp(App):
    def build(self):
        layout = BoxLayout(orientation='vertical')
        
        self.label1 = Label(text="Label 1 : Click Button.")
        self.label2 = MyLabel(text="Label 2 : Pressing this label triggers a touch event.")

        button = Button(text="Click here")
        button.bind(on_press=self.on_button_press)

        layout.add_widget(self.label1)
        layout.add_widget(self.label2)
        layout.add_widget(button)
        
        return layout

    def on_button_press(self, instance):
        self.label1.text = "Button pressed!"
        self.label2.text= instance.text

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

Pythonコード解説

このサンプルでは、ButtonをクリックするとLabel1のtextを変更し、Label2にButtontextを表示します。また、Label2をクリックするとLabel2のtextを変更します。

<RootWidget>
+--- Label
+--- Label
+--- Button

Kivy標準イベントのon_touch_downの書き方

on_touch_*は伝播する

下記のコードはon_touch_down()の正確な書き方を示しています。on_touch_down()は伝播の仕組みを持っているので2つのreturnを書くことで伝播を制御します。return Trueで伝播を停止し、return super()で親のon_touch_down()を呼び出しています。伝播は最終的にWidgetクラスのon_touch_down()まで到達します。

class MyLabel(Label):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self.text = "Label 2 touched!"
            return True
        return super().on_touch_down(touch)

伝播についての詳細は下記の記事をご覧ください。

on_touch_down()を呼んでもいないのに勝手に呼ばれる例を紹介します。下記のコードでは、label3のLabelをドラッグすると、label3から見て親であるlabel2on_touch_down()も一緒に呼ばれます。これは伝播の制御をしていないため発生します。kvコードではこの制御はできないのでPythonコードで記述する必要があります。

<RootWidget>:
    orientation: 'vertical'
    Label:
        id: label1
        size_hint: None, None
        size: 100, 50
        pos_hint: {'center_x': 0.5, 'center_y':0.7}
        text: 'Labelをタッチするとイベントが発生するよ'
    Label:
        id: label2
        text: 'タッチして'
        size_hint: None, None
        size: 100, 50
        pos_hint: {'center_x': 0.5, 'center_y':0.5}
        on_touch_down: label1.text = 'on_touch_downが呼ばれたよ'
        on_touch_up: label1.text += '\non_touch_upが呼ばれたよ'
    Label:
        id: label3
        text: 'ドラッグして'
        size_hint: None, None
        size: 100, 50
        pos_hint: {'center_x': 0.5, 'center_y':0.3}
        on_touch_move: label1.text = 'on_touch_moveが呼ばれたよ'

上記kvコードのon_touch_down()をPythonコードで書くと下記と同じ意味になります。if文とreturn文の行は書かなくてもこれは動作しますが、kvコードの例のように複数のウィジェットでon_touch_*を呼び出すと伝播します。

class MyLabel(Label):
    def on_touch_down(self, touch):
        self.text = "Label 2 touched!"

伝播を使用しないのであれば、下記のように書けば伝播されません。伝播されないようにするサンプルを後の章で紹介しているのでそちらをご覧ください。

class MyLabel(Label):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            self.text = "Label 2 touched!"
            return True

on_touch_down()の書き方

Kivy標準イベントハンドラを使用する場合はイベントハンドラをオーバーライドします。この例ではLabel2on_touch_down()を使用するためにLabelを継承したクラスを定義し、on_touch_down()をオーバーライドして、処理を追加しています。

def on_touch_down(self, touch):

パラメーターのtouchは親の座標が渡されます。

if self.collide_point(*touch.pos):

collide_point()はタッチポイント (x, y) がウィジェットの領域内にあるかどうかをチェックします。touch.posはタッチの位置を示す座標です。つまり、タッチの位置がLabel2の領域内にあるかどうかをチェックしています。

self.text = "Label 2 touched!"

ここではLabel2のtextを変更する処理を追加しています。

return True

この行はKivyイベントの伝播の仕組みに使うためのフラグです。Trueにすることで伝播を停止しています。

Kivy標準イベント+コールバック関数をbindする方法

Kivy標準イベントハンドラにコールバック関数をバインドして呼び出す方法を解説します。

# ボタンを定義し、on_pressイベントハンドラとコールバック関数をバインドしています。
button = Button(text="Click here")
button.bind(on_press=self.on_button_press)

Kivy標準イベントハンドラとコールバック関数を紐づけるにはbind()を使用します。

# bind書き方
instance_name.bind(event_name = callback_function)

この例ではevent_nameにKivy標準イベントのon_press()を指定し、callback_functionに関数のon_button_press()を指定しています。

    def on_button_press(self, instance):
        self.label1.text = "Button pressed!"
        self.label2.text= instance.text

コールバック関数の処理になります。instanceにはイベントをトリガーしたウィジェットが渡されるため、どのウィジェットからイベントが発生したかを特定することができます。この例ではButtonのインスタンスが渡されます。例えば、複数のボタンが同じイベントハンドラを使用している場合、instanceを使用してどのボタンが押されたかを判断するのに役立ちます。

kv言語のイベントの書き方

kv言語で書く場合はかなり簡単にイベント関数を呼び出すことができます。bind()は明示的に行う必要はありません。同じツリー内のウィジェットを参照するだけの簡単な処理の場合はkv言語で記述した方が良い場合があります。しかし、複雑な多くの処理をしたい場合は向いていません。

このサンプルでは前章のPythonコードと同じ処理が実行されますが、kv言語ではreturnが使えないので伝播の制御をしたい場合はPythonコードで記述します。

kivy_events2.py

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

class RootWidget(BoxLayout):
    def on_button_press(self, text):
        self.ids.label1.text = "Button pressed!"
        self.ids.label2.text = text

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

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

kivyevents2.kv

<RootWidget>:
    orientation: 'vertical'
    Label:
        id: label1
        text: 'Label 1 : Click Button.'
    Label:
        id: label2
        text: 'Label 2 : Pressing this label triggers a touch event.'
        on_touch_down: 
            if self.collide_point(*args[1].pos): self.text = "Label 2 touched!"
    Button:
        text: 'Click here'
        on_press: root.on_button_press(self.text)

kvコード解説

kv言語でKivy標準イベントハンドラを使うには、イベントを使いたいウィジェットのプロパティにイベント名を指定します。

event_name: processing

コールバック関数を呼び出したい場合は下記のようになります。

event_name: keyword.callback_function

Kivy標準イベントのon_touch_downの使い方
on_touch_down: 
    if self.collide_point(*args[1].pos): self.text = "Label 2 touched!"

簡単な処理であればkv言語で書くことも可能です。if文やfor文も使用できます。この例ではPythonコードと同じようにcollide_point()を使用してタッチポイント (x, y) がウィジェットの領域内にあるかどうかをチェックしています。

args[1]はタプルで指定する書き方になります。on_touch_down()に渡される引数は通常selftouchの2つです。これらの引数はargsとして参照することができます。args[0]selfを指し、args[1]touchを指しています

self.text = "Label 2 touched!"

#idで指定する場合
label2.text = "Label 2 touched!"

self.textでは自身のLabel2のtextを変更するのでselfキーワードになります。今回はidを設定しているのでid_name.textとしても書けます。

Kivy標準イベント+コールバック関数をbindする方法
on_press: root.on_button_press(self.text)

kv言語でコールバック関数をバインドするにはkeyword.callback_functionを記述します。このように書けばKivyによって自動的にバインドされます。引数にself.textを指定し、自身であるButtontextを渡しています。

    def on_button_press(self, text):
        self.ids.label1.text = "Button pressed!"
        self.ids.label2.text = text

Pythonコード側で書いたコールバック関数になります。引数textでkv言語側から渡された引数を受け取って変更しています。

キーワードやidについての詳細は下記の記事も合わせてご覧ください。

伝播を使いたくない場合の書き方

2025.1.31追記

on_touch_*はコード内で1つのウィジェットで使う分には問題ないですが。複数のウィジェットで使用すると嫌でも伝播されてしまいます。以下に伝播を完全に使用しない場合のサンプルを示します。

kivy_events3.py

from kivy.app import App
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.label import Label

class CustomLabel(Label):
    def on_touch_down(self, touch):
        if self.collide_point(*touch.pos):
            root_widget = self.parent
            root_widget.ids.label1.text = 'on_touch_downが呼ばれたよ'
            return True
        
    def on_touch_up(self, touch):
        if self.collide_point(*touch.pos):
            root_widget = self.parent
            root_widget.ids.label1.text = 'on_touch_upが呼ばれたよ'
            return True

class RootWidget(RelativeLayout):
    def on_touch_move(self, touch):
        if self.collide_point(*touch.pos):
            self.ids.label1.text = '\non_touch_moveが呼ばれたよ'
            return True
        
class kivy_events3(App):
    def build(self):
        return RootWidget()

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

kivyevents3.kv

<RootWidget>:
    orientation: 'vertical'
    Label:
        id: label1
        text: 'Labelをタッチするとイベントが発生するよ'
        size_hint: None, None
        size: 100, 50
        pos_hint: {'center_x': 0.5, 'center_y':0.7}
    CustomLabel:
        text: 'タッチして'
        size_hint: None, None
        size: 100, 50
        pos_hint: {'center_x': 0.5, 'center_y':0.5}
    Label:
        id: label3
        text: 'ドラッグして'
        size_hint: None, None
        size: 100, 50
        pos_hint: {'center_x': 0.5, 'center_y':0.3}    

コード解説

本来であれば、タッチすると全てのラベルに対して伝播するようになっています。前の章でreturn Trueを記述することで伝播されないと言いましたが、実はそれだけでは不十分です。

このコードではCustomLabelを定義しRootWidgetと分離していますが、RootWidget内に全てのイベントをオーバーライドすることもできます。

class RootWidget(RelativeLayout):
    def on_touch_down(self, touch):
        pass     
    def on_touch_up(self, touch):
        pass
    def on_touch_move(self, touch):
        pass

しかし、この構成にすると伝播されてしまいます。完全に伝播されないようにするにはon_touch_down()を別のクラスに分離します。(もしくはon_touch_down()にさらにフラグで処理を分ける方法もあります。)

つまり、RootWidgeton_touch_down()を書かないことで完全に伝播を使わないようにできます。

on_touch_*イベントの場合は、Pythonコードでイベントをオーバーライドした場合は、kvコードの方でイベントの記述は必要ありません。

    CustomLabel:
        text: 'タッチして'
        size_hint: None, None
        size: 100, 50
        pos_hint: {'center_x': 0.5, 'center_y':0.5}
    Label:
        id: label3
        text: 'ドラッグして'
        size_hint: None, None
        size: 100, 50
        pos_hint: {'center_x': 0.5, 'center_y':0.3}    

kv言語のチェーン処理

kv言語でイベント記述する際に役に立つのがセミコロンを使用したチェーン処理になります。セミコロンで区切った順番で処理が実行されます。

processing1; processing2; processing3…

on_touch_down:
    if self.collide_point(*args[1].pos): self.text = "Label 2 touched!"; app.on_label_touch(self, *args)

コードが長くなって見づらい場合は、セミコロンの代わりに改行を使うこともできます。ただし、ifforなどの制御構文の途中で改行するとエラーになるので注意してください。

processing1
processing2
processing3

on_touch_down:
    if self.collide_point(*args[1].pos): self.text = "Label 2 touched!"
    app.on_label_touch(self, *args)

Comment

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