はじめに
仕事で使う技術計算プログラムを Tkinter を用い GUI で使えるようにしてみた。やっていることはニッチな世界なので、GUIプログラム部のみについて紹介している。
概要
- このアプリは、あらかじめ準備したエクセルファイルを1次入力データとし、解析用データファイルの作成、サージング解析の実行と解析結果グラフ表示を行うものである。
- アプリケーションは以下の3つのプログラムで構成されている。
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) -
Clear
ボタンを押すと、Label に格納されているデータを消去する。計算・グラフ表示は Label に格納されているファイル名データに基づいて行われるので、Clear
ボタンが押されて Label がクリアされると。計算もグラフ表示も行われない。計算・グラフ表示を行うには最初からやり直す必要がある。 -
Button
の command で指定した関数からの戻り値は取得できないため、 pack_forget() により非表示のラベルを配置し、そこに他で取得したいデータを書き出し、利用している。 -
subprocess.Popen
による並列処理を導入して計算の高速化を図っている。この種の計算は最低2ケース、通常は3ケースを実行するが、通常処理と比較して計算時間は3分の1程度に短縮(3ケース実行の場合、21 秒 ⇒ 7 秒)される。以下のサイトを参考にした。(https://myenigma.hatenablog.com/entry/2016/04/09/184215#subprocessによる複数子プロセス処理) - tkinter を用いる場合、上記のボタンの command で指定した関数の戻り値が取得できないなどの場合があるため、必要データはラベルに書き出すなどしてこれを用いるのが得策のようである。ファイルのフルパスなど表示すると冗長となり表示したくない場合は、label.pack_forget() により非表示ラベルに記載するなどの対応策もある。
プログラムと作例
エクセルファイル選択画面
計算終了通知画面
計算結果グラフ表示画面
入力データ作成エクセル例
Tips
非表示のラベルの利用( pack_forget() )
ラベルには文字列を書き込むことはできるが、表示されない。
lblx1=ttk.Label(frame1)
lblx1.pack_forget()
ファイル選択
ファイルタイプをexcel(拡張子 xlsx)として filedialog からファイルパスを取得、パスを非表示ラベル lblx1 に書き出している。
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 の最後に改行が入ってしまうので、ラベル書き出し時にはこれを削除。
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 による高速化
基本的な流れは以下の通り。
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 により通知している。
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
ラベルに画像を表示する
下記のようにしないとうまく表示してくれないことに注意。(最後の1枚の図のみしか表示されない)
label.configure(image=img)
label.Image=img
以下の関数では、表示する画像の枚数に応じて準備する Label の個数を変化させるため、Label を numpy 配列( dtype = object )に格納している。
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部のみ)
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()
以 上