【Python/tkinter】どのボタンが押されたかを判別する方法

今回は Python の tkinter で「大量にあるボタンの中からどのボタンが押されたかを判別する」方法について解説します。

どのボタンが押されたを判別し、「そのボタンの文字の色を赤色に変更する例」と「そのボタンのテキストを出力する例」を用いて解説していきたいと思います。

ん?これじゃダメなの?

一番シンプルな方法は下記のスクリプトのように全てのボタンに対して別々の関数を自力で用意することです。

ボタンの数だけ関数を用意する例
import tkinter

# ボタン1が押された時に実行する関数
def button1_click():
	global button1
	button1.config

	button1.config(fg="red")
	print(button1.cget("text") + "を赤くしました")

# ボタン2が押された時に実行する関数
def button2_click():
	global button2
	button2.config

	button2.config(fg="red")
	print(button2.cget("text") + "を赤くしました")

app = tkinter.Tk()

# ボタン1のインスタンス作成
button1 = tkinter.Button(
	app,
	text="ボタン1",
	command=button1_click
)
button1.grid(column=0, row=0)

# ボタン2のインスタンス作成
button2 = tkinter.Button(
	app,
	text="ボタン2",
	command=button2_click
)
button2.grid(column=1, row=0)

# メインループ
app.mainloop()

このスクリプトにはどのような問題があるでしょうか?

確かにそれでも判別できるね!

でもボタンが50あったら、その関数を50個も自分で作らないとダメだね…

50個は流石にきつい…

そう、上記スクリプトでもできるのですが、数が多いとその分用意する関数も多くなってしまいます。

今回は大量にボタンがある場合でも簡潔なスクリプトで目的を達成する方法を紹介します。

bind を使う

ボタンウィジェットではコンストラクタに command を指定でき、ボタンクリック時の動作(実行する関数・イベントハンドラ)を設定することができます(イベントハンドラとは、イベント発生時に実行する関数・メソッドのことです)。

ですので、ついつい command を利用してボタンクリック時の動作を設定しがちです。

ですが、bind でもボタンクリック時の動作(実行する関数・イベントハンドラ)の設定は可能ですし、どのボタンが押されたかを判別したいのであれば bind を使った方が楽です。

bind を使う理由

なぜなら bind で設定したイベントハンドラには引数としてイベントの詳細な情報が渡されるからです。

そして、その情報の中にはイベントがどのウィジェットから発生したイベントであるかの情報も含まれます。

イベントハンドラに渡される情報1

ですので、どのボタンが押されてそのイベントハンドラが実行されたかの判別は、その引数に含まれるウィジェットの情報を利用することで実現することができます。

つまり、ボタンが押された時に実行されるイベントハンドラは1つしか用意しませんが、引数からどのボタンが押されたかを判別します。

bind やイベント、イベントハンドラに引数として渡される情報は下記ページで解説していますので、こちらも合わせて読んでいただけると幸いです。

イベント処理解説ページのアイキャッチTkinterの使い方:イベント処理を行う

bind で押されたボタンを判別する手順

では bind を利用して押されたボタンを判別する手順を解説していきます。

手順は下記のようになります。

  • イベントハンドラの作成
  • 以下をボタンの数だけ繰り返す
    • ボタンを作成する
    • bind を実行する

イベントハンドラの作成

まずはボタンが押された時に実行するイベントハンドラを作成します。

このイベントハンドラは通常の関数と同様に作成することが可能ですが、必ず “1つ” の引数を持たせます。

イベントハンドラの定義
def button_func(event):
	# ボタンが押された時に実行する処理

イベントハンドラが実行される時には、その引数として tkinter.Event クラスのオブジェクトとして「イベントの情報」が渡されます。

引数で渡されるこの「イベントの情報」がどのようなものであるかは下記ページで解説しています詳しくはコチラを参照してください。

イベント処理解説ページのアイキャッチTkinterの使い方:イベント処理を行う

「このイベントの情報(tkinter.Event クラスのオブジェクト)」の中で、どのボタンが押されたかを判別するのに重要なのは widget メンバです。

widgetメンバ
event.widget

この widgetイベント発生のトリガーとなったウィジェットのインスタンスを参照するメンバです。

イベントハンドラに渡される情報2

したがって widget メンバからウィジェットが実行可能なメソッドも実行するができます。

なので、ボタンの文字の色を赤色にするのであれば、下記のように config メソッドにより設定することができます。

ボタンの文字の色の設定
event.widget.config(fg="red")

またボタンのテキストは下記のように cget メソッドにより取得することができます。

文字のテキストの取得
event.widget.cget("text")

イベントハンドラ全体の例を示すと下記のようになります。

イベントハンドラ
def button_func(event):

	event.widget.config(fg="red")
	print(event.widget.cget("text") + "を赤くしました")

実際にボタン後された時にこのイベントハンドラが実行されるように bind を実行すれば、ボタンが押された時に、その押されたボタンの文字の色が赤色になり、そのボタンに表示されているテキスト + "を赤くしました" が標準出力に出力されるようになります。

ボタンの作成と bind の実行

あとはボタンの作成と bind を実行するだけです。

ボタンの作成を行い、そのボタンが押された時に先ほど作成したイベントハンドラが実行されるように bind を実行します。

50個のボタン(横方向に5個・縦方向に10個)をループ処理で作成し、それぞれのボタンに対して bind を実行する例は下記のようになります。

ボタンの作成とbind実行
# ボタンを50個配置
for j in range(10):
	for i in range(5):
		# ボタンの名前(テキスト)を設定
		button_name = "ボタン(" + str(i) + "," + str(j)+ ")"

		# ボタンのインスタンス作成
		button = tkinter.Button(
			app,
			text=button_name
		)
		
		# ボタンを2次元的に配置
		button.grid(column=i, row=j)

		# ボタンクリック時のイベント設定
		button.bind("<ButtonPress>", button_func)

下記でボタンを作成し、

ボタンの作成
# ボタンのインスタンス作成
button = tkinter.Button(
	app,
	text=button_name
)

下記でその作成したボタンのイベント設定を行っています。

bind実行
# ボタンクリック時のイベント設定
button.bind("<ButtonPress>", button_func)

上記により "<ButtonPress>" のイベントが発生した時(マスボタンのクリックが発生した時)に、button_func 関数が実行されるようになります。

どのボタンを押しても同じ button_func 関数が実行されることになりますが、button_func で widget メンバからどのボタンが押されたかは判別できるので問題ありません。

bind で押されたボタンを判別するスクリプト例

bind で押されたボタンを判別するスクリプト全体の例は下記のようになります。

bindで押されたボタンを判別するスクリプト
import tkinter

def button_func(event):

	event.widget.config(fg="red")
	print(event.widget.cget("text") + "を赤くしました")

app = tkinter.Tk()

# ボタンを50個配置
for j in range(10):
	for i in range(5):
		# ボタンの名前(テキスト)を設定
		button_name = "ボタン(" + str(i) + "," + str(j)+ ")"

		# ボタンのインスタンス作成
		button = tkinter.Button(
			app,
			text=button_name
		)
		
		# ボタンを2次元的に配置
		button.grid(column=i, row=j)

		# ボタンクリック時のイベント設定
		button.bind("<ButtonPress>", button_func)

# メインループ
app.mainloop()

スクリプトを実行すれば下のようなボタンが50個配置された画面が表示されます。

bindを利用するスクリプトの実行例1

ボタンを押すと、押したボタンの文字の色が赤色に変化します。

bindを利用するスクリプトの実行例2

さらに「ボタン(x,y)を赤くしました」と、どのボタンが押されてどのボタンを赤くしたかの情報が標準出力に出力されます。

この動きから、どのボタンが押されたかを判別し、判別した結果からそのボタンの設定変更や設定取得ができていることが確認できると思います。

クロージャを利用する

2つ目の方法は「クロージャを利用する」方法になります。

クロージャと聞くと難しそうに感じるかもしれませんが、実は考え方は簡単です。

最初に紹介したスクリプトと考え方は同じで、ボタンの数だけ関数を用意し、どの関数が実行されたかによってボタンを判別します。

ボタンの数だけ関数を用意する様子

ただし自力で関数を作る(ボタンの数だけ自分で関数を記述する)のは大変なので、クロージャの仕組みを利用して自動的に関数を作成するようにしているだけです。

クロージャでボタンが判別できる理由

まず前置きとしてはクロージャについて簡単に解説しておきます。

クロージャとは

クロージャとは他の関数によって動的に作成される関数で、作成された時の変数の値を覚えているという特徴があります。

クロージャを利用した動的な関数の作成

例えば下記のような構造の outer 関数を作成したとします。

クロージャの例
def outer(x):
	
	def inner():
		print("xは" + str(x) + "です")

	return inner

この時、inner 関数がクロージャとはで解説した「クロージャ」に当てはまり、outer 関数がクロージャとはで解説した「他の関数」に当てはまります。つまり outer 関数が inner 関数を動的に作成します。

クロージャを作成する様子

outer 関数実行時には inner 関数は実行されず、inner 関数を新たに作成して返却するだけの関数になります。

inner 関数は outer 関数の引数 x を利用しており、作成される時にこの引数 x の値を覚えた状態で作成されることになります。

例えば引数 x3 であれば、outer 関数実行時に作成される inner 関数は下記のような関数になるイメージです。

x=3の時のinner関数
def inner():
	x = 3
	print("xは" + str(x) + "です")

引数 x10 であれば、outer 関数実行時に作成される inner 関数は下記のような関数になります。

x=10の時のinner関数
def inner():
	x = 10
	print("xは" + str(x) + "です")

こんな感じで、outer 関数から返却されるのは同じ inner 関数ですが、outer 関数実行時に指定された x の値を覚えているので、異なる関数として動作することになります。

outer 関数を実行するたびに異なる関数が作成されますので、例えばループの中で outer 関数に渡す引数を変化させながら outer 関数を実行することで、自動的に多くの関数を簡単に作成することができます。

ループでクロージャを大量に作成する様子

また、outer 関数からは作成した inner 関数が返却されます。

ですので、その戻り値を参照させることで、後から作成した inner 関数を、outer 関数実行時に指定した引数を覚えた状態で動作させることができます。

下記がその例となるスクリプトになります。

クロージャの使用例
def outer(x):
	
	def inner():
		print("xは" + str(x) + "です")

	return inner

a = outer(3)
b = outer(10)

b()
a()

実行すると、下記のような文字列が出力されます。

xは10です
xは3です

outer 関数実行時の引数を覚えた状態の inner 関数が b()a() により実行されていることが確認できると思います。

クロージャを利用した押されたボタンの判別

つまり、押されたボタンがどのボタンであるかを判別するためには、ボタン毎にそのボタン専用のクロージャを動的に作成してやれば良いです。

これにより、押されたボタンに応じて実行される関数が変わるので、実行される関数によってどのボタンが押されたかが判別可能になります。

もちろん、その押されたボタンに対して、ボタンの色を変更したり、ボタンに表示されているテキストを取得するなどの処理を行うこともできます。

クロージャで押されたボタンを判別する手順

ではクロージャを利用して押されたボタンを判別する手順を解説していきます。

手順は下記のようになります。

  • クロージャを作成する関数の用意
  • 以下をボタンの数だけ繰り返す
    • ボタンを作成する
    • クロージャを作成する
    • command を設定する

クロージャを作成する関数の用意

まずはクロージャを作成する関数を用意します。用意するのは、ここまでの例でいうところの outer 関数になります。

関数自体は通常の関数同様に定義してやれば良いですが、下記の2点を行うところがポイントです。

  • 内部にさらに関数をもたせ、その中にボタンが押された時に実行したい処理を記述する
  • その関数を return させる

今回は、押されたボタンの文字の色を赤色にし、さらにそのボタンに表示されているテキストを出力したいため、下記のように関数を作成します。

クロージャを作成する関数
# クロージャを作成する関数
def outer(button):

	# ボタン専用の関数
	def inner():
		button.config(fg="red")
		print(button.cget("text") + "を赤くしました")
	
	return inner

上記で作成される関数は、outer 関数実行時に引数で指定したボタンウィジェットに対して処理を行う関数になります。

outer 関数実行に異なるボタンウィジェットを指定してやれば、異なるボタンウィジェットに対して処理を行う関数を作成することができます。

ボタンの作成・クロージャの作成・command の設定

クロージャを作成する関数が用意できれば、あとはボタンの数だけ下記を行えば良いです。

  • ボタンウィジェットを作成する
  • そのボタンを引数に指定して outer 関数を実行してそのボタン専用の関数(クロージャ)を作成する
  • ボタンが押された時に実行する関数をその作成した関数に設定する

50個のボタン(横方向に5個・縦方向に10個)をループ処理で作成し、それぞれのボタンに対してクロージャの作成を行う例は下記のようになります。

50個のボタンにクロージャを設定するスクリプト
# ボタンを50個配置
for j in range(10):
	for i in range(5):
		# ボタンの名前(テキスト)を設定
		button_name = "ボタン(" + str(i) + "," + str(j)+ ")"

		# ボタンのインスタンス作成
		button = tkinter.Button(
			app,
			text=button_name,
		)

		# ボタン(i,j)専用の関数を作成
		func = outer(button)

		# その関数をcommandにセット
		button.config(command=func)

		# ボタンを2次元的に配置
		button.grid(column=i, row=j)

tkinter.Button() はボタンウィジェットのコンストラクタなので、実行することで新たなボタンインスタンスが作成されて button がそのインスタンスを参照することになります。

なので、クロージャを作成する outer 関数の引数には毎回異なるボタンが指定されることになります。

したがって、outer 関数を実行するたびに、毎回異なるボタンに対して処理(文字を赤くする・テキストを取得する)が実行されるクロージャが作成されることになります(要は作成されるクロージャは outer 関数実行時に引数で指定された button を覚えている)。

さらに下記で、作成したボタンが押された時に作成したクロージャが実行されるように command を設定しています。

commandへのクロージャの設定
button.config(command=func)

クロージャで押されたボタンを判別するスクリプト例

下記がクロージャを利用して押されたボタンを判別してボタンごとに処理を切り替えるスクリプト全体の例になります。

クロージャで押されたボタンを判別するスクリプト
import tkinter

# クロージャを作成する関数
def outer(button):

	# ボタン専用の関数
	def inner():
		button.config(fg="red")
		print(button.cget("text") + "を赤くしました")
	
	return inner

app = tkinter.Tk()

# ボタンを50個配置
for j in range(10):
	for i in range(5):
		# ボタンの名前(テキスト)を設定
		button_name = "ボタン(" + str(i) + "," + str(j)+ ")"

		# ボタンのインスタンス作成
		button = tkinter.Button(
			app,
			text=button_name,
		)

		# ボタン(i,j)専用の関数を作成
		func = outer(button)

		# その関数をcommandにセット
		button.config(command=func)

		# ボタンを2次元的に配置
		button.grid(column=i, row=j)

# メインループ
app.mainloop()

実行時の動作は bind で押されたボタンを判別するスクリプト例と同じですので紹介は省略しますが、実際に実行してみれば、このクロージャを用いる方法でも押されたボタンの判別ができ、押されたボタンに対して処理が実行できていることを確認できると思います。

スポンサーリンク

まとめ

このページでは Python tkinter において「押されたボタンを判別する方法」について解説しました。おそらくボタンを判別したい場面は多いと思いますので、是非この機会にこの方法を身につけておいてください!

コレ、実は簡単そうに思えて実はちょっと難しいです。

私もなかなか出来なくてハマりました…。

ボタンウィジェットは command で押された時に実行される関数を指定できるので便利ですが、引数を渡そうと思うと難易度が上がります。

commandlambda 式を利用して引数を渡そうと思えば渡すことも可能です。

ですが、lambda 式で引数を渡しても、ボタンを押されて関数が実行される際には、command その時点の変数の値が引数として渡されてしまうので意図した通りに動かない可能性が高いです(ループ中の値が渡せない)。

とはいえ、理屈がわかってしまえば bind やクロージャで簡単に押されたボタンを判別することは可能ですので、tkinter でのアプリ開発でどんどん活用して行ってください!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です