[Previous] [Contents] [Next]
22. Writing Your Own Widgets
Although the GTK distribution comes with many types of widgets that should cover most basic needs, there may come a time
when you need to create your own new widget type. Since GTK uses widget inheritance extensively, and there is already a
widget that is close to what you want, it is often possible to make a useful new widget type in just a few lines of code.
But before starting work on a new widget, check around first to make sure that someone has not already written it. This
will prevent duplication of effort and keep the number of GTK widgets out there to a minimum, which will help keep both
the code and the interface of different applications consistent. As a flip side to this, once you finish your widget,
announce it to the world so other people can benefit.
[Previous] [Contents] [Next]
In order to create a new widget, it is important to have an understanding of how GTK objects work. This section is just
meant as a brief overview.
GTK widgets are implemented in an object oriented fashion in the C language. Of course C isn't an OO language, but the claim
is that doing it this way 'greatly improves portability and stability over using current generation C++ compilers'.
The information common to all instances of one class of widgets (e.g., to all Button widgets) is stored in the
class structure. There is only one copy of this in which is stored information about the class's signals (which act
like virtual functions in C). To support inheritance, the first field in the class structure must be a copy of the parent's
class structure.
Of course, since we're using the Pascal unit there's a degree of separation between us and these C structures. Instead
the GTK Unit declares types. The declaration of the GtkButttonClass type looks like:
TYPE
pGtkButtonClass = ^tGtkButtonClass;
tGtkButtonClass = RECORD
parent_class : tGtkBinClass;
pressed : PROCEDURE (button : pGtkButton); cdecl;
released : PROCEDURE (button : pGtkButton); cdecl;
clicked : PROCEDURE (button : pGtkButton); cdecl;
enter : PROCEDURE (button : pGtkButton); cdecl;
leave : PROCEDURE (button : pGtkButton); cdecl;
END;
As you can see the parent_class of tGtkButtonClass is tGtkBinClass. Which in turn is declared as:
TYPE
pGtkBinClass = ^tGtkBinClass;
tGtkBinClass = RECORD
parent_class : tGtkContainerClass;
END;
When a button is treated as a container (for instance, when it is resized), its class structure can be cast to
GtkContainerClass, and the relevant fields used to handle the signals.
There is also a structure for each widget that is created on a per-instance basis. This structure has fields to store
information that is different for each instance of the widget. We'll call this structure the object structure.
For the Button class, it looks like:
TYPE
pGtkButton = ^tGtkButton;
tGtkButton = RECORD
bin : tGtkBin;
child : pGtkWidget;
flag0 : {$ifdef win32} longint
{$else} word
{$endif};
END;
CONST
bm_in_button = 1;
bp_in_button = 0;
bm_button_down = 2;
bp_button_down = 1;
bm_relief = 4;
bp_relief = 2;
And tGtkBin:
TYPE
pGtkBin = ^TGtkBin;
tGtkBin = RECORD
container : tGtkContainer;
child : pGtkWidget;
end;
Note that, similar to the class RECORD, the first field is the object type of the parent class,
so that this RECORD can be cast to the parent class' object type as needed.
[Previous] [Contents] [Next]
Introduction
One type of widget that you may be interested in creating is a widget that is merely an aggregate of other GTK widgets.
This type of widget does nothing that couldn't be done without creating new widgets, but provides a convenient way of
packaging user interface elements for reuse. The FileSelection and ColorSelection widgets in the standard
distribution are examples of this type of widget.
The example widget that we'll create in this section is the Tictactoe widget, a 3x3 array of toggle buttons which triggers
a signal when all three buttons in a row, column, or on one of the diagonals are depressed.
Choosing a Parent Class
The parent class for a composite widget is typically the container class that holds all of the elements of the composite
widget. For example, the parent class of the FileSelection widget is the Dialog class. Since our buttons will be arranged
in a table, it might seem natural to make our parent class the Table class. Unfortunately, this turns out not to work. The
creation of a widget is divided among two functions - a WIDGETNAME_new() function that the user calls, and a
WIDGETNAME_init() procedure which does the basic work of initializing the widget which is independent of the
arguments passed to the _new() function. Descendant widgets only call the _init() procedure of their parent
widget. But this division of labour doesn't work well for tables, which when created need to know the number of rows and
columns in the table. Unless we want to duplicate most of the functionality of gtk_table_new()
in our Tictactoe widget, we had best avoid deriving it from Table. For that reason, we derive it from VBox instead, and
stick our table inside the VBox.
The Unit's Interface
Each widget class has a UNIT file which contains both the INTERFACE
and the IMPLEMENTATION details. The INTERFACE section declares the
object TYPE RECORD's for that widget, along with public functions and procedures. The
UNIT is saved with the .pp or .pas extension like other Pascal files.
All the code presented here has been translated to Pascal (from the original C example) by Frank Loemker
floemaker@techfak.uni-bielefeld.de. We begin with the
INTERFACE for our Tictactoe UNIT:
INTERFACE
USES glib, gdk, gtk;
TYPE
pTictactoe = ^tTictactoe;
tTictactoe = RECORD
vbox : tGtkVBox;
buttons : ARRAY [0..2 , 0..2] OF
pGtkWidget;
END;
pTictactoeClass = ^tTictactoeClass;
tTictactoeClass = RECORD
parent_class : tGtkVBoxClass;
tictactoe : PROCEDURE (ttt : pTictactoe); cdecl;
END;
FUNCTION tictactoe_get_type : guint;
FUNCTION tictactoe_new : pGtkWidget;
PROCEDURE tictactoe_clear(ttt : pTictactoe);
Implementation
We now continue on to the IMPLEMENTATION of our widget. Firstly we need to declare a
type and two constants:
IMPLEMENTATION
CONST
ANZ_SIGNAL = 1;
TYPE
TTT_Signals = (TICTACTOE_SIGNAL);
CONST
tictactoe_signals : ARRAY[TTT_Signals] OF
guint = (0);
The _get_type() FUNCTION
A core function for every widget is
the function WIDGETNAME_get_type(). This function, when first called, tells GTK about the widget class, and gets an
ID that uniquely identifies the widget class. Upon subsequent calls, it just returns the ID.
{ ----------------------------------tictactoe_get_type----------------------------- }
FUNCTION tictactoe_get_type : guint;
CONST
ttt_type : guint = 0;
ttt_info : tGtkTypeInfo = (
type_name : 'Tictactoe';
object_size : sizeof(tTictactoe);
class_size : sizeof(tTictactoeClass);
class_init_func : @tictactoe_class_init2;
object_init_func : @tictactoe_init2;
);
BEGIN
IF (ttt_type = 0) THEN
ttt_type := gtk_type_unique(gtk_vbox_get_type(),
@ttt_info);
tictactoe_get_type := ttt_type;
END;
{ ----------------------------------tictactoe_get_type----------------------------- }
The GtkTypeInfo RECORD has the following definition:
TYPE
pGtkTypeInfo = ^tGtkTypeInfo;
tGtkTypeInfo = RECORD
type_name : pgchar;
object_size : guint;
class_size : guint;
class_init_func : tGtkClassInitFunc;
object_init_func : tGtkObjectInitFunc;
reserved_1 : gpointer;
reserved_2 : gpointer;
base_class_init_func : tGtkClassInitFunc;
END;
The fields of this RECORD are pretty self-explanatory. We'll ignore the reserved_1 and
reserved_2 fields here: they have an important, but as yet largely unimplemented, role in allowing widget options
to be conveniently set from interpreted languages. Once GTK has a correctly filled in copy of this
RECORD, it knows how to create objects of the particular widget type.
The _class_init() PROCEDURE
The WIDGETNAME_class_init() PROCEDURE initializes the fields of the widget's
RECORD, and sets up any signals for the class. For our Tictactoe widget it looks like:
{ ------------------------------tictactoe_class_init------------------------------- }
PROCEDURE tictactoe_class_init(theclass : pTictactoeClass);
VAR object_class : pGtkObjectClass;
BEGIN
object_class := pGtkObjectClass(theclass);
tictactoe_signals[TICTACTOE_SIGNAL] := gtk_signal_new(
'tictactoe',
GTK_RUN_FIRST,
object_class^.thetype,
@theclass^.tictactoe - pointer(theclass),
@gtk_signal_default_marshallerT, GTK_TYPE_NONE, 0);
gtk_object_class_add_signals(object_class, pguint(@tictactoe_signals), ANZ_SIGNAL);
theclass^.tictactoe := NIL;
END;
{ ------------------------------tictactoe_class_init------------------------------- }
Our widget has just one signal, the tictactoe signal that is invoked when a row, column, or diagonal is completely
filled in. Not every composite widget needs signals, so if you are reading this for the first time, you may want to skip
to the next section now, as things are going to get a bit complicated.
The function:
FUNCTION gtk_signal_new( name :
pgchar ;
signal_flags : tGtkSignalRunType;
object_type : tGtkType;
function_offset : guint;
marshaller : tGtkSignalMarshaller;
return_val : tGtkType;
nparams : guint;
args : ARRAY OF CONST) :
guint; cdecl;
Creates a new signal. The parameters are:
- name: The name of the signal.
- signal_flags: Whether the default handler runs before or after user handlers. Usually this will
be GTK_RUN_FIRST, or GTK_RUN_LAST, although there are other possibilities.
- object_type: The ID of the object that this signal applies to. (It will also apply to that objects
descendants.)
- function_offset: The offset within the TYPE RECORD of a pointer to
the default handler.
- marshaller: A function that is used to invoke the signal handler. For signal handlers that have no
arguments
other than the object that emitted the signal and user data, we use the pre-supplied marshaller
PROCEDURE
gtk_marshal_NONE__NONE() which takes no extra parameters.
- return_val: The type of the return value.
- nparams: The number of parameters of the signal handler (other than the two default ones mentioned above)
- args: The types of the parameters.
We define the following PROCEDURE to allow us to call the basic marshaller:
{ ---------------------------gtk_signal_default_marshallerT------------------------- }
PROCEDURE gtk_signal_default_marshallerT( theobject :
pGtkObject ;
func : GTK_SIGNAL_FUNC;
func_data : gpointer ; args : pGtkArg );
cdecl;
BEGIN
gtk_marshal_NONE__NONE(theobject, func, func_data, args);
END;
{ ------------------------------gtk_signal_default_marshallerT------------------------ }
When specifying types, the GtkType constants are used:
CONST
GTK_TYPE_INVALID = 0;
GTK_TYPE_NONE = 1;
GTK_TYPE_CHAR = 2;
GTK_TYPE_UCHAR = 3;
GTK_TYPE_BOOL = 4;
GTK_TYPE_INT = 5;
GTK_TYPE_UINT = 6;
GTK_TYPE_LONG = 7;
GTK_TYPE_ULONG = 8;
GTK_TYPE_FLOAT = 9;
GTK_TYPE_DOUBLE = 10;
GTK_TYPE_STRING = 11;
GTK_TYPE_ENUM = 12;
GTK_TYPE_FLAGS = 13;
GTK_TYPE_BOXED = 14;
GTK_TYPE_POINTER = 15;
GTK_TYPE_SIGNAL = 16;
GTK_TYPE_ARGS = 17;
GTK_TYPE_CALLBACK = 18;
GTK_TYPE_C_CALLBACK = 19;
GTK_TYPE_FOREIGN = 20;
GTK_TYPE_OBJECT = 21;
for the TYPE:
tGtkFundamentalType = longint;
gtk_signal_new() returns a unique integer identifier for the signal, that we store in the
tictactoe_signals array, which we index using an enumeration, TICTACTOE_SIGNAL.
After creating our signals, we need to tell GTK to associate our signals with the Tictactoe class. We do that by calling
gtk_object_class_add_signals(). We then set the pointer which points to the default handler for
the tictactoe signal to NIL, indicating that there is no default action.
The _init() PROCEDURE
Each widget class also needs procedures to initialize the both the object and the class records. Usually, these procedures
have the fairly limited role of setting the fields of the records to default values. For composite widgets, however, they
also create the component widgets.
{ ---------------------------------tictactoe_init--------------------------------- }
PROCEDURE tictactoe_init( ttt : pTictactoe );
VAR
table : pGtkWidget;
i, j : gint;
BEGIN
table := gtk_table_new(3, 3, TRUE);
gtk_container_add(pGtkContainer(ttt), table);
gtk_widget_show(table);
FOR i := 0 TO 2 DO
FOR j := 0 TO 2 DO
BEGIN
ttt^.buttons[i][j] := gtk_toggle_button_new();
gtk_table_attach_defaults(pGtkTable(table), ttt^.buttons[i][j],
i, i+1, j, j+1);
gtk_signal_connect(pGtkObject(ttt^.buttons[i][j]),
'toggled',
GTK_SIGNAL_FUNC(@tictactoe_toggle), ttt);
gtk_widget_set_usize(ttt^.buttons[i][j], 20, 20);
gtk_widget_show(ttt^.buttons[i][j]);
END; { -- inner FOR loop -- }
END;
{ ------------------------------------tictactoe_init-------------------------------- }
{ ------------------------------tictactoe_class_init---------------------------- }
PROCEDURE tictactoe_class_init( theclass : pTictactoeClass );
VAR
object_class : pGtkObjectClass;
BEGIN
object_class := pGtkObjectClass(theclass);
tictactoe_signals[TICTACTOE_SIGNAL] := gtk_signal_new(
'tictactoe',
GTK_RUN_FIRST,
object_class^.thetype,
@theclass^.tictactoe - pointer(theclass),
@gtk_signal_default_marshallerT, GTK_TYPE_NONE, 0);
gtk_object_class_add_signals(object_class, pguint(@tictactoe_signals), ANZ_SIGNAL);
theclass^.tictactoe := NIL;
END;
{ -------------------------------tictactoe_class_init---------------------------- }
And we'll need the following two procedures to allow us to call our _init's in response to signals:
PROCEDURE tictactoe_class_init2( theclass : gpointer );
cdecl;
BEGIN
tictactoe_class_init(theclass);
END;
PROCEDURE tictactoe_init2(ttt : gpointer ;
klass : gpointer); cdecl;
BEGIN
tictactoe_init(ttt);
END;
And the rest...
There is one more function that every widget (except for base widget types like bin that cannot be instantiated)
needs to have - the function that the user calls to create an object of that type. This is conventionally called
WIDGETNAME_new(). In some widgets, though not for the Tictactoe widgets, this function takes arguments, and does
some setup based on the arguments. There are also two procedures that are specific to the Tictactoe widget:
{ ---------------------------------tictactoe_clear------------------------------- }
PROCEDURE tictactoe_clear( ttt : pTictactoe );
VAR
i, j : Integer;
BEGIN
FOR i := 0 TO 2 DO
FOR j := 0 TO 2 DO
BEGIN
gtk_signal_handler_block_by_data(pGtkObject(ttt^.buttons[i][j]), ttt);
gtk_toggle_button_set_active(pGtkToggleButton(ttt^.buttons[i][j]),
FALSE);
gtk_signal_handler_unblock_by_data(pGtkObject(ttt^.buttons[i][j]), ttt);
END; { -- inner FOR loop -- }
END;
{ -----------------------------------tictactoe_clear------------------------------- }
tictactoe_clear() is a public procedure that resets all the buttons in the widget to the up position. Note the use
of gtk_signal_handler_block_by_data() to keep our signal handler for button toggles from being
triggered unnecessarily.
{ -----------------------------------tictactoe_toggle------------------------------ }
PROCEDURE tictactoe_toggle(widget : pGtkWidget ;
ttt: pTictactoe ); cdecl;
CONST
rwins : ARRAY[0..7,0..2] OF
Integer =
( ( 0, 0, 0 ), ( 1, 1, 1 ), ( 2, 2, 2 ),
( 0, 1, 2 ), ( 0, 1, 2 ), ( 0, 1, 2 ),
( 0, 1, 2 ), ( 0, 1, 2 ) );
cwins : ARRAY[0..7,0..2] OF
Integer =
( ( 0, 1, 2 ), ( 0, 1, 2 ), ( 0, 1, 2 ),
( 0, 0, 0 ), ( 1, 1, 1 ), ( 2, 2, 2 ),
( 0, 1, 2 ), ( 2, 1, 0 ) );
VAR
i, k : Integer;
success, found : boolean;
BEGIN
FOR k := 0 TO 7 DO
BEGIN
success := TRUE;
found := FALSE;
FOR i := 0 TO 2 DO
BEGIN
success := success AND boolean(
active(pGTKTOGGLEBUTTON(ttt^.buttons[rwins[k,i],
cwins[k,i]])^)
);
found := found OR
(ttt^.buttons[rwins[k,i], cwins[k,i]] = widget);
END; { -- FOR i:=0 TO 2 DO -- }
IF (success AND found)
THEN
BEGIN
gtk_signal_emit(pGtkObject(ttt),
tictactoe_signals[TICTACTOE_SIGNAL]);
BREAK;
END; { -- IF success AND found -- }
END; { -- FOR k:=0 TO 7 DO -- }
END;
{ ------------------------------------tictactoe_toggle--------------------------------- }
tictactoe_toggle() is the signal handler that is invoked when the user clicks on a button. It checks to see if there
are any winning combinations that involve the toggled button, and if so, emits
the tictactoe signal.
Example Program
And finally, an example program using our Tictactoe widget:

PROGRAM ttt_test;
USES glib, gdk, gtk, tictactoe;
{ -------------------------------------win--------------------------------- }
PROCEDURE win( widget : pGtkWidget ;
data : gpointer ); cdecl;
BEGIN
writeln('Yay!');
tictactoe_clear(pTicTacToe(widget));
END;
{ --------------------------------------win----------------------------------- }
{ --------------------------------Global Variables-------------------------- }
VAR
window, ttt : pGtkWidget;
{ -------------------------------Main Program------------------------------- }
BEGIN
gtk_init(@argc, @argv);
window := gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(pGtkWindow(window), 'Aspect Frame');
gtk_signal_connect(pGtkObject(window), 'destroy',
GTK_SIGNAL_FUNC(@gtk_exit),
NIL);
gtk_container_set_border_width(pGtkContainer(window), 10);
ttt := tictactoe_new ();
gtk_container_add(pGtkContainer(window), ttt);
gtk_widget_show(ttt);
gtk_signal_connect(pGtkObject(ttt), 'tictactoe',
GTK_SIGNAL_FUNC(@win), NIL);
gtk_widget_show(window);
gtk_main();
END.
{ ------------------------------------Main Program-------------------------------- }
[Previous] [Contents] [Next]
Only a small part of the many details involved in creating widgets could be described above. If you want to write your
own widgets, the best source of examples is the GTK source itself. Ask yourself some questions about the widget you want
to write: IS it a Container widget? Does it have its own window? Is it a modification of an existing widget? Then find a
similar widget, and start making changes. Good luck!
[Previous] [Contents] [Next]