Using the Canvas

GnomeCanvas is easy to use; this is its virtue compared to GtkDrawingArea or some other low-level approach. This section describes how to create a canvas, and work with canvas items. It ends with a programming example.

Preparing the GnomeCanvas Widget

The first decision you have to make is whether to use the canvas in GDK mode or antialiased mode. When you create a canvas widget, you must specify the mode you want; there is no way to change it later. gnome_canvas_new() creates a GDK canvas. gnome_canvas_new_aa() creates an antialiased canvas. These are shown in Figure 5.

Sometimes it matters which visual and colormap the canvas will use. In particular:

To create a widget with a non-default visual and colormap, gtk_widget_push_visual() and gtk_widget_push_colormap() are used. Here is the code to create a GDK canvas that supports the image item:


  GtkWidget* canvas;
  gtk_widget_push_visual(gdk_imlib_get_visual());
  gtk_widget_push_colormap(gdk_imlib_get_colormap());
  canvas = gnome_canvas_new();
  gtk_widget_pop_visual();
  gtk_widget_pop_colormap();

      

To create an antialiased canvas, do this:


  GtkWidget* canvas;
  gtk_widget_push_visual(gdk_rgb_get_visual());
  gtk_widget_push_colormap(gdk_rgb_get_cmap());
  canvas = gnome_canvas_new_aa();
  gtk_widget_pop_colormap();
  gtk_widget_pop_visual();

      
       #include <libgnomeui/gnome-canvas.h>
      

GtkWidget* gnome_canvas_new(void);

GtkWidget* gnome_canvas_new_aa(void);

Figure 5. Canvas Constructors

Scroll Region

The canvas is practically infinite from a programmer's standpoint; however, in reality your application probably uses only a small area. When using the canvas you must specify which region is interesting to the user with gnome_canvas_set_scroll_region() (Figure 6). The scroll region is given in world coordinates. You can query the scroll region with gnome_canvas_get_scroll_region().

To add scroll bars to the canvas, simply create a GtkScrolledWindow and add the canvas to it:


  GtkWidget* sw;
  sw = gtk_scrolled_window_new(NULL, NULL);
  gtk_container_add(GTK_CONTAINER(sw), canvas);

    

If you want to implement scrolling via some mechanism other than the scroll bars, you can get and set the "scroll offsets." The scroll offsets are in canvas pixel coordinates; they specify the top left visible pixel. Remember that canvas pixel coordinates are relative to the scroll region.

         #include <libgnomeui/gnome-canvas.h>
        

void gnome_canvas_set_scroll_region(GnomeCanvas* canvas, double x1, double y1, double x2, double y2);

void gnome_canvas_get_scroll_region(GnomeCanvas* canvas, double* x1, double* y1, double* x2, double* y2);

void gnome_canvas_scroll_to(GnomeCanvas* canvas, gint cx, gint cy);

void gnome_canvas_get_scroll_offsets(GnomeCanvas* canvas, gint* cx, gint* cy);

Figure 6. Canvas Scrolling

Zooming

The canvas gives you zooming "for free"; it is included in the world-to-canvas and canvas-to-world coordinate system conversions. You can set the zoom factor with gnome_canvas_set_pixels_per_unit() (Figure 7). By default, there ratio of pixels to canvas units is 1.0, meaning no zoom. Specifying a value less than 1.0 means reduced size; greater than 1.0 means increased size.

In antialiased mode, you could achieve the same visual effect by applying a scaling affine transformation to the root canvas group. The pixels_per_unit member of the GnomeCanvas struct predates the canvas's use of affines. Still, gnome_canvas_set_pixels_per_unit() is a bit more convenient than the affine transform method, and it does work in GDK mode. (Because GDK mode uses Xlib primitives, it's nontrivial to implement arbitrary affine transformations; a future version of Gnome may do so, however.)

         #include <libgnomeui/gnome-canvas.h>
        

void gnome_canvas_set_pixels_per_unit(GnomeCanvas* canvas, double ppu);

Figure 7. Canvas Zooming

Canvas Items

Most of the time you will be interested in canvas items rather than the canvas itself. Canvas items are typically very easy to use, compared to widgets; none of the standard items have any unique signals, since they are not interactive. (Since GnomeCanvasItem is a subclass of GtkObject, however, you could certainly have an item with signals if you wanted to.) The GnomeCanvasItem base class has a single signal, "event", which is used to convey all types of event. The "event" signal has no default handler; canvas items do not respond to events unless you connect handlers of your own. Figure 8 lists all the useful functions for working with the GnomeCanvasItem base class.

To create a canvas item, you use the generic gnome_canvas_item_new() (or gnome_canvas_item_newv()). This function accepts the group to place the item in, the GtkType of the GnomeCanvasItem subclass to create, and finally a NULL-terminated list of arguments to set. The argument list is purely for convenience, so you don't have to call gnome_canvas_item_set() immediately. gnome_canvas_item_new() creates a new instance of the type with gtk_type_new(), adds the item to its GnomeCanvasGroup, and schedules it to be redrawn.

To destroy an item and remove it from the canvas, simply call gtk_object_destroy(). You can also use the standard reference counting mechanism with canvas items.

You can set an item's affine using gnome_canvas_item_affine_absolute(), or compose a new affine with the item's existing affine using gnome_canvas_item_affine_relative(). These functions can be used to translate, scale, or rotate a canvas item (however, scaling and rotation only work in antialiased mode).

Items in a group are normally stacked in the order you add them, with the most recently-added item "on top" and the oldest item on the bottom. You can manipulate the stacking order with gnome_canvas_item_raise() and gnome_canvas_item_lower(). These move an item up or down by the given number of positions. It is safe to pass in a too-large value for positions; the item will be moved as far as possible and no more. You can also request that an item is moved to one extreme or the other, using gnome_canvas_item_raise_to_top() and gnome_canvas_item_lower_to_bottom.

Items can be shown and hidden; hidden items are not rendered by the canvas and do not receive events. All items are visible by default. The routines are gnome_canvas_item_show() and gnome_canvas_item_hide().

Reparenting a canvas item is straightforward; the only rule is that the new group must be on the same canvas as the old group.

gnome_canvas_item_grab_focus() is analagous to gtk_widget_grab_focus(); it sends all key events to the item with the grab. It also sends focus change events to the item (when the item gains or loses the focus).

Canvas items can grab and ungrab the mouse pointer just as a GdkWindow can; the arguments to gnome_canvas_item_grab() are exactly analagous to those of gdk_pointer_grab() (see the chapter called GDK Basics). While a canvas item has the pointer grabbed, no other item receives events. Behind the scenes, GnomeCanvas uses gdk_pointer_grab() to implement gnome_canvas_item_grab(), so an item grabbing the mouse away from other items implies the canvas grabbing the mouse away from other widgets.

The visual properties of canvas items are manipulated almost entirely via object arguments. If you skipped the chapter called The GTK+ Object and Type System, go back and read the section on object arguments now. Two functions are used to set canvas item properties: gnome_canvas_item_set() and gnome_canvas_item_setv(). These are almost but not quite equivalent to gtk_object_set() and gtk_object_setv()---they set object arguments in the same way, but they also mark the canvas item to be redrawn. So you should prefer them to the GtkObject variants. (This is something of a design bug, and future canvas versions will most likely allow you to use gtk_object_set().)

gnome_canvas_item_request_update() marks the canvas item as "dirty" and queues it to be redrawn. Internally, the canvas uses a one-shot idle function to perform redraws; that is, it waits until no more GTK+ events are pending, then redraws itself a single time. It does this by installing an idle function with gtk_idle_add() and removing it after it runs once. Thus gnome_canvas_item_request_update() can be called many times without creating an efficiency problem---it pretty much does nothing at all if an update is already pending.

       #include <libgnomeui/gnome-canvas.h>
      

GnomeCanvasItem* gnome_canvas_item_new(GnomeCanvasGroup* parent, GtkType type, const gchar* first_arg_name, ...);

GnomeCanvasItem* gnome_canvas_item_newv(GnomeCanvasGroup* parent, GtkType type, guint nargs, GtkArg* args);

void gnome_canvas_item_set(GnomeCanvasItem* item, const gchar* first_arg_name, ...);

void gnome_canvas_item_setv(GnomeCanvasItem* item, guint nargs, GtkArg* args);

void gnome_canvas_item_affine_relative(GnomeCanvasItem* item, const double affine[6]);

void gnome_canvas_item_affine_absolute(GnomeCanvasItem* item, const double affine[6]);

void gnome_canvas_item_raise(GnomeCanvasItem* item, int positions);

void gnome_canvas_item_lower(GnomeCanvasItem* item, int positions);

void gnome_canvas_item_raise_to_top(GnomeCanvasItem* item);

void gnome_canvas_item_lower_to_bottom(GnomeCanvasItem* item);

void gnome_canvas_item_show(GnomeCanvasItem* item);

void gnome_canvas_item_hide(GnomeCanvasItem* item);

void gnome_canvas_item_reparent(GnomeCanvasItem* item, GnomeCanvasGroup* new_group);

void gnome_canvas_item_grab_focus(GnomeCanvasItem* item);

int gnome_canvas_item_grab(GnomeCanvasItem* item, unsigned int event_mask, GdkCursor* cursor, guint32 etime);

void gnome_canvas_item_ungrab(GnomeCanvasItem* item, guint32 etime);

void gnome_canvas_item_get_bounds(GnomeCanvasItem* item, double* x1, double* y1, double* x2, double* y2);

void gnome_canvas_item_request_update(GnomeCanvasItem* item);

Figure 8. Using GnomeCanvasItem

Canvas Items and Events

The standard Gnome canvas items have only one signal, "event", which is emitted for all types of event. The canvas widget preprocesses all GDK events that it receives, and forwards some of them to canvas items. It also sythesizes certain events. Remember that X sends events only to X windows (GdkWindows), and canvas items do not have an associated GdkWindow. Thus the canvas widget must act as intermediary. Here are some of the actions it takes:

The canvas does this work behind the scenes, so item events work intuitively and much like normal GDK events.

A canvas item event callback looks like this:


static gint
item_event_callback(GnomeCanvasItem* item, 
                    GdkEvent* event, 
                    gpointer data)
{
  switch (event->type) {
    case GDK_BUTTON_PRESS:
      break;

    case GDK_MOTION_NOTIFY:
      break;

    case GDK_BUTTON_RELEASE:
      break;

    default:
      break;
  }

  /* Returning FALSE propagates the event to parent items;
   * returning TRUE ends event propagation. 
   */
  return FALSE;
}

      

Of course, a real callback would probably examine the contents of the event and take some action in response to some of them.

A Canvas Example

This section gives a brief example program, demonstrating the user of the canvas. It does not explain the particulars of the canvas items being created; see the section called Standard Canvas Item Reference for that. Figure 9 shows the example program in action. You can drag canvas items around the screen with the left mouse button; clicking an item with the Shift key held down destroys it.

Figure 9. Simple GnomeCanvas program

Here is the code to create an antialiased canvas. Notice the call to gdk_rgb_init(); notice that the canvas's scroll region is set; finally, notice that the GdkRGB colormap and visual are pushed when creating the canvas.


#include <gnome.h>

static gint delete_event_cb(GtkWidget* window, GdkEventAny* e, gpointer data);
static void create_canvas_items(GtkWidget* canvas);

int 
main(int argc, char* argv[])
{
  GtkWidget* window;
  GtkWidget* sw;
  GtkWidget* canvas;

  gnome_init("canvas-example", "0.0", argc, argv);  

  gdk_rgb_init();

  window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  gtk_window_set_title(GTK_WINDOW(window), "Canvas Example");

  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);

  sw = gtk_scrolled_window_new(NULL, NULL);

  gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
                                 GTK_POLICY_AUTOMATIC,
                                 GTK_POLICY_AUTOMATIC);


  gtk_widget_push_visual(gdk_rgb_get_visual());
  gtk_widget_push_colormap(gdk_rgb_get_cmap());
  canvas = gnome_canvas_new_aa();
  gtk_widget_pop_colormap();
  gtk_widget_pop_visual();

  gnome_canvas_set_scroll_region(GNOME_CANVAS(canvas), 0, 0, 600, 450);

  create_canvas_items(canvas);

  gtk_container_add(GTK_CONTAINER(sw), canvas);
  gtk_container_add(GTK_CONTAINER(window), sw);

  gtk_window_set_default_size(GTK_WINDOW(window), 300, 300);

  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

static gint 
delete_event_cb(GtkWidget* window, GdkEventAny* e, gpointer data)
{
  gtk_main_quit();
  return FALSE;
}
      

Once the canvas has been created, the program adds some items to it, and connects a simple callback to the item's "event" signal. Here's the code:


static gint
item_event(GnomeCanvasItem *item, GdkEvent *event, gpointer data)
{
  static double x, y;
  double new_x, new_y;
  GdkCursor *fleur;
  static int dragging;
  double item_x, item_y;

  item_x = event->button.x;
  item_y = event->button.y;
  gnome_canvas_item_w2i(item->parent, &item_x, &item_y);

  switch (event->type) 
    {
    case GDK_BUTTON_PRESS:
      switch(event->button.button) 
        {
        case 1:
          if (event->button.state & GDK_SHIFT_MASK)
            {
              gtk_object_destroy(GTK_OBJECT(item));
            }
          else 
            {
              x = item_x;
              y = item_y;
              
              fleur = gdk_cursor_new(GDK_FLEUR);
              gnome_canvas_item_grab(item,
                                     GDK_POINTER_MOTION_MASK | 
                                     GDK_BUTTON_RELEASE_MASK,
                                     fleur,
                                     event->button.time);
              gdk_cursor_destroy(fleur);
              dragging = TRUE;
            }
          break;

        default:
          break;
        }
      break;

    case GDK_MOTION_NOTIFY:
      if (dragging && (event->motion.state & GDK_BUTTON1_MASK)) 
        {
          new_x = item_x;
          new_y = item_y;
            
          gnome_canvas_item_move(item, new_x - x, new_y - y);
          x = new_x;
          y = new_y;
        }
      break;
          
    case GDK_BUTTON_RELEASE:
      gnome_canvas_item_ungrab(item, event->button.time);
      dragging = FALSE;
      break;
          
    default:
      break;
    }
        
  return FALSE;
}

static void
setup_item(GnomeCanvasItem *item)
{
  gtk_signal_connect(GTK_OBJECT(item), "event",
                     (GtkSignalFunc) item_event,
                     NULL);
}

static void 
create_canvas_items(GtkWidget* canvas)
{
  GnomeCanvasPoints* points;
  GnomeCanvasGroup* group;
  GnomeCanvasItem* item;
  double affine[6];

  group = gnome_canvas_root(GNOME_CANVAS(canvas));

  /* A polygon */
  points = gnome_canvas_points_new(14);

  points->coords[0] = 270.0;
  points->coords[1] = 330.0;
  points->coords[2] = 270.0;
  points->coords[3] = 430.0;
  points->coords[4] = 390.0;
  points->coords[5] = 430.0;
  points->coords[6] = 390.0;
  points->coords[7] = 330.0;
  points->coords[8] = 310.0;
  points->coords[9] = 330.0;
  points->coords[10] = 310.0;
  points->coords[11] = 390.0;
  points->coords[12] = 350.0;
  points->coords[13] = 390.0;
  points->coords[14] = 350.0;
  points->coords[15] = 370.0;
  points->coords[16] = 330.0;
  points->coords[17] = 370.0;
  points->coords[18] = 330.0;
  points->coords[19] = 350.0;
  points->coords[20] = 370.0;
  points->coords[21] = 350.0;
  points->coords[22] = 370.0;
  points->coords[23] = 410.0;
  points->coords[24] = 290.0;
  points->coords[25] = 410.0;
  points->coords[26] = 290.0;
  points->coords[27] = 330.0;

  item = gnome_canvas_item_new(group,
                               gnome_canvas_polygon_get_type (),
                               "points", points,
                               "fill_color", "tan",
                               "outline_color", "black",
                               "width_units", 3.0,
                               NULL);

  setup_item(item);

  gnome_canvas_points_unref(points);

  /* Translate the polygon */

  art_affine_translate(affine, -150.0, -300.0);

  gnome_canvas_item_affine_relative(item, affine);

  /* A translucent rectangle */
  setup_item (gnome_canvas_item_new (group,
                                     gnome_canvas_rect_get_type(),
                                     "x1", 90.0,
                                     "y1", 40.0,
                                     "x2", 180.0,
                                     "y2", 100.0,
                                     "fill_color_rgba", 0x3cb37180,
                                     "outline_color", "black",
                                     "width_units", 4.0,
                                     NULL));

  /* A translucent ellipse */
  setup_item (gnome_canvas_item_new (group,
                                     gnome_canvas_ellipse_get_type(),
                                     "x1", 210.0,
                                     "y1", 80.0,
                                     "x2", 280.0,
                                     "y2", 140.0,
                                     "fill_color_rgba", 0x5f9ea080,
                                     "outline_color", "black",
                                     "width_pixels", 0,
                                     NULL));

  /* Create ellipses arranged in a line; they're manipulated as a
     single item. */

  group = 
    GNOME_CANVAS_GROUP (gnome_canvas_item_new (group,
                                               gnome_canvas_group_get_type(),
                                               "x", 0.0,
                                               "y", 0.0,
                                               NULL));
  setup_item(GNOME_CANVAS_ITEM(group));

  {
    double xpos = 20.0;
    while (xpos < 300.0)
      {
        gnome_canvas_item_new(group,
                              gnome_canvas_ellipse_get_type(),
                              "x1", xpos,
                              "y1", 100.0,
                              "x2", xpos + 10.0,
                              "y2", 110.0,
                              "fill_color_rgba", 0x0000FFFF,
                              "outline_color_rgba", 0xFF,
                              NULL);
        xpos += 15.0;
      }
  }
}