Python: Tkinterを用いたMac用appの作成（Label, Entryの格子状配置）（2024.10.27）

（2024.10.29）コードを修正しました。
（2024.10.31）結局こんな感じになりました、
（2024.11.03）修正版


## はじめに
最近PythonのTkinterの使い方を勉強している。そんな中、作ってみたいアプリがあったので、作ってみることにした。
当方の環境は以下の通り。

M1 Pro MacBook Pro 14"
macOS Sequoia 15.0.1
Python 3.13.0
Tcl/Tk 8.6.15


## 動作写真
実行している画面は、以下の写真の通り。Navy色のセルには初期値を入れてあるが、修正して再計算が可能。所定の数値を入力し、左上のボタンを押すと、別プログラムで計算して出力された jpg 画像が、右側の Sub window に表示される。Sub window に表示されたグラフは名前を付けて保存が可能（実際には別プログラムで出力してある jpg 画像を別名で保存している）。新たにデータを入力してグラフを表示するには、Sub window を閉じる必要がある。

![IMG_0110.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/49abbd04-d49b-6d0b-f05d-7346f4dae107.jpeg)


## アプリ概要
アプリ構成の概要は以下の通り。

![fig1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/192222b4-2fde-d9a0-7d67-1f7249888a01.jpeg)


```py_rebar_inp.py```と```py_rebar_cal.py```という２つの Python コードから成り立っている。
やっていることは以下の通り。

1. ```py_rebar_inp.py```を起動し、画面より必要データを入力し、これを csv ファイルとして出力。
2. ```py_rebar_inp.py```から```subprocess.run```により```py_rebar_cal.py```を実行。
3. ```py_rebar_cal.py```では、出力された csv ファイルを読み込み計算処理を行い、jpg イメージのグラフを出力。
4. 再び```py_rebar_inp.py```に制御を戻し、sub window で出力された jpg イメージを表示。
4. ```py_rebar_inp.py```の sub window から画像の保存ができるようにした。filedialog で保存ディレクトリ・ファイル名を指定し、保存が終わったら messagebox で通知する。

プログラムが動くようになったら、Mac の automator で app 化する。
automator は、「automator 起動 => Application 選択 => run shell script 選択 => shell script 記載 => 保存」というステップで app が作成できる。
automator での app 化では、以下に注意。

+ 記載するシェルスクリプトの Python のありか、実行するコード共フルパスで記載。
+ subprocess で実行するプログラム（```py_rebar_cal.py```）、入出力用の csv ファイル名、jpg 画像ファイル名も、全てコード内にフルパスで記載。

```py_rebar_cal.py``` でやっている計算はマニアックなものなので説明は省略。

```py_rebar_inp.py``` の画面作成について、少し解説しておく。画面イメージは以下の通り。

![fig2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129300/2754c164-c4d9-b954-f587-84f8b0468ed3.jpeg)

Label および Entry の配置が格子状になっているのが特徴。これをいかに手を抜いて作成するかを少しだけ考えた。方法としては、numpy 配列の ```dtype=object``` を使うことである。

Label 配置領域は、```lab``` という１次元の numpy 配列を定義し、要素として Label を指定し、grid を用いて row と column を指定して配置する、

Entry 配置領域は、```arc``` という２次元の numpy 配列を定義し、要素として Entry を指定し、grid を用いて row と column を指定して配置する。Entry を配置しないところには文字列 'na' を入れておき、grid で配置するときは 'na' が入っている要素はなにもせず、for ループを回していく。
```arc``` 配列の４行目は、本来 Label を使うところであるが、ここでは Entry を使い、文字列記入後、```readonly``` にしている。


なお、jpg を表示するときは、PIL で処理する必要があるらしい。
ちなみに、ここでは、jpg 画像は、Label 上に表示している。


## コード
以下に、```py_rebar_inp.py```のコードを示す、

```python
import tkinter as tk
import tkinter.font
from tkinter import filedialog
from tkinter import messagebox
from PIL import ImageTk, Image
import numpy as np
import subprocess


def end_q():
    quit()


def out_data(dir,arc):
    # create data file (csv)
    n,m=arc.shape
    fcu=float(arc[0,0].get())
    fy= float(arc[1,0].get())
    bb,hh=float(arc[2,0].get()),float(arc[2,1].get())
    tt=[]; nn=[]; cc=[]
    for i in range(0,m):
        t=float(arc[4,i].get())
        n=float(arc[5,i].get())
        c=float(arc[6,i].get())
        tt.append(t)
        nn.append(n)
        cc.append(c)
    ax,mo=float(arc[7,0].get()),float(arc[7,1].get())

    s1='{0:6.0f}'.format(fcu)
    s2='{0:6.0f}'.format(fy)
    s3='{0:6.0f},{1:6.0f}'.format(bb,hh)
    s4='{0:6.0f},{1:6.0f},{2:6.0f},{3:6.0f}'.format(tt[0],tt[1],tt[2],tt[3])
    s5='{0:6.0f},{1:6.0f},{2:6.0f},{3:6.0f}'.format(nn[0],nn[1],nn[2],nn[3])
    s6='{0:6.0f},{1:6.0f},{2:6.0f},{3:6.0f}'.format(cc[0],cc[1],cc[2],cc[3])
    s7='{0:6.1f},{1:6.1f}'.format(ax,mo)
    ss=s1+'\n'+s2+'\n'+s3+'\n'+s4+'\n'+s5+'\n'+s6+'\n'+s7

    fnameW=dir+'_rebar_inp.csv'
    f=open(fnameW,'w')
    print(ss,file=f)
    f.close()


def calc(dir):
    # calculation and drawing
    p1='/Users/kk/.pyenv/shims/python'
    p2=dir+'py_rebar_cal.py'
    cmd='{0} {1}'.format(p1,p2)
    subprocess.run(cmd,shell=True)


def show_jpg(sub_win,dir):
    def save_image(sub_win,img_org):
        save_path=filedialog.asksaveasfilename(initialfile='fig_rebar_xxx.jpg')
        if save_path!='': # condition of not cancel 
            img_org.save(save_path)
            s='Image was saved.\n'+save_path+'\nDo you want to close this window?'
            if messagebox.askyesno('Close',s):sub_win.destroy()

    if sub_win==None or not sub_win.winfo_exists(): # one sub-window
        fnameF=dir+'_fig_rebar_tk.jpg'
        im_org=Image.open(fnameF)
        w,h=im_org.size
        px=int(400)
        py=int(px*h/w)
        im_resize=im_org.resize((px,py))
        img = ImageTk.PhotoImage(im_resize)

        sub_win=tk.Toplevel()
        sxy='{0}x{1}'.format(px,py)
        sub_win.title('M-N diagram ({0})'.format(sxy))
        label_i=tk.Label(sub_win,image=img)
        label_i.pack()
        save_button=tk.Button(sub_win,relief='raised',text='Save As...',command=lambda:save_image(sub_win,im_org))
        save_button.pack()
        sub_win.mainloop()


def b1_click(sub_win,arc):
    dir='/Users/kk/stream_deck/'
    out_data(dir,arc) # create data file (csv)
    calc(dir) # calculation and drawing (jpg)
    show_jpg(sub_win,dir) # show jpg image


def ival(arc):
    n,m=arc.shape
    # set initial values
    val=np.array([
        [    30,np.nan,np.nan,np.nan],
        [   460,np.nan,np.nan,np.nan],
        [  1000,   500,np.nan,np.nan],
        [np.nan,np.nan,np.nan,np.nan],
        [    25,     0,     0,    25],
        [     5,     0,     0,     5],
        [   100,   300,   300,   100],
        [     0, 255.2,np.nan,np.nan]
    ],dtype=np.float64)
    for i in range(0,n):
        for j in range(0,m):
            if np.isnan(val[i,j]): continue
            arc[i,j].delete(0,tk.END)
            if i<n-1: arc[i,j].insert(tk.END,'{0:.0f}'.format(val[i,j]))
            else:     arc[i,j].insert(tk.END,'{0:.1f}'.format(val[i,j]))


def main():
    sub_win=None # difinition of sub_window
    root = tk.Tk()
    root.title('RC calc (BS 8110-1)')
    fn1,fn2='Menlo','Courier new'
    fontf=tkinter.font.Font(root,family=fn1,size=14,weight='bold',underline=True)
    font1=tkinter.font.Font(root,family=fn1,size=14)
    font2=tkinter.font.Font(root,family=fn2,size=17,weight='bold')

    frame1=tk.LabelFrame(root, text='(Execution Button)',relief='flat',font=fontf)
    button1=tk.Button(frame1,relief='raised',text='Calculation',command=lambda:b1_click(sub_win,arc))
    button2=tk.Button(frame1,relief='raised',text='(Initial values)',comman=lambda:ival(arc))
    button1.grid(row=0,column=0)
    button2.grid(row=0,column=1)
    frame1.pack(fill=tk.X)


    frame2 = tk.LabelFrame(root,text='(Data Input)',relief='flat',font=fontf)
    ss=['Con fcu (MPa)',
        'Rebar fy (MPa)',
        'b (mm) x h (mm)',
        'Reinforcement',
        'Bar dia (mm)',
        'Bar num (nos)',
        'Cover (mm)',
        'Design N (kN),M (kN-m)']
    lab=np.empty(len(ss),dtype=object)
    for i in range(0,len(ss)):
        lab[i]=tk.Label(frame2,text=ss[i],font=font2)
        lab[i].grid(row=i,column=0)

    arc=np.array([
        [      1,     'na',    'na',    'na'],
        [      1,     'na',    'na',    'na'],
        [      1,        1,    'na',    'na'],
        ['Tens-1','Tens-2','Comp-2','Comp-1'],
        [      1,        1,       1,       1],
        [      1,        1,       1,       1],
        [      1,        1,       1,       1],
        [      1,        1,    'na',    'na']
    ],dtype=object)
    n,m=arc.shape
    for i in range(0,n):
        for j in range(0,m):
            if arc[i,j]=='na': continue
            s=arc[i,j]
            arc[i,j]=tk.Entry(frame2,justify=tk.CENTER,width=7,bg='#000080',font=font2)
            if s!=1:
                arc[i,j].insert(tk.END,s)
                arc[i,j].configure(state='readonly',bg='#000000')
            arc[i,j].grid(row=i,column=j+1)
    ival(arc) # set initial values
    frame2.pack(fill=tk.BOTH)


    frame3 = tk.LabelFrame(root,text='(Note)',relief='flat',font=fontf)
    s1='+ Close M-N diagram for next calculation.'
    s2='+ Tens-1 & Tens-2 are tensile bars.'
    s3='+ Comp-1 & Comp-2 are compressive bars.'
    s4='+ Cover is distance from edge to rebar center.'
    ss=s1+'\n'+s2+'\n'+s3+'\n'+s4
    label1=tk.Label(frame3,text=ss,justify=tk.LEFT,font=font1)
    label1.grid(columnspan=2,row=1,column=0)
    frame3.pack(fill=tk.X)


    q_button=tk.Button(root,text='Quit',command=end_q)
    q_button.pack()

    root.mainloop()


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

以　上