このページではピクロス(お絵かきロジックゲーム)を Python の tkinter を使って作成する方法について解説していきます。
このページで紹介するスクリプトは下記環境で動作を確認しています
- OS:macOS Big Sur
- Python:3.9.4
- Tkinter:8.6
ピクロス(お絵かきロジック)とは
ピクロスのゲーム画面は下の図のようなものになります。
縦軸と横軸に複数の数字が記載されており、この数字をヒントにマスを塗りつぶしていきます。
この縦軸と横軸の数字は、その列もしくはその行のマスその数字の分連続して塗りつぶすことを意味しています。
複数の数字がある場合は、連続して塗りつぶす領域が離れていることを意味しています。
縦軸と横軸に記載された数字すべての分のマスを塗りつぶせば絵が出来上がります(星の絵のつもり…)。
これでゲームクリアです。
各マスは塗りつぶすだけでなく、×マークをつけることもできます。
「このマスは塗りつぶさないだろう」と思うマスを×マークを付けておくことで目印にしてゲームを解いていきます。
ピクロス(お絵かきロジック)の作り方の考え方
ではこのピクロスはどのようにして作れば良いのか?この作り方の考え方について説明したいと思います。
作り方は色々あると思いますが、今回は画像からピクロスを作成することを考えたいと思います。
まず、ピクロスのクリア時の画面について考えてみましょう。出来上がるのは下の図のような絵ですね。
各マスは塗りつぶしてるマスと塗りつぶしてないマスの二つに分けられます。
塗りつぶしてるマスを黒色、塗りつぶしてないマスを白色と考えれば、これは白黒画像と考えることができます。
横方向については白黒の画像の横方向のピクセル数(画像の幅)、縦方向については白黒の画像の縦方向のピクセル数(画像の高さ)と考えれば、画像の各ピクセルがマスを表すと考えられます。
次に縦軸と横軸の数字について考えてみましょう。
これらの数字は各行・各列で連続して塗りつぶすマスの数を羅列したものになります。
塗りつぶすマスを黒色のピクセルと考えれば、黒色のピクセルがいくつ連続しているかを羅列したものになると考えられます。
なので、この縦軸と横軸の数字は、白黒画像の黒色のピクセルを数えてやることで求めることができます。
こんな感じで塗りつぶして出来上がる絵を白黒画像と考えれば、画像からお絵かきロジックを作成するために必要な情報を取得することができます。
後はアプリ上にマスを配置したり、マスがクリックされた時にマスを塗りつぶすような処理を実装してやればお絵かきロジックの出来上がりです。
具体的には下記のような手順でお絵かきロジックを作成することができます。
- 画像を読み込む
- 画像を白黒化する
- 画像の各行・各列の黒色ピクセルを数える
- ウィジェットの作成と配置を行う
- 画像のサイズ分のマスを配置する
- 縦軸横軸に塗りつぶすマス数を表示する
- マスに対するマウス操作を受け付ける
細かく言えばもうちょっとやることはありますが、基本的に上記のことを行うことでお絵かきロジックを作成することができます。
ここまで「ピクセル」という言葉を当たり前のように使ってしまいましたが、ピクセルについては下記ページで解説していますので、画像やピクセルについて詳しく知りたい方は是非読んでみてください。
画像データの構造・画素・ビットマップデータについて解説スポンサーリンク
ピクロスのサンプルスクリプト
それでは私が作成したサンプルスクリプトを紹介したいと思います。
サンプルスクリプト
ここまで説明してきた考え方に基づいて作成したサンプルスクリプトが下記のようになります。
(2021/10/25 追記)
環境によっては、右クリックされたイベントの設定時、bind
メソッドの第1引数に "<ButtonPress-3>"
を指定する必要がある場合があるようです。
set_events
メソッドでイベントの設定を行なっていますので、右クリックが反応しない場合、右クリック時のイベント設定を行なっている bind
メソッドの第1引数の部分を "<ButtonPress-2>"
から "<ButtonPress-3>"
に変更して試してみてください。
import tkinter
import cv2
# 読み込ませる画像のファイルパス
FILE_PATH = "face.png"
# 画像のサイズ
IMAGE_WIDTH = 32
IMAGE_HEIGHT = 32
# 色設定
DRAW_COLOR = "#000000"
NO_DRAW_COLOR = "#FFFFFF"
# 印を表すテキスト
MARK_TEXT = "X"
# フォント
FONT = ("", 10)
# 白黒化時の閾値
THRESHOLD = 100
class Picross():
def __init__(self, app, file_path):
# メインウィンドウ
self.app = app
# ピクロスの元になる画像ファイルへのパス
self.file_path = file_path
# 読み込んだ画像オブジェクト
self.load_image = None
# 白黒化した画像オブジェクト
self.image = None
# 画像のサイズ
self.iamge_width = 0
self.image_height = 0
# 各行に対して塗り潰すマス数のリスト
self.row = []
# 各列に対して塗り潰すマス数のリスト
self.column = []
# 画像を読み込む
self.readImage()
# 画像を白黒化する
self.createBinaryImage()
# 塗りつぶすマス数を取得する
self.getRowPixels()
self.getColumnPixels()
# ウィジェットを作成する
self.createWidgets()
# イベント処理を設定する
self.setEvents()
def readImage(self):
'画像を読み込む'
if len(self.file_path) != 0:
# ファイルを開いて読み込む
image = cv2.imread(self.file_path)
# 画像のリサイズ
self.load_image = cv2.resize(
image,
(IMAGE_WIDTH, IMAGE_HEIGHT),
interpolation=cv2.INTER_NEAREST
)
def createBinaryImage(self):
'画像を白黒化する'
# 画像をグレースケール化
gray_image = cv2.cvtColor(self.load_image, cv2.COLOR_BGR2GRAY)
# 画像を白黒化(閾値は自動設定)
ret, self.image = cv2.threshold(
gray_image, THRESHOLD, 255,
cv2.THRESH_BINARY
)
# 画像のサイズを取得
self.image_width = self.image.shape[1]
self.image_height = self.image.shape[0]
def getRowPixels(self):
'各行の塗りつぶすマス数を取得'
# 各行に対するループ
for j in range(self.image_height):
# 行単位でリストを作成
row_list = []
# 行単位でカウント等を初期化
before_pixel = None
count = 0
# 行内に対するループ
for i in range(self.image_width):
# ピクセルの色を取得
pixel = self.image[j, i]
# 塗り潰すべきマスがいくつ連続しているかをカウント
if pixel == 0:
count += 1
else:
# 塗り潰すべきマスが途切れた場合
if before_pixel == 0:
# リストにカウントした数を追加
row_list.append(count)
count = 0
# 区切りが分かるように全角スペースも追加
row_list.append(' ')
# 前のピクセルの情報を覚えておく
before_pixel = pixel
# 1行分カウントが終わったらリストに最後に追加
if count != 0:
row_list.append(count)
# 1行分のリストを全行分管理するリストに追加
self.row.append(row_list)
def getColumnPixels(self):
'各行の塗りつぶすマス数を取得'
# 各列に対するループ
for i in range(self.image_width):
# 列単位でリストを作成
column_list = []
# 列単位でカウント等を初期化
before_pixel = None
count = 0
# 列内に対するループ
for j in range(self.image_height):
# ピクセルの色を取得
pixel = self.image[j, i]
# 塗り潰すべきマスがいくつ連続しているかをカウント
if pixel == 0:
count += 1
else:
# 塗り潰すべきマスが途切れた場合
if before_pixel == 0:
# リストにカウントした数を追加
column_list.append(count)
count = 0
# 区切りが分かるように全角スペースも追加
column_list.append(' ')
# 前のピクセルの情報を覚えておく
before_pixel = pixel
# 1列分カウントが終わったらリストに最後に追加
if count != 0:
column_list.append(count)
# 1列分のリストを全列分管理するリストに追加
self.column.append(column_list)
def createWidgets(self):
'各種ウィジェットを作成・配置するメソッド'
# 左上のフレームを作成・配置
self.frame_UL = tkinter.Frame(
self.app,
)
self.frame_UL.grid(column=0, row=0)
# 右上のフレームを作成・配置
self.frame_UR = tkinter.Frame(
self.app,
)
self.frame_UR.grid(column=1, row=0)
# 左下のフレームを作成・配置
self.frame_BL = tkinter.Frame(
self.app,
)
self.frame_BL.grid(column=0, row=1)
# 右下のフレームを作成・配置
self.frame_BR = tkinter.Frame(
self.app,
)
self.frame_BR.grid(column=1, row=1)
# マスを右下のフレーム上に作成
self.createSquares(self.frame_BR)
# 縦軸を左下のフレーム上に作成
self.createVtclAxis(self.frame_BL)
# 横軸を右上のフレーム上に作成
self.createHztlAxis(self.frame_UR)
# ボタンを左上のフレームに作成
self.createButtons(self.frame_UL)
def createButtons(self, master):
'ボタンを作成'
# 解答表示用のボタンの作成・配置
self.button_answer = tkinter.Button(
master,
text="解答表示",
command=self.drawAnswer
)
self.button_answer.pack()
def createSquares(self, master):
'マスと見立てたラベルを作成'
for j in range(self.image_height):
for i in range(self.image_width):
# ラベルウィジェットを作成
label = tkinter.Label(
master,
width=2,
height=1,
bg=NO_DRAW_COLOR,
relief=tkinter.SUNKEN,
font=FONT
)
# ラベルを配置
label.grid(column=i, row=j)
def createVtclAxis(self, master):
'縦軸に各行の塗りつぶすマス数を記載したラベルを作成'
for j in range(self.image_height):
text = tkinter.Label(
master,
text=self.row[j],
height=1,
font=FONT
)
# 上方向から順にパック
text.pack(side=tkinter.TOP)
def createHztlAxis(self, master):
'横軸に各列の塗りつぶすマス数を記載したラベルを作成'
for i in range(self.image_width):
text = tkinter.Label(
master,
text=self.column[i],
wraplength=1, # 1文字で改行
width=2,
font=FONT
)
# 左方向から順にパック
text.pack(side=tkinter.LEFT)
def setEvents(self):
'各種イベントを設定するメソッド'
# 全ラベルに対してイベントを設定
for j in range(self.image_height):
for i in range(self.image_width):
# gridへの配置場所からウィジェット取得
widgets = self.frame_BR.grid_slaves(column=i, row=j)
label = widgets[0]
# 左クリック時のイベント設定
label.bind("<ButtonPress-1>", self.draw)
# 右クリック時のイベント設定
label.bind("<ButtonPress-2>", self.mark)
# Shiftキー押しながらマウスインした時のイベント設定
label.bind("<Shift-Enter>", self.multiDraw)
# Ctrlキー押しながらマウスインした時のイベント設定
label.bind("<Control-Enter>", self.multiMark)
def draw(self, event):
'''
ラベルを左クリックされた時に
マスに色を塗るorマスの色を元に戻すメソッド
'''
# クリックされたラベルを取得
label = event.widget
if label.cget("text") == MARK_TEXT:
# 既にマークがつけられている場合は塗りつぶさない
return
if label.cget("bg") == NO_DRAW_COLOR:
# まだ塗りつぶされていない場合は塗り潰す
label.config(
bg=DRAW_COLOR,
)
else:
# 既に塗りつぶされている場合は元に戻す
label.config(
bg=NO_DRAW_COLOR,
)
def mark(self, event):
'''
ラベルを右クリックされた時に
マスに印をつけるor元に戻すメソッド
'''
# クリックされたラベルを取得
label = event.widget
if label.cget("bg") == DRAW_COLOR:
# 既に塗りつぶされていればマークつけない
return
if label.cget("text") != MARK_TEXT:
# まだマーク付けられていない場合はマークつける
label.config(
text=MARK_TEXT
)
else:
# 既にマークつけられている場合は元に戻す
label.config(
text=''
)
def multiDraw(self, event):
'Shift押しながらマウスインした場合にマスに色を塗る'
self.draw(event)
def multiMark(self, event):
'Control押しながらマウスインした場合にマスに印つける色'
self.mark(event)
def drawAnswer(self):
'解答を表示する'
for j in range(self.image_height):
for i in range(self.image_width):
# gridへの配置場所からウィジェット取得
widgets = self.frame_BR.grid_slaves(column=i, row=j)
# ピクセルの色からマスにつける色を設定
if self.image[j, i] == 0:
color = DRAW_COLOR
else:
color = NO_DRAW_COLOR
# 答えとなるマスの色を設定
widgets[0].config(
bg=color,
text=""
)
app = tkinter.Tk()
picross = Picross(app, FILE_PATH)
app.mainloop()
事前準備
このスクリプトを実行するためには画像ファイルが必要です。
事前に画像ファイルを用意し、スクリプト先頭付近の FILE_PATH
部分にその画像ファイルへのファイルパスを記載しておいてください。
# 読み込ませる画像のファイルパス
FILE_PATH = "face.png"
絶対パスで記述しても良いですし、スクリプトを実行するフォルダからの相対パスで記述しても大丈夫です。
ちなみに私は「いらすとや」さんの下記画像を、
https://4.bp.blogspot.com/-JBgDcjRyikY/W3abfK3PZOI/AAAAAAABOB8/32O2k1YsArE2XATq0cr34byg3tk6w3f4gCLcBGAs/s800/necchusyou_face_boy1.png
顔部分のみを切り抜きした画像を使用して動作確認しました。
画像を小さくリサイズする関係で、細かい描画のある画像を使用しても、その細かい描画が消えてしまう可能性が高いです
また画像を白黒化する関係で、特にグラデーションのように色の変化の激しい画像を使うと塗り潰し結果で表示される画像が意図しないものになる可能性が高いです
色合いや形が単純な画像を選んだ方が、より意図通りのピクロスを作成できると思います
またアルファチャンネル付きの画像もモノクロ化 or 白黒化の時に結果が意図した結果にならないことがありますので、アルファチャンネルなしの画像で試すことをオススメします
スポンサーリンク
実行結果
例えば、上記の顔の画像ファイルを FILE_PATH
に設定した状態でスクリプトを実行すると、下のような画面が表示されます。
マスを左クリックすれば、そのマスが黒色に塗りつぶされます。
黒色のマスを再度左クリックすれば、色が白に戻って塗り潰していない状態に戻ります。
塗り潰していないマスを右クリックすれば、そのマスに「X」マークを付けることができます。
コレも再度右クリックすることで印を消して元の状態に戻すことができます。
また「Shift キー」を押しながらマウスインさせたマスを連続して塗りつぶすことができます(既に塗りつぶされている場合は元に戻ります)。
また「Ctrl キー」を押しながらマウスインさせたマスに連続して印をつけることができます(既に印がついている場合は元に戻ります)。
これらのマウス操作により、マスを塗り潰して隠れた絵を浮かび上がらせるというピクロスゲームをプレイすることができます
また、「解答表示」ボタンをクリックすれば、解答を表示できるようにもしています。このボタンを使えば、読み込ませた画像がどのようにピクロスの絵として作成されたのかをすぐに確認できます。
スクリプトの設定
スクリプトの最初の部分で、スクリプトの大まかな設定をできるようにしています。
FILE_PATH
:- 読み込む画像へのファイルパス設定
IMAGE_WIDTH
:- 作成する白黒画像の横方向のピクセル数字
- これが横方向のマス数になります
IMAGE_HEIGHT
:- 作成する白黒画像の縦方向のピクセル数字
- これが縦方向のマス数になります
DRAW_COLOR
:- 塗り潰したマスの色
NO_DRAW_COLOR
:- 塗り潰していないマスの色
FONT
:- フォントの指定
- フォントサイズ(第2要素の数字)を変更することで縦軸横軸の文字の色とマスのサイズが連動して変化します
THRESHOLD
:- 白黒化時に用いる閾値
IMAGE_WIDTH
や IMAGE_HEIGHT
を大きくすれば、塗り潰して出てくる絵もきれいなものになりますが、その分画面内にマスが表示しきれない&アプリの動作が重くなるという問題も起こりやすいので気をつけてください。
サンプルスクリプトの解説
最後に、サンプルスクリプトの解説をします。
サンプルスクリプトでは、Picross
というクラスを作成し、このクラスの __init__
の中で、ピクロスの作り方の考え方で挙げた下記のことを実行しています。括弧内にはそれぞれの処理を行うメソッド名を示しています。
ここでは、これらのメソッドについて解説していきます。
- 画像を読み込む(
readImage
) - 画像を白黒化する(
createBinaryImage
) - 画像の各行・各列の黒色ピクセルを数える(
createRowPixels
・createColumnPixels
) - ウィジェットの作成と配置を行う(
createWidgets
)- 画像のサイズ分のマスを配置する(
createSquares
) - 縦軸横軸に塗りつぶすマス数を表示する(
createVtclAxis
・createHztlAxis
)
- 画像のサイズ分のマスを配置する(
- マスに対するマウス操作を受け付ける(
setEvents
)
他にもボタンを作成したりしていますが、上記以外の部分は基本的に解説を省略させていただきます。
各種メソッドの中ではクラスの属性(例えば self.image
など)を利用していますが、これらの属性の詳細は __init__
の前半でコメントで説明していますので、どのような属性か知りたい場合はこちらを参考にしてください。
では各種メソッドについて解説していきます。
スポンサーリンク
画像を読み込む(readImage
)
まずはピクロスの元になる画像の読み込みを行います。
readImage
メソッドでは OpenCV の imread
関数により画像の読み込みを行っています。
# ファイルを開いて読み込む
image = cv2.imread(self.file_path)
imread
関数の引数に画像ファイルへのパスを指定すれば、その画像を読み込み、OpenCV 用の画像オブジェクトを生成することができます。
さらに、その読み込んだ画像を OpenCV の resize
関数により行っています。
# 画像のリサイズ
self.load_image = cv2.resize(
image,
(IMAGE_WIDTH, IMAGE_HEIGHT),
interpolation=cv2.INTER_NEAREST
)
第1引数でリサイズしたい画像オブジェクト、第2引数でリサイズ後の画像サイズをタプル形式で指定すれば、リサイズ後の画像オブジェクトを生成することができます。
interpolation
ではリサイズ時のアルゴリズムを指定することができ、今回は最近傍補間を選択しています(二値化することを考えるとコレが一番良いと思ったため)。
画像のリサイズやリサイズのアルゴリズムについては下記で解説していますので、興味のある方は是非読んでみてください(C言語向けの解説ですが、どのようなアルゴリズムであるかは理解できると思います!)。
画像の拡大縮小・リサイズの原理、アルゴリズムによる違いを解説!画像を白黒化する(createBinaryImage
)
次に読み込んだ画像を白黒化します。
画像の白黒化は、OpenCV の threshold
関数で行うことができます。
ですが threshold
関数に入力する画像はグレースケールである必要があるため、事前に OpenCV の cvtColor
関数でグレースケールに色変換を行っています。
# 画像をグレースケール化
gray_image = cv2.cvtColor(self.load_image, cv2.COLOR_BGR2GRAY)
グレースケール化後の画像オブジェクトを用いて下記で threshold
関数により、画像の白黒かを行っています。
# 画像を白黒化(閾値は自動設定)
ret, self.image = cv2.threshold(
gray_image, THRESHOLD, 255,
cv2.THRESH_BINARY
)
第1引数に白黒化したいグレースケール画像、第2引数に閾値、第3引数に閾値以上のピクセルの色の値、第4引数に threshold
関数の動作の設定をそれぞれ指定することで、白黒画像オブジェクトを生成することができます。
グレースケール画像というのは、各ピクセルの色が 0 〜 255 の1つの値(輝度値という)で表現される画像になります。
上位の threshold
関数では、閾値である第2引数 THRESHOLD
を超えるピクセルのみの値を 255 (第3引数のあたい)に設定し、それ以外のピクセルの値を 0 に設定する処理が行われます(このように動作するのは第4引数に cv2.THRESH_BINARY
を指定しているため)。
特に重要なのが第2引数の THRESHOLD
の値です。この閾値を変更することで出来上がりの白黒画像が大きく変わります。上手く白黒画像が作れない時は、この THRESHOLD
の値を変更してみてください(スクリプトの先頭部分で変更できるようにしています)。
ちなみに、値が 0 のピクセルが黒色、値が 255 のピクセルが白色を表します。直感的には逆に感じる人もいるかもしれないので注意してください。
画像の各行・各列の黒色ピクセルを数える(createRowPixels
・createColumnPixels
)
白黒画像が出来上がったら、次は黒色ピクセルの数を数えていきます。
OpenCV の画像オブジェクトの座標 (i
, j
) のピクセルの色(輝度値)は下記により取得することができます。
# ピクセルの色を取得
# self.imageはOpenCVの画像オブジェクト
pixel = self.image[j, i]
また画像のピクセルの座標を (i
, j
) とした場合、各ピクセルの座標 (i
, j
) は下の図のように左上を原点として、i
は右方向を正の方向、j
は下方向を正の方向として表されます。
例えば下の図のオレンジの行は j = 7
の行になります。
各行のピクセルの色を調べるのであれば、j
を固定し、i
を 0 〜 画像の幅 -1 までループしてやり、その中で self.image[j, i]
の値を調べてやることで、第 j
行の黒色のピクセルの数を数えることができます。
前述の通り、self.image[j, i]
の値が 0 の場合は黒色、self.image[j, i]
の値が 255 の場合は白色のピクセルになります。
ピクロスの場合は、単純に黒色のピクセルを数えるだけでなく、連続した黒色のピクセルの数を数える必要があるところがポイントです。
self.image[j, i]
が黒色の場合、直前のピクセルの色が黒色の場合は連続して塗りつぶすマス数としてカウントアップすれば良いです。
一方で、直線のピクセルの色が白色の場合は、塗りつぶすマスの新しいグループの始まりであると考える必要があります。
また self.image[j, i]
が白色、かつ、直前のピクセルの色が黒である場合、連続して塗りつぶすマスが終了したことになります。
この辺りを考慮して黒色のピクセルを数え、そのピクセルの数を「行ごと」にリスト self.row
に追加しているのが、createRowPixels
メソッドになります。
さらに、コレと同様のことを各列に対して行っているのが、createColumnPixels
メソッドになります。「列ごと」の塗りつぶすマス数は self.column
に格納しています。
スポンサーリンク
ウィジェットの作成と配置を行う(createWidgets
)
ここまでは言わばピクロス作成のための準備になりす。
ここから tkinter を利用して、GUI アプリとして仕立てていきたいます。
まずはアプリの画面の大枠を Frame
ウィジェットにより作成します。
アプリ上に下図のように四つの Frame
ウィジェットを配置し、右下の Frame
ウィジェットにマスを、左下の Frame
ウィジェットに縦軸として各行の塗りつぶすマス数を、右上の Frame
ウィジェットに横軸として各列の塗りつぶすマス数を表示するようにしていきます。
この Frame
ウィジェットの作成や配置を行なっているのが createWidgets
の前半部分になります。
後半部分では、createWidgets
・createVtclAxis
・createHztlAxis
により、これらの Frame
ウィジェット上にマスや塗りつぶすマス数を配置していきます(createButtons
でボタンも作成していますが、この解説は省略します)。
画像のサイズ分のマスを配置する(createSquares
)
続いてマスを作成し、配置していきます。
画像の横幅 x 画像の高さ分の Label
ウィジェットを作成して配置することでマスを表現します。
これは画像の「横幅 x 画像の高さ」分のループを行い、そのループの中でマスと見立てた Label
ウィジェットの作成と配置を行えば実現できます。
マスは二次元的に配置したいので、ここでは grid
メソッドを利用して配置を行なっています。
これを行なっているのが createSquares
メソッドになります。マスはアプリの右下の位置に配置したかったので、引数にアプリの右下の位置に配置した self.frame_BR
を指定し、そのフレーム上にラベルを配置するようにしています。
ウィジェットの配置については下記で詳しく解説していますので、grid
メソッドがよく分からない方は是非こちらを読んでみてください。
またラベルウィジェットについては下記ページで詳しく説明していますので、tkinter.Label
で指定している設定がどのようなものかを知りたい方はこちらを読んでみてください。
縦軸横軸に塗りつぶすマス数を表示する(createVtclAxis
・createHztlAxis
)
次は縦軸と横軸を作っていきます。
塗りつぶすマス数は基本的に getRowPixels
と getColumnPixel
で作成したリスト self.row
と self.column
の要素をラベル上に表示すれば良いだけです。
ただし、特に横軸に塗りつぶすマス数を表示する createHztlAxis
では、ラベルを左方向から順に右方向に対して順々にラベルを配置する必要がある点に注意が必要です。
このようにラベルを配置するために、createHztlAxis
では下記のように pack
メソッドの side
キーワードに tkinter.LEFT
を指定するようにしています。
# 左方向から順にパック
text.pack(side=tkinter.LEFT)
pack
メソッドについても下記で詳しく解説していますので、興味のある方は是非下記ページも読んでみてください。
スポンサーリンク
マスに対するマウス操作を受け付ける(setEvents
)
最後にマス上でのマウス操作を受け付けるようにイベント処理の設定を行います。
イベント処理自体については下記ページで詳しく説明していますので、イベントがどんなものか分からない方は事前に下記ページを読んでおくことをオススメします。
Tkinterの使い方:イベント処理を行う今回作成するピクロスアプリでマスに対して設定するのは下記の4つになります。
- マウスの左クリック:
- マスを塗りつぶす or 元に戻す(
draw
)
- マスを塗りつぶす or 元に戻す(
- マウスの右クリック:
- マスに印をつける or 元に戻す(
mark
)
- マスに印をつける or 元に戻す(
- Shift キーを押しながらのマウスイン:
- 連続してマスを塗りつぶす or 元に戻す(
multiDraw
)
- 連続してマスを塗りつぶす or 元に戻す(
- Control キーを押しながらのマウスイン:
- 連続してマスに印をつける or 元に戻す(
multiMark
)
- 連続してマスに印をつける or 元に戻す(
マス単位でクリックしなければならないのは操作性が悪いと思ったので、後半の2つを用意し、キーを押しながらマウスを移動させることで連続してマスへの操作が行えるようにしています(本当はマウスクリックされた状態でマウスインされたときに連続してマスに対して操作できるようにしたかったのですが、うまく動作してくれなかったので諦めました…)。
これらのイベント処理の設定を行っているのは setEvents
メソッドの下記になります。
# 左クリック時のイベント設定
label.bind("<ButtonPress-1>", self.draw)
# 右クリック時のイベント設定
label.bind("<ButtonPress-2>", self.mark)
# Shiftキー押しながらマウスインした時のイベント設定
label.bind("<Shift-Enter>", self.multiDraw)
# Ctrlキー押しながらマウスインした時のイベント設定
label.bind("<Control-Enter>", self.multiMark)
bind
メソッドを実行したウィジェットに対して、bind
メソッドの第1引数で指定したイベントシーケンスが発生した際に、第2引数で指定したメソッド(関数でも良い)が実行されるようになります。
例えば1番上の bind
メソッドを実行すれば、bind
メソッドを実行したウィジェットに対して左クリックが実行された際に、自動的に self.draw
メソッドが実行されるようになります。
(2021/10/25 追記)
環境によっては、右クリックされたイベントの設定時、bind
メソッドの第1引数に "<ButtonPress-3>"
を指定する必要がある場合があるようです
右クリックが反応しない場合、bind
メソッドの第1引数の部分を "<ButtonPress-2>"
から "<ButtonPress-3>"
に変更して試してみてください
ポイントは全マスに対して bind
メソッドを実行する必要がある点です。
全マスに対して bind
メソッドを実行できるように、画像の全座標 (i
, j
) に対してループを行い、座標 (i
, j
) に対応するマスのラベルを grid_slaves
メソッドにより取得し、さらにその取得したラベルに対して、上記の4つのイベント処理を設定するようにしています。
grid_slaves
メソッドでラベルを取得しているのは下記部分です。この grid_slaves
メソッドは後からウィジェットを取得するのにめちゃめちゃ便利なので、tkinter で特に grid
を使う方は覚えておいた方が良いと思います!
# gridへの配置場所からウィジェット取得
widgets = self.frame_BR.grid_slaves(column=i, row=j)
label = widgets[0]
以上により、どのマスに対してクリックやマウスインが行われても、各メソッドが実行されるようになります。
イベント発生時に実行されるメソッド draw
・mark
・multiDraw
・multiMark
がどのような処理を行っているかは、スクリプトを読んでみていただければ分かるかなぁと思います(基本的にラベルウィジェットの色やテキストを変更しているだけです)。
まとめ
このページでは「ピクロス(お絵かきロジック)を作成する方法について解説しました!
画像からピクロスを作成することで、自動的に・簡単にピクロスを作成することができます。
ピクロスを作成する具体的な手順は下記の通りです。
- 画像を読み込む
- 画像を白黒化する
- 画像の各行・各列の黒色ピクセルを数える
- ウィジェットの作成と配置を行う
- 画像のサイズ分のマスを配置する
- 縦軸横軸に塗りつぶすマス数を表示する
- マスに対するマウス操作を受け付ける
私はこんな感じでピクロスを作ってみましたが、他にもいろんな作り方もあると思います!また操作性や動作の重さなど、改善点などはたくさんあります。
是非みなさんも Python でのピクロス作成や今回紹介したスクリプトの改良に挑戦してみてください!
オススメ参考書(PR)
簡単なアプリやゲームを作りながら Python について学びたいという方には、下記の Pythonでつくる ゲーム開発 入門講座 がオススメです!ちなみに私が Python を始めるときに最初に買った書籍です!
下記ようなゲームを作成しながら Python の基本が楽しく学べます!素材もダウンロードして利用できるため、作成したゲームの見た目にも満足できると思います。
- すごろく
- おみくじ
- 迷路ゲーム
- 落ち物パズル
- RPG
また本書籍は下記のような構成になっているため、Python 初心者でも内容を理解しやすいです。
- プログラミング・Python の基礎から解説
- 絵を用いた解説が豊富
- ライブラリの使い方から解説(tkitner と Pygame)
- ソースコードの1行1行に注釈
ゲーム開発は楽しくプログラミングを学べるだけでなく、ゲームで学んだことは他の分野のプログラミングにも活かせるものが多いですし(キーボードの入力受付のイベントや定期的な処理・画像や座標を扱い方等)、逆に他の分野のプログラミングで学んだ知識を活かしやすいことも特徴だと思います(例えばコンピュータの動作に機械学習を取り入れるなど)。
プログラミングを学ぶのにゲーム開発は相性抜群だと思います。
Python の基礎や tkinter・Pygame の使い方をご存知なのであれば、下記の 実践編 をいきなり読むのもアリです。
実践編 では「シューティングゲーム」や「アクションゲーム」「3D カーレース」等のより難易度の高いゲームを作りながらプログラミングの力をつけていくことができます!
また、単にゲームを作るのではなく、対戦相手となるコンピュータの動作のアルゴリズムにも興味のある方は下記の「Pythonで作って学べるゲームのアルゴリズム入門」がオススメです。
この本はゲームのコンピュータ(AI)の動作アルゴリズム(思考ルーチン)に対する入門解説本になります。例えばオセロゲームにおけるコンピュータが、どのような思考によって石を置く場所を決めているか等の基本的な知識を得ることが出来ます。
プログラミングを挫折せずに続けていくためには楽しさを味わいながら学習することが大事ですので、特にゲームに興味のある方は、この辺りの参考書と一緒に Python を学んでいくのがオススメです!