Custom container

Size allocation is not a very difficult procedure in GTK. Widgets report their preferred size from the bottom of the widget tree up, with each container figuring out the preferred sizes of its children and then calculating its own preferred size using that information. This can be done simply by using gtk_widget_get_preferred_height() and gtk_widget_get_preferred_width(), but you can also have the height depend on the width or the width depend on the height. Once the total preferred size is known, the available space is allocated from the top down: each container divides up the space it receives among its children by calling their gtk_widget_size_allocate() methods. What's more, you can customize the process for any widget by overriding these methods.

GTK 2

This is rather different from the way things worked in GTK 2, and there are probably plenty of GTK 2 instructions still floating around on the Internet.

In most cases, such as writing a composite widget or adding functionality to an existing container, you can get away with subclassing a container like GtkGrid. That way, you don't have to deal with size negotiation and other container stuff directly, since it's already implemented in your parent class. But what if you want a container that arranges its children differently than all the existing GTK containers? Then you'll have to write a new container with its own size request and allocation algorithms.

It wasn't easy to think of a realistic-yet-simple example, since anytime you really have to do this, you're probably attempting something complicated. Therefore, we will write a fairly useless container which arranges all its children in a square, inspired by '80's video clips with cheesy split screen effects, like Heat of the Moment. The container will find an integer n so that

n2 V

where V is the number of visible children of the container. The container will then divide its space into an n×n grid, which it will fill from left to right and top to bottom.

Header file

We will create a subclass of GtkContainer called PSquare. The header file, psquare.h, is a textbook case if there ever was one, so I won't reprint it here. It exports only two functions, p_square_get_type() and p_square_new(), since all of the interesting stuff happens in overridden GtkContainer functions.

Class boilerplate

We will briefly go over the required GObject stuff in the psquare.c file, since it's good to know about the G_DEFINE_TYPE macros and the g_type_class_add_private() mechanism. Not all online resources mention them.

G_DEFINE_TYPE() is a handy macro that saves you a lot of typing. It writes your p_square_get_type() function for you, which you then export in your header file; it also declares a static p_square_parent_class variable, a static p_square_class_init() function, and a static p_square_init() function.

G_DEFINE_TYPE(PSquare, p_square, GTK_TYPE_CONTAINER);

The parent_class variable is there to avoid a costly call to g_type_class_peek_parent() every time you need to chain up to a method in the parent class (often in finalize). You need to write the class and instance initialization functions yourself.

static void
p_square_class_init(PSquareClass *klass)
{
	/* Override GtkWidget methods */
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
	widget_class->get_preferred_width = p_square_get_preferred_width;
	widget_class->get_preferred_height = p_square_get_preferred_height;
	widget_class->size_allocate = p_square_size_allocate;

	/* Override GtkContainer methods */
	GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass);
	container_class->child_type = p_square_child_type;
	container_class->add = p_square_add;
	container_class->remove = p_square_remove;
	container_class->forall = p_square_forall;

	/* Add private indirection member */
	g_type_class_add_private(klass, sizeof(PSquarePrivate));
}

The class initialization function p_square_class_init overrides several methods in ancestor classes. If you are not familiar with the GObject way of doing this, then take a good look now. Our test program will not use the forall and child_type methods, but a peek at gtkcontainer.c will reveal that they are set to NULL in the GtkContainer class initializer; in other words, they are pure virtual functions, and are required to be overridden.

To find out what methods there are to override in the parent class, you have to look at the source code of GtkContainer. There isn't really a good way to tell from the API documentation, which is unfortunate.

The other thing the class initializer does is register PSquare's private indirection member. This is probably the most effective way of hiding your implementation details when you write a class. This is achieved by defining a private member structure and telling GObject to allocate space for it in each instance of that class, using g_type_class_add_private(). That way, each object has private data stored in it, but you can only access that data if the composition of your private structure is exposed. The private structure is defined in the following code.

#define P_SQUARE_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE((obj), \
	P_SQUARE_TYPE, PSquarePrivate))

typedef struct _PSquarePrivate PSquarePrivate;

struct _PSquarePrivate
{
	GList *children;
};

You might prefer to put these lines in a file called something like psquare-private.h. That way, any implementation file that includes it will be able to access the private data, kind of like the friend directive in C++. It also comes in handy if the implementation of your class is very big and you want to split it over several files.

The macro P_SQUARE_PRIVATE() gives us the private data of a PSquare. Our only private member is the list of our container's children. We need to keep track of this list ourselves, since GtkContainer won't do it for us.

The instance initialization function p_square_init() initializes the public and private members of our object:

static void
p_square_init(PSquare *square)
{
	gtk_widget_set_has_window(GTK_WIDGET(square), FALSE);

	/* Initialize private members */
	PSquarePrivate *priv = P_SQUARE_PRIVATE(square);
	priv->children = NULL;
}

The gtk_widget_set_has_window() call means that Psquare doesn't have a GdkWindow associated with it; we don't do any drawing ourselves, we just arrange the container's children. The private member children is initialized to NULL, which is an empty GList, as you should know.

You should also know that it is customary to create a widget with a p_square_new() function, which returns a new instance of the class, cast to a GtkWidget pointer. Here it is:

GtkWidget *
p_square_new()
{
	return GTK_WIDGET(g_object_new(p_square_get_type(), NULL));
}

Overriding GtkContainer methods

Now for the overridden ancestor methods. The GtkContainer ones are fairly trivial, so we'll do them first. The child_type method, specifying what types of children can be added to the container, simply returns GTK_TYPE_WIDGET. The forall pointer in the GtkContainerClass structure doesn't correspond exactly to a GtkContainer method, but gtk_container_forall() and gtk_container_foreach() are use it in their implementations. One iterates over all children including "internal" ones (such as the GtkButtons in a dialog that aren't meant to be manipulated by your program), the other iterates over the container's children but skips internal ones. The class forall function takes a flag telling whether to include internal children or not. PSquare doesn't have any internal children, so we just ignore that.

static void
p_square_forall(GtkContainer *container, gboolean include_internals,
	GtkCallback callback, gpointer callback_data)
{
	PSquarePrivate *priv = P_SQUARE_PRIVATE(container);
	g_list_foreach(priv->children, (GFunc)callback, callback_data);
}

The next things to implement are the add and remove methods. Note that you don't have to have them if, for example, your container has a different mechanism for adding widgets; the default GtkContainer implementations do nothing, and print a warning message saying that the method is not implemented.

static void
p_square_add(GtkContainer *container, GtkWidget *widget)
{
	g_return_if_fail(container || P_IS_SQUARE(container));
	g_return_if_fail(widget || GTK_IS_WIDGET(widget));
	g_return_if_fail(gtk_widget_get_parent(widget) == NULL);

	PSquarePrivate *priv = P_SQUARE_PRIVATE(container);

	/* Add the child to our list of children. 
	 * All the real work is done in gtk_widget_set_parent(). */
	priv->children = g_list_append(priv->children, widget);
	gtk_widget_set_parent(widget, GTK_WIDGET(container));

	/* Queue redraw */
	if(gtk_widget_get_visible(widget))
		gtk_widget_queue_resize(GTK_WIDGET(container));
}

It may surprise you that this function is not longer. However, as the comment says, all the real work is done in gtk_widget_set_parent(): sinking the child's floating reference, redrawing the child, emitting the appropriate signals, etc. We still have to redraw the container ourselves. The corresponding method to remove a child is much the same, with all the real work being done in gtk_widget_unparent().

static void
p_square_remove(GtkContainer *container, GtkWidget *widget)
{
	g_return_if_fail(container || P_IS_SQUARE(container));
	g_return_if_fail(widget || GTK_IS_WIDGET(widget));

	PSquarePrivate *priv = P_SQUARE_PRIVATE(container);

	/* Remove the child from our list of children. 
	 * Again, all the real work is done in gtk_widget_unparent(). */
	GList *link = g_list_find(priv->children, widget);
	if(link) {
		gboolean was_visible = gtk_widget_get_visible(widget);
		gtk_widget_unparent(widget);

		priv->children = g_list_delete_link(priv->children, link);

		/* Queue redraw */
		if(was_visible)
			gtk_widget_queue_resize(GTK_WIDGET(container));
	}
}

Note that we don't have to write p_square_destroy() or p_square_finalize() methods to unref the child widgets, since the GtkContainer code already does that when the container is destroyed.

Size negotiation

Now that we have all that out of the way, we can get to the meat of this section. We will determine how much width our container needs by adding up the width of each column. The width of a column will be equal to the width of the widest widget in that column. The container height will be determined likewise. We will also respect the border_width property of GtkContainer.

There are two functions with which the container reports its size: one for the width, and one for the height. They are passed pointers to two integers, which they must fill with the minimum size (the absolute minimum they need to function) and the natural size (at which they can function comfortably.)

static void p_square_get_preferred_width(GtkWidget *widget,
	int *minimal, int *natural);
static void p_square_get_preferred_height(GtkWidget *widget,
	int *minimal, int *natural);

In our case, these two functions will work nearly the same, so we will write them both in terms of a third function, get_size(). This function gets an extra GtkOrientation parameter, called direction: GTK_ORIENTATION_HORIZONTAL if we are requesting the width, GTK_ORIENTATION_VERTICAL if we are requesting the height.

static void get_size(PSquare *self, GtkOrientation direction,
    int *minimal, int *natural);

We start by setting the minimal and natural parameters to their minimum value, twice the border width. Then we compute the size of the side of our square, or in other words, the number of rows and columns. We call it n_groups because it doesn't matter whether we are talking about rows or columns here. If the number of groups is zero, our work is done.

Then, we iterate over the children, asking them for their sizes, in the function get_group_sizes(). The GtkRequestedSize structure stores both the minimum and the natural size requests. Once we have got this information, we add them all up to find the total size we want to request.

static void
get_size(PSquare *self, GtkOrientation direction, int *minimal, int *natural)
{
	/* Start with the container's border width */
	unsigned border_width =
		gtk_container_get_border_width(GTK_CONTAINER(self));
	*minimal = *natural = border_width * 2;

	/* Find out how many children there are */
	unsigned n_groups = get_n_columns_and_rows(self);
	if(n_groups == 0)
		return;

	/* Find out how much space they want */
	GtkRequestedSize *sizes = get_group_sizes(self, direction, n_groups);

	/* Add the widths and pass that as the container's width */
	unsigned count;
	for(count = 0; count < n_groups; count++) {
		*minimal += sizes[count].minimum_size;
		*natural += sizes[count].natural_size;
	}

	g_free(sizes);
}

The function get_n_columns_and_rows() is shown below. Note that the container might still have children, but the number of groups will be zero if they are all invisible; we don't layout unshown widgets.

unsigned
get_n_columns_and_rows(PSquare *self)
{
	PSquarePrivate *priv = P_SQUARE_PRIVATE(self);

	/* Count the visible children */
	unsigned n_visible_children = 0;
	g_list_foreach(priv->children, (GFunc)count_visible_children,
		&n_visible_children);
	if(n_visible_children == 0)
		return 0;

	/* Calculate the number of columns */
	return (unsigned)ceil(sqrt((double)n_visible_children));
}
/* Convenience function for counting the number of visible
 * children, for use with g_list_foreach() */
static void
count_visible_children(GtkWidget *widget, unsigned *n_visible_children)
{
	if(gtk_widget_get_visible(widget))
		(*n_visible_children)++;
}

Next we describe get_group_sizes(), since that is where most of the real work of finding the preferred size is done.

First, we allocate arrays to store all the group sizes (that is, the column widths or row heights.) When we get all the preferred sizes of the container's children, we will store a running maximum for each group in this array.

GtkRequestedSize *sizes = g_new0(GtkRequestedSize, n_groups);

We then iterate over the children again, asking them for their preferred sizes. If we are in width mode, we ask for the width, or the height in height mode. We also set the variable group_num depending on width or height mode; this is the row or column number the current child widget is in.

Then, if the child's size is larger than the size we have stored for the group we're in, we replace the stored size by the child's size. We return this sizes array.

unsigned count = 0;
GList *iter;
for(iter = priv->children; iter; iter = g_list_next(iter)) {
	if(!gtk_widget_get_visible(iter->data))
		continue;

	int child_minimal, child_natural;
	unsigned group_num;
	if(direction == GTK_ORIENTATION_HORIZONTAL) {
		gtk_widget_get_preferred_width(iter->data,
			&child_minimal, &child_natural);
		group_num = count % n_groups;
	} else {
		gtk_widget_get_preferred_height(iter->data,
			&child_minimal, &child_natural);
		group_num = count / n_groups;
	}

	sizes[group_num].minimum_size =
		MAX(child_minimal, sizes[group_num].minimum_size);
	sizes[group_num].natural_size =
		MAX(child_natural, sizes[group_num].natural_size);

	count++;
}

Size allocation

The size_allocate method is similar. It receives a pointer to a GtkAllocation structure, which contains the size that the widget will have to deal with.

static void p_square_size_allocate(GtkWidget *widget,
	GtkAllocation *allocation);

The first thing it must do is store the allocation in the widget structure.

gtk_widget_set_allocation(widget, allocation);

The next section is very similar to the code in the size_request method. First we obtain the number of columns and rows:

unsigned n_columns, n_rows;
n_columns = n_rows = get_n_columns_and_rows(P_SQUARE(widget));
if(n_columns == 0)
	return;

They are the same, but I'm using two separate n_columns and n_rows variables for clarity in the code. Again, if there are no visible widgets in the container, our work is now done.

Now we have to distribute the container's allocated space to the children. Our policy will be to divide any extra width equally among the columns, and any extra height equally among the rows. If there is too little space, it will be taken equally from each column or row. First we calculate the space surplus or deficit per column and row. We do this with two variables, extra_width and extra_height. We start them off equal to the total width or height, then we will subtract all the space that we need. The first thing is to subtract the container's border width.

unsigned border_width =
	gtk_container_get_border_width(GTK_CONTAINER(widget));
int extra_width = allocation->width - 2 * border_width;
int extra_height = allocation->height - 2 * border_width;

Then we get all the column widths with the get_group_sizes() function described above. We calculate the actual space to allocate to each column, by adding the extra space (which can be negative) to each column. This happens in the distribute_extra_space() function, which we will get to in a moment.

After we have figured out the width of each column, we can calculate the height of each row. This follows the same procedure, except that instead of get_group_sizes() we write a new function called get_group_sizes_for_sizes(), which gets the appropriate height for a certain width, or vice versa. We will skip showing that function here — it's the same as get_group_sizes() except that it calls gtk_widget_get_preferred_height_for_width() and ..._width_for_height(), instead of gtk_widget_get_preferred_height() and ..._width(), respectively. If you want to see it, look in the psquare.c file.

/* Follow the same procedure as in the size request to get 
 * the ideal sizes of each column */
GtkRequestedSize *widths = get_group_sizes(P_SQUARE(widget),
	GTK_ORIENTATION_HORIZONTAL, n_columns);

/* Distribute the extra space per column (can be negative) */
unsigned count;
for(count = 0; count < n_columns; count++)
	extra_width -= widths[count].minimum_size;
distribute_extra_space(P_SQUARE(widget), widths, extra_width, n_columns);

/* Follow the same procedure for height,
 * now that we know the width */
GtkRequestedSize *heights = get_group_sizes_for_sizes(P_SQUARE(widget),
	GTK_ORIENTATION_VERTICAL, widths, n_rows);

/* Distribute the extra space per row (can be negative) */
for(count = 0; count < n_rows; count++)
	extra_height -= heights[count].minimum_size;
distribute_extra_space(P_SQUARE(widget), heights, extra_height, n_rows);

Here is the distribute_extra_space() function, which adds the extra space (which can be negative) to each group (column or row). GTK provides a convenience function to distribute any extra space (which can't be negative) among a group of widgets so that they each get as much as their natural size, so we do this first if we have space left over. Then if there's still space left, or if we had a shortage in the first place, we distribute the surplus or shortage equally among the groups. We have to watch out that we don't allocate less than zero space to any widget, so if any space is less than zero, we borrow pixels one by one from any other nonzero columns until the space is zero.

static void
distribute_extra_space(PSquare *self, GtkRequestedSize *sizes,
	int extra_space, unsigned n_groups)
{
	if(extra_space > 0) {
		extra_space = gtk_distribute_natural_allocation(extra_space,
			n_groups, sizes);
	}

	unsigned count;
	int extra_per_group = extra_space / (int)n_groups;

	for(count = 0; count < n_groups; count++) {
		sizes[count].minimum_size += extra_per_group;
		/* If this results in a negative width, redistribute
		 * pixels from other nonzero-width columns to this one */
		if(sizes[count].minimum_size < 0) {
			unsigned count2;
			for(count2 = (count + 1) % n_groups;
				sizes[count].minimum_size < 0;
				count2++, count2 %= n_groups)
			{
				if(count2 == count || sizes[count2].minimum_size < 0)
					continue;
				sizes[count2].minimum_size--;
				sizes[count].minimum_size++;
			}
		}
	}
}

Back to the size allocation method. We will keep track of the (x,y) coordinates at which we will place the top left corner of our next child widget. Note that the x and y members of the GtkAllocation structure contain (I think) screen coordinates, not offsets from the top left of the containing widget, so you always need to start from those values.

/* Start positioning the items at the container's origin,
 * less the border width */
int x = allocation->x + border_width;
int y = allocation->y + border_width;

Then we iterate once more over the visible children. For each child, we allocate a GtkAllocation structure on the stack, fill it with the correct (x,y) coordinates and size, and notify the child by calling gtk_widget_size_allocate(). Then we update our x and y coordinates appropriately for the next child, moving down a row and returning to the left edge of the container when we reach the end of a row.

count = 0;
GList *iter;
for(iter = priv->children; iter; iter = g_list_next(iter)) {
	if(!gtk_widget_get_visible(iter->data))
		continue;

	/* Give the child its allocation */
	GtkAllocation child_allocation;
	child_allocation.x = x;
	child_allocation.y = y;
	child_allocation.width = widths[count % n_columns].minimum_size;
	child_allocation.height = heights[count / n_columns].minimum_size;
	gtk_widget_size_allocate(iter->data, &child_allocation);

	/* Advance the x coordinate */
	x += child_allocation.width;
	count++;
	/* If we've moved to the next row, return the x coordinate 
	 * to the left, and advance the y coordinate */
	if(count % n_columns == 0) {
		x = allocation->x + border_width;
		y += child_allocation.height;
	}
}

Example application

We will create a simple example application to showcase our new container. You will find test-psquare.c in the code distribution, along with a simple Makefile. We create a toplevel window and require it to be a specific size (although you can resize it to be larger while the program is running), using gtk_widget_set_size_request(). Since the window will stay the same size, then the widgets in the container will have more than enough space if there are only a few, but as more and more widgets are added, they will have to start squeezing themselves into spaces smaller than their size requests.

We will add a toolbar to the window with add and remove buttons, to add a random widget to the container (using gtk_container_add()) or remove the last one added (with gtk_container_remove()).

If you compile the example application and run it, try adding lots of widgets to the container. You'll notice that a GtkEntry requests a lot of width, and so columns with GtkEntrys in them tend to crowd out other columns when horizontal space gets tight. You shouldn't get any warnings about negative size allocations, however.

Exercises

  • Change p_square_size_allocate() so that it allocates any extra space according to the proportion of the total space that the column or row takes up. Likewise, if there is not enough space, then narrow columns and short rows lose less space, and wide columns and tall rows lose more.

  • Change PSquare so that instead of filling its cells from left to right and top to bottom, it fills them in a clockwise spiral starting at the top left.

  • Implement child properties for PSquare children: for example, fill-horizontal and fill-vertical properties, which control whether the child widget fills all of its allocated space or sticks to its own request; and a property that controls how the child aligns itself if it does not fill the entire cell. You could implement this last one as two properties, for horizontal and vertical alignment, or you could use one value of type GtkAnchorType.