Tkinterで閉じると親ウィンドウに戻るサブウィンドウを作る

## はじめに
Tkinterで、子ウィンドウを開いた後に別アプリを開いてから子ウィンドウを閉じると、親ウィンドウへ戻らず直前に開いたアプリのウィンドウへフォーカスが行ってしまう。メインウィンドウどこ行った？

てなわけで、子ウィンドウを閉じたら親ウィンドウへ戻るようにしたい。

## 考え方
ウィンドウが閉じられたら、親ウィンドウにフォーカスをセットする。

### ポイント
実際に子ウィンドウが閉じられる前に親ウィンドウへフォーカスをセットし、その後に子ウィンドウを閉じる。


## 実際のコード
```Python
import tkinter as tk
from tkinter import ttk


class View(ttk.Frame):
    def __init__(self, master):
        super().__init__(master)
        self.app = app

        self.label = ttk.Label(self, text='これがメインウィンドウ')
        self.btn1 = ttk.Button(self, text='OPEN1', command=self.app.op_sub_win1)
        self.btn2 = ttk.Button(self, text='OPEN2', command=self.app.op_sub_win2)

        self.label.pack()
        self.btn1.pack(anchor=tk.S, expand=True)
        self.btn2.pack(anchor=tk.N, expand=True)


class SubWindow1(tk.Toplevel):
    def __init__(self, master):
        super().__init__(master)

        self.geometry('250x250')
        self.focus_set()

        self.label = ttk.Label(self, text='サブウィンドウ1')

        self.label.pack(expand=True)


class SubWindow2(tk.Toplevel):
    def __init__(self, master):
        super().__init__(master)

        self.geometry('250x250')
        self.focus_set()
        # ウィンドウを閉じる動作を上書き
        self.protocol('WM_DELETE_WINDOW', self.destroy)

        self.label1 = ttk.Label(self, text='サブウィンドウ2')
        self.label2 = ttk.Label(self, text='閉じるとメインウィンドウに戻る')

        self.label1.pack(anchor=tk.S, expand=True)
        self.label2.pack(anchor=tk.N, expand=True)

    def destroy(self):
        self.master.focus_set()
        super().destroy()


class App:
    def __init__(self):
        self.root = tk.Tk()
        self.root.geometry('400x300')

        self.view = View(self.root, self)
        self.view.pack(expand=True, fill=tk.BOTH)

    def op_sub_win1(self):
        # ウィンドウを作る
        SubWindow1(self.root)

    def op_sub_win2(self):
        # ウィンドウを作る
        SubWindow2(self.root)

    def mainloop(self):
        self.root.mainloop()


if __name__ == '__main__':
    app = App()
    app.mainloop()
```

上記のコードでは、`Toplevel`クラスを継承した `SubWindow1`、`SubWindow2`という2種類のクラスを用意し、挙動を比べられるようにした。両者の違いは2か所のみ。

```Python
class SubWindow2(tk.Toplevel):
    def __init__(self, master, app):
        ########### 省略 #############
        # ウィンドウを閉じる動作を上書き
        self.protocol('WM_DELETE_WINDOW', self.destroy)
        ########### 省略 #############

    def destroy(self):           # <- destroyメソッドをオーバーライド
        self.master.focus_set()  # <- rootにフォーカスをセット
        super().destroy()        # <- SubWindowを閉じる
```
`self.protocol('WM_DELETE_WINDOW', "任意の関数")` ではウィンドウが閉じられるときの処理を再定義している。つまり、[×]をクリックしたり `Alt + F4` を押したりでウィンドウを閉じた時の処理を変更している。余談だが、 `'WM_DELETE_WINDOW'` の "WM" は "Window Manager" のことのようだ。
`def destroy(self)` では、 `destroy` メソッドをオーバーライドし、`SubWindow2`を __閉じる前に__ その親ウィジェットへフォーカスをセットするようにしている。
これにより、 `SubWindow2` に対して `destroy` メソッドを呼び出せば、必ず親ウィンドウにフォーカスをセットしてからウィンドウを閉じるようになる。一枚のメインウィンドウに多数のモーダルを用意する場合は、あらかじめ`destroy`メソッドをオーバーライドした新クラスを定義しておき、その新クラスを用いるようにすると手間が省ける……かもしれない。

## やりがちなミス
### メソッドのオーバーライドミス
```python
def destroy(self):
    self.master.focus_set()
    self.destroy()  # <- super().destroy()
```
この場合は `RecursionError`が生じてアプリが終了できなくなる可能性がある。

### 閉じてからfocus_setしてしまう
```python
class App:
    #### 中略 ####
    def op_sub_win1(self):
        win = SubWindow1(self.root)  # サブウィンドウを開く
        self.root.wait_window(win) # サブウィンドウが閉じられるのを待つ
        self.root.focus_set()      # メインウィンドウにフォーカス
```
この場合、サブウィンドウが閉じられたタイミングでPC画面上の2番目のウィンドウ（アプリ）にフォーカスが移動してしまい、 `self.root.focus_set()` が働かない。

## おまけ
複数の子ウィンドウを別個に定義するのも手間なので、閉じたら親ウィンドウに戻るだけの機能拡張を施したサブクラスの定義を置いておきます。
```python
class SubWindow(tk.Toplevel):
    def __init__(self, master: tk.Misc = None, cnf: dict ={}, **kw):
        super().__init__(master, cnf, **kw)
        self.protocol("WM_DELETE_WINDOW", self.destroy)

    def destroy(self):
        self.master.focus_set()
        super().destroy()
```

## 参考
[toplevel manual page - Tk Built-In Commands](https://tcl.tk/man/tcl8.6/TkCmd/toplevel.htm)
[wm manual page - Tk Built-In Commands](https://tcl.tk/man/tcl8.6/TkCmd/wm.htm)