LoginSignup
25
43

More than 3 years have passed since last update.

Python Tkinter 入門メモ

Last updated at Posted at 2020-01-31

背景

  • 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の場合明示的に管理するらしい。

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

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)

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

標準ダイアログ

FileChooserやColorPicker、MessageBoxなどもあるようだ。

25
43
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
43