Containers And Widget Layout

There are two kinds of container widgets in GTK+. All of them are subclasses of the abstract GtkContainer. The first type of container widget always descends from GtkBin, another abstract base class. Descendents of GtkBin can contain only one child widget; these containers add some kind of functionality to the child. For example, GtkButton is a GtkBin which makes the child into a clickable button. GtkFrame is a GtkBin which draws a relieved border around the child. GtkWindow allows the child to appear in a toplevel window.

The second type of container widget often has GtkContainer as its immediate parent. These containers can have more than one child, and their purpose is to manage layout. "Manage layout" means that these containers assign sizes and positions to the widgets they contain. For example, GtkVBox arranges its children in a vertical stack. GtkFixed allows you to position children at arbitrary coordinates. GtkPacker gives you Tk-style layout management.

This chapter is about the second kind of container. To produce the layout you want without hard-coding any sizes, you'll need to understand how to use these. The goal is to avoid making assumptions about window size, screen size, widget appearance, fonts, and so on. Your application should automatically adapt if these factors change.

Size Allocation

To understand layout containers, you first have to understand how GTK+ widgets negotiate their size. It's quite simple really; there are only two concepts, requisition and allocation. These correspond to the two phases of layout.

Requisition

A widget's requisition consists of a width and a height---the size the widget would like to be. This is represented by a GtkRequisition struct:


typedef struct _GtkRequisition    GtkRequisition;

struct _GtkRequisition
{
  gint16 width;
  gint16 height;
};

Different widgets choose what size to request in different ways. GtkLabel, for example, requests enough size to display all the text in the label. Most container widgets base their size request on the size requests of their children. For example, if you place several buttons in a box, the box will ask to be large enough to hold all the buttons.

The first phase of layout starts with a toplevel widget such as GtkWindow. Since it's a container, GtkWindow asks its child widget for a size request; that child might ask its own children; and so on recursively. When all child widgets have been queried, GtkWindow will finally get a GtkRequisition back from its child. Depending on how it was configured, GtkWindow may or may not be able to expand to accomodate the size request.

Allocation

Phase two of layout begins at this point. GtkWindow makes a decision about how much space is actually available for its child, and communicates its decision to the child. This is known as the child's allocation, represented by the following struct:


typedef struct _GtkAllocation     GtkAllocation;

struct _GtkAllocation
{
  gint16 x;
  gint16 y;
  guint16 width;
  guint16 height;
};

The width and height elements are identical to GtkRequisition; they represent the size of the widget. A GtkAllocation also includes the coordinates of the child with respect to its parent. GtkAllocations are assigned to children by their parent container.

Widgets are required to honor the GtkAllocation given to them. GtkRequisition is only a request; widgets must be able to cope with any size.

Given the layout process, it's easy to see what role containers play. Their job is to assemble each child's requisition into a single requisition to be passed up the widget tree; then to divide the allocation they receive between their children. Exactly how this happens depends on the particular container.

GtkBox

A GtkBox manages a row (GtkHBox) or column (GtkVBox) of widgets. For GtkHBox, all the widgets are assigned the same height; the box's job is to distribute the available width between them. GtkHBox optionally uses some of the available width to leave gaps (called "spacing") between widgets. GtkVBox is identical, but in the opposite direction (i.e., it distributes available height rather than width). GtkBox is an abstract base class; GtkVBox and GtkHBox can be used almost entirely via its interface. Boxes are the most useful container widget.

To create a GtkBox, you use one of the constructors shown in Figure 2 and Figure 3. The box constructor functions take two parameters. If TRUE, homogeneous means that all children of the box will be allocated the same amount of space. spacing specifies the amount of space between each child. There are functions to change spacing and toggle homogeneity after the box is created.

#include <gtk/gtkhbox.h>

GtkWidget* gtk_hbox_new(gboolean homogeneous, gint spacing);

Figure 2. GtkHBox Constructor

#include <gtk/gtkvbox.h>

GtkWidget* gtk_vbox_new(gboolean homogeneous, gint spacing);

Figure 3. GtkVBox Constructor

There are two basic functions to add a child to a GtkBox; they are shown in Figure 4.

#include <gtk/gtkbox.h>

void gtk_box_pack_start(GtkBox* box, GtkWidget* child, gboolean expand, gboolean fill, gint padding);

void gtk_box_pack_end(GtkBox* box, GtkWidget* child, gboolean expand, gboolean fill, gint padding);

Figure 4. Packing GtkBox

A box can contain two sets of widgets. The first set is packed at the "start" (top or left) of the box; the second at the "end" (bottom or right). If you pack three widgets into the start of a box, the first widget you pack appears topmost or leftmost; the second follows the first; and the third appears closest to the center of the box. If you then pack three widgets into the end of the same box, the first appears bottommost or rightmost; the second follows it; and the third appears closest to the center. With all six widgets packed, the order from top/left to bottom/right is: 1, 2, 3, 3, 2, 1. Figure 5 shows this for GtkVBox. Order of packing is only important within each end of the box; i.e., we could have alternated packing start and packing end, with the same results.

Figure 5. Buttons packed into a GtkVBox

GtkBox Layout Details

Packing is affected by three parameters, which are the same for both start and end packing; the meaning of these parameters is somewhat complicated, because they interact with the homogeneous setting of the box and with each other.

Here's how a GtkBox computes its size request for the "interesting" direction (width for GtkHBox, height for GtkVBox):

  1. The total requested size of each child is considered to be the child's size request, plus two times the padding value used to pack the child. A child's padding is the amount of blank space on either side of it. In short, Child Size = (Child Widget's Size Request) + 2*(Child Padding).

  2. If the box is homogeneous, the base size request for the entire box is equal to the size (request + padding) of the largest child, times the number of children. In a homogeneous box, all children are as large as the largest child.

  3. If the box is not homogeneous, the base size request for the entire box is the sum of the size (request + padding) of each child.

  4. The box-wide spacing setting determines how much blank space to leave between children; so this value is multiplied by the number of chilren minus one, and added to the base size request. Note that spacing does not belong to a child; it is blank space between children and is unaffected by the expand and fill parameters. Padding, on the other hand, is the space around each child and is affected by the child's packing parameters.

  5. All containers have a "border width" setting; two times the border width is added to the request, representing a border on either side. Thus, the total size requested by a GtkBox is: (Sum of Child Sizes) + Spacing*(Number of Children - 1) + 2*(Border Width).

After computing its size request and delivering it to its parent container, GtkBox will receive its size allocation and distribute it among its children as follows:

  1. Enough space for the border width and inter-child spacing is subtracted from the allocation; the remainder is the available space for children themselves. This space is divided into two chunks: the amount actually requested by the children (child requisitions and padding), and the "extra." Extra = (Allocation Size) - (Sum of Child Sizes).

  2. If the box is not homogeneous, the "extra" space is divided among those children with the expand parameter set to TRUE. These children can expand to fit available space. If no child can expand, the extra is used to add more space in the center of the box, between the start-packed widgets and the end-packed widgets.

  3. If the box is homogeneous, the extra is distributed according to need; those children who requested more space get less extra, so that everyone ends up with the same amount of space. The expand parameter is ignored for homogeneous boxes---extra is distributed to all children, not just the expandable ones.

  4. When a child gets some extra space, there are two possibilities. More padding can be added around the child, or the child widget itself can be expanded. The fill parameter determines which will happen. If TRUE, the child widget expands to fill the space---that is, the entire space becomes the child's allocation; if fill is FALSE, the child's padding is increased to fill the space, and the child is allocated only the space it requested. Note that fill has no effect if expand is set to FALSE and the box is not homogeneous, because the child will never receive any extra space to fill.

Whew! Who wants to think about all that? Fortunately, there are some common patterns of usage, so you don't need to solve a multivariate equation to figure out how to use the widget. The authors of the GTK+ Tutorial boil things down nicely to five cases that occur in practice; we'll follow in their footsteps here.

Non-Homogeneous Box Packing Patterns

There are three interesting ways to pack a non-homogeneous box. First, you can pack all the widgets into the end of the box, with their natural size. This means setting the expand parameter to FALSE:


  gtk_box_pack_start(GTK_BOX(box),
                     child, 
                     FALSE, FALSE, 0);

The result is shown in Figure 6. The expand parameter is the only one that matters in this case; no children are receiving extra space, so they wouldn't be able to fill it even if fill were TRUE.

Figure 6. Non-homogeneous, with expand = FALSE

Second, you can spread widgets throughout the box, letting them keep their natural size as in Figure 7; this means setting the expand parameter to TRUE:


  gtk_box_pack_start(GTK_BOX(box),
                     child, 
                     TRUE, FALSE, 0);

Figure 7. Non-homogeneous, with expand = TRUE and fill = FALSE

Finally, you can fill the box with widgets (letting larger children have more space) by setting the fill parameter to TRUE as well:


  gtk_box_pack_start(GTK_BOX(box),
                     child, 
                     TRUE, TRUE, 0);

This configuration is shown in Figure 8

Figure 8. Non-homogeneous, with expand = TRUE and fill = TRUE

Homogeneous Box Packing Patterns

There are only two interesting ways to pack a homogeneous box. Recall that the expand parameter is irrelevant for homogeneous boxes; so the two cases correspond to the fill parameter's setting.

If fill is FALSE, you get Figure 9. Notice that the box is logically divided into three equal parts, but only the largest child widget occupies its entire space. The others are padded to fill their third of the area. If fill is TRUE, you get Figure 10; all the widgets are the same size.

Figure 9. Homogeneous, with fill = FALSE

Figure 10. Homogeneous, with fill = TRUE

Box Packing Summary

Figure Figure 11 shows all five box-packing techniques together. (They are packed into a homogeneous GtkVBox with fill set to TRUE and an interchild spacing of two pixels.) This should give you a sense of their relative effects. Keep in mind that you can also tweak the padding and spacing parameters, to increase or decrease the amount of blank space between widgets. However, you can easily create an ugly layout by using inconsistent spacing---it's a good idea to try to keep widgets "lined up" and consistently spaced.

Figure 11. All Five Ways to Pack a Box

A final point: notice that the expand and fill parameters are only relevant when a box's size allocation is larger than its size request. That is, these parameters determine how extra space is distributed. Typically, extra space appears when a user resizes a window to make it larger than its default size. Thus, you should always try resizing your windows to be sure your boxes are packed correctly.

GtkTable

The second most common layout container is GtkTable. GtkTable divides a region into cells; you can assign each child widget to a rectangle made up of one or more cells. You can think of GtkTable as a sheet of graph paper (with more flexibility---the grid lines do not have to be equidistant, though they can be).

GtkTable comes with the usual constructor, and some functions to attach children to it; these are shown in Figure 12. When creating a table, you specify the number of cells you plan to use; this is purely for efficiency. The table will automatically grow if you place children in cells outside its current area. Like boxes, tables can be homogeneous or not.

#include <gtk/gtktable.h>

GtkWidget* gtk_table_new(guint rows, guint columns, gboolean homogeneous);

GtkWidget* gtk_table_attach(GtkTable* table, GtkWidget* child, guint left_side, guint right_side, guint top_side, guint bottom_side, GtkAttachOptions xoptions, GtkAttachOptions yoptions, guint xpadding, guint ypadding);

Figure 12. GtkTable

The first two arguments to gtk_table_attach() are the table and the child to place in the table. The next four specify which grid lines should form the bounding box of the child. Grid lines are numbered from the top left (northwest) corner of the table, starting with 0; so a 2 by 3 table will have vertical lines 0, 1, 2 and horizontal lines 0,1,2,3. The last two arguments are the amount of padding to put on the left-right sides of the child (xpadding) and the top-bottom (ypadding). This is analagous to padding in boxes.

The GtkAttachOptions arguments require some explanation. Here's a summary of possible values. The values are bitmasks, so more than one can be specified by or-ing them together.

It's possible to set spacing between rows and columns, in addition to padding around particular children; the terms "spacing" and "padding" mean the same thing with respect to tables and boxes. See gtk/gtktable.h for a complete list of available GtkTable functions.

GtkTable Example

The following code creates a table with four cells and three children; one child covers two cells. The children are packed using different parameters:


  GtkWidget* window;
  GtkWidget* button;
  GtkWidget* container;

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  container = gtk_table_new(2, 2, FALSE);

  gtk_container_add(GTK_CONTAINER(window), container);

  gtk_window_set_title(GTK_WINDOW(window), "Table Attaching");

  gtk_container_set_border_width(GTK_CONTAINER(container), 10);

  /* This would be a bad idea in real code; but it lets us 
   * experiment with window resizing. 
   */
  gtk_window_set_policy(GTK_WINDOW(window), TRUE, TRUE, TRUE);

  gtk_signal_connect(GTK_OBJECT(window),
                     "delete_event",
                     GTK_SIGNAL_FUNC(delete_event_cb),
                     NULL);

  button = gtk_button_new_with_label("1. Doesn't shrink\nor expand");
  gtk_table_attach(GTK_TABLE(container),
                   button,
                   0, 1,
                   0, 1,
                   GTK_FILL,
                   GTK_FILL,
                   0, 
                   0);

  button = gtk_button_new_with_label("2. Expands and shrinks\nvertically");
  gtk_table_attach(GTK_TABLE(container),
                   button,
                   0, 1,
                   1, 2,
                   GTK_FILL,
                   GTK_FILL | GTK_EXPAND | GTK_SHRINK,
                   0, 
                   0);

  button = gtk_button_new_with_label("3. Expands and shrinks\nin both directions");
  gtk_table_attach(GTK_TABLE(container),
                   button,
                   1, 2,
                   0, 2,
                   GTK_FILL | GTK_EXPAND | GTK_SHRINK,
                   GTK_FILL | GTK_EXPAND | GTK_SHRINK,
                   0, 
                   0);

It's instructive to observe the resulting table as the window is resized. First, a quick summary of how the children are attached:

  1. The first child will always receive its requested size; it neither expands nor shrinks.

  2. The second child can expand and shrink only in the Y direction.

  3. The third child can expand and shrink in either direction.

The window's natural size is shown in Figure 13; notice that some cells are given more space than the widgets inside them requested because table cells have to remain aligned. (Recall that a button with a label will request only enough space to display the entire label.) The GTK_FILL flag causes GtkTable to allocate extra space to the widgets themselves, instead of leaving blank padding around them.

Figure 13. GtkTable before resizing

Now imagine the user expands the window vertically; notice that extra space is given to the widgets with GTK_EXPAND turned on in the Y direction---namely widgets two and three---while the widget in the top-left corner remains unchanged. Figure 14 shows this state of affairs.

Figure 14. GtkTable after expanding the window vertically

Next, imagine the user expanding the window horizontally; only child widget number three can expand horizontally. Figure 15 shows this.

Figure 15. GtkTable after expanding the window horizontally

Figure 16 shows the result if the user shrinks the table vertically, so that there isn't enough vertical space to give all the widgets their size requests. Child number two gets shortchanged, while child number one gets all the vertical space it needs.

Figure 16. GtkTable after shrinking the window vertically

Finally, Figure 17 shows the result if the user shrinks the table horizontally. Child number three gets the short end of the stick in this situation.

Figure 17. GtkTable after shrinking the window horizontally

It's not a bad idea to try resizing your window like this whenever you're designing a layout, just to be sure something sane happens. The definition of "sane" varies with the exact widgets you've placed in the layout.

Using gtk_table_attach_defaults()

Since gtk_table_attach() is somewhat cumbersome, there's a simpler version called gtk_table_attach_defaults(), shown in Figure 18. This version attaches the child with the options GTK_EXPAND and GTK_FILL, and no padding.

It's tempting to use gtk_table_attach_defaults() all the time to save typing, but really you shouldn't; in fact, it's probably fair to say that it's rarely used. The function is only useful if the defaults happen to be exactly the settings you want. Most of the time, you need to carefully tweak your table attachment parameters to get really nice behavior when your window is resized. Always try resizing your window to be sure you've designed your layout well.

#include <gtk/gtktable.h>

GtkWidget* gtk_table_attach_defaults(GtkTable* table, GtkWidget* child, guint left_side, guint right_side, guint top_side, guint bottom_side);

Figure 18. Attaching with Defaults

Other Layout Widgets

Boxes and tables are the most commonly-used layout widgets by far. However, there are a few others for special situations.

Manually Affecting Layout

It's possible to manually override GTK+'s geometry management. This is a bad idea 95% of the time, because GTK+'s geometry is essentially the user's preferred geometry, determined by the theme, and resizing toplevel windows. If you find yourself wanting to do things manually, it's probably because you're using the wrong layout container, or you really should be writing a custom container widget.

You can force a size or position on a widget with the functions shown in Figure 19. However, it is rarely a good idea to use them. In particular, gtk_widget_set_usize() should not be used to set a toplevel window's default size. Usually you want to set window size because you've saved the application's state and you're restoring it, or because the user specified a window geometry on the command line. Unfortunately, if you use gtk_widget_set_usize() the user will be unable to shrink the window, and you'll get hate mail. Rather than force a size, you want to specify an initial size with gtk_window_set_default_size(), shown in Figure 20. gtk_widget_set_usize() is almost never a good idea for non-toplevel widgets either; most of the time, you can get better results using the proper layout widget.

gtk_widget_set_uposition() is only useful for toplevel windows; it borders on nonsensical for other widgets, and will most likely cause bad things to happen. It's primarily used to honor a --geometry command line argument.

All three of these functions can accept -1 for the x, y, width, or height argument. The functions ignore any -1 argument; this allows you to set only one of the two arguments, leaving the default value for the other.

#include <gtk/gtkwidget.h>

void gtk_widget_set_uposition(GtkWidget* widget, gint x, gint y);

void gtk_widget_set_usize(GtkWidget* widget, gint width, gint height);

Figure 19. Forcing Allocations

#include <gtk/gtkwindow.h>

void gtk_window_set_default_size(GtkWindow* window, gint width, gint height);

Figure 20. Default Window Size