1. はじめに
Tkinterに存在する after() と after_idle() について記載する。
2. 定義
after
tkinter_init_.py にある after() の docstring:
Call function once after given time.
MS specifies the time in milliseconds. FUNC gives the function which shall be called. Additional parameters are given as parameters to the function call. Return identifier to cancel scheduling with after_cancel.
Google Translate 和訳:
指定された時間後に一度だけ関数を呼び出します。
MSはミリ秒単位で時間を指定します。FUNCは呼び出す関数を指定します。追加のパラメータは関数呼び出しの引数として渡されます。after_cancelを使用してスケジューリングをキャンセルするための識別子を返します。
after_idle
tkinter_init_.py にある after_idle() の docstring:
Call FUNC once if the Tcl main loop has no event to process.
Return an identifier to cancel the scheduling with after_cancel.
Google Translate 和訳:
Tclのメインループに処理すべきイベントがない場合、FUNCを一度呼び出します。
after_cancelでスケジューリングをキャンセルするための識別子を返します。
after と after_idle の比較
以上の定義より、次のような違いとなる。
| 関数 | 処理 |
|---|---|
after |
指定時間 [ms] 後に、指定した関数を実行する。 |
after_idle |
イベント処理が全て終わった後に、指定した関数を実行する。 |
この違いは、単なる実行タイミングの違いではなく、Tkinter のイベント処理の仕組みによって決まっている。これを理解するために、処理の流れを以下に示す。
3. イベントキューと処理の流れ
Tkinterでは、ユーザー操作やタイマー処理などは「イベント」としてキューに登録され、順番に処理される。処理の流れは次のようになる。
| No. | 内容 |
|---|---|
| 1 | マウス操作やキー入力などのイベント処理 |
| 2 | bind |
| 3 | command |
| 4 | after |
| 5 | after_idle |
after_idle が最後に処理されるのは、下記の検証コードの結果からも分かる。
import tkinter as tk
def on_button_click():
root.after_idle(lambda: print('after_idle'))
root.after(0, lambda: print('after'))
if __name__ == '__main__':
root = tk.Tk()
button = tk.Button(root, text='Button', command=on_button_click)
button.pack()
root.mainloop()
after
after_idle
4. 使用例
after
現在時刻の表示更新。
import tkinter as tk
from datetime import datetime
def on_closing():
"""ウィンドウを閉じる操作を検出したら、`after` を終了してウィンドウを破棄する。"""
if after_id:
root.after_cancel(after_id)
root.destroy()
def update_label():
"""現在日時の表示を更新する。"""
global after_id
clock_label.config(text=datetime.now().strftime('%Y-%m-%d (%a) %H:%M:%S'))
after_id = root.after(1000, update_label)
if __name__ == '__main__':
after_id = None
root = tk.Tk()
root.protocol('WM_DELETE_WINDOW', on_closing)
clock_label = tk.Label(root)
clock_label.pack()
root.after(0, update_label)
root.mainloop()
after_idle
ttk.Treeview().selection_set() や ttk.Treeview().selection_remove() を実行すると、 <<TreeviewSelect>> を発生させてしまう。
このため、ボタン押下時のコールバック内で selection_set() などを実行すると、その後に <<TreeviewSelect>> の処理が実行され、意図しない状態になる場合がある。
この問題を回避するために、after_idle を使う。これにより、bind に登録したコールバック処理などが終わった後に、任意の処理を実行できる。
下記コードでは、ttk.Treeview のレコード選択時に、ラベルに現在日時を表示する。
ボタン押下時は、最後のレコードを選択する。ただし、ボタン押下時は、ラベルの表示は更新しない。
import tkinter as tk
from datetime import datetime
from tkinter import ttk
def on_button_click():
global bind_disabled
bind_disabled = True
tree.selection_set(iid)
def func():
global bind_disabled
bind_disabled = False
root.after_idle(func)
def on_tree_select(_):
if bind_disabled:
return
label.config(text=datetime.now().strftime('%Y/%m/%d %H:%M:%S.%f'))
if __name__ == '__main__':
root = tk.Tk()
bind_disabled = False
root.rowconfigure(1, weight=1)
root.columnconfigure(0, weight=1)
top_frame = tk.Frame(root)
top_frame.grid(row=0, column=0, sticky=tk.EW)
button = ttk.Button(top_frame, text='Button', command=on_button_click)
button.grid(row=0, column=0, sticky=tk.W)
label = ttk.Label(top_frame)
label.grid(row=1, column=0, sticky=tk.W, pady=(10, 0))
bottom_frame = tk.Frame(root)
bottom_frame.grid(row=1, column=0, sticky=tk.NSEW)
bottom_frame.rowconfigure(0, weight=1)
bottom_frame.columnconfigure(0, weight=1)
tree = ttk.Treeview(
bottom_frame,
columns=(0,),
show='headings',
selectmode='browse'
)
tree.heading(0, text='Data')
tree.grid(row=0, column=0, sticky=tk.NSEW)
y_scrollbar = ttk.Scrollbar(
bottom_frame,
orient=tk.VERTICAL,
command=tree.yview
)
tree.config(yscrollcommand=y_scrollbar.set)
y_scrollbar.grid(row=0, column=1, sticky=tk.NS, padx=(10, 0))
iid = None
for i in range(10):
iid = tree.insert('', tk.END, values=(f'Data{i + 1}'),)
tree.bind('<<TreeviewSelect>>', on_tree_select)
root.mainloop()
5. まとめ
after は「時間による制御」、after_idle は「イベント処理完了後の制御」として使い分けるのが正しい使い方である。
特に、イベントの処理順序が問題となる場合には、after_idle を利用することで意図した動作を実現できる。