イベント処理 - Kivy Advent Calendar 2013

相変わらずネタ切れ/時間切れで申し訳ないです。イベント処理の問題で少し加筆 (http://d.hatena.ne.jp/cheeseshop/20131205) したので、その件で少し書くことにします。
Kivyのイベント処理は、各ウィジェットクラスが「あるタイミング」で「イベントに結びつけた関数群を実行する」というものです。今回のAdvent Calendarで使ってきたものを挙げると

記事 クラス イベント 動作
2013-12-01 ファイル選択 Button on_release Load/Cancelボタンを押したら実行(ダミー)
2013-12-03 QRコード TextInput on_text_validate Enterキーを押したらQRコード生成
2013-12-05 スライド RstDocument on_touch_up タップしたらポップアップを開く
2013-12-08 ポップアップ FloatLayout on_touch_up タップしたらバブルを開く
2013-12-09 画面転換 Spinner on_text 選択肢を変更したら画面効果差替
2013-12-09 画面転換 Button on_press ボタンを押したら別スクリーンに転換
2013-12-10 アプリ復帰 App on_pause 別のアプリへの切替時
2013-12-10 アプリ復帰 App on_resume 別のアプリからの復帰時
2013-12-11 カード Scatter on_touch_up タッチしたらひっくり返す
2013-12-13 流行語 Button on_press ボタンを押したら喋る
2013-12-14 写真 StackLayout on_touch_up タッチしたらカメラ起動
2013-12-14 写真 PythonActivity on_activity_result カメラ終了のタイミング
2013-12-15 テキスト共有 Button on_press ボタンを押したらメーラ起動
2013-12-16 Webブラウザ Button on_press ボタンを押したらブラウザ起動
2013-12-16 Webブラウザ Button on_press ボタンを押したらブラウザ起動
2013-12-18 Launcher Button on_press ボタンを押したらKivyLauncher起動
2013-12-19 キャラ移動 Button on_press ボタンを押したらキャラクタ移動
2013-12-19 キャラ移動 Animation on_progress 画像を順次変えてアニメ
2013-12-19 キャラ移動 Animation on_complete 排他制御を解除

まあ「ボタンを押して○○する」のが多いです。でも中にはアニメ、アプリ、アクティビティのようにKivyにおけるユーザ操作とは関係なくイベントが発生するものがあります。

on_touch_up / on_touch_downについて

さて、問題だったのは「2013-12-05 スライド」のon_touch_upです。ここでは画面をダブルタップしたらreStructureTextソースをポップアップを見せていましたが、ここではon_touch_upの戻り値で問題が解決したかのように書いていました。実際にはそうではありませんでした。一見「今開いたスライド」のソースを見せていましたが、実は

  • 全部のスライド (page01〜page03) のソースが一斉に開く (どれが一番上になるかは不定)
  • 外部をタップしたときには3つとも閉じた

というものでした。必ずしも表示しているスライドのソースが見えなかったということです。
何故こうなるのかといえば、on_touch_upというのはタッチした場所にあるウィジェットへ送られるイベントではなく、単に「タッチパネルから指が離れた」というイベントだからです。
つまり、「タッチした場所以外に置かれたウィジェット」に対してもon_touch_upは発生するし、しかもScreenManagerやCarouselのように画面転換によって「非表示になっているウィジェット」にもon_touch_upは発生してしまうのでした。

:
    orientation: 'vertical'
    Label:
        text: 'LABEL1'
        on_touch_down: self.text = 'LABEL1: DOWN'
        on_touch_up: self.text = 'LABEL1: UP'
    Label:
        text: 'LABEL2'
        on_touch_down: self.text = 'LABEL2: DOWN'
        on_touch_up: self.text = 'LABEL2: UP'
    Label:
        text: 'LABEL3'
        on_touch_down: self.text = 'LABEL3: DOWN'
        on_touch_up: self.text = 'LABEL3: UP'

3つのラベルがあってタッチによって文字列を変えようとしていますが、これでは3つとも一斉に書き換わってしまいます。正しく動かすには次のようにしなければなりません。

:
    orientation: 'vertical'
    Label:
        text: 'LABEL1'
        on_touch_down: self.collide_point(*args[1].pos) and setattr(self, 'text', 'LABEL1: DOWN')
        on_touch_up: self.collide_point(*args[1].pos) and setattr(self, 'text', 'LABEL1: UP')
    Label:
        text: 'LABEL2'
        on_touch_down: self.collide_point(*args[1].pos) and setattr(self, 'text', 'LABEL2: DOWN')
        on_touch_up: self.collide_point(*args[1].pos) and setattr(self, 'text', 'LABEL2: UP')
    Label:
        text: 'LABEL3'
        on_touch_down: self.collide_point(*args[1].pos) and setattr(self, 'text', 'LABEL3: DOWN')
        on_touch_up: self.collide_point(*args[1].pos) and setattr(self, 'text', 'LABEL3: UP')

on_touch_up/on_touch_downの式では、args[1].posでタッチした位置の座標(x,y)が取れ、self.collide_point(x,y)でウィジェット内部かどうかのチェックができます。でもkvレイアウト言語の中なのでandを条件文の代わりにして、代入式が書けないからsetattrに置き換えて…こうしてやっと個別のイベント処理が出来ることになります。
(まあ一般的にイベントハンドラは、kv言語よりもメソッドで書く方が楽だと思います…)
ScreenManagerやCarouselの非表示ウィジェットについては、それが現在表示されているものか調べることができますから、それを条件にしてイベントを受け付けるか受け付けないかを切り替えることになります。

self.collide_point(*touch.pos) 自身にタッチしたか
self.manager.current_screen is self ScreenManagerが自身を表示しているか
self.parent.current_slide is self Carouselが自身を表示しているか (2013-12-05 スライドはこれを使用)

on_touch_up/on_touch_downは、全画面で使う分にはこういった問題はない (2013-12-08 ポップアップなど) のですが、複数あったり非表示要素がある場合は注意が必要です。