canvas , a Tk command, creates and manipulates canvas widgets.
The canvas widget is Tk's workhorse for 2D graphical display, and can handle both bitmap and vector graphics. It was inspired by Joel Bartlett's ezd program, which provides structured graphics in a Scheme environment.
A canvas is one of the most powerful concepts in Tk. It acts as a drawing plane for lines, rectangles, ovals, polygons, text, arcs (e.g. pieslices) as well as container widget to group other widgets, and it provides the ability to group elements together for creation, deletion, moving, etc.
Look at the pages for each item type for more demos.
Arranged roughly in order from simple to complex:
Atomic canvas tag item groups with tags
Tk's canvas has a lot of support for manipulating multiple items at once by giving a few items the same tag, but I find sometimes I would like to group a bunch of items together, and treat them as if they were only one item.
For example the current tag only applies to the item first under the mouse, so if you have a graphic composite of say, a text and a rectangle with the same tag, you can not use the most generic code:
bind .c <1> {set oldX %x ; set oldY %y} bind .c <B1-Motion> {%W move current [expr %x-$oldX] [expr %y-$oldY]}
because only one of the items, the rectangle or the text, will move. To enable both of the items to be moved at once, I implemented an idea I saw in Zinc, atomic groups.
To do this I added two methods to the canvas, tagconf and tagcget, which allow you to modify the behaviour of the tag as opposed to the items under the tag.
I propose an option -atomic which defaults to off. When it's turned on, if an individual item with a tag's whose l-atomic is true is passed to any method that operates on multiple items (basically move and scale), all of the items possesing the atomic tag have the passed tag added to their list of tags.
Unlike frames, canvases come with a 2-pixel -highlightthickness ... set them to zero if you want the canvas to pack/grid/etc. like a frame does.
Anti-aliasing for line, polygons, oval items (X only, requires the Cairo lib): http://phpsource.net/?page=tk
Jeffrey Hobbs wrote in c.l.t on 2001-12-21:
I think the point is that a completely transparent rectangle is essentially a fully filled for bindings, which allows you to create "hot spots" of bindings that don't require visual extras (like for imagemaps). If an outline of fill is specified, then you have a semantically different box, and triggers only occur for the visible portion.
jal_frezie Would it be possible to allow a canvas to have a transparent background? This would be an easy way to implement hierarchies of graphical items, as you could have a child canvas in a window item on its parent canvas with no graphical indication of the distinction between the items on the two canvases. Thus you could replicate the most useful bit of Zinc, with the only change to the language being that the cammand ".canvas configure -bg {}" would set the background transparent rather than raising an exception. I have looked at the code implementing canvases, but not very much, and I imagine this change would be quite simple to implement.
DKF: The current rendering engine (except possibly on OSX) isn't up to handling alpha-blended windows, and you can only efficiently do non-rectangular windows with an extension.
Polygon items, even if displayed transparent with -fill {}, cover the underlying items, so tag bindings to them don't fire. Get truly transparent polygons by constructing them explicitly from lines:
proc myPolygon {c coords {color black}} { lassign $coordsx0 y0 foreach {x1 y1} [concat [lrange $coords 2 end] $x0 $y0] { $c create line $x0 $y0 $x1 $y1 -fill $color -tag markup set x0 $x1; set y0 $y1 } }
DKF: You can do this more easily by using the fact that lines need not just be point-to-point entities.
proc myPolygon2 {c coords {color black}} { # Close the path first set p0 [lrange $coords 0 1] set pEnd [lrange $coords end-1 end] if {$p0 ne $pEnd} { eval lappend coords $p0 } # Now create the item # note that the result of this command (the item id) is the proc result too $c create line $coords -fill $color -tag markup }
Adding panning to a canvas is simple. Suppose you have a canvas widget (with or without scrollbars, that doesn't matter). The only thing to add are two small bindings:
canvas .c pack .c bind .c <ButtonPress-1> {%W scan mark %x %y} bind .c <B1-Motion> {%W scan dragto %x %y 1}
That's all. Pressing the mouse button 1 will initiate panning and subsequent moving pans the widget. You do not need to take care of scaling or other factors since all is done in window coordinates. Note the number '1' as the last argument to 'scan dragto'. This is the gain factor which defaults to 10. Setting it to '1' make the command behave like the cursor sticks to the point where you pressed the mouse button.
If you want to have fun, try to remove the first of the two bindings or set the gain to other values ... :-)
MAK: This function will return all of the tags for items that are currently visible (either entirely visible if partial is 0 or partly off-screen if partial is 1) within the canvas, provided you've got your scroll region set correctly.
proc canvasVisibleTags { hWnd {partial 1} } { lassign [$hWnd cget -scrollregion] xmin ymin xmax ymax lassign [$hWnd yview] y1 y2 lassign [$hWnd xview] x1 x2 set top [expr {($ymax - $ymin) * $y1 + $ymin}] set bot [expr {($ymax - $ymin) * $y2 + $ymin}] set left [expr {($xmax - $xmin) * $x1 + $xmin}] set right [expr {($xmax - $xmin) * $x2 + $xmin}] if {$partial} { return [$hWnd find overlapping $left $top $right $bot] } else { return [$hWnd find enclosed $left $top $right $bot] } }
MAK: I didn't see any "see" functions for the canvas on the Wiki, so here is a somewhat reformatted (for my own aesthetics) version of a similar function found in an old usenet post [L1 ] with the added capability that you can specify more than one item as what you want to scroll to.
Useful if you have multiple items that together comprise one logical item (in my case, in my own tree widget where I want to "see" the expand/collapse button, the icon, and the label rather than just the label). This algorithm will scroll only as far as necessary to make the specified items visible, rather than centering on the specified items.
proc canvasSee { hWnd items } { set bbox [eval $hWnd bbox $items] if {$bbox == ""} { return } lassign $bbox x1 y1 x2 y2 lassign [$hWnd yview] top bottom lassign [$hWnd xview] left right lassign [$hWnd cget -scrollregion] x_min y_min x_max y_max set width [expr {$x_max - $x_min}] set right [expr {$right * $width}] set left [expr {$left * $width}] set height [expr {$y_max - $y_min}] set top [expr {$top * $height}] set bottom [expr {$bottom * $height}] if { $x1 < $left } { $hWnd xview moveto [expr {double($x1-$x_min)/$width}] } elseif {$x2 > $right} { $hWnd xview moveto [expr {double($x2-$x_min-($right-$left))/$width}] } if { $y1 < $top } { $hWnd yview moveto [expr {double($y1-$y_min)/$height}] } elseif {$y2 > $bottom} { $hWnd yview moveto [expr {double($y2-$y_min-($bottom-$top))/$height}] } }
This is another similarly reformatted and extended algorithm from another usenet post [L2 ] -- unlike the above, this one tends to center the specified items within the visible area of the canvas.
proc canvasSee { hWnd items } { set box [eval $hWnd bbox $items] if {$box == ""} { return } if {[string match {} [$hWnd cget -scrollregion]] } { # People really should set -scrollregion you know... lassign $box x y x1 y1 set x [expr round(2.5 * ($x1+$x) / [winfo width $hWnd])] set y [expr round(2.5 * ($y1+$y) / [winfo height $hWnd])] $hWnd xview moveto 0 $hWnd yview moveto 0 $hWnd xview scroll $x units $hWnd yview scroll $y units } else { # If -scrollregion is set properly, use this lassign $box x y x1 y1 lassign [$hWnd yview] top btm lassign [$hWnd xview] left right lassign [$hWnd cget -scrollregion] p q xmax ymax set xpos [expr (($x1+$x) / 2.0) / $xmax - ($right-$left) / 2.0] set ypos [expr (($y1+$y) / 2.0) / $ymax - ($btm-$top) / 2.0] $hWnd xview moveto $xpos $hWnd yview moveto $ypos } }
DKF: Canvases don't directly support tiled backgrounds, but you can easily fix this by using a tiled image underneath everything else.
Robert Heller provided the following sample of how to use the canvas's -highlightcolor option (slightly corrected by Jeff Hobbs):
canvas .thecanvas pack .thecanvas .thecanvas create oval 0 0 50 50 -fill red -outline blue -tag theOval .thecanvas itemconfigure theOval -fill \ [tk_chooseColor -initialcolor [.thecanvas itemcget theOval -fill]] .thecanvas itemconfigure theOval -outline \ [tk_chooseColor -initialcolor [.thecanvas itemcget theOval -outline]] proc Rand255 {} { return [expr int(rand() * 256)] } .thecanvas itemconfigure theOval \ -fill [format {#%02x%02x%02x} [Rand255] [Rand255] [Rand255]] .thecanvas itemconfigure theOval \ -outline [format {#%02x%02x%02x} [Rand255] [Rand255] [Rand255]] .thecanvas configure -highlightcolor [format {#%02x%02x%02x} [Rand255] [Rand255] [Rand255]] focus .thecanvas
MG 2005-02-20: Would it be useful for the [canvas bind ...] command to accept an extra substitution to the regular ones, for the canvas id of the item being clicked, and the name of the tag that invoked the binding? I know it's (always?) possible to get the id with [$canvas find overlapping %x,%y], but I'd guess it's something that's needed in pretty much every instance where canvas tags have bindings, so it would probably be much quicker and easier (in terms of learning how to do it and actual execution time) if it were done with a substitution?
Peter Newman 2005-02-20:
set myCanvasItemsCanvasID [.myCanvas create whatever xxx] ; ... .myCanvas bind $myCanvasItemsCanvasID <Event> "MyEventHandler ${myCanvasItemsCanvasID} %x %y" ; ... proc MyEventHandler { myCanvasItemsCanvasID mouseX_inWindow mouseY_inWindow } { xxx ... xxx } ;
That's even faster that an extra substitution to the regular ones, since the substitution is made only once, when the binding script is compiled, rather than every time the event occurs.
MG: That only works if you're binding to a single canvas item. If you're binding to a tag (which is what I meant, though didn't say too clearly). Then you can do something like
pack [canvas .c] for {set i 1} {$i < 10} {incr i} { .c create image [expr {$i*5}] 20 -image test -tags [list clickable] } .c bind clickable <Button-1> {puts "You pressed canvas item %I on canvas %W"}
RS: The canvas item being clicked has the "current" tag...
MG Thanks, RS. Don't recall ever seeing that before :)
Scott Hill 2005-0-01: The tag current is managed automatically by Tk; it applies to the current item, which is the topmost item whose drawn area covers the position of the mouse cursor. If the mouse is not in the canvas widget or is not over an item, then no item has the current tag.
MG: I think all you need to do, assuming you have the Img package is this:
package require Img pack [canvas .c -height 50 -width 50] .c create rectangle 0 0 25 25 -fill blue .c create rectangle 25 25 50 50 -fill green raise . ;# if there's anything over the window on-screen, it'll be obscured in the image image create photo theCanvas -format window -data .c theCanvas write /your/path/to/image.gif -format gif
MAK 2005-01-26: I just noticed something about the way window canvas items are clipped that's quite interesting. I'm not sure if it's a bug or not, but at least it's the same on different platforms: window items are not clipped to the canvas, but rather to their parent window. A demonstration:
frame .f -bg blue canvas .c2 -bg red -width 100 grid rowconfigure .f 0 -weight 1 -minsize 0 grid rowconfigure .f 1 -weight 0 -minsize 0 grid columnconfig .f 0 -weight 1 -minsize 0 grid columnconfig .f 1 -weight 0 -minsize 0 grid [canvas .f.c -bg yellow -width 100 \ -xscrollcommand ".f.x set" -yscrollcommand ".f.y set"] \ -row 0 -column 0 -sticky news grid [scrollbar .f.y -orient vertical -command ".f.c yview"] \ -row 0 -column 1 -sticky news grid [scrollbar .f.x -orient horizontal -command ".f.c xview"] \ -row 1 -column 0 -sticky news grid columnconfig . 0 -weight 1 -minsize 0 grid columnconfig . 1 -weight 1 -minsize 0 grid rowconfigure . 0 -weight 1 -minsize 0 grid .f -row 0 -column 0 -sticky news grid .c2 -row 0 -column 1 -sticky news checkbutton .cb -text "012345678901234567890123456789" -bg green checkbutton .f.cb -text "012345678901234567890123456789" -bg green checkbutton .f.c.cb -text "012345678901234567890123456789" -bg green .f.c create window 0 0 -anchor nw -window .cb .f.c create window 0 30 -anchor nw -window .f.cb .f.c create window 0 60 -anchor nw -window .f.c.cb
This script creates two canvases side by side - one red and yellow with scrollbars. It then creates three identical checkbuttons with green backgrounds - one that's a child of the toplevel, one that's a child of the frame with the canvas and scrollbars, and one that's a child of the canvas itself.
Notice that the topmost checkbutton is not clipped at all - it overlaps both canvases, while the middle checkbutton is clipped to the frame and overlaps the scrollbars, and the bottom checkbutton is clipped to the canvas.
However, that bottom checkbutton still overlaps the margin of the canvas.
I'm tempted to think it's a bug or an oversight, but on the other hand I think I might find it very useful in working around Aqua's overrideredirect problems in one case. Anyway, I found it interesting and didn't see mention of this behavior in the manpage.
DKF: This is general window-clipping behaviour, and is (and should be) that way on all platforms (and is useful/relevant with the text widget too). It is also why widgets embedded in a canvas should usually be children of the canvas itself.
If you look carefully, you'll notice that the third widget overlaps the border of the canvas (it becomes more visible if you use a more visible - thicker and solid - border and highlight ring). A wrapper frame is sometimes necessary to prevent that sort of nonsense...
AMG: I'd like an -elide option for canvas items, similar to the text widget. This option, when set to true, would hide the item and inhibit user interaction. Currently, hiding an option can be done in one of four ways:
All four options require the script to remember how to recreate or otherwise reestablish the item. Adding an -elide option would allow the full item configuration to persist inside the canvas widget, simplifying the script. Unlike moving or deleting the item, [bbox] will still work.
tomas: Does "$canv itemconfigure $item -state hidden" help? (-state normal brings the item back).
Of course instead of an individual item you can use a "tag search expression", so you can make whole swathes of objects disappear or re-appear.
Or did you have something else in mind?
AMG: No, this is perfect. I was unaware of -state hidden. Thank you!
Zinc looks interesting. On the zinc site there are source & unix binaries. Has anyone successfully compiled it for windows? - Ian Gay
Christophe Mertz: As mentionned in zinc, since 3.2.9x version, TkZinc is now compiled for windows. We are also looking for port on OSX... Any volunteer?
LV: What can people tell me about the canvas -state attribute? For instance, on the perl/tk mailing list recently, the following Tcl code was used to demonstrate what appears to be a Tk canvas bug:
package require Tk pack [canvas .c] set w [.c create window 50 50 -window [ label .c.label -text Hello ] -state hidden] set l [.c create line 30 60 70 60 -state hidden] set i 0 proc repeatproc {} { global i w l incr i if { $i%2 == 0 } { .c itemconfigure $w -state hidden .c itemconfigure $l -state hidden } else { .c itemconfigure $w -state normal .c itemconfigure $l -state normal } after 1000 repeatproc } after 1000 repeatproc