PySimpleGUIでデータViewerを作る

# はじめに
実験データや測定データが入ったファイルがたくさんあって，そのデータを理解するためには何らかの後処理や可視化が必要，という状況は割とよくあると思います．

例えば材料の性質を知るために行われる圧縮試験という試験において，その結果として荷重と変形量が記録されたCSVファイルが得られる場合を考えます．生のデータを理解するのは難しく，応力やひずみの計算や応力ひずみ曲線の作成が必要になります．しかし実験の結果をちょっと確認するためだけに毎回Excelを開いて計算するのも面倒です．

そこでもしWindowsのFile Explorerのプレビュー表示のようにワンクリックでデータを確認できたら便利なのではないでしょうか．そしてPySimpleGUIというPythonライブラリを使うとそれが比較的簡単に実現できます．

この記事では画像のようなシンプルなデータViewerを作ります．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320225/545753a9-2d3d-bab9-0746-d448c4bcfecb.png)

データが入っているフォルダを選ぶと，その中身が左のツリーに表示されます．そしてファイル名をクリックすると中身のデータのプロットを見ることができます．
ツリーは入れ子になっているフォルダにも対応しており，また複数のファイルを選択することで複数のデータを重ねてプロットすることも可能です．


# 環境・ライブラリ等
- Windows10
- Python 3.8.2
- PySimpleGUI
- Matplotlib
- Numpy

# PySimpleGUIについて
PySimpleGUIはPythonでGUIアプリケーションを作成できるライブラリです．PythonのGUIライブラリには様々なものがあり，私もいくつか利用したことがあります．それらのライブラリと比較したときのPySimpleGUIの特徴は，素早く簡単にGUIアプリケーションを作れるというところだと思います．またPySimpleGUIはドキュメントやサンプルコードなどのリソースが豊富で，しばらくドキュメントを読めばすぐにプログラムを書き始められるようになると思います．

PySimpleGUIの基本的な部分については公式サイトにわかりやすい説明がたくさんあるので省きます．
https://pysimplegui.readthedocs.io/en/latest/

GitHubのREADMEもわかりやすくておすすめです．
https://github.com/PySimpleGUI/PySimpleGUI/blob/master/readme.md

READMEの日本語版もあるみたいです．
https://github.com/PySimpleGUI/PySimpleGUI/blob/master/readme.ja.md

大量（200個くらいあるらしい）のデモプログラムも必見です．OpenCVやMatplotlibなどの他ライブラリと連携しているプログラムもあります．
https://github.com/PySimpleGUI/PySimpleGUI/tree/master/DemoPrograms

# プログラム
プログラム全体です．

```python
import os
import numpy as np
import PySimpleGUI as sg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

folder_icon = b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAABnUlEQVQ4y8WSv2rUQRSFv7vZgJFFsQg2EkWb4AvEJ8hqKVilSmFn3iNvIAp21oIW9haihBRKiqwElMVsIJjNrprsOr/5dyzml3UhEQIWHhjmcpn7zblw4B9lJ8Xag9mlmQb3AJzX3tOX8Tngzg349q7t5xcfzpKGhOFHnjx+9qLTzW8wsmFTL2Gzk7Y2O/k9kCbtwUZbV+Zvo8Md3PALrjoiqsKSR9ljpAJpwOsNtlfXfRvoNU8Arr/NsVo0ry5z4dZN5hoGqEzYDChBOoKwS/vSq0XW3y5NAI/uN1cvLqzQur4MCpBGEEd1PQDfQ74HYR+LfeQOAOYAmgAmbly+dgfid5CHPIKqC74L8RDyGPIYy7+QQjFWa7ICsQ8SpB/IfcJSDVMAJUwJkYDMNOEPIBxA/gnuMyYPijXAI3lMse7FGnIKsIuqrxgRSeXOoYZUCI8pIKW/OHA7kD2YYcpAKgM5ABXk4qSsdJaDOMCsgTIYAlL5TQFTyUIZDmev0N/bnwqnylEBQS45UKnHx/lUlFvA3fo+jwR8ALb47/oNma38cuqiJ9AAAAAASUVORK5CYII='
file_icon = b'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsSAAALEgHS3X78AAABU0lEQVQ4y52TzStEURiHn/ecc6XG54JSdlMkNhYWsiILS0lsJaUsLW2Mv8CfIDtr2VtbY4GUEvmIZnKbZsY977Uwt2HcyW1+dTZvt6fn9557BGB+aaNQKBR2ifkbgWR+cX13ubO1svz++niVTA1ArDHDg91UahHFsMxbKWycYsjze4muTsP64vT43v7hSf/A0FgdjQPQWAmco68nB+T+SFSqNUQgcIbN1bn8Z3RwvL22MAvcu8TACFgrpMVZ4aUYcn77BMDkxGgemAGOHIBXxRjBWZMKoCPA2h6qEUSRR2MF6GxUUMUaIUgBCNTnAcm3H2G5YQfgvccYIXAtDH7FoKq/AaqKlbrBj2trFVXfBPAea4SOIIsBeN9kkCwxsNkAqRWy7+B7Z00G3xVc2wZeMSI4S7sVYkSk5Z/4PyBWROqvox3A28PN2cjUwinQC9QyckKALxj4kv2auK0xAAAAAElFTkSuQmCC'

# GUIがぼやける現象を防ぐための関数
def make_dpi_aware():
  import ctypes
  import platform
  if int(platform.release()) >= 8:
    ctypes.windll.shcore.SetProcessDpiAwareness(True)


# 描画用の関数
def draw_figure(canvas, figure):
    figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
    figure_canvas_agg.draw()
    figure_canvas_agg.get_tk_widget().pack(side="top", fill="both", expand=1)
    return figure_canvas_agg

# ツリーデータ作成用関数
def get_tree_data(parent, dirname):
    treedata = sg.TreeData()
    
    # https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Tree_Element.py#L26
    def add_files_in_folder(parent, dirname):
        
        files = os.listdir(dirname)
        for f in files:
            fullname = os.path.join(dirname, f)
            if os.path.isdir(fullname):
                treedata.Insert(parent, fullname, f, values=[], icon=folder_icon)
                add_files_in_folder(fullname, fullname)
            else:

                treedata.Insert(parent, fullname, f, values=[
                                os.stat(fullname).st_size], icon=file_icon)
    
    add_files_in_folder(parent, dirname)
    
    return treedata

if __name__ == "__main__":
    make_dpi_aware()
    
    # ツリーのデータ作成
    treedata = get_tree_data("", os.getcwd())

    # メニュー
    menu_def = [["File", ["Open Folder"]]]
    
    # レイアウト作成
    layout = [[sg.Menu(menu_def)],
            [sg.Tree(data=treedata,
                headings=[],
                auto_size_columns=True,
                num_rows=24,
                col0_width=20,
                key="-TREE-",
                show_expanded=False,
                enable_events=True), sg.Canvas(key="-CANVAS-")]]

    window = sg.Window("CSV Viewer", layout, finalize=True, element_justification="center", font="Monospace 8", resizable=False)

    # 埋め込むfigを作成
    fig = plt.figure(figsize=(7, 5))
    ax = fig.add_subplot(111)

    # fig_agg作成
    fig_agg = draw_figure(window["-CANVAS-"].TKCanvas, fig)

    # イベントループ
    while True:
        event, values = window.read()
        # print(event, values)

        if event is None:
            break
        
        elif event == "-TREE-":
            # 前回描いたグラフを消す
            ax.cla()
            fig_agg.draw()
            
            # 選択されているファイルをそれぞれ開きプロットする
            try:
                for fname in values["-TREE-"]:
                    if not os.path.isdir(fname):
                        data = np.genfromtxt(fname, delimiter=",")
                        ax.plot(data)
            except Exception as e:
                sg.Print(e)
                   
            # キャンバス更新
            fig_agg.draw()
            
        elif event == "Open Folder":
            # 表示するフォルダを変更する
            starting_path = sg.popup_get_folder("Folder to display")
            treedata = get_tree_data("", starting_path)
            window["-TREE-"].update(values=treedata)

    # ウィンドウを閉じる．
    window.close()
```

PySimpleGUI内にMatplotlibのプロットを埋め込む方法については，私が以前書いた記事で説明しています．
[PySimpleGUIにMatplotlibを埋め込みたい](https://qiita.com/bear_montblanc/items/cce4e8c58dfa236200f6)

ファイルツリーの作り方については，PySimpleGUIのGitHubにあるデモプログラムを参考にしています．
https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Tree_Element.py

ファイルツリーは[Tree Element](https://pysimplegui.readthedocs.io/en/latest/call%20reference/#tree-element)というエレメントを利用しています．上のプログラムの`get_tree_data`関数はファイルツリーのデータを持った[Tree Data](https://pysimplegui.readthedocs.io/en/latest/call%20reference/#treedata-for-tree-element)を返します．その後このTree DataをTree Elementに渡します．

`get_tree_data`関数の内側の`all_files_in_folder`関数は少し複雑かもしれません．指定されたフォルダ以下にある全てのフォルダ・ファイルを再帰的にTree Dataに登録する関数です．
表示するフォルダ`dirname`の内側にあるファイルとフォルダを順番に見ていき，もしファイルだったらファイルのアイコンを付けてツリーに挿入，もしフォルダであればフォルダのアイコンを付けてツリーに挿入し，さらにそのフォルダに対して`all_files_in_folder`を呼び出して再帰しています．

```python
    def add_files_in_folder(parent, dirname):

        files = os.listdir(dirname)
        for f in files:
            fullname = os.path.join(dirname, f)
            if os.path.isdir(fullname):
                treedata.Insert(parent, fullname, f, values=[], icon=folder_icon)
                add_files_in_folder(fullname, fullname)
            else:

                treedata.Insert(parent, fullname, f, values=[
                                os.stat(fullname).st_size], icon=file_icon)


```

プログラムの最初にある`folder_icon`と`file_icon`は，ファイルとフォルダのアイコンに使う画像をBase64にエンコードしたものです．Base64については次の記事を参考にしてください．`icon`にはファイル名も指定できます．
[base64ってなんぞ？？理解のために実装してみた](https://qiita.com/PlanetMeron/items/2905e2d0aa7fe46a36d4)

Tree Elementを作る際に`enable_event=True`とすることで，ツリー内のデータ（つまりファイル名orフォルダ名）をクリックしたときにイベント（`"-TREE-"`）が発生するようになっています．

```python
       elif event == "-TREE-":
            # 前回描いたグラフを消す
            ax.cla()
            fig_agg.draw()

            # 選択されているファイルをそれぞれ開きプロットする
            try:
                for fname in values["-TREE-"]:
                    if not os.path.isdir(fname):
                        data = np.genfromtxt(fname, delimiter=",")
                        ax.plot(data)
            except Exception as e:
                sg.Print(e)

            # キャンバス更新
            fig_agg.draw()
```

# おわりに

このプログラム自体は大して役に立ちませんが，用途に応じてカスタマイズすることで便利なツールになると思います．例えばデータを読み込んだ後に何かしらの処理を加えてみたり，メニューからデータを表示する方法を変えられるようにしてみたり．さらにファイルツリーは必ずしも「ファイル」ツリーである必要はなく，例えばExcelファイル内のシートをツリーに表示させるみたいなこともできます．

PySimpleGUIはとてもシンプルなのに強力で，しかもプログラムを書いてて楽しいという最高のライブラリなのでぜひたくさんの人に使ってもらいたいです．