【Python】Tkinterで2つのデータを結合し出力するGUIアプリを作成する

# はじめに
本記事ではPythonのTkinterライブラリを使って2つのデータ（今回はcsvとエクセル）を結合し出力するGUIアプリを作成します。

# 作成したGUIアプリについて
今回作成したGUIアプリは以下の通りです。

## 実装機能
1. 結合に使うデータをダイアログで選択する
2. 出力データを格納するフォルダをダイアログで選択する
3. 結合・出力処理を実行する
4. 結合ファイル・出力フォルダが未選択の場合、アラートを出す
5. 処理実行中は処理中のプログレスバーを出す
6. 処理が終わったら完了のメッセージを出す

## 画面
- 初期画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292716/6211d024-fd06-3ba3-607c-07a41bb03e0a.png)
　
- ダイアログの表示（参照ボタン押下時）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292716/965c4a5b-293c-467c-0a44-2404493971be.png)
　
- 結合ファイル・出力フォルダが未選択時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292716/72446e27-a031-7ca8-b240-18d3d9ec62df.png)
　
- 処理中（プログレスバーを表示する）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292716/0695e7e3-ec48-3f24-18dc-09b9cca11559.png)
　
- 処理完了時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292716/4ef6b2f8-77c4-ed84-00f8-a9a10b8e05b0.png)


# 使用データについて
今回は以下2つのデータのIDをキーに結合し出力します。
- データ1（csv）

| ID | 名前 |
| ---- | ---- |
| 1A | 田中 | 
| 1B | 小林 | 
| 1C | 佐藤 |
| 1D | 伊藤 |

- データ2（Excel）

| ID | 住所 |
| ---- | ---- |
| 1A | 東京 | 
| 1B | 埼玉 | 
| 1D | 神奈川 |

- 出力

| ID | 名前 | 住所 |
| ---- | ---- | ---- |
| 1A | 田中 | 東京 | 
| 1B | 小林 | 埼玉 | 
| 1C | 佐藤 |  |
| 1D | 伊藤 | 神奈川 |

# 実装環境
python 3.10.4
Windows 11 Home

# 作成コード
作成したコードは以下になります。

```python
import os
import pandas as pd
import threading
import tkinter as tk
from tkinter import *
from tkinter import ttk
from tkinter import messagebox
from tkinter import filedialog

class Application(tk.Frame):
    def __init__(self,master):
        super().__init__(master)
        self.pack()
        #タイトルの設定
        self.master.title("csv・Excel結合")

        self.set_csv_widgets()
        self.set_excel_widgets()
        self.set_output_widgets()
        self.set_run_widgets()

    # csv関連（ラベル、テキストボックス、参照ボタン）のパーツを配置する
    def set_csv_widgets(self):
        self.csv_frame = ttk.Frame(self, padding=10)
        self.csv_frame.grid(row=0, column=1, sticky=E)

        self.csv_label = ttk.Label(self.csv_frame, text="csvファイル：", padding=(5, 2))
        self.csv_label.pack(side=LEFT)

        self.csv_entry = StringVar()
        self.csv_dir = ttk.Entry(self.csv_frame, textvariable=self.csv_entry, width=70)
        self.csv_dir.pack(side=LEFT)

        self.csv_button = ttk.Button(self.csv_frame, text="参照", command=self.file_dialog_csv_clicked)
        self.csv_button.pack(side=LEFT)

    # Excel関連（ラベル、テキストボックス、参照ボタン）のパーツを配置する
    def set_excel_widgets(self):
        self.excel_frame = ttk.Frame(self, padding=10)
        self.excel_frame.grid(row=2, column=1, sticky=E)

        self.excel_label = ttk.Label(self.excel_frame, text="Excelファイル：", padding=(5, 2))
        self.excel_label.pack(side=LEFT)

        self.excel_entry = StringVar()
        self.excel_dir = ttk.Entry(self.excel_frame, textvariable=self.excel_entry, width=70)
        self.excel_dir.pack(side=LEFT)

        self.excel_button = ttk.Button(self.excel_frame, text="参照", command=self.file_dialog_excel_clicked)
        self.excel_button.pack(side=LEFT)

    # 出力関連（ラベル、テキストボックス、参照ボタン）のパーツを配置する
    def set_output_widgets(self):
        self.output_frame = ttk.Frame(self, padding=10)
        self.output_frame.grid(row=4, column=1, sticky=E)

        self.output_label = ttk.Label(self.output_frame, text="出力フォルダ：", padding=(5, 2))
        self.output_label.pack(side=LEFT)

        self.output_entry = StringVar()
        self.output_dir = ttk.Entry(self.output_frame, textvariable=self.output_entry, width=70)
        self.output_dir.pack(side=LEFT)

        self.output_button = ttk.Button(self.output_frame, text="参照", command=self.dir_dialog_clicked)
        self.output_button.pack(side=LEFT)

    # 実行関連（ラベル、参照ボタン）のパーツを配置する
    def set_run_widgets(self):
        self.run_frame = ttk.Frame(self, padding=10)
        self.run_frame.grid(row=6,column=1,sticky=E)

        self.run_button = ttk.Button(self.run_frame, text="実行", command=self.run_thread)
        self.run_button.pack(side = RIGHT)

    # 参照ボタン（csv）押下時
    def file_dialog_csv_clicked(self):
        #csvファイル指定のダイアログを出す
        fTyp = [("", "csv")]
        iFile = os.path.abspath(os.path.dirname(__file__))
        iFilePath = filedialog.askopenfilename(filetype = fTyp, initialdir = iFile)
        self.csv_entry.set(iFilePath)

    # 参照ボタン（Excel）押下時
    def file_dialog_excel_clicked(self):
        #Excelファイル指定のダイアログを出す
        fTyp = [("", "xlsx")]
        iFile = os.path.abspath(os.path.dirname(__file__))
        iFilePath = filedialog.askopenfilename(filetype = fTyp, initialdir = iFile)
        self.excel_entry.set(iFilePath)

    # 参照ボタン（出力）押下時
    def dir_dialog_clicked(self):
        #出力フォルダ指定のダイアログを出す
        iDir = os.path.abspath(os.path.dirname(__file__))
        iDirPath = filedialog.askdirectory(initialdir = iDir)
        self.output_entry.set(iDirPath)

    #実行ボタン押下時の処理をスレッドで行う（プログレスバーを動かすため）
    def run_thread(self):
        threading.Thread(target=self.run_clicked, daemon=True).start()

    # 実行ボタン押下時
    def run_clicked(self):
        self.set_progress_widgets()

        alert_text = self.check_file_path()
        if alert_text == "":
            #ファイル、フォルダのパスが指定されている場合、処理を実行する
            self.merge_file()
            self.delete_progress()
            messagebox.showinfo("info", "処理が完了しました")
        else:
            #ファイル、フォルダのパスが指定されていない場合、アラートを出して処理を終了する
            self.delete_progress()
            messagebox.showinfo("info", alert_text)

    #処理中のラベルとプログレスバーを設置する
    def set_progress_widgets(self):
        self.process = ttk.Label(self.run_frame, text="処理中・・・")
        self.process.pack(side=LEFT)
        self.process.update()

        var = tk.IntVar()
        self.pb=ttk.Progressbar(self.run_frame,maximum=100,mode="indeterminate",variable=var)
        self.pb.pack(side=LEFT)
        self.pb.start(interval=10)

    #パスが設定されているか確認する
    def check_file_path(self):
        alert_text = ""

        csv_path = self.csv_entry.get()
        excel_path = self.excel_entry.get()
        output_path = self.output_entry.get()
        if csv_path == '':
            alert_text += "csvファイル" + "\n"
        if excel_path == '':
            alert_text += "excelファイル" + "\n"
        if output_path == '':
            alert_text += "出力フォルダ" 

        if alert_text:
            alert_text = "以下のファイルの指定がありません" + "\n" + alert_text
        
        return alert_text

    #csvとexcelを結合する
    def merge_file(self):
        csv_path = self.csv_entry.get()
        excel_path = self.excel_entry.get()

        csv_df = pd.read_csv(csv_path)        
        excel_df = pd.read_excel(excel_path)

        join_df = pd.merge(csv_df, excel_df, on=["ID"], how="left")

        output_dir = self.output_entry.get() + "\出力.xlsx"
        join_df.to_excel(output_dir,index=FALSE)

    #処理中のラベルとプログレスバーの削除する
    def delete_progress(self):
        self.process.destroy()
        self.pb.destroy()

def main():
    root = tk.Tk()
    app = Application(master=root)
    app.mainloop()

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


# 終わりに
今回はPythonのTkinterライブラリを使って2つのデータ（今回はcsvとエクセル）を結合し出力するGUIアプリを作成しました。
ファイルの選択やプログレスバーの表示はTkinterを用いた他のGUIアプリを作成する際にも利用が出来そうだと感じました。
ご参考になれば幸いです。

# 参考
この記事は以下の記事を参考にして執筆しました。
・[【Python】tkinterでファイル&フォルダパス指定画面を作成する](https://qiita.com/dgkmtu/items/2367a73f7e2d498e6075)
・[【Python】Tkinterのテンプレート](https://qiita.com/kotai2003/items/fe9b5e59c7164a95ded8)
・[【Python】Tkinterを使った雛形（クラス化手法）](https://qiita.com/michimichix521/items/4d8721aaa59e1c913d9a)
・[Threading with Tkinter to introduce Progress bar](https://stackoverflow.com/questions/73864639/threading-with-tkinter-to-introduce-progress-bar)