0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

PythonのGUIで遊んでみた

Last updated at Posted at 2019-12-25

前座

1年以上もほったらかして、いったいどんなツラ下げて帰ってきたんだい!

こんなツラだよぉ!

というわけで白鳥です
光陰矢の如し、ほったらかすとまぁ早いこと早いこと

ひっさしぶりにアクセスしたら以前書いた記事にレスポンス来てましたわ
ありがてぇありがてぇ。


ここから本題

さて、表題のお話

なんとなくpythonいじってたんだけど
"そういえばpythonってGUI無いのかな?"とか思ったんですよね

そしたらtkinterなるものが標準で入ってるとかなんとか

これについて真面目に調べようと思ったんだけど
調べてるうちになんかね?深夜のノリでね?
パズル作りたくなっちゃったんすよ。

ほら、5x5のタイルで構成されてて、どれかのタイル押すと隣接するタイルと一緒に色が反転するヤツ。
(調べたらライツアウトとかって名前なんだとか)

で、作りたくなったので、作ってみました。
こんなん

tkinterライツアウト.jpg

作成時間は2時間弱。

で、ソース上げたいんだけど
久しぶりすぎてもうなんも覚えてないよね。

とりあえずソースと解説は起きたら書く予定。

作ったのはいいんだけどさ
クリアできないでやんの
クリスマスの深夜になにしてんだかなぁ


ってことでソースあげました。
とりあえず全文がこちら

puzzle.py
from tkinter import *
from tkinter import ttk
from tkinter import messagebox

def inRange( pTarget, pMin, pMax ):
    ret = (pTarget >= pMin) and (pTarget <= pMax)
    return ret

def onClick( *args):
    col = args[0]
    row = args[1]

    # 波紋処理
    if( inRange(col-1, 0, 4) ):
        array[col-1][row].set(not array[col-1][row].get())

    for r in range(row - 1, row + 2):
        if( (r != row) and inRange(r, 0, 4) ):
            print(r)
            array[col][r].set(not array[col][r].get())

    if( inRange(col+1, 0, 4) ):
        array[col+1][row].set(not array[col+1][row].get())

    # クリア判定
    clear = True
    for c in range(5):
        for r in range(5):
            clear &= array[c][r].get()

    if( clear == True):
        messagebox.showinfo('おめでとう!','やるじゃない!')


root = Tk()
root.title("puzzle")

mainframe = ttk.Frame(root, padding="3 3 3 3")
mainframe.grid( column=0, row=0, sticky=(N, W, E, S))
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)

array = [[BooleanVar() for j in range(5)] for idx in range(5) ]

for column in range(5):
    for row in range(5):
        ttk.Checkbutton(mainframe, command=lambda c=column, r=row : onClick(c, r), variable=array[column][row]).grid(column=column, row=row, sticky=(N, W, E, S) )

root.bind('<Return>', onClick)
root.mainloop()

50行未満のお手頃感。
まだTkinter触り始めたばっかりだから、怪しいところがあったら指摘してくれるとよろこびます。


んで解説。
基本的なTkinterの処理についてはちょい飛ばす感じで。

今回もだらだら作ったので実は無駄な処理も地味に入ってます。
具体的には"そこ関数にする必要ある?"みたいなやつね。
もっと具体的に言うとinRange()。考えてみたら必要なかったっていう。

ポイントになりそうなとこはこんなとこかな?

  1. TkinterのWidget変数の二次元配列化
  2. ウィジェットの動的生成
  3. CheckButtonウィジェットのイベント取得をlambdaで取る

じゃ、始めましょ

TkinterのWidget変数の二次元配列化

puzzle.py(L.43)
array = [[BooleanVar() for j in range(5)] for idx in range(5) ]

色々参考になりそうな処理探したんだけど、ドンピシャなヤツが見つからなかったのよね。
一番近かったのはやるだけPython競プロ日誌 様の PythonにおけるListの本質と二次元配列(多次元配列)のお話のエントリー。

実は最初は二次元配列化とかやらずにその場でインスタンス作ってなんとかごまかそうとしたんすよ(下記参照)

NG_CODE.py
for column in range(5):
    for row in range(5):
        ttk.Checkbutton(mainframe, command=lambda c=column, r=row : onClick(c, r), variable=BooleanVar(Value=False)).grid(column=column, row=row, sticky=(N, W, E, S) )

みたいな感じで。

そしたらこれがうまく行かないの。
チェックボックスの生成までは出来るものの、初期値がcheck済みでも未checkでもない状態になる。
スコープの問題かな?とか思ったのでfor文から出して別に宣言することにしたんですな。
別に二次元配列にする必要自体はなかったんだけど、指定が楽そうだったから二次元配列に格納することに決定。

で、参考サイトによればPythonでの二次元配列の作成方法は

Example_2DArray.py
list = [[0] * 5 for i in range(3)]

とのこと。
これを踏襲して

Example_2DArray.py
list = [[BooleanVar()] * 5 for i in range(3)]

とやるも、これはダメ。
チェックボックスのいずれかの値を変更すると列単位でチェックの値が変更されてしまう。

どうも参考サイトを見てると似たような挙動を起こす失敗例が乗ってたので、そこを参考にして直したのがこちら。

Ugokuyatu.py
array = [[BooleanVar() for j in range(5)] for idx in range(5) ]

[] * Nの記述法自体が怪しいっぽいですね。
正直この記述方式はpython以外で見たことない(ハズ)なので下回りがどうなってるかはなんとなくしかわかりませんが
[[] * N] * M]という記述が同様の誤動作を起こすあたり、* Nという記述をオブジェクトに適用すること自体が参照を複製する動作を示しているっぽいですね。

上記の記述に直したら全チェックボックスが独立して動いてくれるようになりました。

型を重視しない記述法からもしや、と思ってましたがPHPライクな方法で変数管理してるんですね。
正直型はちゃんと意識しないと危ないと思った(小並)

ウィジェットの動的生成

今回は生成する数に増減無い上に、動作中に変更加えるわけではないんだけどそこはまあそれとして

dynamic.py
for column in range(5):
    for row in range(5):
        ttk.Checkbutton(mainframe, command=lambda c=column, r=row : onClick(c, r), variable=array[column][row]).grid(column=column, row=row, sticky=(N, W, E, S) )

生成手法自体は大したことないんだけど、ポイントになるのはイベントハンドラ(command項)の登録部分。
他の言語だとイベントハンドラはいろんな情報を引数として渡してくれたりするんだけど
Tkinterのイベントハンドラは指定しない限り何も引数を与えてくれないっぽい。

ので、どのウィジェットがイベントを投げたか判定するには、それに応じた引数を個別に設定する必要があるみたい。
調べてみると、ラムダ式を利用して同様の処理をしている例があったので、それを応用。
上記の記述だと2つの引数を設定することができた。

引っかかりそうなポイントとしては(Python使いには常識なのかもしれないけど)
lambda式の記述部分を

 ... , command=lambda : onClick( column, row )

としてしまうと、予期しない挙動をしてしまうこと。
具体的に言うと、どのチェックボックスをいじっても、イベントハンドラに送られる情報がcolumn=4,row=4で固定になってしまった。

"PHPライクな方法で変数を管理している"というのがここでも聞いてくるみたい。
つまり、全ての引数は参照渡しになっているので、個別に変数を切ってやらないと期待した動きになってくれない。
ループ処理を作りこむ際に気を付けないとドツボにはまること請け合い。

しかしPythonに関しては変数スコープについてもう少し調べる必要がありそう。

CheckButtonウィジェットのイベント取得をlambdaで取る

…これ解説するとこある?
def onClick( *args)で受けた直後にcol=..って感じで変数に落とし込んでるのは
…まあ、なんていうか、その…

*argsが使いたかったっていうのと
可読性を上げたかったっていうのが理由

引数をそもそもcol,rowで指定してやった方がスマート。
Pythonっぽいの使いたかったんですぅ!
仕事では真似してはいけない。

イベントハンドラの中身についてもまぁ雑っすね。
基本的には

チェックボックスの値が変化する
⇒値が変化したチェックボックスを中心に、隣接する4マスのチェックボックスの状態を反転させる
⇒処理が終わったら全てのチェックボックスがチェックされた状態か判定する
ってのをやってます。

うん、なんでfor文をここで使ってしまったかと言うとね。
変更したチェックボックスまで反転の対象になる処理にしちゃったからだよね。うっかりさん。
更に言うなら、最初は変更したチェックボックスの周囲のマス全部を反転処理しようとしたよね。
なので、ここに関しては無駄な書き方してます。inRange関数も不要でしたね。
まともに作るならちゃんと直した方がいいです。

具体的には

if( col-1 > 0 ) :
    array[col-1][row].set(not array[col-1][row].get())

if( row-1 > 0 ) :
    array[col][row-1].set(not array[col][row-1].get())

if( col+1 < 4 ) :
    array[col-1][row].set(not array[col-1][row].get())

if( row+1 < 4 ) :
    array[col][row-1].set(not array[col][row-1].get())

みたいな感じで愚直に4回判定でいいですね。
あ、バリデーションチェックは必須です。変なとこ参照させるわけにはいかないからね。

どうしてもinRange関数とかfor文使いたい場合、
対象となる座標を作ってから判定回した方がいいかな。
ひと手間増えるけど拡張性は上がるはず。
具体的には反転する範囲が変形してるライツアウトとか作るなら上記の改造施すといいかも。

以上!
解説の方が時間かかりますな。

参考にしたもの

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?