PySimpleGUIの使い方・前編の続きです。PySimpleGUIを扱っているということ以外とくに前後のつながりはないので、PySimpleGUIの基本的な扱いさえわかれば、後編からでも読み進められると思います。
後編はPySimpleGUIの低レベルのフレームワークtkinterに言及する、PySimpleGUIの機能を使って、より細かなイベント操作を行うことが目的です。
1.ロードマップ
PySimpleGUIのエレメントクラスはbind()
関数を持っており、tkinterで規定されたイベントをエレメントに関連付けることができます。bind()
されたこのエレメント上でクリックなどのtkinterのイベントのトリガーになるアクションが検知されると、PySimpleGUIのWindowインスタンスのread()
関数が認知できるイベントが発生するようになり、さらにtkinterの管理するイベントに付随する情報に、エレメントのインスタンスを通してアクセスできるようになります。
ここで次のようなことが問題になります。
- tkinterで規定されたイベントとはどんなものか
- bind()関数をどう使ったらよいのか
- PySimpleGUI上でどんなイベントが発生するのか
- イベントに付随する情報とはどんな形式なのか
この記事ではこれらの問題を一つずつ潰していき、最後に低レベルのイベント操作を利用したGUIアプリの例を示したいと思います。
2.tkinterのイベント
tkinterのイベントはtkinterのソースを見れば詳細がわかります。ソースは以下のリンクから確認できます。
- Lib/tkinter/init.py
https://github.com/python/cpython/tree/3.11/Lib/tkinter/__init__.py
「Event」をキーワードに検索すれば次のような記述が見つかるはずです。
class EventType:
KeyPress = '2'
Key = KeyPress
KeyRelease = '3'
ButtonPress = '4'
Button = ButtonPress
ButtonRelease = '5'
Motion = '6'
#~中略~
Colormap = '32'
ClientMessage = '33' # undocumented
Mapping = '34' # undocumented
VirtualEvent = '35' # undocumented
Activate = '36'
Deactivate = '37'
MouseWheel = '38'
全部で38種あるようですが、「undocumented」とコメントされているものは外からは利用できないものでしょう。
しかしこの記述だけではイベントの細かい内容が判然としません。これらのイベントの詳細はつづくEventクラスの定義のドキュメンテーションに記述されています。例えば「KeyPress」と「KeyRelease」はキーボードの、「ButtonPress」と「ButtonRelease」はマウスのクリックに対応していることがわかります。
3.bind()
関数の使い方
bind()
関数を利用するためには、まずWindowインスタンスのコンストラクタで次のように指定する必要があります。
window = sg.window('タイトルバー', layout, finalize=True)
finalize=True
を指定することで、PySimpleGUIのWindowインスタンスの生成と同時に、それを構成する低レベルのtkinterのオブジェクトも同時に生成されます。低レベルのtkinterオブジェクトへの言及を含むbind()
関数を利用するためにはfinalize=True
の指定が必須になります。
なおfinalize=True
を指定しない場合、tkinterのオブジェクトはWindowインスタンスのread()
関数が呼び出されたタイミングではじめて生成されます。
bind()
関数のPySimpleGUIのコールリファレンスの記述は、どのエレメントでも次のようなものです。
bind(bind_string,
key_modifier,
propagate = True)
エレメントのインスタンスにtkinterのイベントを関連付けるには、次の二つの引数を指定するだけです。
- bind_string
関連付けたいtkinterのイベントを文字列で指定します。2.の一覧の名称を半角の「<>:山かっこ」でくくって渡す必要があります。たとえば<KeyPress>
(マークダウンの仕様のせいか半角の山かっこがうまく使えないので全角になっています)というようにです。 - key_modifier
このイベントにユーザーが自由につけることのできる識別子で、文字列で指定します。後述しますが、この内容がそのままイベントを示す文字列になるわけではありません。
だいたいこんな感じです。
element_instance.bind('<ButtonPress>', 'b_press')
4.PySimpleGUI上でどんなイベントが発生するのか
tkinterのイベントに関連付けられたエレメント上で、そのイベントのトリガーになるような動作が行われると、PySimpleGUIのWindowインスタンスのread()
関数はそのイベントを示す文字列を返すようになります。
このイベント文字列は、エレメントのkey=
に指定した文字列と、bind()
関数のkey_modifier
に渡した文字列を合わせたものになります。
例えば、key=
にTEXT
と指定したTextエレメントのbind()
関数に、b_press
という文字列をkey_modifier
に渡すと、Textエレメント上でクリック操作が行われるたびに、TEXTb_press
というイベントが発生します。ただの足し算です。
5.イベントに付随する情報とはどんな形式なのか
エレメントにbind()
したtkinterイベントの詳細は、エレメントのuser_bind_event
属性にtkinterのEventクラスのインスタンスとして保持されています。クリックした位置座標などの、イベントに付随する情報はこのEventクラスの属性として記録されています。
tkinterのEventクラスの持つ属性は、Eventクラスの定義に記されたドキュメントに詳細が記述されています。
"""Container for the properties of an event.
#~中略~
serial - serial number of event
num - mouse button pressed (ButtonPress, ButtonRelease)
focus - whether the window has the focus (Enter, Leave)
height - height of the exposed window (Configure, Expose)
width - width of the exposed window (Configure, Expose)
keycode - keycode of the pressed key (KeyPress, KeyRelease)
state - state of the event as a number (ButtonPress, ButtonRelease,
Enter, KeyPress, KeyRelease,
Leave, Motion)
state - state as a string (Visibility)
time - when the event occurred
x - x-position of the mouse
y - y-position of the mouse
x_root - x-position of the mouse on the screen
(ButtonPress, ButtonRelease, KeyPress, KeyRelease, Motion)
y_root - y-position of the mouse on the screen
(ButtonPress, ButtonRelease, KeyPress, KeyRelease, Motion)
char - pressed character (KeyPress, KeyRelease)
send_event - see X/Windows documentation
keysym - keysym of the event as a string (KeyPress, KeyRelease)
keysym_num - keysym of the event as a number (KeyPress, KeyRelease)
type - type of the event as a number
widget - widget in which the event occurred
delta - delta of wheel movement (MouseWheel)
"""
これらの属性はすべてのイベントで有効な値を持つわけではありません。たとえばマウスのクリックが検知され「ButtonPress」イベントが発生したとき、キーボードのどのキーが押されたかを記録するkeycode
属性は、値の持ちようがありません。
このような場合には、Eventオブジェクトは値のつけようのない属性に、文字列で??
という値を与えます。
6.Canvasエレメントを利用したGUIアプリ
これまでの内容とPySimpleGUIのCanvasエレメント利用して、マウス操作で円を描くGUIアプリをつくってみました。
PySimpleGUIのCanvasエレメントは、名前の通りエレメント上に図形などの描画を行えるエレメントです。しかし、Canvasエレメントクラス自体には描画を制御するすべはなく、低レベルのtkinterのCanvasクラスをtk_canvas
から操作する必要があります。
tkinterのCanvasクラスについて、以下のことを押さえておけば、コードの意味は大体わかると思います。
- 図の描画はCanvasクラスの
create_~
系の関数で行う -
create_~
系の関数が呼び出されるたびに描画の内容が記録され、順番に整数値でIDが与えられる -
delete()
関数にIDを指定すると、描画の内容が消去される -
delete()
してもIDの続き番号はリセットされない。百回分の描画をすべてdelete()
した後に行った描画に割り当てられるIDは101になる
import PySimpleGUI as sg
switch = False
canvas = sg.Canvas(size=(500, 500), key='CANVAS', background_color='white')
window = sg.Window("Canvas Test", [[canvas]], finalize=True)
canvas.bind('<Motion>', 'motion')
canvas.bind('<ButtonPress>', 'click_on')
canvas.bind('<ButtonRelease>', 'click_off')
while True:
event, values = window.read()
if event == sg.WIN_CLOSED:
break
if event == 'CANVASclick_on':#マウスの左クリックが押されたときの処理
switch = True
v = canvas.user_bind_event
x1 = v.x
y1 = v.y
item_id = canvas.tk_canvas.create_bitmap(x1, y1)
if event == 'CANVASmotion':#マウスの移動があった際の処理
if switch:
v = canvas.user_bind_event
canvas.tk_canvas.delete(item_id)
x2 = v.x
y2 = v.y
item_id = canvas.tk_canvas.create_oval(x1, y1, x2, y2)
if event == 'CANVASclick_off':#押された状態のマウスの左クリックが離されたときの処理
if switch:
canvas.tk_canvas.delete(item_id)
v = canvas.user_bind_event
x2 = v.x
y2 = v.y
item_id = canvas.tk_canvas.create_oval(x1, y1, x2, y2)
switch = False
window.close()
7.おわりに
前編と合わせて初投稿でしたが、記事を書いてる途中でめんどくさくなってしまい、前編の序盤とこの後編でずいぶんノリの違う記事になってしまったような気がします。Qiitaに何度も助けられた身としてはなんとか貢献できたらとの思いでしたが、うまくいかないものです。わかりやすい記事を載せてくれる執筆者の方々には本当に頭が下がります。