tkinter で画像処理系の GUI を作るメモ


画像系の処理の GUI を作りたい(e.g. 機械学習のためのアノテーションや処理結果の表示など)

Qt だとインストールとかバインディングとかめんどいのよね...
(tkinter だと Python にデフォルトで入っている)
あと Qt だと, OSS 版だと LGPL なのでライセンスがちょっと微妙(tkinter だと Python ライセンスで, Tck/Tk 自体も BSD-style license: https://www.tcl.tk/software/tcltk/license.html)

python 3.8 or later を想定します.

## テーマ

デフォルトではやや古臭いので, ナウでヤングな見た目になる ttkbootstrap あたり使うのがいいでしょう.

https://ttkbootstrap.readthedocs.io/en/latest/

きちんとメンテされていて安心して使えます!
ただ, pip で入るリリース版はやや古いかもしれません. 不都合あってもだいたい github の最新では治っている印象があります.

### custom font, font antialias

https://stackoverflow.com/questions/30335701/anti-aliased-fonts-in-tkinter

Linux だと tcl/tk が freetype 有効でビルドされているかなどの状況次第になります.

#### Conda では antialias + external TTF フォント対応できない.

Ubuntu 20.04 + conda python 3.8 ではアンチエイジングされませんでした... (また, custom font も扱えない)

![Screenshot from 2022-01-08 22-42-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36275/c9b75a26-0577-63c1-c980-8b05567a9ce3.png)

Windows では OK.

![Screenshot from 2022-01-08 22-46-23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36275/5834bb49-5abf-cbe1-11f6-c643c07d5c23.png)

https://stackoverflow.com/questions/47769187/make-anacondas-tkinter-aware-of-system-fonts-or-install-new-fonts-for-anaconda

conda では tcl ライブラリ関連が freetype 有効でビルドされないようです.

とりあえずは stackoverflow にあるように .so コピーで対応でしょうか...
(`LD_LIBRARY_PATH` あたりで特定の .so だけ検索順序を変えるとかできるかな?)


Ubuntu system の tcl/tk 使えばいろいろ解決します!

![Screenshot from 2022-01-09 22-30-08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36275/d1f16279-7837-6a0f-8059-678a77cff8a7.png)

## Livereload?

`jurigged` と `reloading` である程度 livereload できます.

```
root = tk.Tk()
  
b1 = ttk.Button(root, text="Button 2", bootstyle=SUCCESS)
b1.pack(side=LEFT, padx=15, pady=100)
  
b2 = ttk.Button(root, text="Button 1", bootstyle=(INFO, OUTLINE))
b2.pack(side=LEFT, padx=115, pady=10)
  
b3 = ttk.Button(root, text="Button 3", bootstyle=(INFO, OUTLINE))
b3.pack(side=LEFT, padx=115, pady=10)
  
@reloading
def fn():
   root.update()

root.mainloop()
```

ただ, これだと padx などは反映できますが, text などは反映できません.


#### TTF font?

Windows 以外では, 直接カスタムフォントを読む方法はありません.

https://stackoverflow.com/questions/11993290/truly-custom-font-in-tkinter

Linux の場合 `tkextafont` があります.

https://github.com/TkinterEP/python-tkextrafont

ただ, ttf ファイルに fontFamily 情報がない場合エラーがでますので注意ください.

```
  File "/home/syoyo/miniconda3/lib/python3.8/site-packages/tkextrafont/__init__.py", line 92, in __init__
    root.tk.call("extrafont::load", file)
_tkinter.TclError: key "fontFamily" not known in dictionary
```

## ESC で Window と閉じたい

https://stackoverflow.com/questions/28467285/how-do-i-bind-the-escape-key-to-close-this-window/28467330



## 新規 Window の title を設定する.

https://stackoverflow.com/questions/55620787/i-can-not-set-title-of-top-level

`.title()` で設定する. 

```
root = tkinter.Toplevel()
root.title("My title")
```

### ttkbootstrap

#### Theme builder

https://ttkbootstrap.readthedocs.io/en/latest/themes/themecreator/

`python -m ttkcreator`

#### フォントの大きさを変える.

全体のフォントサイズを変える

```
style.configure(".",font=("",14))
```

は ttkbootstrap では使えない.

ttkbootstrap では TButton など個別にフォント設定できるのはあるが, Treeview は個別フォント設定できない.
"TkDefaultFont" の設定を使っている

したがって ttkbootstrap で全体のフォントサイズを変えるなら

```
from tkinter import font
default_font = font.nametofont("TkDefaultFont")
default_font.configure(size=32)
```

とする.

widget ごとの場合,

`style.configure` で新しくスタイルを作り,
widget に `style=` でアサインが必要.

https://stackoverflow.com/questions/37068708/how-to-change-font-size-in-ttk-button

* treeview :  https://memopy.hatenadiary.jp/entry/2017/06/04/093310
(ただしこれは ttkbootstrap では使えないかもしれません)

## Label の文字列の変更

Label の文字列などは, configure でパラメータを変えることができます.

```py
label = ttk.Label(text="Hello")
label.configure(text="muda")
```

## treeview でセルや columnごとに色を変える

row ごとはできますが, セルごとや column ごとは簡単にはできません.

https://stackoverflow.com/questions/48358084/how-to-change-the-foreground-or-background-colour-of-a-selected-cell-in-tkinter

あたいで頑張ればいけるかもしれません.

## ウィンドウをスクロールする.

ttkbootstrap では ScrolledFrame があります.

https://ttkbootstrap.readthedocs.io/en/latest/api/scrolled/scrolledframe/

素の tkinter の場合は以下.

【Python/Tkinter】スクロール可能なFrameを作成するクラス
https://qiita.com/shinno1993/items/3ea14ffd7f17d8214961

ありがとうございます.

## Widgets

他の記事や検索を参照ください. e.g. 

PythonのTkinterを使ってみる
https://qiita.com/nnahito/items/ad1428a30738b3d93762

Tkinter ファイルを開くためのダイアログを表示する (tkinter.filedialog)
http://hanairosoft.skr.jp/programming/tkinter-open-file-dialog.html

### Checkbutton

Checkbutton では, `command` でチェック on/off されたときのコールバックを指定できます.
`.bind` でクリックを拾うやりかただと, チェックの state がうまく取得できないので注意です!

### レイアウト

2. Widget を配置しよう
https://www.shido.info/py/tkinter2.html

【tkinter】grid()を使ったWidgetの配置の方法
https://www.pytry3g.com/entry/grid-widget

ありがとうございます.

ImGui みたいに, 「ここで Widget を改行させる/改行させない」, みたいなレイアウト指示はできません.

うまく整列させたい場合, grid を使うことになるでしょう. grid では内部で row, column 数の最大値を管理しているようですので, 「3x3 のグリッドを作ります」みたいな事前定義はしないです(のでちょっとややこしい)

また, grid を使う場合, 関連する複数 Frame では `pack` ではなく `grid` を使います(混在できない. 混在しているとエラーが出る)

よりフレキシブルな配置をしたい場合, 上記 URL にあるように `place` を使うことになるでしょう.

### grid レイアウト

https://pythonguides.com/python-tkinter-grid/


### セパレータやラベルなど

セパレータやラベル(内容が変わらない)であれば直接インスタンスに対して `pack()`, `grid()` でいけます.

http://www.java2s.com/Code/Python/GUI-Tk/Usingthegridgeometrymanager.htm

### ImGui っぽいレイアウト

https://imagingsolution.net/program/python/tkinter/widget_layout_pack/

定義順で上から下に並べていく感じのものです.
基本は `side="top"` をすべての widget で指定すればいけるはず? 
右詰めしたい場合は `anchor="w"` とします.

`ImGui::SameLine()` のように改行せず横に配置していきたいときは

http://www.java2s.com/Code/Python/GUI-Tk/LayoutsideTOPLEFT.htm

でしょうか. ただ, この場合は一度 frame を作るのがよいかもしれません.

### パディング, スペーシング

padx, pady である程度調整できます. スカラー値だと, 両方にパディングが入ります.

https://stackoverflow.com/questions/4174575/adding-padding-to-a-tkinter-widget-only-on-one-side

`padx=(0, 100)` などと指定することで片方にパディングすることができます.

widget を作らずにスペース作りたい場合, 空の frame をつくることになるでしょうか.

https://stackoverflow.com/questions/52303983/how-to-add-a-specific-spacing-in-pixels-between-tkinter-buttons

他の widget も frame にすることがありちょっとめんどいです. `1x1` 画像を入れるというのも手かもしれません.

## Treeview に scrollbar をつける

縦スクロールバーの場合.

```python
treeview = ...
treeview.pack(side="left", fill="both", expand="yes")


vscrollbar = ttk.Scrollbar(frame, orient="vertical", command=treeview.yview)
treeview.configure(yscrollcommand=vscrollbar.set)
view_vscrollbar.pack(side="right", fill="y")
```

treeview には `fill="both"` と `expand="yes"` を指定することで, treeview のウィジェットが scrollbar 抜いたぶん最大で配置されるようになります.

`padx` などでスペースは調整して.
 
よりレイアウト細かく指定したいときは, `grid` or `place` を使ったほうがよいでしょう.
 
## Treeview cell で text wrap する.

直接的な方法は無いようです.

テキストを固定文字で wrap するなりので対応します.

https://stackoverflow.com/questions/51131812/wrap-text-inside-row-in-tkinter-treeview

https://github.com/jason990420/PySimpleGUI-Solution/issues/30

## Treeview row の高さを変える

style を作って rowheight を指定します.

https://stackoverflow.com/questions/51169603/changing-row-height-for-different-treeviews-tkitner

## 2D, 3D の plot

matplotlib と組み合わせるといいでしょうか.

https://resanaplaza.com/2021/08/14/%E3%80%90%E8%B6%85%E7%B0%A1%E5%8D%98%E3%80%91matplotlib%E3%81%A73d-plot-%E3%82%92%E3%83%9E%E3%82%A6%E3%82%B9%E3%81%A7%E3%82%B0%E3%83%AA%E3%82%B0%E3%83%AA%E5%9B%9E%E3%81%9D%E3%81%86%EF%BC%81%EF%BC%88by/

matplotlib の pyplot もデフォルトでは tkinter を使うっぽい.

### matplotlib を別ウィンドウで pop up で出す.

matplotlib でプロット画面を出したい場合,
matplotlib の backend が tkinter の場合, そのままではメインウィンドウ(GUI)の tkinter のコンテキストとまざってうまくうごきません.

https://stackoverflow.com/questions/23802207/tkinter-matplotlib-figure-as-a-popup-window

を参考にして, 以下のような感じにします.

```
          root = tk.Toplevel() # ttk.Toplevel() when you use ttkbootstrap
          fig = plt.Figure()
          canvas = FigureCanvasTkAgg(fig, master=root)
          canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1.0)
          ax = fig.add_subplot(111)
          
          # test data
          x = [1, 2, 3, 4, 5] 
          y = [0.3, 0.5, 0.8, 1.2, 5.3]
  
          ax.plot(x,y)
          canvas.draw() # pop-ups another window
```

 

https://github.com/jarvisteach/appJar/issues/551

`FigureCanvasTkAgg.show()` は deprecated でエラーになります. `draw` を使います.


## 図形の描画

canvas で行う.

tkinterの使い方：Canvasクラスで図形を描画する
https://daeudaeu.com/tkinter_canvas_draw/


### 画像の表示

`tkinter.PhotoImage` 自体では PNG など一部サポートとなる.
jpg を読む場合は PIL などと組み合わせる.

canvas 自体は `create_image` で与えられた画像でよろしくリサイズはしてくれないので, 画像を表示したい場合 Canvas を作る時点で大きさの指定が必要になりますので注意ください.

PhotoImage で画像の大きさを取得する場合, `width()`, `height()` メソッドを使う.

https://stackoverflow.com/questions/36025267/how-to-find-out-size-of-a-photoimage-in-tkinter

Canvas では画像は中心基準で配置されるため, Canvas == 画像サイズにする場合, x, y は画像のサイズの半分を指定しないといけないので注意です!

また, Frame などをクラスで定義している場合, image のデータは保持しておかないと, 関数から抜けたときにガベコレで消されてしまい表示されないので注意


```py
from PIL import Image, ImageTk
= Label(window, image=img).pack()
    playsound("song.mp3",block=False)
    label.dra
def MyFrame(ttk.Frame):
    ...

    def build_widget(self):
        pil_img = Image.open(open(image_filename, 'rb'))
        self.img = ImageTk.PhotoImage(pil_img)  # <-

        canvas = ttk.Canvas(root, width=self.img.width(), height=self.img.height())
        canvas.create_image(self.img.width//2, self.img.height//2, image=self.img)

        # canvas も self.canvas なりで保存しておくのが望ましいかも
```

## numpy 配列からの画像データ生成

`Image.fromarray` でいけます!

https://stackoverflow.com/questions/53308708/how-to-display-an-image-from-a-numpy-array-in-tkinter

## Pixel 値の取得

たとえばマウスポインタ上の画像の色を取得したいときがあります.

`tk.Canvas` 自体には pixel 値を取得する方法はないようです.
画像の場合は, PhotoImage あたりと同じ大きさの Canvas を作り, PhotoImage のピクセル値を取得で対応になるでしょうか.

## Canvas の大きさの変更

`configure(width=..., height=...)` でいけます.

https://stackoverflow.com/questions/9589867/python-tkinter-change-the-canvas-size-after-inital-declaration

tkinter では, widget の設定を変えるときはだいたい `configure()` でいけるようです.

## 画像の入れ替え

create_image で id が返りますので, これを使って `itemconfigure()` で入れ替えをします.
(`canvas.configure(image=...)` だと `-image` が無いエラーになる(canvas は複数の描画要素をもつため, `image` だけでは判断できない(前のバージョンだとできるかも?))

```python
image_id = canvas.create_image(..., image=...)

...

canvas.itemconfigure(image_id, image=new_img)
```

他の canvas 要素(`create_rectangle` など)も同様です.


## OpenGL

pip で入る pyopengltk がいいでしょうか.
ただ pyopengltk が依存する PyOpenGL は Windows だとビルドが走るため, MSVC などインストールしておく必要があります.

MSVC インストールとかめんどい場合は prebuilt を使うか,

https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyopengl

conda で環境を作っている場合は `conda install pyopengl` で conda ビルドの PyOpenGL をインストールしておくとよいでしょう.

DLL からシンボルを直接読み込むという手もあります.

https://www.codeproject.com/Articles/1073475/OpenGL-in-Python-with-TKinter


## GUI コードの live reload

やや複雑な GUI だったりレイアウト調整とか,  スクリプト書き直して再実行... ではめんどいですよね.

`exec()` あたりで動的実行するといいかもです!

## Table で cell edit できるようにする.

https://stackoverflow.com/questions/18562123/how-to-make-ttk-treeviews-rows-editable

クリックしたら Entry widget を動的に作って対応します.
 
ただ, ttkbootstrap 使っているとレイアウトが崩れますので微調整で対応しましょう(bbox の値とかにオフセットを与えたりなど)

## undo/redo 機能

自前でがんばります!

Python tkinter で undo/redo をやりたいメモ
https://qiita.com/syoyo/items/f1191ebdac2031d67537

## アプリ化

pyinstaller でいけます.

ただ, ttkbootstrap を含める場合, ttkbootstrap v1.7.0 以前では, Windows で空白があると起動できない issue があります.

https://github.com/israel-dryer/ttkbootstrap/issues/184#issuecomment-1032303423

v1.7.1 以上を使いましょう(2022/03/01 時点ではこのバージョン以上になっています).