背景
- 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にさわるまで応答しないのが面倒くさい
Windowの作成
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
- tkinter - TkとToplevelの違い | tkinter Tutorial
(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
- 閉じるボタンがクリックされた時にアプリケーションを終了するように設定する | Swingを使ってみよう
Root windowを非表示にしたアプリケーションの作成
tk.ToplevelによりWindowを表示し、特定のWindowが閉じられたときMain loopが終了するようにする。
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に相当)で検出できる。
window.protocol('WM_DELETE_WINDOW', root.destroy)
結局のところ、1枚のWindow+モーダルのようなアプリケーションでは素直にTkをメインのWindowとして扱ったほうがコードが単純になるだろう。
Frameの配置
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']
)で設定できる。
Label
Label(VB.NET/Java)/UILabel(iOS)/TextView(Android)的なやつ。
label = tk.Label(root, text='Hello')
# label['text'] = 'Another text'
label.place(x=8, y=8)
Buttonとイベントハンドラ
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>
を指定することで、すべてのマウスボタンのクリックを取得できるようだ。
Widgetの大きさ(pixel)を調べる
# 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は自動で拡張された。
画像の表示
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()
テキスト入力(1行)
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()
で取得できる。
テキスト入力(複数行)
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)時の挙動に違和感(選択中のテキストが上書きされず残る)。コンテキストメニューもなく、シンプル。
スクロールバー付きテキスト入力
from tkinter.scrolledtext import ScrolledText
text = ScrolledText(root, width=30, height=10)
# あとは同じ
キー入力を受け取る
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について(Perl/Tk-Basics)
place, pack, gridを使った配置
Widgetの配置にpack、gridを使うことで流し込みができる。
Widgetの削除(除去)
widget.pack_forget()
2020/02/02 追記:placeで配置したものにpack_forgetを適用してもうまく消えてくれなかった。destroy
を使うのがよさそう。
widget.destroy()
その他参考
Canvasに描画する
全画面表示
root.attributes('-fullscreen', True)
# root.update()
# print(root.winfo_width(), root.winfo_height())
Alt+F4で閉じる。
最前面表示
root.wm_attributes('-topmost', 1)
Escape、Ctrl+Wで閉じる
root.bind('<Escape>', lambda e: root.destroy())
# root.bind('<Key-Escape>', lambda e: root.destroy())
root.bind('<Control-w>', lambda e: root.destroy())
フォントサイズの変更
ホバー時の色
Combobox
- ドロップダウンリストの作成【python tkinter sqlite3で家計簿を作る】 - memopy
- python - Intercept event when combobox edited - Stack Overflow
標準ダイアログ
FileChooserやColorPicker、MessageBoxなどもあるようだ。