tkinterのマウスイベントの発生条件を調べる

Python + TkinterGUIアプリケーションを作成しています。 Tkinterのイベントの仕組みを用いてマウスによる操作をハンドリングしようとしていますが、イベントハンドラの発火条件にクセがありそうなので、実際に動かしてみて、発生条件を調べて殴り書きます。気が向いたら最後に整理しようかと。

環境

[前提知識] マウスイベントを処理するには

Python + tkinterでマウスイベントを処理するには、以下のようにします。

widget.bind(event_sequence, event_handler)
  • widget: イベント処理するウィジェットインスタンス。キャンバスとか。
  • event_sequence: イベントの種類を表す文字列。""とか。
  • event_handler: イベントが発行されたときに呼ばれるメソッド。tkinter.EventTypeを第一引数に取る。典型的な定義は、def handler(event): ... とか、クラス内で def handler(self, event): ... みたいな感じ。

キャンバスの上でマウスボタンが押されたら Pressed!! と表示するプログラム例を挙げておきます。

import tkinter

def handler(event):
    print("Pressed!!")

if __name__ == "__main__":
    tk = tkinter.Tk()
    canvas = tkinter.Canvas(tk, bg="black")
    canvas.pack(expand=True, fill=tkinter.BOTH)

    canvas.bind("<ButtonPress>", handler)

    tk.mainloop()

[前提知識] イベントシーケンスについて

bindメソッドの第一引数には、イベントを表す文字列を渡します。この文字列をイベントシーケンスと呼ぶようです。 イベントシーケンスの書式は以下です。

"<" + modifier + "-" + event type + "-" + detail + ">"

なお、modifierはハイフンで区切って複数指定できます。 以下、イベントシーケンスの例。

  • "<ButtonPress-1>" : マウスボタン1(通常は左ボタン)が押された
  • "<B1-Motion>" : マウスボタン1を押しながらマウスが動いた(=左ボタンドラッグされた)
  • "<Control-ButtonRelease-2" : Ctrlを押しながらマウスボタン2(私の環境ではホイールボタン)が離された
  • "<Shift-Control-Motion>" : Shift+Ctrlを押しながらマウスが動かされた

マウス関連の event type

マウスに関するイベントを以下に列挙します。

  • ButtonPress (or Button) : マウスボタンが押された
  • ButtonRelease : マウスボタンが離された
  • Enter : マウスカーソルがウィジェットの領域に入った
  • Leave : マウスカーソルがウィジェットの領域から出た
  • Motion : マウスカーソルがウィジェットの領域上で動いた
  • MouseWheel : マウスのホイールが回転した

マウス関連の modifier

マウスイベントに付随する修飾子を列挙します。

  • Double : マウスボタンが2回連続で押された
  • Triple : マウスボタンが3回連続で押された
  • Shift : Shiftキーが押された状態でイベントが発生した
  • Control : Ctrlキーが押された状態でイベントが発生した
  • Alt : Altキーが押された状態でイベントが発生した
  • Lock : CapsLockがロックされた状態でイベントが発生した
  • B1: ボタン1に関するイベントが発生した
  • B2: ボタン2に関するイベントが発生した
  • B3: ボタン2に関するイベントが発生した
  • B4: ボタン2に関するイベントが発生した
  • B5: ボタン2に関するイベントが発生した

マウス関連の detail

マスイベントに付随する詳述子は以下の通りです。

  • 1 : ボタン1に関するイベント
  • 2 : ボタン2に関するイベント
  • 3 : ボタン3に関するイベント
  • 4 : ボタン4に関するイベント
  • 5 : ボタン5に関するイベント

[本題] 各イベントシーケンスの挙動

さて、マウスイベントをハンドルしたい場合は、前述のイベントシーケンスを組み立てて必要なイベントにイベントハンドラメソッドを割り当てていきます。 ところが、想定したとおりにイベントハンドラが呼び出されない場合があるようなので、いろいろと試してみます。

ButtonPress系

    canvas.bind("<ButtonPress>", handler)

この場合、ウィジェットの上でマウスのボタンを押すとハンドラが呼ばれます。挙動としては普通な感じ。

  • どのボタンでも呼ばれます。
  • 同時押しした場合、どのボタンでも押されたタイミングで呼ばれます。1を押し、離さずに3を押した場合、1を押したときと3を押したときに呼ばれます。
<ButtonPress-1>
    canvas.bind("<ButtonPress-1>", handler)

この場合、ウィジェットの上で左ボタンを押したときにだけ呼ばれます。

  • 右ボタンやホイールボタンを押しても呼ばれません。
  • ほかのボタンを押しっぱなしでも、左ボタンが押されたタイミングで呼ばれます。
  • ウィジェットの上で右ボタンを押し、押したままウィジェットの外に出て、左ボタンを押すと、1回だけイベントが発生します。そのままウィジェットの外で左ボタンを何度も押しても、イベントが発生するのは最初の一回だけです。さらに右ボタンを押したままウィジェットの上に戻って左ボタンを押すと、イベントは発生します。

最後の動作については、ドラッグ&ドロップ操作を途中でキャンセルしたくなったときに他のボタンを押せばキャンセルできるようにするために設けた仕様と想像しました。

<B1-ButtonPress>
    canvas.bind("<B1-ButtonPress>", handler)

この場合、左ボタンが押された状態で、右ボタンやホイールボタンが押されたときに呼ばれます。左ボタンがShiftやCtrlと同じようにモディファイアとして扱われます。

  • 左ボタンを押したときには呼ばれません。
  • 左ボタンを押していなければ、右ボタンやホイールボタンを押しても呼ばれません。
<B1-ButtonPress-3>
    canvas.bind("<B1-ButtonPress-3>", handler)

左ボタンを押しっぱなしの状態で、右ボタンを押したときに呼ばれます。

  • 単に右ボタンを押したときには呼ばれません。
  • 左ボタンを押しただけでも呼ばれません。
  • 右ボタンを押した状態で、左ボタンを押したり離したりしても呼ばれません。

左ボタンのドラッグ&ドロップに何らかの操作を割り当てて、ドロップの前に右ボタンを押すことで操作をキャンセルできる、という機能を実装する場合に使えそうです。

Double-ButtonPress系

<Double-ButtonPress>
    canvas.bind("<Double-ButtonPress>", handler)

マウスボタンを素早く2回押すと呼ばれます。

  • 最初のButtonPressからおよそ0.5秒以内にもう一度押すと、2回目がDoubleと判定されます。Windowsのダブルクリック速度の設定は影響しないようです。
  • ゆっくり2回押しても呼ばれません。一回押した後、1秒開けてもう一度押しても呼ばれません。
  • 正確には、押す、離す、押す、のタイミングで呼ばれます。2回目の押下の時点で呼ばれ、離す必要はありません。
  • 3回押すと、2回呼ばれます。2回目の押下時と3回目の押下時に呼ばれているようです。
  • 左ボタンのダブルクリックはもちろん、右ボタンなど、他のボタンのダブルクリックでも呼ばれます。
  • 二つ以上のボタンを連続で押しても呼ばれます。左を押す、左を離す、右を押す、とした場合、右を押した時点で呼ばれます。さらに、左を押す、左を押したまま右を押す、とした場合も、右を押した時点で呼ばれます。
<Double-ButtonPress-1>
    canvas.bind("<Double-ButtonPress-1>", handler)

左ボタンを素早く2回押すと呼ばれます。

  • ゆっくり2回押しても呼ばれません。一回押した後、1秒開けてもう一度押しても呼ばれません。
  • 正確には、押す、離す、押す、のタイミングで呼ばれます。2回目の押下の時点で呼ばれ、離す必要はありません。
  • 3回押すと、2回呼ばれます。2回目の押下時と3回目の押下時に呼ばれているようです。
  • 右ボタンやホイールボタンのダブルクリックでは呼ばれません。
  • 右ボタンやホイールボタンを押しっぱなしの状態で、左を二回押すと、呼ばれます。
  • 左ボタンを2回押す間に他のボタンを押すが入ると呼ばれないようです。素早く操作せねばならず、もしかすると私が誤って認識しているかもしれませんが、試してみた感じは以下の通りです。
    • 左ボタンを押す、左ボタンを離す、右ボタンを押す、右ボタンを離す、左ボタンを押す、では呼ばれません。
    • 左ボタンを押す、右ボタンを押す、左ボタンを離す、左ボタンを押す、では呼ばれません。
    • 左ボタンを押す、右ボタンを押す、右ボタンを離す、左ボタンを離す、左ボタンを押す、でも呼ばれません。
    • 右ボタンを押す、左ボタンを押す、右ボタンを離す、左ボタンを離す、左ボタンを押す、では呼ばれます。(途中で右を離すだけなら呼ばれる)
  • マウスを少し動かしながら素早く2回押しても呼ばれないようです。
<Double-ButtonPress-1>と<ButtonPress-1>の複合
    canvas.bind("<ButtonPress-1>", handler1)
    canvas.bind("<Double-ButtonPress-1>", handler2)

上記の場合、一回目の押下でhandler1が呼ばれ、二回目の押下でhandler2が呼ばれます。

  • 二回目の押下の時、handler1は呼ばれません。
  • 三回連続で押すと、handler1, handler2, handler2の順で呼ばれます。

ButtonPress-1のみを登録してDouble-ButtonPress-1を登録しない場合は、二回目の押下でもButtonPress-1のハンドラが呼ばれます。つまり、Dobule-ButtonPress-1を登録有無で、ButtonPress-1の挙動が変わることになります。

Triple-ButtonPress系

Tripleを単独で使うことは少ない(それならふつうはDoubleで十分)と思うので、複合のみ確認します。

<Triple-ButtonPress>と<Double-ButtonPress-1>と<ButtonPress-1>の複合
    canvas.bind("<ButtonPress-1>", handler1)
    canvas.bind("<Double-ButtonPress-1>", handler2)
    canvas.bind("<Triple-ButtonPress-1>", handler3)

上記の場合、一回目の押下でhandler1が呼ばれ、二回目の押下でhandler2が、三回目の押下でhandler3が呼ばれます。

  • 0.5秒以内にさらに3回押すと、2回目のButtonPressがDouble、3回目のButtonPressがTripleと判定されるようです。
  • 少しゆっくり目に三回押すと、handler1, handler2, handler2の順で呼ばれます。(handler3は呼ばれず、Double-イベントが二回発生する!)
  • 素早く4回以上押すと、handler1, handler2, handler3, handler3...の順で呼ばれます。
  • 途中で他のボタンを押したり、マウスを動かしたりするとhandler2やhandler3は呼ばれません。

Shiftとの複合

<Shift-Double-ButtonPress-1>
    canvas.bind("<Shift-Double-ButtonPress-1>", handler)

Shiftキーを押しっぱなしにして、左ボタンを素早く2回押すと呼ばれます。

  • ボタン押下一回目と二回目のどちらかでShiftキーが離されていると、呼ばれません。
  • ボタン押下一回目と二回目の間で一瞬Shiftキーを離しても、ボタン押下時にShiftキーが押されていれば、呼ばれます。
<Shift-Double-ButtonPress-1>と<Double-ButtonPress-1>の複合
    canvas.bind("<Double-ButtonPress-1>", handler1)
    canvas.bind("<Shift-Double-ButtonPress-1>", handler2)

Shiftキーを押さずに左ボタンを素早く二回押すとhandler1が呼ばれます。 Shiftキーを押しっぱなしにして、左ボタンを素早く2回押すとhandler2呼ばれます。

  • handler2が呼ばれるタイミングで、同時にhandler1は呼ばれることはありません。
  • ボタン押下一回目と二回目のどちらかでShiftキーが離されていると、handler-1(通常のDouble-ButtonPress)が呼ばれます。
  • ボタン押下一回目と二回目の間で一瞬Shiftキーを離しても、ボタン押下時にShiftキーが押されていれば、handler2が呼ばれます。

Motion

マウスカーソルが移動したときに呼ばれるイベントなのですが、かなりややこしいです。OSやマウスドライバの仕様に左右される可能性もありそうです。

    canvas.bind("<Motion>", handler)

ウィジェットの上でマウスを動かすとhandlerが呼ばれます。頻繁に呼ばれます。

  • ボタンがすべて離されている場合
    • カーソルがウィジェットの外に出ると呼ばれません。
    • 他のアプリケーションがアクティブであっても、カーソルがウィジェットの上に乗ると呼ばれます。
  • ウィジェット上でボタンを押しっぱなしにした場合
    • 押したままカーソルをウィジェットの外に出た場合、ウィジェットの外であっても、マウスを動かすたびに呼ばれます。
    • ボタンを離すと、以降、ウィジェットの外でマウスを動かしても呼ばれません。
      • 離すボタンは、押しっぱなしにしているボタンでなくとも、いずれかのボタンを離した時点で、ハンドラや呼ばれなくなります。
        • 例えば、左ボタンを押したままウィジェットの外に出て、左を押したまま右を押して離すと、右を離した時点からハンドラは呼ばれなくなります。
        • さらに、ウィジェット上で左右両方のボタンを押しっぱなしにして外に出た場合、どちらか一方のボタンを離した時点から、ハンドラは呼ばれなくなります。
  • ウィジェットの外でボタンを押しっぱなしにしてウィジェット上に入ってきた場合
    • 押しっぱなしにしているボタンを離すまでは、マウスを動かしてもハンドラは呼ばれません。
    • 押しっぱなしにしているボタン以外のボタンを離しても、ハンドラが呼ばれない状態は継続します。
<B1-Motion>
    canvas.bind("<B1-Motion>", handler)

ウィジェットの上で左ボタンを押しながらマウスを動かすとhandlerが呼ばれます。頻繁に呼ばれます。

  • 左ボタンを押していないときは呼ばれません。
  • ウィジェット上で左ボタンを押しっぱなしにした場合
    • ウィジェット上でマウスを動かすとハンドラが呼ばれます。
    • 左を押したままウィジェットの外に出ても、ハンドラは呼ばれ続けます。
    • ウィジェットの外で左ボタンを離すと、ハンドラは呼ばれなくなります。
    • ウィジェットの外で左以外のボタンを離すと、左を押しっぱなしだとしてもハンドラは呼ばれなくなります。ただ、左を押したままで再びウィジェットの上に入ると、ハンドラは呼ばれます。この場合、左を押しっぱなしで再びウィジェットの外に出ると、外に出ている間はハンドラは呼ばれません。

他のボタンとの操作の組み合わせはかなり複雑です。例えば、ウィジェット上で左をホールドして外に出て、外で右をホールドして左を離し、再びウィジェットの上に戻るといった操作ができたりするようなので、もう訳が分かりません。普通はそんな操作しないでしょ、と言われればそうなのですが、こういう動作が想定外の動作につながるので、できればしっかり押さえておきたいところです。

<B1-Motion>と<B3-Motion>の複合
    canvas.bind("<B1-Motion>", handler1)
    canvas.bind("<B3-Motion>", handler2)
  • B1のみ押して動かすとB1-Motionが、B3のみ押して動かすとB3-Motionが発行されます。
  • 同時押しの場合、あとからbindしたイベントが優先的に呼ばれるようです。上記の場合、B3-Motionを後から登録しているため、B1とB3を同時に押して動かすと、B3-Motionだけが発行されます。
<B1-Motion>と<B3-Motion>と<B1-B3-Motion>の複合
    canvas.bind("<B1-B3-Motion>", handler1)
    canvas.bind("<B1-Motion>", handler1)
    canvas.bind("<B3-Motion>", handler2)
  • B1のみ押して動かすとB1-Motionが、B3のみ押して動かすとB3-Motionが発行されます。
  • 同時押しの場合、B1-B3-Motionのみが呼ばれるようです。B1-Motion, B3-Motionは呼ばれません。

ButtonRelease系

<ButtonRelease-1>
    canvas.bind("<ButtonRelease-1>", handler)

ウィジェットの上でマウスボタンを離すと発生します。

  • ウィジェットの上で左ボタンを押し、ウィジェットを出てから離した場合、イベント発生します。
  • ウィジェットの上で左ボタンを押したままウィジェットを出て、左ボタンは押したままで右ボタンを押して離し、その後ウィジェットの外で左ボタンを離すと、イベントは発生しません。ウィジェットの中に移動してから左ボタンを離した場合は発生します。右ボタンを押してから離さずに左ボタンを先に離すと、イベントは発生します。
  • ウィジェットの上でほかのボタンを押したままにし、ウィジェットから出て、左ボタンを押して離すと、離したときに1回だけ発生します。もう一度左ボタンを押して離しても、もう発生しません。
  • [変な動き] ウィジェット上で右ボタンを押したままにする、ウィジェットから出る、右を押したまま左を素早くトリプルクリック(このとき1回だけReleaseが発生する)、右はおしたまま数秒後に再びウィジェット上に入る、とすると、なぜか、ウィジェットに入ったときにReleaseが発生します。トリプルの代わりにダブルだと発生しません。バグ?トリプルをゆっくりやった場合は発生しません。トリプルの代わりに4回以上連打した場合は発生します。Triple-ButtonReleaseにハンドラ を登録しているかどうかは関係ありません。シングルのButtonReleaseにハンドラ登録していなければ、Triple-ButtonReleaseの登録有無にかかわらず発生しません。

PressとReleaseは必ずペアで発生するとは限らない、というのは意識しておいた方がよさそうです。例えば、Press-1でドラッグ開始、Release-1でドラッグ終了というコーディングをしてしまった場合、左ドラッグを開始してウィジェット外で右クリックされると、左ボタンを離してもRelease-1が発生せず、左ボタンをホールドしていないのにドラッグ中という状態になってしまいます。

<ButtonRelease-1>との複合
    canvas.bind("<ButtonRelease-1>", handler1)
    canvas.bind("<ButtonRelease>", handler2)

ButtonRelease-1とButtonReleaseを両方登録した場合、ボタンが指定されている方が優先されて発生し、指定されていない方のイベントは発生しないようです。

  • 左ボタンを押して離した場合、ButtonRelease-1が発生します。ButtonReleaseは発生しません。
  • 右ボタンを押して離した場合、ButtonReleaseが発生します。

ButtonRelease-1が発生しないケースがあるので、ButtonReleaseにイベントハンドラを登録しておき、別ボタンが押されたらその時点でドラッグ操作中止という処理にしておくのが無難かと思います。キャンセル後にButtonPress系のイベントがないままButtonReleaseが発生する場合もあるので、ドラッグ中でなければ何もしないという処理を組んでおく必要もありそうです。