Armed with a text editor

mu's views on program and recipe! design

Cairo Tutorial for PyGTK Programmers

by Michael Urman

Cairo is a powerful 2d graphics library. This document leverages what you've learned about cairo, either in my Cairo Tutorial or elsewhere, and shows you ways to apply it in PyGTK.

In order to follow along on your computer, you need the following things:

  1. Cairo itself,
  2. Python to run the code snippets,
  3. PyCairo to join the previous two, and
  4. PyGTK for hosting the application.

Alternately, if you're up for a challenge, you can translate the examples to your preferred language and host environment and only need cairo from above.

We'll start with a simple PyGTK framework hosting a simple example. If you can run this with results like shown, you're good to go.

Cairo Tutorial Python Framework

#! /usr/bin/env python
import pygtk
pygtk.require('2.0')
import gtk, gobject, cairo

# Create a GTK+ widget on which we will draw using Cairo
class Screen(gtk.DrawingArea):

    # Draw in response to an expose-event
    __gsignals__ = { "expose-event": "override" }

    # Handle the expose-event by drawing
    def do_expose_event(self, event):

        # Create the cairo context
        cr = self.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        cr.rectangle(event.area.x, event.area.y,
                event.area.width, event.area.height)
        cr.clip()

        self.draw(cr, *self.window.get_size())

    def draw(self, cr, width, height):
        # Fill the background with gray
        cr.set_source_rgb(0.5, 0.5, 0.5)
        cr.rectangle(0, 0, width, height)
        cr.fill()

# GTK mumbo-jumbo to show the widget in a window and quit when it's closed
def run(Widget):
    window = gtk.Window()
    window.connect("delete-event", gtk.main_quit)
    widget = Widget()
    widget.show()
    window.add(widget)
    window.present()
    gtk.main()

if __name__ == "__main__":
    run(Screen)

Stroke, Fill, and Transform

First let's override the draw function of the Screen class, and show off some simple shapes with cairo's stroke and fill operations. If you read the code above, you've already seen set_source_rgb, rectangle and fill. The following code also uses arc, rel_line_to, move_to, and stroke. I describe those in my Cairo Tutorial; here let's see the code and results. To run it yourself, make sure to download it into the same folder as framework.py from above. Alternately you can take the contents of the draw() method and paste them over the ones in the framework.

Cairo Tutorial: Shapes

#! /usr/bin/env python
import framework
from math import pi

class Shapes(framework.Screen):
    def draw(self, cr, width, height):
        cr.set_source_rgb(0.5, 0.5, 0.5)
        cr.rectangle(0, 0, width, height)
        cr.fill()

        # draw a rectangle
        cr.set_source_rgb(1.0, 1.0, 1.0)
        cr.rectangle(10, 10, width - 20, height - 20)
        cr.fill()

        # draw lines
        cr.set_source_rgb(0.0, 0.0, 0.8)
        cr.move_to(width / 3.0, height / 3.0)
        cr.rel_line_to(0, height / 6.0)
        cr.move_to(2 * width / 3.0, height / 3.0)
        cr.rel_line_to(0, height / 6.0)
        cr.stroke()

        # and a circle
        cr.set_source_rgb(1.0, 0.0, 0.0)
        radius = min(width, height)
        cr.arc(width / 2.0, height / 2.0, radius / 2.0 - 20, 0, 2 * pi)
        cr.stroke()
        cr.arc(width / 2.0, height / 2.0, radius / 3.0 - 10, pi / 3, 2 * pi / 3)
        cr.stroke()

framework.run(Shapes)

Let's examine what we just did. We filled the whole background with grey. Then on top of that we drew a white rectangle. We drew two blue lines as eyes, a full arc as a circle for the head, and a partial arc as a smiling mouth. If you resize the window, you'll see that everything scales with it because of all the fractional math. As the window gets smaller, height / 3.0 gets smaller. But it only works for some window sizes. As things get too small, or too disproportionate, the eyes or even the mouth are drawn outside the head.

Cairo gives us a good way to correct this known as transforms. For instance the next variant sets up a user coordinate space on the white rectangle of (0, 0) to (1, 1). To figure out where a point will be shown, read the transformations backwards and apply them. A point in the center at (0.5, 0.5) gets scaled up by width and height, then translated (moved) over by the 20 pixel offset, and finally drawn.

Cairo Tutorial: Transform

#! /usr/bin/env python
import framework
from math import pi

class Transform(framework.Screen):
    def draw(self, cr, width, height):
        cr.set_source_rgb(0.5, 0.5, 0.5)
        cr.rectangle(0, 0, width, height)
        cr.fill()

        # draw a rectangle
        cr.set_source_rgb(1.0, 1.0, 1.0)
        cr.rectangle(10, 10, width - 20, height - 20)
        cr.fill()

        # set up a transform so that (0,0) to (1,1)
        # maps to (20, 20) to (width - 40, height - 40)
        cr.translate(20, 20)
        cr.scale((width - 40) / 1.0, (height - 40) / 1.0)

        # draw lines
        cr.set_line_width(0.01)
        cr.set_source_rgb(0.0, 0.0, 0.8)
        cr.move_to(1 / 3.0, 1 / 3.0)
        cr.rel_line_to(0, 1 / 6.0)
        cr.move_to(2 / 3.0, 1 / 3.0)
        cr.rel_line_to(0, 1 / 6.0)
        cr.stroke()

        # and a circle
        cr.set_source_rgb(1.0, 0.0, 0.0)
        radius = 1
        cr.arc(0.5, 0.5, 0.5, 0, 2 * pi)
        cr.stroke()
        cr.arc(0.5, 0.5, 0.33, pi / 3, 2 * pi / 3)
        cr.stroke()

framework.run(Transform)

I like this code better. I find it a lot easier to think in terms of some set size rather than in fractions of width and height. If you're trying to make an image that scales with your window, I'd suggest using a transform like this. Note that this one scales differently as you resize the window. Instead of scaling a circular head to fit the center of the rectangle, it stretches and squashes the circle into various ovals.

I made one other important change for this draw correctly: set_line_width. Cairo defaults to a line width of 2.0, but since that value is a user-space size it creates lines wider than the approximately 1.0 units wide view we just set up. By trimming it down to 0.01, it draws something much more like I had in mind. What width would you use to make it look like the 2.0 width it had before?