Writing a Widget Using Cairo and PyGTK 2.8, Part 2

This article is a Python version of the brand new article titled Writing a Widget Using Cairo and GTK+2.8, Part 2 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.

You can find the first part of this article here: Writing a Widget Using Cairo and PyGTK 2.8

Step 3. Making it Tick

Making the clock run is as simple as starting a timer that calls a callback. However, we might also want to be able to set a different time on our clock, so we'll store the time for the clock inside the widget. We don't want to let people change the time directly, in object-orientation speak, we want to make the time variable private.

Python, in its "trust the programmer" philosophy usually use a convention (the single "_" underscore) to identify a variable as private. I implemented the public API as a property in this way:

# public access to the time member
def _get_time(self):
    return self._time
def _set_time(self, datetime):
    self._time = datetime
    self.redraw_canvas()
time = property(_get_time, _set_time)

Now you can externally access the time property as if it's a simple object member.

We can use the property in the update() method which will update the clock with the new time:

def update(self):
    # update the time
    self.time = datetime.now()

    return True # keep running this event

Notice that this method returns a boolean value (true). Functions passed as timeout events return a boolean value. If the value is true, the event will be run again; if the value is false it will not. There is also a method that we haven't defined yet, redraw_canvas(). This method will redraw the canvas for us. From the manual page for GtkDrawingArea (our parent class), we are told to use gdk.Window.invalidate_rect(), to reexpose the canvas (and cause it to redraw). In order to make our event happen now, we should also call gdk.Window.process_all_updates(). Our redraw function looks like this:

def redraw_canvas(self):
    if self.window:
        alloc = self.get_allocation()
        rect = gdk.Rectangle(0, 0, alloc.width, alloc.height)
        self.window.invalidate_rect(rect, True)
        self.window.process_updates(True)

Drawing the hands requires us to think about a little geometry. For the hour hand, the hand is rotated around 30� for each hour and then a 1/2� more per minute.

cairo_clock_geometry

So to draw the hour hand, we might do something like:

context.save()
context.set_line_width(2.5 * context.get_line_width())
context.move_to(x, y)
context.line_to(x + radius / 2 * math.sin(
       math.pi / 6 * hours + math.pi / 360 * minutes),
                        y + radius / 2 * -math.cos(
       math.pi / 6 * hours + math.pi / 360 * minutes))
context.stroke()
context.restore()

The minute hand and the seconds hand each rotate 6° per minute/second. The minute hand is easily implemented as:

context.move_to(x, y)
context.line_to(x + radius * 0.75 * math.sin(math.pi / 30 * minutes),
                       y + radius * 0.75 * -math.cos(math.pi / 30 * minutes))
context.stroke()

Finally, we need to set it running, in __init__() we will add our timeout function:

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

    # make it private
    self._time = None

    self.update()
    # update the clock once a second
    gobject.timeout_add(1000, self.update)

We're left with clock_ex4.py which you can run with:

$ python clock_ex4.py

and should look like this:

animated clock example

Extra: Making the Picture Tick

The animated GIF of the clock ticking was done with a tool called byzanz. I simply recorded 60 seconds of the clock. In order to find out the window location for byzanz-record, I added this to the main function after gtk.Widget.show_all():

rect = gdk.Rectangle()
rect = window.window.get_frame_extents()
print "-x %i -y %i -w %i -h %i" % (rect.x, rect.y, rect.width, rect.height)

This printed settings that I could paste onto my other command line:

$ byzanz-record -d 60 $GEOMETRY -l clock.gif

Step 4: Emitting Signals

So far we've written a GObject with opaque property storage and we've used Cairo to draw our clock face. However the GTK+ widgets we commonly interact with also offer public APIs and emit signals to notify us when certain events take place. We will add a signal to say when someone is dragging the minute hand around.

Firstly we need to decide on what our signal is going to send and add this our file. We will implement a "time_changed" signal that along with the object also gives the time in hours and minutes that the clock has now been set to. If we were connecting this signal, our callback would look something like this:

def time_changed_cb(widget, hours, minutes):
    pass

Finally we need to register our signal in the class:

__gsignals__ = dict(time_changed=(gobject.SIGNAL_RUN_FIRST,
                                                        gobject.TYPE_NONE,
                                                        (gobject.TYPE_INT, gobject.TYPE_INT)))

More information on gobject.signal_new can be found in the documentation.

Next we have to implement a button_press_event handler so that we can determine when someone has actually clicked on a hand. We can override the signals for button_press_event, button_release_event and motion_notify_handler at the same time as replacing the expose_event. In __init()__:

self.connect("expose_event", self.expose)
self.connect("button_press_event", self.button_press)
self.connect("button_release_event", self.button_release)
self.connect("motion_notify_event", self.motion_notify)

From reading the documentation for GtkDrawingArea, our parent class, you will see that button events and motion events are masked out, so we will need to set them so that we receive events for processing. We need to do this for each EggClockFace, so in __init__() we'll add:

self.add_events(gdk.BUTTON_PRESS_MASK |
                        gdk.BUTTON_RELEASE_MASK |
                        gdk.POINTER_MOTION_MASK)

The line formed by the bearing of the hand is infinitely thin, so we can't expect a user to be able to click on it. It would be nice to detect if the user clicked within 5 pixels of the line. To do this we require some more geometry.

We know that (sin φ, cos φ) is a point on the unit circle, that is it has magnitude 1. Thus a vector from the origin to (sin φ, cos φ) will be a unit vector, we will name it l. We will also take a vector p which is the vector from the origin to the point where the user clicked.

This would give vector components equal to:

px = event.x - widget.get_allocation().width / 2
py = widget.get_allocation().height / 2 - event.y
lx = math.sin(math.pi / 30 * minutes)
ly = math.cos(math.pi / 30 * minutes)

Simple reasoning will tell us that there exists a point ul where l is perpendicular to (ulp) (which is the shortest distance between the point and the line and is what we want to measure).

We can project p onto l using the dot product such that u = p.l. The dot product can be done mathematically like so:

u = lx * px + ly * py

cairo_clock_geometry_2

If u comes out to be a negative number we'll ignore it, this means that the user clicked on the opposite side of the clock to where the hand is. Finally, the magnitude of the distance can be found. If the magnitude of the distance (squared) is less then 5 pixels (squared) we can set the private member _dragging to be true (we used squared values here because we have no need to do a slow sqrt()).

if u < 0:
    return False

d2 = math.pow(px - u * lx, 2) + math.pow(py - u * ly, 2)
if d2 < 25: # 5 pixels away from the line
    self._dragging = True

We now need to implement handlers for the motion_notify_event and button_release_event. Both of these signal handlers share a lot of their code, so we can move it out into another method, emit_time_changed_signal(). The geometry for this method is simply the reverse of the geometry that we used to draw the hands on the clock face.

def emit_time_changed_signal(self, x, y):
    # decode the minute hand
    # normalize the coordinates around the origin
    x -= self.get_allocation().width / 2
    y -= self.get_allocation().height / 2

    # phi is a bearing from north clockwise, use the same geometry as we
    # did to position the minute hand originally
    phi = math.atan2(x, -y)
    if phi < 0:
        phi += math.pi * 2

    hour = self.time.hour
    minute = phi * 30 / math.pi

    # update the offset
    self._minute_offset = minute - self.time.minute
    self.redraw_canvas()

    self.emit("time_changed", hour, minute)

The time_changed signal is actually sent to all listeners by emit(). You may also notice the variable _minute_offset, we use this to know how far out of phase the minute hand is with the current time. This offset has to be added to any other requests for the current time. I will leave it as an exercise to the reader to implement a similar offset for the hour hand.

After all of this our two signals handlers from above are simply:

def motion_notify(self, widget, event):
    if self._dragging:
        self.emit_time_changed_signal(event.x, event.y)

def button_release(self, widget, event):
    if self._dragging:
        self._dragging = False
        self.emit_time_changed_signal(event.x, event.y)

    return False

Of course, in order to find out when our signal is emitted, we only need to connect a signal handler to the clock in main():

clock.connect("time_changed", time_changed_cb)

def time_changed_cb(widget, hours, minutes):
    print "::time-changed - %02i:%02i" % (hours, minutes)

Putting it all together, you should have clock_ex5.py.

That's it! You now know how to implement a GObject, draw things inside that GObject, add private data, add signals and animate the object. This forms pretty much everything you need to write your own GtkWidget.

UPDATE: thanks to Johan Dahlin for the hints on __gsignals__.