TIP #48 Version 1.6: Basic themeing infrastructure for Tk

This is not necessarily the current version of this TIP.


TIP:48
Title:Basic themeing infrastructure for Tk
Version:$Revision: 1.6 $
Authors: Frédéric Bonnet <fredericbonnet at free dot fr>
Frédéric Bonnet <fbonnet at users dot sourceforge dot net>
State:Draft
Type:Project
Tcl-Version:8.4
Vote:Pending
Created:Monday, 23 July 2001
Discussions To:news:comp.lang.tcl

Abstract

The Tk Toolkit is one of the last major GUI toolkits lacking themes support. This TIP proposes several changes to widget design that allows custom code to be provided for widget element handling in a transparent and extensible fashion. User-provided code may then be used to alter the widgets' look without the need to alter the Tk core. The proposed changes induce no loss of compatibility, and only slight core changes are needed with no side effect on existing functionality.

Background

The Tk Toolkit appeared on X-Window systems at a time where Motif was the de facto standard for GUI development. It thus naturally adopted Motif's look&feel and its famous 3d border style. First ports to non-X platforms such as Windows and MacOS kept the Motif style, which disappointed many users who felt Tk applications look "foreign". Version 8.0 released around 1996 added native look&feel on these platforms.

Recently, other Open Source toolkits such as QT (used by the KDE project) and Gtk (used by the GIMP graphics editing software and the Gnome project) emerged as powerful and free alternatives to Motif for X-Window GUI development. The rapidly growing success of Open Source systems such as GNU/Linux helped both toolkits attract a vast community of developers, and the firm (and sometimes friendly) competition between both communities led to an explosion of new features. Thirst for freedom and customizability created the need for themeability.

The current implementation of Tk only provides native look&feel on supported platforms (Windows, X-Window, MacOS). This lack partly explains Tk's loss of mindshare, especially amongst Linux developers, where theme support is considered a "cool" or must-have feature.

While yesterday's goal of many GUIs was cross-platform visual uniformity (QT and Gtk borrowed much of their visual appearance to Windows, which borrowed earlier to NeXTStep), it is now quite common to find huge visual differences on today's desktops, even on same systems. Screenshot contests are quite common nowadays.

Rationale

Tk first kept away from the toolkit war. Tk's and its competitors' philosophies are radically opposite. Tk favors high level abstractions and scripting languages such as Tcl, whereas QT and Gtk developments are primarily done using C or C++ (which Tcl/Tk advocates believe to be The Wrong Way). But despite Tk's power, flexibility and ease of use, it has lost serious mindshare, especially amongst newcomers and Linux users who don't care about its cross-platform capabilities.

Many Tk users may see themes support as cosmetic or of lower importance than much needed features suc as megawidgets or objectification. Nevertheless, this is a critical feature to be implemented for the long-term viability of Tk. Many courses are now promoting QT, Gtk or (aarggg!) Swing in place of Motif, leaving no room to Tk. Whatever its qualities (cross-platform, performance, ease of use, internationalization and Unicode support), the lack of themeability will always be seen as one of the main reasons for not using Tk. Applications using Tk instead of Gtk will look as "foreign" on pixmap-themed Linux desktop, or even on newer MacOS and Windows versions, as pre-8.0 applications were on non-X desktops.

The lack of themeability is neither a fatality nor difficult to solve. Tk already allows colors, fonts and border width and relief to be specified for all widgets. What is currently missing is pixmap themeing and border styles. The current proposal describes the needed building blocks for theme support that are both easy to implement and backward compatible.

A straightforward solution would be that introduced by the Dash-patch in the form of new widget options such as -tile. But this approach suffers from several major drawbacks:

Moreover, one of the main goals of a theme being to enforce overall visual consistency, multiplying new options should be avoided. A theme is designed to gather these options into one place so that they can be shared by numerous widgets while avoiding performance or memory hit. A carefully designed theme engine should then only add one new option per widget to set its style (an essential part of a theme).

How far should themeabitily go? A previous version of this document proposed to extend the current 3D border mechanism to allow custom drawing code. Although this proposal was simple, backward compatible and covered most of the needs for themeability (border style often represents the largest part of the visual appearance), it failed to address other significant parts of the user interface. These include radio and check marks, scrollbar arrows, sliders, and other widget elements. From this point of view, the border is only an element of a widget. A complete theme engine should then allow each UI element to be customized, while maximizing code reuse and preserving compatibility. To suit this model, widgets should then be thought of as assembly of elements, and no more as monolithic constructs. This implies a model shift in the way widgets are designed (but not necessarily in the way they are used). Actually, the notion of element is not foreign to Tk, since some widgets (scrollbars) use the same term to designate their subparts.

A quick look at existing implementations

The two most used tookits supporting widget styles are Qt and Gtk+. Both seem to follow the same part, but in slightly distinct manners: they define a fixed set of common elements (arrows, checkmarks...) and associate each with one or several API calls. While Qt follows the OO-path, Gtk+ uses a more traditional procedural API model.

Qt defines a generic QStyle class which is the base class for all styles (Windows, Motif...). QStyle-derived classes implement a number of virtual member methods, each being used to draw or compute the geometry of the many elements. Thanks to polymorphism, widgets can then use any style derived from this base class.

Contrary to the C++ -based Qt that defines a class gathering all style-related methods, GTK+ is C-based and defines individual procedures (eg. gtk_draw_slider).

But overall, both use the same model: a predefined (albeit potentially extensible) set of elements, and associated overloadable methods/procs. Adding new elements implies recompilation and/or code changes. While it is hardly seen as a problem with Qt and Gtk+, since both target C/C++ programming, it doesn't fit the Tcl/Tk model at all.

Proposal (aka There Must Be A Better Way)

This document describes a generic and extensible element handling mechanism. This mechanism allows elements to be created and/or overloaded at run-time in a modular fashion.

Widgets are composed of elements. For instance, a scrollbar is made of arrows, a trough, and a slider. Each element must be declared prior to being used. Elements are designated by a unique name and form a global pool that can be accessed by any widget. Elements may be generic or derived. Elements and classes names are arbitrary, and use a recursive dotted notation. For example, "arrow" designates a generic arrow element, and "Scrollbar.arrow" and "Combobox.arrow" designate derived, widget specific elements.

Elements are declared along with an implementation. This declaration can be made by the system or by widgets themselves, and at run-time, thus allowing extensions to create new and use or derive existing elements.

Implementations are registered in a given style engine. A style engine is thus a collection of element implementations. Style engines can be declared at run-time as well, but are static (since they provide compiled code). Style engines can be layered in order to reuse and redefine existing elements implementations.

A style is an instance of a style engine. Styles can be given client data information that would carry style engine-specific data. For example, a style engine implementing pixmapped elements could be given the pixmaps to use. Styles can be created and deleted at run-time.

Using this scheme, a widget can register elements and their default implementation, but actually use a custom implementation code in a transparent manner depending on its currently applied style. Moreover, elements can be shared across widgets, new elements can be registered dynamically and used transparently. New widgets could also be built in a modular fashion and easily reuse other widget's elements. The proposed mechanism could then be used in a megawidget-like fashion (we could speak about megaelement widgets). Last, it provides a dynamic hook mechanism for overriding the core widget code from loadabkLe extensions, avoiding the need for maintaining core patches.

Functional Specification

Style engines

Style engines gather code for handling a set of elements. For this reason, they are inherently static, alike Tcl_ObjTypes. They can be registered at run-time, queried, but never unregistered.

Styles

Styles are instances of style engines. While engines are static, styles can be dynamic. All styles of the same engine use the same code for handling elements, but using different data provided at creation-time. For example, a generic pixmap engine may be instanciated by several styles providing a different set of pixmaps. Styles can be created at run-time, queried, and freed. Since they are user-visible entities, a Tcl_Obj-based interface is also provided.

Elements

Elements are virtual entities. An element only exists if an implementation has been provided. Thus, elements are created implicitly. They can be queried, but not destroyed. Upon creation, elements are given a unique ID that remains valid for the entire application life time and is used subsequently for all related calls. It serves as a numerical index for fast lookup into internal tables.

Styled elements

Styled elements provide implementations of elements for a given style engine. For this reason, they are inherently static. They can be registered at run-time, queried, but never unregistered. Upon registration, corresponding elements are implicitly registered. A styled element must provide a set of functions for various operations on elements, such as geometry computation and drawing. Since elements can be used on various widgets, a styled element must also provide a list of required widget options. Elements would then pick the option values into the widget record according to the widget's option table.

Detailed Specification

The proposal introduces a set of new public types and APIs, exported from tk.h and the stubs table. The implementation induced very slight and limited changes to the existing code, with only one new private API added (TkGetOptionSpec in tkConfig.c). Most added code is concentrated into one file. There is no side effect on existing functionality.

Types and constants.

 TK_OPTION_STYLE

New Tk_OptionType usually associated with the -style widget option.

 Tk_StyleEngine

Opaque token for handling style engines. May be NULL, meaning the default system engine.

 Tk_StyledElement

Opaque token holding a style-specific implementation of a given element. Subsequently used for performing element ops.

 Tk_Style

Opaque token for handling styles. May be NULL, meaning the default system style.

 typedef struct Tk_ElementOptionSpec {
     char *name;
     Tk_OptionType type;
 } Tk_ElementOptionSpec;

Used to specify a list of required widget options, along with their type. This info will be subsequently used to get option values from the widget record using its option table.

 typedef void (Tk_GetElementSizeProc) _ANSI_ARGS_((ClientData clientData, 
 	char *recordPtr, CONST Tk_OptionSpec **optionsPtr, Tk_Window tkwin,
 	int width, int height, int inner, int *widthPtr, int *heightPtr));
 typedef void (Tk_GetElementBoxProc) _ANSI_ARGS_((ClientData clientData, 
 	char *recordPtr, CONST Tk_OptionSpec **optionsPtr, Tk_Window tkwin,
 	int x, int y, int width, int height, int inner, int *xPtr, int *yPtr, 
 	int *widthPtr, int *heightPtr));
 typedef int (Tk_GetElementBorderWidthProc) _ANSI_ARGS_((ClientData clientData, 
 	char *recordPtr, CONST Tk_OptionSpec **optionsPtr, Tk_Window tkwin));
 typedef void (Tk_DrawElementProc) _ANSI_ARGS_((ClientData clientData, 
 	char *recordPtr, CONST Tk_OptionSpec **optionsPtr, Tk_Window tkwin,
 	Drawable d, int x, int y, int width, int height, int state));

Implementations of various element operations.

 typedef struct Tk_ElementSpec {
     Tk_ElementOptionSpec *options;
     Tk_GetElementSizeProc *getSize;
     Tk_GetElementBoxProc *getBox;
     Tk_GetElementBorderWidthProc *getBorderWidth;
     Tk_DrawElementProc *draw;
 } Tk_ElementSpec;

Static styled element definition.

 #define TK_ELEMENT_STATE_ACTIVE       1<<0
 #define TK_ELEMENT_STATE_DISABLED     1<<1
 #define TK_ELEMENT_STATE_FOCUS        1<<2
 #define TK_ELEMENT_STATE_PRESSED      1<<3

Flags used when drawing elements. Elements may have a slightly different visual appearance depending on their state.

Functions.

 void TkStylePkgInit (TkMainInfo *mainPtr)
 void TkStylePkgFree (TkMainInfo *mainPtr)

Internal procedures used to initialize the style subpackage on a per-application basis.

 CONST Tk_OptionSpec * TkGetOptionSpec (CONST char *name, 
 	Tk_OptionTable optionTable);

Internal function used to retrieve an option specifier from a compiled option table.

 Tk_StyleEngine Tk_RegisterStyleEngine (char *name, Tk_StyleEngine parent)

Registers a new style engine. Name may be NULL, in which case it registers the default engine. Returns a NULL token if an error occurred (eg, registering an existing engine).

 Tk_StyleEngine Tk_GetStyleEngine (char *name)

Returns a token to an existing style engine, or NULL.

 int Tk_RegisterStyledElement (Tk_StyleEngine engine, char *name, 
 	CONST Tk_ElementSpec *templatePtr)

Registers the implementation of an element for a given style engine. Element names use a dotted notation that gives a hierarchical search order. For example, a widget requiring an element named "Scrollbar.vslider" can actually use the "vslider" generic element. Apart from this dotted notation, element names are free-form. However, conventions should be defined, such as capitalized widget classes, and lower case elements. Since whole widgets can act as elements, one can therefore register an element named "Scrollbar".

 int Tk_GetElementId (char *name)

Returns the unique numerical ID for a already registered element.

 Tk_Style Tk_CreateStyle (CONST char *name, Tk_StyleEngine engine, 
 	ClientData clientData)

Creates a new style as an instance of an existing style engine. Client data may be provided, that will be passed as is to element ops.

 Tk_Style Tk_GetStyle (Tcl_Interp *interp, CONST char *name)

Retrieves an existing style by its name, or NULL. In the latter case, leaves an error message in interp if not NULL.

 void Tk_FreeStyle (Tk_Style style)

Free a style returned by Tk_CreateStyle or Tk_GetStyle. It actually decrements an internal reference count so that styles can be shared and deleted safely.

 CONST char * Tk_NameOfStyle (Tk_Style style)

Get a style's name.

 Tk_Style  Tk_AllocStyleFromObj (Tcl_Interp *interp, Tcl_Obj *objPtr)
 Tk_Style Tk_GetStyleFromObj (Tcl_Obj *objPtr)
 void  Tk_FreeStyleFromObj (Tcl_Obj *objPtr)

Tcl_Obj based interface to styles. Tk_AllocStyleFromObj gets (doesn't create) an existing style from an object. Tk_GetStyleFromObj returns the style already stored in the object's internal representation. The object must have been returned by Tk_AllocStyleFromObj. Tk_FreeStyleFromObj frees the style held by the object.

 Tk_StyledElement Tk_GetStyledElement (Tk_Style style, int elementId, 
 	Tk_OptionTable optionTable)

Returns a token for the styled element, for use with widgets having the given optionTable. The token is persistent and doesn't need to be freed, so it can be safely stored if needed. It is used in subbsequent element operations.

 void Tk_GetElementSize (Tk_Style style, Tk_StyledElement element, 
 	char *recordPtr, Tk_Window tkwin, int width, int height, 
 	int inner, int *widthPtr, int *heightPtr)
 void Tk_GetElementBox (Tk_Style style, Tk_StyledElement element, 
 	char *recordPtr, Tk_Window tkwin, int x, int y, int width, 
 	int height, int inner, int *xPtr, int *yPtr, int *widthPtr, 
 	int *heightPtr)
 int Tk_GetElementBorderWidth (Tk_Style style, Tk_StyledElement element, 
 	char *recordPtr, Tk_Window tkwin)
 void Tk_DrawElement (Tk_Style style, Tk_StyledElement element, 
 	char *recordPtr, Tk_Window tkwin, Drawable d, int x, int y, 
 	int width, int height, int state)

Various element operations. First two are used for geometry management. First one only computes the size, while second one computes the box coordinates. The inner parameter is a boolean that controls whether the inner (FALSE) or outer (TRUE) geometry is requested from the maximum outer/minimum inner geometry. Third one returns the uniform internal border width of the element and is mostly intended for whole widgets. Last one draws the element using the given geometry and state.

Implementation

An implementation has been written and completed with respect to the present specification. A patch for tk8.4a3 is available at:

http://www.purl.org/NET/bonnet/pub/style.patch

The square widget implemented test file tkSquare.c has also been rewritten to use the new API for its square element. It demonstrates basic features. Patch file:

http://www.purl.org/NET/bonnet/pub/squarestyle.patch

The sample code registers an element "Square.square" in the default style engine. This element is used by the square widget in its drawing code. A new style engine "fixedborder" is registered, and code is provided for the "Square.square" element. This style engine draws the element's border using a fixed border width given as client data by instanciated styles. Four styles are created as instances of the "fixedborder" element: "flat", "border2", "border4" and "border8" (0, 2, 4 and 8 pixel-wide borders).

Sample test session:

 pack [square .s]
 .s config -style
 .s config -style flat
 .s config -style border2
 .s config -style border4
 .s config -style border8
 .s config -style ""
 pack [square .s2]
 .s2 config -style border2
 .s2 config -style border8

Performances and memory usage

The provided design and implementation is geared towards the best compromise between performances and memory consumption.

Critical performance bottleneck is element querying. In order to minimize element access, elements are identified by unique IDs that act as indexes withing internal tables, allowing direct addressing. Hash tables are used internally by all name pools (engines, styles, elements). Static structures are used whenever possible (for styled element registration, indirectly through widgets's option tables...). Widget processing times are mechanically increased by the extra procedure calls and indirections, but that is the price to pay for better modularity anyway. Additional calls are kept minimal.

Per-widget memory consumption is minimal. A widget usually only needs to store its current style. Element IDs can be shared globally across widgets of the same class and don't need to be stored in the widget record. Moreover, most information is shared internally across widgets of the same class (identified by their option table). Many caching & fast lookup techniques are used throughout the code.

Compatibility

Existing widgets will need to be rewritten in order to become style-aware. The needed code changes may be significant (implying code modularization). However, no incompatibility is introduced. Thus, migrating widgets from the old to the new model can follow a smooth path, similar to that needed for the transition to Tcl_Obj interfaces. Besides, widgets as a whole can act as elements, which may shift the amount of work from the core to the style engines at the expense of a lesser modularity and code reuse.

Future improvements or changes

Glossary

Element

Part of a widget (eg a checkbox mark or a scrollbar arrow), usually active.

Style

The visual appearance of a widget. May include colors, skins, tiles, border drawing style (Windows, Motif...), element pictures.

Styled element

A style-specific implementation of a widget element.

Style engine

A visually consistent collection of styled elements.

Theme

A collection of graphical elements giving a consistent appearance to a whole widget hierarchy, application, or desktop. A theme is usually made of icons, colors, fonts, widget styles, or even desktop background and sounds.

Copyright

This document has been placed in the public domain.


Powered by TclThis is not necessarily the current version of this TIP.

TIP AutoGenerator - written by Donal K. Fellows