The GNOME Canvas is fundamentally a GTK+ widget, and as such it uses an API similar to that used by most other GTK+ widgets. You create it with a _new( ) function and destroy it with the gtk_object_destroy( ) function. You can connect to its GTK+ signals and interact with the user through the GDK event system. In this section we'll explore the ins and outs of interacting with the GNOME Canvas widget.
The GNOME Canvas supports two different drawing modes: GDK mode, which uses native GDK elements; and anti-aliased mode, or AA mode, which uses the advanced rendering features of the GdkRGB system and allows varying degrees of transparency between Canvas items. GDK mode, which is the default Canvas mode, is quicker, more direct, and less intensive but lacks the smoothed edges and translucency of AA mode. The anti-aliased Canvas is also more flexible about which transforms you can perform on its items, but its per- formance is slower,1 and it does not currently support all of the native GDK drawing properties, like dotted lines and stippled fills.
You determine which Canvas mode to use at the time of widget creation; you cannot change modes later without causing a lot of problems, so make your choice carefully. Each mode has its own creation function:
GtkWidget *gnome_canvas_new( ); GtkWidget *gnome_canvas_new_aa( );
The only difference between these two functions is a single Boolean flag, aa, in the GnomeCanvas structure, which is turned on for AA Canvas items and off for GDK Canvas items. This is an internal value, and you should never try to change it yourself. The Canvas uses this flag throughout its operations to differentiate between the two styles of rendering. You are not really creating two different types of Canvas widgets, but rather telling a single type of widget to behave differently in each case.
Each mode handles color depths and visuals according to its own needs. To help accommodate these needs and make sure the Canvas can find the proper color maps and visuals (see Chapter 10), you should wrap the creation function with push and pop calls to load and unload them, respectively, de- pending on how you intend to use the Canvas.
The anti-aliased Canvas always uses GdkRGB to render, so you should push its specially tuned visual and color map for AA-mode Canvas widgets. You should also remember to initialize GdkRGB beforehand, using the gdk_rgb_init( ) function:
GtkWidget *canvas_widget; gdk_rgb_init( ); gtk_widget_push_visual(gdk_rgb_get_visual( )); gtk_widget_push_colormap(gdk_rgb_get_cmap( )); canvas_widget = gnome_canvas_new_aa( ); gtk_widget_pop_visual( ); gtk_widget_pop_colormap( );
If you forget to do this, your Canvas will probably still work, but it may experience strange color problems in certain situations. The GDK-mode Canvas can also use the GdkRGB visual and color map if you plan on using the gdk- pixbuf library to load your images. In this case you will want to use the GnomeCanvasPixbuf item (see Section 11.4.6).
On the other hand, if you want to use Imlib as your image loader, you should load up GdkImlib's visual and color map instead of GdkRGB's:
GtkWidget *canvas_widget; gtk_widget_push_visual(gdk_imlib_get_visual( )); gtk_widget_push_colormap(gdk_imlib_get_colormap( )); canvas_widget = gnome_canvas_new( ); gtk_widget_pop_visual( ); gtk_widget_pop_colormap( );
Since Imlib's color map tends to clash with GdkRGB, it's not a good idea to use Imlib with an AA-mode Canvas. Thus with Imlib you should stick to using GDK mode and the GnomeCanvasImage item (also in Section 11.4.6).
If you want your Canvas to have scroll bars, you can embed them in a GtkScrolledWindow widget with minimal effort. GtkScrolledWindow will attach itself to the Canvas and automatically wire up its scroll bars with the Canvas's scrolling code. The following snippet of code is all you'll need in most cases:
GtkWidget *scrollwin; scrollwin = gtk_scrolled_window_new(NULL, NULL); gtk_container_add(GTK_CONTAINER(scrollwin), canvas_widget);
You can then put the GtkScrolledWindow widget into your GnomeApp widget, or pack it into whatever container you want to hold your Canvas.
If you stop right here, your Canvas will not have all the information it needs to render your Canvas items properly. You need to tell it how much of the potentially infinite reaches of the world coordinate space the Canvas should be allowed to draw on and scroll through. You do this by setting the Canvas's scrolling region. The Canvas cannot scroll outside of this region:
void gnome_canvas_set_scroll_region (GnomeCanvas *canvas, double x1, double y1, double x2, double y2);
You can safely call this function at any time, which implies that it's legal to have Canvas items outside the boundaries of the scrolling region. They just won't be visible until they move inside the region or the region expands to in- clude them.
Another property you can change is the scaling factor between the (abstract) world coordinates and the (pixel-based) canvas coordinates. By default, this value is set to 1.0, which means that world coordinates will have a one-to-one relationship to pixels. When world coordinates are transformed into canvas coordinates, they are multiplied-in both directions, x and y alike-by this scaling factor. Thus if you set the scaling factor to a high number, the Canvas will render everything bigger, as if you had zoomed in. If you set the scaling factor to a fraction, between 0.0 and 1.0, the Canvas will render things smaller. The function gnome_canvas_set_pixels_per_unit( ) changes this scaling factor and triggers a redraw of the entire visible area:
void gnome_canvas_set_pixels_per_unit(GnomeCanvas *canvas, double n);
Usually scroll bars are enough to cover your Canvas scrolling needs, especially if your scrolling region always matches your viewable region. Sometimes, however, you will want to control the scrolling manually, programmatically, to override the default scrolling behavior. Perhaps you need to snap the Canvas up to the top of the scrolling area, or move it to follow something the user is doing.
The gnome_canvas_scroll_to( ) function allows you to explicitly move the view port to any portion of the Canvas's area, identified by Canvas pixel coordinates. The Canvas will attempt to scroll to place those coordinates into the upper left corner of the Canvas display, but it may adjust the position a little to maximize the viewable space. The complementary function, gnome_canvas_get_scroll_offsets( ), retrieves the current scroll position:
void gnome_canvas_scroll_to(GnomeCanvas *canvas, int cx, int cy); void gnome_canvas_get_scroll_offsets(GnomeCanvas *canvas, int *cx, int *cy);
Thus given a viewable area of 100 100 pixels and a Canvas size of 200 200 pixels, an attempt to scroll to the coordinates (150, 150) would theoretically end up showing only a 50 50-pixel square of valid Canvas in the upper left corner of the viewing space, with the other 75 percent showing blank, invalid space (see Figure 11.2). To minimize this wasted space, the Canvas automatically shifts the offset from (150, 150) to (100, 100) so that the entire viewing area contains valid Canvas space.
These two functions deal in absolute coordinates. Relative scrolling is also possible, by identifying the current scrolling position with the gnome_canvas_get_scroll_offsets( ) function and calculating a new position to send to gnome_canvas_scroll_to( ). For example, if you wanted to scroll 20 pixels to the right, regardless of where the Canvas currently was, you could do something like this:
int x; int y; GnomeCanvas *canvas; ... gnome_canvas_get_scroll_offsets(canvas, &x, &y); gnome_canvas_scroll_to(canvas, x + 20, y);
Now that the GnomeCanvas widget exists, you can start adding items to it. GNOME has only one function for creating Canvas items. Each time you create a new item, you must pass in the Canvas group to which you want it added, the GTK+ type for the Canvas item, and an optional list of properties with which to initialize the item:
GnomeCanvasItem *gnome_canvas_item_new(GnomeCanvasGroup *parent, GtkType type, const gchar *first_arg_name, ...);
If you need to set or change any properties after the fact, you can use the item's accessor function:
void gnome_canvas_item_set(GnomeCanvasItem *item, const gchar *first_arg_name, ...);
Both variable parameter lists-indicated by the ellipses-work in the same way. Each property in the list requires exactly two parameters: The first parameter is a string declaring the property's name, and the second parameter is the actual value of that property. The property-value pairs can come in any order. You must always terminate the variable parameter list with a NULL value so that the parameter-parsing code will know when to stop. Also, since the value parameter can be a different type for each property, you have to be very careful not to pass in the wrong type. For example, if you pass in an integer when the item is expecting a double, the parsing code will read too far, thus corrupting the stack and probably causing your application to crash. These parameter lists are often the first place to look when you're experiencing mysterious Canvas crashes.
Here's an example of creating a rectangle Canvas item. The rectangle will be black and will stretch from world coordinates (10.0, 10.0) to (25.0, 50.0). The line will be two units wide:
GnomeCanvasItem *item; item = gnome_canvas_item_new(group, GNOME_TYPE_CANVAS_RECT, "outline_color", "black", "x1", 10.0, "y1", 10.0, "x2", 25.0, "y2", 50.0, "width_units", 2.0, NULL);
You could just as easily use the gnome_canvas_rect_get_type( ) function instead of the GNOME_TYPE_CANVAS_RECT macro. They are equivalent, so it's mostly a matter of style. We'll explore the long list of Canvas items in Section 11.4.
Once your item exists, you can find out its current size and position with gnome_canvas_item_get_bounds( ). This function returns the bounding box of the item inside the coordinate system of its immediate parent item. You can go in the reverse direction and find out which item resides at a given point on the Canvas by calling gnome_canvas_get_item_at( ):
void gnome_canvas_item_get_bounds(GnomeCanvasItem *item, double *x1, double *y1, double *x2, double *y2); GnomeCanvasItem *gnome_canvas_get_item_at(GnomeCanvas *canvas, double x, double y);
You can grab the input focus for an individual Canvas item with gnome_canvas_item_grab_focus( ):
void gnome_canvas_item_grab_focus (GnomeCanvasItem *item);
When you do this, all keyboard input will go to that particular Canvas item until another Canvas item, widget, or application grabs it back. This feature is particularly handy if you need an item to display text as the user types it. We'll learn more about how the Canvas interacts with GDK events in Section 11.5.
Unlike a simple double-buffered rendering engine in which you draw directly to the buffer, the GNOME Canvas supports an infrastructure of movable objects that the Canvas draws to the double buffer for you. If all you wanted was a static graphics buffer, you could fill the Canvas with items and leave them where they lay. However, that would be wasting a good portion of the Canvas's functionality. Moving an item to a new location is as simple as calling a single function with the new world coordinates:
void gnome_canvas_item_move(GnomeCanvasItem *item, double dx, double dy);
Since two Canvas items can't occupy the same rendering space (not counting alpha transparency, but we'll ignore that for now), the Canvas has to choose which one to display on top. It makes this decision by following the hierarchy of items from the topmost leaf nodes all the way down to the root group. Items closer to the root group always appear behind items farther away from the root group. Each branch of the hierarchy is another Canvas group because only groups can contain other items. Thus all items in a higher group appear on top of all items in a lower group.
However, all items contained by the same group are more or less all at the same level. The Canvas remembers the order in which you add items to a group and displays the most recently added items on top. You can change that order with the raise and lower functions:
void gnome_canvas_item_raise(GnomeCanvasItem *item, int positions); void gnome_canvas_item_lower(GnomeCanvasItem *item, int positions);
These first two functions will raise or lower an item within a group by a numerical offset, positions. If you called gnome_canvas_item_raise( ) on the fifth item in a group of 10 items with a positions parameter of 4, that item would move from the middle of the group to the second-to-top item. The Canvas will not move an item past the absolute top or bottom of a group, so if you pass a huge number to those functions, the item will end up at the top or bottom of the same group rather than continuing to the next group. If you specifically want to raise an item to the top or lower it to the bottom of its current group, you can call the following functions:
void gnome_canvas_item_raise_to_top(GnomeCanvasItem *item); void gnome_canvas_item_lower_to_bottom(GnomeCanvasItem *item);
The raise and lower functions, as well as the move function, work equally well on a Canvas group, which is still, by nature, a Canvas item. When you move a group around, you move all the items contained in that group as if they were a single aggregate object, including any groups inside that group. Moving, raising, or lowering a group performs the same action on the entire collection of items.
Although the raise and lower functions cannot implicitly move an item to a different group, you can do that explicitly with the following function:
void gnome_canvas_item_reparent(GnomeCanvasItem *item, GnomeCanvasGroup *new_group);
By default, reparenting an item to a new group will put that item at the top of that group. But don't be surprised if reparenting an item also causes it to jump to new coordinates. The item keeps its old item-based coordinates, so an item at (10.0, 25.0) in the old group's coordinates will move to (10.0, 25.0) in the new group's coordinates. Unless the two groups share exactly the same coordinates in the Canvas, the item will end up at a different Canvas offset in the new group. One limitation you may have noticed from the function prototype is that you can't use the reparent function to move an item to a different Canvas widget. You can only move it to another group in the same Canvas. The function does not ask for a pointer to the GnomeCanvas widget because the pointer is implied by the Canvas item.
Because groups are Canvas items too, you can reparent them as well, but you should be aware of a couple of peculiarities that apply only to group reparenting. First, you cannot reparent a group to itself. This limitation should be obvious. Somewhat less obviously, you may not reparent a group to another group within itself. You must first move the target group outside of the group you want to reparent. If you were allowed to reparent a group to a group within, the Canvas would quickly become corrupted by circular tree branches: Group A contains group B, which contains group A, which contains group B, and so on, ad infinitum.
Another useful feature of the GNOME Canvas is the ability to completely hide an item-or an entire group-without removing it from the Canvas. You can turn it on and off like a light switch, just as you can with normal widgets, using the following functions:
void gnome_canvas_item_show(GnomeCanvasItem *item); void gnome_canvas_item_hide(GnomeCanvasItem *item);
A hidden item or group is still a valid object. The main difference between a hidden item and a visible one is that the Canvas skips over hidden items when it goes to render the Canvas contents to the drawing buffer. You can still move hidden items around and apply transforms to them. When you show them again, they will take on their new, modified state.
Normally you shouldn't need to hide objects to avoid flicker before making major changes to them. The Canvas defers painting updates to an idle function rather than demanding a fresh redraw each time the Canvas changes, so very likely if you apply several changes in a row, they will be rendered to the double buffer right away, and only transferred to the display the next time the idle handler fires. If you're desperate, though, and can't get rid of some annoying flicker, you can use the freeze function from the Canvas's GtkLayout ancestry:
gtk_layout_freeze (GTK_LAYOUT (canvas));
In reality you should never need to go this far, unless you are doing something very unusual with the Canvas, in which case you might be better off rethinking your design.
The Canvas uses libart's affine transformations to move items around, by applying simple spatial offsets. The infrastructure is already there for performing much more complex transforms, separately, on any item on the Canvas. As we learned in Chapter 10, affine transforms are very powerful and very flexible. By adjusting the affine transforms a bit, you can invert, rotate, scale, and shear your Canvas items. Simple operations like scaling and rotating come with dedicated functions, so you won't need to touch the affine matrices directly:
void gnome_canvas_item_scale(GnomeCanvasItem *item, double x, double y, double scale_x, double scale_y); void gnome_canvas_item_rotate(GnomeCanvasItem *item, double x, double y, double angle);
More complex operations can perform custom transforms directly on a Canvas item. Setting an absolute transform will completely override any current transforms:
void gnome_canvas_item_affine_absolute(GnomeCanvasItem *item, const double affine);
You might need to do this if you want to start a series of transforms from scratch. Another use, in conjunction with art_affine_identity( ), is to remove transforms entirely so that the item appears in its most basic state. Be careful with this approach, however, because it will also reverse any gnome_canvas_item_move( ) operations as well.
In most cases you'll probably want to add transforms rather than replacing existing transforms. Affine transforms are inherently cumulative, so it's very easy to accurately apply successive transforms on top of each other. In fact, each time you move an item, you are really applying yet another translational transform to the current offset. This operation is conceptually similar to art_affine_multiply( ), although the Canvas does not actually use that function to move items around. Instead it uses the following function:
void gnome_canvas_item_affine_relative(GnomeCanvasItem *item, const double affine);