GTK+ / Gnome Application Development | |||
---|---|---|---|
<<< Previous | Home | Next >>> |
The most important task of any canvas item is rendering itself onto the canvas. Rendering is a two-stage process for efficiency reasons. The first stage, implemented in a GnomeCanvasItem's update method, is guaranteed to happen only once per item per rendering cycle; the idea is to do any expensive affine transformations or other calculations in the update method. In the second stage, the canvas item renders itself to some region on the screen. The render method implements stage two for antialiased items, while the draw method implements stage two for GDK items. An item's render or draw method may be invoked multiple times during a canvas repaint.
Rendering occurs in a one-shot idle function. That is, whenever the canvas receives an expose event or otherwise determines that a redraw is needed, it adds an idle function which removes itself after a single invocation. (An idle function runs when no GTK+ events are pending and the flow of execution is in the GTK+ main loop---see the section called The Main Loop in the chapter called GTK+ Basics for details.) The canvas maintains a list of redraw regions and adds to it whenever a redraw request is received, so it knows which areas to repaint when the idle handler is finally invoked.
Canvas items carry a flag indicating whether they need to be updated. Whenever a canvas item "changes" (for example, if you set a new fill color for GnomeCanvasRect), it will call gnome_canvas_item_request_update() to set the "update needed" flag for itself and the groups that contain it, up to and including the root canvas group. (The GnomeCanvas widget is only aware of a single canvas item, the root group---all other items are handled recursively when methods are invoked on the root group.) In its one-shot idle function, the canvas invokes the update method of the root canvas item if its update flag is set, then clears the flag so the update method will not be run next time. The GnomeCanvasGroup update method does the same for each child item.
Once all canvas items have been updated, the rendering process begins. The canvas creates an RGB or GdkPixmap buffer, converts its list of redraw regions into a list of buffer-sized rectangles, then invokes the render or draw method of the root canvas group once per rectangle. After each rectangle is rendered, the buffer is copied to the screen.
The update method is primarily used by antialiased canvas items. libart_lgpl can prebuild a vector path to be rendered, performing clipping and affine transformation in advance. The render method stamps the pre-assembled path into the RGB buffer.
The update method is one of the two that GnomeCanvasRect and GnomeCanvasEllipse have to implement differently. Here is the GnomeCanvasRect implementation:
static void gnome_canvas_rect_update (GnomeCanvasItem *item, double affine[6], ArtSVP *clip_path, gint flags) { GnomeCanvasRE *re; ArtVpath vpath[11]; ArtVpath *vpath2; double x0, y0, x1, y1; double dx, dy; double halfwidth; int i; gnome_canvas_re_update_shared (item, affine, clip_path, flags); re = GNOME_CANVAS_RE (item); if (item->canvas->aa) { x0 = re->x1; y0 = re->y1; x1 = re->x2; y1 = re->y2; gnome_canvas_item_reset_bounds (item); if (re->fill_set) { vpath[0].code = ART_MOVETO; vpath[0].x = x0; vpath[0].y = y0; vpath[1].code = ART_LINETO; vpath[1].x = x0; vpath[1].y = y1; vpath[2].code = ART_LINETO; vpath[2].x = x1; vpath[2].y = y1; vpath[3].code = ART_LINETO; vpath[3].x = x1; vpath[3].y = y0; vpath[4].code = ART_LINETO; vpath[4].x = x0; vpath[4].y = y0; vpath[5].code = ART_END; vpath[5].x = 0; vpath[5].y = 0; vpath2 = art_vpath_affine_transform (vpath, affine); gnome_canvas_item_update_svp_clip (item, &re->fill_svp, art_svp_from_vpath (vpath2), clip_path); art_free (vpath2); } else gnome_canvas_item_update_svp (item, &re->fill_svp, NULL); if (re->outline_set) { if (re->width_pixels) halfwidth = re->width * 0.5; else halfwidth = re->width * item->canvas->pixels_per_unit * 0.5; if (halfwidth < 0.25) halfwidth = 0.25; i = 0; vpath[i].code = ART_MOVETO; vpath[i].x = x0 - halfwidth; vpath[i].y = y0 - halfwidth; i++; vpath[i].code = ART_LINETO; vpath[i].x = x0 - halfwidth; vpath[i].y = y1 + halfwidth; i++; vpath[i].code = ART_LINETO; vpath[i].x = x1 + halfwidth; vpath[i].y = y1 + halfwidth; i++; vpath[i].code = ART_LINETO; vpath[i].x = x1 + halfwidth; vpath[i].y = y0 - halfwidth; i++; vpath[i].code = ART_LINETO; vpath[i].x = x0 - halfwidth; vpath[i].y = y0 - halfwidth; i++; if (x1 - halfwidth > x0 + halfwidth && y1 - halfwidth > y0 + halfwidth) { vpath[i].code = ART_MOVETO; vpath[i].x = x0 + halfwidth; vpath[i].y = y0 + halfwidth; i++; vpath[i].code = ART_LINETO; vpath[i].x = x1 - halfwidth; vpath[i].y = y0 + halfwidth; i++; vpath[i].code = ART_LINETO; vpath[i].x = x1 - halfwidth; vpath[i].y = y1 - halfwidth; i++; vpath[i].code = ART_LINETO; vpath[i].x = x0 + halfwidth; vpath[i].y = y1 - halfwidth; i++; vpath[i].code = ART_LINETO; vpath[i].x = x0 + halfwidth; vpath[i].y = y0 + halfwidth; i++; } vpath[i].code = ART_END; vpath[i].x = 0; vpath[i].y = 0; vpath2 = art_vpath_affine_transform (vpath, affine); gnome_canvas_item_update_svp_clip (item, &re->outline_svp, art_svp_from_vpath (vpath2), clip_path); art_free (vpath2); } else gnome_canvas_item_update_svp (item, &re->outline_svp, NULL); } else { get_bounds (re, &x0, &y0, &x1, &y1); gnome_canvas_update_bbox (item, x0, y0, x1, y1); } } |
As you can see, the first thing this function does is invoke an update function shared by GnomeCanvasRect and GnomeCanvasEllipse; here is that function:
static void gnome_canvas_re_update_shared (GnomeCanvasItem *item, double *affine, ArtSVP *clip_path, int flags) { GnomeCanvasRE *re; re = GNOME_CANVAS_RE (item); if (re_parent_class->update) (* re_parent_class->update) (item, affine, clip_path, flags); if (!item->canvas->aa) { set_gc_foreground (re->fill_gc, re->fill_pixel); set_gc_foreground (re->outline_gc, re->outline_pixel); set_stipple (re->fill_gc, &re->fill_stipple, re->fill_stipple, TRUE); set_stipple (re->outline_gc, &re->outline_stipple, re->outline_stipple, TRUE); set_outline_gc_width (re); } } |
There is a lot of code involved here; the update method is almost always the most complicated one, since it does all the work of preparing to render a canvas item. Also, the update method is different for GDK and antialiased mode; notice the code which depends on the item->canvas->aa flag.
The first thing GnomeCanvasRE does during an update is invoke the update method of its parent class. The GnomeCanvasItem default update method does nothing whatsoever in Gnome 1.0, but it is good practice to chain up for future robustness. Then, GnomeCanvasRE calls a series of utility routines to fill in its graphics contexts with their correct values. These are straightforward functions, so their implementations are omitted here.
Next gnome_canvas_rect_update() continues with GnomeCanvasRect-specific details. Several tasks are accomplished:
The bounding box of the canvas item is updated. Every canvas item has an associated bounding box; the GnomeCanvasGroup draw and render methods use this box to determine which items are in the redraw region. The bounding box must be updated in both GDK and antialiased mode.
In antialiased mode, a sorted vector path is created. A sorted vector path is simply a series of drawing instructions, similar to primitive PostScript operations, that libart_lgpl can render to an RGB buffer.
In antialiased mode, the affine and clip_path arguments to the update method are used to transform the sorted vector path; thus the affine and clip path are implicitly stored for use in the render method. If you do not use libart_lgpl's sorted vector paths in your own canvas items, you must arrange some other way to ensure the affine and clip are taken into account when you render.
In both modes, a redraw is requested for both the region the item used to occupy, and the region the item will now occupy.
Much of this work takes place behind the scenes in utility functions from libgnomeui/gnome-canvas-util.h. gnome_canvas_update_bbox() sets the item's new bounding box and requests a redraw on both the old and new bounding boxes; it is used in GDK mode. (gnome_canvas_update_bbox() expects canvas pixel coordinates; get_bounds() is a trivial function which computes the rectangle's bounds in canvas pixel coordinates.)
So you know what's happening behind the scenes, here is the implementation of gnome_canvas_update_bbox():
void gnome_canvas_update_bbox (GnomeCanvasItem *item, int x1, int y1, int x2, int y2) { gnome_canvas_request_redraw (item->canvas, item->x1, item->y1, item->x2, item->y2); item->x1 = x1; item->y1 = y1; item->x2 = x2; item->y2 = y2; gnome_canvas_request_redraw (item->canvas, item->x1, item->y1, item->x2, item->y2); } |
Of course you're free to do the equivalent yourself, this is merely a convenience function.
In GDK mode, that's about all that happens; we update the bounds and then return. Antialiased mode is a bit more complex, but essentially the same tasks are performed. First, gnome_canvas_item_reset_bounds() sets the item's bounds back to an empty rectangle. Then, two sorted vector paths are prepared; one for the solid part of the rectangle (if any), and one for the rectangle's outline (if any). The same procedure is followed each time. First, a vector path for libart_lgpl is prepared; next, the path is affine transformed; then gnome_canvas_item_update_svp_clip() is used to request a redraw on the old path, free the old path, clip the new path, request a redraw on the new one, and save the new one for use in rendering. If the rectangle's fill or outline has been turned off, a redraw is requested on the old vector path, but no new path is created.
To give you a clearer idea what is happening, here is the implementation of gnome_canvas_item_update_svp_clip():
void gnome_canvas_item_update_svp_clip (GnomeCanvasItem *item, ArtSVP **p_svp, ArtSVP *new_svp, ArtSVP *clip_svp) { ArtSVP *clipped_svp; if (clip_svp != NULL) { clipped_svp = art_svp_intersect (new_svp, clip_svp); art_svp_free (new_svp); } else { clipped_svp = new_svp; } gnome_canvas_item_update_svp (item, p_svp, clipped_svp); } |
and gnome_canvas_item_update_svp():
void gnome_canvas_item_update_svp (GnomeCanvasItem *item, ArtSVP **p_svp, ArtSVP *new_svp) { ArtDRect bbox; gnome_canvas_update_svp (item->canvas, p_svp, new_svp); if (new_svp) { bbox.x0 = item->x1; bbox.y0 = item->y1; bbox.x1 = item->x2; bbox.y1 = item->y2; art_drect_svp_union (&bbox, new_svp); item->x1 = bbox.x0; item->y1 = bbox.y0; item->x2 = bbox.x1; item->y2 = bbox.y1; } } |
and then gnome_canvas_update_svp():
void gnome_canvas_update_svp (GnomeCanvas *canvas, ArtSVP **p_svp, ArtSVP *new_svp) { ArtSVP *old_svp; ArtSVP *diff; ArtUta *repaint_uta; old_svp = *p_svp; if (old_svp != NULL && new_svp != NULL) { repaint_uta = art_uta_from_svp (old_svp); gnome_canvas_request_redraw_uta (canvas, repaint_uta); repaint_uta = art_uta_from_svp (new_svp); gnome_canvas_request_redraw_uta (canvas, repaint_uta); } else if (old_svp != NULL) { repaint_uta = art_uta_from_svp (old_svp); art_svp_free (old_svp); gnome_canvas_request_redraw_uta (canvas, repaint_uta); } *p_svp = new_svp; } |
Again, all of these are in libgnomeui/gnome-canvas-util.h for any canvas item to use. Ignore the implementation details; the idea is simply to see what work is being done. The code may be easier to understand if you know that an ArtDRect is a "rectangle defined with doubles," from libart_lgpl, and that an ArtUta is a "microtile array," basically a list of small regions. (The antialiased canvas tracks the redraw region in a fairly sophisticated way. Note that the "U" in "Uta" is supposed to suggest the greek letter symbolizing "micro," it does not stand for a word beginning with "U".)
It is the canvas item's responsibility to request an update or redraw when the properties of the item are changed and the screen should be refreshed. This is straightforward. For example, here is a snippet of code from gnome_canvas_re_set_arg(), which sets the "y2" argument:
case ARG_Y2: re->y2 = GTK_VALUE_DOUBLE (*arg); gnome_canvas_item_request_update (item); break; |
Since "y2" modifies the shape of the rectangle, the path must be recreated and an update is necessary. Note that gnome_canvas_item_request_update() simply sets a flag and installs an idle handler if none is pending, so it can be called many times without a performance penalty.
Not all changes require an update; a redraw may be sufficient, or perhaps the argument is unrelated to the display. It depends on the canvas item and what exactly is being changed.
The render method is shared between GnomeCanvasRect and GnomeCanvasEllipse; all it does is stamp the two paths created in the update method into the RGB buffer:
static void gnome_canvas_re_render (GnomeCanvasItem *item, GnomeCanvasBuf *buf) { GnomeCanvasRE *re; guint32 fg_color, bg_color; re = GNOME_CANVAS_RE (item); if (re->fill_svp != NULL) { gnome_canvas_render_svp (buf, re->fill_svp, re->fill_color); } if (re->outline_svp != NULL) { gnome_canvas_render_svp (buf, re->outline_svp, re->outline_color); } } |
As you can see, most of the work takes place in gnome_canvas_render_svp(), another function from libgnomeui/gnome-canvas-util.h; here is its implementation:
void gnome_canvas_render_svp (GnomeCanvasBuf *buf, ArtSVP *svp, guint32 rgba) { guint32 fg_color, bg_color; if (buf->is_bg) { bg_color = buf->bg_color; fg_color = rgba >> 8; art_rgb_svp_aa (svp, buf->rect.x0, buf->rect.y0, buf->rect.x1, buf->rect.y1, fg_color, bg_color, buf->buf, buf->buf_rowstride, NULL); buf->is_bg = 0; buf->is_buf = 1; } else { art_rgb_svp_alpha (svp, buf->rect.x0, buf->rect.y0, buf->rect.x1, buf->rect.y1, rgba, buf->buf, buf->buf_rowstride, NULL); } } |
To understand gnome_canvas_render_svp(), or to do your own RGB buffer drawing (without using libart_lgpl), you will need to know what a GnomeCanvasBuf is:
typedef struct { guchar *buf; int buf_rowstride; ArtIRect rect; guint32 bg_color; unsigned int is_bg : 1; unsigned int is_buf : 1; } GnomeCanvasBuf; |
The buf member is an RGB buffer, as explained in the section called RGB Buffers in the chapter called GDK Basics. The buf_rowstride is the buffer's rowstride, also explained in the section called RGB Buffers in the chapter called GDK Basics. An ArtIRect is an integer rectangle; rect defines the redraw region in canvas pixel coordinates that this buffer represents. rect.x0 and rect.y0 are the buffer offsets and correspond to row 0, column 0 in the RGB buffer; you can convert from canvas pixel coordinates to RGB buffer coordinates by subtracting these values.
As an optimization, the canvas does not initialize the RGB buffer with the background color, because the first canvas item might cover the entire background anyway. Thus, if your canvas item is the first one to render, you must put some pixel value in every pixel of the redraw region defined by the buffer's rect. If your item does not cover a pixel, you should fill that pixel with the bg_color; bg_color is a packed RGB value (no alpha). If you do this manually, unpack an RGB value rgb like this:
guchar r, g, b; r = (rgb >> 16) & 0xff; g = (rgb >> 8) & 0xff; b = rgb & 0xff; |
However, a convenience function is provided to fill a GnomeCanvasBuf with its bg_color:
void gnome_canvas_buf_ensure_buf (GnomeCanvasBuf *buf) { guchar *bufptr; int y; if (!buf->is_buf) { bufptr = buf->buf; for (y = buf->rect.y0; y < buf->rect.y1; y++) { art_rgb_fill_run (bufptr, buf->bg_color >> 16, (buf->bg_color >> 8) & 0xff, buf->bg_color & 0xff, buf->rect.x1 - buf->rect.x0); bufptr += buf->buf_rowstride; } buf->is_buf = 1; } } |
As you can see from the implementation of gnome_canvas_buf_ensure_buf(), is_bg is a flag indicating that the RGB buffer still contains random memory garbage; it has not been initialized with RGB pixels. is_buf indicates that the buffer has been initialized, and subsequent items should only draw themselves, ignoring background pixels. These two flags are mutually exclusive; if your item receives a buffer with is_bg set, it should take steps to fill the buffer, unset is_bg, and set is_buf:
if (buf->is_bg) { gnome_canvas_buf_ensure_buf(buf); buf->is_bg = FALSE; } |
If you have a large number of objects, RGB mode can be faster than GDK mode. Drawing to an RGB buffer is a simple matter of assigning to an array, which is much, much faster than making a GDK call (since GDK has to contact the X server and ask it to do the actual drawing). The expensive part is copying the RGB buffer to the X server when you're done. However, the copy takes the same amount of time no matter how many canvas items you have, since it is done only once, when all the items have been rendered.
This is a big win in an application called "Guppi" I'm in the process of writing. Guppi is a plot program. One of the things it has to do is render a scatter plot with tens of thousands of points. Each point is a small colored shape; if I called GDK to render each, there would be tens of thousands of trips to the X server, possibly across a network. Instead, I use the canvas in RGB mode, with a custom canvas item representing the scatter plot. This allows me to do all the rendering on the client side, and then the canvas copies the RGB buffer to the server in a single burst. It's quite fast and responsive. For less speed-intensive elements of the plot, such as the legend, I can save time and use the built-in canvas items.
The one difficulty with direct-to-RGB rendering is that you need a rasterization library comparable to the GDK drawing primitives if you want to draw anything interesting. libart_lgpl is a very high-quality antialiased rasterization library, used by the default canvas items. You can use it in your custom items as well, and it is the best choice if you will only be drawing hundreds of shapes. If you're drawing thousands of shapes, however, you'll quickly see the need for something faster. Fortunately, this is available; the maintainers of a package called GNU Plotutils extracted the rasterization library from the X distribution, and during the development of Guppi I extracted it from Plotutils and hacked it to work with the canvas's RGB buffers. I also added alpha transparency support. The resulting library allows you to draw on an RGB buffer much as you would draw using GDK. The library is distributed under the same license as the X Window System, and is free for anyone to include with their application.
Raph Levien, author of libart_lgpl and the GdkRGB module, tells me that still faster routines could be written; if you need more speed, consider this a challenge.
Drawing with GDK is much less complicated than drawing with libart_lgpl, though it is also less flexible and produces lower-quality results. Here is the GnomeCanvasRect implementation of the draw method:
static void gnome_canvas_rect_draw (GnomeCanvasItem *item, GdkDrawable *drawable, int x, int y, int width, int height) { GnomeCanvasRE *re; double i2w[6], w2c[6], i2c[6]; int x1, y1, x2, y2; ArtPoint i1, i2; ArtPoint c1, c2; re = GNOME_CANVAS_RE (item); /* Get canvas pixel coordinates */ gnome_canvas_item_i2w_affine (item, i2w); gnome_canvas_w2c_affine (item->canvas, w2c); art_affine_multiply (i2c, i2w, w2c); i1.x = re->x1; i1.y = re->y1; i2.x = re->x2; i2.y = re->y2; art_affine_point (&c1, &i1, i2c); art_affine_point (&c2, &i2, i2c); x1 = c1.x; y1 = c1.y; x2 = c2.x; y2 = c2.y; if (re->fill_set) { if (re->fill_stipple) gnome_canvas_set_stipple_origin (item->canvas, re->fill_gc); gdk_draw_rectangle (drawable, re->fill_gc, TRUE, x1 - x, y1 - y, x2 - x1 + 1, y2 - y1 + 1); } if (re->outline_set) { if (re->outline_stipple) gnome_canvas_set_stipple_origin (item->canvas, re->outline_gc); gdk_draw_rectangle (drawable, re->outline_gc, FALSE, x1 - x, y1 - y, x2 - x1, y2 - y1); } } |
The draw method receives a drawable (the buffer), the buffer offsets (x and y---the canvas pixel coordinates of the buffer), and the buffer's size (width and height). GnomeCanvasRect's draw method obtains the item-to-world and world-to-canvas affines, then composes (multiplies) them to create an item-to-canvas affine. (See the section called Affine Transformations in the chapter called GnomeCanvas for more on affines.) Using this affine, it converts the rectangle's corner points to canvas pixel coordinates; then it draws the rectangle, converting the canvas coordinates to buffer coordinates by subtracting the buffer offsets.