Метод after в Tkinter ‒ отложенный вызов функции

В прошлом уроке мы использовали новый для нас метод after, который через указанное в первом аргументе количество миллисекунд выполняет функцию, переданную во втором аргументе. В случае необходимости для этой функции в третьем и последующем аргументах метода after передаются аргументы для ее параметров. Рассмотрим пример:

from tkinter import *

def make_bigger(*widgets):
    for i in widgets:
        w = i['width']
        i.config(width=int(w*1.5))

root = Tk()
bt = Button(text='Hello', width=10)
lbl = Label(text='World', bg='pink', width=20)

root.after(2500, make_bigger, bt, lbl)

bt.pack()
lbl.pack()
root.mainloop()

Здесь через 2,5 секунды кнопка и метка поменяют свой размер. То, что при передаче аргументов в функцию make_bigger они там упаковываются в кортеж widgets, является частным случаем. Параметры функции могут быть обычными позиционными.

Метод after есть не только у окна, но и у других виджетов. Однако вызывать его на них редко имеет смысл. В примере ниже не важно, вызываем ли мы after на метку или окно.

from tkinter import *

ph2 = ('Big ball', 'Small roll', 'Round table', 'Hot cup')

root = Tk()
lbl = Label(bg='pink', width=20)

for i in range(len(ph2)):
    lbl.after(1000, lambda: lbl.config(text=ph2[i]))

lbl.pack()
root.mainloop()

Однако проблема этой программы в другом. Она не работает так, как надо. Мы ожидаем, что надпись в метке будет меняться через каждую секунду. На самом деле через 1 секунду отобразится последняя фраза: "Hot cup".

Если метод after откладывает вызов функции на указанное время, значит, она не начинает исполняться, когда вызывается after. Ее код стартует только через указанное время.

Проследим, что происходит в программе выше. Цикл for очень быстро прокручивает все свои итерации, все четыре вызова after происходят почти одномоментно. Через 1 секунду интерпретатор уже давно забыл про цикл for. Однако начинают исполняться отложенные вызовы функции. Поскольку все они идут сразу друг за другом, мы видим в метке только последнюю фразу.

Другими словами, 1 секунда ‒ это не задержка между вызовами функции. Это время отложения вызова функции, переданной в after. И у всех вызовов в нашем случае оно одно и то же.

Чтобы убедиться, что пользовательская функция вызывается четыре раза и с разными аргументами, вместо lambda создадим обычную и в ней дополнительно будем выводить фразу на экран:

def change_ph(widget, phrase):
    widget['text'] = phrase
    print(phrase)
...
for i in range(len(ph2)):
    lbl.after(1000, change_ph, lbl, ph2[i])

И хотя в метке будет только последняя фраза, на экране вы увидите все четыре. Они появятся все сразу через 1 секунду работы программы.

Таким образом, решение проблемы ‒ это вызовы after через разные промежутки времени:

def change_ph(widget, phrase):
    widget['text'] = phrase
...
for i in range(len(ph2)):
    lbl.after((i+1)*1000, change_ph, lbl, ph2[i])

При этом если заменить обычную функцию на lambda: lbl.config(text=ph2[i]), вернемся к варианту неверной работы программы. Видимо это происходит потому, что нет аргумента, и для всех вызовов значение i берется в его конечном варианте. Поэтому рабочий код лямбды будет выглядеть так: lambda j=i: lbl.config(text=ph2[j]).

Чаще метод after вкладывают не в цикл, а вызывают внутри функции для отложенного вызова самой этой функции. Получается своего рода рекурсия.

from tkinter import *

ph2 = ('Big ball', 'Small roll', 'Round table', 'Hot cup')

def change_ph(widget, kit, n):
    widget['text'] = kit[len(kit) - n]
    n = n - 1
    if n > 0:
        lbl.after(1000, change_ph, widget, kit, n)

root = Tk()
lbl = Label(bg='pink', width=20)
change_ph(lbl, ph2, len(ph2))

lbl.pack()
root.mainloop()

В данном случае нет надобности вызывать after через разные промежутки времени, как было с циклом. Здесь каждый следующий вызов change_ph происходит через секунду после предыдущего. В свою очередь и метод after вызывается через 1 секунду после своего предыдущего вызова. Получается следующая последовательность вызовов: change_ph()after() → 1 сек. → change_ph()after() → 1 сек. → ….

Практическая работа

Напишите программу, в которой в метке через каждые 2 секунды появляется новое случайное число. После того как программа сгенерирует и отобразит пять случайных чисел, они должны быть выведены в метке все вместе единым списком.

Курс с примерами решений практических работ: pdf-версия


Tkinter. Программирование GUI на Python




Все разделы сайта