snit - Snit's Not Incr Tcl
package require Tcl 8.3
package require snit ?0.9?
Snit is yet another pure Tcl object and megawidget system. It's unique among Tcl object systems (so far as I know) in that it's a system based not on inheritance but on delegation. Object systems based on inheritance only allow you to inherit from classes defined using the same system, and that's a shame. In Tcl, an object is anything that acts like an object; it shouldn't matter how the object was implemented. I designed Snit to help me build applications out of the materials at hand; thus, Snit is designed to be able to incorporate and build on any object, whether it's a hand-coded object, a Tk widget, an Incr Tcl object, a BWidget or almost anything else.
This man page is intended to be a reference only; see the accompanying snitfaq for a gentler, more tutorial introduction to Snit concepts.
The Instance Command
A Snit type or widget's create type method creates objects of the type; each object has a unique name which is also a Tcl command. This command is used to access the object's methods and data, and has this form:
In addition to any delegated or locally-defined instance methods in the type's definition, all Snit objects will have at least the following methods:
Snit defines the following commands for use in object code: type methods, instance methods, constructors, destructors, onconfigure handlers, oncget handlers, and procs. They do not reside in the ::snit:: namespace; instead, they are created with the type, and are directly available.
install myComp using myObjType $self.myComp options...
|
set myComp [myObjType $self.myComp options...]
|
installhull [frame $win options....]
|
option {-font font Font} {Courier 12}
|
option -font {Courier 12}
|
constructor {args} {
$self configurelist $args
}
|
onconfigure name {value} {
set options(name) $value
}
|
oncget name {
return $options(name)
}
|
method name {args...} {
$comp mymethod args...
}
|
onconfigure name {value} {
$comp configure name $value
}
oncget name {
return [$comp cget name]
}
|
A type or widget definition creates a type command, which is used to create instances of the type. The type command this form.
In addition to any typemethods in the type's definition, all types and widgets will have at least the following method:
When an object includes other objects, as when a toolbar contains buttons or a GUI object contains an object that references a database, the included object is called a component. The standard way to handle component objects owned by a Snit object is to assign their names to a instance variable. In the following example, a dog object has a tail object:
snit::type dog {
variable mytail
constructor {args} {
set mytail [tail %AUTO% -partof $self]
$self configurelist $args
}
method wag {} {
$mytail wag
}
}
snit::type tail {
option -length 5
option -partof
method wag {} { return "Wag, wag, wag."}
}
|
Because the tail object's name is stored in an instance variable, it's easily accessible in any method.
As of Snit 0.84, the install command provides an alternate way to create and install the component:
snit::type dog {
variable mytail
constructor {args} {
install mytail using tail %AUTO% -partof $self
$self configurelist $args
}
method wag {} {
$mytail wag
}
}
|
For snit::types, the two methods are equivalent; for snit::widgets and snit::widgetadaptors, the install command properly initializes delegated options by querying the Tk option database.
In the above examples, the dog object's wag method simply calls the tail component's wag method. In OO circles, this is called delegation. Snit provides an easier way to do this, as shown:
snit::type dog {
delegate method wag to mytail
constructor {args} {
set mytail [tail %AUTO% -partof $self]
$self configurelist $args
}
}
|
The delegate statement in the type definition implicitly defines the instance variable mytail to hold the component's name; it also defines the dog object's wag method, delegating it to the tail component.
If desired, all otherwise unknown methods can be delegated to a specific component:
snit::type dog {
delegate method * to mytail
constructor {args} {
set mytail [tail %AUTO% -partof $self]
$self configurelist $args
}
method bark { return "Bark, bark, bark!" }
}
|
In this case, a dog object will handle its own bark method; but wag will be passed along to mytail. Any other method, being recognized by neither dog nor tail, will simply raise an error.
Option delegation is similar to method delegation, except for the interactions with the Tk option database; this is described in the next section. The Tk Option Database
This section describes how Snit interacts with the Tk option database, and assumes the reader has a working knowledge of the option database and its uses. The book Practical Programming in Tcl and Tk by Welch et al has a good introduction to the option database, as does Effective Tcl/Tk Programming.
Snit is implemented so that most of the time it will simply do the right thing with respect to the option database, provided that the widget developer does the right thing by Snit. The body of this section goes into great deal about what Snit requires. The following is a brief statement of the requirements, for reference.
The interaction of Tk widgets with the option database is a complex thing; the interaction of Snit with the option database is even more so, and repays attention to detail.
Setting the widget class: Every Tk widget has a widget class. For Tk widgets, the widget class name is the just the widget type name with an initial capital letter, e.g., the widget class for button widgets is "Button".
Similarly, the widget class of a snit::widget defaults to the unqualified type name with the first letter capitalized. For example, the widget class of
snit::widget ::mylibrary::scrolledText { ... } |
is "ScrolledText". The widget class can also be set explicitly using the widgetclass statement within the snit::widget definition.
Note that only frame and toplevel widgets allow the user to change the widget class name, which is why they are the allowable hull types for normal snit::widgets.
The widget class of a snit::widgetadaptor is just the widget class of its hull widget; this cannot be changed unless the hull widget is a frame or toplevel, in which case it will usually make more sense to use snit::widget rather than snit::widgetadaptor.
Setting option resource names and classes: In Tk, every option has three names: the option name, the resource name, and the class name. The option name begins with a hyphen is all lowercase; it's used when creating widgets, and with the configure and cget commands.
The resource and class names are used to initialize option default values by querying the Tk option database. The resource name is usually just the option name minus the hyphen, but may contain uppercase letters at word boundaries; the class name is usually just the resource name with an initial capital, but not always. For example, here are the option, resource, and class names for several text widget options:
-background background Background
-borderwidth borderWidth BorderWidth
-insertborderwidth insertBorderWidth BorderWidth
-padx padX Pad
|
As is easily seen, sometimes the resource and class names can be inferred from the option name, but not always.
Snit options also have a resource name and a class name. By default, these names follow the rule given above: the resource name is the option name without the hyphen, and the class name is the resource name with an initial capital. This is true for both locally-defined options and explicitly delegated options:
snit::widget mywidget {
option -background
delegate option -borderwidth to hull
delegate option * to text
# ...
}
|
In this case, the widget class name is "Mywidget". The widget has the following options: -background, which is locally defined, -borderwidth, which is explicitly delegated; all other widgets are delegated to a component called "text", which is probably a Tk text widget. If so, mywidget has all the same options as a text widget. The option, resource, and class names are as follows:
-background background Background
-borderwidth borderwidth Borderwidth
-padx padX Pad
|
Note that the locally defined option, "-background", happens to have the same three names as the standard Tk "-background" option; and "-pad", which is delegated implicitly to the "text" component has the same three names for mywidget as it does for the text widget. "-borderwidth", on the other hand, has different resource and class names than usual, because the internal word "width" isn't capitalized. For consistency, it should be; this is done as shown:
snit::widget mywidget {
option -background
delegate option {-borderwidth borderWidth} to hull
delegate option * to text
# ...
}
|
The class name will default to "BorderWidth", as expected.
Suppose, however, that mywidget also delegated "-padx" and "-pady" to the hull. In this case, both the resource name and the class name must be specified explicitly:
snit::widget mywidget {
option -background
delegate option {-borderwidth borderWidth} to hull
delegate option {-padx padX Pad} to hull
delegate option {-pady padY Pad} to hull
delegate option * to text
# ...
}
|
Querying the option database: If you set your widgetclass and option names as described above, Snit will query the option database when each instance is created, and will generally do the right thing when it comes to querying the option database. The remainder of this section goes into the gory details.
Initializing locally defined options: When an instance of a snit::widget is created, its locally defined options are initialized as follows: each option's resource and class names are used to query the Tk option database. If the result is non-empty, it is used as the option's default; otherwise, the default hardcoded in the type definition is used. In either case, the default can be overridden by the caller. For example,
option add *Mywidget.texture pebbled
snit::widget mywidget {
option -texture smooth
# ...
}
mywidget .mywidget -texture greasy
|
Here, "-texture" would normally default to "smooth", but because of the entry added to the option database it defaults to "pebbled". However, the caller has explicitly overridden the default, and so the new widget will be "greasy".
Initializing options delegated to the hull: A snit::widget's hull is a widget, and given that its class has been set it is expected to query the option database for itself. The only exception concerns options that are delegated to it with a different name. Consider the following code:
option add *Mywidget.borderWidth 5
option add *Mywidget.relief sunken
option add *Mywidget.hullbackground red
option add *Mywidget.background green
snit::widget mywidget {
delegate option -borderwidth to hull
delegate option -hullbackground to hull as -background
delegate option * to hull
# ...
}
mywidget .mywidget
set A [.mywidget cget -relief]
set B [.mywidget cget -hullbackground]
set C [.mywidget cget -background]
set D [.mywidget cget -borderwidth]
|
The question is, what are the values of variables A, B, C and D?
The value of A is "sunken". The hull is a Tk frame which has been given the widget class "Mywidget"; it will automatically query the option database and pick up this value. Since the -relief option is implicitly delegated to the hull, Snit takes no action.
The value of B is "red". The hull will automatically pick up the value "green" for its -background option, just as it picked up the -relief value. However, Snit knows that -hullbackground is mapped to the hull's -background option; hence, it queries the option database for -hullbackground and gets "red" and updates the hull accordingly.
The value of C is also "red", because -background is implicitly delegated to the hull; thus, retrieving it is the same as retrieving -hullbackground. Note that this case is unusual; in practice, -background would probably be explicitly delegated to some other component.
The value of D is "5", but not for the reason you think. Note that as it is defined above, the resource name for -borderwidth defaults to "borderwidth", whereas the option database entry is "borderWidth". As with -relief, the hull picks up its own "-borderwidth" option before Snit does anything. Because the option is delegated under its own name, Snit assumes that the correct thing has happened, and doesn't worry about it any further.
For snit::widgetadaptors, the case is somewhat altered. Widget adaptors retain the widget class of their hull, and the hull is not created automatically by Snit. Instead, the snit::widgetadaptor must call installhull in its constructor. The normal way to do this is as follows:
snit::widgetadaptor mywidget {
# ...
constructor {args} {
# ...
installhull using text -foreground white
#
}
#...
}
|
In this case, the installhull command will create the hull using a command like this:
set hull [text $win -foreground white]
|
The hull is a text widget, so its widget class is "Text". Just as with snit::widget hulls, Snit assumes that it will pick up all of its normal option values automatically; options delegated from a different name are initialized from the option database in the same way.
Initializing options delegated to other components: Non-hull components are matched against the option database in two ways. First, a component widget remains a widget still, and therefore is initialized from the option database in the usual way. Second, the option database is queried for all options delegated to the component, and the component is initialized accordingly--provided that the install command is used to create it.
Before option database support was added to Snit, the usual way to create a component was to simply create it in the constructor and assign its command name to the component variable:
snit::widget mywidget {
delegate option -background to myComp
constructor {args} {
set myComp [text $win.text -foreground black]
}
}
|
The drawback of this method is that Snit has no opportunity to initialize the component properly. Hence, the following approach is now used:
snit::widget mywidget {
delegate option -background to myComp
constructor {args} {
install myComp using text $win.text -foreground black
}
}
|
The install command does the following:
Please understand that while Snit is already very stable, it is still early days in Snit's development, and not be too critical. If you have problems, find bugs, or new ideas you are hereby cordially invited to submit a report of your problem, bug, or idea at the SourceForge trackers for tcllib, which can be found at http://sourceforge.net/projects/tcllib/. The relevant category is snit.
One particular area to watch is the interaction of Snit with other megawidget packages. Some widgets in BWidgets for example place their own <Destroy> binding not on a separate bind-tag, but on the widget itself. When used as the hull of a snit::widgetadaptor this causes them to be called before Snit, removing the widget command. A previous version of Snit was tripped by this and threw errors because it tried to operate on and with an already deleted widget command. Snit is now able to deal with this, despite the fact that the ultimate cause is at least bad behaviour of Bwidget, possibly even a bug. This however does not preclude that there might be other issues lurking.
So, if you use a snit::widgetadaptor to adapt somebody else's megawidget, you need to be very careful about making sure the bindtags are done properly. There's no way for Snit to take into account all the possible weird things other megawidget frameworks might do wrong.
During the course of developing Notebook (See http://www.wjduquette.com/notebook), my Tcl-based personal notebook application, I found I was writing it as a collection of objects. I wasn't using any particular object-oriented framework; I was just writing objects in pure Tcl following the guidelines in my Guide to Object Commands (See http://www.wjduquette.com/tcl/objects.html), along with a few other tricks I'd picked up since. And it was working very well. But on the other hand, it was getting tiresome. Writing objects in pure Tcl is straightforward, once you figure it out, but there's a fair amount of boilerplate code to write for each one, especially if you're trying to create megawidgets or create objects with options, like Tk widgets have.
So that was one thing--tedium is a powerful motivator. But the other thing I noticed is that I wasn't using inheritance at all, and I wasn't missing it. Instead, I was using delegation: objects that created other objects and delegated methods to them.
And I said to myself, "This is getting tedious...there has got to be a better way." And one afternoon, on a whim, I started working on Snit, an object system that works the way Tcl works. Snit doesn't support inheritance, but it's great at delegation, and it makes creating megawidgets easy.
I should add, I'm not particularly down on Incr Tcl. But "Snit's Not Incr Tcl" occurred to me while I was casting about for a name, and I guess there was a certainly inevitability about it.
If you have any comments or suggestions (or bug reports!) don't hesitate to send me e-mail at will@wjduquette.com. In addition, there's now a Snit mailing list; you can find out more about it at the Snit home page, see http://www.wjduquette.com/snit.
Snit has been designed and implemented from the very beginning by William H. Duquette. However, much credit belongs to the following people for using Snit and providing me with valuable feedback: Rolf Ade, Colin McCormack, Jose Nazario, Jeff Godfrey, Maurice Diamanti, Egon Pasztor, David S. Cargo, Tom Krehbiel, and Michael Cleverly.
BWidget, C++, Incr Tcl, adaptors, class, mega widget, object, object oriented, widget, widget adaptors
Copyright © 2003, by William H. Duquette