An efficient optimized tree control widget written in native Python and Tkinter

It's written totally in Python, and only needs Python and Tkinter installed. No other toolkits, widgets, or libraries are needed. Since it seems to fill a void, I've made it publicly available here.

History

This tree widget was written as part of the implementation of a picture browser. I noticed a distinct lack of such a widget for Unix systems, unlike the highly versatile "common control" that's part of Windows 95. (For all the things Microsoft is criticized for, they did invent some neat and very useful new widgets, like the tree control, the tab control, and the drop-down scrollable combo box. X11 widget sets have had to struggle to catch up.)example tree display

I started by searching for such a widget written in Python using Tkinter, and I found a rudimentary one in the contrib section of the Python web site. This worked by the conventional "elegant computer science" solution, with a recursive algorithm that totally re-drew the tree every time the expanded or collapsed state of one of the directory nodes changed. On a file system with thousands of directories and files, this is obviously unacceptable.

During my search I also noticed quite a few other people looking for such a widget, so I decided to write one and contribute it.

My first start-from-scratch attempt was based on the Text widget, where the tree could be moved down to make room for subtree expansion simply by inserting new text lines. All the connecting lines were built piecewise much like MS-DOS text graphics characters. This also had problems with performance and with extreme redraw flicker during scrolling, plus it looked really cheesy.

My next try used the Canvas widget. I'd avoided this approach because I couldn't think of a way to move large bitmapped graphic subtrees around on the fly that didn't involve the consumption of billions of processor cycles in an interpreted language like Python. The breakthrough was when I discovered you could "tag" items such as lines and bitmaps, then move them all to a new position with one Tkinter command. I also discovered the addtag() function would take a bounding box search specification, making it very easy to say "move everything below this node down by x pixels"

This was fast, wasn't strongly impacted by the size of the tree, and looked very nice, in addition to scrolling nicely. Node collapse or expand is fast, and the resulting code is still small and easy to understand.

Originally I subclassed the Frame widget and had scrollbars by default, but this made configuring the internal Canvas widget very difficult and forced my idea of style on people, neither of which was good. I did this because I heard that it "wasn't good" to subclass anything but Frame, but I switched to subclassing Canvas and the universe didn't end, and it made the code cleaner. I guess this is just a coding style question, and I'd be glad to hear arguments pro or con. I also thought about making it a PMW widget, but I figured it would be easiest to use if it was just a standard Canvas with a tree embedded in it.

I didn't notice any broken things about the Tk widgets, Python, or the windowing systems that I had to work around. What a refreshing change.

You can test it by running tree.py interactively from the command line, which should pop up an interactive tree of your file system if all is well.

Key and mouse bindings

Clicking on the folder icons or labels expands or collapses them.

Most of the keystrokes are pretty obvious:

Installation

DownloadTree.py and place it in your site-python directory, or anywhere else on your module search path. Depending on your browser, right-clicking the link and using the "save link as" option may work best.

Tree Methods

These are the methods that the widget supports in addition to the usual Canvas methods.

Tree(master, root_id, root_label, get_contents_callback, [dist_x], [dist_y], [text_offset], [line_flag], [expanded_icon], [collapsed_icon], [regular_icon], [expandable_icon], [collapsible_icon], [node_class], [drop_callback])

Creates a new Tree object. This is a subclass of the Tkinter Canvas widget, so the master argument names the Tkinter parent widget, and any other keyword arguments are handled by the Canvas class. It's a good idea to use all keywords in your code, just as if calling Tkinter, as the arguments are not guaranteed to be in this order.

expanded_icon and collapsed_icon will be the default images for expandable nodes created in this widget. Expandable nodes will also have expand/collapse indicator icons on the end of the connector lines, and the expandable_icon and collapsible_icon arguments control these images. regular_icon will be the default image for nodes that are not expandable.

root_id and root_label are the identifier and the label text for the root node, respectively.

get_contents_callback is the function to be called at the beginning of a node collapse/expand. It is passed the node that's changing state and it should make calls to add_node() to create the children of this node.

dist_x is the horizontal distance in pixels that the icons in a subtree are offset from the parent's icon. The vertical connecting lines will be separated by multiples of this distance. dist_y is the vertical distance between a node and it's parent, or between sibling nodes. text_offset is the horizontal distance between a node's icon and it's label.

line_flag suppresses connecting lines when set to false. The default value is true to allow connector lines.

node_class is by default the Node helper class. This is provided so you can subclass Node for your own purposes, and pass your class in.

drop_callback is called after a successful drag and drop operation. It's called with the source and destination nodes as arguments.

add_node([name], [id], [flag], [expanded_icon], [collapsed_icon])

This method is used by the user-supplied get_contents_callback() function to create subnodes under the node that is currently being expanded. If images are not specified, the tree's defaults will be used.

add_list([list], [name], [id], [flag], [expanded_icon], [collapsed_icon])

This method is similar to add_node() except it adds the new node information to the given list. used. This is intended to generate the new node lists for insert_before(), insert_after(), and insert_children().

Note that it also returns the list, and doesn't require a list argument so the idiom: n.insert_after(add_list(name='foo')) will do the right thing for quick single node insertion operations.

find_full_id()

Returns the first node which matches a given full_id() style list, or None if it couldn't find such a node.

see(node)

Moves the view until node becomes fully visible.

move_cursor(node)

Moves the cursor to node.

cursor_node()

Returns the node under the cursor.

toggle()

Toggle the open/closed state of the node under the cursor.

next()

Move cursor to the next (lower) node.

prev()

Move cursor to the previous (higher) node.

ascend()

Move cursor to immediate parent of current node.

descend()

Attempts to expand node under cursor and move to it's first child. Moves to next lower node if this isn't possible.

first()

Moves cursor to root node.

last()

Moves cursor to last node.

pageup()

Moves cursor one "page" up, where a page is the current height of the window.

pagedown()

Moves cursor one "page" down, similar to pageup().

Node Methods

Node objects are the individual items in the tree.

Note that there are node methods in the code prefixed with "PVT_" - this means that these are housekeeping methods private to the module, and that they may change radically in the future, depending on the whim of the programmer.

Node(parent_node, id, x, y, [parent_widget], [collapsed_icon], [expanded_icon], [label], [expandable_flag])

Creates a new Node object belonging to parent_widget at position x,y with a text label to the right of the icon. collapsed_icon and expanded_icon are images created with a call to PhotoImage(), and are taken from the parent widget if none. If expandable_flag is true, then the node may be expanded/collapsed when double-clicked.

set_collapsed_icon(icon)

Set the image that shows when the node is collapsed. This image must have been created earlier with a call to PhotoImage() or could be one of the tree default images. This is also the image that displays for non-expandable nodes.

set_expanded_icon(icon)

Set the image that shows when the node is expanded. This image must have been created earlier with a call to PhotoImage() or could be one of the tree default images.

parent()

Return node's parent node.

prev_sib()

Return node's previous sibling node. This is the node directly above the current node, with the same parent. If the current node is the first child node of it's parent, thenNoneis returned.

next_sib()

Return node's next sibling node. This is the node directly below the current node, with the same parent. If the current node is the last child node of it's parent, thenNoneis returned.

prev_visible()

Return the node directly above the current node.

next_visible()

Return the node directly below the current node.

children()

Returns the list of child nodes.

get_label()

Returns the text label associated with the node as a string.

set_label(text)

Sets the text label associated with the node to the given string.

full_id()

Returns the current node's id and ids of all parents as a list.

expanded()

Returns true if the node is expanded, false otherwise.

expandable()

Returns true if the node can be expanded, false otherwise.

expand()

Expands the node if it has the expandable flag set, displays the children (if any) and changes the icon to the expanded icon. The get_contents_callback() function is called before the start of the operation, and is expected to make a series of add_child() calls to set the children to be displayed.

collapse()

Collapses the node if it is expanded, removing all children from the display, and changes the icon to the collapsed icon.

toggle_state()

Changes node's state from expanded to collapsed, and vice versa, according to the rules for expand() and collapse().

delete()

Delete node and any children from tree.

insert_before(nodes)

Insert list of nodes immediately before this one. Since the insert operations are very expensive, they take lists of new nodes to be inserted in one step. Call the parent tree's add_node() function to generate the list of nodes.

insert_after(nodes)

Insert list of nodes immediately after this one.

insert_children(nodes)

Insert list of nodes into beginning of list of children.

Icon images

If you supply your own icons of a different size, you will probably also need to set dist_x, dist_y, and text_offset to compensate. The default icons are roughly 15x15 pixels.

As a starting point, here are the transparent GIF89a files used to create the default icons. Since the PhotoImage function only understands GIFs that have been MIME-encoded into a string, you can use the Python "base64" module to get a string you can paste into your code. This technique avoids needing several small auxiliary GIF files.

>>> import base64
>>> x=open('mini-file.gif').read()
>>> base64.encodestring(x).strip()
'R0lGODlhCwAOAJEAAAAAAICAgP///8DAwCH5BAEAAAMALAAAAAALAA4AAAIphA+jA+JuVgtUtMQe
PJlWCgSN9oSTV5lkKQpo2q5W+wbzuJrIHgw1WgAAOw=='

page-style regular file icon File icon
manila folder style closed folder icon Closed folder icon
manila folder style open folder icon Open folder icon
expandable node icon Expandable icon
collapsible node icon Collapsible icon

treedemo-icons.py is a demonstration of how you can control the look and feel of the widget. I made transparent GIFs out of the bitmaps used by the KDE filemanager, and suppressed the lines, to give a good imitation of it's look and feel. It also uses 3 different icon colors to distinguish between symbolic links, regular files, and special files. The original icon files are listed below:
triangle pointing down open.gif
triangle pointing right close.gif
white file file1.gif
green file file2.gif
red file file3.gif

Drag'n'Drop

There is very little the programmer needs to do to enable drag'n'drop. First, you need to pass in a function to the drop_callback argument of the Tree constructor. This function will be called with two arguments. The first argument is the node that was originally dragged (the source), and the second argument is the node that was dropped onto (the target). During this function call, you need to update your application's internal data structures to reflect what the user indicated with the drag'n'drop. For instance, a file manager widget would perform the actual file or directory moves.

This function also has the responsiblity of updating the tree as necessary. If a node appears, or disappears, or gets moved, this function must do that.

This uses Tkdnd, so any other widget that also uses Tkdnd will be able to interact and perform drag'n'drop. For instance, a web browser might drop links into a tree of bookmarks. In this case, the source would be the link object, and not a node. The callback function would create a node containing the new bookmark and insert it at the proper place.

treedemo-dnd.py demonstrates drag'n'drop with two totally independent trees in two separate windows.

More example code

treedemo-dirs.py is a standalone example that does the same thing as the example in the module itself. This should be a little more understandable than digging through the widget code.

treedemo-complex.py demonstrates Node subclassing to attach a Windows-style right-click menu the nodes, and the insert/delete methods.

Valid HTML 4.01 Transitional