1

I am trying to make a pre-defined GUI workflow (i.e., pipeline) using the after method of Tkinter, but it does not work and I am confused.

As a minimal example, I have created this simple GUI:

from tkinter import *

class example:

    global frame, rect, text

    def __init__(self):
        self.root = Tk()
        self.initialize()
        self.workflow()
        self.root.mainloop()

    def initialize(self):
        global frame, rect, text
        # frame
        frame = Canvas(self.root, width=500, height=500, bd=0, highlightthickness=0, relief='ridge')
        frame.grid(row=0, column=0)
        # rectangle
        rect = frame.create_rectangle(20, 20, 400, 350, fill='blue')
        # text
        text = frame.create_text(250, 250, text='FIRST...', fill='#ffff00', font=("Purisa", 20, "bold"))

    def change_message(self, new_message):
        global frame, text
        frame.itemconfig(text, text=new_message)

    def workflow(self):
        global frame, rect, text
        self.root.after(2000, self.change_message("SECOND..."))
        self.root.after(4000, self.change_message("END"))

myExample = example()

The aim is to follow the pipeline that is specified in the workflow method, which is the following:

  • 1st: The message shows the text "FIRST..." when the application is run
  • 2nd: After 2 seconds, the text changes and shows "SECOND..."
  • 3rd: After 4 seconds, the text changes again and shows "END"

But, when I run this example, the GUI waits 4 seconds to show up, and displays the final "END" message, without plotting the previous changes...

enter image description here

What am I doing wrong?

Thanks in advance

Víctor Martínez
  • 854
  • 2
  • 11
  • 29

1 Answers1

2

Replace:

self.root.after(2000, self.change_message("SECOND..."))
self.root.after(4000, self.change_message("END"))

with either:

self.root.after(2000, lambda : self.change_message("SECOND..."))
self.root.after(4000, lambda : self.change_message("END"))

or, like in Bryan's suggestion, with:

self.root.after(2000, self.change_message, "SECOND...")
self.root.after(4000, self.change_message, "END")

.after(ms, callback, *args) requires callback to be a reference as opposed to an actual call to the callback function. Basically ditch () from callback() to have callback.

When callback() is passed instead of callback, as in:

widget.after(ms, callback(), *args)

python acts roughly equivalent to:

callback(*args)
time.sleep(ms / 1000)

which delays the execution of mainloop() line, thus GUI is displayed late.


This is has some similarity to how button commands work.

Nae
  • 14,209
  • 7
  • 52
  • 79
  • 1
    You don't need to use `lambda` with `after`. You can pass additional arguments without it: `self.root.after(2000, self.change_message, "SECOND")` – Bryan Oakley Jan 21 '18 at 19:38
  • @BryanOakley Thanks again, hopefully, 3rd time I will remember that. – Nae Jan 21 '18 at 19:42
  • Your solution works, but it adds unnecessary complexity. Tkinter beginners seem to think that `lambda` is a cure-all. IMO it should only be used when it's absolutely necessary because it makes debugging more difficult. – Bryan Oakley Jan 21 '18 at 19:46
  • @BryanOakley Well, I find it easier with `lambda` as I can connect the dots easier for callbacks to be used with options such as `bind`, `trace_add`, `command`. Then again I'm a beginner as well. – Nae Jan 21 '18 at 19:50