Drawing with the Drawing Area
The drawing area is the widget for custom graphics: a blank surface that receives an expose-event when it needs painting, and the handler draws with Cairo - the 2D graphics library GTK uses for all its rendering. The model is the double-buffered redraw: the area is not a canvas you draw on once, it is a region that the toolkit tells you to repaint (the expose-event, the queue_draw), and the paint handler redraws the entire visible region from the app's state. This chapter covers the drawing area's setup (the event mask, the size request), the expose handler and the Cairo context, the drawing primitives (paths, fills, text), the redraw triggers (queue_draw, the expose), and a complete example - the animated clock face that redraws on a timer.
Setting Up the Drawing Area
import gtk
import gdk
# the drawing area - the blank surface:
area = gtk.DrawingArea()
# the size: the area requests the size it wants (the toolkit
# gives it at least this, more if the container allows)
area.set_size_request(400, 300)
# the event mask: expose is default; motion/keys are opt-in
area.set_events(gdk.EventMask.BUTTON_PRESS_MASK |
gdk.EventMask.POINTER_MOTION_MASK)
# the expose-event is the paint request - connect the handler
area.connect("expose-event", on_expose)
area.show()
# a drawing area has NO background of its own in the sense of
# a widget draw - the expose handler paints EVERYTHING that
# is visible, every time
The DrawingArea is the blank surface, and its setup has two decisions. The size request - set_size_request(400, 300) asks the layout for at least that many pixels, and the container gives it more if the space allows, so the drawing code reads the actual allocated size (area.allocation.width/height) rather than assuming the request. The event mask - the expose event (the paint request) is delivered by default, but any input events the drawing reacts to (the button press for a click position, the motion for a drag) must be in the mask. The critical mental model: the drawing area is not a persistent canvas. It is a region the toolkit owns, and when it needs to show its contents (first map, a resize, another window moved away from it, an explicit queue_draw) it emits expose-event and the handler repaints the whole visible region from the app's current state. There is no "the pixels I drew last time" - there is only the state and the repaint.
The Expose Handler and Cairo
import gtk
import cairo
def on_expose(area, event):
# the Cairo context for the area's surface:
cr = area.window.cairo_create()
# the region to repaint: the expose event's area
# (repainting the whole allocation is the simple, correct form)
w, h = area.allocation.width, area.allocation.height
# clear: paint the background
cr.set_source_rgb(1, 1, 1)
cr.rectangle(0, 0, w, h)
cr.fill()
# a path: move, line, close, fill
cr.set_source_rgb(0.1, 0.4, 0.8)
cr.move_to(w * 0.2, h * 0.7)
cr.line_to(w * 0.5, h * 0.2)
cr.line_to(w * 0.8, h * 0.7)
cr.close_path()
cr.fill()
# text: the Cairo show_text (the simple form)
cr.set_source_rgb(0, 0, 0)
cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(18)
cr.move_to(20, h - 20)
cr.show_text("the drawing area, repainted from state")
return False # the default expose handling proceeds
# the handler is a pure function of the state: it reads the
# app's data (the shape, the text, the clock angle) and draws
# it - the same state always produces the same picture
The expose handler is the paint function, and it gets the Cairo context from the area's window - area.window.cairo_create() - which is the same 2D graphics API GTK uses internally for all its widget rendering. The drawing vocabulary is Cairo's: set_source_rgb for the color, the path operations (move_to, line_to, curve_to, close_path) for the shape, fill and stroke for painting it, select_font_face and show_text for the text. The discipline is the one the paragraph above states: the handler is a pure function of the app's state - it reads the data (the shapes to draw, the text, the clock's current angle) and renders it, and the same state always produces the same picture. That purity is what makes the double-buffered model work: the repaint can happen any time, and it always shows the current state, because the state is the source and the pixels are a derived, disposable view of it.
Triggering a Redraw
import gtk
# the state changed - tell the area to repaint:
def update_angle(angle):
state["angle"] = angle
area.queue_draw() # schedule the expose-event
# queue_draw() (or queue_draw_area(x, y, w, h) for a sub-region)
# does NOT paint immediately - it marks the region dirty and the
# toolkit coalesces the repaint into the next event-loop pass,
# so a burst of state changes costs one repaint, not one per change
# the timer-driven example: the clock ticks, the state changes,
# the queue_draw schedules the repaint:
def tick():
state["angle"] += 6 # the hand advances
area.queue_draw()
return True # True: the timeout repeats
# the alternative for a sub-region: repaint only the changed part
# area.queue_draw_area(old_x, old_y, old_w, old_h)
The redraw is triggered by queue_draw() - the call that marks the area (or a sub-region, via queue_draw_area) dirty and schedules the expose-event on the next event-loop pass. The key property is the coalescing: queue_draw does not paint immediately, it requests a repaint, and the toolkit batches the request so a burst of state changes (a drag that updates the position ten times per frame) costs one repaint, not ten. The pattern for any animated or interactive drawing is the two-step: the state changes (the timer advances the clock angle, the drag updates the position), and queue_draw schedules the repaint, and the expose handler renders the new state. The timer example is the complete loop - the timeout_add that returns True repeats, each tick advances the state and queues the draw, and the expose handler paints the advanced hand - which is the architecture for every animated widget the toolkit's drawing model supports.
The Complete Example: The Animated Clock
import gtk
import math
class ClockArea(gtk.DrawingArea):
def __init__(self):
gtk.DrawingArea.__init__(self)
self.set_size_request(300, 300)
self.angle = 0.0
self.connect("expose-event", self.on_expose)
def on_expose(self, area, event):
cr = area.window.cairo_create()
w, h = area.allocation.width, area.allocation.height
cx, cy, r = w / 2.0, h / 2.0, min(w, h) / 2.0 - 10
# the face
cr.set_source_rgb(1, 1, 1)
cr.arc(cx, cy, r, 0, 2 * math.pi)
cr.fill_preserve()
cr.set_source_rgb(0.1, 0.1, 0.1)
cr.set_line_width(3)
cr.stroke()
# the hand: from the center, at the current angle
cr.move_to(cx, cy)
cr.line_to(cx + r * 0.8 * math.cos(self.angle),
cy - r * 0.8 * math.sin(self.angle))
cr.stroke()
return False
def tick(self):
self.angle += 0.1
self.queue_draw()
return True
def main():
win = gtk.Window()
clock = ClockArea()
win.add(clock)
win.connect("delete-event", lambda *a: gtk.main_quit())
win.show_all()
gtk.timeout_add(50, clock.tick) # 20 fps
gtk.main()
if __name__ == "__main__":
main()
The complete example is the architecture in miniature: a DrawingArea subclass whose state (the angle) is the data, whose expose handler renders the face and the hand as a function of that state, and whose tick (driven by the repeating timeout_add) advances the state and queues the redraw. The clock hand's position is computed from the angle each repaint - cos/sin from the center at the hand's length - and the 50-millisecond timer gives the 20 frames per second the motion reads as smooth. The structure generalizes: any animated or interactive custom widget is a drawing area with a state, a paint handler that is a pure function of the state, and the state-change-plus-queue_draw as the update step. The drawing area is where the toolkit's standard widgets end and the app's own rendering begins, and this three-part structure is the bridge between them.
Summary
The drawing area is the custom-graphics widget: a blank surface that the toolkit repaints on demand, not a persistent canvas. The setup is the size request and the event mask (the expose is default, the input events are opt-in), and the expose handler is the paint function - it takes the Cairo context from the area's window, clears the region, and draws the app's state with Cairo's primitives (the path operations, the fill and stroke, the text). The redraw is triggered by queue_draw, which schedules the expose on the next loop pass and coalesces bursts of state changes into one repaint, and the animated example - the clock with its state, its pure paint handler, and the timer that advances the state and queues the draw - is the complete architecture. The discipline is the one that makes it work: the handler is a pure function of the state, the state is the source, and the pixels are a disposable view repainted whenever the toolkit asks.