Pythonとtkinterを用いた非エンジニアのための社内ツールGUI開発

ども、お久しぶりです、ぺんまるです。
前回の投稿から時間が経ちましたが、再度プログラムを作成したので投稿をしてみました。

自分の近況を少し、最近はゴジラ-1.0の映画を見ました。
タイトル的には何かの前日譚なのでしょうか、映画自体はドルビーアトモス？でみたので迫力いっぱいで楽しむことができました。
やはり、映画はいいものですね。

# プログラムを作成した経緯について
今回プログラムを作成する経緯ですが
* 元のCSVファイルの中身をある処理を施して変換するプログラムと会社の人からの提案。
    * 厳密にいうと、とある装置のプリセット位置レシピ(CSVファイル)と画像の解析結果座標を合わせて、原点からの絶対移動ができれば、変換後のレシピを読みこむだけで楽になりそう…という願い。

* 非エンジニアでも扱えるようにGUI画面にする。
* 意外と記事などで需要あるかなという思った。

なぜPythonとtkinterなのかというと、このプログラムを作成する際に、非エンジニアが使う言語といえばPythonかVBAが多いと思うので、作成のしやすさや配布性を考えてPythonにしました。
tkinterは標準モジュールですが、pandasはモジュールをインストールする必要があります。(numpyもいるかもしれない？)

あくまでもこのまま使用するコードってより、方向性を見出すものとして参考になれば良いと考えています。

# 概要
作成環境： Windows,MacOS
言語：Python
モジュール：tkinter, pandas

今回作成したプログラムは実行すると以下の写真のようなGUIインターフェースが表示されます。

**実行時インターフェース**
![スクリーンショット 2023-12-17 13.58.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3556254/6ac3a935-f867-190a-c29f-e8bece6b631f.png)

* フォルダ参照ボタンはフォルダを選択するダイアログが表示されます。
* レシピ参照ボタンはファイルを選択するダイアログが表示されます。
* 指定位置というものはプルダウンメニューで、数字が羅列されており、そこから数字を選択するものです。
* 実行ボタンは各自設定した処理が行われるものです。今回は以下写真のような処理が実行されるように記述しました。
**実行処理画面**
 ![スクリーンショット 2023-12-17 13.05.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3556254/df36129a-c611-43e5-eb8f-91006e24a3e1.png)
**入力後の画面**
![スクリーンショット 2023-12-17 13.58.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3556254/7d43b626-5444-8a6a-f422-7517391cdefa.png)

guiレシピ.csvファイルの中身は以下です。
**gui.csv**
![スクリーンショット 2023-12-17 13.55.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3556254/1821b73e-65c7-dfde-24ba-d30dd35075f2.png)

そして、最後は新規ウィンドウでレシピの中身が表示されるように記述しました。
![スクリーンショット 2023-12-17 13.55.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3556254/102f3daa-b1fc-6efe-a273-35044095f109.png)

実行ボタンのところはそれぞれ処理したい内容を関数にしてボタンに割り当てる使い方をすればいいかなと思います。

# ひとまずコードを
```python:GUIツール
import os,sys
from tkinter import *
from tkinter import ttk
from tkinter import messagebox
from tkinter import filedialog
import pandas as pd
    
def dir_path_select(iDir):
    defect_dir = filedialog.askdirectory(initialdir = iDir)
    entry_1.set(defect_dir)

def files_path_select(iDir):
    fTyp = [("", "*.csv")]
    recipe_file = filedialog.askopenfilename(filetypes=fTyp, initialdir=iDir)
    entry_2.set(recipe_file)
        
def calculation():
    # entryからそれぞれのパスを取得
    dir_path = entry_1.get()
    setting_path = entry_2.get()
    # プルダウンメニューから値を取得
    position_index = variable.get()
    messagebox.showinfo("ディレクトリパス", f"ディレクトリパスは{dir_path}\nファイルパスは{setting_path}\nプルダウンメニューの値は{position_index}")
    # 設定CSVファイルをデータフレームで読み込み
    df_setting_recipe = pd.read_csv(setting_path)
    # サブウィンドウの設定
    setting_window = Toplevel()
    setting_window.title("setting")
    tree = ttk.Treeview(setting_window)
    tree.column('#0',width=0, stretch='no')
    tree.pack()
    # DataFrameの各列をTreeviewの列として設定
    tree["columns"] = list(df_setting_recipe.columns)
    for column in tree["columns"]:
        tree.heading(column, text=column)

    # DataFrameの各行をTreeviewに追加
    for index, row in df_setting_recipe.iterrows():
        tree.insert("", "end", values=list(row))
    
def dir_disp(*args):
    #　ディレクトリパスの文字列を取得
    dir_path = entry_1.get()
    #　ファイル名だけを抜き出し
    dir_path_name = os.path.basename(dir_path)
    #　ファイル名を表示
    dir_path_label["text"] = dir_path_name
   
# カレントディレクトリのパス取得用(実行ファイル対策)
try:
    iDir = os.path.abspath(os.path.dirname(sys.argv[0]))
except:
    iDir= os.path.abspath(os.path.dirname(__file__))

if __name__ == "__main__":
    root = Tk()
    root.title("GUIツール")
    
    # Frame1の作成
    frame_1 = ttk.Frame(root, padding=10)
    frame_1.grid(row=0, column=1, sticky=E)

    # フォルダ参照」ラベルの作成
    defect_Label = ttk.Label(frame_1, text="フォルダ参照 ", padding=(5, 2))
    defect_Label.pack(side=LEFT)

    #「フォルダ参照」エントリーの作成
    entry_1 = StringVar()
    defect_Entry = ttk.Entry(frame_1, textvariable=entry_1, width=30)
    defect_Entry.pack(side=LEFT)

    # 「フォルダ参照」ボタンの作成
    defect_Button = ttk.Button(frame_1, text="フォルダ参照", command=lambda:dir_path_select(iDir))
    defect_Button.pack(side=LEFT)

    # Frame2の作成
    frame_2 = ttk.Frame(root, padding=10)
    frame_2.grid(row=2, column=1, sticky=E)

    # 「ファイル参照」ラベルの作成
    recipe_Label = ttk.Label(frame_2, text="レシピ参照 ", padding=(5, 2))
    recipe_Label.pack(side=LEFT)

    # 「ファイル参照」エントリーの作成
    entry_2 = StringVar()
    recipe_Entry = ttk.Entry(frame_2, textvariable=entry_2, width=30)
    recipe_Entry.pack(side=LEFT)

    # 「ファイル参照」ボタンの作成
    recipe_Button = ttk.Button(frame_2, text="レシピ参照", command=lambda:files_path_select(iDir))
    recipe_Button.pack(side=LEFT)

    # Frame3の作成
    frame_3 = ttk.Frame(root, padding=10)
    frame_3.grid(row=4,column=1,sticky=W)

    # ドロップダウンリストの説明
    tray_position_Label = ttk.Label(frame_3, text="指定位置", padding=(5, 2))
    tray_position_Label.pack(side=LEFT)

    # ドロップダウンリストの作成
    option = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
    variable = IntVar()
    tray_position_Combo = ttk.Combobox (frame_3, width= 3, values = option, textvariable = variable )
    tray_position_Combo.pack(side=LEFT)

    # Frame4の作成
    frame_4 = ttk.Frame(root, padding=10)
    frame_4.grid(row=5,column=1,sticky=W)
    entry_1.trace('w', dir_disp)
    dir_Label = ttk.Label(frame_4, text="右に選択フォルダが表示されます。", padding=(5, 2))
    dir_Label.pack(side=LEFT)
    dir_path_label = ttk.Label(frame_4, text="", padding=(5, 2))
    dir_path_label.pack(side=LEFT)

    # 実行ボタンの設置
    execution_Button1 = ttk.Button(frame_3, text="実行", command=calculation)
    execution_Button1.pack(fill = "x", padx=30, side = "left")
    
root.mainloop()
```

# 解説
解説の前に参考にさせていただいた記事
[【Python】tkinterでファイル&フォルダパス指定画面を作成する](https://qiita.com/dgkmtu/items/2367a73f7e2d498e6075)
自分の作成したコードは、上記リンク記事の方のコードが主に占めています。
あとは所々自分の欲しい機能を実装して完成しましたが、ネットに公開するにあたり、削除した上でコードを記載しています。

あとは、tkinter,pandasの基本的な使い方は、他の方の記事や公式リファレンスを参考にしていただけると良いと思います。

個人的になかなか情報がないなと感じたところを解説したいと思います。

## エントリー内に入力されたフォルダ名の動的表示について
```python:def dir_disp(*args)について
def dir_disp(*args):
    #　ディレクトリパスの文字列を取得
    dir_path = entry_1.get()
    #　ファイル名だけを抜き出し
    dir_path_name = os.path.basename(dir_path)
    #　ファイル名を表示
    dir_path_label["text"] = dir_path_name
```
上記コードはフォルダパスからフォルダの名前のみを抜きだして、GUIインターフェースにラベル表示するというものです。

このコード、tkinterのtraceメソッドを利用したコードです。
```python:traceの使い方
    frame_4 = ttk.Frame(root, padding=10)
    frame_4.grid(row=5,column=1,sticky=W)
    entry_1.trace('w', dir_disp)
    dir_Label = ttk.Label(frame_4, text="右に選択フォルダが表示されます。", padding=(5, 2))
    dir_Label.pack(side=LEFT)
    dir_path_label = ttk.Label(frame_4, text="", padding=(5, 2))
    dir_path_label.pack(side=LEFT)

```
trace()の"w"は書き込みモードで値が変更されることを検出するそうです。つまり、ここではentry_1のウィジェット変数の変更を検出するということになります。
次のdir_dispは上記のコールバック関数になります。
コールバック関数は引数を3つをとります。
そのため、dir_disp(*args)と可変長引数をとっています。
関数内の処理に引数を使うことがないので、そこら辺は適当にしています。

## レシピの読み込み
```python:def calculation():
    # 設定CSVファイルをデータフレームで読み込み
    df_setting_recipe = pd.read_csv(setting_path)
    # サブウィンドウの設定
    setting_window = Toplevel()
    setting_window.title("setting")
    tree = ttk.Treeview(setting_window)
    tree.column('#0',width=0, stretch='no')
    tree.pack()
    # DataFrameの各列をTreeviewの列として設定
    tree["columns"] = list(df_setting_recipe.columns)
    for column in tree["columns"]:
        tree.heading(column, text=column)

    # DataFrameの各行をTreeviewに追加
    for index, row in df_setting_recipe.iterrows():
        tree.insert("", "end", values=list(row))
```
レシピcsvファイルの読み込みはpandasでDataframeで読み込んでいます。
pandasのDataframeは二次元配列に近い構造をしたオブジェクトになります。
ここではDataframeからtkinterメソッドのtreeviewを使ったGUI表示を解説します。
今回はサブウィンドウで表示させます。
```python:サブウィンドウとTreeviewの設定
    # 設定CSVファイルをデータフレームで読み込み
    df_setting_recipe = pd.read_csv(setting_path)
    # サブウィンドウの設定
    setting_window = Toplevel()
    setting_window.title("setting")
    tree = ttk.Treeview(setting_window)
    tree.column('#0',width=0, stretch='no')
    tree.pack()
```
まずpandasでCSVを読み込む時にcsvファイル側の1行目をカラムに設定されるように読み込んでいます。
サブウィンドウの設定のところはそれぞれお好みで設定ください。
tree.column('#0',width=0, stretch='no')に関しては#0という空白の先頭列が入るため、このコードで削除しています。

```python:Treeviewカラムの設定
# DataFrameの各列をTreeviewの列として設定
    tree["columns"] = list(df_setting_recipe.columns)
    for column in tree["columns"]:
        tree.heading(column, text=column)
```
ここのコードですが、先ほど設定したTreeViewのtreeにカラムを設定しています。
treeのカラムはリストで渡せるので、データフレームのカラムをリストに格納し、treeのcolumn属性として設定しているということになります。
forの部分はその列名を設定するループ文になります。

```python:Treeviewインデックスの設定
    # DataFrameの各行をTreeviewに追加
    for index, row in df_setting_recipe.iterrows():
        tree.insert("", "end", values=list(row))
```
こちらもインデックスをiterrowsを使ってインデックスラベル名とデータ値をそれぞれindex, rowに代入してループしています。
tree.insertではrowに格納されている値をtreeに入れています。indexに入っているラベル（といっても、おそらく1,2という数字）は必要ないので、_を入力してもいいかもしれません。

# まとめ
いかがだったでしょうか。
最初から最後まで全て説明をしているわけではありませんが、僕がこのプログラムを作る際にぶち当たった壁を少し解説してみました。
非エンジニアには意外とない概念があったため最初は手こずりましたが、インターフェース作成というのも奥が深い分野だと考えさせられました。
シンプルなものでもこんなに記述量が増えるので、もう少し凝ったものを作成するとなるとそれに特化した言語やフレームワーク？などを使用した方がいいのかもしれませんね。
また、GUI画面に関しては個性が出ると思うので、様々な記事を参考に頑張ってください。

# 参考にさせていただいたリンク
[【Python】tkinterでファイル&フォルダパス指定画面を作成する](https://qiita.com/dgkmtu/items/2367a73f7e2d498e6075)

[【Python】tkinter：テーブル（表）の作成方法（ttk.Treeview）
](https://office54.net/python/tkinter/ttk-treeview-table)

[How to use trace() in Tkinter?](https://coderslegacy.com/python/tkinter-trace/)