0
1

Pythonで〇×ゲームのAIを一から作成する その96 テンキーとマウスによる中心となるノードの移動

Last updated at Posted at 2024-07-07

目次と前回の記事

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

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

リンク 説明
marubatsu.py Marubatsu、Marubatsu_GUI クラスの定義
ai.py AI に関する関数
util.py ユーティリティ関数の定義。現在は gui_play のみ定義されている
tree.py ゲーム木に関する Node、Mbtree クラスの定義
gui.py GUI に関する処理を行う基底クラスとなる GUI クラスの定義

ルールベースの AI の一覧

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

ボタンの配置のレイアウトの改良

前回の記事に引き続き、ゲーム木の視覚化を行う GUI の改良を行います。

現状では、GUI の 4 つのボタンが下図のように並んでいますが、これではわかりづらいので、ゲーム機のコントローラーの十字キーのような配置にすることにします。

具体的には、下図のように配置します。なお、上図ではボタンの幅を 100 ピクセルにしましたが、下図のように配置した場合にボタンの幅を 100 ピクセルにするとバランスが少々悪く見えたので、下図ではボタンの幅を 50 ピクセルにしました。

ボタンの配置の方法

上図のようにボタンを配置する方法としては、これまで通り HBox と VBox を組み合わせる方法と、格子(grid)状に区切られれた表のようなものの中にウィジェットを配置することができる GridBox というウィジェットを利用する方法が考えられます。

GridBox は柔軟な配置を行うことができて便利なのですが、使いこなすためにはいろいろと学ばなければならないことがあるので、本記事では HBox と VBox を組み合わせる方法を紹介します。GridBox の使い方に興味がある方は、下記のリンク先を参照して下さい。

Label ウィジェットを利用した、大きさを持つ空白のウィジェットの作成

上図のようなボタンの配置は、下記のような 3 つの HBox を作成 し、それらを VBox で縦に並べて配置す ることで、行うことができます。なお、下記の「50 px」は、表示の幅が 50 ピクセルであるという意味を表します

  • 「50 px の空白、50 px の ↑ ボタン、50 px の空白」を並べた Hbox
  • 「50 px の ← ボタン、50 px の空白、50 px の → ボタン」を並べた Hbox
  • 「50 px の空白、50 px の ↓ ボタン、50 px の空白」を並べた Hbox

50 px の 空白 は、文字列を表示する Label というウィジェットを利用することで表示することができます。Lable ウィジェットは、ipywidgets の Label という関数を使って、下記のようなプログラムで作成することができます。実行結果のように、キーワード引数 value で指定した文字列が表示された Label が作成されます。

import ipywidgets as widgets

label = widgets.Label(value="ラベル")
display(label)

実行結果

Label に表示する文字列を 空文字"")にすることで、何も表示しない横幅のある 空白のラベルを表示することができます。また、Label には、他のウィジェットと同様にキーワード属性 layout を使って 表示幅などを設定 することができるので、下記のプログラムによって、幅が 50 ピクセルの空白のラベルを作成することができます。

widgets.Label(value="", layout=widgets.Layout(width=f"50px"))

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

create_widgets の修正

まず、create_widgets で、幅が 50 ピクセルの空白の Label を作成します。下図のボタンの配置を行うためには、5 つの空白の Label を HBox の中に配置する必要がありますが、その 5 つの Label は いずれも幅が同じ で、後から Label に文字を表示することはない ので、Label を 一つだけ作成 し、5 箇所に同じ Label を配置 すれば済みます。

後からいずれかの Label の表示内容を変更する必要がある場合は、5 つの異なる Label を作成する必要があります。同じ Label を 5 箇所に配置してしまうと、5 つの Label の表示内容が同時に変化してしまう からです。

下記は、そのように create_widgets を修正したプログラムです。なお、先程説明したように、ボタンの幅を 50 ピクセルに修正しました。

  • 5 ~ 8 行目:4 つのボタンの幅を 50 ピクセルに修正する
  • 9 行目:幅が 50 ピクセルの空白の Label を作成し、label 属性に代入する
 1  from tree import Mbtree_GUI
 2  import matplotlib.pyplot as plt
 3
 4  def create_widgets(self):
 5      self.left_button = self.create_button("", 50)
 6      self.up_button = self.create_button("", 50)
 7      self.right_button = self.create_button("", 50)
 8      self.down_button = self.create_button("", 50)
 9      self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
元と同じなので省略
10    
11  Mbtree_GUI.create_widgets = create_widgets
行番号のないプログラム
from tree import Mbtree_GUI
import matplotlib.pyplot as plt

def create_widgets(self):
    self.left_button = self.create_button("", 50)
    self.up_button = self.create_button("", 50)
    self.right_button = self.create_button("", 50)
    self.down_button = self.create_button("", 50)    
    self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
    
    with plt.ioff():
        self.fig = plt.figure(figsize=[self.width * self.size,
                                        self.height * self.size])
        self.ax = self.fig.add_axes([0, 0, 1, 1])
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False   
    
Mbtree_GUI.create_widgets = create_widgets
修正箇所
from tree import Mbtree_GUI
import matplotlib.pyplot as plt

def create_widgets(self):
-   self.left_button = self.create_button("", 100)
+   self.left_button = self.create_button("", 50)
-   self.up_button = self.create_button("", 100)
+   self.up_button = self.create_button("", 50)
-   self.right_button = self.create_button("", 100)
+   self.right_button = self.create_button("", 50)
-   self.down_button = self.create_button("", 100)
+   self.down_button = self.create_button("", 50)
+   self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
元と同じなので省略
    
Mbtree_GUI.create_widgets = create_widgets

display_widgets の修正

次に、display_widgets を下記のプログラムのように修正し、4 つのボタンが十字の形に配置されるようにします。先ほど説明したように、5 箇所にある 空白の Label のウィジェット は、同じウィジェットを使いまわしています

  • 2 ~ 4 行目:先ほど説明したようにウィジェットを配置した 3 行分の HBox を作成する
  • 5 行目:3 つの HBox と Figure を縦に並べた VBox を作成し、display で表示する
1  def display_widgets(self):   
2      hbox1 = widgets.HBox([self.label, self.up_button, self.label])
3      hbox2 = widgets.HBox([self.left_button, self.label, self.right_button])
4      hbox3 = widgets.HBox([self.label, self.down_button, self.label])
5      display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas]))  
6    
7  Mbtree_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):   
    hbox1 = widgets.HBox([self.label, self.up_button, self.label])
    hbox2 = widgets.HBox([self.left_button, self.label, self.right_button])
    hbox3 = widgets.HBox([self.label, self.down_button, self.label])
    display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas]))  
    
Mbtree_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):   
-   hbox = widgets.HBox([self.left_button, self.right_button, self.up_button, self.down_button])
+   hbox1 = widgets.HBox([self.label, self.up_button, self.label])
+   hbox2 = widgets.HBox([self.left_button, self.label, self.right_button])
+   hbox3 = widgets.HBox([self.label, self.down_button, self.label])
-   display(widgets.VBox([hbox, self.fig.canvas]))  
+   display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas]))  
    
Mbtree_GUI.display_widgets = display_widgets

上記の修正後に、下記のプログラムを実行することで、実行結果のように 4 つのボタンが十字の形に配置されて表示されるようになります。また、4 つのボタンが正しく動作することを、ボタンを操作して確認して下さい。

from tree import Mbtree

mbtree = Mbtree()
mbtree_gui = Mbtree_GUI(mbtree)

実行結果

任意の子ノードへの移動

現状では、→ ボタンで子ノードへ移動する際に、先頭の子ノードにしか移動できない ので、例えば最後の子ノードに移動する場合は、→ ボタンをクリックした後で、何度も ↓ ボタンをクリックしなければならない点が不便です。

そこで、任意の子ノードへ移動できる ように修正することにします。どのような UI でそのような操作を行うかについて少し考えてみて下さい。

テンキーを使った任意の子ノードへの移動

〇×ゲームのゲーム盤は 3 x 3 の 9 マスで、テンキーの 1 ~ 9 のボタンに対応させることができます。そのため、〇×ゲームの GUI では、以前の記事で説明したように、テンキーを使って、対応するゲーム盤のマスに着手を行うことができるようにしました。

そこで、ゲーム木を視覚化する GUI でも、テンキーを使って任意の子ノードへ移動する という操作方法が考えられます。具体的には、以下のような操作方法です。

  • 現在表示されている部分木の中心となるノードに対して、テンキーに対応するマスに着手 を行った場合の子ノードへ移動する
  • ただし、ゲームの決着がついている場合や、既にマークが配置されているマスに対応するテンキーを押した場合は、何も行わない

Node クラスの修正

上記のように Mbtree_GUI クラスを改良するためには、中心となるノードの それぞれの子ノード の局面の、直前に行われた着手を知る必要 が生じます。

直前に行われた着手は、Marubatsu クラスのインスタンスの last_move 属性に代入されているので、centernode の最初の子ノードの場合は、centernode.mb.children[0].last_move に直前の着手が代入されていますが、そのような方法でそれぞれの子ノードを調べるのは、わかりづらく、プログラムの記述が大変です。

そこで、本記事では dict を使って、ノードに対して 行われた着手子ノードの対応を記録する ことにします。具体的には下記のように Node クラスを修正します。

  • Node クラスのインスタンスに children_by_move という名前の属性を追加する。この名前は、着手によって対応づけられた子ノード の一覧という意味を表す
  • その属性には、キーが着手 を表し、キーの値がその着手を行った局面を表す子ノード を表す dict を代入する
  • ↑、↓ ボタンを押した時に必要となる、前後の兄弟ノードや、最後の子ノードを調べる処理は、list のほうが dict よりやりやすいので、children 属性はそのまま利用する

まず、Node クラスの __init__ メソッドを下記のプログラムのように修正します。

  • 8 行目children_by_move 属性を、空の dict で初期化する
 1  from tree import Node
 2
 3  def __init__(self, mb, parent=None, depth=0):
 4      self.mb = mb
 5      self.parent = parent
 6      self.depth = depth
 7      self.children = []
 8      self.children_by_move = {}
 9    
10  Node.__init__ = __init__
行番号のないプログラム
from tree import Node

def __init__(self, mb, parent=None, depth=0):
    self.mb = mb
    self.parent = parent
    self.depth = depth
    self.children = []
    self.children_by_move = {}
    
Node.__init__ = __init__
修正箇所
from tree import Node

def __init__(self, mb, parent=None, depth=0):
    self.mb = mb
    self.parent = parent
    self.depth = depth
    self.children = []
+   self.children_by_move = {}
    
Node.__init__ = __init__

次に、子ノードを挿入する insert メソッドを下記のプログラムのように修正します。

  • 3 行目children_by_move 属性に対して、挿入する子ノードに対して行われた着手をキーとし、キーの値にその子ノードを代入する処理を追加する
1  def insert(self, node):
2      self.children.append(node)
3      self.children_by_move[node.mb.last_move] = node
4
5  Node.insert = insert
行番号のないプログラム
def insert(self, node):
    self.children.append(node)
    self.children_by_move[node.mb.last_move] = node

Node.insert = insert
修正箇所
def insert(self, node):
    self.children.append(node)
+   self.children_by_move[node.mb.last_move] = node

Node.insert = insert

なお、以前の記事で説明したように、dict のキー には、ハッシュ可能なオブジェクト を利用することができ、last_move 属性には、下記のプログラムの実行結果のように、ハッシュ可能なオブジェクトである tuple が代入されているので、上記のプログラムは正しく動作します。なお、下記のプログラムでは、深さ 1 のノードの last_move 属性を表示しました。

print(mbtree.nodelist_by_depth[1][0].mb.last_move)

実行結果

(0, 0)

Node クラスのインスタンスに 新しい属性を加えた ので、下記のプログラムで Mbtree クラスのインスタンスを作成し直す 必要があります。作成し直した後で、ルートノードの children_by_move 属性を表示すると、実行結果のようにルートノードに対して行われた着手と、子ノードの対応を記録する dict が代入されていることが確認できます。

なお、dict の内容をわかりやすく表示するために pprint を利用しました。

from pprint import pprint

mbtree = Mbtree()
print(mbtree.root.children_by_move)

実行結果

ノードの作成に関する表示は省略
{(0, 0): <tree.Node object at 0x000002DC13F4EF50>,
 (0, 1): <tree.Node object at 0x000002DC13F565D0>,
 (0, 2): <tree.Node object at 0x000002DC13E35D50>,
 (1, 0): <tree.Node object at 0x000002DC1381CE50>,
 (1, 1): <tree.Node object at 0x000002DC137966D0>,
 (1, 2): <tree.Node object at 0x000002DC13D70D50>,
 (2, 0): <tree.Node object at 0x000002DC13BAF6D0>,
 (2, 1): <tree.Node object at 0x000002DC13C74590>,
 (2, 2): <tree.Node object at 0x000002DC133AB1D0>}

create_event_handler の修正

次に、create_event_handler に、テンキーが押された場合の処理を行うイベントハンドラを定義 します。まず、押されたキーに対応するマスの座標を計算 する必要がありますが、その処理は以前の記事で説明したように、押されたテンキーの番号が num に代入されていた場合は、下記の式で計算できます。

x = num % 3
y = 2 - (num // 3)

また、テンキーが押された場合の処理は、Mbtree_GUI クラスの create_event_handler に記述されている下記のプログラムと同様の方法で記述できます。

 1  if event.key in keymap:
 2      keymap[event.key]()1  try:
 3  else:
 4      try:
 5          num = int(event.key) - 1
 6          event.inaxes = True
 7          event.xdata = num % 3
 8          event.ydata = 2 - (num // 3)
 9          on_mouse_down(event)
10      except:
11          pass

なお、上記のプログラムでは、ゲーム盤の上でマスを押した際に呼び出される on_mouse_down を利用して着手を行うために、event.xdataevent.ydata にマスの x、y 座標を計算して代入していますが、今回はそのような処理は必要ありません。

また、4、10 行目の tryexcept は、テンキー以外のキーが押された場合に 5 行目の int(event.key) の処理でエラーが発生する点に対処するためのものです。忘れた方は、以前の記事を復習して下さい。

従って、create_event_handler は、下記のプログラムのように修正します。tryexcept の行に関する説明は、上記で説明したので省略します。

  • 7 ~ 9 行目:押されたテンキーに対応するマスの x、y 座標を計算する
  • 10 行目:着手を表す tuple を計算し、move に代入する
  • 11 行目movecenternodechildren_by_move 属性に代入された dict のキーの中に存在するかどうかを in 演算子で判定する。なお、dict に対する in 演算子は、指定した値が dict のキー に存在するかどうかを判定する
  • 12 行目:存在する場合は、そのキーの値に、キーの着手を行った局面のノードが代入されているので、centernode にそのノードを代入する
  • 13 行目:中心となるノードが変更されたので、GUI の表示を更新する
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_key_press(event):
元と同じなので省略
 3          if event.key in keymap:
 4              keymap[event.key]()
 5          else:
 6              try:
 7                  num = int(event.key) - 1
 8                  x = num % 3
 9                  y = 2 - (num // 3)
10                  move = (x, y)
11                  if move in self.centernode.children_by_move:
12                      self.centernode = self.centernode.children_by_move[move]
13                      self.update_gui()
14              except:
15                  pass            
16            
17      # fig の画像イベントハンドラを結び付ける
18      self.fig.canvas.mpl_connect("key_press_event", on_key_press)
19
20  Mbtree_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    def on_left_button_clicked(b=None):
        if self.centernode.parent is not None:
            self.centernode = self.centernode.parent
            self.update_gui()
            
    def on_right_button_clicked(b=None):
        if self.centernode.depth < 6 and len(self.centernode.children) > 0:
            self.centernode = self.centernode.children[0]
            self.update_gui()

    def on_up_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if index > 0:
                self.centernode = self.centernode.parent.children[index - 1]
                self.update_gui()
            
    def on_down_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if self.centernode.parent.children[-1] is not self.centernode:
                self.centernode = self.centernode.parent.children[index + 1]
                self.update_gui()            
            
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)
    self.up_button.on_click(on_up_button_clicked)
    self.down_button.on_click(on_down_button_clicked)

    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
            "right": on_right_button_clicked,
            "up": on_up_button_clicked,
            "down": on_down_button_clicked,
        }
        if event.key in keymap:
            keymap[event.key]()
        else:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.centernode.children_by_move:
                    self.centernode = self.centernode.children_by_move[move]
                    self.update_gui()
            except:
                pass            
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)

Mbtree_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
    def on_key_press(event):
元と同じなので省略
        if event.key in keymap:
            keymap[event.key]()
+       else:
+           try:
+               num = int(event.key) - 1
+               x = num % 3
+               y = 2 - (num // 3)
+               move = (x, y)
+               if move in self.centernode.children_by_move:
+                   self.centernode = self.centernode.children_by_move[move]
+                   self.update_gui()
+           except:
+               pass            
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)

Mbtree_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、テンキーで任意の子ノードに移動できるようになったことを確認して下さい。なお、キー操作を行う場合は、以前の記事で説明したように、Figure の上でマウスをクリックして、Figure を選択状態にする必要がある点に注意して下さい。

mbtree_gui = Mbtree_GUI(mbtree)

バグの修正と改良

上記のプログラムでテンキーの操作を行うと、下記のような問題がある事がわかります。

  • 親ノードに移動 したい場合に押す必要がある ← キー が、テンキーから離れた場所にあるので 押しづらい
  • 前回の記事で移動できないようにした、深さが 7 以上のノードに移動できてしまう

前者の問題を解決する方法として、本記事ではテンキーの中にある、0 キーで親ノードに移動 できるようにすることにします。

下記は、上記の 2 つを修正したプログラムです。

  • 5 行目:0 キーを押した場合に on_left_button_clicked が呼び出されるように、keymap にその情報を追加する
  • 12 行目centernode の深さが 6 未満の場合のみテンキーの操作が行えるようにする
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_key_press(event):
 3          keymap = {
 4              "left": on_left_button_clicked,
 5              "0": on_left_button_clicked,
 6              "right": on_right_button_clicked,
 7              "up": on_up_button_clicked,
 8              "down": on_down_button_clicked,
 9          }
10          if event.key in keymap:
11              keymap[event.key]()
12          elif self.centernode.depth < 6:
13              try:
元と同じなので省略
14
15  Mbtree_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    def on_left_button_clicked(b=None):
        if self.centernode.parent is not None:
            self.centernode = self.centernode.parent
            self.update_gui()
            
    def on_right_button_clicked(b=None):
        if self.centernode.depth < 6 and len(self.centernode.children) > 0:
            self.centernode = self.centernode.children[0]
            self.update_gui()

    def on_up_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if index > 0:
                self.centernode = self.centernode.parent.children[index - 1]
                self.update_gui()
            
    def on_down_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if self.centernode.parent.children[-1] is not self.centernode:
                self.centernode = self.centernode.parent.children[index + 1]
                self.update_gui()            
            
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)
    self.up_button.on_click(on_up_button_clicked)
    self.down_button.on_click(on_down_button_clicked)

    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
            "0": on_left_button_clicked,
            "right": on_right_button_clicked,
            "up": on_up_button_clicked,
            "down": on_down_button_clicked,
        }
        if event.key in keymap:
            keymap[event.key]()
        elif self.centernode.depth < 6:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.centernode.children_by_move:
                    self.centernode = self.centernode.children_by_move[move]
                    self.update_gui()
            except:
                pass            
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)

Mbtree_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
+           "0": on_left_button_clicked,
            "right": on_right_button_clicked,
            "up": on_up_button_clicked,
            "down": on_down_button_clicked,
        }
        if event.key in keymap:
            keymap[event.key]()
-       else:
+       elif self.centernode.depth < 6:
            try:
元と同じなので省略

Mbtree_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、上記の修正後に下記のプログラムを実行し、2 つの問題が修正されたことを確認して下さい。

mbtree_gui = Mbtree_GUI(mbtree)

マウスの操作による中心となるノードの移動

上記では、テンキーによって任意の子ノードに移動できるようにしましたが、画面に表示される ノードをマウスで押す ことで、そのノードに 直接移動できるとさらに便利 です。どのような処理を記述すれば、そのようなことが行えるようになるかを少し考えてみて下さい。

上記のような操作を行うには、マウスを押した際に、押した マウスの座標 が、画面に表示された どのノード上にあるかを判定する 必要があります。

そのためには、画面に表示されているそれぞれの ノードの表示範囲を記録しておく という方法があります。ノードの表示範囲の表現方法 について少し考えてみて下さい。

Rect クラスの定義

ノードはゲーム盤として表示され、ゲーム盤は正方形の形状で描画 されます。そのため、ノードの表示範囲は 長方形1を表すデータとして記録 することができます。また、ゲーム盤の は、x 軸と y 軸に平行 になります。

辺が x 軸と y 軸に平行な長方形 を表すデータは、一般的に長方形の 頂点の中の 1 つの座標 と、長方形の 幅と高さ を使って表現するのが一般的です。また、一般的に 4 つの頂点の中で、x と y 座標が最も小さい値を持つ頂点が選ばれます

長方形のデータは、上記のように 4 つのデータから構成されるので、長方形を表す rectangle の略である Rect という 長方形を表すクラスを定義 し、そのインスタンスによって表現することにします。

これは、Mbtree_GUI クラスの 機能を拡張 する際に、拡張する機能を 別のクラスで定義して利用する という 委譲 です。長方形を表す Rect クラスは、Mbtree_GUI クラスと is-a の関係にない ので、継承を使うべきではありません

下記は、Rect クラスの定義です。__init__ メソッドでは、長方形を表す 4 つのデータを代入する 仮引数を用意 し、それぞれの 仮引数を同じ名前の属性に代入 しています。

class Rect:
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height

__str__ メソッドの定義

下記のプログラムのように、Rect メソッドの インスタンスを作成して print で表示 すると、その中身がわからないような表示 が行われてしまします。

rect = Rect(0, 0, 10, 20)
print(rect)

実行結果

<__main__.Rect object at 0x000002DC48040D50>

そこで、以前の記事で説明した __str__ メソッドを定義する事で、print で Rect のインスタンスを表示した際に、その中身がわかるような内容が表示 されるようにします。

下記は、__str__ メソッドの定義で、print で表示した際に表示する、図形の形状頂点の座標高さ を表す文字列を返すように定義します。

def __str__(self):
    return f"Rectangle ({self.x}, {self.y}) width = {self.width} height = {self.height}"

Rect.__str__ = __str__

上記の修正後に、下記のプログラムを実行することで、print で Rect の内容がわかるようになったことが確認できます。

print(rect)

実行結果

Rectangle (0, 0) width = 10 height = 20

is_inside メソッドの定義

マウスによるノードの移動処理では、マウスを押した座標 が、ノードを表す 長方形の内部にあるかどうかを判定する必要 があります。そこで、Rect クラスに、指定した座標が長方形の内部にあるか どうかを 判定 する、下記のようなメソッドを定義する事にします。このメソッドをどのように定義すれば良いかについて少し考えてみて下さい。

  • 名前:長方形の内部(inside)あるか(is)どうかを判定するので is_inside とする
  • 処理:実引数で指定した座標が、長方形の内部にあるかどうかを判定する
  • 入力:判定する座標を仮引数 x、y に代入する
  • 出力:(x, y) が長方形の内部にある場合は True、そうでない場合は False を返す

頂点が (x, y)、幅が width、高さが height の長方形の内部にある点は、下図から以下の性質を同時に満たす必要がある事がわかります。

  • x 座標が、x 以上 かつ x + width 未満である
  • y 座標が、y 以上 かつ y + height 未満である

上記では、長方形の内部の判定の際に良く使われる x 以上x + width 未満 を条件としたので、以下のような判定が行われます。

  • 左の辺 の上の点を長方形の 内部とみなす
  • 右の辺 の上の点を長方形の 内部とみなさない

x より大きい や、x + width 以下 とすることで、左右の辺を長方形の内部とみなすかどうかを変更することができます。

今回の場合は、辺を長方形の内部とみなすかどうかの違いは、ほとんど影響を与えないのでどちらでもかまいませんが、辺が長方形の内部であるかどうかが重要になる場合は、このことを正しく理解して判定を行う必要がある点に注意して下さい。

このことは、y 座標と上下の辺に関しても同様です。

従って、is_inside は下記のプログラムのように定義できます。

  • 1 行目:仮引数 xy を持つメソッドとして定義する
  • 2 行目:上記の条件を計算する条件式を記述し、その値を return 文で返す
def is_inside(self, x, y):
    return self.x <= x < self.x + self.width and self.y <= y < self.y + self.height

Rect.is_inside = is_inside

下記のプログラムで、頂点が (0, 0)、幅が 10 高さが 20 の長方形を表す rect の中に、(5, 5) は 入っているが、(20, 10) は入っていないことが確認できます。興味がある方は、他の座標でもうまく判定できるかを確認してみて下さい。

print(rect.is_inside(5, 5))
print(rect.is_inside(20, 10))

実行結果

True
False

draw_node の修正

次に、Rect クラスを使って、部分木の各ノードの表示範囲を記録する ことにします。そのためには、それぞれの ノードの表示範囲を記録するための属性 が必要になるので、その属性の名前を nodes_by_rect と命名することにします。

nodes_by_rect には、ノードの表示範囲そのノードを対応づける dict を代入 します。具体的には、キーがノードの表示範囲 を表し、キーの値がそのノード を表す dict を代入します。これは、先ほどの children_by_move と同様の考え方 で作られた dict です。

なお、自作のクラスのインスタンスハッシュ可能なオブジェクト2なので、Rect クラスのインスタンスを dict のキーとして利用することができます。

ノード は、Node クラスの draw_node で表示する ので、draw_node の処理を行う際に表示範囲を知る ことができます。ただし、draw_node は、ゲーム木の視覚化を行う 以外の場合でも呼び出して利用できる ので、draw_node の中 でゲーム木のノードの表示範囲を表す nodes_by_rect 属性の値を変更するべきではありません。そこで、draw_node の外で表示範囲を利用できる ようにするために、下記のプログラムのように、draw_node が描画したノードの 表示範囲を表す Rect を返り値として返す ように修正することにします。

  • 7 行目:ゲーム盤を描画した後で、ゲーム盤の描画範囲を表す Rect クラスのインスタンスを作成し、rect に代入する
  • 11 行目:上記の rect を返り値として返すようにする
 1  from marubatsu import Marubatsu_GUI
 2
 3  def draw_node(self, ax=None, maxdepth=None, emphasize=False, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
 4      # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
 5      y = dy + (self.height - 3) / 2
 6      Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, emphasize=emphasize, lw=lw, dx=dx, dy=y)
 7      rect = Rect(dx, y, 3, 3)
元と同じなので省略           
 8          else:
 9              plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
10
11      return rect
12
13  Node.draw_node = draw_node
行番号のないプログラム
from marubatsu import Marubatsu_GUI

def draw_node(self, ax=None, maxdepth=None, emphasize=False, size=0.25, lw=0.8, dx=0, dy=0):
    width = 8
    if ax is None:
        height = len(self.children) * 4
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")
        for childnode in self.children:
            childnode.height = 4
        self.height = height
            
    # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
    y = dy + (self.height - 3) / 2
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, emphasize=emphasize, lw=lw, dx=dx, dy=y)
    rect = Rect(dx, y, 3, 3)
    # 子ノードが存在する場合に、エッジの線と子ノードを描画する
    if len(self.children) > 0:
        if maxdepth != self.depth:   
            plt.plot([dx + 3.5, dx + 4], [y + 1.5, y + 1.5], c="k", lw=lw)
            prevy = None
            for childnode in self.children:
                childnodey = dy + (childnode.height - 3) / 2
                if maxdepth is None:
                    Marubatsu_GUI.draw_board(ax, childnode.mb, show_result=True, dx=dx+5, dy=childnodey, lw=lw)
                edgey = childnodey + 1.5
                plt.plot([dx + 4 , dx + 4.5], [edgey, edgey], c="k", lw=lw)
                if prevy is not None:
                    plt.plot([dx + 4 , dx + 4], [prevy, edgey], c="k", lw=lw)
                prevy = edgey
                dy += childnode.height
        else:
            plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)
            
    return rect

Node.draw_node = draw_node
修正箇所
from marubatsu import Marubatsu_GUI

def draw_node(self, ax=None, maxdepth=None, emphasize=False, size=0.25, lw=0.8, dx=0, dy=0):
元と同じなので省略
    # 自分自身のノードを真ん中の位置になるように (dx, dy) からずらして描画する
    y = dy + (self.height - 3) / 2
    Marubatsu_GUI.draw_board(ax, self.mb, show_result=True, emphasize=emphasize, lw=lw, dx=dx, dy=y)
+   rect = Rect(dx, y, 3, 3)
元と同じなので省略           
        else:
            plt.plot([dx + 3.5, dx + 4.5], [y + 1.5, y + 1.5], c="k", lw=lw)

+   return rect

Node.draw_node = draw_node

7 行目を rect = Rect(dx, dy, 3, 3) としないように注意して下さい。筆者は最初はそのように記述してしまい、後で間違いに気づきました。

draw_subtree の修正

部分木は、draw_subtree メソッドで描画するので、draw_subtree を下記のプログラムのように修正します。

  • 2 行目:これから部分木を新しく描画するので、nodes_by_rect 属性に空の dict を代入して初期化する
  • 3、5、7、9 行目:ノードの表示範囲を表す draw_node の返り値を rect に代入する
  • 4、6、8、10 行目node_by_rect に代入された dict に、rect をキーとし、そのキーの値に描画したノードを代入する処理を追加する
 1  def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):   
 2      self.nodes_by_rect = {}
元と同じなので省略
 3                  rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
 4                  self.nodes_by_rect[rect] = node
元と同じなので省略
 5                  rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
 6                  self.nodes_by_rect[rect] = sibling
元と同じなので省略
 7          rect = parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
 8          self.nodes_by_rect[rect] = parent
元と同じなので省略
 9              rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
10              self.nodes_by_rect[rect] = node
11            
12  Mbtree.draw_subtree = draw_subtree
行番号のないプログラム
def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):   
    self.nodes_by_rect = {}

    if centernode is None:
        centernode = self.root
    self.calc_node_height(maxdepth)
    width = 5 * (maxdepth + 1)
    height = centernode.height
    parent = centernode.parent
    if parent is not None:
        height += (len(parent.children) - 1) * 4
        parent.height = height
    if ax is None:
        fig, ax = plt.subplots(figsize=(width * size, height * size))
        ax.set_xlim(0, width)
        ax.set_ylim(0, height)   
        ax.invert_yaxis()
        ax.axis("off")        
    
    nodelist = [centernode]
    depth = centernode.depth
    while len(nodelist) > 0 and depth <= maxdepth:        
        dy = 0
        if parent is not None:
            dy = parent.children.index(centernode) * 4
        childnodelist = []
        for node in nodelist:
            if node is None:
                dy += 4
                childnodelist.append(None)
            else:
                dx = 5 * node.depth
                emphasize = node is centernode
                rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = node
                dy += node.height
                if len(node.children) > 0:  
                    childnodelist += node.children
                else:
                    childnodelist.append(None)
        depth += 1
        nodelist = childnodelist
        
    if parent is not None:
        dy = 0
        for sibling in parent.children:
            if sibling is not centernode:
                sibling.height = 4
                dx = 5 * sibling.depth
                rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
                self.nodes_by_rect[rect] = sibling
            dy += sibling.height
        dx = 5 * parent.depth
        rect = parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
        self.nodes_by_rect[rect] = parent
    
        node = parent
        while node.parent is not None:
            node = node.parent
            node.height = height
            dx = 5 * node.depth
            rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, dy=0)
            self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree
修正箇所
def draw_subtree(self, centernode=None, ax=None, size=0.25, lw=0.8, maxdepth=2):   
+   self.nodes_by_rect = {}
元と同じなので省略
-               node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
+               rect = node.draw_node(ax=ax, maxdepth=maxdepth, emphasize=emphasize, size=size, lw=lw, dx=dx, dy=dy)
+               self.nodes_by_rect[rect] = node
元と同じなので省略
-               sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
+               rect = sibling.draw_node(ax, maxdepth=sibling.depth, size=size, lw=lw, dx=dx, dy=dy)
+               self.nodes_by_rect[rect] = sibling
元と同じなので省略
-       parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
+       rect = parent.draw_node(ax, maxdepth=maxdepth, size=size, lw=lw, dx=dx, dy=0)
+       self.nodes_by_rect[rect] = parent
元と同じなので省略
-           node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, y=0)
+           rect = node.draw_node(ax, maxdepth=node.depth, size=size, lw=lw, dx=dx, y=0)
+           self.nodes_by_rect[rect] = node
            
Mbtree.draw_subtree = draw_subtree

draw_subtree メソッドを呼び出すことで、nodes_by_rect 属性に正しく値が代入されるかどうかを確認 することにします。まず、下記のプログラムで、ルートノードを中心とする、深さ 1 までの部分木を draw_subtree メソッドで描画します。

mbtree.draw_subtree(mbtree.root, maxdepth=1)

実行結果

次に、下記のプログラムで、nodes_by_rect のそれぞれの キー と、キーの値が表す ノードの局面表示 します。items()によって、dict のキーとキーの値の両方を繰り返し処理で得ることができます。実行結果から、上図で表示されたノードが表示される長方形の範囲と、対応するノードが正しく nodes_by_rect に記録されていることが確認できます。

for rect, node in mbtree.nodes_by_rect.items():
    print(rect)
    print(node.mb)

実行結果

Rectangle (0, 16.5) width = 3 height = 3
Turn o
...
...
...

Rectangle (5, 0.5) width = 3 height = 3
Turn x
O..
...
...

Rectangle (5, 4.5) width = 3 height = 3
Turn x
.O.
...
...

Rectangle (5, 8.5) width = 3 height = 3
Turn x
..O
...
...

Rectangle (5, 12.5) width = 3 height = 3
Turn x
...
O..
...

Rectangle (5, 16.5) width = 3 height = 3
Turn x
...
.O.
...

Rectangle (5, 20.5) width = 3 height = 3
Turn x
...
..O
...

Rectangle (5, 24.5) width = 3 height = 3
Turn x
...
...
O..

Rectangle (5, 28.5) width = 3 height = 3
Turn x
...
...
.O.

Rectangle (5, 32.5) width = 3 height = 3
Turn x
...
...
..O

create_event_handler の修正

それぞれのノードの表示範囲が記録できたので、create_event_handler を修正して、マウスを押した場所にノードが描画されていた場合に、そのノードに移動する処理を記述します。

具体的には、マウスを押した座標が nodes_by_rect に記録した 長方形の範囲の中にあるか どうかを 順番に調べ中にある事が判明したノードへ移動する という処理を行います。

マウスが Figure の上で押された場合に実行するイベントハンドラと Figure の結び付けは、以前の記事で説明したように、下記のプログラムで行います。

self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)     

従って、create_event_handler を下記のように修正します。

  • 2 行目:マウスを押した時に実行するイベントハンドラを定義する。マウスを押した場所の座標は、event.xdataevent.ydata に代入される
  • 3 行目nodes_by_rect からキーとキーの値を順番に取り出す繰り返し処理を行う
  • 4 ~ 7 行目node の深さが 6 以下で、マウスを押した場所が rect の内部にある場合は、部分木の中心なるノードそのノードをノードに変更し、GUI の描画の更新処理を行う。残りの繰り返し処理を行う必要はないので、break によって繰り返し処理を終了する
  • 11 行目:Figure とマウスを押した時に実行するイベントハンドラを結び付ける
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_mouse_down(event):
 3          for rect, node in self.mbtree.nodes_by_rect.items():
 4              if node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
 5                  self.centernode = node
 6                  self.update_gui()
 7                  break               
 8           
 9      # fig の画像イベントハンドラを結び付ける
10      self.fig.canvas.mpl_connect("key_press_event", on_key_press)
11      self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    
12
13  Mbtree_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    def on_left_button_clicked(b=None):
        if self.centernode.parent is not None:
            self.centernode = self.centernode.parent
            self.update_gui()
            
    def on_right_button_clicked(b=None):
        if self.centernode.depth < 6 and len(self.centernode.children) > 0:
            self.centernode = self.centernode.children[0]
            self.update_gui()

    def on_up_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if index > 0:
                self.centernode = self.centernode.parent.children[index - 1]
                self.update_gui()
            
    def on_down_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if self.centernode.parent.children[-1] is not self.centernode:
                self.centernode = self.centernode.parent.children[index + 1]
                self.update_gui()            
            
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)
    self.up_button.on_click(on_up_button_clicked)
    self.down_button.on_click(on_down_button_clicked)

    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
            "0": on_left_button_clicked,
            "right": on_right_button_clicked,
            "up": on_up_button_clicked,
            "down": on_down_button_clicked,
        }
        if event.key in keymap:
            keymap[event.key]()
        elif self.centernode.depth < 6:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.centernode.children_by_move:
                    self.centernode = self.centernode.children_by_move[move]
                    self.update_gui()
            except:
                pass            
            
    def on_mouse_down(event):
        for rect, node in self.mbtree.nodes_by_rect.items():
            if node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
                self.centernode = node
                self.update_gui()
                break               
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    

Mbtree_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
+   def on_mouse_down(event):
+       for rect, node in self.mbtree.nodes_by_rect.items():
+           if node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
+               self.centernode = node
+               self.update_gui()
+               break               
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)
+   self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    

Mbtree_GUI.create_event_handler = create_event_handler

実行結果は省略しますが、下記のプログラムを実行し、深さが 6 以下のノードの上でマウスを押すと、そのノードを中心とする部分木が表示されることを確認して下さい。

mbtree_gui = Mbtree_GUI(mbtree)

操作説明の表示

Marubatsu_GUI では、? ボタンをクリックした際に、操作説明を表示するようにしましたので、同様のボタンを用意し、操作説明を表示することにします。

create_widgets メソッドの修正

操作説明を表示する Output ウィジェットと、? ボタンのウィジェットを作成する必要があります。下記は、そのように create_widgets メソッドを修正したプログラムです。

  • 2 行目:Output ウィジェットを作成し、output 属性に代入する
  • 7 行目:ヘルプボタンのウィジェットを作成し、help_button 属性に代入する
 1  def create_widgets(self):
 2      self.output = widgets.Output()  
 3      self.left_button = self.create_button("", 50)
 4      self.up_button = self.create_button("", 50)
 5      self.right_button = self.create_button("", 50)
 6      self.down_button = self.create_button("", 50)
 7      self.help_button = self.create_button("", 50)
 8      self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
元と同じなので省略
 9
10  Mbtree_GUI.create_widgets = create_widgets
行番号のないプログラム
def create_widgets(self):
    self.output = widgets.Output()  
    self.left_button = self.create_button("", 50)
    self.up_button = self.create_button("", 50)
    self.right_button = self.create_button("", 50)
    self.down_button = self.create_button("", 50)
    self.help_button = self.create_button("", 50)
    self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
    
    with plt.ioff():
        self.fig = plt.figure(figsize=[self.width * self.size,
                                        self.height * self.size])
        self.ax = self.fig.add_axes([0, 0, 1, 1])
    self.fig.canvas.toolbar_visible = False
    self.fig.canvas.header_visible = False
    self.fig.canvas.footer_visible = False
    self.fig.canvas.resizable = False   
    

Mbtree_GUI.create_widgets = create_widgets
修正箇所
def create_widgets(self):
+   self.output = widgets.Output()
    self.left_button = self.create_button("", 50)
    self.up_button = self.create_button("", 50)
    self.right_button = self.create_button("", 50)
    self.down_button = self.create_button("", 50)
+   self.help_button = self.create_button("", 50)  
    self.label = widgets.Label(value="", layout=widgets.Layout(width=f"50px"))
元と同じなので省略

Mbtree_GUI.create_widgets = create_widgets

display_widgets メソッドの修正

Marubatsu_GUI では、操作説明をゲーム盤の下に表示しましたが、Mbtree_GUI では部分木を描画する Figure が縦にかなり長いので、一番上に操作説明を表示するウィジェットを配置することにします。また、? ボタンは、→ ボタンの右に配置することにします。その際に、→ ボタンと少し離して表示するために、間に空白のラベルを表示することにします。下記はそのように display_widgets メソッドを修正したプログラムです。

  • 4 行目:→ ボタンの右に、空白のラベルと ? ボタンを配置する
  • 6 行目:VBox の先頭に Output ウィジェットを配置するようにする
1  def display_widgets(self):   
2      hbox1 = widgets.HBox([self.label, self.up_button, self.label])
3      hbox2 = widgets.HBox([self.left_button, self.label, self.right_button,
4                            self.label, self.help_button])
5      hbox3 = widgets.HBox([self.label, self.down_button, self.label])
6      display(widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas]))  
7    
8  Mbtree_GUI.display_widgets = display_widgets
行番号のないプログラム
def display_widgets(self):   
    hbox1 = widgets.HBox([self.label, self.up_button, self.label])
    hbox2 = widgets.HBox([self.left_button, self.label, self.right_button],
                          self.label, self.help_button)
    hbox3 = widgets.HBox([self.label, self.down_button, self.label])
    display(widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas]))  
    
Mbtree_GUI.display_widgets = display_widgets
修正箇所
def display_widgets(self):   
    hbox1 = widgets.HBox([self.label, self.up_button, self.label])
-   hbox2 = widgets.HBox([self.left_button, self.label, self.right_button])
+   hbox2 = widgets.HBox([self.left_button, self.label, self.right_button],
+                         self.label, self.help_button)
    hbox3 = widgets.HBox([self.label, self.down_button, self.label])
-   display(widgets.VBox([hbox1, hbox2, hbox3, self.fig.canvas]))  
+   display(widgets.VBox([self.output, hbox1, hbox2, hbox3, self.fig.canvas]))  
    
Mbtree_GUI.display_widgets = display_widgets

上記の修正後に下記のプログラムを実行すると、実行結果のように ? ボタンが表示されるようになります。なお、実行結果は上半分だけを表示します。

mbtree_gui = Mbtree_GUI(mbtree)

実行結果

create_event_handler の修正

次に、create_event_handler を下記のプログラムのように修正することで、? ボタンをクリックした際に操作説明が表示されるようにします。

  • 2 ~ 15 行目:? ボタンをクリックした際に実行するイベントハンドラを定義する
  • 3 ~ 15 行目with self.outputのブロックの中に print を記述することで、Output ウィジェット内にメッセージを表示する。なお、? ボタンを何度もクリックした際に、操作説明が何度も表示されないようにするために、3 行目で Output の表示をクリアする
  • 21 行目:? ボタンとイベントハンドラを結び付ける
 1  def create_event_handler(self):
元と同じなので省略
 2      def on_help_button_clicked(b=None):
 3         self.output.clear_output()
 4         with self.output:
 5             print("""操作説明
 6
 7  下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
 8  、0 キー:親ノードへ移動
 9  ↑:一つ前の兄弟ノードへ移動
10  ↓:一つ後の兄弟ノードへ移動
11  →:先頭の子ノードへ移動
12
13  テンキーで、対応するマスに着手が行われた子ノードへ移動する
14  ノードの上でマウスを押すことでそのノードへ移動する
15  """)
16                           
17      self.left_button.on_click(on_left_button_clicked)
18      self.right_button.on_click(on_right_button_clicked)
19      self.up_button.on_click(on_up_button_clicked)
20      self.down_button.on_click(on_down_button_clicked)
21      self.help_button.on_click(on_help_button_clicked)
元と同じなので省略
22
23  Mbtree_GUI.create_event_handler = create_event_handler
行番号のないプログラム
def create_event_handler(self):
    def on_left_button_clicked(b=None):
        if self.centernode.parent is not None:
            self.centernode = self.centernode.parent
            self.update_gui()
            
    def on_right_button_clicked(b=None):
        if self.centernode.depth < 6 and len(self.centernode.children) > 0:
            self.centernode = self.centernode.children[0]
            self.update_gui()

    def on_up_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if index > 0:
                self.centernode = self.centernode.parent.children[index - 1]
                self.update_gui()
            
    def on_down_button_clicked(b=None):
        if self.centernode.parent is not None:
            index = self.centernode.parent.children.index(self.centernode)
            if self.centernode.parent.children[-1] is not self.centernode:
                self.centernode = self.centernode.parent.children[index + 1]
                self.update_gui()            
                
    def on_help_button_clicked(b=None):
        self.output.clear_output()
        with self.output:
            print("""操作説明

下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
←、0 キー:親ノードへ移動
↑:一つ前の兄弟ノードへ移動
↓:一つ後の兄弟ノードへ移動
→:先頭の子ノードへ移動

テンキーで、対応するマスに着手が行われた子ノードへ移動する
ノードの上でマウスを押すことでそのノードへ移動する
""")
                           
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)
    self.up_button.on_click(on_up_button_clicked)
    self.down_button.on_click(on_down_button_clicked)
    self.help_button.on_click(on_help_button_clicked)

    def on_key_press(event):
        keymap = {
            "left": on_left_button_clicked,
            "0": on_left_button_clicked,
            "right": on_right_button_clicked,
            "up": on_up_button_clicked,
            "down": on_down_button_clicked,
        }
        if event.key in keymap:
            keymap[event.key]()
        elif self.centernode.depth < 6:
            try:
                num = int(event.key) - 1
                x = num % 3
                y = 2 - (num // 3)
                move = (x, y)
                if move in self.centernode.children_by_move:
                    self.centernode = self.centernode.children_by_move[move]
                    self.update_gui()
            except:
                pass            
            
    def on_mouse_down(event):
        for rect, node in self.mbtree.nodes_by_rect.items():
            if node.depth <= 6 and rect.is_inside(event.xdata, event.ydata):
                self.centernode = node
                self.update_gui()
                break               
            
    # fig の画像イベントハンドラを結び付ける
    self.fig.canvas.mpl_connect("key_press_event", on_key_press)
    self.fig.canvas.mpl_connect("button_press_event", on_mouse_down)    

Mbtree_GUI.create_event_handler = create_event_handler
修正箇所
def create_event_handler(self):
元と同じなので省略
+   def on_help_button_clicked(b=None):
+       self.output.clear_output()
+       with self.output:
+           print("""操作説明
+
+下記のキーとボタンで中心となるノードを移動できる。ただし、深さが 7 以上のノードへは移動できない
+、0 キー:親ノードへ移動
+↑:一つ前の兄弟ノードへ移動
+↓:一つ後の兄弟ノードへ移動
+→:先頭の子ノードへ移動
+
+テンキーで、対応するマスに着手が行われた子ノードへ移動する
+ノードの上でマウスを押すことでそのノードへ移動する
+""")
                           
    self.left_button.on_click(on_left_button_clicked)
    self.right_button.on_click(on_right_button_clicked)
    self.up_button.on_click(on_up_button_clicked)
    self.down_button.on_click(on_down_button_clicked)
+   self.help_button.on_click(on_help_button_clicked)
元と同じなので省略

Mbtree_GUI.create_event_handler = create_event_handler

上記の修正後に下記のプログラムを実行し、? ボタンをクリックすると実行結果のように上部に操作説明が表示されるようになります。なお、実行結果は上半分だけを表示します。

mbtree_gui = Mbtree_GUI(mbtree)

本記事では採用しませんが、? ボタンで操作説明の表示の ON/OFF の切り替えを行えるようにしたい場合は、下記のプログラムのように Mbtree_GUI クラスの __init__ メソッド内で、操作説明を表示するかどうかを表す仮引数 show_instructionFalse を代入します。

class Mbtree_GUI(GUI):
    def __init__(self, mbtree, size=0.15):   
        self.mbtree = mbtree
        self.show_instruction = False
        self.size = size
        self.width = 50
        self.height = 64
        self.centernode = self.mbtree.root
        super().__init__()

修正箇所
class Mbtree_GUI(GUI):
-   def __init__(self, mbtree, size=0.15):   
+   def __init__(self, mbtree, size=0.15):   
        self.mbtree = mbtree
+       self.show_instruction = False
        self.size = size
        self.width = 50
        self.height = 64
        self.centernode = self.mbtree.root
        super().__init__()

イベントハンドラのon_help_button_clicked では、下記のプログラムの 3 行目のように、show_instruction 属性の値を not 演算子で反転し、4 行目で True の場合のみ操作説明を表示するように修正します。

1  def on_help_button_clicked(b=None):
2      self.output.clear_output()
3      self.show_instruction = not self.show_instruction
4      if self.show_instruction:
5          with self.output:
6              print("""操作説明
元と同じなので省略
修正箇所
def on_help_button_clicked(b=None):
    self.output.clear_output()
+   self.show_instruction = not self.show_instruction
+   if self.show_instruction:
+       with self.output:
+           print("""操作説明
元と同じなので省略

他には、操作説明を削除するボタンを別に用意するという方法も考えられます。

今回の記事のまとめ

今回の記事では、テンキーとマウスの操作によって、より柔軟にノードを移動できるように修正しました。また、? ボタンを配置して操作説明を表示できるように修正しました。

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

リンク 説明
marubatsu.ipynb 本記事で入力して実行した JupyterLab のファイル
tree_new.py 今回の記事で更新した tree.py

次回の記事

  1. 正方形は、長方形の中の特別な形状です

  2. オブジェクトの ID がハッシュ値になります

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1