Tkinterのボタンサイズをピクセル単位で指定できるようにする

# Pythonの GUI ツール Tkinter
**Tkinter** に於いて、**geometry** では、ウィンドウのサイズをピクセル単位でサイズを指定するのに、**Button** は文字数で指定するという、直接的にピクセルで指定する方法がないということに憤りを感じました。
そこで、以下のようなものを作ってみました。

```python:example.py
import tkinter as TK

class X:

    def __init__(self):
        self._app = TK.Tk()
        self._pv = TK.PhotoImage(width=1, height=1)
        self._bwfx, self._bhfx, self._bwmin, self._bhmin = None, None, None, None

    def _ent(self, func, *args, **kargs):
        ret = None
        if callable(func):
            self._app.update()
            ret = func(*args,**kargs)
            self._app.update()
        return ret

    def protoType1(self):

        self._app.geometry("200x200")
        w_width = self._ent(self._app.winfo_width)

        btn1 = self.newBtn(0, 0)
        btn2 = self.newBtn(0, 1)

        self.setSize(btn1, w_width, 20)
        self.setSize(btn2, w_width, 20)

        self._app.mainloop()

    def newBtn(self, column, row):
        widget = self._ent(TK.Button, self._app, image=self._pv)
        self._ent(widget.grid, column=column, row=row)
        if self._bwfx is None:
            self.sizeOne(widget)
        return widget

    def setSize(self, widget, width, height):
        _width = round(width / self._bwfx) - self._bwmin
        _height = round(height / self._bhfx) - self._bhmin
        self._ent(widget.configure, width=_width, height=_height)

    # Find the size per width|height
    def sizeOne(self, btn):
        # The minimum width of the button per button requested width 0.
        self._ent(btn.configure, width=0, height=0)
        self._bwmin = self._ent(btn.winfo_width)
        self._bhmin = self._ent(btn.winfo_height)
        # Button width per button requested width 5.
        self._ent(btn.configure, width=5, height=5)
        b5w = self._ent(btn.winfo_width)
        b5h = self._ent(btn.winfo_height)
        # Calculating size per unit of width
        self._bwfx = round((b5w - self._bwmin) / 5)
        self._bhfx = round((b5h - self._bhmin) / 5)


if __name__ == '__main__':
    x = X()
    x.protoType1()
```
※ このコードは、**RaspberryPi5** と **WSL(Ubuntu)** の **python** で動作を確認しています。


# 解説
|要素 or メソッド　|説明|
|:---:|---|
|**_pv** |ボタンの **image** オプションに設定される 1x1 ピクセルの透明な **PhotoImage** で、これによりボタンの **width** と **height** がピクセル単位で扱われるようにします。
|**_ent** |関数呼び出しの前後に **self._app.update()** を実行することで、GUI の状態を強制的に更新します。
|**protoType1**|サンプルとして、ウィンドウを作成し、2つのボタンを配置し、ウィンドウの初期幅に合わせてボタンの幅を設定します。|
|**newBtn** |ボタンを作成し、最初に **sizeOne** メソッドを呼び出して基準となるピクセル単位の幅と高さを取得します。<br/>ボタンを作成したら、まず **grid** で配置することで、その後に呼ばれる **sizeOne** に於いて幅やサイズが取れるようになります。|
|**setSize**|指定されたピクセル単位の幅と高さに基づいて、ボタンの **width** と **height** を計算しています。<br/>**self._bwfx** と **self._bhfx** は 1 ピクセルあたりの文字幅・高さの係数を表します。|
|**sizeOne**|ボタンの **width** を **0** と **5** に設定したときの実際のピクセル幅の変化を計測し、1 文字あたりのピクセル幅と高さを計算します<br/>ボタンの **width** が **0** のときは、**_bwmin** と **_bhmin** が、文字が含まれていない場合の、ボタン自体のサイズとなります。<br/>ここで重要なのが、**app.update()** です。スクリプトでは、**grid()** の直後に、**winfo_width()** を行うと、文字数が返ってきます。しかし、**python** のインタプリタから手入力で確認する場合には、**winfo_width()** はピクセル数を返すので、**app.update()** によってイベントを処理させることでピクセル数が取れるようにしています。|

# 本節のまとめ
今回は、ボタンを例に作成しました。
他のウィジェットでは、周囲のパディングやエッジのサイズが異なるかもしれないので、ウィジェットの種類ごとに行うと良いかもしれません。これらの処理をコンポーネント、もしくはアサインツールとして利用できれば、ピクセル単位でGUI要素を配置する際に利用できるのでは、と思います。