Использование tkinter после создания анимации

Chris спросил: 13 июня 2018 в 10:58 в: python

Фоновая информация - Я пытаюсь создать анимацию для объекта фрейма с TKinter со следующим кодом: из импорта tkinter Frame, Tk, Label, Buttonimport time

def runAnim():
    for width in range(0, 200):
        app.after(5000, lambda width = width: test_label.config(width=width))app = Tk()
app.geometry("500x500")
test_label = Frame(bg="#222", width=0)
test_label.pack(side="left", fill="y")
test_button = Button(text="toggle", command=lambda: runAnim() )
test_button.pack(side="right")

Проблема заключается в том, что это не создает желаемого поведения. Я понимаю, что это должно постепенно увеличивать ширину каждые 5 секунд, однако диапазон 0-200 кажется завершенным за эти 5 секунд, вместо того, чтобы увеличивать ширину 1 каждые 5 секунд. Все решения будут оценены!

1 ответ

Есть решение
abarnert ответил: 13 июня 2018 в 02:53

Этот after(5000, …) означает через 5 секунд после , так как after вызывается , а не через 5 секунд после некоторого будущего момента времени, который может быть только tkinter предположите, прочитав свой ум.

Итак, вы просто создаете 200 обратных вызовов и планируете их все, чтобы они запустили 5 секунд. Это явно не то, что вы хотите , но это то, что вы запрашиваете , так вот что вы получаете.


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


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

def runAnim():
    iterwidth = iter(range(0, 200))
    stepAnim(iterwidth)def stepAnim(iterwidth):
    try:
        width = next(iterwidth)
    except StopIteration:
        return
    test_label.config(width=width))
    app.after(5000, stepAnim, iterwidth)

В то время как это работает для любого итеративного процесса, когда вы просто перебираете числа, обычно лучше использовать цикл for в явном счетчик, который легче инвертировать. (Да, это противоположность "обычного for вместо while и += 1), когда вы not переворачиваете вещи. разница в том, что здесь мы не можем получить доступ к магии for или while, а while намного менее волшебны и, следовательно, проще инвертировать.)

def runAnim():
    stepAnim(0, 200):def stepAnim(width, maxWidth):
    test_label.config(width=width))
    width += 1
    if width < maxWidth:
       app.after(5000, stepAnim, width, maxWidth)

Однако в этом особенно простом случае вы можете уйти с планированием 200 обратных вызовов от 5 до 1000 секунд в будущем:

def runAnim():
    for width in range(0, 200):
        app.after(5000 * width, lambda width = width: test_label.config(width=width))

Этот может заставлять таймер дрейфовать намного хуже, или он может даже захлебывать планировщик и добавлять отставание в вашу программу, но он по крайней мере стоит


Говоря о дрифте:

В начале я упомянул, что after(5000, …) означает через 5 секунд после этого.

An after может немного опоздать. Как говорят документы: "Tkinter гарантирует только, что обратный вызов не будет вызван раньше; если система занята, фактическая задержка может быть намного длиннее.

Итак, что произойдет, если она срабатывает после, скажем, 5,2 секунды? Затем второй тик происходит через 5 секунд после , что , на 10,2 секунды, а не на 10 секунд. И если они все увольняются немного поздно, это добавляет, поэтому к концу мы могли бы отстать на 20 секунд.

Хуже того, что, если after срабатывает ровно через 5 секунд, но Label.config занимает 0,2 секунды для запуска? Тогда мы гарантируем на 20 секунд. (Плюс любой дополнительная ошибка самого after.)

Если это имеет значение, вам нужно отслеживать желаемый "следующий раз" и ждать до тех пор, пока не будет 5 секунд, когда это будет теперь. Например:

import datetime as dtdef runAnim():
    stepAnim(0, 200, dt.datetime.now() + dt.timedelta(seconds=5):def stepAnim(width, maxWidth, nextTick):
    test_label.config(width=width))
    width += 1
    if width < maxWidth:
       now = dt.datetime.now()
       delay = (nextTick - now).total_seconds() * 1000
       nextTick += dt.timedelta(seconds=5)
       app.after(delay, stepAnim, width, maxWidth, nextTick)
Bryan Oakley ответил: 13 июня 2018 в 02:51
Вместо `app.after (5000, lambda: stepAnim (iterwidth)` вы можете избежать использования lambda, выполнив его следующим образом: app.after(5000, stepAnim, iterwidth)
abarnert ответил: 13 июня 2018 в 02:53
@BryanOakley Хорошо, я забыл об этом. Это новое с Python 2.2 или около того, поэтому мне потребовалось некоторое время, чтобы привыкнуть к нему. :)

Дополнительное видео по вопросу: Использование tkinter после создания анимации

Библиотека Tkinter - 16 - Работа с Canvas (статические изменения фигур)

Python & Tkinter - Поле ввода

Библиотека Tkinter - 9 - Использование ООП