Any sufficiently complex GUI toolkit needs some way to render graphics to the screen. In Chapter 10 we learned how to do this on a lower level. The lowlevel approach is fine for simple things, like rendering icons, widget faces, and raw images. However, it does not scale well, and it can become quite cumbersome in a large, graphic-intensive application. The GNOME Canvas meets this higher-level need by providing an object-based interface to an encapsulated drawing buffer, and it goes the extra mile by wiring those drawing objects into the GDK event and signal systems. In this chapter we'll learn how to create and populate a GNOME Canvas drawing widget and interact with its drawing objects.
Before we get into the specifics, we should take a look at the general features of the GNOME Canvas. The Canvas is a very powerful, highly customizable widget that can meet many of your complex graphics needs with only a moderate amount of work on your part.
As we learned in Chapter 10, rendering a pixmap can be a very expensive operation in terms of resources, especially when you're also sending it across the wire to a networked X server. We discussed a few techniques like clipping areas and using shared memory to help ease the negative effects on performance.
Another very useful performance technique is called double buffering, a topic we encountered in Chapter 10. Rather than drawing directly on the pixmap stored on the X server, which can lead to a significant latency for even the slightest change, you can do all your drawing on a locally held pixmap with no network latency, and then push the entire prerendered image out to the X server in one big chunk. You'll make only one expensive round-trip through the X protocol stack rather than one round-trip for each line, pixel, or subimage you send.
Double buffers can also help simplify and streamline your graphics code. If you are rendering directly to an X drawable, you must intermix your rendering code with your clipping code. If you have a complex scene to render, with many objects floating around-for example, in a game-you'll end up with lots of overhead when comparing clipping regions with each object's position and deciding how much of that object to include in the exposed area. You have to worry about everything at the same time, more or less.
With a double buffer, however, you can use one chunk of code to update the local pixmap and an entirely different chunk of code to cut out a clipping region from the prerendered scene and send it off to the X server. You don't have to worry about the overhead of rendering during the exposure event because you already did that the last time the image changed. One way of looking at it is that direct rendering is an active strategy, while double buffering is a passive strategy. The GNOME Canvas uses double buffering, in combination with libart's sorted vector paths (SVPs) and microtile arrays (utas) (see Chapter 10), to optimize its drawing and refreshing operations.
In one sense the GNOME Canvas is an easy-to-use, high-level wrapper around a double-buffered rendering engine. It takes care of all the meticulous details of creating drawing objects, populating the pixmaps, and updating the display. You don't have to know much about color depths and visuals and dithering; you only have to tell the Canvas what you want it to draw, and where. If this were the extent of the Canvas's usefulness, it would still be a valuable, flexible tool. But this is only the beginning.
At a deeper level the Canvas is an object-oriented hierarchical image management engine. Each element inside the Canvas is a separate object that you can manipulate and modify without touching the other objects. Each object can have different properties that affect its appearance. Specifically, each Canvas object-or item, as they're called-is derived from GtkObject, giving it access to GTK+'s signals and dynamic property system. The GNOME Canvas makes heavy use of both of these features, as we'll see throughout this chapter.
One mistake people often make when they first start using the Canvas is to store all their data inside it. Although it's a good idea to tag each Canvas item with a pointer to the data it represents, that pointer should never be your sole access to the data. The Canvas is designed to be efficient at rendering graphics, not at managing data. If you must pass through the Canvas hierarchy and the GTK+ object system each time you look up data, your access time will be much slower and clumsier than it needs to be. The ideal solution is to keep your important data in a dedicated database or structure elsewhere in your application and use the Canvas only for displaying it. The Canvas works best when you keep your data separate from the rendered view of it.
The test-gnome application bundled with gnome-libs provides a good overview of the GNOME Canvas. One of the views in its Canvas demonstration is displayed in Figure 11.1.
The Canvas also employs a hierarchical system of organization for its Canvas items. You can group objects together and move them around as if they were a single object. You can even nest groups and items inside other groups, and then move them all as a single object or reach inside and move the subgroups around separately. The Canvas group is the key to the Canvas hierarchy.
A Canvas group is really a specialized Canvas item, and it contains all the basic Canvas item features. However, it is different in that it does not render itself to the drawing buffer. It organizes the items contained within it and ren- ders those instead. You can think of a Canvas group as a meta-item whose job is to manage other Canvas items.
All Canvas widgets have at least one Canvas group: the root group. This root group is the base of the Canvas hierarchy and will ultimately contain every item or group in the entire Canvas widget. You can find the root at any time with the following function:
GnomeCanvasGroup *gnome_canvas_root (GnomeCanvas *canvas);
Events in GTK+, and thus GNOME, are propagated by GDK, from widget to widget. When the user clicks the mouse on a GNOME Canvas widget or types on the keyboard while the Canvas has the input focus, GDK sends the proper GdkEvent structure to the Canvas widget. However, GDK has no concept of what's inside the Canvas widget. It can't see the individual Canvas items or groups; it just knows where on the screen the Canvas widget is. It's up to the Canvas to react to the GDK event on its own terms.
When the Canvas receives a GDK event, it uses its internal logic to decide which Canvas item the event was intended for, depending on the type of event, the current state of the Canvas, and where on the Canvas it happened. When it finds the target item, the Canvas performs a little extra processing on the event and then passes it to that item through the GTK+ signal system, using the event signal owned by all Canvas items.
Another way to look at it is that GDK passes the real external event to the Canvas widget. The Canvas widget extracts the useful information from that event and uses it to create a new synthetic internal event, which it then hands off to the item. Despite all the back-end hoopla, the event that reaches the Canvas item looks a lot like a real GDK event. You can connect to the item's event signal and set up an event callback just like you do with "normal" GDK events. Aside from a few minor differences that we'll discuss in Section 11.5, the Canvas closes the gap between the widget and the Canvas item almost transparently.