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.
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.