目次と前回の記事
これまでに作成したモジュール
以下のリンクから、これまでに作成したモジュールを見ることができます。
リンク | 説明 |
---|---|
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.xdata
と event.ydata
にマスの x、y 座標を計算して代入していますが、今回はそのような処理は必要ありません。
また、4、10 行目の try
と except
は、テンキー以外のキーが押された場合に 5 行目の int(event.key)
の処理でエラーが発生する点に対処するためのものです。忘れた方は、以前の記事を復習して下さい。
従って、create_event_handler
は、下記のプログラムのように修正します。try
、except
の行に関する説明は、上記で説明したので省略します。
- 7 ~ 9 行目:押されたテンキーに対応するマスの x、y 座標を計算する
-
10 行目:着手を表す tuple を計算し、
move
に代入する -
11 行目:
move
がcenternode
のchildren_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 という 長方形を表すクラスを定義 し、そのインスタンスによって表現することにします。
下記は、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 行目:仮引数
x
、y
を持つメソッドとして定義する - 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.xdata
とevent.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_instruction
に False
を代入します。
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 |
次回の記事