PythonでGUIを作る③ PySimpleGUI編

1.はじめに

今回は、PySimpleGUIを使ってGUIを作成していきたいと思います。ちょっと寄り道的な内容になります。

そもそもPySimpleGUIとは何かといいますと、GUIを作成するライブラリの1つで、今まで使用していたTkinterと似たようなものだと思ってください。①で紹介したKivyやPyQtと同じようなものです。折角なのでいろいろ試しながら進めれればと思います。

2.PySimpleGUIについて

PySimpleGUIとは、PythonでGUIを作成するライブラリの1つです。Tkinterと違って、何もしなくても初期から入っているものではなく、Pythonインストール後に改めてインストール必要があります。インストール方法等は、下記のURLから公式ホームページをご参照ください。
PySimpleGUIの特徴としまして、コードがシンプルで細部まで整えなくてもきれいなGUIを作成してくれます。ただ、ライセンス登録が必要だったり商用利用する場合は有料ライセンスが必要だったりと気を付けなければいけない部分もあります。

PySimpleGUI

TkinterとPySimpleGUIの大きな違いについてお話したいと思います。個人的にこの考え方が一番手こずりました。
例としてそれぞれのGUI上でボタンを押したときのお話をします。ボタンを押したときの処理は、基本的にdef関数を使用します。なぜdef関数を使用するかといいますと、GUIでボタンが押されるたびに一定の処理をするので、def関数を作成して呼び出す方がわかりやすいからです(素人の考えですが…)。
TkinterとPySimpleGUIの大きな違いは、def関数の呼び出し方です。もともとPythonで何かしらの計算コードを作成して、それを社内で使えるようにGUIにしようとしたとします。Tkinterでは、GUIの枠組みを作りボタンにdef関数を起動する設定をすればいいのですが、すでにdef関数がある場合は、ボタンとdef関数をつなげるdef関数を作成することがベターです。インプットラベル(GUI上で数値を入力するところ)から数値を読み込み、それを引数として事前に作成していいたdef関数を呼びだす処理をするものになると思います。
PySimpleGUIの場合、eventを設定できます。eventとは、起動しているGUI上でなにが起きたらどうするという処理を定めることができます。現状ボタンしか試していませんが、たぶんボタンを押す以外のこともできると思います。PySimpleGUIではこのeventにボタンを押したときの処理を書きます。Tkinterで作成したdef関数を動かすためのdef関数はeventで対応できます。もちろんdef関数を動かすためのdef関数も使えますが、2度手間気味になります。
こういった処理の違いがあるので、Tkinterで作成したコードをそのままPySimpleGUIで使おうとすると意外と大変だったりします。ですが、全く使えないわけではないので、処理手順が違うことを意識しながら引数などをあてていけば、すんなり作成できると思います。

3.PySimpleGUIを使ってグラフ付きの計算ソフトを作成する

実際にGUIを作成していきたいと思います。見出しでは大層なことを言っていますが、要するに②で作成したものをPySimpleGUIで作成するだけです。

import PySimpleGUI as sg
import numpy as np
import matplotlib.pyplot as plt
from scipy import optimize
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.backends.backend_tkagg as tkagg

#=============================
#浅水係数算出式
#=============================

#グラフの設定
fig,ax = plt.subplots(figsize=[8,6])
plt.grid(which='major',color='black',linestyle='--')
plt.grid(which='minor',color='black',linestyle='--')
ax.set_xscale('log')
ax.set_xlabel("$h/L_0$")
ax.set_ylabel("Ks")
ax.set_xlim(4.e-3,0.1)
ax.set_ylim(0.8,3.)

#グラフの計算

def make_data_fig(fig, H0_1, t_1, h_1, make = True):

#=============================
#浅水係数算出式
#=============================
        return fig

    else:
        ax.cla()
        plt.grid(which='major',color='black',linestyle='--')
        plt.grid(which='minor',color='black',linestyle='--')
        ax.set_xscale('log')
        ax.set_xlabel("$h/L_0$", labelpad=10)
        ax.set_ylabel("Ks")
        ax.set_xlim(4.e-3,0.1)
        ax.set_ylim(0.8,3.)
        return fig

#canvasにグラフを描く
def draw_figure(canvas, figure):
    figure_canvas = FigureCanvasTkAgg(figure, canvas)
    figure_canvas.draw()
    figure_canvas.get_tk_widget().pack(side='top', fill='both', expand=1)
    return figure_canvas

#計算
def judgement(value1, value2, value3):
#=============================
#浅水係数算出式
#=============================
    
    result1 = format(H0/L, '.4f')
    result2 = format(h/H0, '.3f')
    result3 = format(h/L0, '.4f')
    result4 = H
    result5 = Ks
    
    return result1, result2, result3, result4, result5

#GUIのレイアウト
col1 = [
    [sg.Text('換算沖波波高', size=(10, 1)), sg.Text('Ho´:', size=(6, 1), justification='right'), sg.InputText(size=(10, 1), key='-VALUE1-', justification='right'), sg.Text('m')],
    [sg.Text('沖波周期', size=(10, 1)), sg.Text('To:', size=(6, 1), justification='right'), sg.InputText(size=(10, 1), key='-VALUE2-', justification='right'), sg.Text('s')],
    [sg.Text('計算水深', size=(10, 1)), sg.Text('h:', size=(6, 1), justification='right'), sg.InputText(size=(10, 1), key='-VALUE3-', justification='right'), sg.Text('m')],
    [sg.Button('計算実行', key='-Load-')]
]

col2 = [
    [sg.Text('波形勾配', size=(10, 1)), sg.Text('Ho´/L:', size=(6, 1), justification='right'), sg.Text('', key='-OUTPUT1-', size=(10, 1), justification='right')],
    [sg.Text('相対水深', size=(10, 1)), sg.Text('h/Ho´:', size=(6, 1), justification='right'), sg.Text('', key='-OUTPUT2-', size=(10, 1), justification='right')],
    [sg.Text('水深波長比', size=(10, 1)), sg.Text('h/Lo:', size=(6, 1), justification='right'), sg.Text('', key='-OUTPUT3-', size=(10, 1), justification='right')],
    [sg.Text('波高', size=(10, 1)), sg.Text('H:', size=(6, 1), justification='right'), sg.Text('', key='-OUTPUT4-', size=(10, 1), justification='right')],
    [sg.Text('浅水係数', size=(10, 1)), sg.Text('Ks:', size=(6, 1), justification='right'), sg.Text('', key='-OUTPUT5-', size=(10, 1), justification='right')]
]

layout = [
    [sg.Column(col1, justification='center'), sg.Column(col2, justification='center')],
    [sg.Button('Display',key='-display-'), sg.Button('clear',key='-clear-')],
    [sg.Graph((100,50), (0,0), (100,50), key='-CANVAS-')]
]



#Windowの設定
window = sg.Window('不規則波の浅水変形', layout, location = (100,100), finalize=True)

# figとCanvasを関連付ける
fig_agg = draw_figure(window['-CANVAS-'].TKCanvas, fig)

#GUIのevent設定
while True:
    event, values = window.read()

    if event in (None, 'Cancel'):
        break
        
    elif event == '-Load-':
        value1 = values['-VALUE1-']
        value2 = values['-VALUE2-']
        value3 = values['-VALUE3-']
        
        value1 = float(value1)
        value2 = float(value2)
        value3 = float(value3)
        
        result1, result2, result3, result4, result5 = judgement(value1, value2, value3)
        
        window['-OUTPUT1-'].update(result1)
        window['-OUTPUT2-'].update(result2)
        window['-OUTPUT3-'].update(result3)
        window['-OUTPUT4-'].update(result4)
        window['-OUTPUT5-'].update(result5)
        

    elif event == '-display-':
        fig = make_data_fig(fig, value1, value2, value3, make=False)
        fig_agg.draw()
        
        value1 = values['-VALUE1-']
        value2 = values['-VALUE2-']
        value3 = values['-VALUE3-']
        
        value1 = float(value1)
        value2 = float(value2)
        value3 = float(value3)
        
        fig = make_data_fig(fig, value1, value2, value3, make=True)
        fig_agg.draw()

    elif event == '-clear-':
        fig = make_data_fig(fig, value1, value2, value3, make=False)
        fig_agg.draw()

window.close()

作成してGUIが↓のようになります。

動かしてみたものが↓のようになります。

Tkinterで作成したものから流用しているのでdef関数が多いですが、大目に見てください。0ベースで作成するとおそらくいくつかのdef関数はeventに統合されると思います。

参考になりそうなポイントを解説します

3-1.レイアウトについて

#GUIのレイアウト
col1 = [
    [sg.Text('換算沖波波高', size=(10, 1)), sg.Text('Ho´:', size=(6, 1), justification='right'), sg.InputText(size=(10, 1), key='-VALUE1-', justification='right'), sg.Text('m')],
・・・

レイアウトについてですが、PySimpleGUIは賢いので適当に書いてもいい感じのレイアウトにしてくれます。その分カスタムにコツがいります。上記のコードでほぼすべてにサイズを指定しているのですが、基本的にサイズを指定しなくても勝手に適切な幅にしてくれます。ではなぜ幅を設定しているかというと、縦方向の並びがそろわないからです。下に参考画像を添付します。参考画像では一番左は揃っていますが、他がガタガタになってしまいます。いろいろ試してみましたが、一つ一つサイズを指定する方法に落ち着きました。一括設定は基本機能に無いようなので、def関数などでしか効率的にできなさそうな感じがします。

3-2.ウィンドウの大きさついて

この問題は、記載したコードで解決できていないのです。
PySimpleGUIは、テキストや画像に合わせてウィンドウの大きさを自動で調節してくれます。特にこだわりがなければ問題ないのですが、レイアウトの都合上、画像が見切れてしまうことがあります。ウィンドウや画像のサイズは調整できるのですが、自動調整をする都合上、更新が新しいものの大きさが優先されます。基本的にウィンドウを作成してから画像を貼り付けるので、どうしてもウィンドウの大きさを固定することが難しいです。
解決方法をご存じの方がいましたらご教授お願いいたします。

4.おわりに

今回は、PySimpleGUIを使って、以前Tkinterで作成したものを再現してみました。なれもありますが、PySimpleGUIはかなり使いやすいと感じました。ただ、自動でいろいろやってくれる分、カスタムしたいところに手が届きづらいと感じました。また、Tkinterに比べマイナーなので、調べてもなかなか解決方法を見つけられないことや、ChatGPTなどのAIに質問しても的外れな回答が返ってくることがありました。

どちらのほうがいいとは一概に言えませんが、それぞれいいところがあるので、相互にコードを流用しつつ、適宜用途に合ったものを使えればと思います。

コメント