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.

Note: the full source code for the Tictactoe example described below is in the Code Examples Appendix

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 is natural to make our parent class the Table class.

The header file

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.

The _get_type() function

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 _class_init() function

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:

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.

The _init() function

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]);
      }
}

And the rest...

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;
}