How the IncrCanvas works: tagging

Tycho's IncrCanvas class manipulates canvas item tags in order to give the illusion of hierarchically-grouped items, and without the cost of implementing a truly recursive implementation. Although the idea is simple enough, the implementation -- especially when it comes to dealing with bindings and maintaining backwards compatibility with the Tk canvas -- is a little tricky. This section describes IncrCanvas' tagging mechanism.

Each complex item is assigned a unique ID by the IncrCanvas when it is created. The ID starts with an underscore to distinguish it from Tk canvas IDs (which are integers). Every simple item that is part of that complex item is tagged with that ID:

IncrCanvas rule 1: Every simple item contained in a complex item is tagged with the complex item's ID.

Complex items can in general contain other complex items. Thus, the extension of the above rule is:

IncrCanvas corollary 1: Every simple item is tagged with the IDs of all complex items in the hierarchy above it.
If we need to find the complex item containing a particular simple item, we usually want the root complex item of the tree in which that simple item lives. To make finding the root efficient, we give each item contained in a complex item a special tag:
IncrCanvas rule 2 (simple): Every simple item contained in a complex item is tagged with the ID of its root prefixed by "!".

A corollary of these rules identifies root simple items -- that is, simple items that are not contained in a complex item:

IncrCanvas corollary 2: A simple item that has no tag beginning with "!" is a root simple item.

These rules are all that we need to move, find, and manipulate complex items. For example, to move a complex item, all we need to do is ask the canvas to move all items tagged with the complex item's ID -- a single call to the canvas; to find a complex item overlapping a given region of the canvas, we call the canvas' find method and then take the set of roots of the simple items returned, and the root simple items.

Some additional rules are needed to effectively deal with bindings. The issue of bindings to hierarchical items is not straightforward for two reasons: firstly, either item tagging or processing bindings is more complex, and secondly, the semantics of bindings on hierarchical items must be clearly defined first!

(As an example of the latter, suppose that A contains B, and both have a script bound to them. If I click on B, B's script should obviously get executed. But what about A's script? Should I execute that too? At first, the answer seems like yes: do whatever both items have asked for. But suppose the scripts move the items a certain amount: B will get moved twice! So then A has to somehow know that B will be moved and account for it -- but then we have to guarantee something about the order in which the scripts are executed. And so on...)

The approach we have taken is a reasonably efficient and reasonably straightforward compromise. We extend the notion of the root item to distinguish between actual and logical roots: a logical root is an item that is treated as a root even though it is not (necessarily). Only logical roots can be tagged or have scripts bound to them. There is a special method (createroot) for creating new logical roots. Rule 2 thus applies to logical roots as well as actual roots.

This strategy enables finding and binding to items to be implemented with good efficiency using the Tk canvas' tag manipulation commands. Note that if the IncrCanvas is used in the same manner as the Tk canvas -- that is, creating only simple items and non-hierarchical complex items -- binding and tagging works exactly as on the Tk canvas and with little extra cost.

Now, a simple item can have only one root:

IncrCanvas rule 3: A simple item has at most one root, and therefore at most one tag beginning with "!".
When we make a binding to a complex root item, we want only the simple items that have that complex item as root to respond to the binding. Therefore:
IncrCanvas rule 4: To bind an event to a complex item, bind to the tag constructed by prefixing its ID with "!".
Suppose, for example, that item A contains item B, and that A is a root and B is a logical root. Simple items in B are tagged with !B, and simple items in A but not in B are tagged with !A. If we bind event E to A, the event is in fact bound to the tag !A; if we bind event F to B, the event is in fact bound to the tag !B. Now, events on items in B will trigger only F; events on items in A which are not in B will trigger only E.

Note that it is possible for tags to be attached to non-root -- this is because tags are often used to identify individual items, even if no events are bound to them. This will cause inconsistent behavior if events are bound to tags which are attached to non-root items.

Now, because bindings may be made to tags, we make the assignment of tags mirror the assignment of the root tag (!A for item A) to simple items. For example, suppose that A is tagged with "foo." If a binding is made to foo, we want only items tagged with !A to respond. Thus:

IncrCanvas rule 5: If a root item A is tagged with T, then all simple items with tag !A are tagged with T.
Thus, binding event X to foo will cause items in A but not in B to respond to X. There are some minor consequences of rule 5:
  1. Because not all items under A are tagged with foo, it is not possible to move all items tagged foo with a single call to the canvas; instead, we must find the complex items and move them individually.
  2. Finding the bounding box of all items with a given tag may be incorrect if an item contains complex items that are (a) logical roots and (b) outside the bounding box of the other items so tagged. Since components should be inside the bounds of their container item, this shouldn't happen.

The implementation of most IncrCanvas methods based on these rules is reasonably straight-forward. Most of the methods test for three types of argument -- a simple item ID, a complex item ID, or a tag -- and act accordingly. This does makes the code rather verbose, but it's fairly simple to follow once you become familiar with the structural pattern of the methods. For example, a typical method has the pattern:

    if { [string match {[0-9]*} $tag] } {
        # Process a canvas item
        ...
    } elseif { [string match {_*} $tag] } {
        # Process a complex item
        ...
    } else {
        # Process a tag: find matching items
        set items [find withtag $tag]
        ...
    }

Back up
Tycho Home Page


Copyright © 1996-1998, The Regents of the University of California. All rights reserved.
Last updated: 05/16/97, comments to: johnr@eecs.berkeley.edu