[Python]OpenPyXLを使用してtxtをExcelファイルに変換してみた #2

**本記事は
[[Python]OpenPyXLを使用してtxtをExcelファイルに変換してみた #1](https://qiita.com/OshirukoRI/items/0b68ea5558a65a637770)
の続きになります。**

# はじめに
　前回はOpenPyXLを使用してtxtファイルをExcelファイルに変換（＆ついでにExcel内での関数を使用）するプログラムを作成しました。
　一回ぽっきりの使用なら前回のプログラムでも問題ないのですが、
**・平均以外のちょっと煩雑な計算もしてほしい**
**・解析対象のファイル指定を簡単にしたい**
と欲が出てきます。~~実験屋としては毎回の解析を楽にするためにツールを使いたいのです...~~

そんなわけで今回は前回のコードを改良して
**・コードの短縮化や3項移動平均の列の追加**
**・TKinterを使用したデスクトップアプリ化**
を行いたいと思います。

# 使用したものとバージョン

　Windows 10 Home: 21H2
　VS Code: 1.71.0
　Python: 3.10.1, pip: 22.2.2
　　ライブラリ
　OpenPyXL: 3.0.9
　tkinter: 8.6

# 前回のコードの修正と3項移動平均の追加
## コードの修正
　前回のコードを短くなるよう修正していきます。
開始電圧など情報を書き込む2行目は、前回のコードで冗長な~~無駄にスペースを食ってる~~部分でした。そこで2行目の奇数列は文字、偶数列は数値(`float`か`int`)とわかっていることを考慮して、`if文`を使って簡略化します。偶奇判定は典型的な**2で割ったら余りが出るかどうか**で行っています。
~~~Python: 2行目
"""
# 修正前
wsMain.cell(row=2, column=1).value = lines[1].split("\t")[0]
wsMain.cell(row=2, column=2).value = float(lines[1].split("\t")[1])
wsMain.cell(row=2, column=3).value = lines[1].split("\t")[2]
wsMain.cell(row=2, column=4).value = float(lines[1].split("\t")[3])
wsMain.cell(row=2, column=5).value = lines[1].split("\t")[4]
wsMain.cell(row=2, column=6).value = float(lines[1].split("\t")[5])
wsMain.cell(row=2, column=7).value = lines[1].split("\t")[6]
wsMain.cell(row=2, column=8).value = int(lines[1].split("\t")[7])
"""
# 修正後
for i in range(1, 9):
    if i % 2 == 0:      # 列の偶奇判定
        wsMain.cell(row=2, column=i).value = float(lines[1].split("\t")[i-1])
    else:
        wsMain.cell(row=2, column=i).value = lines[1].split("\t")[i-1]
~~~
また、これに伴って、データ書き込みの準備の部分を
~~~Python:
"""
# 修正前
scan = wsMain.cell(row=2, column=8).value
"""

# 修正後
# scanがfloat値なのでintにキャスト
scan = int(wsMain.cell(row=2, column=8).value)
~~~
と微修正しています。

## 3項移動平均の列を作成
　次に3項移動平均を作っていきます。そもそも移動平均とは
>移動平均は、時系列データ（より一般的には時系列に限らず系列データ）を平滑化する手法である。[移動平均(Wikipedia)](https://ja.wikipedia.org/wiki/%E7%A7%BB%E5%8B%95%E5%B9%B3%E5%9D%87)より

だそうです。
僕が先輩から習ったときは隣接平均って呼んでたんですがどっちなんですかね...
とにかくこの移動平均を求める列を作成していきます。
　以下が目標のExcelです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852776/09439e8d-2ace-9ce6-1302-4d59aede51d4.png)
x軸と各ヘッダーは、平均の列を作った時と同じです。
~~~Python: 3項隣接平均
# Averageの3項隣接平均を出す"隣接平均"列を作成
ma_n = 3    # moving average of n
wsMain.cell(row=4, column= scan + 3).value = "x"                            # ヘッダー
wsMain.cell(row=4, column= scan + 4).value = str(ma_n) + "項隣接平均"       # ヘッダー
for i_x in range(0, end + 1):   # x軸の計算は同じ
    wsMain.cell(row=i_x + 5, column= scan + 3).value = float(start + i_x * step)
for i_ma in range(0, end):      # 3項隣接平均は(end - 1)個までしか取れない
    wsMain.cell(row=i_ma + 6, column= scan + 4).value = \
        str("=AVERAGE(" + wsMain.cell(row=i_ma + 5, column= scan + 2).coordinate\
            + ":" + wsMain.cell(row= i_ma + 7, column= scan + 2).coordinate + ")")
~~~
特筆すべきは3項隣接平均の関数を書き込むところです。**i_ma行目は前後1行目の平均である**ため`.coordinate`でとってくるアドレスは±1のオフセットがかかっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852776/ffb397bc-2813-19ed-5de0-f949fe9394e2.png)

以上で前回のコードの修正のすべてを終わります。
# Tkinterを使ってアプリ化
　では、TKinterを使ってアプリにしていきます。
## Tkinterでアプリの骨組みを作る
　アプリの機能を実装する前に外側を作っていきます。今回は
**・指定したファイルパスを表示するテキストボックス
・ファイルを変換する関数を動かすボタン
・現在の状況を確認するステータスバー**
の3つの機能を持たせようと思います。
　以下が骨組みになります。
~~~Python: TKinterの骨組み
# Tkinterを使用したGUIの大枠を設定
root = tk.Tk()
root.geometry("300x400")    # アプリの大きさ（ピクセル）
root.title("Text to Excel") # ウィンドウに表示する名前
# ステータスバーを上部に作成
statusBar = tk.Label(root, bd=2, relief = tk.SUNKEN, anchor = tk.W)
statusBar.pack(side = tk.TOP, fill = tk.X)
# テキストボックスやボタンを配置するメインフレームを設定
mainFlame = ttk.LabelFrame(root, text="Raw data", padding=10)
mainFlame.pack(padx=20)
# テキストボックスの配置
input_box = tk.Entry(mainFlame, width = 20)
input_box.grid(row=0, column=0)
# Setボタンの追加
set_button = tk.Button(mainFlame, text="Set")
set_button.grid(row=0, column=1)
# 実行ボタンを追加
run_button = tk.Button(mainFlame, text="実行")
run_button.grid(row=1, column=0)

root.mainloop()
~~~
 TKinterでは
・機能ごとの枠を作る
・作った枠をアプリ内に配置する
としてGUIを構成しています。

ちなみに配置の仕方には`.grid`や`.pack`があり前者では行列、あるいはセルのように、後者では上から（指定のない限り）埋めるように表示場所を指定できます。パディングや上下左右の大きさなどの細かい指定は各引数から設定できます。

ここまでのコードを動かすとこのようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2852776/8754085a-b09f-dc0d-5a1a-e274060cb4fb.png)
このままではボタンを押してもなにも起きないので、ボタンに各種機能を振るために関数を作っていきます。
## 各種関数の作成
### ファイルのパスを設定する関数の作成
　ファイルのパスを指定するにはtkinter内の`filedialog`を使用します。以下が作成した関数のコードになります。
~~~Python: setFunc
def setFunc():
    # 既存のパスを削除
    input_box.delete(0, tk.END)
    # ファイルダイアログメニューの設定
    # 選択可能なファイルを絞る
    typ = [("Raw Data","*.txt")]
    inputFile_path = filedialog.askopenfilename(filetypes = typ)
    # パスを挿入
    input_box.insert(tk.END, inputFile_path)
~~~
input_box内はsetFuncが呼び出されるたびに新しいパスのみを表示したいので
`.delete`を使用して、次のファイルを選択する前に削除します。

`.askopenfilename`の引数`filetypes`では表示するファイルを絞る（拡張子を指定）することができます。今回は**実験の生データ**であり**txt**の拡張子であることを強調するため
`typ = [("Raw Data","*.txt")]`
としています。何も指定しなければフォルダ内のすべてのデータから選ぶことができます。

### txtを変換するコードを関数化
次に今回修正したコードを関数化していきます。関数名は`TransFunc`としています。
~~~Python: TransFunc
def transFunc():
    #   カレントディレクトリ
    currentDir = str(os.path.dirname(input_box.get()) + "\\")
    #   ファイル名の指定
    fileName = os.path.splitext(os.path.basename(input_box.get()))[0]
    extention2 = ".xlsx"
    file = open(input_box.get(), "r")
    # すべての行をlinesとして読み込む
    lines = file.readlines()
    file.close()

    #   新規エクセルファイルの作成
    wb = px.Workbook()
    wb.save(currentDir + fileName + extention2)
"""
変更がないため中略
"""
    #   新規エクセルファイルの保存
    wb.save(currentDir + fileName + extention2)
    wb.close()
    print("start /V = " + str(start) + "\t" + "stop /V = " + str(stop) + "\t" + "step /V = " + str(step))
    print("scan = " + str(scan) + "\t" + "end = "  + str(end))
    os.startfile(currentDir + fileName + extention2, operation="open")
~~~

関数化するうえで最大の変更点はファイルの指定方法です。
詳しく説明すると
・ゲッター`.get()`を使ってinput_box内のファイルのパスを取得
・`os.path.dirname()`を使用してパスのうちディレクトリの名前だけを取得
・`os.path.basename()`を使用して拡張子込みのファイルの名前を取得
としています。

また、ファイル名から拡張子を取ったものを`fileName`としたいので
・`.splitext()[]`を使用してファイル名をドット区切りの配列化したのち
　0番目（つまり拡張子のないファイル名）の要素を取得
としています。

さらに、テキストデータを取得する際には
~~~Python
file = open(input_box.get(), "r")
~~~
とすることで、input_box内のファイルパスをリテラルとして直接取得しています。

## アプリに実装する
　以上の関数を実装していきます。
tkinterのボタンモジュールでは引数`command=`を使うことで、関数の呼び出しを行うことができます。したがって`set_button`と`run_button`にそれぞれ
~~~Python
set_button = tk.Button(mainFlame, text="Set", command=setFunc)
~~~
~~~Python
run_button = tk.Button(mainFlame, text="実行", command=transFunc)
~~~
と引数を指定すれば完成です。

# 使用したコード（全体）
今回作成したコードの全体は以下になります。
~~~Python: T2E_withTK.py
import openpyxl as px
import tkinter as tk
from tkinter import filedialog
from tkinter import ttk
import os

def setFunc():
    # 既存のパスを削除
    input_box.delete(0, tk.END)
    # ファイルダイアログメニューの設定
    typ = [("Raw Data","*.txt")]
    inputFile_path = filedialog.askopenfilename(filetypes = typ)     # 選択可能なファイルを絞る
    # パスを挿入
    input_box.insert(tk.END, inputFile_path)


def transFunc():
    #   カレントディレクトリ
    currentDir = str(os.path.dirname(input_box.get()) + "\\")
    #   ファイル名の指定
    fileName = os.path.splitext(os.path.basename(input_box.get()))[0]
    extention2 = ".xlsx"
    file = open(input_box.get(), "r")
    # すべての行をlinesとして読み込む
    lines = file.readlines()
    #   ファイルを閉じる
    file.close()

    #   新規エクセルファイルの作成
    wb = px.Workbook()
    wb.save(currentDir + fileName + extention2)     # ファイル名はtxtの拡張子がxlsxに変わったもの

    #   先頭のシート名を変更
    wsMain = wb.worksheets[0]
    wsMain.title = fileName

    #   現在のワークシートの構成
    #   ファイル名[currentDir + fileName + extention2]
    #   シート名(定義)      エクセルファイルで表示される名前
    #   [wsMain]    [fileName]

    # wsMainをアクティブにする
    wsMain = wb.active

    # セルへの書き込み
    # 1行目
    wsMain.cell(row=1, column=1).value = lines[0].split("\t")[0]
    wsMain.cell(row=1, column=2).value = lines[0].split("\t")[1]
    # 2行目
    for i in range(1, 9):
        if i % 2 == 0:
            wsMain.cell(row=2, column=i).value = float(lines[1].split("\t")[i-1])
        else:
            wsMain.cell(row=2, column=i).value = lines[1].split("\t")[i-1]
    # 3行目
    wsMain.cell(row=3, column=1).value = lines[2].split("\t")[0]
    # 4行目
    for i in range(0, 4):
        wsMain.cell(row=4, column=i+1).value = lines[3].split("\t")[i]
    # データ書き込みの準備
    start = wsMain.cell(row=2, column=2).value
    stop = wsMain.cell(row=2, column=4).value
    step = wsMain.cell(row=2, column=6).value
    scan = int(wsMain.cell(row=2, column=8).value)      # scanがfloat値なのでintにキャスト
    end = int((stop - start) / step)

    # 本データ書き込み(5行目以降)
    # セル情報  (row, col) = (n+1, m+1)
    # 5 <= n <= 5 + end + 1     5行目からスタートしてend行分書き込む
    for n in range(4, 4 + end + 1):
        for m in range(0, scan):
            wsMain.cell(row= n+1, column= m+1).value = float(lines[n].split("\t")[m])

    # 平均を出す"Average"列を作成
    wsMain.cell(row=4, column= scan + 1).value = "x"
    wsMain.cell(row=4, column= scan + 2).value = "Average"
    for i_x in range(0, end + 1):
        wsMain.cell(row=i_x + 5, column= scan + 1).value = float(start + i_x * step)
    for i_avg in range(0, end + 1):
        wsMain.cell(row=i_avg + 5, column= scan + 2).value = \
            str("=AVERAGE(" + wsMain.cell(row=i_avg + 5, column=1).coordinate + ":" \
                            + wsMain.cell(row=i_avg + 5, column=scan).coordinate + ")")

    # Averageの3項隣接平均を出す"隣接平均"列を作成
    ma_n = 3    # moving average of n
    wsMain.cell(row=4, column= scan + 3).value = "x"                          
    wsMain.cell(row=4, column= scan + 4).value = str(ma_n) + "項隣接平均"
    for i_x in range(0, end + 1):   # x軸の計算は同じ
        wsMain.cell(row=i_x + 5, column= scan + 3).value = float(start + i_x * step)
    for i_ma in range(0, end):      # 3項隣接平均は(end - 1)個までしか取れない
        wsMain.cell(row=i_ma + 6, column= scan + 4).value = \
            str("=AVERAGE(" + wsMain.cell(row=i_ma + 5, column= scan + 2).coordinate\
                + ":" + wsMain.cell(row= i_ma + 7, column= scan + 2).coordinate + ")")


    #   新規エクセルファイルの保存
    wb.save(currentDir + fileName + extention2)
    wb.close()
    print("start /V = " + str(start) + "\t" + "stop /V = " + str(stop) + "\t" + "step /V = " + str(step))
    print("scan = " + str(scan) + "\t" + "end = "  + str(end))
    os.startfile(currentDir + fileName + extention2, operation="open")

# Tkinterを使用したGUI
root = tk.Tk()
root.geometry("300x400")
root.title("Text to Excel")

statusBar = tk.Label(root, bd=2, relief = tk.SUNKEN, anchor = tk.W)
statusBar.pack(side = tk.TOP, fill = tk.X)

mainFlame = ttk.LabelFrame(root, text="Raw data", padding=10)
mainFlame.pack(padx=20)
# テキストボックスの配置
input_box = tk.Entry(mainFlame, width = 20)
input_box.grid(row=0, column=0)
# Setボタンの追加
# 押されたときにset_funcを実行
set_button = tk.Button(mainFlame, text="Set", command=setFunc)
set_button.grid(row=0, column=1)
# 押されたときにtransFuncを実行
run_button = tk.Button(mainFlame, text="実行", command=transFunc)
run_button.grid(row=1, column=0)

root.mainloop()
~~~
# 終わりに
いかがでしたでしょうか。もしわかりにくいところがあればコメント等いただけると幸いです。

なお、**TKinter**ではこのほかに**matplotlib**と組み合わせてグラフの表示なんかもできたりします。今回作成したアプリには不自然なスペースがありました。つまり...

次回では、今回作成したアプリを拡張して、解析したエクセルファイルから移動平均の値を取ってきてグラフ化できるようにしたいと思います。

前回 => [[Python]OpenPyXLを使用してtxtをExcelファイルに変換してみた #1](https://qiita.com/OshirukoRI/items/0b68ea5558a65a637770)
次回 => []()

参考
・[Pythonではじめるアプリ開発 ②：Python GUI編 その１](http://pc-chem-basics.blog.jp/archives/24049408.html)