Python Tkinter GUIを用いたアプリを解説してみる

# はじめに
仕事で使う技術計算プログラムを Tkinter を用い GUI で使えるようにしてみた。やっていることはニッチな世界なので、GUIプログラム部のみについて紹介している。


# 概要
- このアプリは、あらかじめ準備したエクセルファイルを１次入力データとし、解析用データファイルの作成、サージング解析の実行と解析結果グラフ表示を行うものである。
- アプリケーションは以下の３つのプログラムで構成されている。

| py_surge_inp.py | GUI によるコントロールプログラム（本記事で紹介） |
| --- | --- |
| py-surge_data.py | エクセルデータを読み込み解析用データファイルを作成する外部プログラム |
| Py-surge_calc.py | サージング計算を行いグラフを作成する外部プログラム |
- ttkのスタイルは`style.theme_use('aqua')` を指定。実行中のボタンは自動で色が変わる（ここでは青色）。
- `Open Excel` ボタンで選択したエクセルファイルを読み込み、サージング解析用のデータファイルを作成。外部プログラム py_surge_data,py を利用。
- `Calculation` ボタンでサージング解析を実施。外部プログラム py_surge_calc.py を利用。
- `Show jpg` ボタンで解析結果に基づき作成した水位時刻歴グラフを表示。グラフのjpg画像はLabelを用いて表示している。以下のサイトを参考にした。([https://maruo-pythonstudy.hatenablog.com/entry/2023/03/01/002002](https://maruo-pythonstudy.hatenablog.com/entry/2023/03/01/002002))
- `Clear` ボタンを押すと、Label に格納されているデータを消去する。計算・グラフ表示は Label に格納されているファイル名データに基づいて行われるので、`Clear` ボタンが押されて Label がクリアされると。計算もグラフ表示も行われない。計算・グラフ表示を行うには最初からやり直す必要がある。
- `Button` の command で指定した関数からの戻り値は取得できないため、 pack_forget() により非表示のラベルを配置し、そこに他で取得したいデータを書き出し、利用している。
- `subprocess.Popen` による並列処理を導入して計算の高速化を図っている。この種の計算は最低２ケース、通常は３ケースを実行するが、通常処理と比較して計算時間は３分の１程度に短縮（３ケース実行の場合、21 秒 ⇒ 7 秒）される。以下のサイトを参考にした。([https://myenigma.hatenablog.com/entry/2016/04/09/184215#subprocessによる複数子プロセス処理](https://myenigma.hatenablog.com/entry/2016/04/09/184215#subprocess%E3%81%AB%E3%82%88%E3%82%8B%E8%A4%87%E6%95%B0%E5%AD%90%E3%83%97%E3%83%AD%E3%82%BB%E3%82%B9%E5%87%A6%E7%90%86))
- tkinter を用いる場合、上記のボタンの command で指定した関数の戻り値が取得できないなどの場合があるため、必要データはラベルに書き出すなどしてこれを用いるのが得策のようである。ファイルのフルパスなど表示すると冗長となり表示したくない場合は、label.pack_forget() により非表示ラベルに記載するなどの対応策もある。

# プログラムと作例
## エクセルファイル選択画面
![Screenshot_2025-03-02_at_21.47.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/9f43e1a5-259c-4bd6-9cf8-da85bec9928d.png)

## 計算終了通知画面
![Screenshot_2025-03-02_at_21.48.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/d47e37d8-0d2e-4995-a053-0656a7fdfa4c.png)

## 計算結果グラフ表示画面
![Screenshot_2025-03-02_at_21.48.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/cf327239-54cb-49ad-8b2d-d66ca878a095.png)


## 入力データ作成エクセル例
![surge_data_HR.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/7810ad3a-4fa0-4e0f-bac9-a9d22ff5d1ce.jpeg)


# Tips

## 非表示のラベルの利用（ pack_forget() ）

ラベルには文字列を書き込むことはできるが、表示されない。

```python
lblx1=ttk.Label(frame1)
lblx1.pack_forget()
```

## ファイル選択

ファイルタイプをexcel（拡張子 xlsx）として filedialog からファイルパスを取得、パスを非表示ラベル lblx1 に書き出している。

```python
from tkinter import filedialog

filetype=[('Excel files','*.xlsx')]
path=filedialog.askopenfilename(initialdir=dir,filetypes=filetype) 
lblx1['text']=path
```

以下の関数では、入力用エクセルファイルを選択後、外部プログラム py_surge_data.py を実行。内部で　print 出力したテキスト（解析用入力データファイル名）を戻り値 res として取得し。ラベルに書き出している。res の最後に改行が入ってしまうので、ラベル書き出し時にはこれを削除。 

```python
ef open_excel(dir,lblx1,label1):
    filetype=[('Excel files','*.xlsx')]
    path=filedialog.askopenfilename(initialdir=dir,filetypes=filetype) 
    lblx1['text']=path
    p1='/Users/kk/.pyenv/shims/python'
    p2=dir+'/py_surge_data.py'
    cmd='{0} {1} {2}'.format(p1,p2,path)
    res=subprocess.run(cmd,shell=True,capture_output=True,text=True).stdout
    label1['text']=res.rstrip('\r\n')
```

## subprocess.Popen による高速化

基本的な流れは以下の通り。

```python
procs=[]
for i in range(0,n):
    proc=subprocess.Popen(cmd,shell=True)
    procs.append(proc)
for proc in procs:
    proc.communicate()
```

以下の関数では、ラベルに記載したファイル名から入力用ファイルのフルパスを作成、入力用ファイル名でループを回しながら、出力用テキストファイル名、画像ファイル名を作成し解析を実行。解析実行は subprocess.Popen を活用。計算終了は messagebox により通知している。

```python
def calc(dir,lblx1,label1,label2):
    start=time()
    res = label1['text']
    text=res.split('\n')
    fname=text[1:]
    print(fname)
    path=lblx1['text']
    dirname = os.path.dirname(path)
    procs=[]
    gra=[]
    for fnameR in fname:
        fnameR=dirname+'/'+fnameR
        fnameW=fnameR.replace('inp_','out_')
        fnameF=(fnameR.replace('.csv','.jpg')).replace('inp_','fig_')
        gra.append(os.path.basename(fnameF))
        print(fnameR) # input csv file name
        print(fnameW) # output csv file name
        print(fnameF) # output image jpg file name
        p1='/Users/kk/.pyenv/shims/python'
        p2=dir+'/py_surge_calc.py'
        cmd='{0} {1} {2} {3} {4}'.format(p1,p2,fnameR,fnameW,fnameF)
        proc=subprocess.Popen(cmd,shell=True)
        procs.append(proc)
    for proc in procs:
        proc.communicate()
    end=time()
    st='{0:.3f} sec'.format(end-start)
    print(st)
    
    ss='Calculation\nfinished\n'+st
    messagebox.showinfo('surge',ss)
    sgra='\n'.join(gra)
    label2['text']=sgra
```

## ラベルに画像を表示する

下記のようにしないとうまく表示してくれないことに注意。（最後の１枚の図のみしか表示されない）

```python
label.configure(image=img)
label.Image=img
```

以下の関数では、表示する画像の枚数に応じて準備する Label の個数を変化させるため、Label を numpy 配列（ dtype = object ）に格納している。

```python
def show_jpg(lblx1,label2):
    path=lblx1['text']
    dirname = os.path.dirname(path)

    res = label2['text']
    text=res.split('\n')
    gra=text
    print(gra)
    n=len(gra)

    sub=tk.Toplevel()
    sub.resizable(False,False)
    sub.title('Surging')
    lbl=np.empty(n,dtype=object)    
    for i in range(0,n):
        lbl[i]=ttk.Label(sub)
        lbl[i].grid(row=i%2,column=i//2)

    for i in range(0,n):
        fnameF=dirname+'/'+gra[i]
        print(fnameF)
        im_org=Image.open(fnameF)
        w,h=im_org.size
        px=int(500)
        py=int(px*h/w)
        im_resize=im_org.resize((px,py))
        img = ImageTk.PhotoImage(im_resize)
        lbl[i].configure(image=img)
        lbl[i].Image=img
    sub.protocol('WM_DELETE_WINDOW', sub.destroy)
    sub.mainloop()
```


# コード全文（GUI部のみ）
```python: py_surge_inp.py
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
import sys
import os
import subprocess
from PIL import ImageTk, Image
import numpy as np
from time import time


def end_q():
    sys.exit()


def show_jpg(lblx1,label2):
    path=lblx1['text']
    dirname = os.path.dirname(path)

    res = label2['text']
    text=res.split('\n')
    gra=text
    print(gra)
    n=len(gra)

    sub=tk.Toplevel()
    sub.resizable(False,False)
    sub.title('Surging')
    lbl=np.empty(n,dtype=object)    
    for i in range(0,n):
        lbl[i]=ttk.Label(sub)
        lbl[i].grid(row=i%2,column=i//2)

    for i in range(0,n):
        fnameF=dirname+'/'+gra[i]
        print(fnameF)
        im_org=Image.open(fnameF)
        w,h=im_org.size
        px=int(500)
        py=int(px*h/w)
        im_resize=im_org.resize((px,py))
        img = ImageTk.PhotoImage(im_resize)
        lbl[i].configure(image=img)
        lbl[i].Image=img
    sub.protocol('WM_DELETE_WINDOW', sub.destroy)
    sub.mainloop()


def calc(dir,lblx1,label1,label2):
    start=time()
    res = label1['text']
    text=res.split('\n')
    fname=text[1:]
    print(fname)
    path=lblx1['text']
    dirname = os.path.dirname(path)
    procs=[]
    gra=[]
    for fnameR in fname:
        fnameR=dirname+'/'+fnameR
        fnameW=fnameR.replace('inp_','out_')
        fnameF=(fnameR.replace('.csv','.jpg')).replace('inp_','fig_')
        gra.append(os.path.basename(fnameF))
        print(fnameR)
        print(fnameW)
        print(fnameF)
        p1='/Users/kk/.pyenv/shims/python'
        p2=dir+'/py_surge_calc.py'
        cmd='{0} {1} {2} {3} {4}'.format(p1,p2,fnameR,fnameW,fnameF)
        proc=subprocess.Popen(cmd,shell=True)
        procs.append(proc)
    for proc in procs:
        proc.communicate()
    end=time()
    st='{0:.3f} sec'.format(end-start)
    print(st)
    
    ss='Calculation\nfinished\n'+st
    messagebox.showinfo('surge',ss)
    sgra='\n'.join(gra)
    label2['text']=sgra


def open_excel(dir,lblx1,label1):
    filetype=[('Excel files','*.xlsx')]
    path=filedialog.askopenfilename(initialdir=dir,filetypes=filetype) 
    lblx1['text']=path
    p1='/Users/kk/.pyenv/shims/python'
    p2=dir+'/py_surge_data.py'
    cmd='{0} {1} {2}'.format(p1,p2,path)
    res=subprocess.run(cmd,shell=True,capture_output=True,text=True).stdout
    label1['text']=res.rstrip('\r\n')


def clear_label(lblx1,label1,label2): 
    lblx1['text']=''
    label1['text']=''
    label2['text']=''


def main():
    dir='/Users/kk/0_py_app/surge_analysis'

    root = tk.Tk()
    root.resizable(False,False)
    root.title('py_surge_inp.py')

    style=ttk.Style()
    style.theme_use('aqua')
    fontn='Calibri 12'
    fontb='Calibri 12 bold'
    fontt='Menlo 11'
    style.configure('.',font=(fontn))

    frame1=ttk.Frame(root,relief=tk.FLAT)
    button1=ttk.Button(frame1,text='Open Excel',command=lambda:open_excel(dir,lblx1,label1))
    label1=ttk.Label(frame1,font=(fontt))
    button2=ttk.Button(frame1,text='Calculation',command=lambda:calc(dir,lblx1,label1,label2))
    label2=ttk.Label(frame1,font=(fontt))
    button3=ttk.Button(frame1,text='Show jpg',command=lambda:show_jpg(lblx1,label2))
    button4=ttk.Button(frame1,text='Clear',command=lambda:clear_label(lblx1,label1,label2))
    lblx1=ttk.Label(frame1)

    button1.pack()
    label1.pack()
    button2.pack()
    label2.pack()
    button3.pack()
    button4.pack()
    lblx1.pack_forget()
    frame1.pack(fill=tk.X)

    q_button=ttk.Button(root,text='Exit',command=end_q)
    q_button.pack()



    root.mainloop()


if __name__ == '__main__': main()
```

以　上