memopy

pythonで作ってみました的なブログ

python tkinter どのボタンが押されたか判定する方法

python tkinter どのボタンが押されたか判定する方法

ボタンをforループ文などで機械的にたくさん作った時のコールバック関数の定義について整理する。
f:id:memopy:20170611175835p:plain
上記のGUIは次のスクリプトで作成したものだ。
※python3で作成(python2で使用するためには、モジュール名をTkinterとする。以下、同じ。)

# -*- coding:utf-8 -*-

import tkinter as tk

root = tk.Tk()

# ボタンが押されたときのコールバック関数
def callback():
    pass
# ボタンを格納するリストを定義
buttons = []
for i in range(5):
    # リストにボタンを追加
    buttons.append(tk.Button(root,text=i,command=callback))
    # リストのインデックスを使用して、ボタンを配置
    buttons[i].pack(fill="x")

root.mainloop()

このとき、どのボタンが押されたか判定するコールバック関数はどのように定義したらよいだろうか?

単純な例

もっとも単純な例は次のようになるだろう。

# -*- coding:utf-8 -*-

import tkinter as tk
root = tk.Tk()
# ボタンが押されたときのコールバック関数
def btn0_callback():
    print("ボタン0が押されました")
def btn1_callback():
    print("ボタン1が押されました")
def btn2_callback():
    print("ボタン2が押されました")

# ボタンの作成  
button0 = tk.Button(root,text="0",command=btn0_callback)
button0.pack(fill="x")
button1 = tk.Button(root,text="1",command=btn1_callback)
button1.pack(fill="x")
button2 = tk.Button(root,text="2",command=btn2_callback)
button2.pack(fill="x")

root.mainloop()

f:id:memopy:20170611180827p:plain
ボタン3つで挫折した。もしこのボタンをたくさん定義しなければいけないとなるとぞっとする。
f:id:memopy:20170611185122p:plain

コールバック関数により、どのボタンが押されたか判定する

# -*- coding:utf-8 -*-

import tkinter as tk

root = tk.Tk()

# コールバック関数をネストして定義
def callback(i):
    def x():
        print(str(i)+"が押されました")
    return x
    
buttons = []
for i in range(5):
    # コールバック関数にボタン番号の値を引数で渡す
    buttons.append(tk.Button(root,text=i,command=callback(i)))
    buttons[i].pack(fill="x")

root.mainloop()

このように定義すると、押されたボタンを判定することができる。
f:id:memopy:20170611193404p:plain
しかし、これを理解するためには、ボタンのcommandオプションの振る舞いと、ネストされた関数について理解しなければならない。
以下で詳しく整理する。

commandオプションの振る舞い

はじめに、commandオプションにprint関数を用いたらどうなるのか見てみたい。

# -*- coding:utf-8 -*-

import tkinter as tk
root = tk.Tk()

buttons = []
for i in range(5):
    buttons.append(tk.Button(root,text=i,command=print(i)))
    buttons[i].pack(fill="x")

root.mainloop()

これを実行すると、「ボタンを押していないのに」コンソールに次の出力がなされた。
f:id:memopy:20170611203823p:plain
すなわち、commandオプションは、ボタンオブジェクトを作成されるときに、一度呼び出される(実行される)というがわかる。
この状態で、ボタンを押してもなにも表示されない。

ラムダ関数 lambda を使用したコールバック

コールバック関数を定義しない場合は、lambda関数を使用することもできる。
では、lambda関数で上記のprint関数を実装したらどうなるだろうか。

# -*- coding:utf-8 -*-

import tkinter as tk
root = tk.Tk()

buttons = []
for i in range(5):
    buttons.append(tk.Button(root,text=i,command=lambda:print(i)))
    buttons[i].pack(fill="x")

root.mainloop()

今度は、ちゃんと「ボタンがクリックされたとき」にprint関数が実行されたが、今度はすべて「4」が表示されてしまった。
f:id:memopy:20170611205110p:plain

これは、ボタンがクリックされたとき、lambda関数が実行されたが、実行時にprint関数の中のiは、既に4になってしまっているから、どのボタンを押しても4が表示されてしまった。
すなわち、このprint関数のiには、ボタンが作成されたときのiの値が渡されたわけではないことになる。

ここでは、lambda関数の説明は深く掘り下げないが、lambda関数とは、無名関数とも呼ばれ、通常の関数を簡略して1行で記述できるものである。

# ボタンをクリックするとき、iの値は既に4の状態
i=4

# ラムダ関数の定義
callback_lamb = lambda : print(i)
# ラムダ関数の実行
callback_lamb()
# →4が表示される
# オブジェクトを作らない実行方法
( lambda : print(i) )()
# →4が表示される

# 通常の関数を定義
def callback():
    print(i)
# 通常の関数を実行
callback()
# →4が表示される

以上のことから、ボタンのコールバックオプションは、次のような振る舞いとなっている。

# printの例
tk.Button(command=print(i))
# lambdaの例
tk.Button(command=lambda:print(i) )
  1. ボタンオブジェクトが作成されるときに、commandオプションに記載された文字列がそのまま実行される。
    printの例:print(i)がそのまま実行されて、ボタン作成時のiが表示される。
    lambdaの例:lambda : print(i) という文字列がそのまま実行されたが、なにも起こらない。
  2. ボタンがクリックされたときは、commandオプションに記載された文字列が引数なしの関数として実行される。
    printの例:print(i)()として実行される。→このような関数を定義していないので、結果、なにも表示されない。
    lambdaの例:( lambda:print(i) )()が実行されるが、iは既に4のため、どのボタンを押しても4が出力される。

すなわち、print(i)()というような形の関数を定義すればよいということになる。これを定義するのが、ネストされた関数だ。

ネストされた関数

ネストされた関数とは、関数の中に関数を定義することである。関数内関数とも呼ばれる。
はじめに、ネストされた関数がどのように振舞うのか整理したい。
f:id:memopy:20170611212039p:plain
上の図は、関数aの中に関数bが定義されている。
a()として関数aを呼び出すと、print関数によりaが出力され、関数bがオブジェクトとして返ってくる。
このとき、a() = b の状態になっているから、a()()を実行すると、b()を実行するのと同じことになり、関数bが呼び出される仕組みだ。
もちろん、下図のように引数を渡すことも可能である。
f:id:memopy:20170611212752p:plain
この例が、まさに、主題のコールバック関数の定義の仕方である。

# -*- coding:utf-8 -*-

import tkinter as tk
root = tk.Tk()

# コールバック関数をネストして定義
def callback(i):
    def x():
        print(str(i)+"が押されました")
    return x

buttons = []
for i in range(5):
    buttons.append(tk.Button(root,text=i,command=callback(i)))
    buttons[i].pack(fill="x")

root.mainloop()

順を追ってこのロジックを見ていくと、

  1. ボタン0が作成されるときに、callback(0)が実行されるが、なにも起こらない。
    (正確にいうと、関数xがリターンされているが見た目に変化なし)
  2. ボタン1が作成されるときに、callback(1)が実行されるが、なにも起こらない。
    (正確にいうと、関数xがリターンされているが見た目に変化なし)
  3. (ボタン2~4が作成されたときに・・・略)
  4. ボタン0がクリックされたときに、callback(0)()が実行され、関数xのprint(0)が呼び出される。
  5. ボタン1がクリックされたときに、callback(1)()が実行され、関数xのprint(1)が呼び出される。
  6. (ボタン2~4がクリックされたときに・・・略)

という仕組みだ。


今回は、コールバック関数を使用してクリックされたボタンを判定した。
次の記事では、bindメソッドを使用して、ボタンだけではなく、全てのウィジェットに対してクリックされたものを判定する方法を紹介する。
python tkinter クリックされたウィジェットのテキストや属性を取得する - memopy