1#!/usr/bin/env python 2from __future__ import division, print_function 3""" 4A basic widget for showing the progress being made in a task. 5Includes a countdown timer RemainingTime. 6 7History: 82002-03-15 ROwen Added a start method to TimeBar. 92002-07-30 ROwen Moved into the RO.Wdg module. 102002-12-20 ROwen Changed bd to borderwidth for clarity. 112003-07-25 ROwen ProgressbBar: added barBorder, helpText, helpURL; 12 can now be resized while in use; 13 fixed bugs affecting large and small borderwidths. 14 TimeBar: renamed from TimeRemaining; added countUp and updateInterval; 15 improved accuracy of pause and resume 162004-05-18 ROwen Changed ProgressBar.configure to ._configureEvt to avoid collision 17 with Tkinter.Frame.configure. 182004-08-11 ROwen Fixed some import errors. 19 Define __all__ to restrict import. 202004-09-14 ROwen Modified test code to not import RO.Wdg. 212005-08-03 ROwen Modified to handle max=min gracefully instead of raising an exception. 22 Added doc strings to many methods. 232006-03-06 ROwen Added setUnknown method. To support this, many parameters 24 now can take two values for (known, unknown) state. 25 Added barStipple argument. 262012-07-10 ROwen Modified to use RO.TkUtil.Timer. 27 Removed use of update_idletasks. 282015-09-24 ROwen Replace "== None" with "is None" to modernize the code. 29""" 30__all__ = ['ProgressBar', 'TimeBar'] 31 32import time 33import RO.SeqUtil 34from RO.TkUtil import Timer 35import Tkinter 36import Button 37import Entry 38import Gridder 39import Label 40 41class ProgressBar (Tkinter.Frame): 42 """A bar graph showing a value or fraction of a task performed. 43 44 Contains three widgets: 45 - labelWdg: an optional label (None if absent) 46 - cnv: the bar graph 47 - numWdg: an optional numerical value display 48 49 Inputs: 50 - minValue: value for zero-length bar 51 - maxValue: value for full-length bar 52 - value: starting value 53 - label: one of: a string, Tkinter Variable, Tkinter widget or None 54 - constrainValue: if value is out of range, pin it else display as supplied 55 the bar graph is always constrained, this only affects the numeric display 56 - valueFormat: numeric value is displayed as valueFormat % value; 57 set to None if not wanted. 58 If two values, the 2nd is the string displayed for unknown value. 59 60 The following control the appearance of the progress bar 61 and the background field against which it is displayed 62 - barLength: length of full bar (pixels) within the background border; 63 - barThick: thickness of bar (pixels) within the background border; 64 if horizontal then defaults to label height, else defaults to 10 65 - barFill: color of bar; if two values, the 2nd is used for unknown value. 66 - barStipple: stipple pattern for bar; if two values, the 2nd is used for unknown value. 67 - barBorder: thickness of black border around progress bar; typically 0 or 1; 68 to the extent possible, this is hidden under the background's border 69 - **kargs options for the bar's background field (a Tkinter Canvas), 70 including the following which default to Entry's default settings 71 - borderwidth: thickness of border around bar's background field 72 - relief: type of border around bar's background field 73 - background: color of bar's background field 74 75 Warnings: 76 - barStipple may only work on unix (due to known bugs in Tk). 77 """ 78 UnkValue = "?" 79 def __init__ (self, 80 master, 81 minValue=0, 82 maxValue=100, 83 value=0, 84 label = None, 85 constrainValue = False, 86 valueFormat=("%d", "?"), 87 isHorizontal = True, 88 barLength = 20, 89 barThick = None, 90 barFill = "dark gray", 91 barStipple = (None, "gray50"), 92 barBorder = 0, 93 helpText = None, 94 helpURL = None, 95 **kargs): 96 # handle defaults for background, borderwidth and relief 97 e = Tkinter.Entry() 98 for item in ("background", "borderwidth", "relief"): 99 kargs.setdefault(item, e[item]) 100 101 # handle default=0 for other borders 102 for item in ("selectborderwidth", "highlightthickness"): 103 kargs.setdefault(item, 0) 104 105 Tkinter.Frame.__init__(self, master) 106 107 # basics 108 self.constrainValue = constrainValue 109 self.valueFormat = RO.SeqUtil.oneOrNAsList(valueFormat, 2) 110 self.isHorizontal = isHorizontal 111 self.knownInd = 0 # 0 for known, 1 for unknown value 112 self.fullBarLength = barLength 113 if barThick is None: 114 if self.isHorizontal: 115 self.barThick = 0 116 else: 117 self.barThick = 10 118 else: 119 self.barThick = barThick 120 self.barFill = RO.SeqUtil.oneOrNAsList(barFill, 2) 121 self.barStipple = RO.SeqUtil.oneOrNAsList(barStipple, 2) 122 self.barBorder = barBorder 123 self.hideBarCoords = (-1, -1 - self.barBorder) * 2 124 self.helpText = helpText 125 self.helpURL = helpURL 126 127 cnvPadY = 0 128 if self.isHorizontal: 129 labelAnchor = "e" 130 packSide = "left" 131 if barThick is None: # default to label hieght 132 cnvPadY = 5 133 packFill = "both" 134 else: 135 packFill = "x" 136 cnvWidth, cnvHeight = self.fullBarLength, self.barThick 137 numAnchor = "w" 138 else: 139 labelAnchor = "center" 140 packSide = "bottom" 141 packFill = "y" 142 cnvWidth, cnvHeight = self.barThick, self.fullBarLength 143 numAnchor = "center" 144 145 # handle label 146 self.labelWdg = self._makeWdg(label, anchor = labelAnchor) 147 if self.labelWdg is not None: 148 self.labelWdg.pack(side = packSide) 149 150 # create canvas for bar graph 151 self.cnv = Tkinter.Canvas(self, 152 width = cnvWidth, 153 height = cnvHeight, 154 **kargs) 155 self.cnv.pack(side = packSide, expand = True, fill = packFill, pady = cnvPadY) 156 157 # thickness of canvas border; initialize to 0 and compute later 158 # drawable width/height = winfo_width/height - (2 * border) 159 self.cnvBorderWidth = 0 160 161 # bar rectangle (starts out at off screen and zero size) 162 self.barRect = self.cnv.create_rectangle( 163 fill = self.barFill[self.knownInd], 164 stipple = self.barStipple[self.knownInd], 165 width = self.barBorder, 166 *self.hideBarCoords 167 ) 168 169 # handle numeric value display 170 if self.valueFormat[0]: 171 self.numWdg = Label.StrLabel(self, 172 anchor = numAnchor, 173 formatStr = self.valueFormat[0], 174 helpText = self.helpText, 175 helpURL = self.helpURL, 176 ) 177 elif self.labelWdg is None and self.barThick is None: 178 # use an empty label to force bar thickness 179 def nullFormat(astr): 180 return "" 181 self.numWdg = Label.StrLabel(self, formatFunc = nullFormat) 182 else: 183 self.numWdg = None 184 if self.numWdg is not None: 185 self.numWdg.pack(side = packSide, anchor = numAnchor) 186 187 # set values and update display 188 self.value = value 189 self.minValue = minValue 190 self.maxValue = maxValue 191 192 self.cnv.bind('<Configure>', self._configureEvt) 193 self._setSize() 194 195 def clear(self): 196 """Hide progress bar and associated value label""" 197 self.cnv.coords(self.barRect, *self.hideBarCoords) 198 if self.numWdg: 199 self.numWdg["text"] = "" 200 201 def setValue(self, 202 newValue, 203 newMin = None, 204 newMax = None, 205 ): 206 """Set the current value and optionally one or both limits. 207 208 Note: always computes bar scale, numWdg width and calls update. 209 """ 210 self.knownInd = 0 211 if self.constrainValue: 212 newValue = max(min(newValue, self.maxValue), self.minValue) 213 self.value = newValue 214 self.setLimits(newMin, newMax) 215 216 def setUnknown(self): 217 """Display an unknown value""" 218 self.knownInd = 1 219 self.value = self.maxValue 220 self.fullUpdate() 221 222 def setLimits(self, 223 newMin = None, 224 newMax = None, 225 ): 226 """Set one or both limits. 227 228 Note: always computes bar scale, numWdg width and calls update. 229 """ 230 if newMin is not None: 231 self.minValue = newMin 232 if newMax is not None: 233 self.maxValue = newMax 234 self.fullUpdate() 235 236 def fullUpdate(self): 237 """Redisplay assuming settings have changed 238 (e.g. current value, limits or isKnown). 239 Compute current bar scale and numWdg width and then display. 240 """ 241 # compute bar scale 242 try: 243 self.barScale = float(self.fullBarLength) / float(self.maxValue - self.minValue) 244 except ArithmeticError: 245 self.barScale = 0.0 246 247 # set bar color scheme 248 self.cnv.itemconfig( 249 self.barRect, 250 fill = self.barFill[self.knownInd], 251 stipple = self.barStipple[self.knownInd], 252 ) 253 254 # set width of number widget 255 if self.numWdg: 256 # print "valfmt=%r, knownInd=%r, minValue=%r, maxValue=%r" % (self.valueFormat, self.knownInd, self.minValue, self.maxValue) 257 if self.knownInd == 0: 258 maxLen = max([len(self.valueFormat[0] % (val,)) for val in (self.minValue, self.maxValue)]) 259 else: 260 maxLen = len(self.valueFormat[1]) 261 self.numWdg["width"] = maxLen 262 263 self.update() 264 265 def update(self): 266 """Redisplay based on current values.""" 267 # 0,0 is upper-left corner of canvas, but *includes* the border 268 # thus positions must be offset 269 # the smallest starting position required is 1 - self.cnvBorderWidth 270 # but -1 is simpler and works for all cases 271 value=self.value 272 273 barLength = self._valueToLength(value) 274 if barLength <= 0: 275 # works around some a Tk bug or misfeature 276 x0, y0, x1, y1 = self.hideBarCoords 277 elif barLength >= self.fullBarLength: 278 # works around a Tk bug or misfeature that is only relevant if the bar has a border 279 x0 = y0 = 0 280 x1 = self.cnvSize[0] - 1 281 y1 = self.cnvSize[1] - 1 282 elif self.isHorizontal: 283 x0 = 0 284 y0 = 0 285 x1 = self._valueToLength(value) + self.cnvBorderWidth 286 y1 = self.cnvSize[1] - 1 287 else: 288 x0 = 0 289 y0 = (self.fullBarLength - self._valueToLength(value)) + self.cnvBorderWidth 290 x1 = self.cnvSize[0] - 1 291 y1 = self.cnvSize[1] - 1 292 # print "x0, y0, x1, y1 =", x0, y0, x1, y1 293 self.cnv.coords(self.barRect, x0, y0, x1, y1) 294 295 # And update the label 296 if self.numWdg: 297 if self.knownInd == 0: 298 self.numWdg["text"] = self.valueFormat[0] % (value,) 299 else: 300 self.numWdg["text"] = self.valueFormat[1] 301 302 def _configureEvt(self, evt=None): 303 """Handle the <Configure> event. 304 """ 305 self._setSize() 306 self.update() 307 308 def _makeWdg(self, wdgInfo, **kargs): 309 """Return a widget depending on wdgInfo: 310 - None or False: returns None or False 311 - a string: returns a Label with text=wdgInfo 312 - a Tkinter Variable: returns a Label with textvariable=wdgInfo 313 - a Tkinter Widget: returns wdgInfo unaltered 314 315 kargs is ignored if wdgInfo is a widget 316 """ 317 if wdgInfo is None: 318 return wdgInfo 319 elif isinstance(wdgInfo, Tkinter.Widget): 320 # a widget; assume it's a Label widget of some kind 321 return wdgInfo 322 323 # at this point we know we are going to create our own widget 324 # set up the keyword arguments 325 kargs.setdefault("helpText", self.helpText) 326 kargs.setdefault("helpURL", self.helpURL) 327 if isinstance(wdgInfo, Tkinter.Variable): 328 kargs["textvariable"] = wdgInfo 329 else: 330 kargs["text"] = wdgInfo 331 332 return Label.StrLabel(self, **kargs) 333 334 def _setSize(self): 335 """Compute or recompute bar size and associated values.""" 336 # update border width 337 self.cnvBorderWidth = int(self.cnv["borderwidth"]) + int(self.cnv["selectborderwidth"]) + int(self.cnv["highlightthickness"]) 338 self.cnvSize = [size for size in (self.cnv.winfo_width(), self.cnv.winfo_height())] 339 barSize = [size - (2 * self.cnvBorderWidth) for size in self.cnvSize] 340 341 # compute bar length 342 if self.isHorizontal: 343 self.fullBarLength = barSize[0] 344 else: 345 self.fullBarLength = barSize[1] 346 # print "_setSize; self.fullBarLength =", self.fullBarLength 347 348 # recompute scale and update bar display 349 self.fullUpdate() 350 351 def _valueToLength(self, value): 352 """Compute the length of the bar, in pixels, for a given value. 353 This is the desired length, in pixels, of the colored portion of the bar. 354 """ 355 barLength = (float(value) - self.minValue) * self.barScale 356 # print "barLength =", barLength 357 barLength = int(barLength + 0.5) 358 # print "barLength =", barLength 359 return barLength 360 361 362class TimeBar(ProgressBar): 363 """Progress bar to display elapsed or remaining time in seconds. 364 Inputs: 365 - countUp: if True, counts up, else counts down 366 - autoStop: automatically stop when the limit is reached 367 - updateInterval: how often to update the display (sec) 368 **kargs: other arguments for ProgressBar, including: 369 - value: initial time; 370 typically 0 for a count up timer, maxValue for a countdown timer 371 if omitted then 0 is shown and the bar does not progress until you call start. 372 - minvalue: minimum time; typically 0 373 - maxValue: maximum time 374 """ 375 def __init__ (self, 376 master, 377 countUp = False, 378 valueFormat = ("%3.0f sec", "??? sec"), 379 autoStop = False, 380 updateInterval = 0.1, 381 **kargs): 382 ProgressBar.__init__(self, 383 master = master, 384 valueFormat = valueFormat, 385 **kargs 386 ) 387 self._autoStop = bool(autoStop) 388 self._countUp = bool(countUp) 389 390 self._updateInterval = updateInterval 391 self._updateTimer = Timer() 392 self._startTime = None 393 394 if "value" in kargs: 395 self.start(kargs["value"]) 396 397 def clear(self): 398 """Set the bar length to zero, clear the numeric time display and stop the timer. 399 """ 400 self._updateTimer.cancel() 401 ProgressBar.clear(self) 402 self._startTime = None 403 404 def pause(self, value = None): 405 """Pause the timer. 406 407 Inputs: 408 - value: the value at which to pause; if omitted then the current value is used 409 410 Error conditions: does nothing if not running. 411 """ 412 if self._updateTimer.cancel(): 413 # update timer was running 414 if value: 415 self.setValue(value) 416 else: 417 self._updateTime(reschedule = False) 418 419 def resume(self): 420 """Resume the timer from the current value. 421 422 Does nothing if not paused or running. 423 """ 424 if self._startTime is None: 425 return 426 self._startUpdate() 427 428 def start(self, value = None, newMin = None, newMax = None, countUp = None): 429 """Start the timer. 430 431 Inputs: 432 - value: starting value; if None, set to 0 if counting up, max if counting down 433 - newMin: minimum value; if None then the existing value is used 434 - newMax: maximum value: if None then the existing value is used 435 - countUp: if True/False then count up/down; if None then the existing value is used 436 437 Typically you will only specify newMax or nothing. 438 """ 439 if newMin is not None: 440 self.minValue = float(newMin) 441 if newMax is not None: 442 self.maxValue = float(newMax) 443 if countUp is not None: 444 self._countUp = bool(countUp) 445 446 if value is not None: 447 value = float(value) 448 elif self._countUp: 449 value = 0.0 450 else: 451 value = self.maxValue 452 self.setValue(value) 453 454 self._startUpdate() 455 456 def _startUpdate(self): 457 """Starts updating from the current value. 458 """ 459 if self._countUp: 460 self._startTime = time.time() - self.value 461 else: 462 self._startTime = time.time() - (self.maxValue - self.value) 463 self._updateTime() 464 465 def _updateTime(self, reschedule = True): 466 """Automatically update the elapsed time display. 467 Call once to get things going. 468 """ 469 # print "_updateTime called" 470 # cancel pending update, if any 471 self._updateTimer.cancel() 472 473 if self._startTime is None: 474 raise RuntimeError("bug! nothing to update") 475 476 # update displayed value 477 if self._countUp: 478 value = time.time() - self._startTime 479 if (self._autoStop and value >= self.maxValue): 480 self.setValue(self.maxValue) 481 return 482 else: 483 value = (self._startTime + self.maxValue) - time.time() 484 if (self._autoStop and value <= 0.0): 485 self.setValue(0) 486 return 487 488 self.setValue(value) 489 490 # if requested, schedule next update 491 if reschedule: 492 self._updateTimer.start(self._updateInterval, self._updateTime) 493 494 495if __name__ == "__main__": 496 import PythonTk 497 root = PythonTk.PythonTk() 498 499 # horizontal and vertical progress bars 500 501 hProg = [ProgressBar( 502 root, 503 isHorizontal = True, 504 borderwidth = bw, 505 barLength = 99, 506 relief = "solid", 507 ) for bw in (0, 1, 2, 10) 508 ] 509 vProg = [ProgressBar( 510 root, 511 isHorizontal = False, 512 borderwidth = bw, 513 barLength = 99, 514 relief = "solid", 515 ) for bw in (0, 1, 2, 10) 516 ] 517 518 def setProg(*args): 519 for pb in hProg + vProg: 520 pb.setValue ( 521 newValue = valEntry.getNum(), 522 newMin = minEntry.getNum(), 523 newMax = maxEntry.getNum(), 524 ) 525 526 valEntry = Entry.IntEntry(root, defValue = 50, width=5, callFunc = setProg) 527 minEntry = Entry.IntEntry(root, defValue = 0, width=5, callFunc = setProg) 528 maxEntry = Entry.IntEntry(root, defValue = 99, width=5, callFunc = setProg) 529 530 setProg() 531 532 gr = Gridder.Gridder(root) 533 534 gr.gridWdg ("Val", valEntry) 535 gr.gridWdg ("Minimum", minEntry) 536 gr.gridWdg ("Maximum", maxEntry) 537 gr.gridWdg (False) # spacer row 538 539 540 vbarRowSpan = gr.getNextRow() 541 col = gr.getNextCol() 542 for vpb in vProg: 543 vpb.grid(row=0, column = col, rowspan = vbarRowSpan) 544 col += 1 545 root.grid_rowconfigure(vbarRowSpan-1, weight=1) 546 547 hbarColSpan = col + 1 548 for hpb in hProg: 549 gr.gridWdg(False, hpb, colSpan=hbarColSpan, sticky="") 550 551 root.grid_columnconfigure(hbarColSpan-1, weight=1) 552 553 # time bars 554 555 def startRemTime(): 556 time = timeEntry.getNum() 557 for bar in timeBars: 558 bar.start(time) 559 560 def pauseRemTime(): 561 for bar in timeBars: 562 bar.pause() 563 564 def resumeRemTime(): 565 for bar in timeBars: 566 bar.resume() 567 568 def clearRemTime(): 569 for bar in timeBars: 570 bar.clear() 571 572 573 timeEntry = Entry.IntEntry(root, defValue = 9, width=5) 574 gr.gridWdg ("Rem Time", timeEntry) 575 gr.gridWdg (False, ( 576 Button.Button(root, text="Start", command=startRemTime), 577 Button.Button(root, text="Pause", command=pauseRemTime), 578 Button.Button(root, text="Resume", command=resumeRemTime), 579 Button.Button(root, text="Clear", command=clearRemTime), 580 ), 581 ) 582 583 hTimeBar = [TimeBar( 584 master=root, 585 label = "Time:", 586 isHorizontal = True, 587 autoStop = countUp, 588 valueFormat = "%3.1f sec", 589 countUp = countUp, 590 ) for countUp in (False, True) 591 ] 592 for hbar in hTimeBar: 593 gr.gridWdg(False, hbar, colSpan=hbarColSpan, sticky="ew") 594 595 vTimeBar = [TimeBar( 596 master=root, 597 label = "Time", 598 isHorizontal = False, 599 autoStop = not countUp, 600 countUp = countUp, 601 ) for countUp in (False, True) 602 ] 603 col = gr.getNextCol() 604 for vbar in vTimeBar: 605 vbar.grid(row=0, column=col, rowspan = gr.getNextRow(), sticky="ns") 606 col += 1 607 timeBars = hTimeBar + vTimeBar 608 609 root.mainloop() 610