Introduction

The Tile extension is primarily seen as a means to obtain native-looking widgets with Tk. An application developed with the tile widgets will look like a native application on both Windows and MacOSX and tries to do so on X Windows as well. However it is not currently well known that tile also provides a significantly more simple path to creating new Tk widgets than Tk itself. This article discusses how to use tile to create a new widget.

Note first that the features we shall discuss for constructing new widgets are not currently exposed by tile. In the future the tile stubs table with provide these features but at the time or writing this should be considered a proof-of-concept unless you are actually writing a widget to be included with tile itself.

Tile includes the source for a sample square widget in the square.c file. This file well commented and we shall examine it here. All the other widget code is available for inspection as well and will provide useful tips on achieving the desired results. It is worth comparing this file to the standard Tk sample tkSquare.c to see just how much is gained by using the tile framework.

The Square Widget

modified widget Following the fine example from the original Tk code we are going to create a square widget. This widget contains a square element that can have its size configured at runtime and its placement controlled by an anchoring option. The first thing we must do is break down our design into graphical display elements and widget functionality. In this case we only have to deal with drawing a square in a defined location so we will require a single element for this. To allow the user to modify the position and size of the element we will need some functionality in the widget to manage this.

Widget specification

A tile widget is defined by a widget specification structure that contains pointers to further structures and the implementation functions. There are default and null implementations defined for all the functions so it is only necessary to write code that is specific to our widget.

WidgetSpec SquareWidgetSpec =
{
    "TSquare",			/* className */
    sizeof(Square),		/* recordSize */
    SquareOptionSpecs,		/* optionSpecs */
    SquareCommands,		/* subcommands */
    NullInitialize,		/* initializeProc */
    NullCleanup,		/* cleanupProc */
    CoreConfigure,		/* configureProc */
    NullPostConfigure,		/* postConfigureProc */
    WidgetGetLayout,		/* getLayoutProc */
    WidgetSize, 		/* sizeProc */
    SquareDoLayout,		/* layoutProc */
    WidgetDisplay		/* displayProc */
};

The above structure defines the implementation for the TSquare widget class. The first element sets the name of the class and the third and fouth are pointers to structures that specify the widget sub-commands and options. The remaining elements are the functions that are called to display, modify and configure the widget. In this case the only function we have had to override is the layoutProc because we wish to modify the position of our element at display time. All the remaining functions are pre-written. This alone will significantly reduce the number of bugs in our widget.

Widget configuration

Programmers expect to be able to configure their widgets. Tile widgets are rather less configurable than standard Tk widgets as many display properties are gathered from the theme. Just as in Tk, tile widget configuration is managed by an options specification structure. We also require a widget structure to manage the data. The Tile package provides a structure that contains standard widget data so it is only necessary to define a structure that holds the additional data required for our widget. We do this by defining a widget part and then specifying the widget record as the concatenation of the two structures.

typedef struct
{
    Tcl_Obj *widthObj;
    Tcl_Obj *heightObj;
    Tcl_Obj *reliefObj;
    Tcl_Obj *borderWidthObj;
    Tcl_Obj *foregroundObj;
    Tcl_Obj *paddingObj;
    Tcl_Obj *anchorObj;
} SquarePart;

typedef struct
{
    WidgetCore core;
    SquarePart square;
} Square;

This structure is the same as the option specification structure used for Tk widgets. For each option we provide the type, name and options database name and class name and the position in the structure and default values. At the bottom we bring in the standard widget option defined for all widgets.

static Tk_OptionSpec SquareOptionSpecs[] =
{
    WIDGET_TAKES_FOCUS,

    {TK_OPTION_PIXELS, "-borderwidth", "borderWidth", "BorderWidth",
     DEFAULT_BORDERWIDTH, Tk_Offset(Square,square.borderWidthObj), -1,
     0,0,GEOMETRY_CHANGED },
    {TK_OPTION_BORDER, "-foreground", "foreground", "Foreground",
     DEFAULT_BACKGROUND, Tk_Offset(Square,square.foregroundObj),
     -1, 0, 0, 0},
    
    {TK_OPTION_PIXELS, "-width", "width", "Width",
     "50", Tk_Offset(Square,square.widthObj), -1, 0, 0,
     GEOMETRY_CHANGED},
    {TK_OPTION_PIXELS, "-height", "height", "Height",
     "50", Tk_Offset(Square,square.heightObj), -1, 0, 0,
     GEOMETRY_CHANGED},
    
    {TK_OPTION_STRING, "-padding", "padding", "Pad", NULL,
     Tk_Offset(Square,square.paddingObj), -1, 
     TK_OPTION_NULL_OK,0,GEOMETRY_CHANGED },
    
    {TK_OPTION_RELIEF, "-relief", "relief", "Relief",
     NULL, Tk_Offset(Square,square.reliefObj), -1, TK_OPTION_NULL_OK, 0, 0},
    
    {TK_OPTION_ANCHOR, "-anchor", "anchor", "Anchor",
     NULL, Tk_Offset(Square,square.anchorObj), -1, TK_OPTION_NULL_OK, 0, 0},
    
    WIDGET_INHERIT_OPTIONS(CoreOptionSpecs)
};

Widget commands

A widget is implemented as an ensemble and the subcommands are specified in a NULL terminated array. Tile provides default implementations that are sufficient for our needs again eliminating any requirement to write additional code. It is only necessary for us to list the available commands. For the square widget we will enable the standard set found on all tile widgets. configure and cget work just as they do for standard Tk widgets and set or retrieve widget configuration options. identify is used to identify the display element at a given position within the widget. We shall see more about how this can be used later on. state and instate are used to examine and modify the widgets state from a set of possible states. These commands are used in event handling and will be examined in more detail below.

static WidgetCommandSpec SquareCommands[] =
{
    { "configure",	WidgetConfigureCommand },
    { "cget",		WidgetCgetCommand },
    { "identify",	WidgetIdentifyCommand },
    { "instate",	WidgetInstateCommand },
    { "state",  	WidgetStateCommand },
    { NULL, NULL }
};

Widget layout

A widget is composed from one or more display elements. A key point in designing tile widgets is to correctly separate display from function. How then do we what elements do display and how they should be ordered. We provide a style layout. The layout is defined either as a C structure or using Tcl code and is then associated with a widget class for a specified theme. This ensures that different themes can have different widget layouts. The layout is where we set just which elements are to be used for the widget. Remember is all the idget specification code we have supplied so far we have never yet mentioned that we want the widget to use a square element and not a downarrow or something else.

The layout for our widget is as follows. We want to erase the background with whatever is defined as the background in the current theme. To do this we use a background element and give flags to have it fill all the available space. We also want to provide for padding so we list a padding element in the same grouping as the background. This will also fill the available area and will be drawn on top of the background. The square element should not have its size changed as we need to have it placed and not expanded. We also need it drawn after the other elements and so it is a child of the padding element. This ensures that padding will always surround the square element. We can see later on that we can use Tcl code to modify the widget layout and examine how the various options affect the end result.

TTK_BEGIN_LAYOUT(SquareLayout)
     TTK_NODE("Square.background", TTK_FILL_BOTH)
     TTK_GROUP("Square.padding", TTK_FILL_BOTH,
	 TTK_NODE("Square.square", 0))
TTK_END_LAYOUT

Widget specific code

So far we have specified the data, configuration options, implementation and display layout for our widget without writing any functions. In fact the only function we need to write at this point is a package initialization function that registers the widget specification structure with tile. However we want the placement of the display element to be managed by the widget and so we shall override the default widget layout implementation.

static void
SquareDoLayout(void *clientData)
{
    WidgetCore *corePtr = (WidgetCore *)clientData;
    Ttk_Box winBox;
    Ttk_LayoutNode *squareNode;

    squareNode = Ttk_LayoutFindNode(corePtr->layout, "square");
    winBox = Ttk_WinBox(corePtr->tkwin);
    Ttk_PlaceLayout(corePtr->layout, corePtr->state, winBox);

    /*
     * Adjust the position of the square element within the widget according
     * to the -anchor option.
     */
    
    if (squareNode) {
	Square *squarePtr = clientData;
	Tk_Anchor anchor = TK_ANCHOR_CENTER;
	Ttk_Box b;
	
	b = Ttk_LayoutNodeParcel(squareNode);
	if (squarePtr->square.anchorObj != NULL)
	    Tk_GetAnchorFromObj(NULL, squarePtr->square.anchorObj, &anchor);
	b = Ttk_AnchorBox(winBox, b.width, b.height, anchor);
	
	Ttk_PlaceLayoutNode(corePtr->layout, squareNode, b);
    }
}

The default version of this function (WidgetDoLayout) just calls Ttk_PlaceLayout to walk the layout graph and call the geometry function on each element. We call this as well to get the basic functionality handled and then we search for the square element by name. If we find a square element then we can use the -anchor option and adjust its position within the widget box. Note that we may not find a square element as the widget layout can be redefined at any time from script. This is more obvious when you consider a scrollbar widget. Some themes like to place a grip or dimple on the scrollbar thumb element while others do not.

With this our widget is complete. If we modify the layout to use a display element that is already defined such as a downarrow or sizegrip then we could finish here. But instead we shall continue to see how we write an element.

The Square Element

Elements are defined using a structure which holds a pointer to an option specification and two functions. The first is a Ttk_ElementSizeProc and is used to find the preferred element geometry. The second is a Ttk_ElementDrawProc and is called to have the element draw itself onto a drawable surface.

static Ttk_ElementSpec SquareElementSpec =
{
    TK_STYLE_VERSION_2,
    sizeof(SquareElement),
    SquareElementOptions,
    SquareElementGeometry,
    SquareElementDraw
};

There must be a way to pass display data to the element and this is done using the elementSize and options fields of the element specification structure. This informs tile how much memory to allocate for the element data structure and gives a pointer to an options specification array that will be used to configure this data. Lets see how this looks for our element.

typedef struct
{
    Tcl_Obj *borderObj;
    Tcl_Obj *foregroundObj;
    Tcl_Obj *borderWidthObj;
    Tcl_Obj *reliefObj;
    Tcl_Obj *widthObj;
    Tcl_Obj *heightObj;
} SquareElement;

static Ttk_ElementOptionSpec SquareElementOptions[] = 
{
    { "-background", TK_OPTION_BORDER, Tk_Offset(SquareElement,borderObj),
    	DEFAULT_BACKGROUND },
    { "-foreground", TK_OPTION_BORDER, Tk_Offset(SquareElement,foregroundObj),
    	DEFAULT_BACKGROUND },
    { "-borderwidth", TK_OPTION_PIXELS, Tk_Offset(SquareElement,borderWidthObj),
    	DEFAULT_BORDERWIDTH },
    { "-relief", TK_OPTION_RELIEF, Tk_Offset(SquareElement,reliefObj),
    	"raised" },
    { "-width",  TK_OPTION_PIXELS, Tk_Offset(SquareElement,widthObj), "20"},
    { "-height", TK_OPTION_PIXELS, Tk_Offset(SquareElement,heightObj), "20"},
    { NULL }
};

So the square element can handle background, relief and variable sized borders. The element size is also variable. The options specifications are similar to the standard widget equivalent although all the data items will be Tcl_Obj objects. Now we can see how this information may be used to set the desired geometry.

static void
SquareElementGeometry(
    void *clientData, void *elementRecord,
    Tk_Window tkwin, int *widthPtr, int *heightPtr, Ttk_Padding *paddingPtr)
{
    SquareElement *square = elementRecord;
    int borderWidth = 0;

    Tcl_GetIntFromObj(NULL, square->borderWidthObj, &borderWidth);
    *paddingPtr = Ttk_UniformPadding((short)borderWidth);
    Tk_GetPixelsFromObj(NULL, tkwin, square->widthObj, widthPtr);
    Tk_GetPixelsFromObj(NULL, tkwin, square->heightObj, heightPtr);
}

The geometry function receives a pointer to the element data and from this can return the desired height and width. We have no control over the position we can be drawn in - hence handling the -anchor option at the widget level. We also provide padding information here.

static void
SquareElementDraw(void *clientData, void *elementRecord,
    Tk_Window tkwin, Drawable d, Ttk_Box b, unsigned int state)
{
    SquareElement *square = elementRecord;
    Tk_3DBorder border = NULL, foreground = NULL;
    int borderWidth = 1, relief = TK_RELIEF_FLAT;

    border = Tk_Get3DBorderFromObj(tkwin, square->borderObj);
    foreground = Tk_Get3DBorderFromObj(tkwin, square->foregroundObj);
    Tcl_GetIntFromObj(NULL, square->borderWidthObj, &borderWidth);
    Tk_GetReliefFromObj(NULL, square->reliefObj, &relief);

    Tk_Fill3DRectangle(tkwin, d, foreground,
	b.x, b.y, b.width, b.height, borderWidth, relief);
}

Now we can examine the element drawing function. This function receives a Tk_Window and an X Windows Drawable which describe the surface to draw onto. The region to draw into is passed in as a Ttk_Box and we have a state parameter in case our element must change in response to state changes. For instance we might paint the border sunken when the state includes the pressed bit.

The remaining code is standard Tk code to get the configured options from the element data and draw our shape. Note that the element functions both have a clientData parameter in addition to the element record pointer. This can be initialized when the element is created to point to data that is shared among all instances of this element type. This feature is necessary for elements like those from the Windows XP native theme that require some platform specific setup.

Using the Widget

Not that we have described how to build a widget using the tile APIs we can examine how this widget can be used and modified from Tcl script. First lets create and display a new instance of our square widget to see how it looks by default.

ttk::square .s
pack .s -fill both -expand 1

pressed square default square The above code creates something that looks like a rather boring button. We can change the colour using the -foreground option and we can change the position of the square within the space available using the -anchor option but it is not very interactive. To make this widget do things in response to user input we need to hook up some bindings.

So lets make this act a bit more like a button.

bind TSquare <Enter> {%W instate !disabled {%W state active}}
bind TSquare <Leave> {%W state !active}
bind TSquare <ButtonPress-1> {%W instate !disabled {%W state pressed}}
bind TSquare <ButtonRelease-1> {%W instate {pressed !disabled} {%W state !pressed}}

style configure TSquare -padding {1 1} -relief raised
style map TSquare -relief [list {!disabled pressed} sunken]

With a different set of bindings we could make it 'draggable' - that is the user could drag the box to one side or another. repositioned square

proc SquareDragBegin {w x y} {
    if {![string equal [$w identify $x $y] "padding"]} {
        $w instate !disabled {
            $w state pressed
            variable $w
            array set $w [list x $x y $y]
        }
    }
}

proc SquareDragEnd {w x y} {
    $w instate {pressed !disabled} {
        $w state !pressed
        variable $w
        if {[info exists $w]} {
            set anchor ""
            if {$y < ([winfo height $w] / 3)} { set anchor n }
            if {$y > ([winfo height $w] / 3) * 2} { set anchor s }
            if {$x < ([winfo width $w] / 3)} { append anchor w }
            if {$x > ([winfo width $w] / 3) * 2} { append anchor e }
            if {[string length $anchor] == 0} { set anchor center }
            $w configure -anchor $anchor
            unset $w
        }
    }
}

bind TSquare <ButtonPress-1> {SquareDragBegin %W %x %y}
bind TSquare <ButtonRelease-1> {SquareDragEnd %W %x %y}

We can also change the layout it we desire. To illustrate this lets replace the square element with a label element.

style layout TSquare {
    Square.background -sticky nsew
    Square.padding -sticky nsew -children {
        Square.label
    }
}
 

Now the widget doesn't have a -text option to use to set the label element text so we have to set this as follows:

style configure TSquare -text testing

Which isn't all that convenient as all the square widgets are going to get the same text. But at least we can see it now. As you can see it works just like the previous example and can be dragged around as before.

In the event binding you may have noticed that we can tell which part of a widget the event has been raised for using the widget identify command. All widgets implement this command and we can examine the parts using the folloing binding:

bind TSquare <Motion> {puts "%W [%W identify %x %y]"}

This feature enables us to compose widgets using a number of elements and then perform operations when activity occurs on a particular element. For instance a combobox is a lable and a downarrow. When the downarrow is pressed the element needs to reflect that and the widget needs to show a lis box. However we do not want this to occur when the entry field is clicked.

In fact, left put a down arrow into this layout and see how it looks:

style layout TSquare {
   Square.padding -sticky nsew -children {
       Square.label -sticky ew
       Square.downarrow -sticky e
   }
}

modified widget Now we have a widget with a label on the left and a downarrow on the right. A kind of disabled combobox if we were to add code to show a listbox in reponse to a downarrow click. The image to theright shows how this looks with the Windows XP native theme. If we change to another theme the downarrow element will be drawn differently and another font may be used for the label element.

In fact if you have followed all these steps and now proceed to change the current theme using the style theme use command you will find that the widget reverts to the original widget appearence. This is because we have only modified the layout in the current theme. All the other themes retain their original widget specification.

Finally

Now we have seen how to create a tile widget and make use of it. We have demonstrated that our widget appearance can be modified on a theme by theme basis. Further information can be gathered by examining files distributed with tile.

 

 
Valid XHTML 1.0! SourceForge.net