GTK+ / Gnome Application Development | |||
---|---|---|---|
<<< Previous | Home | Next >>> |
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.
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.
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.
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.
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.
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.
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):
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).
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.
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.
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.
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:
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).
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.
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.
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.
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.
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); |
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
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 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.
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.
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.
GTK_EXPAND specifies that this section of the table will expand to fit available space, much like the expand option when packing boxes.
GTK_FILL specifies that the child widget will expand to fill available space. Important only if GTK_EXPAND is set, because GTK_EXPAND permits extra space to exist.
GTK_SHRINK determines what will happen if there is insufficient space to meet the child's size request. If GTK_SHRINK is set, the child is given a smaller allocation which reflects available space---i.e., the table shrinks the child. If it isn't set, the child is given its requested size; this may result in overlapping children within the table, and children will be "chopped off" at the table edges (because they'll try to draw outside the table's GdkWindow).
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.
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:
The first child will always receive its requested size; it neither expands nor shrinks.
The second child can expand and shrink only in the Y direction.
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.
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.
Next, imagine the user expanding the window horizontally; only child widget number three can expand horizontally. Figure 15 shows this.
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.
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.
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.
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.
Boxes and tables are the most commonly-used layout widgets by far. However, there are a few others for special situations.
GtkButtonBox is a special kind of box appropriate for the "action area" of a dialog.
GtkPacker supports Tk-style packing, useful if you're familiar with Tk.
GtkLayout provides an infinite scrolling area. In general, scrolling areas in GTK+ are limited to just over 30,000 pixels, because that is the maximum size of an X window.
GtkFixed allows you to manually position widgets at fixed coordinates.
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.