Matplotlib を tkinterの子Windowに描画する

# <font color="Red">tkinter 子Window ← Matplotlib埋め込み</font>
## <font color="HotPink">構想：ボタンクリックでグラフwindowを表示</font>

ボタンがいくつか（Bar、Scatter、Hist、Heatmap、、、など）あり、各種グラフを表示してくれるようなイメージ。この基本として、ボタンクリック　→　グラフ表示、を作成。
子WindowにはMatplotlibの標準アイコン付き

![{E1FDA9E3-6693-4914-A49B-1D4AC9083196}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800268/80c33838-b4af-4e6e-95a3-b760698fdb27.png)

コード全体は末尾（[こちら](#コード全体)）

## <font color="HotPink">必要なもの</font>
以下の要素を併せていく
 - fig ： Matplotlib.pyplotで作成するグラフ描画のオブジェクト
 - MainWindow　：　親window。ボタン配置し、クリックで子Window（グラフwindow）表示
 - childWindow　：　子Window。ここにcanvas設置（FigureCanvasTkAgg）、canvas上にグラフを載せる
 - toolbar　：　NavigateToolbar2Tkでcanvas上にデフォルトのツールバーを表示。カスタマイズも可能


### <font color="Orange">fig部：抜粋</font>
単純にfigを作るだけ。
`fig,axes = plt.subplots(n, m)` して`axes.scatter(x,y)`などで描画し、このfigを使うだけ。
```python
class CreateFigure:
    def __init__(self, n, m, data_x, data_y, labelList):
        self.fig, self.axes = plt.subplots(n, m)
        self.data_x = data_x
        self.data_y = data_y
        self.labelList = labelList
        self.plot()
    def plot(self):
        for ax, x, y, label in zip(self.axes.flatten(),self.data_x, self.data_y, self.labelList):
            ax.scatter(x, y, label=label)
            # 必要に応じてその他装飾
            ax.set_title(f"for {label}")
```

### <font color="Orange">MainWindow部：抜粋</font>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800268/ad8536bc-c37a-4c8c-a147-902da7df64bf.png)

通常の`tkinter`でのWindow作成、ボタン配置。
ボタンには子Window作成の関数を指定する
細かな点では以下がある。
 - Windowクローズ時の処理を忘れないこと
 - childWindowを複数作る場合、childWindows[]配列を作ってそこにchildWindowオブジェクトを入れると管理できる


```python
class MainWindow:
    # 初期化時にframe作成、frame上にボタン配置
    def __init__(self,windowWidth, windowHeight):
        self.root = tk.Tk()
        self.root.geometry(f"{windowWidth}x{windowHeight}")
        self.frame = tk.Frame(self.root, relief="raised", borderwidth=5 , bg="lightgray")
        self.DrawFigButton = tk.Button(self.frame, text="Draw Fig.", command=self.DrawFig)

        self.frame.pack(fill=tk.BOTH, expand=True)
        self.DrawFigButton.pack()

        self.childWindows=[]

    # ここでグラフ描画してfigを生成、addNewWindowを呼び出して子Window生成
    def DrawFig(self):
        data_len = 20
        n = 2
        m = 3
        data_x = np.random.random(size=(data_len, n,m))*10
        data_y = np.random.random(size=(data_len,n,m))*20
        print(data_x)
        labelList = [f"name_{i}" for i in range(n*m)]

        figBase = CreateFigure(n ,m, data_x, data_y, labelList)
        self.childWindow = self.addNewWindow(figBase.fig)
        self.childWindow.transient(self.root) # 常にMainWindowの手前に表示
        # self.childWindow.grab_set()   # 「常に手前に表示」で検索するとこれがヒットするが、子がモーダルになるので却下
        self.childWindows.append(self.childWindow)

    # 子Windowの追加。ChildWindowオブジェクト（後述）のインスタンシングにて
    def addNewWindow(self,fig):
        childWindow = ChildWindow(fig, self.root,len(self.childWindows),self)
        return childWindow

    # 子Windowを閉じたときに、子Window側から呼び出される。子Windowリストの要素削除
    def deleteChildWindow(self, childWindow):
        if childWindow in self.childWindows:
            self.childWindows.remove(childWindow)

    # クローズ時の終了処理
    def onClose(self):
        for wnd in self.childWindows:
            wnd.quit()
            wnd.destroy()
        self.root.quit()
        self.root.destroy()

    # mainプロシージャで呼ばれてmainloop()する
    def run(self):
        self.root.protocol("WM_DELETE_WINDOW", self.onClose)
        self.root.mainloop()


# mainプロシージャでMainWindowオブジェクト作成、mainloop()。
def main():
    oGUI = MainWindow(200,100)
    oGUI.run()

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

### <font color="Orange">ChildWindow部：抜粋</font>
`MainWindow`から作成された子Window。
figオブジェクトを引数取得している。

2ステップに分かれている。
tk.Toplevelをベースに子Windowが作成される。

![{F8902D3C-C2C7-477A-B9A9-17686389A70B}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800268/921c37c1-5416-4510-a715-c9c15b35f8e7.png)

これに`self.cavas = FigureCanvasTkAgg(fig, self)` することでfigが載る。
（以下の図ではちょっと先回りして「toolbar」も載っているが容赦願いたい）

![{E5117DDD-7442-46AA-8E42-AE4C4EB03B84}.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3800268/8c472dc5-cb87-495a-b242-84b842c83532.png)

ここのコード抜粋
```python
class ChildWindow(tk.Toplevel):
    def __init__(self,fig,root,windowNum, parent):  # parent means "MainWindow object". to delete childWindow from childWindows[]
        super().__init__(root)
        self.parent = parent
        self.fig = fig
        self.title(f"child:{windowNum}")

###### ここが主なポイント ######
        self.canvas = FigureCanvasTkAgg(fig, self)
        self.canvas.draw()
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        # おまけ：子Window閉じたときの解法処理
        self.protocol("WM_DELETE_WINDOW", self.onClosing)

    def onClosing(self):
        self.parent.deleteChildWindow(self)
        self.destroy()
```

### <font color="Orange">Toolbar部：抜粋</font>
Windowの`self.canvas`に配置する
`self.canvas = FigureCanvasTkAgg(fig, self)`　したすぐ後に記述する

```python
class ChildWindow(tk.Toplevel):
    def __init__(self,fig,root,windowNum, parent):  # parent means "MainWindow object". to delete childWindow from childWindows[]
        super().__init__(root)
        self.parent = parent
        self.fig = fig
        self.title(f"child:{windowNum}")

        self.canvas = FigureCanvasTkAgg(fig, self)
        self.canvas.draw()

###### ここでToolbar作成 ######
        self.toolbar = NavigationToolbar2Tk(self.canvas, self)
        self.toolbar.update()
        self.toolbar.pack(side=tk.TOP, fill=tk.X, expand=False)
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

```


# <font color="Red">コード全体</font>
```python
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as tk
import numpy as np

class MainWindow:
    def __init__(self,windowWidth, windowHeight):
        self.root = tk.Tk()
        self.root.geometry(f"{windowWidth}x{windowHeight}")
        self.frame = tk.Frame(self.root, relief="raised", borderwidth=5 , bg="lightgray")
        self.DrawFigButton = tk.Button(self.frame, text="Draw Fig.", command=self.DrawFig)

        self.frame.pack(fill=tk.BOTH, expand=True)
        self.DrawFigButton.pack()

        self.childWindows=[]

    def DrawFig(self):
        data_len = 20
        n = 2
        m = 3
        data_x = np.random.random(size=(data_len, n,m))*10
        data_y = np.random.random(size=(data_len,n,m))*20
        print(data_x)
        labelList = [f"name_{i}" for i in range(n*m)]

        figBase = CreateFigure(n ,m, data_x, data_y, labelList)
        self.childWindow = self.addNewWindow(figBase.fig)
        self.childWindow.transient(self.root) # 常にMainWindowの手前に表示
        # self.childWindow.grab_set()   # 「常に手前に表示」で検索するとこれがヒットするが、子がモーダルになるので却下
        self.childWindows.append(self.childWindow)

    def addNewWindow(self,fig):
        childWindow = ChildWindow(fig, self.root,len(self.childWindows))
        return childWindow

    def onClose(self):
        for wnd in self.childWindows:
            wnd.quit()
            wnd.destroy()
        self.root.quit()
        self.root.destroy()

    def run(self):
        self.root.protocol("WM_DELETE_WINDOW", self.onClose)
        self.root.mainloop()

class ChildWindow(tk.Toplevel):
    def __init__(self,fig,root,windowNum):
        super().__init__(root)
        self.fig = fig
        self.title(f"child:{windowNum}")
        self.canvas = FigureCanvasTkAgg(fig, self)
        self.canvas.draw()

        # addToolbar
        self.toolbar = NavigationToolbar2Tk(self.canvas, self)
        self.toolbar.update()
        self.toolbar.pack(side=tk.TOP, fill=tk.X, expand=False)
        self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        self.protocol("WM_DELETE_WINDOW", self.onClosing)

    def onClosing(self):
        self.destroy()
    
class CreateFigure:
    def __init__(self, n, m, data_x, data_y, labelList):
        self.fig, self.axes = plt.subplots(n, m)
        self.data_x = data_x
        self.data_y = data_y
        self.labelList = labelList
        self.plot()
    def plot(self):
        for ax, x, y, label in zip(self.axes.flatten(),self.data_x, self.data_y, self.labelList):
            ax.scatter(x, y, label=label)
            # 必要に応じてその他装飾
            ax.set_title(f"for {label}")

def main():
    oGUI = MainWindow(200,100)
    oGUI.run()

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