[Python][tkinter] 1次元配列のループでn行×m列のgrid表示をさせる方法



作ったプログラムの備忘録

# はじめに
tkinterでボタンなどを配置する際に似たようなコードを列挙するのが嫌で、ループで表示させる方法について検討した際のメモ

## 動作テスト環境
OS: Windows 10 Pro 64bit
言語： Python 3.9.13

# ソースコード
```Python:4×3のグリッド表示例
import tkinter as tk
from tkinter import messagebox as mb
from functools import partial

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("grid example app")
        self.master.geometry("300x400")

        self.master.grid_rowconfigure((0, 1, 2, 3), weight=1)
        self.master.grid_columnconfigure((0, 1, 2), weight=1)

        clr = ['#ffb6b9', '#bbded6', '#8ac6d1', '#ffeead', '#fae3d9', '#96ceb4',
               '#d9534f', '#ffad60', '#a696c8', '#2470a0', '#b689b0', '#fe5f55']

        self.button = []
        for i in range(12):
            tmp = tk.Button(self.master, text=f'{i+1}', bg=f'{clr[i]}',
                            command=partial(self.click_button, i+1))
            tmp.grid(row=i//3, column=i%3, sticky=tk.NSEW)
            self.button.append(tmp)

    def click_button(self, num):
        mb.showinfo(title='click', message=f'click: {num}')

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()
```

![FC14C67D-9A18-4930-82B0-0DCDD57E8011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079481/5d3cf336-7924-fba1-48ab-108f8765a03b.png)



* 上記は`4×3`のグリッド表示させる際のコードです。

* for文で`tk.Button()`を生成して、grid配置を繰り返しています。

* `self.button = []` はボタンでは必須ではない表記ですが、`Entry`や`Checkbox`ではあとからインデックスで個別に入力内容を取得できるので、配列に格納する方がよいと考えています。

* <strong>ここで行×列に配置する際の行番号は`i//列数`、列番号は`i%列数`でそれぞれ取得できます。</strong>書いている内容は以下と同じですが、行番号の規則性が`切り捨て除算`、列番号の規則性が`剰余`になっているのがわかるかと思います。

```Python:for文を使わないで配置する際のコード
        self.button[0].grid(row=0, column=0, sticky=tk.NSEW)
        self.button[1].grid(row=0, column=1, sticky=tk.NSEW)
        self.button[2].grid(row=0, column=2, sticky=tk.NSEW)
        self.button[3].grid(row=1, column=0, sticky=tk.NSEW)
        self.button[4].grid(row=1, column=1, sticky=tk.NSEW)
        self.button[5].grid(row=1, column=2, sticky=tk.NSEW)
        self.button[6].grid(row=2, column=0, sticky=tk.NSEW)
        self.button[7].grid(row=2, column=1, sticky=tk.NSEW)
        self.button[8].grid(row=2, column=2, sticky=tk.NSEW)
        self.button[9].grid(row=3, column=0, sticky=tk.NSEW)
        self.button[10].grid(row=3, column=1, sticky=tk.NSEW)
        self.button[11].grid(row=3, column=2, sticky=tk.NSEW)
```

* またループで回してボタンを配置する際に`command`で引数を持たせたいときに`command=lambda: click_button(i)`としても、どのボタンを押しても変数`i`は回りきった最後の値（今回なら11）を返してしまうので、それを回避するには`functools`の`partial`関数を使う必要があります。

---

```Python:
        self.button = [tk.Button(self.master, text=f'{i+1}', bg=f'{clr[i]}',
                                 command=partial(self.click_button, i+1)) for i in range(12)]
        for i in range(12):
            self.button[i].grid(row=i//3, column=i%3, sticky=tk.NSEW)
```

* ちなみにwidgetsの配列への入れ方は内包表記も可能です（1行が長くなりすぎるので可読性が低いですが）

```Python:
        self.button = [tk.Button(self.master, text=f'{i+1}', bg=f'{clr[i]}',
                                 command=partial(self.click_button, i+1)) for i in range(12)]
        self.button = [self.button[i].grid(row=i//3, column=i%3, sticky=tk.NSEW) for i in range(12)]
```
* さすがに`.grid()`を内包表記すると挙動の意味合いがよくわからなくなるので良くないと思いますが、一応表示はされます。

## 一部を変更する場合

* またgrid表示を一部結合させたい場合は、for文中にif文をかいてもいいと思いますし、後から上書きと`grid_forget()`や`grid_remove()`で修正してもいいと思います。

```Python:for文の後に変更する場合の例
        self.button = []
        for i in range(12):
            tmp = tk.Button(self.master, text=f'{i+1}', bg=f'{clr[i]}',
                            command=partial(self.click_button, i+1))
            tmp.grid(row=i//3, column=i%3, sticky=tk.NSEW)
            self.button.append(tmp)

        self.button[0].grid(row=0, column=0, columnspan=2, sticky=tk.NSEW)
        self.button[1].grid_remove()
        self.button[7].grid(row=2, column=1, columnspan=2, sticky=tk.NSEW)
        self.button[8].grid_forget()
```

![308B2DE7-76E9-46B8-B45F-DE5EA31F2B58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079481/7450f846-1a9f-505b-131f-212786d99e95.png)

## 具体例1
いまいち何に使うんだ？という感じかもしれませんが、電卓っぽいGUIはすっきり書けます。

```Python:
import tkinter as tk
from tkinter import messagebox as mb
from functools import partial

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("grid example app")
        self.master.geometry("300x400")

        self.master.grid_rowconfigure((0, 1, 2, 3, 4), weight=1)
        self.master.grid_columnconfigure((0, 1, 2, 3), weight=1)

        btn = ['7', '8', '9', '÷', '4', '5', '6', '×', '1', '2', '3', '－', '0', '.', '＝', '＋']

        self.entry = tk.Entry(self.master, text='')
        self.entry.grid(row=0, column=0, columnspan=4, sticky=tk.NSEW)

        self.button = []
        for i in range(16):
            tmp = tk.Button(self.master, text=f'{btn[i]}', command=partial(self.click_button, btn[i]))
            tmp.grid(row=i//4+1, column=i%4, sticky=tk.NSEW)
            self.button.append(tmp)

    def click_button(self, num):
        self.entry.insert(tk.END, f'{num}')

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()
```

![D642439B-4189-477A-B765-10ECC2C6CC9C.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3079481/e7100013-e9ac-0e87-2beb-f5f92ea50032.png)

## 具体例2
あとはマインスイーパーぽいGUIもすっきり書けます。

https://qiita.com/takafi/items/5da949fda61c29184f7d

```Python:
import random
import tkinter as tk
from tkinter import messagebox as mb
from functools import partial

class Application(tk.Frame):
    def __init__(self, master = None):
        super().__init__(master)

        self.master.title("grid example app")
        self.master.geometry("400x400")
        self.master.grid_rowconfigure(tuple(range(20)), weight=1)
        self.master.grid_columnconfigure(tuple(range(20)), weight=1)

        self.mine = [0 if random.random() < 0.85 else 1 for i in range(400)]

        self.label = []
        for i in range(400):
            tmp = tk.Label(self.master, text='　', relief=tk.RAISED)
            tmp.grid(row=i//20, column=i%20, sticky=tk.NSEW)
            self.label.append(tmp)
            self.label[i].bind('<Button-1>', partial(self.click_left, i))
            self.label[i].bind('<Button-3>', partial(self.click_right, i))

    def click_left(self, num, button):
        self.label[num].config(relief=tk.SUNKEN, text=f'{self.mine[num]}')

    def click_right(self, num, button):
        self.label[num].config(text='F')

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(master = root)
    app.mainloop()
```

## 具体例3

カレンダー表示もできます

https://qiita.com/takafi/items/ee4215272159a98dea53