I have a tkinter/python program which does something time consuming when I push a button. I'd like to give the user progress reports by updating a label field at each stage of the time consuming process. If have simple loop to the effect

for x in y:
     dosomething(x)
     self.label1.config(text = x)

alas, the label only updates at the end, rather than as things go. I also noticed that the button stays pressed until the process is done.

On the hope this makes sense, how can I show users the current x while in the loop?

You use a Tkinter.StringVar(). This is not what you are doing, but a program I have lying around. The string variable is attached to both the entry box and the label, so they change as the StringVar changes.

import Tkinter

class EntryTest:
   def __init__(self):
      self.top = Tkinter.Tk()

      self.str_1 = Tkinter.StringVar()
      label_lit = Tkinter.StringVar()

      label_1 = Tkinter.Label(self.top, textvariable = label_lit )
      label_1.pack()
      label_lit.set( "Test of Label")
    
      label_2 = Tkinter.Label(self.top, textvariable = self.str_1 )
      label_2.pack()

      entry_1 = Tkinter.Entry(self.top, textvariable=self.str_1)
      entry_1.pack()
      self.str_1.set( "Entry Initial Value" )

      cont = Tkinter.Button(self.top, text='PRINT CONTENTS',
             command=self.getit, bg='blue', fg='white' )
      cont.pack(fill=Tkinter.X, expand=1)

      exit=  Tkinter.Button(self.top, text='EXIT',
             command=self.top.quit, bg='red', fg='white' )
      exit.pack(fill=Tkinter.X, expand=1)

      entry_1.focus_set()
      self.top.mainloop()

   ##-----------------------------------------------------------------
   def getit(self) :
      print "getit: variable passed is", self.str_1.get()

##===============================================================

if "__main__" == __name__  :
   ET=EntryTest()

Doesn't work any better. The problem seems to be that tkinter does not update the visible frame while it's busy. For example, the button stays depressed until the loop is done.

code executes line by line, so with your code the label won't update until dosomething(x) is complete.

Your dosomething function should theoretically call a callback everytime it finishes a block. urllib.urlretrieve sends a callback after every block for example. Since I'm assuming you wrote your own function, maybe this would work:

def mybigfunction(x,callback):
    somethingtimeconsuming(x)
    callback(50)
    somethingtimeconsuming2(x)
    callback(100)

def callback(num):
    print "%d percent done!"

lol I'm just guessing this is how they do callbacks in those functions like urllib.urlretrieve. :D

The basic idea is that (save for threading) your script is going to execute line by line. So, execute part of your function, and then call a function to tell the user how it's going. The nice thing about using callbacks is that you don't have to write the label update into the function. Your callback can be whatever, and your function can go wherever.

I often have my callback update a progress bar on a TopLevel widget. Users expect time consuming processes to open up new windows. Like when you download something in firefox.


Let me know if this works for you. I'm genuinely curious.

UPDATE: lol, I guess it's as simple as that:

import time

def counter(x,callback):
    while x > 0:
        callback(x)
        time.sleep(1)
        x = x - 1
    

def callback(num):
    print "%d seconds left!" %num


counter(20,callback)

returns:

20 seconds left!
19 seconds left!
18 seconds left!
17 seconds left!
16 seconds left!
15 seconds left!
14 seconds left!
13 seconds left!
12 seconds left!
11 seconds left!
10 seconds left!
9 seconds left!
8 seconds left!
7 seconds left!
6 seconds left!
5 seconds left!
4 seconds left!
3 seconds left!
2 seconds left!
1 seconds left!

Neat!

print works fine as is. I have a label update statement and a print statement in the middle of my loop. The label updates (and the button I pushed unpushes itself) when the loop completes. Print produces output throughout.

The problem is that, as far as I can tell, tkinter does not update the screen while it's doing something, or at least while it's doing what I'm doing (basically a loop which copies files from here to there and similar os operations).

I don't see why

def mybigfunction(x,callback):
    somethingtimeconsuming(x)
    callback(50)
    somethingtimeconsuming2(x)
    callback(100)

def callback(num):
    print "%d percent done!"

should work any differently than

def mybigfunction(x,callback):
    somethingtimeconsuming(x)
       print "%d percent done!"
    somethingtimeconsuming2(x)
    print "%d percent done!"

I could break out of my loop periodically and return to the main tkinter wait for input loop, but wonder if that's really necessary.

Aha! One solution is to call update_idletasks(). The screen redraws whenever it's called. Downside is it slows things down. Is there a better way?

Okay Mr.Smartypants,

You're right. Print works exactly the same. Using a callback is only useful if you're doing something more involved than just printing "%d percent done!" ;) I guess I thought you'd figure that out. Also, you wouldn't want to pass your widget to your functionality, hence why you would want to use a callback. Take a look at functool.partial for more info about passing a widget to a callback function.

Wanna get fancy? Here's a progress bar:

'''Michael Lange <klappnase at 8ung.at>
The Meter class provides a simple progress bar widget for Tkinter.

INITIALIZATION OPTIONS:
The widget accepts all options of a Tkinter.Frame plus the following:

    fillcolor -- the color that is used to indicate the progress of the
                 corresponding process; default is "orchid1".
    value -- a float value between 0.0 and 1.0 (corresponding to 0% - 100%)
             that represents the current status of the process; values higher
             than 1.0 (lower than 0.0) are automagically set to 1.0 (0.0); default is 0.0 .
    text -- the text that is displayed inside the widget; if set to None the widget
            displays its value as percentage; if you don't want any text, use text="";
            default is None.
    font -- the font to use for the widget's text; the default is system specific.
    textcolor -- the color to use for the widget's text; default is "black".

WIDGET METHODS:
All methods of a Tkinter.Frame can be used; additionally there are two widget specific methods:

    get() -- returns a tuple of the form (value, text)
    set(value, text) -- updates the widget's value and the displayed text;
                        if value is omitted it defaults to 0.0 , text defaults to None .
'''

import Tkinter

class Meter(Tkinter.Frame):
    def __init__(self, master, width=300, height=20, bg='white', fillcolor='orchid1',\
                 value=0.0, text=None, font=None, textcolor='black', *args, **kw):
        Tkinter.Frame.__init__(self, master, bg=bg, width=width, height=height, *args, **kw)
        self._value = value

        self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
                                    highlightthickness=0, relief='flat', bd=0)
        self._canv.pack(fill='both', expand=1)
        self._rect = self._canv.create_rectangle(0, 0, 0, self._canv.winfo_reqheight(), fill=fillcolor,\
                                                 width=0)
        self._text = self._canv.create_text(self._canv.winfo_reqwidth()/2, self._canv.winfo_reqheight()/2,\
                                            text='', fill=textcolor)
        if font:
            self._canv.itemconfigure(self._text, font=font)

        self.set(value, text)
        self.bind('<Configure>', self._update_coords)

    def _update_coords(self, event):
        '''Updates the position of the text and rectangle inside the canvas when the size of
        the widget gets changed.'''
        # looks like we have to call update_idletasks() twice to make sure
        # to get the results we expect
        self._canv.update_idletasks()
        self._canv.coords(self._text, self._canv.winfo_width()/2, self._canv.winfo_height()/2)
        self._canv.coords(self._rect, 0, 0, self._canv.winfo_width()*self._value, self._canv.winfo_height())
        self._canv.update_idletasks()

    def get(self):
        return self._value, self._canv.itemcget(self._text, 'text')

    def set(self, value=0.0, text=None):
        #make the value failsafe:
        if value < 0.0:
            value = 0.0
        elif value > 1.0:
            value = 1.0
        self._value = value
        if text == None:
            #if no text is specified use the default percentage string:
            text = str(int(round(100 * value))) + ' %'
        self._canv.coords(self._rect, 0, 0, self._canv.winfo_width()*value, self._canv.winfo_height())
        self._canv.itemconfigure(self._text, text=text)
        self._canv.update_idletasks()


"""
def _demo(meter, value):
    meter.set(value)
    if value < 1.0:
        value = value + 0.005
        meter.after(50, lambda: _demo(meter, value))
    else:
        meter.set(value, 'Demo successfully finished')

if __name__ == '__main__':
    root = Tkinter.Tk(className='meter demo')
    m = Meter(root, relief='ridge', bd=3)
    m.pack(fill='x')
    m.set(0.0, 'Starting demo...')
    m.after(1000, lambda: _demo(m, 0.0))
    root.mainloop()
"""

All credit to Michael Lange. It works really nicely. And the colors are awesome.

Note line 72, which calls update_idletasks(). That seems to be the key.

Googling update_idletasks() reveals much goodness.

This article has been dead for over six months. Start a new discussion instead.