PySimpleGUIの使い方・後編

　[PySimpleGUIの使い方・前編](https://qiita.com/Aroe/items/492ca0194c55335e28c0, '前編')の続きです。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」をキーワードに検索すれば次のような記述が見つかるはずです。
~~~python:sample01.py
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インスタンスのコンストラクタで次のように指定する必要があります。
~~~Python:sample02.py
window = sg.window('タイトルバー', layout, finalize=True)
~~~
　`finalize=True`を指定することで、PySimpleGUIのWindowインスタンスの生成と同時に、それを構成する低レベルのtkinterのオブジェクトも同時に生成されます。低レベルのtkinterオブジェクトへの言及を含む`bind()`関数を利用するためには`finalize=True`の指定が必須になります。
　なお`finalize=True`を指定しない場合、tkinterのオブジェクトはWindowインスタンスの`read()`関数が呼び出されたタイミングではじめて生成されます。
　`bind()`関数のPySimpleGUIのコールリファレンスの記述は、どのエレメントでも次のようなものです。
~~~Python:sample03.py
bind(bind_string,
    key_modifier,
    propagate = True)
~~~
　エレメントのインスタンスにtkinterのイベントを関連付けるには、次の二つの引数を指定するだけです。
* bind_string
関連付けたいtkinterのイベントを文字列で指定します。2.の一覧の名称を半角の「<>：山かっこ」でくくって渡す必要があります。たとえば`＜KeyPress＞`（マークダウンの仕様のせいか半角の山かっこがうまく使えないので全角になっています）というようにです。
* key_modifier
このイベントにユーザーが自由につけることのできる識別子で、文字列で指定します。後述しますが、この内容がそのままイベントを示す文字列になるわけではありません。

　だいたいこんな感じです。
~~~Python:sample04.py
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クラスの定義に記されたドキュメントに詳細が記述されています。
~~~Python:sample05.py
"""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になる
~~~Python:sample06
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()
~~~
![canvas.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3469668/401cd1a2-a58c-1896-ddb5-1d0ce1739c58.png)
# 7.おわりに
　前編と合わせて初投稿でしたが、記事を書いてる途中でめんどくさくなってしまい、前編の序盤とこの後編でずいぶんノリの違う記事になってしまったような気がします。Qiitaに何度も助けられた身としてはなんとか貢献できたらとの思いでしたが、うまくいかないものです。わかりやすい記事を載せてくれる執筆者の方々には本当に頭が下がります。
# 8.参考
* Python tkinter
https://docs.python.org/ja/3/library/tkinter.html#module-tkinter