資格勉強のため、更新を一時中断しております。今年中にまた戻って来ます!

【Tkinter】afterの使い方!タイマー・繰り返し処理を実装!

Tkinter after解説 GUI遅延処理

こんにちは、Youtaです。

今回は、Tkinterのafterをご紹介します。

すずか
すずか

Tkinterでプログラム作ったけど、フリーズするんだよね。

「処理中…」って文言も出ないし。

先生
先生

もしかしてtime.sleep 使っていませんか?

Tkinter の afterを利用すると解決しますよ。

すずか
すずか

after?何それ?

この記事では、Tkinterで「遅延処理」を司るafterメソッドを解説します。

基本的な使い方から応用例まで、実践的なコード例を交えて紹介します。

ぜひ最後までご覧ください。

after

afterは「指定時間後にこの処理を行ってね」と予約を入れる機能です。

言い換えると、処理を好きな時間だけ遅らせることができます。

処理は一度しか呼ばれません。

ただし、定刻に処理が呼ばれるかは、その時のTkinterの忙しさによります。

例えば重い処理が一緒に動いている場合、処理が遅れて実行されるかもしれません。

確実なのは、指定した時間よりも先に実行されないということですね。

基本構文

afterの構文は以下の通りです。

ウィジェットのメソッドとして呼びます。

ウィジェット.after(ms, func=None, *args)

引数のmsfuncは必ず覚えましょう。

*argsの存在はあまり知られていない印象です。

引数必須/任意説明
msint必須遅延時間(ミリ秒)
funccallable任意実行したい関数
*argsfuncの引数の型任意funcに渡す引数
afterメソッドの引数

戻り値は「タイマーID」(str型)です。

このIDは予約をキャンセルする際に使用します(後述)。

使い方

afterの使い方を3パターンに分けてご紹介します。

気になる章からご覧ください。

単発の遅延処理

afterの最も基本的な使い方です。

一定時間後に一度だけ処理を実行します。

以下は、GUI起動後に3秒経過したらラベルを消すプログラムです。

import tkinter as tk

def hide_label():
    label.pack_forget()           # 3 秒後にラベルを消す

root = tk.Tk()

label = tk.Label(root, text="3 秒後に消えます")
label.pack()

root.after(3000, hide_label)  # ms, callback の2つだけ
root.mainloop()
先生
先生

ポイントを2つ解説します。

まずはなんと言ってもafterメソッド。

時間の単位はミリ秒なので、3000は3秒を意味します(1000ミリ秒=1秒)。

root.after(3000, hide_label)

afterrootではなくlabelからも呼べます。

任意のウィジェットで呼び出せますし、挙動はまったく同じです。

# root.after(3000, hide_label)
label.after(3000, hide_label)

また、時間の後には呼びたい処理を渡します。

今回のhide_labelのように、関数名を直接渡してください。

def hide_label():
    label.pack_forget()

root.after(3000, hide_label)

(pack_forgetメソッドはpackで配置したウィジェットを取り除く機能です。)

>>【Tkinter】pack_forgetメソッドを解説!ウィジェットを非表示にできる!

ちなみに、第二引数のfuncは関数名だけでなくlambda式でもOKです。

lambda式で直接pack_forgetメソッドを呼び出すとこうなります。

root.after(3000, lambda: label.pack_forget())

>>【Python】lambda式(無名関数)をわかりやすく解説!

さて、早速プログラムを起動しましょう。

ピッタリ3秒後にlabelが消えましたね。

繰り返し処理

after定期的に処理を実行させることもできます。

作れるプログラムの幅がぐっと広がる非常に便利な機能です。

結論、コールバック関数の中で再度afterを呼びます。

時間差で呼ばれる再帰関数のようなイメージです。

def func():
    ...
    # 1秒後に再度funcを呼ぶ
    root.after(1000, func)

再帰的なコールバックを定義した後は、好きなタイミングでfunc関数を呼びます。

これで、1秒ごとに自動で処理を行う無限ループの完成です。

なんだかドミノ倒しに似ていて面白いですよね。

func()

早速、具体例を見てみましょう。

例えば、時計なんかは絶好の例です。

import tkinter as tk
from datetime import datetime

def update_time():
    current_time = datetime.now().strftime("%H:%M:%S")
    time_label.config(text=current_time)
    
    # 1秒後に再度この関数を呼び出す
    root.after(1000, update_time)

root = tk.Tk()

time_label = tk.Label(root, text="")
time_label.pack()

# 最初の実行
update_time()

root.mainloop()

注目ポイントはupdate_time関数です。

time_labelの時間表示を更新した後、1秒後に再び自身を呼んでいます。

これにより、毎秒時間が更新される時計になるわけです。

def update_time():
    current_time = datetime.now().strftime("%H:%M:%S")
    time_label.config(text=current_time)
    
    # 1秒後に再度この関数を呼び出す
    root.after(1000, update_time)

...

# 最初の実行
update_time()

こんな簡単にデジタルな時計が作れてしまいます。

やはり動的なプログラムはテンション上がりますよね。

ちなみに、アナログな時計にも応用可能です。

>>【Tkinter】アナログ時計の作り方!プログラミング練習に!

コールバックに引数を渡す

afterで登録する関数に引数を渡す方法をご紹介します。

結論、渡す引数をafterの第3引数以降に指定します。

その際は指定した引数の順序でコールバックに渡っていきます。

先生
先生

可変長引数なので、いくつ渡してもOKです。

例として、countnameという2つの引数をgreet関数に渡してみます。

import tkinter as tk

def greet(count, name):
    label.config(text=f"{count}秒経過。こんにちは、{name}さん!")

label = tk.Label(width=30)
label.pack()
label.after(2000, greet, 2, "Mike")   # *args 部分にそのまま列記
label.mainloop()

初めは何も表示されていませんが…。

2秒後、ラベルに文字が表示されました。

afterに渡した引数もそのまま現れています。

ついでなのでもう1つ別のやり方を紹介します。

lambda式で引数をセットしたコールバックをafterに渡してもOKです。

label.after(2000, lambda: greet(2, "Mike"))
after・ after_idleのキーワード引数

Python 3.14+からafterと after_idleキーワード引数(*kwargs) を渡せます。 

詳細は公式ドキュメントからご覧ください。

例えば、1秒後に画面を赤色にする処理があります。
従来、lambdaを使って簡潔に書くと下記のようになります。

import tkinter

root = tkinter.Tk()
root.after(1000, lambda: root.configure(bg="red"))
root.mainloop()

では、Python 3.14+のafterのキーワード引数を使って書き換えてみましょう。

わずかな差ですが、lambdaを使うよりはスッキリします。

もちろん、従来の可変長引数(*args)も使えますよ。

import tkinter as tk

root = tkinter.Tk()
root.after(1000, root.configure, bg="red")
root.mainloop()

※ コードはAdd **kw to tkinter.Misc.after and tkinter.Misc.after_idle #126899より引用

after_cancel

after_cancelafterで登録した処理をキャンセルします。

キャンセルにはafterの戻り値である「タイマーID」が必要です。

変数に入れるなどをして事前に控えておきましょう。

基本構文

after_cancelの構文は以下の通りです。

ウィジェット.after_cancel(id)

after_cancelの引数です。

引数必須/任意説明
idstr必須取り消したいafterの戻り値
after_cancelメソッドの引数

戻り値はありません。

使い方

after_cancelの使い方は簡単です。

afterの戻り値「タイマーID」を渡すと、その予約を取り消します。

timer_id = root.after(1000, some_func)  # ← 整数や文字列の ID
...
root.after_cancel(timer_id)             # これで停止

それでは実際の例を見てみましょう。

afterafter_cancelを使って文字の点滅を停止・再開させるデモを作りました。

import tkinter as tk

# 点滅ロジック
timer_id = None  # afterが返すタイマーID
is_visible  = True  # 現在の表示状態

def blink():
    global timer_id, is_visible
    is_visible = not is_visible

    if is_visible:
        center_x = canvas.winfo_width() / 2
        center_y = canvas.winfo_height() / 2
        
        canvas.create_text(center_x, center_y, text="BLINK", anchor="center")
    else:
        canvas.delete("all")

    timer_id = root.after(300, blink)  # 0.3 秒後に自分を再登録

# ボタンコマンド
def start():
    blink()

    start_button.config(state="disabled")
    stop_button.config(state="normal")

def stop():
    global timer_id
    root.after_cancel(timer_id)  # ここが after_cancel

    start_button.config(state="normal")
    stop_button.config(state="disabled")

# GUI作成
root = tk.Tk()

canvas = tk.Canvas(root)
canvas.pack()

start_button = tk.Button(root, text="Start", command=start)
start_button.pack(side="left", expand=True, fill="x")

stop_button = tk.Button(root, text="Stop", command=stop)
stop_button.pack(side="left", expand=True, fill="x")

root.mainloop()

簡単にコードの説明をします。

まずstart_buttonを押すと、afterの効果で文字が0.3 秒ごとに点滅します。

def blink():
    ...
    timer_id = root.after(300, blink)  # 0.3 秒後に自分を再登録

def start():
    blink()

    start_button.config(state="disabled")
    stop_button.config(state="normal")

start_button = tk.Button(root, text="Start", command=start)

そしてstop_buttonを押すと、after_cancelが働いて点滅が停止します。

blink関数内で保持したtimer_idを利用しているのにご注目ください。

def blink():
    ...
    timer_id = root.after(300, blink)  # 0.3 秒後に自分を再登録

def stop():
    global timer_id
    root.after_cancel(timer_id)  # ここが after_cancel

    start_button.config(state="normal")
    stop_button.config(state="disabled")

stop_button = tk.Button(root, text="Stop", command=stop)

画面を起動してstart_buttonを押すと文字の点滅が始まります。

途中でstop_buttonを押すと点滅が止まります。

afterに似ている処理

afterと混同しがちな処理を2つピックアップします。

特に遅延処理をafterではなくtime.sleepを使って失敗する方が多いです。

afterメソッドに似ている処理

ここからは、Tkinterのメインループを理解している前提で説明します。

すずか
すずか

メインループって何?

このような方は、一旦次の「メインループ」をお読みください。

Tkinterのアプリはmainloopを呼び出すことで動作しますね。

そう、コードの末尾にあったおまじないのことです。

...
root.mainloop()  # イベントループ開始

実はこのmainloop無限ループ開始の合図なのです。

この無限ループは「イベント」を待機して処理するループです。

「イベント」とは、例えばクリック、キー入力、マウス移動などを指します。

このメインループ内で、Tkinterは次の処理を高速で繰り返します。

Tkinterのメインループ
  1. イベントの発生をチェック
  2. イベントがあれば対応する処理を実行
  3. 画面の描画更新
  4. ①に戻る

メインループの予備知識としてはこれで十分です。

以上を踏まえ、次章のtime.sleepをご覧ください。

time.sleep

time.sleepは「一定時間待つ」という点でafterと似ています。

しかし、決定的な違いがあります。

それは、time.sleepはメインループを停止させてしまう点です。

冒頭のフリーズするプログラムを例に出します。

何が問題かを一緒に考えていきましょう。

import time
import tkinter as tk

def on_click():
    button.config(text="処理中…")
    time.sleep(3)            # ← ここで GUI が 3 秒固まる
    button.config(text="完了!")

button = tk.Button(text="押してね", width=5, command=on_click)
button.pack()

button.mainloop()
すずか
すずか

これがやりたいことね。

  1. ボタン押下
  2. 「処理中…」表示
  3. 3秒待機
  4. 「完了!」表示
先生
先生

けれども、
①→3秒フリーズ→④となるのが問題でしたね。

time.sleepメインループを含む一才の処理を一時停止させます。

つまり、その間はイベントの発生を検知しても画面を更新できません。

したがって、ユーザーからするとフリーズに見えるのです。

ではどうしましょうか。

メインループを停止させずに「一定時間待つ」afterを使えば良いのです。

プログラムを書き換えましょう。

def on_click():
    button.config(text="処理中…")

    # 修正前
    # time.sleep(3)
    # button.config(text="完了!")

    # 修正後
    button.after(3000, lambda: button.config(text="完了!"))

①→②→③→④がフリーズ無しに実行できました。

すずか
すずか

わーい🙌

after_idle

after_idleは「暇なときにこの処理を行ってね」と予約を入れる機能です。

「暇なとき」とは、メインループが処理すべきイベントがない状態です。

基本構文

after_idleの構文は以下の通りです。

ウィジェット.after_idle(func, *args)

引数はafterとほぼ同じです。

引数必須/任意説明
funccallable必須実行したい関数
*argsfuncの引数の型任意funcに渡す引数
after_idleメソッドの引数

戻り値はafterと同様に「タイマーID」(str型)です。

このIDはafter_cancelで予約を取り消す際に使用します。

afterとの違い

afterは一定時間後に、after_idleはイベントが一段落すると実行されます。

つまりタイミングがafter時間after_idleシステムの状態に依存します。

すずか
すずか

んー何となくわかる気がするけど
多分わかってない。

先生
先生

イメージが湧かないですよね…。
では「いつどちらが呼ばれるのか?」を明確に可視化しましょう。

実は、両者の違いを把握するのに欠かせないのがメインループの状態です。

そのため「暇な状態」と「忙しい状態」の2パターンで試します。

import time
import tkinter as tk

# 共通のコールバック
def on_after():
    print(f"after      {time.perf_counter() - start:6.3f}s")

def on_after_idle():
    print(f"after_idle {time.perf_counter() - start:6.3f}s")

# メインループの状態を場合分け
def case_nonblocking():
    # ① メインループが暇な状態になる普通のケース
    global start
    start = time.perf_counter()

    print("\n--- non-blocking run ---")
    root.after(2000, on_after)      # 2秒後
    root.after_idle(on_after_idle)  # 次に暇になった瞬間
    # ここで何もしないのでメインループはすぐ暇な状態

def case_blocking():
    # ② メインループを3秒ブロックするケース
    global start
    start = time.perf_counter()

    print("\n--- blocking run ---")
    root.after(2000, on_after)      # 2秒後(でもあとで3秒ブロック)
    root.after_idle(on_after_idle)  # 暇になったら
    time.sleep(3)                   # ★メインループを3秒止める

# GUI作成
root = tk.Tk()

tk.Button(root, text="① 普通に実行", command=case_nonblocking).pack()
tk.Button(root, text="② 3秒ブロック付き実行", command=case_blocking).pack()

root.mainloop()
先生
先生

最初にコードの説明をします。

case_nonblocking関数はメインループが暇な状態のパターンです。

afterを「2秒後」に、after_idleを「暇な状態になったらすぐ」に実行します。

メインループが何もブロックされないのがポイントです。

def case_nonblocking():
    ...
    root.after(2000, on_after)
    root.after_idle(on_after_idle)

tk.Button(root, text="① 普通に実行", command=case_nonblocking).pack()

一方case_blocking関数はメインループが忙しい状態のパターンです。

先ほどと同じですが、最後に 3秒間time.sleepでメインループをブロックします。

これで、処理が重すぎて関数終了まで3秒掛かる状況を再現できます。

def case_blocking():
    ...
    root.after(2000, on_after)
    root.after_idle(on_after_idle)
    time.sleep(3)

tk.Button(root, text="② 3秒ブロック付き実行", command=case_blocking).pack()

画面を立ち上げて実行しましょう。

まずメインループが暇な状態を試すので、①のボタンを押します。

以下がコンソール出力です。

--- non‑blocking run ---
after_idle  0.001s
after       2.001s

after_idleが即実行され、2秒後にafterが実行されていますね。

after実行まで暇なメインループが、先にafter_idleを呼んだからです。

次にメインループが忙しい状態を試すので、②のボタンを押します。

以下がコンソール出力です。

--- blocking run ---
after       3.002s
after_idle  3.002s

3秒間メインループは止まっており、2秒後に実行予定だったafterは呼べませんでした。

そのため、メインループ再開時に「期限切れ」のafterが最優先で実行されたのです。

その後、メインループは暇になりafter_idleが呼ばれました。

呼ばれるタイミングの違いについて理解できましたでしょうか。

改めてafterafter_idleの違いをまとめます。

メソッド目的実行タイミング
after一定時間後に処理したいメインループが空いていればその時間後に、空いてなければ遅れて実行される。
after_idle他の処理がすべて終わったあとに1回だけ処理したい他にやることがない」状態 になると実行される。
after vs after_idle

以上のようにafter_idleは暇なときに呼ばれる特性があります。

そのため低優先度のバックグラウンド処理等に用いると良いでしょう。

まとめ

今回はTkinterの遅延処理を担うafterを解説しました。

最後に復習してから終わりましょう。

afterは「指定時間後にこの処理を行ってね」と予約を入れる機能です。

msにミリ秒、funcに関数、*argsfuncの引数を渡します。

ウィジェット.after(ms, func=None, *args)

特に「単発遅延」と「繰り返し処理」が大切です。

after_cancelafterで登録した処理をキャンセルできます。

引数の「タイマーID」はafterの戻り値です。

ウィジェット.after_cancel(id)

また、afterと混同しがちな処理を2つ上げました。

しっかりと区別できるようになりましょう。

afterメソッドに似ている処理

この記事が皆様のお役に立てれば幸いです。

ご覧いただきありがとうございました。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA