#!/usr/bin/env python
#
# $Id: ProgressMeter.py,v 1.3 2001/11/03 11:05:22 doughellmann Exp $
#
# Copyright 2001 Doug Hellmann.
#
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Doug
# Hellmann not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# DOUG HELLMANN DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL DOUG HELLMANN BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
#
"""A Pmw style widget for showing the progress being made on a task.
"""
__rcs_info__ = {
#
# Creation Information
#
'module_name' : '$RCSfile: ProgressMeter.py,v $',
'rcs_id' : '$Id: ProgressMeter.py,v 1.3 2001/11/03 11:05:22 doughellmann Exp $',
'creator' : 'Doug Hellmann <doug@hellfly.net>',
'project' : 'UNSPECIFIED',
'created' : 'Sat, 05-May-2001 12:22:48 EDT',
#
# Current Information
#
'author' : '$Author: doughellmann $',
'version' : '$Revision: 1.3 $',
'date' : '$Date: 2001/11/03 11:05:22 $',
}
#
# Import system modules
#
import Tkinter, Canvas
import Pmw
import sys, os, string, math, time
#
# Import Local modules
#
import colormath
#
# Module
#
class ProgressMeter(Pmw.LabeledWidget):
"""A Pmw style widget for showing progress being made on a task.
Progress is displayed by color-filling a horizontal bar. It is
also possible to show the progress using a text representation,
including the elapsed time.
Options
'finishvalue' -- When the progress reaches this value the color
fill should reach 100%.
'indicatorcolor' -- Color to use to display the text progress
indicator value.
'indicatorformat' -- Format string showing the indicator
information desired. Potential values are:
'elapsedtime' -- The amount of clock time which has passed
since starting processing (when progress was last reset to 0).
'estimatedtime' -- The full amount of time which will have to
elapse before progress will reach 100% at the current rate of
progress.
'finishvalue' -- The 'finishvalue' associated with the widget
which represents 100% progress.
'percentdone' -- Percent of the whole which has been
processed. This is calculated based on the current 'progress'
value vs. the 'finishvalue'.
'progress' -- The actual progress value given by the caller of
the 'updateProgress' method.
'remainingtime' -- The amount of time left which will have to
elapse before progress will reach 100% at the current rate of
progress.
'ratetime' -- The amount of time which must elapse to make 1%
progress. This value is used to calculate 'remainingtime'.
'ipadx=0' -- Internal X padding for component separation.
'ipady=0' -- Internal Y padding for component separation.
'progresscolor' -- Color which fills meter as progress is
advanced.
'shadowthickness' -- Width of the beveled edge around the fill
color.
"""
defaultIndicatorColor = 'black'
defaultShadowThickness=1
defaultTroughColor='grey'
defaultProgressColor='grey'
defaultIndicatorFormat='%(percentdone)s %% %(remainingtime)s'
def __init__(self, parent=None, **kw):
"""Create a ProgressMeter.
"""
optiondefs = (
('ipadx', 0, Pmw.INITOPT),
('ipady', 0, Pmw.INITOPT),
('progresscolor', self.defaultProgressColor, Pmw.INITOPT),
('shadowthickness', self.defaultShadowThickness, Pmw.INITOPT),
('finishvalue', 100, None),
('indicatorcolor', self.defaultIndicatorColor, Pmw.INITOPT),
('indicatorformat', self.defaultIndicatorFormat, None),
('troughcolor', self.defaultTroughColor, Pmw.INITOPT),
('showindicator', 1, Pmw.INITOPT),
)
self.defineoptions(kw, optiondefs)
# Initialize the base class
Pmw.LabeledWidget.__init__(self, parent=parent)
self.progress = 0
self.start_time = time.clock()
self.elapsed_time = 0
self.estimated_time = 0
self.remaining_time = 0
self.rate_time = 0
# Create our interface
self._createInterior()
# Check for initialization options
# for this class
self.initialiseoptions(ProgressMeter)
# Initialize the display.
self.updateProgress(0)
self.redraw()
def createProgressMeterCanvas(self, parent):
"""Create the canvas on which the meter will be drawn.
Create the canvas widget and the components on the canvas
to draw the progress meter. This is separated out to make
subclassing easier.
"""
# The meter canvas
meter = self.createcomponent('meter', (), None,
Tkinter.Canvas,
(parent,),
height=15,
width=100,
borderwidth=0,
highlightthickness=0,
bd=0
)
meterHeight = meter.winfo_height()
meterWidth = meter.winfo_width()
meter.bind("<Configure>", self._canvasReconfigure)
# Get our color set
progresscolor, topshadowcolor, bottomshadowcolor = self.getColors()
# Create the trough rectangle with the right colors
self.troughRect = Canvas.Rectangle(meter,
0,0,
meterWidth, meterHeight,
fill=self['troughcolor'],
outline=self['troughcolor'],
width=0,
)
# Create the top shadow of the meter bar
self.topShadow = Canvas.Rectangle(meter,
0,0,
0,0,
fill=topshadowcolor,
outline=topshadowcolor,
width=0,
)
# Create the bottom shadow of the meter bar
self.bottomShadow = Canvas.Rectangle(meter,
0,0,
0,0,
fill=bottomshadowcolor,
outline=bottomshadowcolor,
width=0
)
# Create the meter face rectangle
self.meterRect = Canvas.Rectangle(meter,
self['shadowthickness'], 0,
self['shadowthickness'], meterHeight,
fill=progresscolor,
outline=progresscolor,
width=0,
)
self.meterRect.islower = 0
return meter
def getColors(self):
"""Returns colors to use to draw the progress bar.
Once we know the progresscolor attribute, we should be able to
compute appropriate shadow colors. Returns a tuple with
(progresscolor, top shadow(light), bottom shadow(dark)).
"""
progresscolor = self['progresscolor']
triplet = colormath.computeColorTriplet(self.border, progresscolor)
return triplet
def _createInterior(self):
"""Create the interior components of the widget.
"""
interior = self.interior()
interior.configure(bd=0)
# The border frame
self.border = self.createcomponent('border', (), None,
Tkinter.Frame,
(interior,),
relief=Tkinter.SUNKEN,
bd=1,
highlightthickness=0
)
# The progress meter canvas
self.meter = self.createProgressMeterCanvas(self.border)
self.percentText = None
self.meter.pack(side=Tkinter.LEFT,
expand=Tkinter.YES,
fill=Tkinter.X,
padx=self['ipadx'],
pady=self['ipady']
)
self.border.pack(side=Tkinter.LEFT,
expand=Tkinter.YES,
fill=Tkinter.X,
padx=0,
pady=0,
)
def computePercentDone(self):
"Compute our progress as a percent done and return the number."
fractionDone = (self.progress * 1.0) / (self['finishvalue'] * 1.0)
percentDone = math.floor(fractionDone * 100.0)
return percentDone
def _recenterText(self):
"""Move the text component around to be in the center.
"""
meterWidth = self.meter.winfo_width()
meterHeight = self.meter.winfo_height()
newX = meterWidth / 2
newY = meterHeight / 2
self.percentText.coords( ((newX, newY),) )
return
def _rescaleTroughRectangle(self):
"""Change the size of the trough as a result of progress or resize events.
"""
meterWidth = self.meter.winfo_width()
meterHeight = self.meter.winfo_height()
self.troughRect.coords( ( (0,0), (meterWidth, meterHeight) ) )
return
def _canvasReconfigure(self, event=None):
"""Event handler for <Configure> events sent to the meter canvas.
This lets us redraw ourselves so that the proportions stay
correct during/after a resize.
"""
if event and self.progress:
self.redraw()
self._rescaleTroughRectangle()
self._recenterText()
return
def formatTime(self, timeValue):
"""Format the timeValue with our standard formatting string for display.
"""
local = time.localtime(timeValue)
return time.strftime('%M:%S', local)
def indicatorTextGet(self, percentDone):
"""Returns the text to be used in the indicator, already formatted.
"""
mergeDict = {}
mergeDict['percentdone'] = '%d' % percentDone
mergeDict['progress'] = '%d' % self.progress
mergeDict['finishvalue'] = '%d' % self['finishvalue']
mergeDict['elapsedtime'] = self.formatTime(self.elapsed_time)
mergeDict['estimatedtime'] = self.formatTime(self.estimated_time)
mergeDict['remainingtime'] = self.formatTime(self.remaining_time)
mergeDict['ratetime'] = self.formatTime(self.rate_time)
return self['indicatorformat'] % mergeDict
def drawProgressMeter(self, percentDone):
"""Draw the progress meter the way it should look with the specified percentDone.
"""
rectFraction = (percentDone * 1.0) / 100.0
meterWidth = self.meter.winfo_width()
meterHeight = self.meter.winfo_height()
shadowOffset = self['shadowthickness']
shadowWidth = math.floor(rectFraction*meterWidth)
rectWidth = shadowWidth - (2*shadowOffset)
rectHeight = meterHeight - (2*shadowOffset)
self.troughRect.coords( ( (0,0), (meterWidth, meterHeight) ) )
if percentDone == 0 and not self.meterRect.islower:
self.topShadow.lower()
self.bottomShadow.lower()
self.meterRect.lower()
self.meterRect.islower = 1
elif percentDone != 0 and self.meterRect.islower:
self.topShadow.tkraise()
self.bottomShadow.tkraise()
self.meterRect.tkraise()
self.meterRect.islower = 0
self.topShadow.coords( ((0,0), (shadowWidth,meterHeight)) )
self.bottomShadow.coords( ((shadowOffset,shadowOffset),
(shadowWidth,meterHeight)) )
self.meterRect.coords( ((shadowOffset,shadowOffset),
( shadowWidth-shadowOffset ,
meterHeight-shadowOffset)) )
return
def redraw(self):
"""Redraw method to update the display.
"""
percentDone = self.computePercentDone()
self.drawProgressMeter(percentDone)
if (not self.percentText) and self['showindicator']:
self.percentText = Canvas.CanvasText(
self.meter,
(1.0 * self.meter.winfo_width())/2,
(1.0 * self.meter.winfo_height())/2,
fill=self['indicatorcolor'],
text='0 %',
)
self.percentText.islow = 1
self.percentText.lower()
if self['showindicator']:
self.percentText.config(text=self.indicatorTextGet(percentDone))
if percentDone == 0:
self.percentText.lower()
self.percentText.islow = 1
elif self.percentText.islow:
self.percentText.tkraise()
self.percentText.islow = 0
else:
self.percentText.lower()
return
def showPercentDone(self):
"Show our progress."
self.redraw()
self.interior().update_idletasks()
return
def updateProgress(self, newValue=0):
"Set a new progress value."
self.progress = newValue
self.updateTimeValues()
self.showPercentDone()
return
def updateTimeValues(self):
"""Update the time left value on the progress indicator.
"""
current_time = time.clock()
if self.progress == 0:
self.start_time = current_time
self.elapsed_time = 0
self.estimated_time = 0
self.remaining_time = 0
self.rate_time = 0
else:
percentDone = self.computePercentDone()
self.elapsed_time = current_time - self.start_time
try:
self.rate_time = math.ceil( (1.0 * self.elapsed_time) /
(1.0 * percentDone)
)
except ZeroDivisionError:
self.rate_time = 0
try:
# remaining time is the rate times the percent left to do
self.remaining_time = self.rate_time * (100 - percentDone)
except ZeroDivisionError:
self.estimated_time = 0
self.remaining_time = 0
return
def updateMessage(self, messageString=''):
"Display a new message in our label, if we have one."
try:
label = self.component('label')
except:
pass
else:
label.configure(text=messageString)
return
class ProgressDial(ProgressMeter):
"""A Pmw style widget for showing progress being made on a task.
Progress is displayed by color-filling a circle. It is
also possible to show the progress using a text representation,
including the elapsed time.
Options
'finishvalue' -- When the progress reaches this value the color
fill should reach 100%.
'indicatorcolor' -- Color to use to display the text progress
indicator value.
'indicatorformat' -- Format string showing the indicator
information desired. Potential values are:
'elapsedtime' -- The amount of clock time which has passed
since starting processing (when progress was last reset to 0).
'estimatedtime' -- The full amount of time which will have to
elapse before progress will reach 100% at the current rate of
progress.
'finishvalue' -- The 'finishvalue' associated with the widget
which represents 100% progress.
'percentdone' -- Percent of the whole which has been
processed. This is calculated based on the current 'progress'
value vs. the 'finishvalue'.
'progress' -- The actual progress value given by the caller of
the 'updateProgress' method.
'remainingtime' -- The amount of time left which will have to
elapse before progress will reach 100% at the current rate of
progress.
'ratetime' -- The amount of time which must elapse to make 1%
progress. This value is used to calculate 'remainingtime'.
'ipadx=0' -- Internal X padding for component separation.
'ipady=0' -- Internal Y padding for component separation.
'progresscolor' -- Color which fills meter as progress is
advanced.
'shadowthickness' -- Width of the beveled edge around the fill
color.
"""
defaultTroughColor='grey65'
defaultIndicatorFormat='%(percentdone)s %%'
def drawProgressMeter(self, percentDone):
"Draw the arcs with the correct extent."
degreeFraction = (percentDone * 1.0) / 100.0
newExtent = 360.0 * degreeFraction
if newExtent >= 360:
newExtent = 359
if percentDone == 0 and not self.meterArc.islower:
self.innerBrightShadow1.lower()
self.innerDarkShadow1.lower()
self.meterArc.lower()
self.meterArc.islower = 1
elif percentDone != 0 and self.meterArc.islower:
self.innerBrightShadow1.tkraise()
self.innerDarkShadow1.tkraise()
self.meterArc.tkraise()
self.meterArc.islower = 0
self.meterArc.config(extent=newExtent)
self.innerBrightShadow1.config(extent=newExtent)
self.innerDarkShadow1.config(extent=newExtent)
def _canvasReconfigure(self, event=None):
"""Event handler for <Configure> events sent to the meter canvas.
This lets us redraw ourselves so that the proportions stay
correct during/after a resize.
"""
if event and self.progress:
self.redraw()
self._recenterText()
return
def createProgressMeterCanvas(self, parent):
"""Create arcs and ovals instead of rectangles.
"""
# Create the meter canvas
meter = self.createcomponent('meter', (), None,
Tkinter.Canvas,
(parent,),
height=50,
width=50,
borderwidth=0,
highlightthickness=0
)
meterHeight = meter.winfo_height()
meter.bind("<Configure>", self._canvasReconfigure)
# Get colors
progresscolor, topshadowcolor, bottomshadowcolor = self.getColors()
shadowOffset = self['shadowthickness']
# Get some default shadow colors for the hole
# in which the oval goes.
darkcolor = 'grey20'
brightcolor = 'grey90'
# Create the shadow rings
numShadowRings=4
self.outerDarkShadow1 = Canvas.Oval(meter,
0,0,
50-(numShadowRings*shadowOffset)+1,
50-(numShadowRings*shadowOffset)+1,
fill=darkcolor,
outline=darkcolor,
width=0,
)
self.outerBrightShadow1 = Canvas.Oval(meter,
numShadowRings*shadowOffset,
numShadowRings*shadowOffset,
50,50,
fill=brightcolor,
outline=brightcolor,
width=0,
)
self.innerBrightShadow1 = Canvas.Arc(meter,
shadowOffset,shadowOffset,
50-(3*shadowOffset)+1,
50-(3*shadowOffset)+1,
#fill=brightcolor,
#outline=brightcolor,
fill=topshadowcolor,
outline=topshadowcolor,
width=0,
extent=0
)
self.innerBrightShadow1.lower()
self.innerDarkShadow1 = Canvas.Arc(meter,
3*shadowOffset, 3*shadowOffset,
50-shadowOffset,50-shadowOffset,
#fill=darkcolor,
#outline=darkcolor,
fill=bottomshadowcolor,
outline=bottomshadowcolor,
width=0,
extent=0
)
self.innerDarkShadow1.lower()
self.troughOval = Canvas.Oval(meter,
shadowOffset*(numShadowRings/2),
shadowOffset*(numShadowRings/2),
50-((numShadowRings/2)*shadowOffset),
50-((numShadowRings/2)*shadowOffset),
fill=self['troughcolor'],
outline=self['troughcolor'],
width=0,
)
# Create the arc that is the actual meter
self.meterArc = Canvas.Arc(meter,
shadowOffset*(numShadowRings/2),
shadowOffset*(numShadowRings/2),
50-((numShadowRings/2)*shadowOffset),
50-((numShadowRings/2)*shadowOffset),
fill=progresscolor,
outline=progresscolor,
width=1,
extent=0
)
self.meterArc.lower()
self.meterArc.islower = 1
return meter
def _createInterior(self):
"""
Create the interior just like the ProgressMeter,
but ensure that the border relief is flat instead
of sunken.
"""
ProgressMeter._createInterior(self)
self.border.configure(relief=Tkinter.FLAT)
return
if __name__ == '__main__':
import GuiAppD
class PMTest(GuiAppD.GuiAppD):
appname = 'Test the ProgressMeter widget'
full_colorset = (
#None,
'SlateBlue',
'DodgerBlue',
'turquoise',
'SteelBlue',
'goldenrod',
'blue',
'lightblue',
#'grey',
'tan',
'green',
'DarkSeaGreen',
'SeaGreen',
'PaleGreen',
'SpringGreen',
'VioletRed',
'MediumPurple',
'thistle',
'cornsilk',
)
small_colorset = (
#None,
'SpringGreen',
'DodgerBlue',
'VioletRed',
'MediumPurple',
'thistle',
'cornsilk',
'IndianRed',
'seagreen',
'orange',
'cyan',
'magenta',
'wheat',
)
quick_colorset = ( None, 'SeaGreen')
#colorset = full_colorset
colorset = small_colorset
#colorset = quick_colorset
def createDial(self, parent, i, progresscolor):
name='dial%d' % i
if not progresscolor:
w = self.createcomponent(
name, (), None,
ProgressDial,
(parent,),
#labelpos='n',
#label_text=name,
#troughcolor='orange'
).pack(
side=Tkinter.LEFT,
expand=Tkinter.NO,
fill=Tkinter.NONE,
pady=5
)
else:
w = self.createcomponent(
name, (), None,
ProgressDial,
(parent,),
progresscolor=progresscolor,
#troughcolor='%s3' % progresscolor,
#labelpos='n',
#label_text=name,
).pack(
side=Tkinter.LEFT,
expand=Tkinter.NO,
fill=Tkinter.NONE,
pady=5
)
#self.bind(w, name, name)
self.component(name).updateProgress(0)
return w
def createMeter(self, parent, i, progresscolor):
name='meter%d' % i
if not progresscolor:
w = self.createcomponent(
name, (), None,
ProgressMeter,
(parent,),
labelpos='n',
label_text=name,
troughcolor='slateblue'
).pack(
side=Tkinter.TOP,
expand=Tkinter.YES,
fill=Tkinter.X,
pady=5,
)
else:
w = self.createcomponent(
name, (), None,
ProgressMeter,
(parent,),
progresscolor=progresscolor,
labelpos='n',
label_text=name
).pack(
side=Tkinter.TOP,
expand=Tkinter.YES,
fill=Tkinter.X,
pady=5
)
#self.bind(w, name, name)
self.component(name).updateProgress(0)
return w
def createInterface(self):
meter_frame = Tkinter.Frame(self.interior())
meter_frame_left = Tkinter.Frame(meter_frame)
meter_frame_right = Tkinter.Frame(meter_frame)
meter_frames = (meter_frame_left, meter_frame_right)
dial_frame = Tkinter.Frame(self.interior())
dial_frame_top = Tkinter.Frame(dial_frame)
dial_frame_bottom = Tkinter.Frame(dial_frame)
dial_frames = (dial_frame_top, dial_frame_bottom)
i = 0
for progresscolor in self.colorset:
self.createDial(dial_frames[ i % 2 ], i, progresscolor)
self.createMeter(meter_frames[ i % 2 ], i, progresscolor)
i = i + 1
meter_frame.pack(side=Tkinter.LEFT,
expand=Tkinter.YES,
fill=Tkinter.X, padx=2)
meter_frame_left.pack(side=Tkinter.LEFT,
expand=Tkinter.YES,
fill=Tkinter.X,
anchor='n',
padx=2)
meter_frame_right.pack(side=Tkinter.LEFT,
expand=Tkinter.YES,
fill=Tkinter.X,
anchor='n',
padx=2)
dial_frame.pack(side=Tkinter.LEFT,
expand=Tkinter.YES,
fill=Tkinter.X)
dial_frame_top.pack(side=Tkinter.TOP,
expand=Tkinter.YES,
fill=Tkinter.BOTH)
dial_frame_bottom.pack(side=Tkinter.TOP,
expand=Tkinter.YES,
fill=Tkinter.BOTH)
self.createcomponent('Start', (), None,
Tkinter.Button,
(self.interior(),),
command=self.startTest,
text='Start',
).pack(
side=Tkinter.TOP)
self.createcomponent('Reset', (), None,
Tkinter.Button,
(self.interior(),),
command=self.resetTest,
text='Reset',
).pack(
side=Tkinter.TOP)
#self.component('progressmeter').configure(indicatorformat='%(progress)s of %(finishvalue)s Steps')
self.component('progressmeter').configure(indicatorformat='%(progress)s/%(finishvalue)s colors %(remainingtime)s')
#self.component('progressmeter').configure(indicatorformat='%(percentdone)s %% %(remainingtime)s remains')
#self.component('progressmeter').configure(indicatorformat='%(ratetime)s / %%')
def startTest(self):
import time
#completion=30
completion=100
# Compute the finish value for the
# application progress meter.
#finishValue = 0
#for w in range(len(self.colorset)):
# finishValue = finishValue + (completion * (w+1)) + 1
#finishValue = finishValue * 2
#print 'finishValue = %d' % finishValue
#finishValue = 2 * (completion + 1) * len(self.colorset)
finishValue = len(self.colorset)
appProgress = 0
self.updateProgress(appProgress, finishValue)
#print 'starting test'
self.busyStart()
for w in range(len(self.colorset)):
#for i in range( ( (w+1) * completion ) + 1 ):
for i in range( completion + 1 ):
widget = self.component('dial%d' % w)
widget.updateProgress(i)
widget.updateMessage('%s: %d' %
(widget['progresscolor'], i))
widget = self.component('meter%d' % w)
widget.updateProgress(i)
widget.updateMessage('%s: %d' %
(widget['progresscolor'], i))
#appProgress = appProgress + 2
#self.updateProgress(appProgress)
appProgress = appProgress + 1
self.updateProgress(appProgress)
#time.sleep(10)
self.busyEnd()
self.updateProgress(0, 100)
def resetTest(self):
# Compute the finish value for the
# application progress meter.
finishValue = len(self.colorset)
appProgress = 0
self.updateProgress(appProgress, finishValue)
self.busyStart()
for w in range(len(self.colorset)):
widget = self.component('dial%d' % w)
widget.updateProgress(0)
widget.updateMessage('%s: %d' % (widget['progresscolor'], 0))
widget = self.component('meter%d' % w)
widget.updateProgress(0)
widget.updateMessage('%s: %d' % (widget['progresscolor'], 0))
appProgress = appProgress + 1
self.updateProgress(appProgress)
self.busyEnd()
self.updateProgress(0, 100)
PMTest().run()
|