Глава 16 · Создаём классные приложения с Tkinter
after(): таймеры без блокировки
Нужно подождать — не значит нужно остановить всё приложение.
after() — отложенный вызов без блокировки
after_prosto.py
after_id = root.after(1000, lambda: print("Прошла секунда"))
# программа продолжает обрабатывать другие события, ожидая эту секунду
событийный цикл
↓
запланировать через after()
↓
другие события продолжают обрабатываться
callback вызывается
↓время подошло
«Через секунду» — не хронометр с точностью до миллисекунды
after(1000, callback) означает «сделай callback готовым к выполнению примерно через 1000 мс, когда событийный цикл сможет его обработать» — не гарантию исполнения ровно в указанную миллисекунду. Если в этот момент цикл занят другим событием, вызов произойдёт чуть позже.Повторяющийся таймер (self-rescheduling)
tik.py
after_id = None
def tik():
global after_id
label.config(text=f"Секунд: {schet.get()}")
schet.set(schet.get() + 1)
after_id = root.after(1000, tik) # сам себя планирует заново — вот и повторение
Не только for/while — это тоже повторение
Из главы 10 мы знаем циклы
for/while. Повторяющийся вызов через self-scheduling after() — ещё один способ организовать повторение, устроенный на событиях, а не на последовательном блоке кода.Каждый новый after() должен обновлять сохранённый id
У
tik() — новый вызов after() на каждом тике, и, значит, новый идентификатор. Если не переприсваивать after_id заново при каждом вызове (как выше), сохранённая переменная быстро устареет — будет указывать на уже сработавший вызов, а не на актуально ожидающий, и попытка остановить таймер по этому id ничего не остановит.Остановка таймера: after_cancel
after_cancel.py
def stop():
global after_id
if after_id is not None:
root.after_cancel(after_id)
after_id = None
Не потеряйте идентификатор
Чтобы остановить запланированный вызов, нужно сохранить актуальный идентификатор, который вернул последний вызов
after(). Без него отменить именно этот запланированный вызов не получится. Полную трёхсостояньевую модель (остановлен/идёт/завершён) с корректным after_id смотрите в мини-проекте «Таймер» (раздел 16.28).Debug Lab 3: Повторный запуск создаёт дублирующиеся таймеры
duplicate_after.py
def start():
tik() # каждый клик по «Старт» запускает ЕЩЁ один цикл tik()
ttk.Button(root, text="Старт", command=start).pack()
# После нескольких нажатий «Старт» счётчик начинает прыгать сразу на 2, 3, 4 за секунду —
# работает уже несколько параллельных цепочек after().
Каждый клик запускает новую независимую цепочку самопланирующихся вызовов tik(), и они не заменяют друг друга, а работают одновременно. Нужно блокировать повторный старт через общий флаг running — и этот же флаг должна проверять сама tik(), иначе цепочка не остановится даже после stop().
Исправленный код
duplicate_after_fixed.py
running = False
after_id = None
def tik():
global after_id, running
if not running:
return # stop() уже сбросил флаг — новую цепочку не планируем
label.config(text=f"Секунд: {schet.get()}")
schet.set(schet.get() + 1)
after_id = root.after(1000, tik)
def start():
global running
if running:
return # уже запущено — повторный клик игнорируем
running = True
tik()
def stop():
global running, after_id
running = False
if after_id is not None:
root.after_cancel(after_id)
after_id = None
Никогда time.sleep() внутри callback
time.sleep(...) в обработчике останавливает событийный цикл целиком на всё время сна — интерфейс перестаёт отвечать. Подробный разбор — в разделе 16.32.Практика: таймер обратного отсчёта через after()
Модуль tkinter открывает нативное окно Python — выполните локально в VS Code, PyCharm или Jupyter
Практика выполняется локально