2025, Oct 31 05:47

Как избежать зависания прогрессбара в Tkinter: используем after вместо sleep

Почему прогрессбар Tkinter «зависает» при time.sleep и как это исправить: обновляйте через after, не блокируйте цикл событий — GUI останется отзывчивым.

Делать небольшие, переиспользуемые элементы интерфейса — отличная практика, но у кода GUI есть жёсткое правило: не блокируйте цикл обработки событий. Классический пример — окно прогресса Tkinter, которое мгновенно превращается в «Не отвечает», стоит его передвинуть или сменить фокус, хотя сам скрипт продолжает работать. Ниже — минимальная иллюстрация и простое решение.

Проблема: индикатор прогресса зависает во время работы

Окно перестаёт перерисовываться и обрабатывать события, потому что главный поток застрял в цикле с задержкой sleep. Логика программы продолжает выполняться, но интерфейс не получает времени на обработку.

from tkinter import *
from tkinter.ttk import *
from datetime import *
import time
def boot():
    started_at = datetime.now().replace(microsecond=0)
    print(started_at, "\n")
    total_units = 100
    root = Tk()
    pct_var = StringVar()
    status_var = StringVar()
    left_var = StringVar()
    done = 0
    step = 5
    pct_lbl = Label(root, textvariable=pct_var).pack()
    status_lbl = Label(root, textvariable=status_var).pack()
    left_lbl = Label(root, textvariable=left_var).pack()
    prog = Progressbar(root, orient=HORIZONTAL, length=300)
    prog.pack(pady=10)
    while (done < total_units):
        done += step
        time.sleep(1)
        prog['value'] += (step/total_units) * 100
        pct_var.set(str(int((done/total_units)*100)) + "%")
        status_var.set(str(done) + " files completed!")
        left_var.set(str(int(total_units - done)) + " left to complete.")
        root.update_idletasks()
    root.mainloop()
if __name__ == '__main__':
    boot()

Почему так происходит

Цикл вместе с time.sleep() в главном потоке блокирует основной цикл Tkinter. Пока цикл «спит» или выполняет работу, отложенные обновления и пользовательские события не обрабатываются, поэтому ОС помечает окно как не отвечающее. Вызова update_idletasks() недостаточно, потому что код не передаёт управление главному циклу достаточно регулярно.

Решение: планируйте работу с .after()

Вместо циклов и задержек планируйте периодические обновления через .after(). Так цикл событий остаётся свободен для перерисовок и действий пользователя, и индикатор прогресса остаётся отзывчивым.

from tkinter import *
from tkinter.ttk import *
from datetime import *
import time
def boot():
    started_at = datetime.now().replace(microsecond=0)
    print(started_at, "\n")
    total_units = 100
    root = Tk()
    pct_var = StringVar()
    status_var = StringVar()
    left_var = StringVar()
    done = 0
    step = 5
    pct_lbl = Label(root, textvariable=pct_var).pack()
    status_lbl = Label(root, textvariable=status_var).pack()
    left_lbl = Label(root, textvariable=left_var).pack()
    prog = Progressbar(root, orient=HORIZONTAL, length=300)
    prog.pack(pady=10)
    def tick(current):
        progress = round((current / total_units) * 100, 0)
        prog['value'] = progress
        pct_var.set(f'{progress:.0f}%')
        status_var.set(f'{current} files completed!')
        left_var.set(f'{total_units - current} left to complete.')
        if current < total_units:
            root.after(1000, tick, current + step)
        if current == total_units:
            quit()
    tick(done)
    root.mainloop()
if __name__ == '__main__':
    boot()

Почему это важно

Фреймворки для GUI событийно-ориентированы. Если вы блокируете главный цикл с помощью sleep или длительного цикла, интерфейс не может обрабатывать ввод или перерисовку, что приводит к «замершему» окну и плохому пользовательскому опыту. Использование .after() для имитации цикла сохраняет отзывчивость, а логика остаётся простой и пригодной к повторному использованию.

Выводы

Не блокируйте главный поток Tkinter циклами и time.sleep(). Используйте .after() для планирования периодических обновлений интерфейса и «тиков» прогресса в фоне. Если нужно завершить программу по окончании работы, вызовите quit(), когда прогресс достигнет финального состояния.

Статья основана на вопросе на StackOverflow от Lee Donovan и ответе от acw1668.