Глава 16 · Создаём классные приложения с Tkinter

Мини-проект: таймер обратного отсчёта

after() в деле: обратный отсчёт без единой блокирующей операции.

Таймер обратного отсчёта — образцовая демонстрация after() и состояния приложения.

format_time.py
def format_time(sekundy):
    minuty, ostatok = divmod(sekundy, 60)
    return f"{minuty:02d}:{ostatok:02d}"

print(format_time(65))    # 01:05
print(format_time(600))   # 10:00
Окно таймера: 00:42 и кнопки Старт, Стоп, Сброс
Стартовое отображение — до первого тика. Пример внешнего вида на среде курса. На вашей ОС шрифты, размеры и тема могут немного отличаться.

Три состояния таймера

STOPPED
running = False
after_id = None
Старт
RUNNING
running = True
after_id — активен
remaining достиг 0
FINISHED
running = False
after_id = None
remaining = 0
STOPPED → RUNNING → FINISHED — и обратно в STOPPED через Сброс (не показан отдельной стрелкой).
tajmer_gui.py
class TimerApp:
    def __init__(self, root, start_seconds=60):
        self.root = root
        self.start_seconds = start_seconds
        self.remaining = start_seconds
        self.running = False
        self.after_id = None
        self.label_var = tk.StringVar(value=format_time(self.remaining))
        ttk.Label(root, textvariable=self.label_var).pack()
        ttk.Button(root, text="Старт", command=self.start).pack()
        ttk.Button(root, text="Стоп", command=self.stop).pack()
        ttk.Button(root, text="Сброс", command=self.reset).pack()

    def start(self):
        if self.running or self.remaining <= 0:
            return   # уже идёт, или уже FINISHED — сначала Сброс
        self.running = True
        self._schedule_tick()

    def _schedule_tick(self):
        self.after_id = self.root.after(1000, self.tick)

    def tick(self):
        self.remaining -= 1
        self.label_var.set(format_time(self.remaining))
        if self.remaining <= 0:
            self.running = False
            self.after_id = None   # FINISHED — планировать больше нечего
            return
        self._schedule_tick()

    def stop(self):
        self.running = False
        if self.after_id is not None:
            self.root.after_cancel(self.after_id)
            self.after_id = None

    def reset(self):
        self.stop()
        self.remaining = self.start_seconds
        self.label_var.set(format_time(self.remaining))
after(1000, ...) — «примерно через секунду», не хронометр
after(1000, callback) означает «сделай callback готовым к выполнению примерно через 1000 мс, когда событийный цикл сможет его обработать» — не гарантию исполнения ровно в указанную миллисекунду. Если цикл в этот момент занят другим событием, вызов произойдёт чуть позже.
Debug Lab 6: Таймер продолжает тикать после «Стоп»
tajmer_bez_proverki.py
def tick(self):
    self.remaining -= 1
    self.label_var.set(format_time(self.remaining))
    self.after_id = self.root.after(1000, self.tick)   # планирует себя ВСЕГДА

def stop(self):
    self.running = False   # состояние поменяли, но tick() его не проверяет
# После нажатия «Стоп» таймер продолжает уменьшать remaining и тикать —
# флаг running изменился, но ничто не проверяет его внутри tick().
Что видно на экране

stop() лишь меняет флаг running, но сам метод tick() не проверяет его перед тем, как запланировать себя заново — цепочка after() продолжает жить независимо от флага.

Исправленный код
tajmer_s_proverkoj.py
def tick(self):
    if not self.running:   # проверяем состояние ПЕРЕД тем как продолжить
        return
    self.remaining -= 1
    self.label_var.set(format_time(self.remaining))
    self.after_id = self.root.after(1000, self.tick)
Практика: таймер обратного отсчёта
Открыть практику →