morikomorou’s blog

自分が学んだことなどの備忘録的なやつ

【python】matplotlibでGUIアプリ作成(ライフゲーム)


はじめに

以前の記事でmatplotlibでライフゲームを作成してみたりしました。
その際は初期値はランダムでアニメーションを作るだけでした。


今回は前回紹介したボタン等を使って初期値をユーザーが自由に作れるように改造してGUIアプリっぽいものを作成したいと思います。
matplotlibのみで作っていきます。




実装

準備

グラフやボタンの配置を行います。

# モジュールのインポート
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.widgets as wg


def btn_click(event):
    global now_processing
    if now_processing:
        now_processing = 0
        btn.label.set_text('Start')
        plt.draw()
        return
    else:
        now_processing = 1
        btn.label.set_text('Stop')
        plt.draw()
        return


board = np.random.randint(0, 2, (25, 25))
now_processing = 0
fig, ax = plt.subplots(2, 1, gridspec_kw=dict(width_ratios=[1], height_ratios=[8, 1]))
img = ax[0].imshow(board, vmin=0, vmax=1)
line1 = ax[0].hlines(y=np.arange(0, 25)+0.5, xmin=np.full(25, 0)-0.5,
                    xmax=np.full(25, 25)-0.5, color="black", linewidth=1)
line2 = ax[0].vlines(x=np.arange(0, 25)+0.5, ymin=np.full(25, 0)-0.5,
                    ymax=np.full(25, 25)-0.5, color="black", linewidth=1)
btn = wg.Button(ax[1], 'Start', color='#f8e58c', hovercolor='#f8b500')
btn.on_clicked(btn_click)
plt.show()


アプリの仕様

以下のような動作をするアプリを作成します

  • スタートボタンを押すとライフゲームの世代を進める
  • ストップボタンを押すとその時の世代で固定
  • 世代が固定されているときは、盤面の任意のセルを押すとそのセルの生死を入れ替える

それでは順番に実装していきましょう

ボタンでライフゲームの実行、停止を操作する

ライフゲームの実装は以前やったので割愛します。
過去記事を参照ください。

ボタンでの動作の実装は下記のようにします。

  • 各セルの現在の状態はグローバル変数boardに格納
  • ボタンの状態にかかわらずアニメーションで一定時間ごとに現在のboardを表示
  • ボタンを一回押すごとにグローバル変数のnow_processingを0, 1でトグルさせる
  • アニメーション更新タイミングで、now_processing=1ならライフゲームのルールにのっとりboardを1世代進めて表示
  • アニメーション更新タイミングで、now_processing=0ならboardをそのまま表示
# モジュールのインポート
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.widgets as wg
from matplotlib.animation import FuncAnimation


def btn_click(event):
    global now_processing
    if now_processing:
        now_processing = 0
        btn.label.set_text('Start')
        plt.draw()
        return
    else:
        now_processing = 1
        btn.label.set_text('Stop')
        plt.draw()
        return


def count_cell(i, j):
    '''i列j行目のセルの周りの生きたセルの数をカウントする関数
    入力:
        i, j: int 対象セルの番地
    出力:
        cnt: int 対象セルの周りの生きたセルの数
    '''
    cnt = 0
    for ii, jj in area:
        indh = i + ii
        indw = j + jj
        if indh < 0 or indh >= board.shape[0]:
            continue
        elif indw < 0 or indw >= board.shape[1]:
            continue
        else:
            if board[indh, indw]:
                cnt += 1
            else:
                continue
    return cnt


def evaluate(cnt, now):
    '''セルの周りの生きているセルの数をもとに、次世代での状態を決定する関数
    入力:
        cnt: int 対象セルの周りの生きているセルの数
        now: int 対象セルの現世代での状態
    出力:
        new: int 対象セルの次世代での状態
    '''
    new = 0
    if now:
        if cnt == 2 or cnt == 3:
            new = 1
    else:
        if cnt == 3:
            new = 1
    return new


def update():
    '''盤面の世代を更新する関数
    '''
    global board
    new_board = np.zeros(board.shape)
    for i in range(board.shape[0]):
        for j in range(board.shape[1]):
            now = board[i, j]
            cnt = count_cell(i, j)
            new = evaluate(cnt, now)
            new_board[i, j] = new
    board = new_board
    return


# グラフ更新関数
def update_anim(dt):
    global board
    if now_processing:
        update()
    img.set_data(board)
    return img, line1, line2,


board = np.random.randint(0, 2, (25, 25))
area = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
now_processing = 0
fig, ax = plt.subplots(2, 1, gridspec_kw=dict(width_ratios=[1], height_ratios=[8, 1]))
img = ax[0].imshow(board, vmin=0, vmax=1)
line1 = ax[0].hlines(y=np.arange(0, 25)+0.5, xmin=np.full(25, 0)-0.5,
                    xmax=np.full(25, 25)-0.5, color="black", linewidth=1)
line2 = ax[0].vlines(x=np.arange(0, 25)+0.5, ymin=np.full(25, 0)-0.5,
                    ymax=np.full(25, 25)-0.5, color="black", linewidth=1)
btn = wg.Button(ax[1], 'Start', color='#f8e58c', hovercolor='#f8b500')
btn.on_clicked(btn_click)
ani = FuncAnimation(fig, update_anim, interval=50, blit=True)
plt.show()

実行結果は下記のとおりです。




初期値をユーザーに入力させる

最後にユーザが自由にセルの状態を書き換えられるようにしたいと思います。
以下の関数をグラフに紐づけるだけです。

def onclick_graph(event):
    global board, now_processing
    if event.xdata and event.ydata and now_processing == 0 and event.inaxes == ax[0]:
        x = int(event.xdata + 0.5)
        y = int(event.ydata + 0.5)
        if board[y, x] == 0:
            board[y, x] = 1
        else:
            board[y, x] = 0
        img.set_data(board)
        plt.draw()
    else:
        return

# figureとクリックイベントを紐づけ
fig.canvas.mpl_connect('button_press_event', onclick_graph)

実行結果はこちら。

全コード

最後に全コードを載せておきます。

# モジュールのインポート
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.widgets as wg
from matplotlib.animation import FuncAnimation


def btn_click(event):
    global now_processing
    if now_processing:
        now_processing = 0
        btn.label.set_text('Start')
        plt.draw()
        return
    else:
        now_processing = 1
        btn.label.set_text('Stop')
        plt.draw()
        return


def onclick_graph(event):
    global board, now_processing
    if event.xdata and event.ydata and now_processing == 0 and event.inaxes == ax[0]:
        x = int(event.xdata + 0.5)
        y = int(event.ydata + 0.5)
        if board[y, x] == 0:
            board[y, x] = 1
        else:
            board[y, x] = 0
        img.set_data(board)
        plt.draw()
    else:
        return


def count_cell(i, j):
    '''i列j行目のセルの周りの生きたセルの数をカウントする関数
    入力:
        i, j: int 対象セルの番地
    出力:
        cnt: int 対象セルの周りの生きたセルの数
    '''
    cnt = 0
    for ii, jj in area:
        indh = i + ii
        indw = j + jj
        if indh < 0 or indh >= board.shape[0]:
            continue
        elif indw < 0 or indw >= board.shape[1]:
            continue
        else:
            if board[indh, indw]:
                cnt += 1
            else:
                continue
    return cnt


def evaluate(cnt, now):
    '''セルの周りの生きているセルの数をもとに、次世代での状態を決定する関数
    入力:
        cnt: int 対象セルの周りの生きているセルの数
        now: int 対象セルの現世代での状態
    出力:
        new: int 対象セルの次世代での状態
    '''
    new = 0
    if now:
        if cnt == 2 or cnt == 3:
            new = 1
    else:
        if cnt == 3:
            new = 1
    return new


def update():
    '''盤面の世代を更新する関数
    '''
    global board
    new_board = np.zeros(board.shape)
    for i in range(board.shape[0]):
        for j in range(board.shape[1]):
            now = board[i, j]
            cnt = count_cell(i, j)
            new = evaluate(cnt, now)
            new_board[i, j] = new
    board = new_board
    return


# グラフ更新関数
def update_anim(dt):
    global board
    if now_processing:
        update()
    img.set_data(board)
    return img, line1, line2,


board = np.zeros((25, 25))
board = np.random.randint(0, 2, (25, 25))
area = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
now_processing = 0
fig, ax = plt.subplots(2, 1, gridspec_kw=dict(width_ratios=[1], height_ratios=[8, 1]))
img = ax[0].imshow(board, vmin=0, vmax=1)
line1 = ax[0].hlines(y=np.arange(0, 25)+0.5, xmin=np.full(25, 0)-0.5,
                    xmax=np.full(25, 25)-0.5, color="black", linewidth=1)
line2 = ax[0].vlines(x=np.arange(0, 25)+0.5, ymin=np.full(25, 0)-0.5,
                    ymax=np.full(25, 25)-0.5, color="black", linewidth=1)
btn = wg.Button(ax[1], 'Start', color='#f8e58c', hovercolor='#f8b500')
btn.on_clicked(btn_click)
fig.canvas.mpl_connect('button_press_event', onclick_graph)
ani = FuncAnimation(fig, update_anim, interval=50, blit=True)
plt.show()

おわりに

matplotlibでも割と簡単にボタンとか作成できますので(配置はめんどくさいですが)、グラフ操作する程度の簡単なアプリであればmatplotlibだけでも十分かとおもいます。