Python Tkinterの大量のボタンをpartialを使って楽に設置

#Python Tkinterの大量のボタンをpartialを使って楽に設置

## この記事の目的
Python GUIのTkinterで大量のボタンをループして設置するには **partialが一番楽に感じた** という記事。
**「楽に設置」** を目的として調査した結果です。

##背景
たくさんのボタンを配置する。
そのため、Tkinterにてtkinter.Button()のループで生成した。
↓
ボタン押下した際の **「どのボタンを押したか」という挙動がうまくいかなかった**。
↓
いくつか試した。
↓
functoolsの **partialが一番楽に感じた** 。

という書き置き。

##まずはcommand属性へ関数名を記載してみる

単純に5つのボタンをループで設置して、ボタンを押下したら関数を実行するプログラム。

```python:tkinter_button_test1.py
import tkinter

tk_root = tkinter.Tk()
frame_main = tkinter.Frame(tk_root)
frame_main.grid()

def on_click1() -> None:
    print("click1")

print("1:loop-click1")
for i in range(5):
    button = tkinter.Button(frame_main, text=i, command=on_click1)
    button.grid(row=0, column=i)

tk_root.mainloop()
```
注目点
- tkinter.Buttonのcommand属性に"on_click1"を記載。

### 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435600/3d2144af-c68c-372a-69b7-b8dd03aa1da3.png)

```bash:実行結果（起動時）
$ python tkinter_button_test1.py
1:loop-click1
```
```bash:0ボタンを押した
click1
```
```bash:1ボタンを押した
click1
```
（2～4ボタンも1と同じだったので省略）

注目点: ちゃんとボタン押下のイベントを受け付けている

## 次にcommand属性へ関数へ引数を渡そうとして勘違いした記載
ボタンを複数設置しても、**どのボタンかを判別できないと意味がない**。
↓
ボタンに引数を渡してみる

```python:tkinter_button_test1.py
import tkinter

tk_root = tkinter.Tk()
frame_main = tkinter.Frame(tk_root)
frame_main.grid()

def on_click2(num:int) -> None:
    print("click2:%d" % num)

print("2:loop-click2")
for i in range(5):
    button = tkinter.Button(frame_main, text=i, command=on_click2(i))
    button.grid(row=0, column=i)

tk_root.mainloop()
```
注目点
- tkinter.Buttonのcommand属性を"on_click2(i)"に変更。
- on_click2関数を引数を受け付ける様に変更。

```bash:実行結果（起動時）
$ python tkinter_button_test2.py 
2:loop-click2
click2:0
click2:1
click2:2
click2:3
click2:4
```
思ってたのと違う。
command属性に関数名でなく()のついた関数を記載したので**ボタン生成時に動作した**様子。

何が問題なのかわかりにくいので補足：ここではボタンをクリックしたらclick2:iが表示されてほしかった→が、ボタンをまだクリックしてないプログラム開始時にon_click2が動いてしまっている


## 変数を渡し方をぐぐる
ここでぐぐってみた。
いくつか手段はあるらしい
1. lambdaで記載
2. functoolのpartialを利用
3. ~~ループせすに「ボタン毎の関数とcommand属性を全てつくる」~~
4. ~~「ボタン毎に関数をつくって」、生成時にcallbackするハンドラを記載~~

目的は **楽に設置** なので「ややこしい」とか「力技」は却下したい。
→ 3, 4は却下として外します。

1, 2をやってみました。

## lambdaで記載
```python:tkinter_button_test3.py
import tkinter

tk_root = tkinter.Tk()
frame_main = tkinter.Frame(tk_root)
frame_main.grid()

def on_click3(num) -> None:
    print("on_click3:%d" % num)

print("3:loop-click3")
for i in range(5):
    button = tkinter.Button(frame_main, text=i, command=lambda:on_click3(i))
    button.grid(row=0, column=i)

tk_root.mainloop()
```
注目点
- tkinter.Buttonのcommand属性に"lambda:on_click3(i)"と引数付きで記載。

```bash:実行結果
$ python .\tkinter_button_test3.py
3:loop-click3
```
on_click(i)としたものの、生成時には実行されずにいる。
ところが。

```bash:0ボタンを押した
on_click3:4
```

```bash:1ボタンを押した
on_click3:4
```
（2～4ボタンも同じ結果なので省略）

押下した際にlambdaでon_click(i)を実行しようとするが、この段階での変数iはループを終えて4になっているというオチ。
これを解決するためには、ややこしいことをしないといけなそう。
**楽に設置** の目的のため、別法方法を模索。

## たどりついたpartial

```python:tkinter_button_test4.py
import tkinter
from functools import partial

tk_root = tkinter.Tk()
frame_main = tkinter.Frame(tk_root)
frame_main.grid()

def on_click4(num) -> None:
    print("on_click4:%d" % num)

print("4:loop-click4")
for i in range(5):
    button = tkinter.Button(frame_main, text=i, command=partial(on_click4, i))
    button.grid(row=0, column=i)

tk_root.mainloop()
```
注目点：
- tkinter.Buttonのcommand属性に"partial(on_click4, i)"と引数を渡して記載。
- partialをimport（from functools import partial）

```bash:実行結果
$ python tkinter_button_test4.py
4:loop-click4
```
```bash:ボタン0を押した
on_click4:0
```
```bash:ボタン1を押した
on_click4:1
```
```bash:ボタン2を押した
on_click4:2
```
```bash:ボタン3を押した
on_click4:3
```
```bash:ボタン4を押した
on_click4:4
```
正しい挙動ができました。
変にcallbackすることもなく、importとcommand属性の中だけで解決できたので、
個人的には「**楽に設置**」という目的達成です。

例えば、midi情報chをリアルタイムにオンオフするフィルターを作ってみたのですが、
16ch分のボタンもループで**楽に設置**できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/435600/23f87c56-6ff3-7661-03f7-96e08b6e87d7.png)

## まとめ
Python tkinterにて大量なボタンとかをループで生成する際に、partialを使うと**楽に設置**できる。
**楽って大事**。はい。