Python Tkinter 入門メモ

## 背景
- Visual Studio（VB.NET）、Android Studio（Java）、XCode（Swift）では標準のGUIデザイナで楽にGUI開発してた/してる（Desktop AppはVB.NET→Javaに移行、C#はUnityでしか使ってない）
- JavaではAWT/Swingでデザイナ使わずにGUI組んでた（Layout）
- メインの言語がJava→Pythonに移行してからCUIツール/Webばかり作ってるので、Desktop GUIも作りたい気持ち

高性能というわけではない（透過まわりや標準機能面で不便な点あり）が、標準なので備忘録として自分用にまとめておく。

\# Ctrl+Cで終了しようとすると、Windowにさわるまで応答しないのが面倒くさい

- [tkinter --- Tcl/Tk の Python インタフェース — Python 3.8.1 ドキュメント](https://docs.python.org/ja/3/library/tkinter.html)

## Windowの作成
```python
import tkinter as tk

root = tk.Tk() # Window
root.title('Hello Window') # Window title
root.geometry('%dx%d' % (400, 400)) # Window size (width, height)

# ここでWidgetの初期化、配置

root.mainloop()
```

`root.mainloop()`でブロックする。Windowが閉じられるとMain loopを抜ける。

### メインループ
正確には、TkはWindowというよりTkinterのメインループを管理するもののようだ。モーダルなどを使うためマルチウインドウ化するときは`tk.Toplevel`で第2、第3のWindowを作成できる（Form=VB.NET、Frame=Javaに相当）。Javaの場合、勝手にGUI用のループを実行するスレッドが立つ（main関数を実行するMain Threadが終了しても生き続ける）ものだったと思うので気にしたことはなかったが、Tkinterの場合明示的に管理するらしい。

- [python - How to create a modal dialog in tkinter? - Stack Overflow](https://stackoverflow.com/questions/16803686/how-to-create-a-modal-dialog-in-tkinter)
- [tkinter - TkとToplevelの違い | tkinter Tutorial](https://riptutorial.com/ja/tkinter/example/22130/tk%E3%81%A8toplevel%E3%81%AE%E9%81%95%E3%81%84)

（tk.Toplevelを使用していて）Tk自体がRoot windowとして存在しているのがわずらわしい場合、`root.withdraw()`で消すことができる。が、「Windowが閉じられた＝Main loopを終了＝プログラムを終了」のロジックが働かなくなることになる（SwingのDefaultCloseOperationを思わせる）ので、Main loopを終了するためアプリケーション終了時に`root.destroy()`を呼び出す。

- [winapi - How do I get rid of Python Tkinter root window? - Stack Overflow](https://stackoverflow.com/questions/1406145/how-do-i-get-rid-of-python-tkinter-root-window)
- [閉じるボタンがクリックされた時にアプリケーションを終了するように設定する | Swingを使ってみよう](https://www.javadrive.jp/tutorial/jframe/index4.html)


### Root windowを非表示にしたアプリケーションの作成
tk.ToplevelによりWindowを表示し、特定のWindowが閉じられたときMain loopが終了するようにする。

```python
import tkinter as tk

root = tk.Tk()
root.withdraw()

window = tk.Toplevel(root)
window.title('Hello Window')
window.geometry('%dx%d' % (400, 400))
window.protocol('WM_DELETE_WINDOW', root.destroy)

# ここでWidgetの初期化、配置

root.mainloop()
```

`window`が閉じられたことは`window.protocol('WM_DELETE_WINDOW', handler_func)`（SwingのWindowListener\#windowClosingに相当）で検出できる。

```python
window.protocol('WM_DELETE_WINDOW', root.destroy)
```

結局のところ、1枚のWindow+モーダルのようなアプリケーションでは素直にTkをメインのWindowとして扱ったほうがコードが単純になるだろう。

- [python - How do I handle the window close event in Tkinter? - Stack Overflow](https://stackoverflow.com/questions/111155/how-do-i-handle-the-window-close-event-in-tkinter)

## Frameの配置
```python
frame = tk.Frame(root)
# frame = tk.Frame(root, bg='#FF00FF')
# frame['bg'] = '#FF00FF'
frame.place(x=8, y=8, width=200, height=200)
```

Panel（VB.NET/Java）/UIView（iOS）/ViewGroup（Android）的なやつ。Widgetのコンストラクタにbgとしてカラーコードを渡すと背景色を設定できる。初期化後に背景色を設定したい場合はディスクリプタ経由（`widget['bg']`）で設定できる。

- [python tkinter 色(color)の設定方法 - memopy](http://memopy.hatenadiary.jp/entry/2017/06/11/092554)
- [ディスクリプタを制する者は Python を制す - Qiita](https://qiita.com/koshigoe/items/848ddc0272b3cee92134)

## Label
Label（VB.NET/Java）/UILabel（iOS）/TextView（Android）的なやつ。

```python
label = tk.Label(root, text='Hello')
# label['text'] = 'Another text'
label.place(x=8, y=8)
```

- [PythonのGUIツールtkinterチートシート - Qiita](https://qiita.com/nanako_ut/items/b5393363b9e21d6342ea)

## Buttonとイベントハンドラ
```python
def onHelloClicked(event):
    print(event) # <ButtonPress event num=BUTTON_NUM x=MOUSE_X y=MOUSE_Y>
    # print(event.x, event.y, event.num)

button = tk.Button(root, text='Hello')
# button = tk.Button(root, text='Hello', width=10)
# button['text'] = 'Another text'

button.bind('<Button-1>', onHelloClicked)
# button.bind('<Button-2>', onHelloClicked)
# button.bind('<Button-3>', onHelloClicked)
# button.bind('<Button>', onHelloClicked)

button.place(x=8, y=8)
# button.place(x=8, y=8, width=60)
# button.place(x=8, y=8, width=60, height=31)
```

Widgetのコンストラクタに`width`を渡せるが、単位がpixelでなくわかりにくい（文字数？）。

bindでイベントハンドラを登録できる。`<Button-*>`はマウスボタンのクリックを表す（ややこしいが、Widgetのtk.Buttonとは関係ない）。`<Button-1>`は左クリック、`<Button-2>`は中クリック、`<Button-3>`は右クリック。`event.num`にはボタン番号が入る（左クリックのとき`event.num=1`、右クリックのとき`event.num=3`。まぎらわしいが、クリック回数ではない）。また`<Button>`を指定することで、すべてのマウスボタンのクリックを取得できるようだ。

- [PythonのTkinterを使ってみる - Qiita](https://qiita.com/nnahito/items/ad1428a30738b3d93762)

### Widgetの大きさ（pixel）を調べる
```python
# initialize button
# place button
root.update()
print(button.winfo_width(), button.winfo_height())
# root.mainloop()
```

手元の環境では、Buttonにコンストラクタでwidthを渡した場合、winfo_widthは`36+(width-1)*8`だった。またデフォルト（文字数0）のwinfo_widthは28（pixel）だった。いずれのwidthも設定しない場合、与えられた文字列に応じてwinfo_widthが変動する。winfo_heightは改行を入れない場合31（pixel）で、改行を入れると改行が表示に反映され、winfo_heightは自動で拡張された。

- [python - How to find out the current widget size in tkinter? - Stack Overflow](https://stackoverflow.com/questions/3950687/how-to-find-out-the-current-widget-size-in-tkinter)


## 画像の表示
```python
from PIL import Image, ImageTk

# initialize root

image = Image.new('RGB', (128, 128), color=(255, 0, 255)) # 適当な画像
image_tk = ImageTk.PhotoImage(image)

image_view = tk.Label(root, image=image_tk)
image_view.place(x=8, y=8)

# root.mainloop()
```

- [PythonのGUIツールtkinterチートシート - Qiita](https://qiita.com/nanako_ut/items/b5393363b9e21d6342ea)

## テキスト入力（1行）

```python
entry = tk.Entry(root, width=10)

entry.insert(tk.END, 'Hello') # 初期値
print(entry.get()) # 'Hello'

entry.delete(0, tk.END) # クリア
print(entry.get()) # ''

entry.place(x=8, y=8)
```

入力された値は`entry.get()`で取得できる。

- [PythonのTkinterを使ってみる - Qiita](https://qiita.com/nnahito/items/ad1428a30738b3d93762)

## テキスト入力（複数行）

```python
text = tk.Text(root, width=30, height=10)

text.insert(tk.END, 'Hello') # 初期値
print(text.get('1.0', tk.END)) # 'Hello'

text.delete('1.0', tk.END) # クリア
print(text.get('1.0', tk.END)) # ''

text.place(x=8, y=8)
```

行数がheightを超えても自動でスクロールバーが出てきたりはしなかった。また、Ctrl+Aが効かなかったり（行頭へ移動？）、カット＆ペーストのショートカットとクリップボードは使えたが選択+貼り付け（Ctrl+V）時の挙動に違和感（選択中のテキストが上書きされず残る）。コンテキストメニューもなく、シンプル。

- [PythonのGUIツールtkinterチートシート - Qiita](https://qiita.com/nanako_ut/items/b5393363b9e21d6342ea)

### スクロールバー付きテキスト入力
```python
from tkinter.scrolledtext import ScrolledText

text = ScrolledText(root, width=30, height=10)
# あとは同じ
```

- [tkinterで忘れがちなこと色々 其の4](https://tonton7.blogspot.com/2017/02/tkinter-4.html#002)

## キー入力を受け取る

```python
def onKeyPressed(event):
    print(event)
    # <KeyPress event keysym=a keycode=38 char='a' x=205 y=132>
    # <KeyPress event keysym=Shift_L keycode=50 x=442 y=272>
    # <KeyPress event state=Shift keysym=A keycode=38 char='A' x=127 y=183>

    # print(event.keycode, event.char, event.state)
    # stateは数値（Modifier）
    # デフォルト0
    # Shiftで1（0b0001）、Ctrlで4（0b0100）、Altで8（0b1000）
    # Shift+Altで9（0b1001）、Win+Shiftで65（0b01000001）。

root.bind('<Key>', onKeyPressed)
```

- [bindとeventについて](http://www27.cs.kobe-u.ac.jp/~masa-n/misc/cmc/perltk/basic/bind.html)（Perl/Tk-Basics）


## place, pack, gridを使った配置
Widgetの配置にpack、gridを使うことで流し込みができる。

- [Tkinter 入門: 2. Widget を配置しよう](https://www.shido.info/py/tkinter2.html)

## Widgetの削除（除去）

```python
widget.pack_forget()
```

- [python - How to delete Tkinter widgets from a window? - Stack Overflow](https://stackoverflow.com/questions/12364981/how-to-delete-tkinter-widgets-from-a-window/12365098)

2020/02/02 追記：placeで配置したものにpack_forgetを適用してもうまく消えてくれなかった。`destroy`を使うのがよさそう。

```python
widget.destroy()
```

- [python - pack_forget() not working - Stack Overflow](https://stackoverflow.com/a/43155582)

## その他参考
### Canvasに描画する

- [【Python】Tkinterのcanvasを使ってみる - Qiita](https://qiita.com/nnahito/items/2ab3ad0f3adacc3314e6)

### 全画面表示

```python
root.attributes('-fullscreen', True)
# root.update()
# print(root.winfo_width(), root.winfo_height())
```

Alt+F4で閉じる。

- [Tkinterで全画面表示する方法 - Qiita](https://qiita.com/Guilty/items/741f88b2da97d5c40c1b)

### 最前面表示
```python
root.wm_attributes('-topmost', 1)
```

- [Binding Escape key in Tkinter GUI. - Raspberry Pi Forums](https://www.raspberrypi.org/forums/viewtopic.php?t=224773)

### Escape、Ctrl+Wで閉じる
```python
root.bind('<Escape>', lambda e: root.destroy())
# root.bind('<Key-Escape>', lambda e: root.destroy())

root.bind('<Control-w>', lambda e: root.destroy())
```

- [Binding Escape key in Tkinter GUI. - Raspberry Pi Forums](https://www.raspberrypi.org/forums/viewtopic.php?t=224773)
- [Tkinter 入門: 12. Tk.Text の使い方](https://www.shido.info/py/tkinter12.html)

### フォントサイズの変更
- [python tkinter フォント(font)の設定方法 - memopy](http://memopy.hatenadiary.jp/entry/2017/06/11/112619)

### ホバー時の色
- [python - Tkinter Hovering over Button -> Color change - Stack Overflow](https://stackoverflow.com/questions/49888623/tkinter-hovering-over-button-color-change)

### Combobox
- [ドロップダウンリストの作成【python tkinter sqlite3で家計簿を作る】 - memopy](http://memopy.hatenadiary.jp/entry/2017/06/01/231206)
- [python - Intercept event when combobox edited - Stack Overflow](https://stackoverflow.com/questions/8432419/intercept-event-when-combobox-edited)

### 標準ダイアログ
FileChooserやColorPicker、MessageBoxなどもあるようだ。

- [PythonのGUIツールtkinterチートシート - Qiita](https://qiita.com/nanako_ut/items/b5393363b9e21d6342ea#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E9%81%B8%E6%8A%9E)
- [tkinterで忘れがちなこと色々 其の4](https://tonton7.blogspot.com/2017/02/tkinter-4.html)