6 Tix Object Oriented Programming

This chapter is intended for experienced programmers who want to create new Tix widgets. If you just want use the Tix widgets in your applications, you can skip this chapter.

6.1 Introduction to Tix Object Oriented Programming

Tix comes with a simple object oriented programming (OOP) framework, the Tix Intrinsics, for writing mega-widgets. The Tix Intrinsics is not a general purpose OOP system and it does not support some features found in general purpose OOP systems such as [incr Tcl]. However, the Tix Intrinsics is specially designed for writing mega-widgets. It provides a simple and efficient interface for creating mega-widgets so that you can avoid the complexity and overheads of the general purpose OOP extensions to Tcl.

The hard thing about programming with mega-widgets is to make sure that each instance you create can handle its own activities. Events must be directed to the right widget, procedures must act on data that is internal to that widget, and users should be able to change the options associated with the widget. For instance, we'll show an arrow widget that needs to know what direction it's pointing; this requires each instance of the widget to have its own variable.

Furthermore, each widget should respond properly to changes requested by the application programmer during the program's run. The whole reason people use Tcl/Tk is because they can alter things on the fly.

The advantage of an object-oriented programming system is that you can easily associate a widget with its own data and procedures (methods). This chapter shows how to do that, and how to configure data both at the time the widget is initialized and later during the program.

6.1.1 Widget Classes and Widget Instances

All the mega-widget classes in Tix, such as TixComboBox and TixControl, are implemented in the Tix Intrinsics framework. Also, you can write new widget classes with the Tix Intrinsics. In the next section, I'll go through all the steps of creating a new widget class in Tix. I'll illustrate the idea using a new class ``TixArrowButton'' as an example. TixArrowButton is essentially a button that can display an arrow in one of the for directions (see figure 6-1 ).


(Figure 6-1) Arrow Buttons

Once you have defined your classes, you can create widget instances of these classes. For example, the following code will create four instances of your new TixArrowButton class:

tixArrowButton .up    -direction n
tixArrowButton .left  -direction e
tixArrowButton .right -direction w
tixArrowButton .down  -direction s

6.1.2 What is in a Widget Instance

Each widget instance is composed of three integral parts: variables, methods and component widgets

Variables

Each widget instance is associated with a set of variables. In the example of an instance of the TixArrowButton class, we may use a variable to store the direction to which the arrow is pointing to. We may also use a variable to count how many times the user has pressed the button.

Each variable can be public or private. Public variables may be accessed by the application programmer (usually via configure or cget methods) and their names usually start with a dash ( -). They usually are used to represent some user-configurable options of the widget instance. Private variables, on the other hand, cannot be accessed by the application programmer. They are usually used to store information about the widget instance that are of interests only to the widget writer.

All the variables of an instance are stored in a global array that has the same name as the instance. For example, the variables of the instance .up are stored in the global array .up:. The public variable -direction, which records the direction to which the arrow is pointing to, is stored in .up(-direction). The private variable count, which counts how many times the user has pressed the button, is stored in .up(count). In comparison, the same variables of the .down instance are stored in .down(-direction) and .down(count).

Methods

To carry out operations on the widget, you define a set of procedures called methods (to use common object-oriented terminology). Each method can be declared as public or private. Public methods can be called by the application programmer. For example, if the TixArrowButton class supports the public methods invoke and invert, the application programmer can issue the commands to call these method for the widget instance .up.

.up invert
.up invoke
In contrast, Private methods are of interests only to widget writers and cannot be called by application programmers.

Component Widgets

A Tix mega-widget is composed of one or more component widgets. The main part of a mega-widget is called the root widget, which is usually a frame widget that encompasses all other component widgets. The other component widgets are called subwidgets.

The root widget has the same name as the the mega-widget itself. In the above example, we have a mega-widget called .up. It has a root widget which is a frame widget and is also called .up. Inside .up we have a button subwidget called .up.button.

Similar to variables and methods, component widgets are also classified into public and private component widgets. Only public widgets may be accessed by the application programmer, via the subwidget method (see section 1.3.1 ) of each widget instance.

6.2 Widget Class Declaration

The first step of writing a new widget class is to decide the base class from which the new class. Usually, if the new class does not share any common features with other classes, it should be derived from the TixPrimitive class. If it does share common features with other classes, then it should be derived from the appropriate base class. For example, if the new class support scrollbars, it should be derived from TixScrolledWidget; if it displays a label next to its ``main area'', then it should be derived from TixLabelWidget.

In the case of our new TixArrowButton class, it doesn't really share any common features with other classes, so we decide to use the base class TixPrimitive as its superclass.

6.2.1 Using the tixWidgetClass Command

We can use the tixWidgetClass command to declare a new class. The syntax is:

tixWidgetClass classCommandName {
    -switch value
    -switch value
    ....
}
For example, the following is the declaration section of TixArrowButton:

tixWidgetClass tixArrowButton {
    -classname  TixArrowButton
    -superclass tixPrimitive
    -method {
        flash invoke invert
    }
    -flag {
        -direction -state
    }
    -configspec {
        {-direction direction Direction e}
        {-state state State normal}
    }
    -alias {
        {-dir -direction}
    }
    -default {
        {*Button.anchor         c}
        {*Button.padX           5}
    }
}

(Figure 6-2) declaration of the TixArrowButton Class

We'll look at what each option means as I describe the command in the following sections.

The first argument for tixWidgetClass is the command name for the widget class ( tixArrowButton). Command names are used to create widgets of this class. For example, the code

tixArrowButton .arrow
creates a widget instance .arrow of the class TixArrowButton. Also, the command name is used as a prefix of all the methods of this class. For example, the Foo and Bar methods of the class TixArrowButton will be written as tixArrowButton:Foo and tixArrowButton:Bar.

The class name of the class ( TixArrowButton)is specified by the -classname switch inside the main body of the declaration. The class name is used only to specify options in the TK option database. For example, the following commands specifies the TixArrowButton widget instances should have the default value up for their -direction option and the default value normal for their -state option.

option add *TixArrowButton.direction up
option add *TixArrowButton.state     normal

Notice the difference in the capitalization of the class name and the command name of the TixArrowButton class: both of them has the individual words capitalized, but the command name ( tixArrowButton)starts with a lower case letter while the class name ( TixArrowButton) starts with an upper case letter. When you create your own classes, you should follow this naming convention.

The -superclass switch specifies the superclass of the new widget. In our example, we have set it to tixPrimitive. Again, pay attention to the capitalization: we should use the command name of the superclass, not its class name.

6.3 Writing Methods

After we have declared the new widget class, we can write methods for this class to define its behavior. Methods are just a special type of TCL procedures and they are created by the proc command. There are, however, three requirements for methods. First, their names must be prefixed by the command name of their class. Second, they must accept at least one argument and the first argument that they accept must be called w. Third, the first command executed inside each method must be

upvar #0 $w data

For example, the following is an implementation of the invert method for the class TixArrowButton:

proc tixArrowButton:invert {w} {
    upvar #0 $w data

set curDirection $data(-direction) case $curDirection { n { set newDirection s } s { set newDirection n } # .... } }

Notice that the name of the method is prefixed by the command name of the class ( tixArrowButton). Also, the first and only argument that it accepts is w and the first line it executes is `` upvar #0 $w data''.

The argument w specifies which widget instance this method should act upon. For example, if the user has issued the command

.up invert
on an instance .up of the class tixArrowButton, the method tixArrowButton:invert will be called and the argument w will have the value .up.

The invert method is used to invert the direction of the arrow. Therefore, it should examine the variable .up(-direction), which stores the current direction of the instance .up, and modify it appropriately. It turns out that in TCL, the only clean way to access an array whose name is stored in a variable is the `` upvar #0 $w data'' technique: essentially it tells the intepreter that the array data should be an alias for the global array whose name is stored in $w. We will soon see how the widget's methods use the data array.

Once the mysterious `` upvar #0 $w data'' line is explained, it becomes clear what the rest of the tixArrowButton:invert method does: it examines the current direction of the arrow, which is stored in $data(-direction) and inverts it.

6.3.1 Declaring Public Methods

All the methods of a class are by default private methods and cannot be accessed by the application programmer. If you want to make a method public, you can include its name in the -method section of the class declaration. In our TixArrowButton example, we have declared that the methods flash, invert and invoke are public methods and they can be accessed by the application programmer. All other methods of the TixArrowButton class will be private.

Usually, the names of private methods start with a capital letter with individual words capitalized. The names of public methods start with a lowercase letter.

6.4 Standard Initialization Methods

Each new mega-widget class must supply three standard initialization methods. When an instance of a Tix widget is created, three three methods will be called to initialize this instance. The methods are InitWidgetRec, ConstructWidget and SetBindings and they will be called in that order. The following sections show how these methods can be implemented.

6.4.1 The InitWidgetRec Method

The purpose of the InitWidgetRec method is to initialize the variables of the widget instance. For example, the following implementation of tixArrowButton:InitWidgetRec sets the count variable of each newly created instance to zero.

proc tixArrowButton:InitWidgetRec {w} {
    upvar #0 $w data

set data(count) 0 }

Earlier, we showed how each widget you create is associated with an array of the same name. Within the methods, you always refer to this array through the name data --the method then works properly in each instance of the widget.

Chaining Methods

The above implementation is not sufficient because our TixArrowButton class is derived from TixPrimitive. The class derivation in Tix is basically an is-a relationship: TixArrowButton is a TixPrimitive. TixPrimitive defines the method tixPrimitive:InitWidgetRec which sets up the instance variables of every instance of TixPrimitive. Since an instance of TixArrowButton is also an instance of TixPrimitive, we need to make sure that the instance variables defined by TixPrimitive are also properly initialized. The technique of calling a method defined in a superclass is called the chaining of a method. The following implementation does this correctly:

proc tixArrowButton:InitWidgetRec {w} {
    upvar #0 $w data

tixPrimitive:InitWidgetRec $w set data(count) 0 }

Notice that tixPrimitive:InitWidgetRec is called before anything else is done. This way, we can define new classes by means of successive refinement: we can first ask the superclass to set up the instance variables, then we can modify some of those variables when necessary and also define new variables.

The tixChainMethod call

The above implementation of tixArrowButton:InitWidgetRec is correct but it may be cumbersome if we want to switch superclasses. For example, suppose we want to create a new base class TixArrowWidget, which presumably defines common attributes of any class that have arrows in them. Then, instead of deriving TixArrowButton directly from TixPrimitive, we decide to derive TixArrowButton from TixArrowWidget, which is in turn derived from TixPrimitive:

tixWidgetClass tixArrowWidget {
    -superclass tixPrimitive
    ...
}
tixWidgetClass tixArrowButton {
    -superclass tixArrowWidget
    ...
}
Now we would need to change all the method chaining calls in TixArrowButton from:

tixPrimitive:SomeMethod
to:

tixArrowWidget:SomeMethod
This may be a lot of work because you may have chained methods in many places in the original implementation of TixArrowButton.

The tixChainMethod command solves this problem. It will automatically find a superclass that defines the method we want to chain and calls this method for us. For example, the following is a better implementation of tixArrowButton:InitWidgetRec that uses tixChainMethod to avoid calling tixPrimitive:InitWidgetRec directly:

proc tixArrowButton:InitWidgetRec {w} {
    upvar #0 $w data

tixChainMethod $w InitWidgetRec set data(count) 0 }

Notice the order of the arguments for tixChainMethod: the name of the instance, $w, is passed before the method we want to chain, InitWidgetRec. In general, if the method we want to chain has $1+n$ arguments:

proc tixPrimitive:MethodToChain {w arg1 arg2 ... argn} {
    ...
}
We call it with the arguments in the following order

tixChainMethod $w MethodToChain $arg1 $arg2 ... $argn
We'll come back to more detailed discussion of tixChainMethod shortly. For the time being, let's take it for granted that tixChainMethod must be used in the three standard initialization methods: InitWidgetRec, ConstructWidget and SetBindings

6.4.2 The ConstructWidget Method

The ConstructWidget method is used to creates the components of a widget instance. In the case of TixArrowButton, we want to create a new button subwidget, whose name is button, and use a bitmap to display an arrow on this button. Assuming the bitmap files are stored in the files up.xbm, down.xbm, left.xbm and right.xbm, the string substitution @$data(-direction).xbm will give us the appropriate bitmap depending on the current direction option of the widget instance.

proc tixArrowButton:ConstructWidget {w} {
    upvar #0 $w data

tixChainMethod $w ConstructWidget

set data(w:button) [button $w.button -bitmap @$data(-direction).xbm] pack $data(w:button) -expand yes -fill both }

The tixArrowButton:ConstructWidget method shown above sets the variable data(w:button) to be the pathname of the button subwidget. As a convention of the Tix Intrinsics, we must declare a public subwidget swid by storing its pathname in the variable data(w: swid ).

6.4.3 The SetBindings Method

In your interface, you want to handle a lot of events in the subwidgets that make up your mega-widget. For instance, when somebody presses the button in a TixArrowButton widget, you want the button to handle the event. The SetBindings method is used to creates event bindings for the components inside the mega-widget. In our TixArrowButton example, we use the bind command to specify that the method tixArrowButton:IncrCount should be called each time when the user presses the first mouse button. As a result, we can count the number of times the user has pressed on the button (obviously for no better reasons than using it as a dumb example).

proc tixArrowButton:SetBindings {w} {
    upvar #0 $w data

tixChainMethod $w SetBindings

bind $data(w:button) <1> "tixArrowButton:IncrCount $w" }

proc tixArrowButton:IncrCount {w} { upvar #0 $w data

incr data(count) }

6.5 Declaring and Using Variables

The private variables of a widget class do not need to be declared. In fact they can be initialized and used anywhere by any method. Usually, however, general purpose private variables are initialized by the InitWidgetRec method and subwidget variables are initialized in the ConstructWidget method.

We have seen in the tixArrowButton:InitWidgetRec example that the private variable data(count) was initialized there. Also, the private variable data(w:button) was initialized in tixArrowButton:ConstructWidget and subsequently used in tixArrowButton:SetBindings.

In contrast, public variables must be declared inside the class declaration. The following arguments are used to declare the public variables and specify various options for them:

6.5.1 Initialization of Public Variables

When a widget instance is created, all of its public variables are initialized by the Tix Intrinsics before the InitWidgetRec method is called. Therefore, InitWidgetRec and any other method of this widgte instance are free to assume that all the public variables have been properly initialized and use them as such.

The public variables are initialized by the following criteria.

Type Checker

You can use a type ckecker procedure to check whether the user has supplied a value of the correct type for a public variable. The type checker is specified in the -configspec section of the class declaration after the default value. The following code specifies the type checker procedure CheckDirection for the -direction variable:

    -configspec {
        {-direction direction Direction e CheckDirection}
        {-state state State normal}
    }
    ...
}

proc CheckDirection {dir} { if {[lsearch {n s w e} $dir] != -1} { return $dir } else { error "wrong direction value \"$dir\"" }

Notice that no type checker has been specified for the -state variable and thus its value will not be checked.

If a type checker procedure is specified for a public variable, this procedure will be called once the value of a public variable is determined by the three steps mentioned above.

6.5.2 Public Variable Configuration Methods

After a widget instance is created, the user can assign new values to the public variables using the configure method. For example, the following code changes the -direction variable of the .arr instance to n.

.arr configure -direction n

In order for configuration to work, you have to define a configuration method that does what the programmer expects. The configuration method of a public variable is invoked whenever the user calls the configure method to change the value of this variable. The name of a configuration method must be the name of the public variable prefixed by the creation command of the class and :config. For example, the name configuration method for the -direction variable of the TixArrowButton class is tixArrowButton:config-direction. The following code implements this method:

proc tixArrowButton:config-direction {w value} {
    upvar #0 $w data

$data(w:button) config -bitmap @$value.xbm }

Notice that when tixArrowButton:config-direction is called, the value parameter contains the new value of the -direction variable but data(-direction) contains the old value. This is useful when the configuration method needs to check the previous value of the variable before taking in the new value.

If a type checker is defined for a variable, it will be called before the configuration method is called. Therefore, the configuration method can assume that the type of the value parameter is got is always correct.

Sometimes it is necessary to override the value supplied by the user. The following code illustrates this idea:

proc tixArrowButton:config-direction {w value} {
    upvar #0 $w data

if {$value == "n"} { set value s set data(-direction) $value }

$data(w:button) config -bitmap @$value.xbm return $data(-direction) }

Notice the above code always overrides values of n to s. If you need to override the value, you must do the following two things:

If you do not need to override the value, you don't need to return anything from the configuration method. In this case, the Tix Intrinsics will assign the new value to the instance variable for you.

Configuration Methods and Public Variable Initialization

For efficiency reasons, the configuration methods are not called during the intialization of the public variables. If you want to force the configuration method to be called for a particular public variable, you can specify it in the -forcecall section of the class declaration. In the following example, we force the configuration method of the -direction variable to be called during intialization:

-forcecall {
    -direction
}

6.6 Summary of Widget Instance Initialization

The creation of a widget instance is a complex process. You must understand how it works in order to write your widget classes. The following is the steps taken by the Tix Intrinsics when a widget instance is created:

After the above steps, the creation of the instance is complete and the user can iterate with it using its widget command.

6.7 Loading the New Classes

Usually, you can use a separate script file to store the implementaion of each new widget class. If you have several of those files, it will be a good idea to group the files into a single directory and create a tclIndex file for them so that the new classes can be auto-loaded.

Suppose you have put the class files into the directory /usr/my/tix/classes. You can create the tclIndex file using the tools/tixindex program that comes with Tix:

cd /usr/my/tix/classes
/usr/my/Tix4.0/tools/tixindex *.tcl

The tclIndex file must be created by the tixindex program. You cannot use the standard auto_mkindex command that comes with Tcl.

Once you have created the tclIndex file, you can use your new widget classes by auto-loading. Here is a small demo program that uses the new TixArrowButton class:

#!/usr/local/bin/tixwish
lappend auto_path /usr/my/tix/classes

# Now I can use my TixArrowButton class! # tixArrowButton .arr -direction n pack .arr