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:
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)
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?