[Previous] [Contents] [Next]

22. Writing Your Own Widgets

22.1 Overview

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]

22.2 The Anatomy Of A Widget

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]

22.3 Creating a Composite Widget

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:

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:
Tictactoe Example

 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]

22.4 Learning More

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]