tkinterのマウスイベントの発生条件を調べる
Python + Tkinter でGUIアプリケーションを作成しています。 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が発生する場合もあるので、ドラッグ中でなければ何もしないという処理を組んでおく必要もありそうです。
tkinterで、自分で用意したカスタムのマウスカーソルを使用する
Python + tkinter で手のひらのマウスカーソルを使いたかったのですが、方法を調べても簡単には見つからなかったのでここにメモしておきます。
Windowsでしか試していませんが、X Windows System や Mac でもできるとのこと。プラットフォームによってやり方はそれぞれ違うみたいです。
結論
Windowsにおける手順は以下の通り。
- .cur または .ani 形式のファイルを用意する。
- ウィジェットの "cursor" オプションに、@ファイルパス という形式で用意したファイルを指定する。
サンプルソースファイルを以下に示します。
指定するファイルとして C:\Windows\Cursors\earo_busy_xl.ani を指定しています。
お使いのシステムに存在しない場合は適当に修正してください。
import tkinter if __name__ == "__main__": root = tkinter.Tk() # メインウィンドウにカーソルが乗ったときに # マウスカーソルを組み込みカーソル "hand2" にする root.configure(cursor="hand2") # ラベル "watch" にカーソルが乗ったときに # マウスカーソルを組み込みカーソル "watch" にする label = tkinter.Label(root, text="watch", bg="white", cursor="watch") label.pack(ipadx=10, ipady=10, padx=20, pady=20) # [本題] # ラベル "custom" にカーソルが乗ったときにマウスカーソルを # ファイル "C:\Windows\Cursors\aero_busy_xl.ani"にする label = tkinter.Label(root, text="custom", bg="white", cursor="@C:/Windows/Cursors/aero_busy_xl.ani") label.pack(ipadx=10, ipady=10, padx=20, pady=20) root.mainloop()
注意 パスを記述する際は、区切り文字にスラッシュを使わなければならないようでした。 私は最初にバックスラッシュを使ってしまい、エラーとなってしまいました。
ビルトインカーソルを指定するには
やり方を検索し始めて最初に見つかったサイトが以下です。ほとんどの人はこのサンプルだけで十分だと思います。
Tkinterカーソルの種類(マウスポインターの種類) | 株式会社 石川設計
しかし、私が表示したかった開いた手のひらのアイコンはありませんでした。
私は思いました。「でも、ここにあるキーワードですべてなのか?実はほかのカーソルも用意されてたりしないかな?」
ビルトインカーソルの種類
ビルトインカーソルの種類は、結論から言うと、前項のサイトで網羅されているようでした。
tkinterのリファレンスには、以下のように書かれています。
cursor
cursorfont.h の標準Xカーソル名を、接頭語 XC_ 無しで使うことができます。例えば、handカーソル(XC_hand2)を得るには、文字列 "hand2" を使ってください。あなた自身のビットマップとマスクファイルを指定することもできます。 Ousterhout の本の179ページを参照してください。
つまり、cursorに指定できる文字列はソースを見ろ、ということでした。またしても適当に探して見つけた X11 の cursorfont.h は以下です。
include/X11/cursorfont.h · master · xorg / lib / libX11 · GitLab
前項の石川設計さんのサンプルは、上記のヘッダファイルのシンボルをすべて網羅していることがわかります。
ということで、ビルトインカーソルの中に 私がほしい形状は無さそうだ、と結論付けました。
一方で、上記の引用文には「あなた自身のビットマップとマスクファイルを指定することもできます」とあります。まだ手段はありそうです。
X11でカスタムのカーソルを使用するには
以下のQ&Aが見つかりました。
Python Tkinterでカスタムマウスカーソルを作ることは可能ですか? (TkAggバックエンドでmatplotlibを使用する) - CODE Q&A
上記の回答の中に、cursorオプションに(ビルトインカーソルを表す文字列ではなく)4つの要素を持つタプルを指定しているものがあります。「UNIX11 XBMファイル」で動作する、とも。
4要素に何を指定すればよいかはよくわかりませんし、Windowsでは動作しないようにも思いました。
とりあえず cursor オプションにはビルトインの文字列以外も指定できるようです。これまで tkinter をキーワードに調べてきましたが、そのものずばりな解にたどり着けないので、今度は Tcl/Tk をキーワードにして調べることにします。
Tcler's Wiki に記載されていた解
Tcl/Tk をキーワードに入れてたどり着いたのが以下のページです。
上記によれば、-cursor オプションとして以下のような指定ができるのだそう。
- name fgColor bgColor
- @sourceName maskName fgColor bgColor
- @sourceName
2番目の項目が前項のQ&Aにあった指定方法のようです。しかし、その説明には以下のようにあります。
This form of the command will not work on Macintosh or Windows computers.
WindowsやMacでは使えません、と。で、3番目の項目に以下のように書かれています。これが答えですね。
@sourceName
This form only works on Windows, and will load a Windows system cursor (.ani or .cur) from the file specified in sourceName.
というわけで、最初に記載したサンプルコードを書いたらめでたく動いてくれました。
以上です。