[PySimpleGUI]Tab切り替え時に画像を切り替える

## はじめに
`PySimpleGUI`でのアプリケーション作成時に
複数タブ切り替え時に、どのタブを選択したか参照したい時にちょっと手間取ったのでメモ

## PySimpleGUIとは
PythonのGUI作成ライブラリ
PythonではGUI作成を目的とした標準ライブラリとして`tkinter`が組み込まれており、`PySimpleGUI`はそのラッパー

`tkinter`と比べ**直感的にコーディング**することができ、**記述量も短い**のが特徴
個人的には、[公式リファレンス](https://pysimplegui.readthedocs.io/en/latest/)がとても見やすくサンプルも豊富で情報量が多いのが助かります

## やりたいこと
* 1つのアプリケーション内で複数機能が存在、機能ごとにタブでレイアウトを切り替える
* どんな機能なのかを表す画像をいい感じに表示、**タブ切り替え時に画像も切り替えたい**

下図のようなイメージです

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289210/d9c08826-4570-1548-61f6-b6d085583704.png" width=400>


## とりあえず動かしてみる
自分で1からコーディングするのは手間なので、[公式リファレンスのCookbook](https://pysimplegui.readthedocs.io/en/latest/cookbook/)から参考にできそうなサンプルを動かしてみます

```python
import PySimpleGUI as sg

tab1_layout = [[sg.T('This is inside tab 1')]]
tab2_layout = [
    [sg.T('This is inside tab 2')],
    [sg.In(key='in')]
]

layout = [
    [sg.TabGroup([[
        sg.Tab('Tab 1', tab1_layout),
        sg.Tab('Tab 2', tab2_layout)
    ]])],
    [sg.Button('Read')]
]

window = sg.Window('My window with tabs', layout)

while True:
    event, values = window.read()
    print(event,values)
    if event == sg.WIN_CLOSED:           # always,  always give a way out!
        break
```

動作させるとこんな感じ

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289210/5aa38b01-0660-2b89-9679-0586351137a8.gif">

タブの切り替えができていますね
上記コードは`Tabgroup`要素を使った最小構成です

これをちょっといじってアプリケーションを作ります

## できたもの

```python
import PySimpleGUI as sg
from PIL import Image
import io

# 画像をリサイズ、bytes形式で返す
def load_bytesImg(img_path, size_ratio=1):
    img = Image.open(img_path)
    resize_img = img.resize((int(img.width*size_ratio), int(img.height*size_ratio)))

    output = io.BytesIO()
    resize_img.save(output, format="png")
    return output.getvalue()

# タブごとのレイアウト
text_width, text_height = 40, 5
tab1_layout = [[sg.Text( 'tab1です', size=(text_width, text_height) )]]
tab2_layout = [[sg.Text( 'tab2です', size=(text_width, text_height) )]]
tab3_layout = [[sg.Text( 'tab3です', size=(text_width, text_height) )]]

# 画像データをbytes形式で用意
img = load_bytesImg("./images/func01.png", size_ratio=0.3)

# 全体のレイアウト
layout = [
    [sg.Image(data=img, pad=((220, 10), (0, 0)), key="image")],
    [sg.TabGroup([[
        sg.Tab('Tab 1', tab1_layout, key="tab1"),
        sg.Tab('Tab 2', tab2_layout, key="tab2"),
        sg.Tab('Tab 3', tab3_layout, key="tab3")
    ]], key="tab_group", enable_events=True)],
    [sg.Output( size=(40, 5) )]
]

window = sg.Window('TabGroup sample', layout, margins=(20,20))

# イベントループ
while True:
    event, values = window.read()

    # ウィンドウを閉じたら終了
    if event == sg.WIN_CLOSED:
        break

    # タブ切り替え
    elif event=="tab_group":
        select_tab = values["tab_group"]
        img_name = ""

        if select_tab=="tab1":
            print(f"select tab1")
            img_name = "func01.png"

        elif select_tab=="tab2":
            print(f"select tab2")
            img_name = "func02.png"

        elif select_tab=="tab3":
            print(f"select tab3")
            img_name = "func03.png"

        # 表示する画像を更新
        img = load_bytesImg(f"./images/{img_name}", size_ratio=0.3)
        window["image"].update(data=img)
```

動作例はこんな感じ

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289210/3d343360-232a-32e6-1b1f-a4bc5f3cc22d.gif">

右上に表示する画像は`images`ディレクトリ内に配置しています
ディレクトリ構成はこんな感じ

```
├─tabgroup.py
├─images
    ├─func01.png
    ├─func02.png
    ├─func03.png
```

以下、コードの解説です

### レイアウト定義部分

```python
# タブごとのレイアウト
text_width, text_height = 40, 5
tab1_layout = [[sg.Text( 'tab1です', size=(text_width, text_height) )]]
tab2_layout = [[sg.Text( 'tab2です', size=(text_width, text_height) )]]
tab3_layout = [[sg.Text( 'tab3です', size=(text_width, text_height) )]]

# 画像データをbytes形式で用意
img = load_bytesImg("./images/func01.png", size_ratio=0.3)

# 全体のレイアウト
layout = [
    [sg.Image(data=img, pad=((220, 10), (0, 0)), key="image")],
    [sg.TabGroup([[
        sg.Tab('Tab 1', tab1_layout, key="tab1"),
        sg.Tab('Tab 2', tab2_layout, key="tab2"),
        sg.Tab('Tab 3', tab3_layout, key="tab3")
    ]], key="tab_group", enable_events=True)],
    [sg.Output( size=(40, 5) )]
]
```

`PySimpleGUI`にて、レイアウトは基本的に`list`形式で定義します
今回使用しているものは以下の通り

* `Text`
  * テキストを表示する
  * `size`で幅と高さを指定
* `Image`
  * 画像を表示する
  * 画像は パス指定 or `bytes`形式 で渡す
  * `padding`で右寄せにしている
* `TabGroup`
  * タブ切り替え要素
* `Output`
  * 標準出力要素
  * `print`などでテキストを表示することができる

要素の役割や引数などは[Call reference](https://pysimplegui.readthedocs.io/en/latest/call%20reference)で確認できます

注意しなければならないのが`TabGroup`要素の`enable_events`で、この値を`True`にしておかなければタブの切り替え時にイベントを検知できません(デフォルトでは`False`)

また、`load_bytesImg()`に関しては後述にて解説します

### イベントループ部分

```python
window = sg.Window('TabGroup sample', layout, margins=(20,20))

# イベントループ
while True:
    event, values = window.read()

    # ウィンドウを閉じたら終了
    if event == sg.WIN_CLOSED:
        break

    # タブ切り替え
    elif event=="tab_group":
        select_tab = values["tab_group"]
        img_name = ""

        if select_tab=="tab1":
            print(f"select tab1")
            img_name = "func01.png"

        elif select_tab=="tab2":
            print(f"select tab2")
            img_name = "func02.png"

        elif select_tab=="tab3":
            print(f"select tab3")
            img_name = "func03.png"

        # 表示する画像を更新
        img = load_bytesImg(f"./images/{img_name}", size_ratio=0.3)
        window["image"].update(data=img)
```

`window`オブジェクトを定義、ここでタイトルバーのテキストと先程定義したレイアウトを設定します

全体の流れとしては
イベントループで常に`window.read()`を呼び出し、返された`event`でイベントを検知、`values`で`window`内の要素を参照、更新する、という感じです

要素へのアクセスは`values["keyで指定した文字列"]`という形式で行えます
今回の例では、`layout`定義にて`key="tab_group"`と指定しているので`values["tab_group"]`で要素へのアクセスができます
この値が何なのかでどのタブを選択しているかを検知することができます

選択したタブに応じた画像ファイル名を指定、要素を更新し表示に反映させます

### 画像データのbytes変換部分
`Image`要素での画像指定は、ファイルのパス(文字列)または`bytes`形式で行いますが
**画像のリサイズ機能**は無いみたいです

画像の表示範囲は指定できても、画像自体のリサイズはできないので
元々レイアウトに合わせた画像を用意する必要があり、ちょっとめんどくさいです

サイズが合ってない画像だとこうなります

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289210/17efb0b0-2906-67d6-2b5c-be504001e4ff.png">

表示範囲を大きくはみでてますね
別ツールで画像をリサイズして保存して...とやるのは非効率なのでアプリ内処理でリサイズくらいはやりたいです


今回は`load_bytesImg()`関数がその処理部分に当たります

```python
# 画像をリサイズ、bytes形式で返す
def load_bytesImg(img_path, size_ratio=1):
    img = Image.open(img_path)
    resize_img = img.resize((int(img.width*size_ratio), int(img.height*size_ratio)))

    output = io.BytesIO()
    resize_img.save(output, format="png")
    return output.getvalue()
```

この関数でやってることは

1. 画像を`Pillow.Image`オブジェクトとして読み込み
2. `Image.resize`メソッドでリサイズ
3. `io`モジュールの`ByteIO`メソッドで`bytes`形式に変換して返す

です
引数`size_ratio`の値で拡大縮小倍率を指定しています　画像のアスペクト比は固定です

## まとめ
`PySimpleGUI`を用いて複数機能を搭載した簡単なインタフェースを作成できました
`TabGroup`要素によるタブ切り替え時のイベント検知、画像のリサイズについて言及している記事があまりなかったので自分用としてまとめてみました

## リンク
[PySimpleGUI：公式リファレンス](https://pysimplegui.readthedocs.io/en/latest/)
[Pillow.Image：公式リファレンス](https://pillow.readthedocs.io/en/stable/reference/Image.html)