はじめに
PySimpleGUIは、Python用のGUIライブラリです。
このライブラリを使用したサンプルとして、スライディングパズル(15パズル)を作成しました。Python の GUI プログラミングの参考になればと思います。
インストールと実行方法
ダウンロード
ソースは、https://github.com/tostos5963/PySimpleGUI-Sliding15Puzzle で公開していますので以下のどちらかの手順でダウンロードしてください。
git clone
git clone https://github.com/tostos5963/PySimpleGUI-Sliding15Puzzle.git
URL部は github のサイトで、①[Code]押下 → ②URL コピー を押下したらクリップボードにコピーできます。
ZIPファイル
①[Code]押下 → ② [Download ZIP]押下で、ZIPファイルをダウンロードできます。ダウンロードしたら任意のフォルダに展開してください。
Windowsの場合
pip install pysimplegui
python Sliding15Puzzle.py
Linux (ubuntu)の場合
pip3 install pysimplegui
python3 Sliding15Puzzle.py
以下のエラーが出た場合
ModuleNotFoundError: No module named 'tkinter'
python3-tk をインストールしてください
sudo apt-get install python3-tk
Macの場合
すみません。動作すると思いますがMacを所持していないため確認できていません。
ソースコード
主な処理の説明です。
初期処理
class Sliding15Puzzle():
def __init__(self):
# 0 - 15 をシャッフル
self.board = [(no+1) for no in range(15)]
self.board.append(0)
self.shuffle()
# 数字ボタンを作成し、二次元配列(4行 x 4列)に保持
self.no_buttons = []
for row in range(4):
row_btns = []
for col in range(4):
# ボタンの数字を取得(シャッフルした board の値を取り出す)
no = self.board[row * 4 + col]
# 数字ボタンを作成。キーはタプル(行,列)
no_button = sg.Button(image_filename = self.image_fname(no), key=(row, col))
# 作成したボタンを1行用のリストに追加
row_btns.append(no_button)
# 1行の数字ボタン(4つ)を、二次元配列(4 x 4)に追加
self.no_buttons.append(row_btns)
# フレーム(数字ボタン(4 x 4))と「終了」ボタンでレイアウト作成
self.layout = [ [sg.Frame('', self.no_buttons)] ] + [ [sg.Button('終了', expand_x=True)] ]
# ウィンドウ作成
self.window = sg.Window('スライディングパズル', self.layout)
images は、ボタンに表示する画像ファイル名のリストです。スペースボタン用と、1~15の数字ボタン用の画像ファイル名を設定しています。
board は、0~15 の数を保持しているリストです。0はスペース、1~15は各数字ボタンに対応します。リスト作成後、後述する shuffle() を呼び出します。
no_buttons は、PySimpleGUI の Button を保持する二次元配列(4 x 4)です。スペースボタンと 1~15の数字ボタンをboardの順番(シャッフルした順番)で作成し保持しています。Buttonの Key はボタン位置(行, 列)にしています。
layout はウィンドウに表示する エレメント(ウィジェット)のリストです。Frame(no_buttons) と 終了ボタンを保持してします。
window は、PySimpleGUIのWindow です。タイトル名とlayoutを設定しています。
シャッフル
def shuffle(self):
random.seed()
# 0=スペースのidxを取得
sp_idx = -1
for idx in range(len(self.board)):
if self.board[idx] == 0:
sp_idx = idx
break
if sp_idx == -1:
return
# 「スペース」ボタンの(行, 列)
sp_row, sp_col = divmod(sp_idx, 4)
# 「スペース」を上下左右に動かす(移動回数はランダムに決める)
n_loops = random.randrange(200, 300)
for n in range(n_loops):
target_idx = -1
while target_idx == -1:
# 移動方向(上下左右)をランダムに決める
direction = random.randrange(0, 99) % 4
target_row = sp_row
target_col = sp_col
if direction == 0: # 上
if sp_row >= 1:
target_row = sp_row - 1
target_idx = target_row * 4 + target_col
elif direction == 1: # 下
if sp_row < 3:
target_row = sp_row + 1
target_idx = target_row * 4 + target_col
elif direction == 2: # 左
if sp_col >= 1:
target_col = sp_col - 1
target_idx = target_row * 4 + target_col
elif direction == 3: # 右
if sp_col < 3:
target_col = sp_col + 1
target_idx = target_row * 4 + target_col
if target_idx != -1:
# 決定した方向(上下左右)に「スペース」ボタン動かすことが可能なら動かす
self.board[sp_idx] = self.board[target_idx]
self.board[target_idx] = 0
sp_row = target_row
sp_col = target_col
sp_idx = sp_row * 4 + sp_col
boardに保持している 0~15の数字をシャッフルする処理です。単純にシャッフルすると、パズルが解けない配置になることがあるため、「スペース」を上下左右にランダムに動かしてシャッフルします。
「スペース」を動かす回数は、200回~300回の範囲からランダムに決めています。
メインループ
def main_loop(self):
while True: # Event Loop
event, values = self.window.read()
if event in (None, '終了'):
# 「終了」ボタンが押下されたらループを抜ける
break
# イベントがタプルであれば数字ボタンが押下されたと判断する
if type(event) is tuple:
self.touch(event)
# アプリ終了
self.window.close()
event, values = self.window.read() でイベントを読み取ります。
イベントが ‘終了’ だったらメインループを抜けてアプリを終了します。
イベントの型がタプルだったら数字orスペースボタンが押下された(イベントが(行, 列))と判断して、後述する touch() を呼び出します。
数字ボタン押下時に呼び出される処理
# クリックした数字ボタンに隣接する「スペース」があれば入れ替え
def touch(self, pos):
no = self.pos2no(pos)
if no == 0:
# 「スペース」ボタン押下
return
# クリックした数字ボタンを「上」に移動
if self.up_swap(pos):
pass
# クリックした数字ボタンを「下」に移動
elif self.down_swap(pos):
pass
# クリックした数字ボタンを「左」に移動
elif self.left_swap(pos):
pass
# クリックした数字ボタンを「右」に移動
elif self.right_swap(pos):
pass
if self.is_complete():
sg.popup('完成!')
引数posは、押下されたボタンの位置(行, 列)です。
pos2no(pos) はposに表示している数字を取り出す処理です。0だったらスペースボタンと判断して処理を抜けます。
up_swap(pos)は押下された数字ボタンの「上」がスペースだったら、数字ボタンとスペースボタンを入れ替えて結果(戻り値)Trueを返します。スペースでなければ何も行わず結果(戻り値)Falseを返します。
down_swap(pos)=下、left_swap(pos)=左、right_swap(pos)=右 は、方向が違いますが処理内容はup_swap(pos)と同じです。
is_complete()は、数字ボタンが1~15まで並んでいるかチェックします。並んでいたらポップアップで「完成!」を表示します。
「スペース」と「スペースに隣接する数字ボタン」を入れ替える処理
# 「スペース」と「スペースに隣接する数字ボタン」を入れ替え
def swap(self, my_pos, target_pos):
ret = False
sp_no = self.pos2no(target_pos)
if sp_no == 0:
sp_idx = self.pos2idx(target_pos)
my_idx = self.pos2idx(my_pos)
self.board[sp_idx] = self.board[my_idx]
self.board[my_idx] = 0
if self.window != None:
self.window[my_pos].Update(image_filename = self.image_fname(0))
self.window[target_pos].Update(image_filename = self.image_fname(self.board[sp_idx]))
ret = True
return ret
引数my_posは 押下した数字ボタンの位置(行, 列)です。引数target_posは隣接(上下左右)ボタンの位置(行, 列)です。
pos2no(target_pos)は、隣接(上下左右)ボタンの数字です。0(スペース)でなければ処理を抜けます。
pos2idx(pos) は位置(行, 列)から boardのインデックス(0~15)に変換します。変換したインデックスを使用して board の数字を入れ替えます。
window[KEY]は window内のエレメント(ウィジェット)の内、KEYが一致するものを返します。各ボタンのKEYは、初期化処理でボタン位置(行, 列)にしているため、my_pos と target_pos を渡せば対象の Button を取得できます。
取得したボタンの画像(数字、スペース)をUpdate()で入れ替えます。
まとめ
今回は、PySimpleGUIを使って、スライディングパズル(15パズル)を作成しました。Python用のGUIライブラリはいろいろありますが、その中でも PySimpleGUIはシンプルで使いやすいように感じました。
コメント