to make it look a little prettier. The code now relies on both snit and tile. I might add back in the tile-independence, but snit is here to stay. Next on the agenda is a proper mini-version so that you can put lots of these into a scrollable frame much like Firefox's download window. (Similar to Zipguy's version).BTW - moving the network protocol code out of the widget means that I can effectively avoid issues of resuming downloads etc -- that's not my problem anymore! :-) More seriously, the proper place for such code is probably the uri package in tcllib, although that could do with some work (which I might in fact do at some point).Due to the major interface change I bumped the version number to 2.0 just in case anyone is actually using this.Screenshot
WinXP - no tile (old version)
Mac OS X - with tile and icon (new version)Zipguy 06/2005 I needed a smaller version of the download dialog. It works great! # dlprogress.tcl --
#
# A Simple Download Progress Widget.
# http://wiki.tcl.tk/10571
#
# Copyright (c) 2005-2006 Neil Madden.
#
# License: http://www.cs.nott.ac.uk/~nem/license.terms (Tcl-style)
package require Tcl
package require Tk
package require tile
package require snit
snit::widget dlprogress {
option -source "unknown"
option -destination "unknown"
variable done
variable ts
variable start
variable speed
variable progress
constructor {args} {
set done 0
set ts [clock seconds]
set start [clock seconds]
set speed "0 Bytes/Sec"
set progress [list 0 ??]
$self configurelist $args
$self CreateProgressDialog
}
method CreateProgressDialog {} {
# Source URL
ttk::label $win.logo -image $networkImg
ttk::label $win.src_l -text "Source:" -anchor e
ttk::label $win.src -text $options(-source) -width 40 -anchor w
# Destination file
ttk::label $win.dst_l -text "Destination:" -anchor e
ttk::label $win.dst -text [file nativename $options(-destination)] \
-width 40 -anchor w
# Bytes transferred and rate
ttk::label $win.done_l -text "Status:" -anchor e
ttk::label $win.done -text "0/?? bytes (at 0 Bytes/Sec)" -anchor w
# Time remaining and elapsed
ttk::label $win.left_l -text "Time Left:" -anchor e
ttk::label $win.left -text "??" -anchor w
ttk::label $win.elapsed_l -text "Time Elapsed:" -anchor e
ttk::label $win.elapsed -text "00:00:00" -anchor w
# Progress bar and percentage done
ttk::label $win.prg_l -text "Progress:" -anchor e
$self MakeProgBar $win.p
grid $win.logo -sticky nsew
grid ^ $win.src_l $win.src -sticky ew
grid ^ $win.dst_l $win.dst -sticky ew
grid ^ $win.done_l $win.done -sticky ew
grid ^ $win.left_l $win.left -sticky ew
grid ^ $win.elapsed_l $win.elapsed -sticky ew
grid $win.prg_l $win.p - -sticky ew
}
method MakeProgBar path {
ttk::frame $path
ttk::progressbar $path.prg -maximum 100 -length 100
$path.prg configure -value 0
ttk::label $path.per -width 4 -text "0%"
grid $path.prg $path.per -sticky ew
grid columnconfigure $path 0 -weight 1
}
# Callback to update status during download. Call with total expected
# bytes and received so far.
method progress {expected received} {
if {$expected == 0} { return }
lassign $progress got total
set now [clock seconds]
# Work out percent download, and speed
set percent [expr {int(100.0 * (double($received)/double($expected)))}]
set byteDiff [expr {$received - $got}]
set tsDiff [expr {$now - $ts}]
if {$tsDiff > 0} {
set rate [expr {$byteDiff/$tsDiff}]
set speed "[$self FormatUnits $rate]/Sec"
# Work out time left
set left [expr {int(($expected - $received)/$rate + 1)}]
$self.left configure \
-text [clock format $left -format %T -gmt 1]
set elapsed [expr {$now - $start}]
$self.elapsed configure \
-text [clock format $elapsed -format %T -gmt 1]
set progress [list $received $expected]
}
$win.p.prg configure -value $percent
set txt "[$self FormatUnits $received]/[$self FormatUnits $expected]"
append txt " (at $speed)"
$self.p.per configure -text "${percent}%"
$self.done configure -text $txt
set ts $now
}
# Format a byte size in most appropriate units, up to Gigabyte size.
method FormatUnits size {
foreach {div unit } {
1073741824.0 GB
1048576.0 MB
1024.0 KB
1 Bytes
} {
if {($size / $div) >= 1} {
return "[format %.2f [expr {double($size)/$div}]] $unit"
}
}
return "$size Bytes"
}
# Base-64 encoded GIF image representing a network, from
# http://www.openclipart.org/ -- Public Domain.
typevariable networkImg
typeconstructor {
set networkImg [image create photo -data {
R0lGODlhUABQAOYAAAAAADBYgMjIyEiAsMDI2LjI0FBwkJigsNjY4NDQ0Ehw
kGiYwKCgoLDAyJCwyDBomFCIsDhwoKiwsKCoqFB4mFCAqKiosFB4oFiIqNDY
2JCoyICo0ICo2ICgwFh4oFh4mFiAoFiAqFiAsGiQsMjQ2MjQ0ICgyEh4oEBw
oFB4qJCgqChYkFiQuMDQ2EhwmEBwmJiYmChgkChYiFiIsFiIuLi4uKiwuIio
yICowICoyIiowDhomDBgmDBYiJiYoKCgqDBgkEB4oKioqLC4uHiYuKCosGCQ
sGCIsEh4qEB4qEiAqGiYyLjA0ICw2GCQwLi4wLCwuHCgyGCQuGCIuLC4wLCw
sHCYuFCAsHCYyHCYwMDAwGiYuGiQuHCQuGiQwMDI0MjI0LjAyNjg4NDQ2Hig
yHig0HigwHio0DhooDhwmIiw2NjY2ODg4ODg6Ojo6ODo6Ojo8PDw8JigqGiI
sHiYwMDIyJigoMDAyLjAwAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAHkA
LAAAAABQAFAAAAf/gHmCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXmJmam5prnmts
oaJsbqWmpXGpqZybn5+jo6enqnGsma6vsKGypqq2mAlrd1TExEM2yMhFy0UH
cs8qRau/lgkJdxxNTRtnUVELC1I0M1dKSEEoOw9AcqXU1ddNattnZVFLC040
NFcDSEko0Dzg0c7Nu0rW6pzZwM0elnziyJlDp46du4OTEpbZ6O0bRBoQriBB
giKCuhgHLmKMZO0LFm9Y8HlxErHcv5IDYxRRufKRgARgwIFzQnMfOX8ATa7T
SapnJAECwEiZOtXoFZtJ1fGIsaJIKKeQoJbYZzTk1ZFZc66QYeMrWEdi/6/K
tXmzpFauMti6fcsIKomRgP+hs/tg6dq8bdnwbfSTBIrHkEsqXYo3b48hexcn
+jkmgmc0O0IXpnxYRo8eAaiA0rwoWIYKsGFfmH3ihIvbuBUY2K16DWtFrjOM
GT48QYnjyI+DWb48mO/fiBKwcWWteoKf1qvjgo5IgCjq2bFnd8X9kBZQ3z9l
X6/9eflCWrRUmU+/So37+PPf1yLgfSItMAQo4IAEEliFf4Xg4gl74UHloIOE
KKjLhLxUSAsth6why4TpKbhgMP3l4aEnHMJSIS8XThMhKW6MgguLLcKCC1SC
jEhiiaKcKEuKhkgXYygLfvIjkLhYp0WNNqKHI/+MOrpxYY+ggCfeGvFpoV8N
9dUQH5JrMDiec0mOyIYvhfz04XUC8AdVgw9WaWUNgjiXgBZ44PHEnU/UMMQQ
UPQJhQSASmDBoBZMYOgPiP5gx6J2+DCBk7XA92CaVTroZpVXYjlfnMEk0IA2
oHJwxqijkmHqqSakmkMON7SKgxUzPGYSBY86ad6lbmaaX331cWpNGNqoMQ89
pZ5qqgmrJttqqw44YAYSs9ZqUE/WAQtqE6KO2pE3xpJhRgcdIMtqqxrccMIO
FwhhilPVMsQBB9yMak8UWNSbxb34mqGvvuB2oIMOHURwgQXrUltdGKQmPO89
QjWM77107NvvER4QzNP/QdYxsdHGCzM8FFVScCEyF1tYYbIVRBBhhhUeSFDw
StYVsO22MQlFlDg0TAHyyCJ3YTIRIFTxsiMBZlRdAS/BFBM+Q9GE8zgzRB31
EVQbMcIIXHQR9NCNCChJzA2HPZPTZJEzlwgiSE31EXOAAAXXjHj9FHZfiE3U
zVKwULZc5owUWwUhhIABBiBg1hQkcoe15hd33w0yWeOYRdc/SQSWAmweGK4Y
4kXP/dMXIE+lN+QzSE5XEgCVBFltJ1xQgyiRJO7T4jQ8TToEpvctmKwmhZbG
Cyhc8ATsnMMgiYMJEFB7RLibfpXudUmmzmgP7EABHsQ/IjtcaxIwAxdSTBH1
/1zlQJ/EYJ6JVhgQMcQAxPXZE9254j8RcIXI4Y8PPeWQedZ7YTxgH1dWQAEt
xK9r85vdT1qAhJFJ4QgzEAFsKDcYyfxvfe0boAw+YMDMINB4T6lOC5JQASuI
zAgQvEIFRtK/9E1vIALU4AbvcMC4JRAu1lhDCx5TgS6IjGozCMEKT2AX9cEw
gysozQbrgJ7Y3ZAxOWyB/4JwBC6MgGoSXOFj1BfADMowLxsUAImcCMKwdGkN
JAiNGiNwAiTUBjJpEM1WkJhEMFqmBx8QoyfIeLxPxAEOcJDFG9rQhlGIQQwI
QAAuMiCc7CDnE3x8ypm8RMn1hCmSYZmOghj0oErm0P9DmPRJh9TjpTV5EpTF
K5Aqy5gH77hok+wxZSUVRMZVqnIQHZQRLGPpSTC5R3u2vKUg4qMkInmol16C
pFO0hCtcTeqZsqSkZmrAgGpa85rYzKY2r3mg31Bzm+AMJzf5UiRPQvOck1LT
gw5SzCWFyUalPNIvfFShJeninbBM0y809MpyFjNM5jSlPFmBniEZM0iuqGeJ
RgSVgXJCOv8spx5zwaEkNcihm/BlOasjHmtMZz3obGilKAUnW3SKlNax1Hjk
wyv6COGlMI2pEBggBEz9QoyxTCdHsZPOZsYnU28qKSt+wtOe/nRSO4WmT3+6
q5uiyVKXwk+blhrUTLUUS78lUKdIqUpVXVl1PpqiBldz5VVdtfSs3XxHWdeK
1ra6VAgIekcgAAA7
}]
}
}Now, as I've cut all the networking code out of the widget, our example is a bit longer than it needs to be. I need to work out how best to split this functionality into other packages so that we can reduce this to a simple 3-liner again: package require Tk 8.4
package require tile
package require dlprogress 2.0
package require http
array set DownloadState {
uniqueId 0
}
# Fetch a URL to a given file
proc geturl {url to} {
global DownloadState
# Try to open file before anything else
set out [open $to.download w]
fconfigure $out -translation binary
# Create progress dialog
set w [toplevel .dl[incr DownloadState(uniqueId)]]
set prog [dlprogress $w.progress -source $url -destination $to]
# Initialise state
set DownloadState($w,close) 0
set DownloadState($w,out) $out
set DownloadState($w,file) $to
# Create a minimal user interface
ttk::frame $w.bs
ttk::button $w.bs.cancel -command [list ::cancel $w] \
-text "Cancel" -width 8
ttk::button $w.bs.ok -command [list ::destroy $w] \
-text "OK" -width 8 -state disabled
pack $w.bs.cancel $w.bs.ok -anchor e -side right -padx 2 -pady 4
ttk::checkbutton $w.close -variable ::DownloadState($w,close) \
-text "Close dialog when complete"
grid $prog -sticky ew
grid $w.close -sticky ew
grid $w.bs -sticky ew -padx {0 10}
wm title $w "0% of [file tail $to]"
wm resizable $w 0 0
wm protocol $w WM_DELETE_WINDOW [list ::cancel $w]
# Fetch the URL
set token [http::geturl $url -channel $out \
-progress [list ::progress $w] \
-command [list ::cleanup $w]]
set DownloadState($w,token) $token
return $w
}
# Progress callback to update display
proc progress {w token expected received} {
global DownloadState
# Update download progress widget
$w.progress progress $expected $received
# Update window title
set percent [expr {int(100.0 * double($received)/double($expected))}]
wm title $w "${percent}% of [file tail $DownloadState($w,file)]"
}
# Called when user clicks the cancel button
proc cancel w {
global DownloadState
http::reset $DownloadState($w,token) "cancelled"
}
# Called when download ends
proc cleanup {w token} {
variable DownloadState
# Close output file
catch {close $DownloadState($w,out)}
if {[http::status $token] eq "cancelled"} {
# User cancelled the download
destroy $w
} else {
# Assume download went fine (i.e. HTTP 200 return code)
# This would be the place to handle redirects etc
$w.bs.cancel configure -state disabled
$w.close configure -state disabled
$w.bs.ok configure -state normal
wm title $w "100% of [file tail $DownloadState($w,file)]"
if {$DownloadState($w,close)} {
destroy $w
} else {
# Reset window deletion handler to its usual
wm protocol $w WM_DELETE_WINDOW [list ::destroy $w]
}
# Move file to requested name
file rename -force $DownloadState($w,file).download \
$DownloadState($w,file)
http::cleanup $token
}
foreach item [array names DownloadState $w,*] {
catch { unset DownloadState($item) }
}
}
# Grab Tcl 8.4.12 sources
set url http://heanet.dl.sourceforge.net/sourceforge/tcl/tcl8.4.12-src.tar.gz
set file [tk_getSaveFile]
geturl $url $fileHope you like it!
CommentsMost of the comments below refer to older versions of the widget
MDD Very nice!KPV I'd love to see this combined with wish-reaper.escargo 21 Jan 2004 - Why would you want this combined with wish-reaper? Most pages I reap take only about a second, and a progress bar just does not seem to be of much use? (Of course, I'm connected via DSL, so maybe a progress bar would be of use to modem users.)NEM To combine it with wish-reaper, you would have to change the code slightly to allow a callback instead of writing to a file. This is so that wish-reaper can filter the html before writing to file. I might change the code so that it works as a callback, rather than doing the download itself.NEM Added some "-gmt 1" options to the [clock format] statements so that it prints actually accurate times.
When you feel that this is has reached a point that it is working, would you consider adding it to tklib?NEM Sure. But there's a whole bunch of stuff which would need to be done before then. Factor out the http stuff to maybe use the uri package in tcllib, make it download to file directly (to temp, then copy at end), instead of into memory (downloaded a huge file with it the other day, and started paging badly). I don't really have time to update this myself -- too many other projects. But if someone else wants to, I have no problem with that.
Peter Newman 14 April 2005:
- Am I correct in assuming that this can't currently handle resuming downloads?
- Am I correct in assuming that http can't handle resuming downloads?
Zipguy 06/2005 This is great!I posted a screenshot above of my modified version of dlprogress. I'll try to email you to let you know that I've modified the page. It is a LOT smaller and can be run hundreds of times from the same execution sucessfully.Too make it a lot smaller, I basically I just commented out lots of the "grid"ing of items down to just
grid $t.done $t.p -sticky ewand then changed the proc dlprogress::FormatUnits to add an if check to remove the units size (KB, MB,etc). I added an if check to see if the units were requested.
if {[expr [string length $units] - 1]} {
return "[format %.2f [expr {double($size)/$div}]]"
} else {
return "[format %.2f [expr {double($size)/$div}]] $unit"
}and lastly I changed the line to omit the units from the $received in the dlprogress::Progress proc:set txt "[FormatUnits $received nounits] of [FormatUnits $expected] at $data($id,speed)"(note the "FormatUnits $received nounits" call, which asked for no units in the format routine)NEM Glad you like it. I noticed your screen-shot. Looks nice. One day I might generalise this to be a general download manager. However, before that happens it'd be nice to have a well-thought-out abstraction of network protocols. At present, there is the uri stuff in tcllib which provides a geturl method, but that needs a bit of work to be really useful. I've got other stuff on my plate at the moment, but it's definitely something I'm considering looking at.[Mookie] I have updated it with the new ::ttk::progressbar.
See also
- progressbar
- Indeterminate Progress Bar with Tile
- Progress Bar (Fellows)
- Tcl Progress Meter
- canvas progress bar widget
- poor man's progressbar
- progressbars

