PySimpleGUIの使い方・前編

　PythonのGUIパッケージ「PySimpleGUI」の基本的な使い方について、主にイベントをテーマに紹介します。前編のこの記事ではPySimpleGUIだけでできることを扱います。後編では低レベルのフレームワーク「tkinter」を利用したイベント処理を扱う予定です。
# 1.環境について
　この記事のすべてのコードは以下の環境で動作を確認しています。
* Windows11
* Python：3.11.0
* PySimpleGUI：4.61.0

　ほとんどの内容はWindowsのPCに最新のPyhonと、pipでインストールできる最新のPySimpleGUIを用意すれば利用できると思います。ただし一部の内容は今現在、pipでインストールできるPySimpleGUIでは利用できません。詳しくは当該箇所で改めて説明します。
# 最初のウィンドウを作る
　PySimpleGUIはウィンドウという土台に、ボタンやテキスト表示域や画像表示域といった部品を配置する、という形でウィンドウを構成します。
　以下のコードから、画像のようなウィンドウを作ることができます。
~~~python:sample_01.py
import PySimpleGUI as sg

layout = [[sg.Text('テキスト表示域', key='TEXT')],
          [sg.Input(key='INPUT'), sg.Button('ボタン', key='BUTTON')]]

window = sg.Window('タイトルバーに表示されるテキスト', layout)

while True:
    event, values = window.read()

    if event == sg.WIN_CLOSED:
        break
window.close()
~~~
![f_window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3469668/197ff87c-232e-66ac-fe20-0fad22ed73c0.png)


　ウィンドウ上に配置する部品はPySimpleGUIのElementクラスのインスタンスです。上の例では三種類のElementクラスを使っています。
* Textエレメント：指定したテキストを表示する
* Buttonエレメント：クリックに反応するボタン
* Inputエレメント：テキストの入力フィールドを表示する

　二次元の配列を使ってこれらの部品の配置を表現したものが`layout`です。PySimpleGUIのWindowクラスはこのような二次元配列を受け取ってWindowインスタンスを作ります。個々のエレメントについては公式のコールリファレンスを参照してください。
# 2.イベントループ
　Windowインスタンスの`read()`でウィンドウが生成されます。ループの中で`read()`を呼び出すことで、スクリプトが終了することなくウィンドウが表示され続けます。
　`read()`は二つの値を返します。最初の戻り値はその時点で問題になるイベントを示す文字列で、二つ目の戻り値はその時点でエレメントが保持している「何らかの値」を記録した辞書形です。この二つ目の戻り値は、イベントに付随する何らかの情報（例えばクリック操作が行われた位置座標のようなもの）を持っているわけではないという点に注意してください。
　この二つの戻り値に基づいて、エレメントクラスを次の四種に分類できます。
1. イベントを生成するエレメント
2. `read()`の戻り値として取得できる「何らかの値」を持ったエレメント
3. 1.と2.に該当するエレメント
4. 1.にも2.にも該当しないエレメント

　上の例の三種のエレメントは次のように分類できます。
* Textエレメント：4.。基本的には`read()`の戻り値には何の影響もありません
* Buttonエレメント：1.。ボタンのクリックに応じてイベントが発生します
* Inputエレメント：2.。入力フィールド上の文字列が「何らかの値」になります

　1.に該当するエレメントの`key=`で指定した文字列が`read()`の一つ目の戻り値の「イベントを示す文字列」に、2.に該当するエレメントの`key=`で指定した文字列が`read()`の二番目の戻り値の辞書型のキーになります。
## 2.1イベント処理
　ループ内に`read()`の二つの戻り値に応じた処理を記述することで、ウィンドウに配置したエレメントに役割を持たせることができます。上の例にボタンイベントの処理を加えると次のようになります。
~~~python:sample_02.py
import PySimpleGUI as sg

layout = [[sg.Text('テキスト表示域', key='TEXT'],
          [sg.Input(key='INPUT'), sg.Button('ボタン', key='BUTTON')]]

window = sg.Window('タイトルバーに表示されるテキスト', layout)

while True:
    event, values = window.read()

    if event == sg.WIN_CLOSED:
        break
    if event == 'BUTTON':
        window['TEXT'].update(values['INPUT'])
window.close()
~~~
　イベント処理の記述を加えただけなので見た目は最初の例から変化はありませんが、テキスト入力フィールドに文字を与えてボタンを押すと、次のようにテキストの表示域が変化するようになりました。
![s_window.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3469668/93c1aaa4-ff29-85de-865f-05d015cdfb67.png)
　このボタンイベントの処理には、Windowインスタンスが辞書型のようにふるまう特徴を利用しています。レイアウトにセットしたエレメントの`key=`が、そのまま辞書のキーになって自由にエレメントのインスタンスに言及することができます。
　`update()`はほとんどのエレメントクラスが持っているデータ更新用の関数で、その挙動はエレメントごとに若干異なります。たとえばTextエレメントの`update()`であれば、位置引数で渡した文字列で表示内容が更新されます。
# 3.いろいろなイベントを発生させる
　ここからはPySimpleGUIの枠組みだけで利用できる、いろいろなイベントを紹介していきます。

## 3.1enable_events = True
　エレメントのインスタンを作る際に`enable_events=True`とセットすることで、本来ならイベントを発生させないエレメントに、イベントを発生させることができます。
~~~python:sample_03.py
layout = [[sg.Text('テキスト表示域', key='TEXT', enable_events = True)],
          [sg.Input(key='INPUT'), sg.Button('ボタン', key='BUTTON')]]
~~~
　上の例のレイアウトの記述をこのように書き換えるだけで、Textエレメントがクリックに反応してイベントを発生するようになります。イベントを示す文字列はButtonエレメントと同じようにエレメントの指定した`key=`です。
　したがって、ループ中に次のような記述を付け加えればTextエレメントの発したイベントに応対することができます。
~~~python:sample_04.py
if event == 'TEXT':
    #処理の記述
~~~
　`enable_events=True`をセットした際の挙動は、エレメントごとに異なります。Textエレメントはクリックに反応するようになりますが、Inputエレメントは入力フィールドへの変更を検知してイベントを発生させます。
## 3.2キーボードイベントとマウススクロール
　Windowインスタンスを作成する際に`return_keyboard_events=True`とセットすることで、キーボードのボタン操作を検知してイベントを発生するようになります。
~~~python:sample_05.py
window = sg.Window('タイトルバー', layout, return_keyboard_events=True)
~~~
　発生するイベントを示す文字列は押されたキーによって変わります。たとえば`a`というキーが押されれば`a`というイベントが、`s`なら`s`というイベントが発生します。
　要するにこういうことです。
~~~python:sample_06.py
if event == 'a':
    #処理の記述
~~~
　PySimpleGUIではマウスホイールのアップダウンも、キーボードの入力の一種として捉えられています。ホイールアップ時には`MouseWheel:Up`、ホイールダウン時には`MouseWheel:Down`というイベントが発生します。
~~~python:sample_07.py
if event == 'MouseWheel:Up':
    #処理の記述
~~~
## 3.3enable_window_config_events=True
　Windowインスタンスを作る際に`enable_window_config_events=True`をセットすることで、ウインドウの位置やサイズが変化した際に`sg.WINDOW_CONFIG_EVENT`というイベントが発生するようになります。
~~~python:sample_08.py
window = sg.Window('タイトルバー', layout, enable_window_config_events=True)
~~~
~~~python:sample_09.py
if event == sg.WINDOW_CONFIG_EVENT:
    #処理の記述
~~~
　ただし、この機能は現在pipでインストールできる最新のPySimpleGUIであっても利用することができません。この機能を利用するにはGitHubからPySimpleGUIの最新のアップデートを導入する必要があります。
 Pythonで次のコードを実行すると、PySimpleGUIのGUIが起動します。
~~~python:sample_10.py
import PySimpleGUI as sg
sg.main()
~~~
![pygui.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3469668/9fbca333-2207-2996-f64b-f4f5bb764b8d.png)
　赤いボタンを押すだけです。
# おわりに
　以上で前半の内容は終わりです。エレメントとの組わせ次第でPySimpleGUIだけでもそれなりのGUIアプリが組めるものと思います。
　後半ではPySimpleGUIの低レベルのフレームワークに言及する処理を利用して、もう少し細かなイベント操作を実現する方法を紹介します。
# 参考
* PySimpleGUI Cookbook
https://www.pysimplegui.org/en/latest/cookbook/
* GitHub
https://github.com/PySimpleGUI/PySimpleGUI/issues/5567

　二つ目のリンクは`enable_window_config_events`に関する機能強化の、公式による投稿です。イベントの具体的な利用法も紹介されているので参考になると思います。