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.
Note: the full source code for the Tictactoe example described below is in the Code Examples Appendix
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 is natural to make our parent class the Table class.
Each GObject class has a header file which declares the object and class structures for that object, along with public functions. A couple of features are worth pointing out. To prevent duplicate definitions, we wrap the entire header file in:
#ifndef __TICTACTOE_H__ #define __TICTACTOE_H__ . . . #endif /* __TICTACTOE_H__ */
And to keep C++ programs that include the header file happy, in:
#include <glib.h> G_BEGIN_DECLS . . . G_END_DECLS
Along with the functions and structures, we declare five standard macros in our header file, TICTACTOE_TYPE, TICTACTOE(obj), TICTACTOE_CLASS(klass), IS_TICTACTOE(obj), and IS_TICTACTOE_CLASS(klass), which cast a pointer into a pointer to the object or class structure, and check if an object is a Tictactoe widget respectively.
We now continue on to the implementation of our widget. A core function for every object is the function WIDGETNAME_get_type(). This function, when first called, tells Glib about the new class, and gets an ID that uniquely identifies the class. Upon subsequent calls, it just returns the ID.
GType tictactoe_get_type (void) { static GType ttt_type = 0; if (!ttt_type) { static const GTypeInfo ttt_info = { sizeof (TictactoeClass), NULL, /* base_init */ NULL, /* base_finalize */ (GClassInitFunc) tictactoe_class_init, NULL, /* class_finalize */ NULL, /* class_data */ sizeof (Tictactoe), 0, /* n_preallocs */ (GInstanceInitFunc) tictactoe_init, }; ttt_type = g_type_register_static (GTK_TYPE_TABLE, "Tictactoe", &ttt_info, 0); } return ttt_type; }
The GTypeInfo structure has the following definition:
struct _GTypeInfo { /* interface types, classed types, instantiated types */ guint16 class_size; GBaseInitFunc base_init; GBaseFinalizeFunc base_finalize; /* classed types, instantiated types */ GClassInitFunc class_init; GClassFinalizeFunc class_finalize; gconstpointer class_data; /* instantiated types */ guint16 instance_size; guint16 n_preallocs; GInstanceInitFunc instance_init; /* value handling */ const GTypeValueTable *value_table; };
The important fields of this structure are pretty self-explanatory. We'll ignore the base_init and base_finalize as well as the value_table fields here. Once Glib has a correctly filled in copy of this structure, it knows how to create objects of a particular type.
The WIDGETNAME_class_init() function initializes the fields of the widget's class structure, and sets up any signals for the class. For our Tictactoe widget it looks like:
enum { TICTACTOE_SIGNAL, LAST_SIGNAL }; static guint tictactoe_signals[LAST_SIGNAL] = { 0 }; static void tictactoe_class_init (TictactoeClass *klass) { tictactoe_signals[TICTACTOE_SIGNAL] = g_signal_new ("tictactoe", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (TictactoeClass, tictactoe), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); }
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:
guint g_signal_new( const gchar *signal_name, GType itype, GSignalFlags signal_flags, guint class_offset, GSignalAccumulator *accumulator, gpointer accu_data, GSignalCMarshaller *c_marshaller, GType return_type, guint n_params, ...);
Creates a new signal. The parameters are:
signal_name: The name of the signal.
itype: The ID of the object that this signal applies to. (It will also apply to that objects descendants.)
signal_flags: Whether the default handler runs before or after user handlers and other flags. Usually this will be one of G_SIGNAL_RUN_FIRST or G_SIGNAL_RUN_LAST, although there are other possibilities. The flag G_SIGNAL_ACTION specifies that no extra code needs to run that performs special pre or post emission adjustments. This means that the signal can also be emitted from object external code.
class_offset: The offset within the class structure of a pointer to the default handler.
accumulator: For most classes this can be set to NULL.
accu_data: User data that will be handed to the accumulator function.
c_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 can use the pre-supplied marshaller function g_cclosure_marshal_VOID__VOID.
return_type: The type of the return value.
n_params: The number of parameters of the signal handler (other than the two default ones mentioned above)
...: The types of the parameters.
When specifying types, the following standard types can be used:
G_TYPE_INVALID G_TYPE_NONE G_TYPE_INTERFACE G_TYPE_CHAR G_TYPE_UCHAR G_TYPE_BOOLEAN G_TYPE_INT G_TYPE_UINT G_TYPE_LONG G_TYPE_ULONG G_TYPE_INT64 G_TYPE_UINT64 G_TYPE_ENUM G_TYPE_FLAGS G_TYPE_FLOAT G_TYPE_DOUBLE G_TYPE_STRING G_TYPE_POINTER G_TYPE_BOXED G_TYPE_PARAM G_TYPE_OBJECT
g_signal_new() returns a unique integer identifier for the signal, that we store in the tictactoe_signals array, which we index using an enumeration. (Conventionally, the enumeration elements are the signal name, uppercased, but here there would be a conflict with the TICTACTOE() macro, so we called it TICTACTOE_SIGNAL instead.
Each class also needs a function to initialize the object structure. Usually, this function has the fairly limited role of setting the fields of the structure to default values. For composite widgets, however, this function also creates the component widgets.
static void tictactoe_init (Tictactoe *ttt) { gint i,j; gtk_table_resize (GTK_TABLE (ttt), 3, 3); gtk_table_set_homogeneous (GTK_TABLE (ttt), TRUE); for (i=0;i<3; i++) for (j=0;j<3; j++) { ttt->buttons[i][j] = gtk_toggle_button_new (); gtk_table_attach_defaults (GTK_TABLE (ttt), ttt->buttons[i][j], i, i+1, j, j+1); g_signal_connect (G_OBJECT (ttt->buttons[i][j]), "toggled", G_CALLBACK (tictactoe_toggle), ttt); gtk_widget_set_size_request (ttt->buttons[i][j], 20, 20); gtk_widget_show (ttt->buttons[i][j]); } }
There is one more function that every object (except for abstract classes 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 OBJECTNAME_new(). In some widgets, though not for the Tictactoe widgets, this function takes arguments, and does some setup based on the arguments. The other two functions are specific to the Tictactoe widget.
tictactoe_clear() is a public function that resets all the buttons in the widget to the up position. Note the use of g_signal_handlers_block_matched() to keep our signal handler for button toggles from being triggered unnecessarily.
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.
GtkWidget* tictactoe_new (void) { return GTK_WIDGET ( g_object_new (TICTACTOE_TYPE, NULL)); } void tictactoe_clear (Tictactoe *ttt) { int i,j; for (i=0;i<3;i++) for (j=0;j<3;j++) { g_signal_handlers_block_matched (G_OBJECT (ttt->buttons[i][j]), G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, ttt); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ttt->buttons[i][j]), FALSE); g_signal_handlers_unblock_matched (G_OBJECT (ttt->buttons[i][j]), G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, ttt); } } static void tictactoe_toggle (GtkWidget *widget, Tictactoe *ttt) { int i,k; static int rwins[8][3] = { { 0, 0, 0 }, { 1, 1, 1 }, { 2, 2, 2 }, { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 } }; static int cwins[8][3] = { { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 }, { 0, 0, 0 }, { 1, 1, 1 }, { 2, 2, 2 }, { 0, 1, 2 }, { 2, 1, 0 } }; int success, found; for (k=0; k<8; k++) { success = TRUE; found = FALSE; for (i=0;i<3;i++) { success = success && GTK_TOGGLE_BUTTON(ttt->buttons[rwins[k][i]][cwins[k][i]])->active; found = found || ttt->buttons[rwins[k][i]][cwins[k][i]] == widget; } if (success && found) { g_signal_emit (G_OBJECT (ttt), tictactoe_signals[TICTACTOE_SIGNAL], 0); break; } } }
And finally, an example program using our Tictactoe widget:
#include <gtk/gtk.h> #include "tictactoe.h" /* Invoked when a row, column or diagonal is completed */ void win (GtkWidget *widget, gpointer data) { g_print ("Yay!\n"); tictactoe_clear (TICTACTOE (widget)); } int main (int argc, char *argv[]) { GtkWidget *window; GtkWidget *ttt; gtk_init (&argc, &argv); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "Aspect Frame"); g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (exit), NULL); gtk_container_set_border_width (GTK_CONTAINER (window), 10); /* Create a new Tictactoe widget */ ttt = tictactoe_new (); gtk_container_add (GTK_CONTAINER (window), ttt); gtk_widget_show (ttt); /* And attach to its "tictactoe" signal */ g_signal_connect (G_OBJECT (ttt), "tictactoe", G_CALLBACK (win), NULL); gtk_widget_show (window); gtk_main (); return 0; }