Writing a Widget Using Cairo and GTK+2.8

Since version 2.8, GTK+ renders many of its interface widgets with Cairo, a powerful vector graphics library. Davyd Madeley explains how you can implement your own GTK+ widget using Cairo for the actual drawing.

Cairo is a powerful 2-dimensional graphics library designed to support a number of modern graphics techniques including stroking, alpha blending and antialiasing. It supports multiple output formats, which allows developers to use the same code to display graphics on the screen, print them to the printer or accellerate them with OpenGL.

As of GTK+ 2.8, GTK+ has been integrated with the Cairo 1.0 rendering library, giving developers access to this flexible graphics API.

Chapter 1 of this article looks at the more mundane work required to implement a complete GTK+ widget that uses Cairo. Chapter 2 looks at using Cairo to do actual drawing. If you simply want to draw inside an existing widget or simple GtkDrawingArea with Cairo, you can skip straight to Chapter 2. Later chapters will cover emitting signals from your widget and some of the features in the Cairo drawing API.

Step 1. Writing a GObject

While it is possible to simply start using Cairo drawing to draw inside a drawing area, many times you will want to be able to write your own custom widget that you can use over and over again. Much of this chapter can be used for the development of many widgets, not just ones using Cairo.

The first step to writing your own custom widget is creating a new GObject to use with that widget. GObjects can get quite complicated, but we’re going to look at the basics for writing our widget.

GTK+ is an object-oriented environment. This means we have two data structures that we need to worry about: the class and the instance. The instance is the data structure that we are most familar with, it is the GtkWidget struct that we pass around inside our program.

Classes also inherit from other classes. Since we are writing a widget that will be drawn on with Cairo, it will be easiest to inherit GtkDrawingArea, which inherits GtkWidget, GtkObject and finally GObject. GtkDrawingArea already implements a lot of functions we need for our widget and will save us from writing a lot of code.

The class is initialised only once, whereas an instance is initialised every time you create a new copy of the widget. If this is confusing or you are not incredibly familiar with object-orientated programming, don’t worry; we will go into depth with what you can do here in a later chapter.

In clock.c you’ll want to define our new object type:


 G_DEFINE_TYPE (EggClockFace, egg_clock_face, GTK_TYPE_DRAWING_AREA);

We will call our object EggClockFace and preface all of our function calls with egg_clock_face. We are inheriting the type of GtkDrawingArea.

We now need to define structs for our new class and for instances of that class. In our example these structs are quite simple. If we wanted any publicly accessibly variables we could add them here. Private variables are defined in another struct (we’ll get to that). So, in clock.h:

 typedef struct _EggClockFace EggClockFace; typedef struct _EggClockFaceClass EggClockFaceClass; struct _EggClockFace { GtkDrawingArea parent; /* private */ }; struct _EggClockFaceClass { GtkDrawingAreaClass parent_class; }; 

There is also some boilerplate that is required in the header file for a new class. This allows all of those convienience macros that you see when using GTK+ widgets. Without further explanation, this should go at the top of clock.h:

 #define EGG_TYPE_CLOCK_FACE (egg_clock_face_get_type ()) #define EGG_CLOCK_FACE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), EGG_TYPE_CLOCK_FACE, EggClockFace)) #define EGG_CLOCK_FACE_CLASS(obj) (G_TYPE_CHECK_CLASS_CAST ((obj), EGG_CLOCK_FACE, EggClockFaceClass)) #define EGG_IS_CLOCK_FACE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EGG_TYPE_CLOCK_FACE)) #define EGG_IS_CLOCK_FACE_CLASS(obj) (G_TYPE_CHECK_CLASS_TYPE ((obj), EGG_TYPE_CLOCK_FACE)) #define EGG_CLOCK_FACE_GET_CLASS (G_TYPE_INSTANCE_GET_CLASS ((obj), EGG_TYPE_CLOCK_FACE, EggClockFaceClass)) 

When the class is initialised the function egg_clock_face_class_init() is going to be called. This gives us a chance to set certain class wide functions and properties in our new class. Since we’re doing drawing, we’re going to need to override the parent’s expose handler. So in clock.c:

 static void egg_clock_face_class_init (EggClockFaceClass *class) { GtkWidgetClass *widget_class; widget_class = GTK_WIDGET_CLASS (class); widget_class->expose_event = egg_clock_face_expose; } 

Later on we can also use the class_init function to add a struct of private variables to each instance using g_type_class_add_private().

When someone creates a new EggClockFace, egg_clock_face_init() will be called. We don’t need to worry about constructing the GtkWidget or allocating any memory, that has already been handled for us. For the moment, let’s just keep the function empty.

 static void egg_clock_face_init (EggClockFace *clock) { } 

You’ll notice that all of these functions are static. You should remember that these functions are all internal to how the object works and aren’t meant to be exposed in a public API.

We do need a way to create an object though, so let’s add egg_clock_face_new().

 GtkWidget * egg_clock_face_new (void) { return g_object_new (EGG_TYPE_CLOCK_FACE, NULL); } 

As you can see, this function is simply a wrapper for convienience.

Finally, to make it compile, we’re going to need to define our expose handler:

 static gboolean egg_clock_face_expose (GtkWidget *clock, GdkEventExpose *event) { return FALSE; } 

You’ll also want to write a main() function so you can see something. This is what I have in main.c:

 int main (int argc, char **argv) { GtkWidget *window; GtkWidget *clock; gtk_init (&argc, &argv); window = gtk_window_new (GTK_WINDOW_TOPLEVEL); clock = egg_clock_face_new (); gtk_container_add (GTK_CONTAINER (window), clock); g_signal_connect (window, "destroy", G_CALLBACK (gtk_main_quit), NULL); gtk_widget_show_all (window); gtk_main (); } 

So, if you’ve made it this far. You should have files that look like clock-ex1.c, clock.h, and main.c.

You can compile it with the simple line:

 gcc -o clock `pkg-config --cflags --libs gtk+-2.0` clock.h clock-ex1.c main.c 

And it should look something like this:

Pretty neat, huh? Well… maybe not yet. Now we need to draw something in there!

Step 2. Drawing with Cairo

When things need drawing in GTK+ an “expose-event” will be emitted. As you’ll recall from Step 1, we put the stub for an expose handler in our code. When an expose event occurs, GTK+ will also give us other information, including the area of the widget that we need to redraw. All of this information is contained within the GdkEventExpose struct.

In order to do drawing with Cairo we need a Cairo context (called a cairo_t). We can get a cairo_t for a GdkWindow (this is what you’re drawing into). You should be aware that a GdkWindow is not like a GtkWindow and that all sorts of widgets have one or more GdkWindows inside them for doing drawing. If you can’t keep track of all the names, don’t worry too much, you’ll get the hang of it eventually.

You can access the GdkWindow of most widgets (such as a GtkDrawingArea) by accessing the window member of the widget struct, eg. widget->window. So to get our cairo_t we can extend our expose stub to look like this:

 static gboolean egg_clock_face_expose (GtkWidget *clock, GdkEventExpose *event) { cairo_t *cr; /* get a cairo_t */ cr = gdk_cairo_create (clock->window); draw (clock, cr); cairo_destroy (cr); return FALSE; } 

This will redraw the entire widget on each expose event. To make things faster we might like to set a clip region.

 static gboolean egg_clock_face_expose (GtkWidget *clock, GdkEventExpose *event) { cairo_t *cr; /* get a cairo_t */ cr = gdk_cairo_create (clock->window); /* set a clip region for the expose event */ cairo_rectangle (cr, event->area.x, event->area.y, event->area.width, event->area.height); cairo_clip (cr); draw (clock, cr); cairo_destroy (cr); return FALSE; } 

Now we actually need to draw something. Drawing instructions in Cairo work by describing paths and then stroking them. Think of it like tracing your design out with a pencil and then inking it in with a pen. You can choose a variety of different pens with different nibs and colors, but each stroking is done with a particular pen. You can also do other actions like filling a path with a solid color as well as being able to preserve a path so that you can stroke around it after you fill it.

Firstly, we’re trying to draw a clock face, so we need to do a little bit of simple geometry. The size of our canvas is stored in the widget struct as the member ‘allocation’. Therefore, we can find out the center of our canvas (x, y) pretty easily:

 double x, y; x = clock->allocation.x + clock->allocation.width / 2; y = clock->allocation.y + clock->allocation.height / 2; 

Since we want to draw a circle, we can work out the biggest radius that we’re
able to draw based on the size of our canvas:

 double radius; radius = MIN (clock->allocation.width / 2, clock->allocation.height / 2) - 5; 

To draw the clock face we want to describe an arc centered at (x, y) and sweep
between 0 and 2? radians.

 cairo_arc (cr, x, y, radius, 0, 2 * M_PI); 

We then want to fill that circle with white and then stroke around that white circle with a black outline.

 cairo_set_source_rgb (cr, 1, 1, 1); cairo_fill_preserve (cr); cairo_set_source_rgb (cr, 0, 0, 0); cairo_stroke (cr); 

It will look rather like this:

We can also add a marker for each hour on the clock face. Using a bit of geometry again, we know that we need to divide 2? into 12 pieces, which means that each line is ?/6 radians apart.

We want to draw a line from just inside the circle to the edge of the circle. We can define the path for a line using cairo_move_to() and cairo_line_to().

 for (i = 0; i < 12; i++) { int inset; inset = 0.1 * radius; cairo_move_to (cr, x + (radius - inset) * cos (i * M_PI / 6), y + (radius - inset) * sin (i * M_PI / 6)); cairo_line_to (cr, x + radius * cos (i * M_PI / 6), y + radius * sin (i * M_PI / 6)); cairo_stroke (cr); } 

We could also mark the tick for every quarter more strongly than the hour marks.

 for (i = 0; i < 12; i++) { int inset; cairo_save (cr); /* save pen size to stack */ if (i % 3 == 0) { inset = 0.2 * radius; } else { inset = 0.1 * radius; cairo_set_line_width (cr, 0.5 * cairo_get_line_width (cr)); } cairo_move_to (cr, x + (radius - inset) * cos (i * M_PI / 6), y + (radius - inset) * sin (i * M_PI / 6)); cairo_line_to (cr, x + radius * cos (i * M_PI / 6), y + radius * sin (i * M_PI / 6)); cairo_stroke (cr); cairo_restore (cr); /* recover pen size from stack */ } 

You can see here that we’ve used two new functions cario_save() and cairo_restore(). These functions allow us to manipulate a stack of cairo states. To save our old values for the pen width, we simply put it on the stack. We can then modify that width and stroke some paths. Finally we can simply restore the old state again once we’re ready. This is an easy way to not have to keep track of your existing drawing defaults throughout each stage of a drawing operation.

Your C file should look like clock-ex3.c with the new drawing instructions.

If you compile it with

 gcc -o clock `pkg-config --cflags --libs gtk+-2.0` clock.h clock-ex3.c main.c 

you will get a blank clock face:

So we’ve covered the basics of drawing something on your canvas and making sure that GTK+ redraws it when it is exposed. Next time we’ll look at how to extend your widget with an API so that we can actually start drawing content on it and implementing signals so that we can receive updated information.

If you want to learn more about Cairo and the Cairo drawing API, see their website: http://www.cairographics.org/

Discuss this story with other readers on the GNOME forums.

Advertisements

Posted on December 2, 2005, in December 2005. Bookmark the permalink. Leave a comment.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: