GTK+ 2.0 Tutorial
<<< Previous | Writing Your Own Widgets | Next >>>

# 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()` function 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` function of their parent widget. But this division of labor 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 header file

Each widget class has a header file which declares the object and class structures for that widget, 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:

    #ifdef __cplusplus
    extern "C" {
    #endif /* __cplusplus */
    .
    .
    .
    #ifdef __cplusplus
    }
    #endif /* __cplusplus */

Along with the functions and structures, we declare three standard macros in our header file, `TICTACTOE(obj)`, `TICTACTOE_CLASS(klass)`, and `IS_TICTACTOE(obj)`, which cast a pointer into a pointer to the object or class structure, and check if an object is a Tictactoe widget respectively.

Here is the complete header file:

    /* GTK - The GIMP Toolkit
     * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
     *
     * This library is free software; you can redistribute it and/or
     * modify it under the terms of the GNU Library General Public
     * License as published by the Free Software Foundation; either
     * version 2 of the License, or (at your option) any later version.
     *
     * This library is distributed in the hope that it will be useful,
     * but WITHOUT ANY WARRANTY; without even the implied warranty of
     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     * Library General Public License for more details.
     *
     * You should have received a copy of the GNU Library General Public
     * License along with this library; if not, write to the
     * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
     * Boston, MA 02111-1307, USA.
     */
    #ifndef __TICTACTOE_H__
    #define __TICTACTOE_H__

    #include <gdk/gdk.h>
    #include <gtk/gtkvbox.h>

    #ifdef __cplusplus
    extern "C" {
    #endif /* __cplusplus */

    #define TICTACTOE(obj)          GTK_CHECK_CAST (obj, tictactoe_get_type (), Tictactoe)
    #define TICTACTOE_CLASS(klass)  GTK_CHECK_CLASS_CAST (klass, tictactoe_get_type (), TictactoeClass)
    #define IS_TICTACTOE(obj)       GTK_CHECK_TYPE (obj, tictactoe_get_type ())

    typedef struct _Tictactoe       Tictactoe;
    typedef struct _TictactoeClass  TictactoeClass;

    struct _Tictactoe
    {
      GtkVBox vbox;

      GtkWidget *buttons[3][3];
    };

    struct _TictactoeClass
    {
      GtkVBoxClass parent_class;

      void (* tictactoe) (Tictactoe *ttt);
    };

    GtkType        tictactoe_get_type        (void);
    GtkWidget*     tictactoe_new             (void);
    void	       tictactoe_clear           (Tictactoe *ttt);

    #ifdef __cplusplus
    }
    #endif /* __cplusplus */

    #endif /* __TICTACTOE_H__ */

## The `_get_type()` function

We now continue on to the implementation of our widget. 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.

    GtkType
    tictactoe_get_type ()
    {
      static guint ttt_type = 0;

      if (!ttt_type)
        {
          GtkTypeInfo ttt_info =
          {
    	"Tictactoe",
    	sizeof (Tictactoe),
    	sizeof (TictactoeClass),
    	(GtkClassInitFunc) tictactoe_class_init,
    	(GtkObjectInitFunc) tictactoe_init,
    	(GtkArgSetFunc) NULL,
            (GtkArgGetFunc) NULL
          };

          ttt_type = gtk_type_unique (gtk_vbox_get_type (), &ttt_info);
        }

      return ttt_type;
    }

The GtkTypeInfo structure has the following definition:

    struct _GtkTypeInfo
    {
      gchar *type_name;
      guint object_size;
      guint class_size;
      GtkClassInitFunc class_init_func;
      GtkObjectInitFunc object_init_func;
      GtkArgSetFunc arg_set_func;
      GtkArgGetFunc arg_get_func;
    };

The fields of this structure are pretty self-explanatory. We'll ignore the `arg_set_func` and `arg_get_func` 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 structure, it knows how to create objects of a particular widget 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 gint tictactoe_signals[LAST_SIGNAL] = { 0 };

    static void
    tictactoe_class_init (TictactoeClass *class)
    {
      GtkObjectClass *object_class;

      object_class = (GtkObjectClass*) class;

      tictactoe_signals[TICTACTOE_SIGNAL] = gtk_signal_new ("tictactoe",
    					 GTK_RUN_FIRST,
    					 object_class->type,
    					 GTK_SIGNAL_OFFSET (TictactoeClass, tictactoe),
    					 gtk_signal_default_marshaller, GTK_TYPE_NONE, 0);

      gtk_object_class_add_signals (object_class, tictactoe_signals, LAST_SIGNAL);

      class->tictactoe = NULL;
    }

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:

    gint gtk_signal_new( const gchar         *name,
                         GtkSignalRunType     run_type,
                         GtkType              object_type,
                         gint                 function_offset,
                         GtkSignalMarshaller  marshaller,
                         GtkType              return_val,
                         guint                nparams,
                         ...);

Creates a new signal. The parameters are:

- `name`: The name of the signal.

- `run_type`: 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 class structure 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 can use the pre-supplied marshaller function `gtk_signal_default_marshaller`.

- `return_val`: The type of the return val.

- `nparams`: 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 `GtkType` enumeration is used:

    typedef enum
    {
      GTK_TYPE_INVALID,
      GTK_TYPE_NONE,
      GTK_TYPE_CHAR,
      GTK_TYPE_BOOL,
      GTK_TYPE_INT,
      GTK_TYPE_UINT,
      GTK_TYPE_LONG,
      GTK_TYPE_ULONG,
      GTK_TYPE_FLOAT,
      GTK_TYPE_DOUBLE,
      GTK_TYPE_STRING,
      GTK_TYPE_ENUM,
      GTK_TYPE_FLAGS,
      GTK_TYPE_BOXED,
      GTK_TYPE_FOREIGN,
      GTK_TYPE_CALLBACK,
      GTK_TYPE_ARGS,

      GTK_TYPE_POINTER,

      /* it'd be great if the next two could be removed eventually */
      GTK_TYPE_SIGNAL,
      GTK_TYPE_C_CALLBACK,

      GTK_TYPE_OBJECT

    } GtkFundamentalType;

`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. (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.

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 NULL, indicating that there is no default action.

## The `_init()` function

Each widget 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)
    {
      GtkWidget *table;
      gint i,j;

      table = gtk_table_new (3, 3, TRUE);
      gtk_container_add (GTK_CONTAINER(ttt), table);
      gtk_widget_show (table);

      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(table), ttt->buttons[i][j],
    				   i, i+1, j, j+1);
    	gtk_signal_connect (GTK_OBJECT (ttt->buttons[i][j]), "toggled",
    			    GTK_SIGNAL_FUNC (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 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. 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 `gtk_signal_handler_block_by_data()` 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 ()
    {
      return GTK_WIDGET ( gtk_type_new (tictactoe_get_type ()));
    }

    void
    tictactoe_clear (Tictactoe *ttt)
    {
      int i,j;

      for (i=0;i<3;i++)
        for (j=0;j<3;j++)
          {
    	gtk_signal_handler_block_by_data (GTK_OBJECT(ttt->buttons[i][j]), ttt);
    	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ttt->buttons[i][j]),
    				     FALSE);
    	gtk_signal_handler_unblock_by_data (GTK_OBJECT(ttt->buttons[i][j]), 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)
    	{
    	  gtk_signal_emit (GTK_OBJECT (ttt),
    			   tictactoe_signals[TICTACTOE_SIGNAL]);
    	  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");

      gtk_signal_connect (GTK_OBJECT (window), "destroy",
    		      GTK_SIGNAL_FUNC (gtk_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 */
      gtk_signal_connect (GTK_OBJECT (ttt), "tictactoe",
    		      GTK_SIGNAL_FUNC (win), NULL);

      gtk_widget_show (window);

      gtk_main ();

      return 0;
    }

<<< Previous | Home | Next >>>
The Anatomy Of A Widget | Up | Creating a widget from scratch