Kivy + Matplotlib で複数のファイル処理＋GUI上にGraphを描写

ネット上の情報をいろいろ参考にさせて頂きながら、GUIの勉強を兼ねて、入出力＋処理＋グラフを表示できるGUIプログラムに取り組んでみました。

##実行環境
Windows10
Python 3.7.7
Kivy 1.11.1
GUIは、近年人気らしいということで、Kivyに挑戦。

##作ったGUI
上段がファイル入出力と処理を行うためのボタンなど。ボタンの下がグラフ表示の領域です。グラフは、左のプルダウンで、選んで表示します。

![AnalysisGUI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489188/5af554ab-845b-cc16-e1d4-f69e3ec75d64.jpeg)

##作ったコード
以下コード詳細です。kivy側のコードはGUIに関連する内容（レイアウトや、ファイル選択、ボタンを押したときなどに呼び出す関数）、Python側のコードは実際の処理を記述しています。
まずはPython

```python:main.py
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import openpyxl
import os

from kivy.uix.boxlayout import BoxLayout
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.floatlayout import FloatLayout
from kivy.properties import ObjectProperty 
from kivy.uix.popup import Popup
from kivy.garden.matplotlib.backend_kivyagg import FigureCanvasKivyAgg

class LoadDialog(FloatLayout):
    load = ObjectProperty(None)
    cancel = ObjectProperty(None)
    Analysis = ObjectProperty(None)        
    current_dir = os.path.dirname(os.path.abspath(__file__))
 
class SaveDialog(FloatLayout):
    save = ObjectProperty(None)
    text_input = ObjectProperty(None)
    cancel = ObjectProperty(None)
    current_dir = os.path.dirname(os.path.abspath(__file__))  

class MainBoard(BoxLayout):
    file_name = ObjectProperty(None)
    info = ObjectProperty(None)
    bar_graph = ObjectProperty(None)
    lbl4spinner = ObjectProperty([])
    
    def __init__(self, **kwargs):
        super(MainBoard, self).__init__(**kwargs)
        self.master_flg = 0
        self.lbl=[]
        self.Data=[]
        
    def dismiss_popup(self):
        self._popup.dismiss()

    def show_load(self):
        content = LoadDialog(load=self.load, cancel=self.dismiss_popup, Analysis=self.Analysis)
        self._popup = Popup(title="Load file", content=content,size_hint=(0.9, 0.9))
        self._popup.open()

    def show_save(self):
        if self.master_flg==0:
            self.info.text = 'Load masterfile first, please'
        else:
            content = SaveDialog(save=self.save, cancel=self.dismiss_popup)
            self._popup = Popup(title="Save file", content=content, size_hint=(0.9, 0.9))
            self._popup.open()

    def load(self, path, filename, chkflg):
        try:
            if chkflg==1:
                self.file_name.text=str(filename.pop(0))
            else:
                self.file_name.text=str(filename[0])
        except Exception as e:
            self.info.text=str(type(e))+' : '+str(e) 
            
        Matrix=pd.DataFrame([])
        for i in range(len(filename)):
            NewData = pd.read_excel(filename[i])
            tmp_Data=NewData.iloc[0:,1:]
            tmp_Data.index=NewData.iloc[:,0]
            Matrix=pd.concat([Matrix,tmp_Data])
        
        self.lbl=list(Matrix.index)
        self.Data = Matrix
        self.dismiss_popup()
    
    def save(self, path, dirname):
        with pd.ExcelWriter(os.path.join(path, dirname)) as writer:
            self.Data.to_excel(writer)  
        self.dismiss_popup()         
    
    def Analysis(self):
        try:
            self.Data=self.Data*2
            self.lbl4spinner=map(str,self.lbl)#spinner に渡すリスト
            self.master_flg=1
            self.info.text = 'Analysis completed!'
        except Exception as e:
            self.info.text=str(type(e))+' : '+str(e) 
    
    def on_spinner_select(self,text):
        row_no=self.lbl.index(text)
        Forgraph=self.Data.iloc[row_no,:]
        plt.clf()
        bar_plt=plt.bar(Forgraph.index, Forgraph)
        self.ids.bar_graph.clear_widgets()
        self.ids.bar_graph.add_widget(FigureCanvasKivyAgg(plt.gcf()))
        
class MainApp(App):
    title = '解析'
    def build(self):
        return MainBoard()
        
if __name__ == '__main__':
    MainApp().run()
```
続いてKivy

```python:main.kv
<MainBoard>:
    info: info
    file_name: file_name
    bar_graph: bar_graph
    
    
    BoxLayout:
        orientation: 'vertical'
        pos: root.pos
        size: root.size
        
        canvas.before:
            Color: 
                rgba: 0.9, 0.9, 0.9, 1
            Rectangle:
                pos: self.pos
                size: self.size
                
        Label:
            id : info
            text: 'load file first, please'
            size_hint_y: 0.1
            canvas.before:
                Color: 
                    rgba: 0, 0, 0, 1
                Rectangle:
                    pos: self.pos
                    size: self.size
            
        BoxLayout:
            orientation: 'horizontal'
            size_hint_y: 0.1
            
            TextInput:
                id: file_name
                font_size: 12
                size_hint_x: 0.7
                           
        BoxLayout:
            orientation: 'horizontal'
            size_hint_y: 0.1
            
            Button:
                text: 'Load & Analysis'
                size_hint_x: 0.2
                on_press: root.show_load()
                
            Button:
                text: 'Save data'
                size_hint_x: 0.2
                on_press: root.show_save()
                 
            Button:
                text: "Exit"
                id: btnExit
                size_hint_x: 0.2               
                on_press: app.root_window.close()  

        BoxLayout:
            orientation: 'horizontal'
            size_hint_y: 0.7
   
            Spinner:
                text: "No."
                values: root.lbl4spinner
                size_hint: 0.1, 0.1
                pos_hint: {'center_x': .5, 'center_y': .5}
                on_text: root.on_spinner_select(self.text)
                
            BoxLayout:
                id: bar_graph
                
<LoadDialog>:

    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: "vertical"
            
        FileChooser:
            id: filechooser
            path:root.current_dir
            multiselect: True
            filters: ['*.xlsx']
            FileChooserIconLayout

        BoxLayout:
            size_hint_y: None
            height: 30
 
            Button:
                text: "Load selected file(s)"
                on_press: root.load(filechooser.path, filechooser.selection,0)
                on_release: root.Analysis()
                
            Button:
                text: "Load all files"
                on_press: root.load(filechooser.path,filechooser.files,1)
                on_release: root.Analysis()
            
            Button:
                text: "Cancel"
                on_release: root.cancel()
                
<SaveDialog>:
   
    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: "vertical"
        
        BoxLayout:
            size_hint_y: 0.1
            Button:
                text: 'ListView'
                on_release: filechooser.view_mode = 'list'
            Button:
                text: 'IconView'
                on_release: filechooser.view_mode = 'icon'
                
        FileChooser:
            id: filechooser
            path:root.current_dir
            on_selection: dir_name.text = self.selection and self.selection[0] or ''
            filters: ['*.xls','*.xlsx']
            FileChooserListLayout

        TextInput:
            id: dir_name
            size_hint_y: None
            height: 30
            multiline: False

        BoxLayout:
            size_hint_y: None
            height: 30

            Button:
                text: "Save"
                on_release: root.save(filechooser.path, dir_name.text)         
                            
            Button:
                text: "Cancel"
                on_release: root.cancel()
```

##解説
ベースのプログラムは、KivyのFilechooserを使ってます。Filechooser自体には公式ドキュメントや、多数の先人の解説があるので省略。私が躓いたりしたところだけ説明していきます。

フローの都合上、プログラムが上から流れるわけではなく、KivyコードとPyコードを行ったり来たりするので、フローに沿って、説明します。

####１　ファイル選択
- プログラム起動後、GUI上のLoadボタンを押すと、Kivy側から、Show‗Load関数が呼ばれます。

```python:main.kv
            Button:
                text: 'Load & Analysis'
                size_hint_x: 0.2
                on_press: root.show_load()
```
- Show load関数は、LoadDialogを別ウインドウで立ち上げます。その際、MainBoardクラス内の指定した関数（今回は、Load関数、Cancell関数、Analisys関数）をcontentにて紐づけています。（最初、なんでこんな面倒なことを、と思いましたが、LoadDialogは別クラスなんで、紐づけした関数を直接呼べないんですね。）

```python:main.py
    def show_load(self):
        content = LoadDialog(load=self.load, cancel=self.dismiss_popup, Analysis=self.Analysis)
        self._popup = Popup(title="Load file", content=content,size_hint=(0.9, 0.9))
        self._popup.open()
```
- KivyのLoadDialogのコードです。FileChooserで、ファイルを選択します。今回は、元データがxlsx形式で保存されているものとしました。不要な形式のファイルを表示しない様、フィルターをかけています。また、複数ファイルを選択できるよう、multiselectはTrueで指定。idには、filechooserという変数で、選択したファイルのアドレスが保持されます。  
  
```python:main.kv           
        FileChooser:
            id: filechooser
            path:root.current_dir
            multiselect: True
            filters: ['*.xlsx']
            FileChooserIconLayout
```

- 上記コードに続いて3つボタンを配置。①クリックで読込ファイルを選択指定する場合 (filechooser.selection) ②現在のディレクトリのすべてのファイルを読み込む場合 (filechooser.files) ③キャンセルボタンです。①②のボタンを押すと、まずload関数が呼ばれます。load関数の引数の最後の数字は、識別番号です（同じ関数を呼び出すので）。
 
```python:main.kv   
        BoxLayout:
            size_hint_y: None
            height: 30
 
            Button:
                text: "Load selected file(s)"
                on_press: root.load(filechooser.path, filechooser.selection,0)
                on_release: root.Analysis()
                
            Button:
                text: "Load all files"
                on_press: root.load(filechooser.path,filechooser.files,1)
                on_release: root.Analysis()
            
            Button:
                text: "Cancel"
                on_release: root.cancel()
```
####2　ファイル読み込み
- Python側のLoad関数にて、Filechooserから渡されたファイルのアドレス（List形式で引き渡される）を用いて、xlsからデータを吸い出します。以下の部分は、GUI上にアドレスを表示するコードです。
 - この時、ディレクトリのファイルを一括で読んだ場合は、リストの最初はディレクトリのアドレス（ファイルが紐づいていない）なので、注意必要です。このために、リストから最初の項を抜き出しています。
 - ```except Ececpiion as e:```以降の部分は、エラーが起きたときに内容を表示するものです。

```python:main.pv   
    def load(self, path, filename, chkflg):
        try:
            if chkflg==1:
                self.file_name.text=str(filename.pop(0))
            else:
                self.file_name.text=str(filename[0])
        except Exception as e:
            self.info.text=str(type(e))+' : '+str(e) 
```
- 続いてデータ読込です。今回のxlsxは、A列にパラメータ名、1行目にデータNo.を入れる形式としています。
 - for文にて、選択したすべての関数のデータを繰返し読み込んで、最終的に```self.lbl```, ```self.Data```という変数（行列）に格納します。
 - 最後に```dismiss_popup()```で```LoadDialog```を閉じたらLoad関数は終了です。

```python:main.pv   
        Matrix=pd.DataFrame([])
        for i in range(len(filename)):
            NewData = pd.read_excel(filename[i])
            tmp_Data=NewData.iloc[0:,1:]
            tmp_Data.index=NewData.iloc[:,0]
            Matrix=pd.concat([Matrix,tmp_Data])
        
        self.lbl=list(Matrix.index)
        self.Data = Matrix
        self.dismiss_popup()
```

#### 3．解析
- kivy側の LoadDialogでは、Load関数に続いて、```on_release: root.Analysis()```によって、Analysis関数が呼び出されます。
 - ここで実際に行いたいデータ処理を記述します。今回は、単純にデータを2倍にします。
 - 次の```self.lbl4spinner=map(str,self.lbl)```は、GUI上のグラフ選択のプルダウンに渡すリストです。
 - ```self.master_flg```は、データを読み込んだかどうかのフラグです。Save可否の判定に使います。

```python:main.pv   
    def Analysis(self):
        try:
            self.Data=self.Data*2
            self.lbl4spinner=map(str,self.lbl)#spinner に渡すリスト
            self.master_flg=1
            self.info.text = 'Analysis completed!'
        except Exception as e:
            self.info.text=str(type(e))+' : '+str(e) 
```
#### 4．グラフ表示
- まずkivy側の説明です。Spinnerを使って、プルダウンメニューを表示します。
 - ``` text: "No."```は、プルダウンする前に表示する文字を指定します。
 - ```values: root.lbl4spinner```は、解析の項で取得したリストをプルダウン時に表示します。
 - ```on_text: root.on_spinner_select(self.text)```で、プルダウンで選択した項目を引数(```self.text```)として、python側のon_spinner_select関数を呼び出します。

```python:main.kv   
            Spinner:
                text: "No."
                values: root.lbl4spinner
                size_hint: 0.1, 0.1
                pos_hint: {'center_x': .5, 'center_y': .5}
                on_text: root.on_spinner_select(self.text)
                
            BoxLayout:
                id: bar_graph
```
- 呼び出された on_spinner_select関数は、引数の情報を基に、グラフを作成します。
 - ```self.lbl.index(text)```でプルダウンで選択された情報と一致する順番（行数）を返します。
 - ```Forgraph=self.Data.iloc[row_no,:]```で指定した行数のデータを読み込みます
 - ```bar_plt=plt.bar(Forgraph.index, Forgraph)```でグラフ作成。今回は棒グラフを作成しました。
 - ```self.ids.bar_graph.add_widget(FigureCanvasKivyAgg(plt.gcf()))```でKivyの```BoxLayout``` (```id: bar_graph```)にグラフ描写します。
 - ```plt.clf()```,```self.ids.bar_graph.clear_widgets()```は、プルダウンの選択を切り替えて再表示するときに前のグラフを消去するためのものです。

```python:main.py
    def on_spinner_select(self,text):
        row_no=self.lbl.index(text)
        Forgraph=self.Data.iloc[row_no,:]
        plt.clf()
        bar_plt=plt.bar(Forgraph.index, Forgraph)
        self.ids.bar_graph.clear_widgets()
        self.ids.bar_graph.add_widget(FigureCanvasKivyAgg(plt.gcf()))
```   
#### 5．Save
- Saveのところは、Loadと同じような処理をしているので省略。

#### 6．その他
- 前準備として、KivyとPython間でやり取りをするために、MainBoardクラスの最初にObjectの定義が必要です。合わせて、Kivy側ではObjectとidの紐付けが必要になります。

```python:main.py
class MainBoard(BoxLayout):
    file_name = ObjectProperty(None)
    info = ObjectProperty(None)
    bar_graph = ObjectProperty(None)
    lbl4spinner = ObjectProperty([])
```
```python:main.kv
<MainBoard>:
    info: info
    file_name: file_name
    bar_graph: bar_graph
```
- あと、関数間で利用する変数は、重複を避けるために、初期化の段階で宣言、まとめています。

```python:main.py
    def __init__(self, **kwargs):
        super(MainBoard, self).__init__(**kwargs)
        self.master_flg = 0
        self.lbl=[]
        self.Data=[]
```
##お世話になったサイト
以下サイトのKivy関連の情報を特に参考にさせて頂きました。
[kivy公式ドキュメント：Filechooser](https://kivy.org/doc/stable/api-kivy.uix.filechooser.html)
やはり一番は公式ドキュメントですね。
[フリーランスに憧れて](https://free-free.site/)
最初のとっかかりとして参考になりました。今回のGUIのレイアウトもここを参考にさせて頂いています。
[naritoブログ](https://torina.top/)
KivyとPythonの関係を理解するうえで、非常に参考になりました。
[stackoverflow : Python - Kivy framework - Spinner values list](https://stackoverflow.com/questions/13445362/python-kivy-framework-spinner-values-list)
Spinnerへのリスト表示、この投稿がなければ行き詰ってたと思います。

##今後改善たいこと
グラフ描写にidsを使ってますが、これをObjectPropatyで記述したい。