Pythonで〇×ゲームのAIを一から作成する　その87　ファイルダイアログの設定とOutputウィジェット

# 目次と前回の記事

https://qiita.com/ysgeso/items/2381dd4e3283cbed49a0

https://qiita.com/ysgeso/items/5e5b2fdaf9eda23a9230

## これまでに作成したモジュール

以下のリンクから、これまでに作成したモジュールを見ることができます。

https://github.com/ysgeso/marubatsu/blob/master/087/marubatsu.py

https://github.com/ysgeso/marubatsu/blob/master/087/ai.py

https://github.com/ysgeso/marubatsu/blob/master/087/util.py

## ルールベースの AI の一覧

ルールベースの AI の一覧については、下記の記事を参照して下さい。

https://qiita.com/ysgeso/items/10d1d01192c014173e4b

# ファイルダイアログに関する問題点

[前回の記事](https://qiita.com/ysgeso/items/5e5b2fdaf9eda23a9230#%E5%AF%BE%E6%88%A6%E3%81%AE%E4%BF%9D%E5%AD%98%E3%81%A8%E8%AA%AD%E8%BE%BC)では、対戦結果をファイルに読み書きする処理を実装しましたが、その際に表示される __[ファイルダイアログ](https://qiita.com/ysgeso/items/5e5b2fdaf9eda23a9230#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%80%E3%82%A4%E3%82%A2%E3%83%AD%E3%82%B0%E3%81%AE%E8%A1%A8%E7%A4%BA)__ には以下のような __欠点__ があります。

今回の記事では、それぞれの対処法を説明します。

* __ファイルを開く__ ダイアログで __すべて拡張子のファイルが表示__ される。__実際に開く__ ファイルの __拡張子__ のファイル __だけを表示__ にしたほうが便利である
* __ダイアログに表示__ される __フォルダ__ が __`marubatsu.ipynb` のフォルダとは限らない__ ので、ファイルを保存するフォルダが必ず表示されるようにしたほうが便利である
* __ファイルの保存__ は `marubatsu.ipynb` とは __別のフォルダに行ったほうが便利__ である
* __ファイルを保存__ するダイアログで、__ファイル名をすべて入力する必要がある__。__ひな形となるファイル名__ と __拡張子__ が __あらかじめ入力__ されていたほうが便利である
* ファイルダイアログを表示した際に、ファイアダイアログが __フォーカスされない__ ので、キーボードでファイルダイアログを操作するためには、ファイルダイアログをクリックしてフォーカスする必要がある（後で説明しますが、__この問題は未解決__ です。修正方法をご存時の方がいればコメントしていただければ嬉しいです）

ファイルダイアログに関する設定は、ファイルダイアログを作成する際に __キーワード引数を記述__ することで行います。記述できるキーワード引数の一覧については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/dialog.html#native-load-save-dialogs

## JupyterLab でファイルダイアログを開くための前処理の記述場所

前回の記事で記述したプログラムには、修正したほうが良い点がある事がわかったので、ファイルダイアログの設定の説明を行う前に、その修正を行うことにします。

[前回の記事](https://qiita.com/ysgeso/items/5e5b2fdaf9eda23a9230#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%92%E9%96%8B%E3%81%8F%E3%83%80%E3%82%A4%E3%82%A2%E3%83%AD%E3%82%B0%E3%81%AE%E8%A1%A8%E7%A4%BA)で、JupyterLab でファイルダイアログを開くための __前処理__ として、下記のプログラムを記述しました。

```python
from tkinter import Tk, filedialog

root = Tk()
root.withdraw()
root.call('wm', 'attributes', '.', '-topmost', True)
```

前回の記事では、下記のプログラムのように、ファイルダイアログを開くたびにこの処理を実行しましたが、__この処理は 1 回だけ実行すれば良い__ ことがわかりました。

```python
def on_load_button_clicked(b):
    root = Tk()
    root.withdraw()
    root.call('wm', 'attributes', '.', '-topmost', True)        
    path = filedialog.askopenfilename()
略
```

:::note
上記の `root = Tk()` という処理では、`Tk` というクラスのインスタンスが作成されます。そのため、上記のプログラムでは、ファイルダイアログを開くたびに `Tk` クラスのインスタンスが作成されることになります。

ファイルダイアログを JupyterLab から開くためには、`Tk` クラスのインスタンスは 1 つだけ作成すれば十分です。__必要のないインスタンスの作成__ は __無駄なメモリの消費などにつながる__ ので避けたほうが良いでしょう。
:::

そこで、上記の処理を下記のプログラムのように __`Marubatsu_GUI` クラス__ の __ブロックの中に記述__ することにします。[以前の記事](https://qiita.com/ysgeso/items/44884dcf168aab949508#%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E5%AE%9A%E7%BE%A9%E3%81%AE%E5%AE%9F%E8%A1%8C%E6%99%82%E3%81%AB%E8%A1%8C%E3%82%8F%E3%82%8C%E3%82%8B%E5%87%A6%E7%90%86)で説明したように、__クラスの定義を実行__ すると、__クラスのブロックの中__ に記述された __プログラムが実行されます__。そのため、下記のプログラムのように marubatsu.py の中で `Marubatsu_GUI` クラスを定義し、__marubatsu モジュールをインポート__ すると、`Marubatsu_GUI` の定義が実行され、__一度だけ__ JupyterLab でファイルダイアログを開くための前処理が __実行される__ ようになります。

```python
class Marubatsu_GUI:
    # JupyterLab からファイルダイアログを開く際に必要な前処理
    root = Tk()
    root.withdraw()
    root.call('wm', 'attributes', '.', '-topmost', True)  
    
    def __init__(self, mb, ai_dict=None, seed=None, size=3):
元と同じなので省略
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
class Marubatsu_GUI:
    # JupyterLab からファイルダイアログを開く際に必要な前処理
+   root = Tk()
+   root.withdraw()
+   root.call('wm'+, 'attributes', '.', '-topmost', True)  
    
    def __init__(self, mb, ai_dict=None, seed=None, size=3):
元と同じなので省略

```
</details>

:::note
ファイルダイアログの前処理は、今回の記事の最初で marubatsu.iypnb に記述して実行済なので、今回の記事の残りのプログラムを実行する際に、上記のクラスの定義を marubatsu.ipynb に記述して実行する必要はありません。

上記のプログラムは、今回の記事の marubatsu_new.py に反映します。
:::

上記のように記述することで、下記のプログラムのように、`create_event_handler` の中で、ファイルダイアログを開く処理だけを記述すれば良くなります。

* 8、11 行目の下に記述されていた、ファイルダイアログを開くための前処理を削除する

```python
 1  from marubatsu import Marubatsu, Marubatsu_GUI
 2  import math
 3  import pickle
 4
 5  def create_event_handler(self):
元と同じなので省略
 6      # 開く、保存ボタンのイベントハンドラを定義する
 7      def on_load_button_clicked(b):
 8          # この下にあったファイルダイアログを開くための前処理を削除する
 9          path = filedialog.askopenfilename()
元と同じなので省略
10      def on_save_button_clicked(b):
11          # この下にあったファイルダイアログを開くための前処理を削除する
12          path = filedialog.asksaveasfilename()
元と同じなので省略
13    
14  Marubatsu_GUI.create_event_handler = create_event_handler
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
from marubatsu import Marubatsu, Marubatsu_GUI
import math
import pickle

def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b):
        path = filedialog.askopenfilename()
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                change_step(data["move_count"])

    def on_save_button_clicked(b):
        path = filedialog.asksaveasfilename()
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                }
                pickle.dump(data, f)
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)   
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
from marubatsu import Marubatsu, Marubatsu_GUI
import math
import pickle

def create_event_handler(self):
元と同じなので省略
    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b):
-       root = Tk()
-       root.withdraw()
-       root.call('wm', 'attributes', '.', '-topmost', True) 
        path = filedialog.askopenfilename()
元と同じなので省略
    def on_save_button_clicked(b):
-       root = Tk()
-       root.withdraw()
-       root.call('wm', 'attributes', '.', '-topmost', True) 
        path = filedialog.asksaveasfilename()
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

実行結果は省略しますが、下記のプログラムで `gui_play` を実行し、開く、保存ボタンをクリックするとファイルダイアログが表示されることを確認して下さい。

```python
from util import gui_play

gui_play()
```

## `filetypes` によるファイルの種類の設定

__ファイルダイアログ__ に表示する __ファイルの拡張子を設定する方法__ を説明します。そのためには、〇×ゲームの __対戦データ__ を保存するファイルの __拡張子を決める__ 必要があります。

### ファイルの拡張子の決定

ファイルの拡張子は、どこかの機関が管理していると思っている人がいるかもしれませんが、そのような機関はありません。__ファイルの拡張子__ は、その __ファイルの作成者__ が __自由に決めて構いません__。[前回の記事](https://qiita.com/ysgeso/items/5e5b2fdaf9eda23a9230#fn-7)で `pickle` で保存したファイルは `.pkl` という拡張子で良く保存されると説明しまいたが、下記の理由から、〇×ゲームの対戦データを保存するファイルは、__別の拡張子で保存したほうが良い__ でしょう。

* 他の Python のプログラムが `pickle` で保存した __関係のないファイルと区別がつかない__
* 間違って他人が作った __[危険な `pickle` のファイル](https://qiita.com/ysgeso/items/5e5b2fdaf9eda23a9230#pickle-%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E9%9A%9B%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9)__ を __開いてしまう可能性が生じる__

本記事では、〇×ゲームの対戦のデータなので __`.mbsav`__（marubatsu save の略）という拡張子で保存することにします。おそらくこの拡張子が他のアプリの保存ファイルで使われていることはほぼないと思いますので、__間違って別のアプリが作成した `.mbsav` ファイルを開く可能性は低い__ でしょう。なお、別の拡張子が良いと思った方は自由に変更して下さい。

### ファイルの種類の設定方法

ファイルダイアログに表示する __ファイルの拡張子の設定__ は、__ファイルの種類__ と __拡張子__ の情報を __ペアで設定__ します。具体的には、__`filetypes`__ という __キーワード引数__ に、__`("ファイルの種類の説明", "*.拡張子")`__ という __tuple__ を、__list の形式で記述__ します。

なお、キーワード引数の名前が __`filetypes`__ になっているので、以後はこの設定のことを拡張子の設定ではなく、__ファイルの種類の設定__ と表記することにします。

__`"*.拡張子"`__ の部分は、ファイルダイアログに __表示するファイルの検索の設定__ という意味を持ちます。__拡張子の前に記述__ する __`*`__ は、__ワイルドカード__ という、__検索__ のために利用できる __特殊な記号__ で、__任意の文字列__ という意味を持ちます。例えば __`*.mbsav`__ と記述することで、__`.mbsav` の前__ に __任意の文字列が入ったファイルを検索する__ という意味になるので、__拡張子が `.mbsav` のファイルだけ__ がファイルダイアログに __表示される__ ようになります。

なお、__検索の対象となるのはファイルだけ__ で、__フォルダはすべて表示__ されます。

:::note
本記事では利用しませんが、__`*.*`__ と記述すると、__任意の拡張子のファイルを検索__ するという意味になります。ファイルダイアログのファイルの種類で良く見かける「__すべてのファイル（\*.\*）__」は、すべての拡張子のファイルを検索して表示するという意味です。ただし、細かい話になりますが、`*.*` は、ファイル名の中に必ず `.` が入っている必要があるので、拡張子がないファイルは検索されません
:::

:::note
__ワイルド__ は「__特殊な__」という意味を持つ wild という英単語で、カードは文字通り、トランプやタロットなどのカードのことです。例えば __トランプの Joker__ は __特殊なカード__ なので __ワイルドカード__（wild card）と呼ばれます。また、__ウノ の Wild__ や __Wild 4__ なども特殊な意味を持つカードです。

大富豪やポーカーなどの多くのトランプのゲームの Joker や、ウノの Wild は __任意のカード__ や、__最強のカード__ として利用することができ、そのような性質を持つものを、英語で「__全能の__」という意味を持つ __オールマイティ__（allmighty）と呼びます。__ワイルドカード__ は、__オールマイティの意味__ で用いられることが __多く__、上記の `*` という記号は、任意の文字列のという、オールマイティな意味を持つ記号です。

余談ですが、大リーグのプレーオフで、リーグで優勝していない出場チームを表すワイルドカードは、スポーツにおける特別な追加枠という別の意味の用語です。
:::

`filetypes` は list の形式なので、__複数のファイルの種類__ を設定することができます。複数のファイルの種類を設定した場合は、ファイルダイアログの中に表示されるドロップダウンメニューから選択できるようになります。なお、__ファイルの種類が 1 つしかない場合__ でも __list の形式で記述する必要がある__ 点に注意して下さい。

下記は、1 行目が実引数を記述せずにファイルダイアログを開く、2 行目がファイルを開くダイアログに `.mbsav` の拡張子のファイルのみを表示するプログラムで、実行結果の左図が 1 行目、右図が 2 行目のプログラムで表示されるダイアログです。なお、ダイアログに表示されているフォルダは、どちらも marubatsu.ipynb 保存されるフォルダです。

```python
filedialog.askopenfilename()
filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")])
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/328010ee-417f-1700-cff4-811311bac12d.png" width="300">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/8defe6cb-4275-df6d-e328-86fdeda88ee8.png" width="300">

左右の図は、以下の点が異なります。

|  | 左図 | 右図 |
|:--|:--|:--|
| __表示されるファイル__  | すべてのファイル  | 拡張子が .mbsav のファイル<br>（存在しないので何も表示されない）  |
| __ファイル名の__<br>__テキストボックスの右__  | 何も表示されない  | ファイルの種類を選択する<br>ドロップダウンメニュー  |

ファイルの保存ダイアログの場合もほぼ同様ですが、下記のプログラムの実行結果のように、__ファイル名の下__ にファイルの種類を選択するドロップダウンメニューが表示される点が異なります。

```python
filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")])
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/6aeaf1e5-4ca1-634a-c100-f7edc20bca2d.png" width="300">

## `initialdir` による表示するフォルダの設定

ファイルダイアログの __作成時に表示されるフォルダ__ は、キーワード引数 __`initialdir`__ を __記述しない__ 場合は、__直前にファイルダイアログでファイルを選択したフォルダ__ になります。

ファイルダイアログの作時に __特定のフォルダを表示__ したい場合は、キーワード引数 __`initialdir`__ [^1]で __フォルダのパス__（住所）を指定します。ファイルダイアログを呼び出したプログラム（marubatsu.ipynb）のファイルと __同じフォルダを設定__ する場合は、下記のプログラムのように、__現在のフォルダのパス__ を意味する __`"./"`__ を記述します。

```python
filedialog.askopenfilename(initialdir="./")
```

実行結果は省略しますが、上記のプログラムを実行すると、ファイルダイアログの __作成時に表示されるフォルダ__ が、必ず __`marubatsu.ipynb` が保存されているフォルダ__ になります。

### 別のフォルダの表示

対戦結果を保存する __ファイルの数が多くなる__ と、`marubatsu.ipynb` とは __別のフォルダに対戦結果のファイルを保存__ したほうが __ファイルの整理がついて__ わかりやすくなります。そこで、本記事では __save__ というフォルダの中に対戦結果のファイルを保存することにします。

`marubatsu.ipynb` と __同じフォルダ内__ にあるフォルダを表示する場合は、下記のプログラムのように、__`initialdir`__ にその __フォルダの名前を記述__ します。

```python
filedialog.askopenfilename(initialdir="save")
```

ただし、実行結果は省略しますが、上記のプログラムのように、__存在しないフォルダ__ をキーワード引数 `initialdir` に記述してファイルダイアログを表示した場合は、エラーにはならず、__直前にファイルダイアログでファイルを選択したフォルダ__ が表示されます。

従って、ファイルダイアログを表示した際に __save フォルダを表示__ したい場合は、ファイルダイアログを表示する前に __save フォルダを作成する必要があります__。

## フォルダの作成

あらかじめ __`save`__ フォルダを __自分で作成するのは面倒__ なので、ファイルダイアログを開く際に、__Python のプログラム__ で `marubatsu.ipynb` と同じフォルダに __save というフォルダを作成する__ ことにします。

__フォルダの作成__ は、__os__ という組み込みモジュールの __`mkdir`__[^2] という関数で行えます。__`mkdir` の実引数__ には、作成する __フォルダのパスを記述__ します。これから作成する save フォルダのように、`mkdir` を実行する Python のプログラムと __同じフォルダ__ に新しいフォルダを作成する場合は、下記のプログラムのように __フォルダの名前だけ__ を記述します。

```python
import os

os.mkdir("save")
```

上記のプログラムを実行しても何も表示されませんが、下図のように __`marubatsu.ipynb` と同じフォルダ__ に __save というフォルダが作成される__ ので、確認して下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/97fbec64-8117-8084-585e-4a806cad3b9d.png" width="300">


`mkdir` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/os.html#os.mkdir

### 既にフォルダが存在する場合の対処法

__`mkdir`__ を実行した際に、 __既に__ 作成しようとした __フォルダが存在する場合__ は __エラーが発生__ します。例えば、上記のプログラムを実行後に、下記のプログラムで __もう一度 save フォルダを作成__ しようとすると実行結果のような「既に存在するファイルを作成することはできません」というメッセージが表示される __エラーが発生__ します。

```python
os.mkdir("save")
```

実行結果

```
---------------------------------------------------------------------------
FileExistsError                           Traceback (most recent call last)
Cell In[9], line 1
----> 1 os.mkdir("save")

FileExistsError: [WinError 183] 既に存在するファイルを作成することはできません。: 'save'
```

従って、__`mkdir` を利用する場合__ は、作成しようとした __フォルダが存在しない__ ことを __確認する必要があります__。__フォルダが存在するか__ どうかは、__`os.path.exists`__ という関数で確認することができます。`os.path.exists` は、__実引数に記述__ された __パスのファイルやフォルダが存在する__ 場合は __`True` を返す__ 関数で、下記のプログラムを実行すると、__save フォルダ__ は先程作成して __存在する__ ので、実行結果のように __`True` が表示__ されます。

```python
print(os.path.exists("save"))
```

実行結果

```
True
```

従って、下記のプログラムのように記述することで、__フォルダが存在しない場合のみ__、__`mkdir` が実行される__ ようになるので、実行結果のようにエラーが発生しなくなります。なお、下記のプログラムは if 文の条件文の計算結果が `False` になり、2 行目の処理は行われないため、実行結果には何も表示されません。

```python
if not os.path.exists("save"):
    os.mkdir("save")
```

実行結果

```
```

__`save` フォルダを作成__ する処理は __一度だけ行えば良い__ ので、下記のプログラムの 7、8 行目のように、ファイルダイアログの前処理と同様に、__`Marubatsu_GUI` クラスのブロックの中に記述する__ ことにします。

```python
 1  class Marubatsu_GUI:
 2      # JupyterLab からファイルダイアログを開く際に必要な前処理
 3      root = Tk()
 4      root.withdraw()
 5      root.call('wm', 'attributes', '.', '-topmost', True)  
 6
 7      # save フォルダが存在しない場合は作成する
 8      if not os.path.exists("save"):
 9          os.mkdir("save")    
10    
11      def __init__(self, mb, ai_dict=None, seed=None, size=3):
元と同じなので省略
```

<details><summary><strong>修正箇所</strong></summary>

```diff_python
class Marubatsu_GUI:
    # JupyterLab からファイルダイアログを開く際に必要な前処理
    root = Tk()
    root.withdraw()
    root.call('wm'+, 'attributes', '.', '-topmost', True)  

    # save フォルダが存在しない場合は作成する
+   if not os.path.exists("save"):
+       os.mkdir("save")    
    
    def __init__(self, mb, ai_dict=None, seed=None, size=3):
元と同じなので省略
```
</details>

:::note
save フォルダは先程作成したので、上記のクラスの定義を marubatsu.ipynb に記述して実行する必要はありません。

上記のプログラムは、今回の記事の marubatsu_new.py に反映します。
:::

`os.path.exists` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/os.path.html#os.path.exists

:::note
`os.makedirs`[^3] という関数にキーワード引数 `exist_ok=True` を記述して実行することで、既にフォルダが作成されている場合でもエラーが発生しないようにすることもできます。なお、`os.mkdir` と `os.makedirs` は、他にも行う処理に若干の違いがあります。興味がある方は、下記のリンク先を参照して下さい。
:::

https://docs.python.org/ja/3/library/os.html#os.makedirs

## ファイルの保存ダイアログのファイル名のひな形の設定

__ファイルの保存ダイアログ__ の __ファイル名__ のテキストボックスに、__あらかじめファイル名のひな形を入力する__ ためには、ひな形となるファイル名をどのように設定するかを決める必要があります。どのような名前がふさわしいかについて少し考えてみて下さい。

ファイルの作成の際に問題となるのは、__過去に保存したファイル__ と __異なる名前を考える必要がある__ 点です。作成する __ファイル名の重複を避ける方法__ の一つに、下記のような理由から、ファイル名に __ファイルを保存した日付と時刻を入れる__ というものがあります。

* 日付と時刻を入れることで、__過去に保存したファイル名__ と __重複しなくなる__
* __いつ保存した__ ファイルであるかが __わかりやすくなる__

### `datetime` クラスによる現在時刻の取得

__現在の日付と時刻__ は、__datetime__ というモジュールで定義された、__日付と時刻を管理する機能__ を持つ __`datetime`__ というクラスの __`now`__ というメソッドで得ることができます。

具体的には、下記のプログラムのように __`datetime.datetime.now`__ を記述します。

```python
import datetime

print(datetime.datetime.now())
```

実行結果

```
2024-06-06 12:06:22.770140
```

__`now`__ は、datetime モジュールで定義された、__モジュールと同じ名前__ の __`datetime`__ というクラスのメソッドなので、__`datetime.datetime.now()`__ と記述する必要があります。間違えやすいですが、`datetime.now()` と記述するとエラーが発生する点に注意して下さい。

:::note
datetime はクラスとして定義されたものですが、名前の頭文字が大文字になっていません。これは筆者の勝手な想像ですが、datetime から直接インスタンスを作成するという使い方をしないので、そのようになっているのではないかと思います（ちがっていたらすみません）。
:::

この記述がわかりづらいと思った方は、下記のプログラムのように __datetime モジュールから `datetime` クラスをインポート__ することで、__`datetime.now()` と記述できる__ ようになります。本記事では、以降はこの表記を採用します。

```python
from datetime import datetime

print(datetime.now())
```

実行結果

```
2024-06-06 12:06:23.668461
```

:::note
`now` は、`datetime` クラスの __[クラスメソッド](https://qiita.com/ysgeso/items/ad5e7ddb071653fd2ac5#%E3%82%AF%E3%83%A9%E3%82%B9%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89)として定義__ されているので、`datetime` クラスの __インスタンスを作成せず__ に、__`datetime`__ クラスから __直接呼び出します__。
:::

`datetime` クラスの詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/datetime.html#datetime-objects

`datetime` クラスの `now` メソッドの詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/datetime.html#datetime.datetime.now

### 時刻を表す文字列のフォーマットの設定方法

下記は、先程の実行結果です。これは __2024年6月6日12時6分23.668461秒__ を意味し、__`now`__ が、__現在時刻__ を __ミリ秒単位で計算__ することがわかります。しかし、ファイル名に __ミリ秒の部分の情報__ を入れるとファイル名が長くなるので、__削除したほうが良い__ でしょう。

```
2024-06-06 12:06:23.668461
```

また、__Windows__ では、__ファイル名__ に __半角の `:` を入れることはできない__ という決まりになっているので、__上記の文字列__ を __そのままファイル名に入れることはできません__。そこで、現在の日付と時刻を __別の文字列で表現する__ ことにします。


日付と時刻は、下記のように、__さまざまな文字列の形式__ で記述することができます。このような、何かを表現する __文字列の形式__ の事を文字列の __フォーマット__（format）と呼びます。

* 2024-06-06 12:06:23.668461
* 2024/06/06 12:06:23
* 2024-June-06 12:06:23
* 2024年6月6日 12時6分23秒
* 2024年06月06日 12時06分23秒

上記の 1、2、5 番目のフォーマットでは、__1 桁の数字__ である 6 の __先頭に 0 をつけて 2 桁の数字で表現__ しています。このように __数字の先頭に 0 をつけて、桁の数をそろえる__ ことを __0 埋め__ と呼び、下記のような利点があるので良く使われます。

* 日付と時刻の __文字列の長さが常に同じになる__ ので、並べた時に見やすくなる
* 名前の順で並べ替えた時に、日付の __新しい（または古い）順で並べ替えることができる__

上記の 2 番目の `2024/06/06` のような日付のフォーマットは良く使われますが、__Windows__ では __ファイル名に半角の `/` を入れることはできない__ ので、本記事では上記の 5 番目のフォーマットで日付と時刻の文字列を表現することにします。

__`now`__ は、__現在時刻__ の情報を持つ __`datetime` クラスのインスタンスを作成__ する関数で、`datetime` クラスには、__時刻__ を __任意のフォーマットの文字列に整形__ する __`strftime`__ というメソッドがあります。

__`strftime`__ の実引数には、__フォーマットを表す文字列__ を記述し、フォーマットの中に記述された下記の表の __% で始まる 2 文字の文字列__ を、__対応する文字列に変換する__ という処理を行います。なお、__数値に変換__ する際には、先程説明した __`0` 埋めが行われます__。

|  | 意味 | 文字の語源 |
|:--|:--|:--|
| __%Y__  | 4 桁の西暦に変換する  | year の略 |
| __%m__  | 2 桁の月に変換する  | month の略 |
| __%d__  | 2 桁の日に変換する  | day の略 |
| __%H__  | 2 桁の時間に変換する  | hour の略 |
| __%M__  | 2 桁の分に変換する  | minute の略 |
| __%S__  | 2 桁の秒に変換する  | second の略 |

なお、上記は本記事で利用するもののみを記述しています。`strftime` の詳細については、下記のリンク先を参照して下さい。

https://docs.python.org/ja/3/library/datetime.html#strftime-strptime-behavior

わかりづらいと思いますので具体例を示します。

下記の 2 ~ 5 行目は、現在時刻を `strftime` で指定したフォーマットに変換する処理を行うプログラムです。1 行目と比較して `strftime` が行う処理を確認して下さい。

なお、3、4 行目のように日付や時刻だけの文字列を作成することも可能です。また、本記事では、下記の 5 行目のフォーマットを採用しますが、もっとわかりやすいフォーマットを思いついた人は、自由に変更して下さい。

```python
print(datetime.now())
print(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print(datetime.now().strftime("%Y-%m-%d"))
print(datetime.now().strftime("%H:%M:%S"))
print(datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒"))
```

実行結果

```
2024-06-06 12:19:19.645087
2024-06-06 12:19:19
2024-06-06
12:19:19
2024年06月06日 12時19分19秒
```

### 名前への対戦カードの記述

ひな形となるファイル名には、下記のように時刻だけでなく、__対戦カードを入れたほうが便利__ だと思いましたので、ファイル名は下記のようにすることにします。対戦カードを入れないほうが良いと思った人、対戦カードを日付の後に入れたほうが良いと思った人、他のもっと良い名前を思いついた人などは、自由に変更して下さい。

__人間 VS ai1s 2024年06月06日 12時19分19秒.mbsav__

### ファイル名が入力済のファイルの保存ダイアログの作成

ファイル名が入力済のファイル保存ダイアログは、下記のプログラムのように、キーワード引数 __`initialfile`__ を記述することで、実行結果のように作成できます。また、下記のプログラムを実行して表示されるダイアログの保存ボタンをクリックすると、実行結果のように、`initialfile` に記述したファイルのパスが表示されます。

```python
print(filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")], initialdir="save",
                            initialfile="人間 VS ai1s 2024年06月04日 23時55分41秒.mbsav"))
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e6d1295e-ad2a-fe31-292e-f8a9eb905fa3.png" width="400">

```
C:/Users/ys/ai/marubatsu/087/save/人間 VS ai1s 2024年06月04日 23時55分41秒.mbsav
```

:::note
save フォルダには、拡張子が `.mbsav` のファイルが存在しないので、上図のようにファイルの保存ダイアログにはファイルは表示されません。

また、上図で「__検索条件に一致する項目はありません__」と表示されることから __`*.mbsav` によって__ 拡張子が `.mbsav` のファイルが __検索された__ ことが確認できます。
:::

### デフォルトの拡張子の設定

ファイル保存ダイアログを作成する際に、キーワード引数 __`defaultextension`__ を記述することで、ファイル名に __拡張子を記述しなかった場合__ に、__自動的にその拡張子を付加__ したファイルのパスが作成されるようになります。

例えば、下記のプログラムでは __`initialfile`__ に __拡張子のない `"test"` を設定__ し、__`defaultextension`__ で __`mbsav`__ というデフォルトの拡張子を設定しています。このプログラムを実行すると、実行結果のように、__ファイル名__ には __`test.mbsav`__ という、`defaultextension` で設定した __拡張子が付加__ されたものが表示されます。

```python
print(filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")], initialdir="save",
                                   initialfile="test", defaultextension="mbsav"))
```

実行結果

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/e8ed7008-5d32-6697-ddce-91f6b11e5cec.png" width="400">

また、上図のファイル名を編集して、下図のように __ファイル名に拡張子の無い `test` を入力__ して保存ボタンをクリックすると、下記の実行結果のように、`defaultextension` で設定した __拡張子が付加されたファイルのパスが表示__ されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7e57e3ef-fee4-f716-569b-fbb59d061e77.png" width="400">

```
C:/Users/ys/ai/marubatsu/087/save/test.mbsav
```

## プログラムの修正

下記は、ここまでで説明した内容を使って、ファイルダイアログの欠点を修正するように `create_event_handler` を修正したプログラムです。

* __4 行目__：「ファイルの種類」と、「ファイルダイアログの作成時に表示するフォルダ」を設定した、ファイルを開くダイアログを作成して表示する
* __7、8 行目__：ファイル名のひな形で利用する、それぞれの手番を表す文字列を計算し、list 内包表記を利用して list の形式で `name` に代入する
* __9 行目__：現在の日付と時刻を表す文字列を計算して `timestr` に代入する
* __10 行目__：`name` と `timestr` を使ってファイル名のひな形を計算し、`fname` に代入する
* __11 行目__：「ファイルの種類」、「ファイルダイアログの作成時に表示するフォルダ」、「ファイル名のひな形」、「デフォルトの拡張子」を設定した、ファイルの保存ダイアログを作成して表示する

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      # 開く、保存ボタンのイベントハンドラを定義する
 3      def on_load_button_clicked(b):
 4          path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
 5                                            initialdir="save")
元と同じなので省略
 6      def on_save_button_clicked(b):
 7          name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__ 
 8                  for i in range(2)]
 9          timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
10          fname = f"{name[0]} VS {name[1]} {timestr}"
11          path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
12                                              initialdir="save", initialfile=fname,
13                                              defaultextension="mbsav")
元と同じなので省略            
14    
15  Marubatsu_GUI.create_event_handler = create_event_handler
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                          initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                change_step(data["move_count"])

    def on_save_button_clicked(b):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[1]} {timestr}"
        path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save", initialfile=fname,
                                            defaultextension="mbsav")
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                }
                pickle.dump(data, f)
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)   
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def create_event_handler(self):
元と同じなので省略
    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b):
        path = filedialog.askopenfilename()
+       path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
+                                         initialdir="save")
元と同じなので省略         
    def on_save_button_clicked(b):
+       name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
+               for i in range(2)]
+       timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
+       fname = f"{name[0]} VS {name[1]} {timestr}"
-       path = filedialog.asksaveasfilename()
+       path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
+                                           initialdir="save", initialfile=fname,
+                                           defaultextension="mbsav")
元と同じなので省略            
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

上記の修正後に、下記のプログラムで `gui_play` を実行し、保存ボタンをクリックすると、`save` フォルダが表示され、実行結果の左図のように __ひな形のファイル名が入った__ ファイルの保存ダイアログが表示されます。ファイルを保存後に、開くボタンをクリックすると、実行結果の右図のように、`save` フォルダ内の __拡張子が `.mbsav` のファイルのみが表示__ されるようになります。また、保存したファイルを読み込むことができることを確認して下さい。

```python
gui_play()
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/5cfcdb34-866c-24f5-9af2-af2c3c6cd38d.png" width="300">　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/bb8dcb8d-a4df-28a2-9df5-34e7212e3481.png" width="300">

## ファイルダイアログのフォーカス（この問題は未解決です）

ウィンドウやパネルなどが、__キーボードからの入力の対象__ となっている状態のことを、__フォーカス__ されていると呼びます。現状では、開くや保存ボタンをクリックした際に、表示される __ファイルダイアログ__ が __フォーカスされず__、__JupyterLab のウィンドウ__ が __フォーカスされたまま__ になっているという問題があります。

そのため、例えば保存ボタンをクリックして表示されるファイルの保存ダイアログの __ひな形のファイル名のままファイルを保存__ しようとしてエンターキーを押しても、__JupyterLab 上でエンターキーが押されたことになる__ のでファイルは保存されません。また、ファイルの保存ダイアログの __ファイル名に文字を入力する__ ためには、__ファイルダイアログをマウスでクリック__ してフォーカスする必要があります。

JupyterLab でファイルダイアログを作成した際に、__ファイルダイアログをフォーカスする方法__ を探してみたのですが、__みつかりませんでした__ ので、現状では残念ながら、ファイルダイアログを表示した後で __ファイルダイアログに対してキー入力操作を行う際__ には、ファイルダイアログを __クリックしてフォーカス__ する必要があります。

JupyterLab でファイルダイアログを作成した際に、__ファイルダイアログをフォーカスする方法をご存じの方__ がいれば、__コメントで教えて頂ければ大変助かります__。

以上でファイルダイアログの欠点の修正は完了です。残りの記事では、他の〇×ゲームの GUI の改良方法について紹介します。

# ファイルを開いた際の Dropdown の設定と乱数の種の保存

現状では、ファイルを開いた際に、手番の担当を表す __Dropdown は変更されません__。そのため、例えば下記のような操作を行うと、__意図しない処理が行われる__ 可能性があります。

1. 人間どうしで対戦を行い、ファイルに保存する
2. Dropdown を操作して、対戦カードを変更した後で、手順 1 で保存したファイルを開く
3. 人間どうしの対戦のデータが読み込まれるが、Dropdown は手順 2 で変更された対戦カードのまま変化しない
4. そのため、リセットボタンをクリックすると、リセットボタンをクリックする前に表示されていた人間どうしの対戦ではなく、手順 2 で変更した対戦カードが開始されてしまう。これは、人によっては意図した対戦ではないかもしれない

また、現状では対戦結果のファイルに __乱数の種の情報が保存されていない__ ので、__乱数の種を設定した AI どうしの対戦__ を保存たファイルを __読み込んだ際__ に、__どのような乱数の種で対戦が行われたかがわからなくなる__ という問題があります。

そこで、上記の問題を解決するために、下記のようにプログラムを修正することにします。

* ファイルを読み込んだ際に、Dropdown に対戦カードが選択されるようにする
* ファイルに乱数の種の情報を保存する
* ファイルを読み込んだ際に、乱数の種のウィジェットを、読み込んだ情報で更新する

:::note
本記事では行いませんが、ファイル名のひな形に、下記のように乱数の種の情報を入れるという工夫も考えられます。


__人間 VS ai1s 乱数の種 123 2024年06月06日 12時19分19秒.mbsav__
:::

下記は、そのように `create_event_handler` を修正したプログラムです。なお、下記の説明は、わかりやすさを重視して、先に保存の処理の修正から説明します。

* __30 行目__：乱数の種の情報を保存する際に、乱数の種の Chekcbox と IntText の `value` 属性を保存してもかまわないが、__Checkox が OFF の状態の乱数の種の数値を保存しても意味はない__ ので、dict の __`seed`__ というキーの値に、下記のデータを代入することにした
    * 乱数の種の Checkbox が ON の場合は IntText の `value` 属性の値を代入する
    * 乱数の種の Checkboxが OFF の場合は `None` を代入する
* __14 ~ 16 行目__：ファイルから読み込んだ dict の __`ai`__ 属性の値を、それぞれの手番の Dropdown の `value` 属性に代入することで、Dropdown の選択を対戦カードのものに設定する。ただし、__人間が担当する場合__ は __`ai` 属性の要素には `None` が代入__ されているのに対し、__Dropdown__ の __人間の項目の `value` 属性__ には、[以前の記事](https://qiita.com/ysgeso/items/64de676735836b963712#dropdown-%E3%81%AE-value-%E5%B1%9E%E6%80%A7%E3%81%AE-none-%E3%81%AE%E6%84%8F%E5%91%B3)で説明した理由により __`"人間"` という文字列が代入__ されているので、__その変換の処理__ を 15 行目で行っている
 * __17 ~ 21 行目__：ファイルから読み込んだ dict の __`seed`__ 属性の値によって下記の処理を行う
    * `None` 以外の場合は Checkbox を ON にし、IntText の `value` 属性にその値を代入する
    * `None` の場合は Checkbox を OFF にする

```python
 1  import math
 2
 3  def create_event_handler(self):
元と同じなので省略
 4      # 開く、保存ボタンのイベントハンドラを定義する
 5      def on_load_button_clicked(b):
 6          path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
 7                                            initialdir="save")
 8          if path != "":
 9              with open(path, "rb") as f:
10                  data = pickle.load(f)
11                  self.mb.records = data["records"]
12                  self.mb.ai = data["ai"]
13                  change_step(data["move_count"])
14                  for i in range(2):
15                      value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
16                      self.dropdown_list[i].value = value      
17                  if data["seed"] is not None:                   
18                      self.checkbox.value = True
19                      self.inttext.value = data["seed"]
20                  else:
21                      self.checkbox.value = False
22                     
23      def on_save_button_clicked(b):
元と同じなので省略
24          if path != "":
25              with open(path, "wb") as f:
26                  data = {
27                      "records": self.mb.records,
28                      "move_count": self.mb.move_count,
29                      "ai": self.mb.ai,
30                      "seed": self.inttext.value if self.checkbox.value else None
31                  }
32                  pickle.dump(data, f)
元と同じなので省略
33    
34  Marubatsu_GUI.create_event_handler = create_event_handler
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
import random

def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                          initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                change_step(data["move_count"])
                for i in range(2):
                    value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                    self.dropdown_list[i].value = value                       
                if data["seed"] is not None:                   
                    self.checkbox.value = True
                    self.inttext.value = data["seed"]
                else:
                    self.checkbox.value = False
                    
    def on_save_button_clicked(b):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[1]} {timestr}"
        path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save", initialfile=fname,
                                            defaultextension="mbsav")
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)   
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
import math

def create_event_handler(self):
元と同じなので省略
    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                          initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                change_step(data["move_count"])
+               for i in range(2):
+                   value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
+                   self.dropdown_list[i].value = value     
+               if data["seed"] is not None:                   
+                   self.checkbox.value = True
+                   self.inttext.value = data["seed"]
+               else:
+                   self.checkbox.value = False
                    
    def on_save_button_clicked(b):
元と同じなので省略  
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
+                   "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

実行結果は省略しますが、上記の修正後に下記のプログラムで `gui_play()` を実行し、下記の操作を行ってプログラムが正しく動作することを確認して下さい。

```python 
gui_play()
```

* 以下の対戦を行い、それぞれの対戦結果をファイルに保存する（下記以外の対戦を行ってファイルに保存してもかまいません）
    * 人間 VS 人間 で乱数の種を利用しない
    * 人間 VS AI で乱数の種を利用しない
    * AI VS AI で乱数の種を利用する
* それぞれの対戦結果のファイルを開き、正しい対戦結果が再現され、乱数の種と Dropdown が正しく設定されることを確認する

:::note
__ファイルの保存データの形式を変更__ したので、上記の __修正を行う前に保存したファイルを読み込むとエラーが発生します__。そのため、上記の修正前に保存したファイルは github にはアップロードしません。
:::

# Output ウィジェットによるメッセージの表示と消去

現状のプログラムでは、__既にマークが配置されたマスをクリック__ すると、下図のように、`print` による __メッセージがゲーム盤の画像の下に表示__ されます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/999c493f-54a6-c54d-582a-8e3d5767ffa4.png" width="300">


現状では、この __メッセージはずっと残りつづけます__ が、ゲームが __リセットされた場合__ は、__表示を消去したほうが良い__ のでメッセージを消去する方法を説明します。

## Output ウィジェット

__通常__ は `print` による __文字列のメッセージの表示__ は、プログラムを実行したセルの下の __末尾に表示__ が行われますが、ipywidgets の __Output__ というウィジェットを利用することで、文字列のメッセージの __表示位置を制御__ したり、__表示内容を消去できる__ ようになります。

__Output__ は __`print`__ などによる、プログラムの __出力（output）の結果を表示__ するウィジェットで、下記のプログラムのように記述することで、作成して表示することができます。

```python
import ipywidgets as widgets

output = widgets.Output()
display(output)
```

Output は、__作成した時点では表示内容は空__ なので、上記のプログラムを実行しても何も表示されませんが、実行した JupyterLab のセルの下に Output が作成されています。

Output の詳細については、下記のリンク先を参照して下さい。

https://ipywidgets.readthedocs.io/en/stable/examples/Output%20Widget.html

### Output への文字の表示と表示内容の消去

__Output__ に __`print` で文字を表示__ するには、下記のプログラムの 2 行目のように __`with output:` のブロックの中に記述__ します。下記のプログラムを実行すると、2 行目の `with output:` のブロックの中で実行された `print` によるメッセージは、先程 Output を作成したセルの下に作成された Output の中に表示されますが、4 行目の `print` によるメッセージは、下記のプログラムを実行したセルの下に表示されます。

```python
with output:
    print("output message")  # このメッセージは Output を作成したセルの下に表示される

print("message")             # このメッセージはこのセルの下に表示される
```

__Output の表示内容を消去__ するには、下記のプログラムのように、Output の __`clear_output`__ メソッドを実行します。下記のプログラムを実行すると、先程の Output ウィジェットの表示が消去されます。

```python
output.clear_output()
```

### Output の作成、配置の実装

下記は、`create_widgets` を修正して Output を作成するプログラムです。

* __6 行目__：Output を作成して、`output` 属性に代入する

```python
1  def create_widgets(self):
元と同じなので省略
2      # ゲーム盤の画像を表す figure を作成する
3      self.create_figure()
4
5      # print による文字列を表示する Output を作成する
6      self.output = widgets.Output()
7
8  Marubatsu_GUI.create_widgets = create_widgets
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def create_widgets(self):
    # 乱数の種の Checkbox と IntText を作成する
    self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
                                    indent=False, layout=widgets.Layout(width="100px"))
    self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
                                layout=widgets.Layout(width="100px"))   

    # 読み書きのボタンを作成する
    self.load_button = self.create_button("開く", 100)
    self.save_button = self.create_button("保存", 100)
    
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセット、待ったボタンを作成する
    self.change_button = self.create_button("変更", 50)
    self.reset_button = self.create_button("リセット", 80)
    self.undo_button = self.create_button("待った", 60)    
    
    # リプレイのボタンとスライダーを作成する
    self.first_button = self.create_button("<<", 50)
    self.prev_button = self.create_button("<", 50)
    self.next_button = self.create_button(">", 50)
    self.last_button = self.create_button(">>", 50)     
    self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()

    # print による文字列を表示する Output を作成する
    self.output = widgets.Output()

Marubatsu_GUI.create_widgets = create_widgets
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def create_widgets(self):
元と同じなので省略
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()

    # print による文字列を表示する Output を作成する
+   self.output = widgets.Output()

Marubatsu_GUI.create_widgets = create_widgets
```
</details>


下記は、`create_widgets` を修正して __Output を一番下に配置__ するプログラムです。本記事では行いませんが、VBox の配列の先頭に `self.output` を記述することで、Output を一番上に表示するようにすることもできます。

* __3 行目__：VBox の配列の最後に `self.output` を記述し、Output を一番下に配置する

```python
1  def display_widgets(self):
元と同じなので省略
2      # hbox1 ~ hbox3、Output を縦に配置した VBox を作成し、表示する
3      display(widgets.VBox([hbox1, hbox2, hbox3, self.output]))
4    
5  Marubatsu_GUI.display_widgets = display_widgets 
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def display_widgets(self):
    # 乱数の種のウィジェット、読み書きのボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox3、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, self.output]))
    
Marubatsu_GUI.display_widgets = display_widgets
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def display_widgets(self):
元と同じなので省略
    # hbox1 ~ hbox3、Output を縦に配置した VBox を作成し、表示する
-   display(widgets.VBox([hbox1, hbox2, hbox3]))
+   display(widgets.VBox([hbox1, hbox2, hbox3, self.output]))
    
Marubatsu_GUI.display_widgets = display_widgets 
```
</details>

### Output へのメッセージの表示と消去の処理の実装

先程のメッセージの表示は、プレイヤーが __マークが配置済のマスに配置を行おうとした際__ に表示され、GUI でのその処理は __`on_mouse_down` の中__ で行われます。また、__配置の処理__ は __`move` メソッドで行われる__ ので、__`move` メソッドの呼び出し__ を __`with self.output:` のブロックの中に記述__ することで、メッセージを Output 内に表示することができます。

__メッセージの消去__ は、__ゲームのリセット__ が行われた際に行う必要があります。GUI でのゲームのリセットは __`on_reset_button_clicked`__ で行われるので、__その中で `clear_output` メソッドを呼び出す__ ことで、リセット時に Output の表示を削除することができます。

下記は、そのように `create_event_handler` を修正したプログラムです。

* __8 行目__：7 行目でゲームをリセットした後で、Output の表示内容を消去する
* __16、17 行目__：着手を行う `move` メソッドの呼び出しを `with self.output` のブロック内で行うように修正する

```python
 1  def create_event_handler(self):
元と同じなので省略
 2      # リセットボタンのイベントハンドラを定義する
 3      def on_reset_button_clicked(b=None):
 4          # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
 5          if self.checkbox.value:
 6              random.seed(self.inttext.value)
 7          self.mb.restart()
 8          self.output.clear_output()
 9          on_change_button_clicked(b)
元と同じなので省略
10      # ゲーム盤の上でマウスを押した場合のイベントハンドラ
11      def on_mouse_down(event):
12          # Axes の上でマウスを押していた場合のみ処理を行う
13          if event.inaxes and self.mb.status == Marubatsu.PLAYING:
14              x = math.floor(event.xdata)
15              y = math.floor(event.ydata)
16              with self.output:
17                  self.mb.move(x, y)                
18              # 次の手番の処理を行うメソッドを呼び出す
19              self.mb.play_loop(self)
元と同じなので省略
20    
21  Marubatsu_GUI.create_event_handler = create_event_handler
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                          initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                change_step(data["move_count"])
                for i in range(2):
                    value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                    self.dropdown_list[i].value = value               
                if data["seed"] is not None:                   
                    self.checkbox.value = True
                    self.inttext.value = data["seed"]
                else:
                    self.checkbox.value = False
                    
    def on_save_button_clicked(b):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[1]} {timestr}"
        path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save", initialfile=fname,
                                            defaultextension="mbsav")
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        self.output.clear_output()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            with self.output:
                self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)   
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def create_event_handler(self):
元と同じなので省略
    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
+       self.output.clear_output()
        on_change_button_clicked(b)
元と同じなので省略
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
-           self.mb.move(x, y)                
+           with self.output:
+               self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

上記の修正後に、下記のプログラムで `gui_play` を実行し、既にマークが配置されたマスをクリックすると、実行結果のようにメッセージが表示されますが、__メッセージがゲーム盤の画像の上に表示される__ という問題が生じています。ただし、__リセットボタンをクリック__ すると、__メッセージが消去される__ という処理は __正しく実装されている__ ことが確認できます。

```python
gui_play()
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/eb65669f-37b9-43d0-457f-bf8c0c19c372.png" width="300">

## matplotlib の figure の配置

上記の問題は、ゲーム盤を描画する matplotlib の __Figure__ を __`display` を使って表示していない__ ことが原因です。Figure は[以前の記事](https://qiita.com/ysgeso/items/ab2770693c947c108dd0#figure-%E3%81%AE%E6%8F%8F%E7%94%BB)で説明したように、JupyterLab の __セルの実行後に自動的に描画される__ ので、`display` によって表示された __ウィジェットより後に表示__ されることになり、ゲーム盤の画像の上に Output のメッセージが表示されます。

matplotlib の __Figure__ は、ipywidgets の __ウィジェット同様__ に、__`display` で描画できます__。その際には、Figure の __`canvas` 属性を指定__ します。 また、__HBox__ や __VBox の中__ に __Figure を配置__ することもできるので、下記のプログラムのように、__Figure を VBox の中で Output の前に配置__ することで、__ゲーム盤の下に Output のメッセージが表示される__ ようになります。

* __3 行目__：VBox の中で、Output の前に Figure を登録するように修正する

```python
1  def display_widgets(self):
元と同じなので省略
2      # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
3      display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output]))
4    
5  Marubatsu_GUI.display_widgets = display_widgets 
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def display_widgets(self):
    # 乱数の種のウィジェット、読み書きのボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output]))
    
Marubatsu_GUI.display_widgets = display_widgets 
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def display_widgets(self):
元と同じなので省略
    # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
-   display(widgets.VBox([hbox1, hbox2, hbox3, self.output]))
+   display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output]))
    
Marubatsu_GUI.display_widgets = display_widgets 
```
</details>

上記の修正を行った後で、下記のプログラムで `gui_play()` を実行し、マークが配置されているマスをクリックすると、実行結果のように、ゲーム盤の下にメッセージが表示されるようになりますが、その下に __2 つ目のゲーム盤が描画されてしまう__ という問題が発生します。

```python
gui_play()
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/b36d73c9-0dbf-d458-0933-bb344998f1de.png" width="250">

### Figure が 2 回描画される問題の修正方法

この問題は、`display` と、JupyterLab のセルの実行の終了後で、__2 回 Figure が描画される__ ことが原因です。従って、JupyterLab の __セルの実行の終了後__ に __自動的に Figure を描画しないようにする__ ことでこの問題を解決することができます。

__`%matplotlib widget` を実行した場合__ の matplotlib の Figure は、__Figure の作成時__ に [以前の記事](https://qiita.com/ysgeso/items/1920c68f5a1e8a998796#%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%83%96%E3%83%A2%E3%83%BC%E3%83%89)で説明した matplotlib の __インタラクティブモードを OFF__ にすることで、__セルの実行後に自動的に描画されない__ ようにすることができます。

matplotlib のインタラクティブモードは、__`plt.on()`__ と __`plt.off()`__ を実行することで、ON/OFF を切り替えることができるので、下記のプログラムのように `create_figure` を修正することで、この問題を解決することができます。なお、__Figure の作成後__ にインタラクティブモードを __ON に戻す必要がある__ 点に注意して下さい。

* __4 ~ 6 行目__：5 行目で Figure を作成する前に 4 行目でインタラクティブモードを OFF にし、Figure の作成後の 6 行目で ON に戻す

```python
 1  import matplotlib.pyplot as plt
 2
 3  def create_figure(self):
 4      plt.ioff()
 5      self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
 6      plt.ion()
 7      self.fig.canvas.toolbar_visible = False
 8      self.fig.canvas.header_visible = False
 9      self.fig.canvas.footer_visible = False
10      self.fig.canvas.resizable = False
11    
12  Marubatsu_GUI.create_figure = create_figure
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
import matplotlib.pyplot as plt

def create_figure(self):
    plt.ioff()
    self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
    plt.ion()
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False
    
Marubatsu_GUI.create_figure = create_figure
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
import matplotlib.pyplot as plt

def create_figure(self):
+   plt.ioff()
    self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
-   plt.ion()
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False
    
Marubatsu_GUI.create_figure = create_figure
```
</details>

:::note
matplotlib のインタラクティブモードについては、初心者が完全に理解するのは難しいと思いますので、当面は __`display` を使って Figure の配置を行いたい場合__ は、__Figure を作成する際にインタラクティブモードを OFF にする必要がある__ とだけ覚えておけばよいでしょう。
:::

なお、インタラクティブモードを __一時的に OFF にする__ 場合は、下記のプログラムの 2、3 行目のように __`with plt.ioff():` のブロックの中__ に Figure を作成する処理を記述するという方法があります。この方法を利用した場合は、処理の後で `plt.ion()` を記述してインタラクティブモードを __ON に戻す処理を記述しなくても済む__ という利点があるので、本記事でもこの記述方法を採用することにします。

```python
1  def create_figure(self):
2      with plt.ioff():
3          self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
4      self.fig.canvas.toolbar_visible = False
5      self.fig.canvas.header_visible = False
6      self.fig.canvas.footer_visible = False
7      self.fig.canvas.resizable = False
8    
9  Marubatsu_GUI.create_figure = create_figure
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def create_figure(self):
    with plt.ioff():
        self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False
    
Marubatsu_GUI.create_figure = create_figure
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def create_figure(self):
-   plt.ioff()
-   self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
+   with plt.ioff():
+       self.fig, self.ax = plt.subplots(figsize=[self.size, self.size])
-   plt.ion()       
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False
    
Marubatsu_GUI.create_figure = create_figure
```
</details>

上記の修正を行った後で、下記のプログラムで `gui_play()` を実行するとゲーム盤が 1 つだけ描画されるようになります。また、マークが配置されているマスをクリックすると、実行結果のように、ゲーム盤の下にメッセージが表示されるようになることを確認して下さい。

```python
gui_play()
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/7032d32e-0999-b899-74a4-e24e45579ba2.png" width="250">

# キー操作の追加とヘルプの表示

最後に以下の機能を追加することにします。__操作説明__ は、作成したプログラムを自分で遊ぶだけであれば必要はありませんが、__他人に遊んでもらう場合はあったほうが良い__ でしょう。

* 開く、保存の操作をキー入力で行えるようにする
* 操作説明のヘルプの表示機能の追加

まず、開く、保存、ヘルプのそれぞれの操作に対して、どのキーをそれぞれの操作に対応させるかを決める必要があります。本記事では、以下のキーをそれぞれの機能に対応させることにします。基本的にはテンキーに対応させましたが、保存、開く、ヘルプは save、open、help の頭文字を取って `s`、`o`、`h` キーにも対応させることにしました。

| 機能| キー |
|:--|:--|
| __開く__  |  `-` と `l` |
| __保存__ | `+` と `s`  |
| __ヘルプ__ | `*` と `h` |

ヘルプ機能に関しては、以下の処理を実装することにします。

* 開くボタンの右に、ヘルプ機能を呼び出す ？ が表示されたボタンを配置する
* Output の表示内容を削除し、操作説明を表示する

下記は、`create_widgets` にヘルプボタンの作成する処理を追加したプログラムです。

* __3 ~ 5 行目__：開く、保存ボタンの幅を縮小し、ヘルプボタンを作成する

```python
1  def create_widgets(self):
元と同じなので省略
2      # 読み書き、ヘルプのボタンを作成する  
3      self.load_button = self.create_button("開く", 80)
4      self.save_button = self.create_button("保存", 80)
5      self.help_button = self.create_button("？", 30)
元と同じなので省略    
6
7  Marubatsu_GUI.create_widgets = create_widgets
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def create_widgets(self):
    # 乱数の種の Checkbox と IntText を作成する
    self.checkbox = widgets.Checkbox(value=self.seed is not None, description="乱数の種",
                                    indent=False, layout=widgets.Layout(width="100px"))
    self.inttext = widgets.IntText(value=0 if self.seed is None else self.seed,
                                layout=widgets.Layout(width="100px"))   

    # 読み書き、ヘルプのボタンを作成する
    self.load_button = self.create_button("開く", 80)
    self.save_button = self.create_button("保存", 80)
    self.help_button = self.create_button("？", 30)
    
    # AI を選択する Dropdown を作成する
    self.create_dropdown()
    # 変更、リセット、待ったボタンを作成する
    self.change_button = self.create_button("変更", 50)
    self.reset_button = self.create_button("リセット", 80)
    self.undo_button = self.create_button("待った", 60)    
    
    # リプレイのボタンとスライダーを作成する
    self.first_button = self.create_button("<<", 50)
    self.prev_button = self.create_button("<", 50)
    self.next_button = self.create_button(">", 50)
    self.last_button = self.create_button(">>", 50)     
    self.slider = widgets.IntSlider(layout=widgets.Layout(width="200px"))
    # ゲーム盤の画像を表す figure を作成する
    self.create_figure()

    # print による文字列を表示する Output を作成する
    self.output = widgets.Output()

Marubatsu_GUI.create_widgets = create_widgets
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def create_widgets(self):
元と同じなので省略
    # 読み書き、ヘルプのボタンを作成する
-   self.load_button = self.create_button("開く", 100)
+   self.load_button = self.create_button("開く", 80)
-   self.save_button = self.create_button("保存", 100)
+   self.save_button = self.create_button("保存", 80)
+   self.help_button = self.create_button("？", 30)
元と同じなので省略    

Marubatsu_GUI.create_widgets = create_widgets
```
</details>

下記は、`display_widgets` にヘルプボタンを配置する処理を追加したプログラムです。

* __3、4 行目__：ヘルプボタンを保存ボタンの右に配置する

```python
1  def display_widgets(self):
2      # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
3      hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, 
4                            self.save_button, self.help_button])
元と同じなので省略
5    
6  Marubatsu_GUI.display_widgets = display_widgets 
```

<details><summary><strong>行番号のないプログラム</strong></summary>

```python
def display_widgets(self):
    # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
    hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button,
                          self.save_button, self.help_button])
    # 〇 と × の dropdown とボタンを横に配置した HBox を作成する
    hbox2 = widgets.HBox([self.dropdown_list[0], self.dropdown_list[1], self.change_button, self.reset_button, self.undo_button])
    # リプレイ機能のボタンを横に配置した HBox を作成する
    hbox3 = widgets.HBox([self.first_button, self.prev_button, self.next_button, self.last_button, self.slider]) 
    # hbox1 ~ hbox3、Figure、Output を縦に配置した VBox を作成し、表示する
    display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas, self.output]))
    
Marubatsu_GUI.display_widgets = display_widgets 
```
</details>

<details><summary><strong>修正箇所</strong></summary>

```diff_python
def display_widgets(self):
    # 乱数の種のウィジェット、読み書き、ヘルプのボタンを横に配置した HBox を作成する
-   hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button, self.save_button])
+   hbox1 = widgets.HBox([self.checkbox, self.inttext, self.load_button,
+                         self.save_button, self.help_button])
元と同じなので省略
    
Marubatsu_GUI.display_widgets = display_widgets 
```
</details>

下記は `create_event_handler` を修正したプログラムです。

修正箇所が長いので、行番号は表示せずに、プログラムの説明のみを行います。なお、表示する操作説明を変更した人は自由に変更して下さい。

__`on_help_button_clicked`__ は、ヘルプボタンをクリックした際の下記の処理を記述します。

* Output の表示を削除する
* `with self.output:` のブロックの中に、Output に表示する __操作説明を表示__ する。__複数行にまたがって表示__ するので、[以前の記事](https://qiita.com/ysgeso/items/4d55e73d6571fe85aa49#%E3%83%88%E3%83%AA%E3%83%97%E3%83%AB%E3%82%AF%E3%82%AA%E3%83%BC%E3%83%88%E3%81%AB%E3%82%88%E3%82%8B%E6%96%87%E5%AD%97%E5%88%97%E3%81%AE%E3%83%AA%E3%83%86%E3%83%A9%E3%83%AB)で説明した __トリプルクオート__（"""）を使って表示を行っている。なお、文字列の中に記述されている __`\t`__ は、__タブ__ という __空白文字__ を表す。__タブ__ は、__次の 8 の倍数の文字までの長さ__ の __空白を表す文字__ で、この操作説明のように、__複数行にまたがって__、表示する文字の __縦の位置を揃える__ 際に良く使われる

__`on_key_press`__ では、他のボタンと同様の方法で、キーと対応する処理を行う関数を表すデータを追加しています。

```python
def create_event_handler(self):
元と同じなので省略
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

マスの上でクリックすることで着手を行う。
下記の GUI で操作を行うことができる。
()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
開く(-,L)\tファイルから対戦データを読み込む
保存(+,S)\tファイルに対戦データを保存する
？(*,H)\t\tこの操作説明を表示する
手番の担当\tメニューからそれぞれの手番の担当を選択する
\t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
変更\t\tゲームの途中で手番の担当を変更する
リセット\t手番の担当を変更してゲームをリセットする
待った(0)\t1つ前の自分の着手をキャンセルする
<<(↑)\t\t最初の局面に移動する
<(←)\t\t1手前の局面に移動する
>(→)\t\t1手後の局面に移動する
>>(↓)\t\t最後の着手が行われた局面に移動する
スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    self.help_button.on_click(on_help_button_clicked)
元と同じなので省略
    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
            "-": on_load_button_clicked,            
            "l": on_load_button_clicked,            
            "+": on_save_button_clicked,            
            "s": on_save_button_clicked,            
            "*": on_help_button_clicked,            
            "h": on_help_button_clicked,            
        }
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```

<details><summary><strong>プログラム全体</strong></summary>

```python
def create_event_handler(self):
    # 乱数の種のチェックボックスのイベントハンドラを定義する
    def on_checkbox_changed(changed):
        self.update_widgets_status()
        
    self.checkbox.observe(on_checkbox_changed, names="value")

    # 開く、保存ボタンのイベントハンドラを定義する
    def on_load_button_clicked(b=None):
        path = filedialog.askopenfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                          initialdir="save")
        if path != "":
            with open(path, "rb") as f:
                data = pickle.load(f)
                self.mb.records = data["records"]
                self.mb.ai = data["ai"]
                change_step(data["move_count"])
                for i in range(2):
                    value = "人間" if self.mb.ai[i] is None else self.mb.ai[i]
                    self.dropdown_list[i].value = value               
                if data["seed"] is not None:                   
                    self.checkbox.value = True
                    self.inttext.value = data["seed"]
                else:
                    self.checkbox.value = False
                    
    def on_save_button_clicked(b=None):
        name = ["人間" if self.mb.ai[i] is None else self.mb.ai[i].__name__
                for i in range(2)]
        timestr = datetime.now().strftime("%Y年%m月%d日 %H時%M分%S秒")
        fname = f"{name[0]} VS {name[1]} {timestr}"
        path = filedialog.asksaveasfilename(filetypes=[("〇×ゲーム", "*.mbsav")],
                                            initialdir="save", initialfile=fname,
                                            defaultextension="mbsav")
        if path != "":
            with open(path, "wb") as f:
                data = {
                    "records": self.mb.records,
                    "move_count": self.mb.move_count,
                    "ai": self.mb.ai,
                    "seed": self.inttext.value if self.checkbox.value else None
                }
                pickle.dump(data, f)
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

マスの上でクリックすることで着手を行う。
下記の GUI で操作を行うことができる。
()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
開く(-,L)\tファイルから対戦データを読み込む
保存(+,S)\tファイルに対戦データを保存する
？(*,H)\t\tこの操作説明を表示する
手番の担当\tメニューからそれぞれの手番の担当を選択する
\t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
変更\t\tゲームの途中で手番の担当を変更する
リセット\t手番の担当を変更してゲームをリセットする
待った(0)\t1つ前の自分の着手をキャンセルする
<<(↑)\t\t最初の局面に移動する
<(←)\t\t1手前の局面に移動する
>(→)\t\t1手後の局面に移動する
>>(↓)\t\t最後の着手が行われた局面に移動する
スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
    self.help_button.on_click(on_help_button_clicked)
    
    # 変更ボタンのイベントハンドラを定義する
    def on_change_button_clicked(b):
        for i in range(2):
            self.mb.ai[i] = None if self.dropdown_list[i].value == "人間" else self.dropdown_list[i].value
        self.mb.play_loop(self)

    # リセットボタンのイベントハンドラを定義する
    def on_reset_button_clicked(b=None):
        # 乱数の種のチェックボックスが ON の場合に、乱数の種の処理を行う
        if self.checkbox.value:
            random.seed(self.inttext.value)
        self.mb.restart()
        self.output.clear_output()
        on_change_button_clicked(b)

    # 待ったボタンのイベントハンドラを定義する
    def on_undo_button_clicked(b=None):
        if self.mb.move_count >= 2 and self.mb.move_count == len(self.mb.records) - 1:
            self.mb.move_count -= 2
            self.mb.records = self.mb.records[0:self.mb.move_count+1]
            self.mb.change_step(self.mb.move_count)
            self.draw_board()
        
    # イベントハンドラをボタンに結びつける
    self.change_button.on_click(on_change_button_clicked)
    self.reset_button.on_click(on_reset_button_clicked)   
    self.undo_button.on_click(on_undo_button_clicked)   
    
    # step 手目の局面に移動する
    def change_step(step):
        self.mb.change_step(step)
        # 描画を更新する
        self.draw_board()        

    def on_first_button_clicked(b=None):
        change_step(0)

    def on_prev_button_clicked(b=None):
        change_step(self.mb.move_count - 1)

    def on_next_button_clicked(b=None):
        change_step(self.mb.move_count + 1)
        
    def on_last_button_clicked(b=None):
        change_step(len(self.mb.records) - 1)

    def on_slider_changed(changed):
        if self.mb.move_count != changed["new"]:
            change_step(changed["new"])
            
    self.first_button.on_click(on_first_button_clicked)
    self.prev_button.on_click(on_prev_button_clicked)
    self.next_button.on_click(on_next_button_clicked)
    self.last_button.on_click(on_last_button_clicked)
    self.slider.observe(on_slider_changed, names="value")
    
    # ゲーム盤の上でマウスを押した場合のイベントハンドラ
    def on_mouse_down(event):
        # Axes の上でマウスを押していた場合のみ処理を行う
        if event.inaxes and self.mb.status == Marubatsu.PLAYING:
            x = math.floor(event.xdata)
            y = math.floor(event.ydata)
            with self.output:
                self.mb.move(x, y)                
            # 次の手番の処理を行うメソッドを呼び出す
            self.mb.play_loop(self)

    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
            "-": on_load_button_clicked,            
            "l": on_load_button_clicked,            
            "+": on_save_button_clicked,            
            "s": on_save_button_clicked,            
            "*": on_help_button_clicked,            
            "h": on_help_button_clicked,            
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                event.inaxes = True
                event.xdata = num % 3
                event.ydata = 2 - (num // 3)
                on_mouse_down(event)
            except:
                pass
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)   
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

<details><summary><strong>修正箇所（長いので <code>on_help_button_clicked</code>の定義には + はつけません）</strong></summary>

```diff_python
def create_event_handler(self):
元と同じなので省略
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

マスの上でクリックすることで着手を行う。
下記の GUI で操作を行うことができる。
()が記載されているものは、キー入力で同じ操作を行うことができることを意味する。
なお、キー入力の操作は、ゲーム盤をクリックして選択状態にする必要がある。

乱数の種\tチェックボックスを ON にすると、右のテキストボックスの乱数の種が適用される
開く(-,L)\tファイルから対戦データを読み込む
保存(+,S)\tファイルに対戦データを保存する
？(*,h)\t\tこの操作説明を表示する
手番の担当\tメニューからそれぞれの手番の担当を選択する
\t\tメニューから選択しただけでは担当は変更されず、変更またはリセットボタンによって担当が変更される
変更\t\tゲームの途中で手番の担当を変更する
リセット\t手番の担当を変更してゲームをリセットする
待った(0)\t1つ前の自分の着手をキャンセルする
<<(↑)\t\t最初の局面に移動する
<(←)\t\t1手前の局面に移動する
>(→)\t\t1手後の局面に移動する
>>(↓)\t\t最後の着手が行われた局面に移動する
スライダー\t現在の手数を表す。ドラッグすることで任意の手数へ移動する

手数を移動した場合に、最後の着手が行われた局面でなければ、リプレイモードになる。
リプレイモード中に着手を行うと、リプレイモードが解除され、その着手が最後の着手になる。""")
            
    self.load_button.on_click(on_load_button_clicked)
    self.save_button.on_click(on_save_button_clicked)
+   self.help_button.on_click(on_help_button_clicked)
元と同じなので省略
    # ゲーム盤を選択した状態でキーを押した場合のイベントハンドラ
    def on_key_press(event):
        keymap = {
            "up": on_first_button_clicked,
            "left": on_prev_button_clicked,
            "right": on_next_button_clicked,
            "down": on_last_button_clicked,
            "0": on_undo_button_clicked,
            "enter": on_reset_button_clicked,            
+           "-": on_load_button_clicked,            
+           "l": on_load_button_clicked,            
+           "+": on_save_button_clicked,            
+           "s": on_save_button_clicked,            
+           "*": on_help_button_clicked,            
+           "h": on_help_button_clicked,            
        }
元と同じなので省略
    
Marubatsu_GUI.create_event_handler = create_event_handler
```
</details>

上記の修正を行った後で、下記のプログラムで `gui_play()` を実行すると、実行結果のように ？ ボタンが表示されるようになります。また、？ ボタンをクリックすると、実行結果のように、Output に操作説明が表示されるようになります。また、開く、保存、？ に対応するキーを押すと、それぞれの機能が呼び出されることを確認して下さい。

```python
gui_play()
```

実行結果（下図は、画像なので操作することはできません）

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524802/ad50b44e-93b4-703a-2690-300854b6f524.png" width="400">

以上で、GUI の機能の実装は終了です。以前の記事でも説明しましたが、アイディアがあればいくらでも GUI の機能を充実させることができるので、良いアイディアを思いついた人は実装してみて下さい。

# 今回の記事のまとめ

今回の記事では、ファイルダイアログに関するプログラムの修正、Output に関する処理の実装、ヘルプ機能の実装を行いました。

次回からは、再び AI の話に戻り、探索型の AI の実装を開始します。

# 本記事で入力したプログラム

以下のリンクから、本記事で入力して実行した JupyterLab のファイルを見ることができます。

https://github.com/ysgeso/marubatsu/blob/master/087/marubatsu.ipynb

以下のリンクは、今回の記事で更新した marubatsu.py です。

https://github.com/ysgeso/marubatsu/blob/master/087/marubatsu_new.py

# 次回の記事

https://qiita.com/ysgeso/items/7cd67869855550d222f5

[^1]:最初（initial）に表示するディレクトリ（directory）（フォルダの別名のこと）が名前の由来です
[^2]:フォルダ（directory）の作成（make）を意味する __make directory の略__ です
[^3]:`mkdir` と 名前が良く似ていますが、__make が略されていない__ 点と、__名前が複数形__ になっている点が異なります