Pythonで簡単なGUIアプリケーションを作る


## どんな記事？
* `Python`のライブラリを使ってGUIアプリを作った
* Excelファイルの読み込みやDB(MySQL)への接続などを行った

## はじめに
社外製の業務システムを運用することになりました
ネットワークを介してデータの通信を行うWebアプリケーションではなく、ローカルPCにDB(MySQL)を用意して接続・利用するスタンドアロン型のシステムです

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289210/91634156-5171-8946-57d9-dabf4643bbc3.png">

しかしこのシステム、元々社外での利用を想定していない状況で作られたアプリケーションなので、
開発者でない人が使用することを想定していません
よって、利用するパラメータやしきい値の設定は**直接DBをいじって変更する**しかないようです
またテーブル構成の仕様上、同じ入力値を何回も入力する必要があったりする部分が多々あります

というか、そもそも部署内の方は非エンジニアの人が多く、SQLクエリを記述してデータ更新～といった操作は難しい...
-> 非エンジニアでもDB内のパラメータを簡単に操作できるツールとか作れる？

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289210/6175b8a7-f801-8d00-5080-29a8ff1515ed.png">

要は「入力しやすいインタフェースを作る」って感じです

という経緯で、GUIアプリケーションを作ることになりました
自身の知識の定着も兼ねて、記事として残しておきます

## 機能概要
### 運用イメージ
1. 入力シートとしてExcelファイルを作成、このファイルにパラメータ等を入力する
2. 1で作成したシートを元にCSVを生成
3. 2で作成したCSVをDBにインポート

という流れで利用するツールとして作成していこうと思います
別にCSV生成を挟まなくても実現できそうなのですが、部署内から「一度入力した設定が上書きされて復元不可能になるのが怖い」との意見が出たので、バックアップ的な役割としてワンクッション置いておきます

### 2つの機能
入力シートのフォーマットは別として、今回作成するアプリケーションには以下の機能が最低限必要です

* (パラメータ等を入力した)Excelファイルを元にしてCSVファイル生成
* 生成したCSVファイルをDBにインポート

## できたもの
動作例はこんな感じ

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/289210/40b174d2-f386-4ebe-5c04-e5085aba4400.gif" width=400px>


## 構成
### 使用したツール・ライブラリなど
* 使用言語は`Python`
* DBとの接続(インポート機能)には`mysql.connector`ライブラリを使用
* Excelファイルへの値の読み書きは`openpyxl`ライブラリを使用
* コマンドプロンプトで操作するのは不便なのでGUI作成に`PySimpleGUI`ライブラリを使用
* python実行環境がないPCでも使えるようパッケージング（exe化）を行う。`pyinstaller`ライブラリを使用

### 動作環境
* Python 3.6.1
  * openpyxl 3.0.5
  * PySimpleGUI 4.34.0
  * pyinstaller 4.0
* MySQL 5.7.19

## ソースコード
<details><summary>以下、ソースコード</summary><div>

```python
import openpyxl as ex
import PySimpleGUI as sg
import os
import csv
import datetime
import mysql.connector as mydb

class Person:
    def __init__(self, name: str, mail_address:str, age: str) -> None:
        self.name = name
        self.mail_address = mail_address
        self.age = age

    def toList(self):
        return [
            self.name,
            self.mail_address,
            self.age
        ]

class Persons:
    # ExcelファイルからデータをLoad
    def __init__(self, inputsheet_name: str) -> None:
        self.inputsheet_name = inputsheet_name # データ元のファイル名
        self.persons = []

        wb = ex.load_workbook(f"./InputSheet/{inputsheet_name}")
        ws = wb["Sheet1"]

        for index in range(3, 9999):
            name = ws.cell(index, 3).value
            mail_address = ws.cell(index, 5).value
            age = ws.cell(index, 7).value

            if name is None:
                break

            self.persons.append(Person(name, mail_address, age))

    # Personの属性名をリスト形式で出力
    @staticmethod
    def attributeList():
        return [
            "name",
            "mail_address",
            "age"
        ]

    # 格納されているデータをすべてリスト表示
    def print_all(self):
        print(self.attributeList())
        for p in self.persons:
            print(p.toList())

        return 0

    # csvファイルに出力
    def output_csv(self):
        # List形式でpersonsの情報を格納
        out = []
        out.append(self.attributeList())
        for record in self.persons:
            out.append(record.toList())

        # csv書き出し
        filename = f"persons_{datetime.datetime.now():%Y%m%d_%H%M%S}"
        csvFilePath = f"./csv/{filename}.csv"
        with open(csvFilePath, "w", encoding="utf-8") as f:
            writer = csv.writer(
                f,
                lineterminator="\n",
                escapechar=" "
            )
            writer.writerows(out)

        print(f"Created file {csvFilePath}")
        return 0

# csvデータからDBにインポート
def importCSVTable(csvfile_name):
    # *一部本来の記述から変更しています
    conn = mydb.connect(
        host="localhost",
        port=3306,
        user="sampleuser",
        password="*****",
        database="sample_db",
        allow_local_infile=True
    )

    # DB操作用カーソル
    cur = conn.cursor()

    # 既存レコード全削除
    table_name = "persons"
    cur.execute(f"TRUNCATE {table_name} ;")

    # csvファイルからレコード読み込み
    csvfile_path = f"./csv/{csvfile_name}"
    enclosed = "\""     # 囲み文字
    terminated = ","    # 区切り文字
    lines = "\\r\\n"    # 改行コード
    sql = f"""
        LOAD DATA LOCAL INFILE
            "{csvfile_path}"
        INTO TABLE {table_name}
            FIELDS TERMINATED BY '{terminated}'
            OPTIONALLY ENCLOSED BY '{enclosed}'
            LINES TERMINATED BY '{lines}'
            IGNORE 1 LINES ;
    """
    cur.execute(sql)

    # DBに反映
    conn.commit()
    conn.close()

    return 0

def Layout():
    textbox_width = 50
    textbox_height = 8

    inputsheet_list = os.listdir("./InputSheet")

    tab1_layout = [
        [sg.Text("InputSheet list")],
        [sg.Listbox(
            inputsheet_list,
            size=(textbox_width, textbox_height),
            key="inputsheet"
        )],
        [sg.Button("CREATE csv file", key="create_csv")]
    ]

    csv_list = os.listdir("./csv")
    tab2_layout = [
        [sg.Text("csv list")],
        [sg.Listbox(
            csv_list,
            size=(textbox_width, textbox_height),
            key="select_csv",
            enable_events=True
        )],
        [sg.Button("IMPORT to database", key="import_csv")]
    ]

    layout = [
        [sg.TabGroup(
            [[
                sg.Tab("csv生成", tab1_layout),
                sg.Tab("DBへインポート", tab2_layout)
            ]],
            enable_events=True
        )],
        # Log出力
        [sg.Text("Log")],
        [sg.Output( size=(textbox_width, textbox_height*0.5) )]
    ]

    return layout

def run():
    window = sg.Window(
        title="SampleApp",
        layout=Layout(),
    )

    while True:
        event, values = window.read(timeout=10)

        if event is sg.WIN_CLOSED:
            break

        elif event=="create_csv" and len(values['inputsheet'])!=0:
            persons = Persons(inputsheet_name=values['inputsheet'][0]) # Personsインスタンス生成
            persons.output_csv()    # CSV形式で書き出し
            sg.popup("csvファイルへの書き出しが完了しました", title="")

        elif event=="import_csv" and len(values['select_csv'])!=0:
            csvfile_name = values['select_csv'][0]
            print(csvfile_name)
            importCSVTable(csvfile_name)
            sg.popup("DBへのインポートが完了しました", title="")

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

</div></details>

## まとめ
「直接ではなくExcelファイルなどの入力でDBの中身を変更したい」という要望に対して
pythonを用いて簡単なGUIアプリケーションを作成しました

* `PySimpleGUI`
pythonではGUI作成ライブラリとして`tkinter`が標準インストールされているのですが、今回使用した`PySimpleGUI`はそれと比べ直感的にコーディングでき、[公式リファレンス](https://pysimplegui.readthedocs.io/en/latest/)が非常にわかりやすいのでとても使いやすかったです

* `pyinstaller`
コマンドのみで簡単にexeファイルを生成でき、python実行環境が無いPCでもアプリケーションを動作させられるのも良かったです
パッケージングライブラリは`pyinstaller`の他にもいくつかあるようですが、`pyinstaller`はその中でも非常に簡単に利用できるので採用しました
ただ、生成したexeのファイルサイズが大きめだったり、起動時間が長くなったりするなどの欠点はあります

* `openpyxl`
こちらに関しては[別記事](https://qiita.com/mk1600/items/ce8bdca007e969de29ff)でも解説を行っています