Welcome, guest | Sign In | My Account | Store | Cart

Provides a status meter that displays the current status of a running function, percentage completion of that function, and allows cancellation of that function. This helps for long-running tasks that need to run without blocking the GUI and should display their progress to a user.

Python, 193 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
"""
Demonstrates the use of the ProgressMeter class.

Author: Tucker Beck
Last Tested: 3/2/2009
Verified with: Python 2.6, Tkinter 8.4
"""

from __future__ import division
from Tkinter import *
from random import randint
from time import sleep

class ProgressMeter( Frame ):
    """
    The ProgressMeter is-a Frame widget provides a progress bar and
    accompanying information to a user regarding a long, computationaly
    intensive process.  A ProgressMetar can control any generator function
    that returns string message or None after each iteration.  Furthermore,
    the ProgressMeter can interrupt the process at any time.
    """
    def __init__( self, parent, height=30 ):
        """
        Initializes this ProgressMeter
        
        Arguments:
          parent:   The master widget for this ProgressMeter
          height:   The desired height of the progress bar
        """
        self.parent = parent
        Frame.__init__( self, parent )
        self.columnconfigure( 0, weight=1 )                                     # Forces the canv object to resize any time this widget is resized 
        self.rowconfigure( 0, weight=1 )
        self.statusMessage = 'Normal'
        self.w = 0
        self.h = 0
        self.canv = Canvas( self, height=height)                                # This canvas will display the progress bar and accompanying percentage text
        self.canv.grid( row=1, column=0, sticky=N+S+E+W )
        self.canv.bind( '<Configure>', lambda e:
                        self.resize( e.width, e.height ) )                      # When the canvas is resized the progress bar should be redrawn.
        self.killVar = IntVar()                                                 # The killBtn can cancel execution
        self.killVar.set( 0 )
        self.killBtn = Button( self, text='Cancel',
                               command=lambda: self.killVar.set(1) )
        self.killBtn.configure( state=DISABLED )
        self.killBtn.grid( row=1, column=1 )
        self.targetGen = None                                                   # Placekeeper for the generator function that will be metered
        self.targetArgs = []                                                    # Argument list for the generator function
        self.targetKwds = {}                                                    # Keyword dictionary for the generator funciton
        self.targetIdx = 0                                                      # Keeps track of which step in iteration is currently being executed
        self.targetLen = 0                                                      # Total number of steps in exectuion
    
    
    def resize( self, w, h ):
        """
        Handles resize events for the canv widget.  Adjusts the height and width
        of the canvas for the progress bar calculations.
        
        Arguments:
          w: The new width
          h: The new height
        """
        self.w = w
        self.h = h
        self.canv.delete( 'frame' )
        self.canv.create_rectangle( 1, 1, self.w, self.h, outline='black',
                                    fill='gray75', tag='frame' )

    def reset( self ):
        """
        Resets the control values or the generator function and also clears the
        progress bar
        """
        self.canv.delete( 'bar' )
        self.canv.delete( 'text' )
        self.killBtn.configure( state=DISABLED )
        self.targetGen = None
        self.targetArgs = []
        self.targetKwds = []
        self.killVar.set( 0 )
        self.targetIdx = 0
        self.targetLen = 0
        
    def clearStatus( self ):
        """"
        Clears the statusMessage member.  Might be used by parent GUI that
        reports child status.
        """
        self.statusMessage = 'Normal'

    def drawBar( self ):
        """
        Updates the status bar for the percentage of completion.
        """
        pct = self.targetIdx / self.targetLen                                   # The percentage of completion
        x0 = 2                                                                  # The bar is inset by 2 pixels
        x1 = pct * ( self.w - 3 ) + 2
        y0 = 2
        y1 = self.h
        self.canv.delete( 'bar' )
        self.canv.create_rectangle( x0, y0, x1, y1, fill='SteelBlue3',
                                    outline='', tag='bar' )
        self.canv.delete( 'text' )
        pctTxt = '%02.2f%%' % ( pct*100, )
        self.canv.create_text( self.w/2, self.h/2, text=pctTxt,
                               anchor=CENTER, tag='text' )
        
    def startGen( self, targetGen, targetLen, targetArgs=[], targetKwds={} ):
        """
        Initializes the target generator function with supplied arguments and
        keyword.  Requests Tk to call iterGen after all idle events have been
        handled.
        
        Arguments:
          targetGen:  The target generator function
          targetLen:  The number of iterations in the target generator
          targetArgs: The arguments for the generator function
          targetKwds: The keyword arguments fo the generator function
        Note:
          Having iterGen called by Tk ensures that redraws and other sorts of
          normal Tkinter events can be processed.  Results in the status bar
          updating real-time with execution while allowing the GUI to function
          normally.
        """
        self.targetGen = targetGen( *targetArgs, **targetKwds )
        self.targetLen = targetLen
        self.killBtn.configure( state=NORMAL )
        self.after_idle( self.iterGen )
        
    def iterGen( self ):
        """
        Iterates through the target generator using delayed self referencing
        funcition calls to allow GUI updates between iterations
        """
        try:
            msg = self.targetGen.next()                                         # Execute the next iteration of the genrator
        except StopIteration:
            self.reset()                                                        # When the generator is finished, a StopIteration exception is raised.  This signals a normal finish in the generator
            self.statusMessage = 'Completed'
            self.event_generate( '<<Finished>>' )                               # A <<Finished>> virtual event signals the GUI that the progress meter is finished
            return
        self.targetIdx += 1
        self.drawBar()
        if msg == None:
            pass
        elif msg.startswith( 'AbortIteration' ):                                # The target generator can signal that something irrevocable has happend by yielding a value of 'AbortIteration'
            self.reset()
            self.statusMessage = msg
            self.event_generate( '<<Finished>>' )
            return
        else:
            self.statusMessage = msg                                            # If the generator yields a value other than None or 'AbortIteration', this message will be sent out to the controlling gui
            self.event_generate( '<<StatusRequest>>' )
        if self.killVar.get() == 1:                                             # Occurs if the user clicks the killBtn
            self.reset()
            self.statusMessage = 'Canceled'
            self.event_generate( '<<Finished>>' )
            return
        self.update_idletasks()
        self.after_idle( self.iterGen )
        
def dummy_gen( alices, bobs ):
    """
    A simple, stupid example of a ProgressMeter iterable generator function
    """
    for alice in alices:
        for bob in bobs:
            if bob==alice:
                yield 'Match: %s==%s' % ( str(alice), str(bob) )
            else:
                yield 'No Match: %s!=%s' % ( str(alice), str(bob) )

def main():
    root = Tk()
    root.title( 'ProgressMeter Demo' )
    pgress = ProgressMeter( root )                                              # Initialize the ProgressMeter with default arguments
    pgress.grid( row=1 )
    alices = range( 53 )
    bobs = [ randint( 0,53 ) for i in range( 53 ) ]
    btn = Button( root, text="Go!", command=lambda:
         pgress.startGen( dummy_gen, len(alices) * len(bobs), [alices, bobs] ) )# Starts the ProgressMeter going when the button is clicked
    btn.grid( row=0 )
    statusVar = StringVar( root, 'None' )
    status = Label( root, textvariable=statusVar )
    status.grid( row=2 )                                                        # This label will be used to display status messages from the ProgressMeter
    root.bind( '<<StatusRequest>>', lambda event:
               statusVar.set(pgress.statusMessage) )
    root.bind( '<<Finished>>', lambda event:
               statusVar.set( pgress.statusMessage ) )
    root.mainloop()

if __name__=='__main__':
    main()

Note that this widget takes advantage of Python's built-in generator functionality. This allows a looping function to yield values before iteration is finished. The advantage here is that the GUI may update itself or process events after each iteration of the worker function.