Writing a Widget Using Cairo and PyGTK 2.8

This article is a Python version by Lawrence Oluyede of the brand new article titled Writing a Widget Using Cairo and GTK+2.8 available on The GNOME Journal website. It is written by Davyd Madeley

I’ll not make modifications to the text except for differences between C and Python.

Writing a Widget Using Cairo and PyGTK 2.8

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. PyGTK added Cairo support in the 2.8 version. See the wiki for further info.

Section 1 of this article looks at the more mundane work required to implement a complete PyGTK widget that uses Cairo. Section 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 Section 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.

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.

In clock.py you’ll want to define our new class:

class EggClockFace(gtk.DrawingArea):

We will call our object EggClockFace.

When the class is initialised the __init__() method is going to be called. Since we’re doing drawing, we’re going to need to override the parent’s expose handler. So in clock.py:

def __init__(self):
    self.connect("expose_event", self.expose)

Our expose_event event handler will be like this:

def expose(self, widget, event):
    return False

You’ll also want to write a main() function so you can see something:

def main():
    window = gtk.Window()
    clock = EggClockFace()
    window.connect("destroy", gtk.main_quit)

So, if you’ve made it this far. You should have a file that look like clock_ex1.py.

Run it with $ python clock_ex1.py

And it should look something like this:

clock window example

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 object.

In order to do drawing with Cairo we need a Cairo context. We can get it 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 object. So to get our cairo context we can extend our expose stub to look like this:

def expose(self, widget, event):
    self.context = widget.window.cairo_create()
    return False

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

def expose(self, widget, event):
    self.context = widget.window.cairo_create()
    # set a clip region for the expose event
    self.context.rectangle(event.area.x, event.area.y,
                           event.area.width, event.area.height)
    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 object as the member “allocation”. Therefore, we can find out the center of our canvas (x, y) pretty easily:

rect = self.get_allocation()
x = rect.x + rect.width / 2

y = rect.y + rect.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:

radius = min(rect.width / 2, rect.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.

context.arc(x, y, radius, 0, 2 * math.pi)

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

context.set_source_rgb(1, 1, 1)

context.set_source_rgb(0, 0, 0)

It will look rather like this:

clock window example, now with the clock surface painted

Your file at this point should look like clock_ex2.py.

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’s move_to() and line_to().

for i in xrange(12):
    inset = 0.1 * radius
    context.move_to(x + (radius - inset) * math.cos(i * math.pi / 6),
                               y + (radius - inset) * math.sin(i * math.pi / 6))
    context.line_to(x + radius * math.cos(i * math.pi / 6),
                            y + radius * math.sin(i * math.pi / 6))

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

for i in xrange(12):
    if i % 3 == 0:
        inset = 0.2 * radius
        inset = 0.1 * radius
        context.set_line_width(0.5 * context.get_line_width())
     context.move_to(x + (radius - inset) * math.cos(i * math.pi / 6),
                                y + (radius - inset) * math.sin(i * math.pi / 6))
     context.line_to(x + radius * math.cos(i * math.pi / 6),
                             y + radius * math.sin(i * math.pi / 6))

You can see here that we’ve used two new functions save() and 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 Python file should look like clock_ex3.py with the new drawing instructions.

If you run it with

$ python clock_ex3.py

you will get a blank clock face:

clock window example, now with the clock surface painted and ticks

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/.

This work is licensed under a Creative Commons License

– Friday, December 2nd 2005