The Tree Widget

Much of the information that computers deal with is hierarchical, or can be viewed as a hierarchy. A UNIX file system is one of the most common examples. The Tk widget hierarchy is another. One of the best ways to display an overview of this kind of data is in the form of a tree.

This chapter describes a widget for displaying trees in Tk. The tree widget really has two interfaces. The first is to the basic Tk widget, which is implemented in C++. This is the most flexible and is the one we will look at first. The second interface is implemented as an [incr Tcl] class built on top of the tree widget. The [incr Tcl] class offers a simpler, but less flexible interface that displays trees where each node is made up of a label and an optional bitmap. We will come to that later on.

The tree widget differs from most other Tk widgets in that it doesn't display its own window and does not need to be packed in the window hierarchy. What the tree widget does is manage items that you create in a Tk canvas. You create the tree as a child of a canvas widget and then tell it which canvas items make up each tree node and line. The tree widget calculates the layout and repositions the canvas items to make the tree. Using canvas items for the tree nodes and lines has some great advantages. The tree nodes can be made up of any combination of canvas items, including text, bitmaps, images and other graphics. You design the lines connecting the tree nodes, so you can make them in any color, style or thickness you like.

A Simple Tree

Before we get into any more detail, let's look at an example of a simple tree application: a directory tree. The program below uses the Unix ls(1) command to recursively build up a tree of directories. Its only a simple demonstration, so there are no menus and no bindings yet. (Note that all of the examples shown here can be found in the demos directory of the tree widget extension.)

# create a canvas with horizontal and vertical scrollbars in the # given frame with the given name proc MakeCanvas {frame canvas} { set vscroll [scrollbar $frame.vscroll -command "$canvas yview"] set hscroll [scrollbar $frame.hscroll -orient horiz -command "$canvas xview"] set canvas [canvas $canvas -xscroll "$hscroll set" -yscroll "$vscroll set"] pack $vscroll -side right -fill y pack $hscroll -side bottom -fill x pack $canvas -fill both -expand 1 bind $canvas "$canvas scan mark %x %y" bind $canvas "$canvas scan dragto %x %y" return $canvas } # add the directories under $dir to the tree (recursively) proc ListDirsRec {canvas tree dir} { foreach i [exec ls $dir] { if {[file isdir $dir/$i]} { AddDir $canvas $tree $dir $dir/$i $i ListDirsRec $canvas $tree $dir/$i } } } # add the given directory to the tree # # Args: # canvas - tree's canvas # tree - the tree # parent - pathname of parent dir # dir - pathname of new dir being added # text - text for tree node label (last component of dir) proc AddDir {canvas tree parent dir text} { $canvas create text 0 0 -text $text -tags $dir set line [$canvas create line 0 0 0 0 -tag "line"] $tree addlink $parent $dir $line } # main wm geometry . 400x275 set canvas [MakeCanvas . .c] set tree [tree $canvas.t -layout vertical] cd .. set dir [pwd] AddDir $canvas $tree {} $dir [file tail $dir] ListDirsRec $canvas $tree $dir The figure below shows the window produced by the above code when run from the demos directory in the tree-4.0 distribution (the rootnode directory is a dummy directory created for test purposes).

Creating the Tree

The first thing you need to do to use the tree widget is to create a canvas widget. The MakeCanvas procedure above creates a canvas with two scrollbars and the usual Tk bindings, so you can scroll by dragging the middle mouse button. As we will see later on, the [incr Tcl] tree interface takes care of this part for us.

The tree widget itself is created toward the end of the example with the following command:

set tree [tree $canvas.t -layout vertical] The tree widget, like all Tk widgets, returns its path name, in this case .c.t, when created. For clarity, I prefer to use variable names such as $canvas or $tree to refer to widgets rather than the path names. In this case the path name is quite short, but that is not usually the case.

The tree widget must be created as a child of the canvas widget it will manage (in fact the tree widget derives the name of the canvas widget from its own path name). The default layout of the tree is horizontal or left to right. In this example the layout was set to vertical. You can change the layout at any time by using the configure subcommand:

$tree configure -layout horizontal This produces a tree as shown below.

Adding Nodes to the Tree

Tree nodes are made up of one or mode canvas items, such as text, bitmap, image or other graphics. Canvas items in Tk are referred to by their tags or Ids. Each canvas item has an id and an optional list of tags, where a tag is any (non-integer) string. You can refer to a group of canvas items by assigning them all a common tag. This feature is used by the tree widget to refer to the tree nodes as well.

When the tree widget is created, a single, invisible root node is also created. This node is given an empty tag and can be referred to as "" or {}. To start adding nodes to the tree, you use the tree subcommand addlink:

$canvas create text 0 0 -text $text -tags $dir set line [$canvas create line 0 0 0 0 -tag "line"] $tree addlink $parent $dir $line The addlink subcommand takes three arguments, which are the canvas tags or ids of the parent node, the node you are adding to the tree and the line connecting them. If the new node should be the root of the tree, then parent is specified as the empty tag {}. In the example above, the tree node consists only of a canvas text item displaying the last component of the directory name $text. We use the full path name $dir as the canvas tag, since it uniquely describes the node. We could also have used some other unique value, such as the the directory's i-node. The label is created at the arbitrary position (0, 0). The tree widget will move it to its final position, so we can place it anywhere we like. Likewise the line connecting the parent and child nodes is created as a canvas line with length 0 at position (0, 0), since the tree widget will position the line as needed. The advantage of creating the line yourself rather than having the tree widget do it for you is that you can specify the color, thickness and other properties of the line when you create it. The line does not need to (and should not) have the same tag as the node. It is enough to specify the line's id, as returned from the canvas create command. In fact, in the example above, we could also have specified the canvas id for the node's label rather than using the $dir tag, but that would only work as long as the node is made up of a single canvas item.

More Complex Nodes

To illustrate the point, let's modify the example above to add a bitmap image to each node. To do that we modify the AddDir procedure from the example above and add a new procedure LayoutNode to arrange the items in the node: # add the given directory to the tree # # Args: # canvas - tree's canvas # tree - the tree # parent - pathname of parent dir # dir - pathname of new dir being added # text - text for tree node label (last component of dir) proc AddDir {canvas tree parent dir text} { set bitmap @bitmaps/dir.xbm $canvas create text 0 0 -text $text -tags [list $dir text $dir:text] $canvas create bitmap 0 0 -bitmap $bitmap -tags [list $dir bitmap $dir:bitmap] set line [$canvas create line 0 0 0 0 -tag "line"] LayoutNode $canvas $tree $dir $tree addlink $parent $dir $line } # layout the components of the given node depending on whether # the tree is vertical or horizontal proc LayoutNode {canvas tree dir} { set text $dir:text set bitmap $dir:bitmap if {[$tree cget -layout] == "horizontal"} { scan [$canvas bbox $text] "%d %d %d %d" x1 y1 x2 y2 $canvas itemconfig $bitmap -anchor se $canvas coords $bitmap $x1 $y2 } else { scan [$canvas bbox $bitmap] "%d %d %d %d" x1 y1 x2 y2 $canvas itemconfig $text -anchor n $canvas coords $text [expr "$x1+($x2-$x1)/2"] $y2 } } The figure below shows what the new tree looks like.

In the example above, each node's bitmap and label are initially created at the position (0, 0) and then the LayoutNode procedure is called to center the bitmap above the label for vertical trees or to the left of the label for horizontal trees. All of the components that make up a node are later moved as a unit to their final position in the tree, so the relative positions of the items stay the same. The LayoutNode procedure was written so that it could be called again for each node if the tree's layout is changed. For example, we could now toggle the tree's layout between vertical and horizontal with the following procedure:

# Toggle the layout of the tree between vertical and horizontal proc ToggleLayout {canvas tree} { if {[$tree cget -layout] == "horizontal"} { $tree config -layout vertical } else { $tree config -layout horizontal } # change the layout of the nodes so that the bitmap is on top for # vertical trees and at left for horizontal trees foreach i [$canvas find withtag text] { set dir [lindex [$canvas gettags $i] 0] LayoutNode $canvas $tree $dir $tree nodeconfig $dir } } What this procedure does is loop through all of the tree nodes and change the layout of the items in each node depending on the layout of the tree (horizontal or vertical). The nodeconfig tree subcommand is then called for each node to tell the tree widget to recalculate the node's size and position. The figure below shows the same tree now with the layout set to horizontal.

Using Canvas Tags to Reference Nodes

In the previous example the canvas items that make up a tree node were each given a list of three tags: $canvas create text 0 0 -text $text -tags [list $dir text $dir:text] $canvas create bitmap 0 0 -bitmap $bitmap -tags [list $dir bitmap $dir:bitmap] The first tag $dir is shared by all of the items that make up a single node and is used to reference that node. the ToggleLayout procedure depends on this tag being the first in the list. The second tag identifies all of the node labels (text) or all of the bitmaps (bitmap) in the tree. The third tag is a combination of the first two and identifies the label or bitmap of a particular tree node. This is used in the LayoutNode procedure to change the position of the label and bitmap of each node.

The command:

$canvas find withtag text returns a list of all the canvas items having the tag text. Since each of the tree nodes we created above also has a label with this tag, the return value is a list of all of the labels in the tree. Since we know that the $dir tag (the directory path name for the node) is the first in the list of tags, it can be retrieved with the command: set dir [lindex [$canvas gettags $i] 0] where $i is the id for the node's label or bitmap. We also use this feature later on to get the directory name associated with a node when the user clicks on it with the mouse.

Changing the Distance between Sibling Nodes

Depending on the layout of the tree nodes, you may want to add some extra space between the nodes. For example, in the horizontal tree shown above, the layout looks a bit crowded. You can change this by specifying a border space around the individual nodes with the -border: option to addlink

proc AddDir {canvas tree parent dir text} { ... $tree addlink $parent $dir $line -border 2m } Although this option is set separately on each node, it usually looks best when all of the nodes have the same border size, as shown below. By default there is no border around a node:

Changing the Distance between Parent and Child Nodes

You can also change the distance between parent and child nodes. Unlike the -border option above, which is applied to each node, the -parentdistance option is set once for the entire tree, either as an option when creating the tree or through the configure widget subcommand. The default parentdistance is 30 pixels, however you can use any of the standard Tk units, such as c for centimeter as shown below:

proc AddDir {canvas tree parent dir text} { ... $tree addlink $parent $dir $line -border 2m } ... set tree [tree $canvas.t -layout horizontal -parentdistance 3c] ...

The resulting tree is shown below:

Manipulating Trees

In the previous examples we have seen how to create and display a tree in a Tk canvas. Most applications that display trees also need to modify them dynamically and possibly allow the user to manipulate them directly. In this section we will discuss ways to examine and modify the tree structure and handle user input.

Selecting Tree Nodes

Let's extend the example directory tree application to allow the user to select tree nodes with the mouse and perform operations on them. Since the tree nodes are made up of canvas items, we use the canvas widget's commands to handle the user input. First we add some bindings so that clicking with mouse button 1 on a tree node highlights the label or bitmap and prints out the pathname of the selected directory. The bindings and the procedures that implement them are listed below. # set the bindings for the tree canvas proc SetBindings {canvas tree} { # bind mouse button <1> to select the label or bitmap $canvas bind text <1> "focus %W; SelectNode $canvas" $canvas bind bitmap <1> "SelectBitmap $canvas" } # select the current node's label proc SelectNode {canvas} { $canvas select from current 0 $canvas select to current [string length [$canvas itemcget current -text]] DeSelectBitmap $canvas puts "selected label: [GetPath $canvas]" } # de-select all node labels proc DeSelectNode {canvas} { $canvas select clear } # highlight the node's bitmap proc SelectBitmap {canvas} { catch {focus {}} set path [lindex [$canvas gettags current] 0] DeSelectNode $canvas DeSelectBitmap $canvas $canvas itemconfig current -background [$canvas cget -selectbackground] \ -tags "[$canvas gettags current] selected" puts "selected bitmap: [GetPath $canvas]" } # stop highlighting the node's bitmap proc DeSelectBitmap {canvas} { $canvas itemconfig selected -background [$canvas cget -background] $canvas dtag selected } # return the pathname (dir) for the item currently selected (bitmap or text) proc GetPath {canvas} { set id [$canvas select item] if {"$id" == ""} { return [lindex [$canvas gettags selected] 0] } return [lindex [$canvas gettags $id] 0] } The code to select a node's label is fairly straight forward. You use the canvas features to select the text item under the mouse, given by the special canvas tag current. Since there is no predefined way of selecting a bitmap, we simply set the bitmap's background color to the same color used to display selected text and give it the tag selected, which otherwise has no special meaning in this context. The GetPath routine checks for either a selected label or a selected bitmap and returns the first tag in the list of tags for the item, which is in this case the directory path name.

On my machine, clicking on the node highlighted above produced the following output:

selected label: /home/tcl/new/dist/tree-4.0/demos/rootnode You could of course use other methods to select a node, such as drawing a border around it or changing the bitmap or image used to display it.

Editing Trees Interactively

Suppose we want to allow the user to navigate the tree by double-clicking on the tree nodes. We could define the following behavior: To implement this behavior we add two new bindings that call some new Tcl procedures. These procedures make use of a number of tree subcommands that we haven't discussed yet, but I will explain them shortly. $canvas bind text "ToggleChildren $canvas $tree" $canvas bind bitmap "ToggleParent $canvas $tree" # If the current selection is a leaf, add its subnodes, otherwise # remove them proc ToggleChildren {canvas tree} { set path [GetPath $canvas] if [$tree isleaf $path] { ListDirs $canvas $tree $path $tree draw } else { $tree prune $path } } # If the selected node is the root of the tree, add its parent and siblings # to the tree, otherwise make the selected node the new root of the tree. proc ToggleParent {canvas tree} { global dirtree set path [GetPath $canvas] if [$tree isroot $path] { set dir [file dirname $path] if {$dir != $path} { AddDir $canvas $tree "" $dir [dir_tail $dir] set tail [file tail $path] foreach i [exec ls $dir] { if {[file isdirectory $dir/$i]} { if {$i == $tail} { $tree movelink $path $dir } else { AddDir $canvas $tree $dir $dir/$i "$i" } } } $tree draw } } else { $tree root $path } } # return the last component of the directory name # (/ is a special case) proc dir_tail {dir} { if {$dir == "/"} {return $dir} return [file tail $dir] } The ToggleChildren procedure adds child nodes to a node if none are present or removes them otherwise. First, we use the GetPath procedure defined earlier to get the tag for the selected node, which is, in our case, the directory path name used to uniquely identify the node.

To determine if the node has any subnodes, we use the isleaf tree subcommand. Given the canvas tag or id of a tree node, isleaf will return 1 if the node is a leaf node and 0 otherwise. In this case, if the node is a leaf node, we add its subnodes by calling ListDirs to add the directories under the selected directory.

The call to $tree draw causes the tree to be drawn on the canvas. This call is not actually necessary, since the tree would be drawn anyway, but it makes sure that the nodes (canvas items) are in the right places before they become visible.

If the node is not a leaf, then it has subnodes and we want to remove them from the tree. This is exactly what the prune tree subcommand does. It removes any subnodes of the given node and the corresponding canvas items, but otherwise leaves the node intact.

The other procedure, ToggleParent, is somewhat more complicated. It makes the selected node the root of the tree, unless it already is the root, in which case a new root and its subnodes is inserted before the selected node.

The isroot tree subcommand is similar to isleaf used above. It returns 1 if the given node is the root of the tree and 0 otherwise.

If the selected node is the root of the tree (but the directory is not /), then we add a new subtree for the parent directory, initially under the pseudo root node (tag = ""). We then add the subdirectories of the new root node to the tree using AddDir as before, except that when we come to the node that was originally selected, instead of re-creating it, we move it to its new position under the new root node using the movelink tree subcommand. movelink works much like the UNIX mv command when both arguments are directories. It unhooks the node named by the first argument and inserts it as a child of the node named by the second argument.

In the case where the selected node was not the root of the tree, we simply use the tree root subcommand to make the given node the new root of the tree. The canvas items for any nodes that are no longer accessible from the root node are automatically deleted.

The figure below shows the tree after navigating up the directory hierarchy a few levels.

Pruning Trees, Moving and Removing Nodes

In the previous example we used the prune and movelink tree subcommands. The prune subcommand is used to remove the subnodes from the given node. The movelink command moves the given node to a new position in the tree. One other command that we did not discuss yet is used to remove the named node and all of its subnodes from the tree. The rmlink tree subcommand is similar to the prune subcommand, except that the given node is also removed from the tree. The example below adds key bindings to the previous example so that Control-d removes the selected node and its subtree. bind $canvas "HideNode $canvas $tree" # remove the selected node and its subnodes from the display proc HideNode {canvas tree} { set path [GetPath $canvas] if {"$path" != "" && ![$tree isroot $path]} { $tree rmlink $path } }

Setting a New Root Node

As we saw in the previous example, any tree node that is not already the root of the tree can be made the root of the tree by using the root subcommand. Any nodes that are not subnodes of the given node will be deleted from the tree and the corresponding canvas items deleted.

Displaying A Forest

When we talk about the root of the tree, we usually mean the visible root. As mentioned earlier, there is actually a pseudo root node that is not visible and has the empty tag {}. Since you add the visible root by specifying this pseudo root as the parent, it is also possible to display multiple trees in one canvas by adding multiple visible root nodes under this node. This kind of thing might be useful for displaying a class hierarchy where there might not be a single root.

Traversing the Tree

There are a number of tree subcommands available for querying and traversing the tree that is being displayed. Each of the following subcommands takes as an argument the canvas tag or id of a tree node and returns the tags of one or more tree nodes as a result: Let's use the above commands to implement key bindings for our example tree so that you can use the cursor (arrow) keys to navigate the tree. # add bindings for the arrow keys bind $canvas "SelectNext $canvas $tree %K" bind $canvas "SelectNext $canvas $tree %K" bind $canvas "SelectNext $canvas $tree %K" bind $canvas "SelectNext $canvas $tree %K" # select the current node's parent, child or sibling # depending on the value of direction (Left, Right, Up or Down) proc SelectNext {canvas tree direction} { set id [$canvas select item] set path [lindex [$canvas gettags $id] 0] # for vertical trees its different... if {[$tree cget -layout] == "vertical"} { case $direction in { Left {set direction Up} Right {set direction Down} Up {set direction Left} Down {set direction Right} } } case $direction in { Left { set node [$tree parent $path] if {"$node" == ""} { ToggleParent $canvas $tree set node [$tree parent $path] } } Right { set node [$tree child $path] if {"$node" == ""} { ListDirs $canvas $tree $path set node [$tree child $path] $tree draw } } Down { set node [$tree sibling $path] if {"$node" == ""} { set node [$tree child [$tree parent $path]] } } Up { set next [$tree child [$tree parent $path]] while {"$next" != ""} { set node $next set next [$tree sibling $next] if {"$next" == "$path"} { break; } } } default {return} } if {"$node" != ""} { set next [$canvas find withtag $node:text] $canvas select from $next 0 $canvas select to $next [string length [$canvas itemcget $next -text]] } } The SelectNext procedure above takes as arguments the name of the tree and canvas widgets and a direction, which is one of the arrow key names Up, Down, Right or Left. The action taken depends on the key was pressed, the selected node and the layout of the tree. For example, if the tree layout is horizontal, pressing Left will traverse the tree selecting the current node's parent, adding it to the tree first if necessary. Pressing Right will traverse the tree in the other direction adding subtrees as needed. The Up and Down arrow keys don't add any nodes, they only traverse the selected node's siblings.

Reconfiguring Tree Nodes

When you make any changes to the canvas items that make up a tree node, such as rearranging the layout, adding or removing items, the tree widget needs to be told to recalculate the node's size. The nodeconfigure subcommand does this and also allows you to change the node's options. nodeconfig accepts the same options as the addlink command:
-border width
Specifies the width of the nodes border. This determines the distance between the individual nodes in the tree. The default is 0.
-remove command
Specifies a Tcl command to be evaluated when the node is removed from the tree. This option is necessary when a node contains a window such as a listbox or button, so that these will also be properly deleted. The default action, when a node is to be removed, is to use the canvas delete command to delete the canvas items with the given tag or id.
The -remove option will be used in the next example, where we add Tk windows to tree nodes.

Using Tk Windows in Tree Nodes

In the examples so far the trees have only displayed directories. Normally you would also want to see a list of the files in a selected directory. We could display files in the tree along with the directories, say with a different bitmap, but that might clutter up the tree too much, since there tend to be many more files than directories. We could display a list of files in the selected directory in a separate listbox. In fact, we could make the listbox a part of a tree node and display it there when needed. Whether this is a good idea or not probably depends on the application, but for the sake of example, let's add bindings to the example directory tree application so that clicking on a tree node with the middle mouse button opens or closes a listbox under the node listing the files in the directory. In order to be useful, the example also has to allow resizing of the listbox and add code to get the name of a selected file.

# add a binding to tree node labels to open/close a listbox listing files in # the selected directory $canvas bind text <2> "focus %W; SelectNode $canvas; ShowFiles $canvas $tree" # Display the file names in the selected directory in # a scrolling list beneath the node proc ShowFiles {canvas tree} { set dir [GetPath $canvas] set frame [UniqueName $canvas $dir] # create the frame and list if not already there if {![llength [$canvas find withtag $dir:list]]} { MakeListFrame $canvas $tree $frame $dir } else { RemoveListFrame $canvas $tree $frame $dir } } # generates a unique name for a widget from the given # directory name by replacing the '.'s with '_'s proc UniqueName {canvas dir} { global dirtree if {![catch {set path $dirtree($canvas.$dir)}]} { return $path } if {[catch {incr dirtree(listboxcnt)}]} { set dirtree(listboxcnt) 0 } set dirtree($canvas.$dir) $canvas.list$dirtree(listboxcnt) return $dirtree($canvas.$dir) } # make the list frame for displaying the files in dir proc MakeListFrame {canvas tree frame dir} { global dirtree set bg [$canvas cget -bg] # make the list frame and set up resizing frame $frame -borderwidth 3 -cursor bottom_right_corner scrollbar $frame.scroll -relief sunken -command "$frame.list yview" -width 10 listbox $frame.list -yscrollcommand "$frame.scroll set" -relief sunken \ -bg $bg -selectmode single pack $frame.scroll -side right -fill y pack $frame.list -side left -expand yes -fill both # fill the list with the files in the selected dir # (with the uninteresting files filtered out) set n 0 foreach i [exec ls $dir] { if {[file isfile $dir/$i]} { incr n $frame.list insert end $i } } # insert the list frame in the canvas $canvas create window 0 0 -tags "$dir $dir:list list" \ -window $frame -width 3c -height 2c # arrange the items in the tree node and change the bitmap # to show an "open" directory LayoutNode $canvas $tree $dir $canvas itemconfig $dir:bitmap -bitmap "@bitmaps/open_dir.xbm" $tree nodeconfig $dir -remove "destroy $frame" # add bindings for resizing the listbox in the canvas SetFileListBindings $canvas $tree $frame $frame.list $frame.scroll $dir } # remove the list frame for displaying the files in dir proc RemoveListFrame {canvas tree frame dir} { global dirtree $canvas delete $dir:list destroy $frame $canvas itemconfig $dir:bitmap -bitmap @bitmaps/dir.xbm $tree nodeconfig $dir -remove "" -border 2 set dirtree(list) "" set dirtree(dir) "" } # event proc called to resize the file listbox in the canvas # while the user is dragging its corner proc ResizeList {canvas tree frame list x y dir {when any}} { global dirtree case "$when" { "first" { set dirtree(tlx) $x set dirtree(tly) $y } "last" { $tree nodeconfig $dir } default { set w [$canvas itemcget $dir:list -width] set h [$canvas itemcget $dir:list -height] set nw [expr $w+($x-$dirtree(tlx))] set nh [expr $h+($y-$dirtree(tly))] $canvas itemconfig $dir:list -width $nw -height $nh set dirtree(tlx) $x set dirtree(tly) $y } } } # set the bindings for a file listbox in the tree canvas proc SetFileListBindings {canvas tree frame list scroll dir} { global dirtree bind $frame "ResizeList $canvas $tree $frame $list %x %y $dir first" bind $frame "ResizeList $canvas $tree $frame $list %x %y $dir" bind $frame "ResizeList $canvas $tree $frame $list %x %y $dir last" bind $list <1> "set dirtree(list) %W; set dirtree(dir) $dir; [bind Listbox <1>]" bind $list ChooseFile # set/clear the resize cursor bind $list "$frame config -cursor {}" bind $list "$frame config -cursor bottom_right_corner" bind $scroll "$frame config -cursor {}" bind $scroll "$frame config -cursor bottom_right_corner" } # This proc is called when the user double-clicks on a filename # in one of the listboxes listing the files in a directory. proc ChooseFile {} { set file [GetFilename] if {"$file" == ""} {return} # for test purposes, just print the file name here puts "got $file" } # Return the path name of the file currently selected in the # current node's listbox, or the empty string if none are selected. proc GetFilename {} { global dirtree set dir $dirtree(dir) set list $dirtree(list) if {"$dir" == "" || "$list" == ""} {return ""} set sel [$list curselection] if {![llength $sel]} {return ""} return $dir/[$list get [lindex $sel 0]] } # layout the components of the given node depending on whether # the tree is vertical or horizontal # (Also take the file listbox into consideration if it is open) proc LayoutNode {canvas tree dir} { set text $dir:text set bitmap $dir:bitmap set list [$canvas find withtag $dir:list] if {[$tree cget -layout] == "horizontal"} { scan [$canvas bbox $text] "%d %d %d %d" x1 y1 x2 y2 $canvas itemconfig $bitmap -anchor se $canvas coords $bitmap $x1 $y2 if {"$list" != ""} { scan [$canvas bbox $text $bitmap] "%d %d %d %d" x1 y1 x2 y2 $canvas itemconfig $list -anchor nw $canvas coords $list $x1 $y2 } } else { scan [$canvas bbox $bitmap] "%d %d %d %d" x1 y1 x2 y2 $canvas itemconfig $text -anchor n $canvas coords $text [expr "$x1+($x2-$x1)/2"] $y2 if {"$list" != ""} { scan [$canvas bbox $text $bitmap] "%d %d %d %d" x1 y1 x2 y2 $canvas itemconfig $list -anchor n $canvas coords $list [expr "$x1+($x2-$x1)/2"] $y2 } } } The figure below shows the example application now after some file lists have been opened and resized.

As usual when working with the tree widget, most of the operations have more to do with the canvas widget than with the tree. The tree widget only arranges the nodes in the canvas.

The ShowFiles procedure in the previous example is called to open or close the file list for the selected node in the tree. If the file list is present in the tree (the canvas tag $dir:list is found), it is removed and deleted, otherwise a new frame with a unique name is created as a child of the canvas widget. The listbox and scrollbar will be packed into this frame.

The MakeListFrame procedure takes care of filling out the contents of the new frame in the tree. It creates the listbox and scrollbar and packs them in the frame and then fills the listbox with the list of files in the selected directory. The LayoutNode procedure from earlier examples has been extended here to handle the layout when a listbox frame is part of the node. We also add some bindings to the listbox frames here so that you can resize them by dragging on the corner of the frame with mouse button 1.

The important thing to remember when working with windows in tree nodes or as canvas items in general is that, since they are not managed by a geometry manager, such as the packer, you have to set and get the window size with canvas commands and not with the -width or -height widget options. We set the default size of the listbox frame when we create the canvas window item:

$canvas create window 0 0 -tags "$dir $dir:list list" \ -window $frame -width 3c -height 2c And later, when we want to resize the listbox frame interactively, we use the canvas itemconfig subcommand: set w [$canvas itemcget $dir:list -width] set h [$canvas itemcget $dir:list -height] ... $canvas itemconfig $dir:list -width $nw -height $nh It is important that the canvas knows the size of the window, so that the tree can get the correct information from the canvas about the size of the the tree nodes.

An [incr Tcl] Interface to the Tree Widget

The example directory tree application developed in the previous section only made use of standard Tcl, Tk and the tree extension. In practice, I almost always use a wish with at least the [incr Tcl], TclX and BLT extensions also added. In particular, I prefer to organize Tcl code in an object oriented way using [incr Tcl] classes. The [incr Tcl] extension, also called Itcl, adds classes and inheritance to Tcl and also includes features that make it easy to create mega-widgets or combinations of widgets that behave like the standard Tk widgets.

The tree widget release also contains a library of Itcl mega-widgets, including a wrapper for the basic tree widget. In order to use the Itcl version of the tree widget, you use the capitalized widget name Tree instead of tree. The Itcl tree widget accepts all of the standard tree options and subcommands and passes them on to the underlying tree widget. In addition, the Itcl widget takes care of creating the canvas, tree and scrollbars, defines a standard layout for tree nodes, handles node selection and offers methods for common operations on trees. Below is an example of a simple tree application using the Itcl tree class.

# This is an example of a simple "application" class, i.e.: # this program does nothing but create an instance of this class. itcl_class Simple { inherit TopLevelWidget # constructor: create a toplevel window for the demo constructor {config} { TopLevelWidget::constructor wm title $this {Simple [incr Tcl] Tree Demo} wm minsize $this 10 10 # create an instance of the Itcl "Tree" widget # (based on the C++ "tree" widget) pack [Tree $this.tree] -fill x -expand 1 # add a row of buttons at botton pack [ButtonFrame $this.b -ok_cmd exit] -side bottom -fill x -expand 1 # add some nodes to the tree add_nodes {} root add_nodes root node1 node2 node3 node4 add_nodes node1 node1.1 node1.2 node1.3 add_nodes node2 node2.1 node2.2 node2.3 add_nodes node3 node3.1 node3.2 } # Add one or more subnodes to the given parent node. # In this case, the label is just the same as the node's tag method add_nodes {parent args} { foreach node $args { $this.tree add_node $parent $node \ -bitmap @$bitmap \ -label $node } } # -- public variables (also program options) -- public bitmap "../bitmaps/node.xbm" } # Start the demo: # Note that "start" is a "member" proc in the TopLevelWidget class. # It creates an instance of the above class and handles options and # error checking. Simple :: start The figure below shows the window created by the simple application above:

In the example above, we create a class named Simple that inherits from the TopLevelWidget class, which is part of the Itcl library included in the tree distribution. While, neither of these are needed in order to use the Itcl tree widget, they demonstrate some of the things you can do with Itcl. The TopLevelWidget class creates a new toplevel window with the same name as the class object and also defines a number of useful methods that can be used by the derived classes. One of these methods (actually a class procedure, since it does not require an instance of the class) is used at the end of the example to start the application:

Simple :: start Calls the start procedure in the TopLevelWidget class to withdraw the default Tk main window "." (the Simple class has its own toplevel window), create an instance of the Simple class and pass it any command line arguments as class options. This is eqivalent to the following, but with some extra error reporting code added to give the user a list of valid options if an unknown option is found: wm withdraw . eval "Simple .simple $argv" tkwait window .simple The Itcl tree is then created and packed in the normal way with this command: pack [Tree $this.tree] -fill x -expand 1 The actual window created here is a frame called $this.tree containing a canvas with two scrollbars and the tree widget. Note that $this is the name of the instance of the Simple class that we are working with and also the name of the toplevel window in this example.

Another Itcl class, ButtonFrame is used to add a frame at the bottom of the window with an OK and a Cancel button, used to end the application.

To be completed...

A Simple Drag&Drop Tree Application

To be completed...