Keith Vetter 2006-02-13 : Here's a package I recently wrote that solves the problem of capturing the contents of a canvas into an image. If the canvas is small enough to fit entirely in the current window then you can use the Img package (see Capture a window into an image).
This package handles the situation when the canvas is too large. It first displays a dialog letting the user select which portions of the canvas to capture, then for each section it scrolls the canvas to that section, does a screen capture and joins them together into one large image.
There are some really nasty corner situations with partial sections at the edges.
NB. this package doesn't handle two cases: first, if you have embedded widgets they show up as blank (a limitation of the Img package), and second, it assumes highlightthickness is set to 0.
##+########################################################################## # # snap.tcl -- Takes snapshot of current or multiple connected screens # by Keith Vetter, February 2006 # # Revisions: # KPV Feb 13, 2006 - initial revision # ##+########################################################################## ############################################################################# package require Tk package require Img package require tile namespace import -force ::ttk::button image create photo ::img::camera -data { R0lGODlhVQBbALMAAAQCBMzO/LT6/PwCBPz+/OkAABIAAAAAALgB394A2hgA1AAAd8CgAN7qABgS AAAAACH5BAEAAAMALAAAAABVAFsAAwT/cMhJq72Ygsy7/2AIbGFpWuN5jaTqiuw7sK1sr3QN03eP 5joP0EeUAGOfY7DoogmGHeWSeWIJrlDc6Mmj2qzXcO63xXZ3XiM4bD47xefMMa0GsO9SOz6ukdJn ZXd7b4J8eSl/gIKLXHqMPId8XoGMlY+HcIh/awIEnpZhnp+ZhGySk46iopWqq3CWY5trrapstK2g pFR5obe+v4wEpkgykYS/yL69o21TSZjBydLJw5oqOblX09vAzTfYrNzit7rfpcvj6cyNzlWc2urx 1Tp5z5Sd8erzxu0V5+M5xmVr1k+LI3zTjEnLlcXEMWn8jii7dKrELGQR85AbZK1YKoxA/yAGpLWv oEU9ySRyG7GRncmTZUCODECzpk1VLHG5fAniCMJbSjzZHErTU85amYj4/AlUpdAAh3Tu8wiOqS+n T2tiFVaoYc9/CUOKIjqS5CIl9u5Z/eUUqsaJXdEaLCUOK1G3LLrFlTv3osiyWqXALVkPw9K1rdoG poGMox+HQBCLsrs4b8udPNOio6X4boDBxHb5bSr2V9HLoZWOJl02pVlvRehuy2hZKrvY7wDyG3y7 xzl4dUs33mtOra3ZQYc7ztx3qzoAyknFuvZYQspI4vxVFDL9AtvW6iaFuJrc8+JWiT5wFkxTymno BLymH8CaNov38AOnZ7wefvzkrbQHQP9RR+mXBoD18edLewLe1yBzIEwW0mdGvUWAeTblUBluFeKU E4GCGYWhefK9cNSHUKlSWYMSukfWHKpdiBdR8a1Y01g0kBgJbgPidV+OPvZY1Ih3HcJjj3k8SOSM nhXmg4YuFjlCZQ76mCFfvmm4JBBUTqlgf9tRd4SUXHYJnowU/hemO/YBGaSGY3nWIhNtavnmfXEq kWZHxdUppJ6fmengf6KNaYyZQx1KKJ16Htogkw8mueiRUEaipFtXChlpThByZ6ilkGqK6Ys5xlco l45WKaqoXQZwYKOgqmoempu+WmaqUxKpYqS2aokrqzTu+mCvQP4KFYm0bIjKrbHm2uRkR2iuSVWx zSI5Kl7oDEsHs4coOaCzbwib2qkDCEqtl6paEeBnfC5b7qbclingHWPVON8ME8Abr51qZeXqvfnW scSfYwYmRic2AcxjplX+q7BqmgL7MKPOEjXxsiVeXKjGHBMRAQA7 } namespace eval ::Snapshot { variable WHO variable W .snap variable S variable BOX variable saveName "snapshot" variable msg "Snapshot Demo" ;# For use as -texvariable variable tops ;# For hiding other toplevels array set S {w 200 h 200} } ##+########################################################################## # # ::Snapshot::Dialog -- puts up the snapshot dialog # proc ::Snapshot::Dialog {who} { variable WHO $who variable S variable W variable BOX destroy $W toplevel $W wm title $W "Snapshot" scan [wm geom .] "%dx%d+%d+%d" w h x y wm geom $W +[expr {$x+$w+30}]+$y wm resizable $W 0 0 label $W.icon -image ::img::camera -width 100 -height 100 frame $W.btns -bd 2 -relief ridge button $W.snap -text "Take Snapshot" -command ::Snapshot::_Snap button $W.dismiss -text Dismiss -command [list destroy $W] frame $W.f -bd 5 -relief ridge -pady 5 -padx 5 canvas $W.c -highlightthickness 0 -width $S(w) -height $S(h) -bg red label $W.l1 -text "Click on cells to select" -font {Helvetica 10 bold} label $W.l2 -textvariable ::Snapshot::BOX(size) -font {Helvetica 10 bold} grid $W.icon $W.f -sticky n -pady 10 grid configure $W.f -sticky news -padx 10 grid x $W.l1 grid x $W.l2 grid $W.btns - -sticky ew grid rowconfigure $W 0 -weight 1 grid columnconfigure $W 1 -weight 1 pack $W.c -side top -in $W.f -fill both -expand 1 pack $W.snap $W.dismiss -in $W.btns -side left -expand 1 -pady 10 ::Snapshot::_MakeGrid ::Snapshot::_click $BOX(me,row) $BOX(me,col) grab $W ;# Use this if you want to freeze the display } ##+########################################################################## # # ::Snapshot::_MakeGrid -- draws the grid to select cells on # proc ::Snapshot::_MakeGrid {} { variable W variable BOX variable S $W.c delete all ::Snapshot::_GetDimensions foreach arr [array names BOX xy,*] { scan $arr "xy,%d,%d" row col set tag box$row,$col foreach {x0 y0 x1 y1} $BOX($arr) break set x0 [expr {$x0 * $S(w)}] set y0 [expr {$y0 * $S(h)}] set x1 [expr {$x1 * $S(w)}] set y1 [expr {$y1 * $S(h)}] $W.c create rect $x0 $y0 $x1 $y1 -tag [list box $tag] -width 2 \ -outline black -fill black -stipple gray50 $W.c bind $tag <1> [list ::Snapshot::_click $row $col] set BOX(val,$row,$col) 0 } } ##+########################################################################## # # ::Snapshot::_click -- handles clicking on a grid cell # proc ::Snapshot::_click {row col} { variable W variable BOX set tag box$row,$col if {$BOX(val,$row,$col) == 0} { set BOX(val,$row,$col) 1 $W.c itemconfig $tag -fill red -stipple gray75 } else { set BOX(val,$row,$col) 0 $W.c itemconfig $tag -fill black -stipple gray50 } ::Snapshot::_GetISize } ##+########################################################################## # # ::Snapshot::_GetISize -- Figures outs image size based on clicked cells # proc ::Snapshot::_GetISize {} { variable BOX variable W set cnt 0 for {set row 0} {$row < $BOX(rows)} {incr row} { for {set col 0} {$col < $BOX(cols)} {incr col} { if {$BOX(val,$row,$col) == 0} continue if {[incr cnt] == 1} { set rmin $row set rmax $row set cmin $col set cmax $col } else { if {$row < $rmin} {set rmin $row} if {$row > $rmax} {set rmax $row} if {$col < $cmin} {set cmin $col} if {$col > $cmax} {set cmax $col} } } } if {$cnt == 0} { set BOX(size) "? x ?" $W.snap config -state disabled return } set w [expr {$cmax - $cmin + 1}] set h [expr {$rmax - $rmin + 1}] $W.snap config -state [expr {$cnt == ($w*$h) ? "normal" : "disabled"}] foreach var {rmin rmax cmin cmax} { set BOX($var) [set $var] } foreach {. . w h} [::Snapshot::_GetToCoords $BOX(rmax) $BOX(cmax)] break set BOX(size) "[comma $w] x [comma $h]" set BOX(cnt) $cnt } ##+########################################################################## # # ::Snapshot::_GetDimensions -- get scroll percentages for all cells # proc ::Snapshot::_GetDimensions {} { variable WHO variable BOX unset -nocomplain BOX # Get size of whole screen set bbox [$WHO cget -scrollregion] if {$bbox eq ""} { set bbox [$WHO bbox all]} if {$bbox eq ""} {error problem} set ::bbox $bbox foreach {l t r b} $bbox break set BOX(s,w) [expr {$r - $l}] set BOX(s,h) [expr {$b - $t}] foreach {xlo xxhi} [$WHO xview] break foreach {ylo yyhi} [$WHO yview] break set wx [expr {$xxhi - $xlo}] set wy [expr {$yyhi - $ylo}] for {set x $xlo} {$x > $wx} {set x [expr {$x - $wx}]} {} set xx 0 if {$x == 0} {set x $wx} while {$x < 1} { lappend xx $x set x [expr {$x + $wx}] } lappend xx 1 set BOX(cols) [expr {[llength $xx]-1}] for {set y $ylo} {$y > $wy} {set y [expr {$y - $wy}]} {} set yy 0 if {$y == 0} {set y $wy} while {$y < 1} { lappend yy $y set y [expr {$y + $wy}] } lappend yy 1 set BOX(rows) [expr {[llength $yy]-1}] # Now we can compute coordinates for all the boxes for {set col0 0; set col1 1} {$col1 < [llength $xx]} {incr col0; incr col1} { set xlo [lindex $xx $col0] set xhi [lindex $xx $col1] if {abs($xhi - $xxhi) < .0001} { set BOX(me,col) $col0 } for {set row0 0; set row1 1} {$row1 < [llength $yy]} {incr row0; incr row1} { set ylo [lindex $yy $row0] set yhi [lindex $yy $row1] if {abs($yhi - $yyhi) < .0001} { set BOX(me,row) $row0 } set BOX(xy,$row0,$col0) [list $xlo $ylo $xhi $yhi] } } } ##+########################################################################## # # ::Snapshot::_Snap -- High level handler for capturing image # proc ::Snapshot::_Snap {} { variable W variable msg "Taking Snapshot..." destroy $W ::Snapshot::HideToplevels 1 set iname [::Snapshot::_MakeImage] ::Snapshot::HideToplevels 0 ::Snapshot::_Save $iname image delete $iname } ##+########################################################################## # # ::Snapshot::_MakeImage -- middle level handler for creating image # proc ::Snapshot::_MakeImage {} { variable BOX variable msg set iname ::snap::img set tname ::snap::tmp foreach cmd [info commands ::snap::*] {image delete $cmd} foreach {. . w h} [::Snapshot::_GetToCoords $BOX(rmax) $BOX(cmax)] break image create photo $iname -width $w -height $h set cnt 0 for {set row $BOX(rmin)} {$row <= $BOX(rmax)} {incr row} { for {set col $BOX(cmin)} {$col <= $BOX(cmax)} {incr col} { incr cnt set msg "Taking snapshot $cnt/$BOX(cnt)" ::Snapshot::_MakeOneImage $tname $row $col set to [::Snapshot::_GetToCoords $row $col] set from [::Snapshot::_GetFromCoords $row $col $tname] eval $iname copy $tname -to $to -from $from } } image delete $tname ::Snapshot::_MoveTo $BOX(me,row) $BOX(me,col) return $iname } ##+########################################################################## # # ::Snapshot::_MakeOneImage -- lowest level handler for creating image # proc ::Snapshot::_MakeOneImage {iname row col} { variable WHO ::Snapshot::_MoveTo $row $col if {[info commands $iname] ne ""} {image delete $iname} image create photo $iname -data $WHO return $iname } ##+########################################################################## # # ::Snapshot::_GetToCoords -- returns coords where this cell goes in # the final image # proc ::Snapshot::_GetToCoords {row col} { variable BOX # Get top left corner of image in percentages foreach {xx yy} $BOX(xy,$BOX(rmin),$BOX(cmin)) break foreach {x0 y0 x1 y1} $BOX(xy,$row,$col) break set x0 [expr {round(($x0-$xx) * $BOX(s,w))}] set x1 [expr {round(($x1-$xx) * $BOX(s,w))}] set y0 [expr {round(($y0-$yy) * $BOX(s,h))}] set y1 [expr {round(($y1-$yy) * $BOX(s,h))}] return [list $x0 $y0 $x1 $y1] } ##+########################################################################## # # ::Snapshot::_GetFromCoords -- for the extreme right column # and bottom row we might have to grab from within the image # proc ::Snapshot::_GetFromCoords {row col img} { variable BOX set fromX 0 set fromY 0 foreach {x0 y0 x1 y1} $BOX(xy,$row,$col) break if {$x1 >= 1 && $x0 > 0} { set needX [expr {round($BOX(s,w) * ($x1-$x0))}] set fromX [expr {[image width $img] - $needX}] } if {$y1 >= 1 && $y0 > 0} { set needY [expr {round($BOX(s,h) * ($y1-$y0))}] set fromY [expr {[image height $img] - $needY}] } return [list $fromX $fromY] } ##+########################################################################## # # ::Snapshot::_MoveTo -- Moves screen so a given cell is visible # proc ::Snapshot::_MoveTo {row col} { variable WHO variable BOX foreach {xmin ymin} $BOX(xy,$row,$col) break $WHO xview moveto $xmin $WHO yview moveto $ymin update } ##+########################################################################## # # ::Snapshot::_Save -- Saves our image in a file # proc ::Snapshot::_Save {iname} { variable saveName variable msg set types {} lappend types {"JPEG Files" ".jpg"} lappend types {"PNG Files" ".png"} set ext "jpg" set fname [tk_getSaveFile -defaultextension $ext \ -title "Save Snapshot" \ -filetypes $types \ -initialfile $saveName] if {$fname eq ""} { set msg "Cancelled snapshot" return } set saveName $fname set fmt [expr {[file extension $saveName] eq ".png" ? "png" : "jpeg"}] $iname write $saveName -format $fmt set msg "Saved snapshot as $saveName" } ##+########################################################################## # # ::Snapshot::HideToplevels -- withdraws all top levels so the # main window is not obscured # proc ::Snapshot::HideToplevels {hide} { variable tops if {$hide} { set tops {} foreach w [winfo child .] { if {[winfo class $w] ne "Toplevel"} continue lappend tops $w [wm state $w] if {[wm state $w] eq "normal"} { wm withdraw $w } } raise . update } else { foreach {w wstate} $tops { if {$wstate eq "normal"} { wm deiconify $w } } } } proc comma { num {sep ,} } { while {[regsub {^([-+]?\d+)(\d\d\d)} $num "\\1$sep\\2" num]} {} return $num } ################################################################ # # Demo code # scrollbar .sb_x -command {.c xview} -orient horizontal scrollbar .sb_y -command {.c yview} -orient vertical canvas .c -highlightthickness 0 bind .c <2> [bind Text <2>] ;# Enable dragging w/ <2> bind .c <B2-Motion> [bind Text <B2-Motion>] .c config -xscrollcommand {.sb_x set} .c config -yscrollcommand {.sb_y set} label .l -textvariable ::Snapshot::msg -font {Times 12 bold} -bd 2 -relief ridge button .go -text "Snapshot" -command {::Snapshot::Dialog .c} ::ttk::separator .sep grid .c .sb_y -sticky news grid .sb_x -sticky news #grid .sep - -sticky ew -pady 20 -padx 10 grid .l - -sticky news grid .go - -pady 10 grid rowconfigure . 0 -weight 1 grid columnconfigure . 0 -weight 1 for {set i 0} {$i < 200} {incr i} { set x0 [expr {rand() * 1000 - 500}] set y0 [expr {rand() * 1000 - 500}] set x1 [expr {$x0 + rand()*200}] set y1 [expr {$y0 + rand()*200}] set clr [format "\#%06x" [expr {int(rand()*0xFFFFFF)}]] set clr2 [format "\#%06x" [expr {int(rand()*0xFFFFFF)}]] set width [expr {rand() * 5}] set type [expr {rand() < .5 ? "oval" : "rect"}] .c create $type $x0 $y0 $x1 $y1 -fill $clr -width $width -outline $clr2 } .c config -scrollregion [.c bbox all] return