2011-11-18

Analog Clock in PyGTK

I was searching the web for some PyGTK tutorial and came across this one to build an analog clock.  However only the clock face is done.  The hour, minute and second hands are yet to be implemented.


Then I came across this tutorial which implements the clock hands but it is done in gtkmm (C++).  So I translated the C++ code to PyGTK in the draw() method:


# compute the angles in radians of the hands of the clock
now = datetime.datetime.now()
hours = now.hour * math.pi / 6
minutes = now.minute * math.pi / 30
seconds = now.second * math.pi / 30

context.set_line_cap(cairo.LINE_CAP_ROUND)

# draw the hours hand
context.save()
context.set_source_rgba(0.337, 0.612, 0.117, 0.9) # green
context.set_line_width(7)
context.move_to(x, y)
context.line_to(x + math.sin(hours + minutes/12) * (radius * 0.5),
                y - math.cos(hours + minutes/12) * (radius * 0.5))
context.stroke()
context.restore()

# draw the minutes hand
context.save()
context.set_source_rgba(0.117, 0.337, 0.612, 0.9) # blue
context.set_line_width(5)
context.move_to(x, y)
context.line_to(x + math.sin(minutes + seconds/60) * (radius * 0.8),
                y - math.cos(minutes + seconds/60) * (radius * 0.8))
context.stroke()
context.restore()

# draw the seconds hand
context.save()
context.set_source_rgba(0.7, 0.7, 0.7, 0.8) # gray
context.set_line_width(3)
context.move_to(x, y)
context.line_to(x + math.sin(seconds) * (radius * 0.9),
                y - math.cos(seconds) * (radius * 0.9))
context.stroke()
context.restore()



At this point the clock hands are not moving yet.  We need a timer to move the clock hands every second.  I need some extra help to translate the gtkmm timer code to PyGTK, and this tutorial just did that.  What it does is to set up a timer in __init__():

gobject.timeout_add_seconds(1, self.on_timeout)

to invalidate the clock face every second:

def on_timeout(self):
    win = self.get_window()
    rect = self.get_allocation()
    win.invalidate_rect(rect, False)
    return True


Now we have an analog clock written in PyGTK.  Here is the complete source code:

import cairo
import datetime
import gobject
import gtk
import math

class ClockFace(gtk.DrawingArea):
    def __init__(self):
        gtk.DrawingArea.__init__(self)
        self.connect("expose_event", self.expose)
        gobject.timeout_add_seconds(1, self.on_timeout)
       
    def expose(self, widget, event):
        self.context = widget.window.cairo_create()
        self.context.rectangle(event.area.x, event.area.y,
                               event.area.width, event.area.height)
        self.context.clip()
        self.draw(self.context)
        return False

    def on_timeout(self):
        win = self.get_window()
        rect = self.get_allocation()
        win.invalidate_rect(rect, False)
        return True

    def draw(self, context):
        rect = self.get_allocation()
        x = rect.x + rect.width / 2
        y = rect.y + rect.height / 2
        radius = min(rect.width / 2, rect.height / 2) - 5

        # clock background
        context.arc(x, y, radius, 0, 2 * math.pi)
        context.set_source_rgb(1, 1, 1)
        context.fill_preserve()
        context.set_source_rgb(0, 0, 0)
        context.stroke()

        # clock ticks
        for i in range(12):
            context.save()
            if i % 3 == 0:
                inset = 0.2 * radius
            else:
                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))
            context.stroke()
            context.restore()

        # compute the angles in radians of the hands of the clock
        now = datetime.datetime.now()
        hours = now.hour * math.pi / 6
        minutes = now.minute * math.pi / 30
        seconds = now.second * math.pi / 30

        context.set_line_cap(cairo.LINE_CAP_ROUND)

        # draw the hours hand
        context.save()
        context.set_source_rgba(0.337, 0.612, 0.117, 0.9) # green
        context.set_line_width(7)
        context.move_to(x, y)
        context.line_to(x + math.sin(hours + minutes/12) * (radius * 0.5),
                        y - math.cos(hours + minutes/12) * (radius * 0.5))
        context.stroke()
        context.restore()

        # draw the minutes hand
        context.save()
        context.set_source_rgba(0.117, 0.337, 0.612, 0.9) # blue
        context.set_line_width(5)
        context.move_to(x, y)
        context.line_to(x + math.sin(minutes + seconds/60) * (radius * 0.8),
                        y - math.cos(minutes + seconds/60) * (radius * 0.8))
        context.stroke()
        context.restore()

        # draw the seconds hand
        context.save()
        context.set_source_rgba(0.7, 0.7, 0.7, 0.8) # gray
        context.set_line_width(3)
        context.move_to(x, y)
        context.line_to(x + math.sin(seconds) * (radius * 0.9),
                        y - math.cos(seconds) * (radius * 0.9))
        context.stroke()
        context.restore()

def main():
    window = gtk.Window()
    clock = ClockFace()

    window.add(clock)
    window.connect("destroy", gtk.main_quit)
    window.show_all()

    gtk.main()

if __name__ == '__main__':
    main()

No comments: