Custom Widgets
A custom widget is a gtk.Widget subclass that draws its own content and handles its own input - the thermometer, the dial, the custom control the toolkit does not ship. In PyGTK the mechanism is the virtual-function override: the C widget's vfuncs (the expose_event, the size_allocate, the button_press_event) are overridable Python methods named do_expose_event, do_size_allocate, do_button_press_event, and the subclass implements the ones it needs. This chapter covers the subclass shape (the __init__, the do_expose_event), the size negotiation (the do_size_request/do_size_allocate), the input handling, and the complete example - the custom thermometer widget.
The Subclass Shape
import gtk
import cairo
class MyWidget(gtk.Widget):
def __init__(self):
# the base init - the Widget is created in the
# unstyled, undrawn state
gtk.Widget.__init__(self)
# the size: request a minimum (the layout honors it)
self.set_size_request(200, 40)
# the style: the widget has a GdkStyle the expose
# can read (the theme's colors, the state)
# and a window for drawing:
self.set_flags(gtk.WIDGET_DRAWING_AREA)
# the DRAWING_AREA flag: the widget has its own
# window, the expose is delivered to it
def do_expose_event(self, event):
# the vfunc override - the paint handler:
cr = self.window.cairo_create()
w, h = self.allocation.width, self.allocation.height
cr.set_source_rgb(0.9, 0.9, 0.9)
cr.rectangle(0, 0, w, h)
cr.fill()
cr.set_source_rgb(0.0, 0.3, 0.6)
cr.move_to(10, h / 2)
cr.line_to(w - 10, h / 2)
cr.stroke()
return False # propagate (the default expose handling)
# the vfunc naming: the C vfunc "expose_event" becomes the
# Python method "do_expose_event" - the "do_" prefix is the
# pygtk convention for the overridable virtual functions
# the other common overrides:
# do_size_allocate - the allocation was assigned
# do_button_press_event, do_key_press_event - the input
# do_draw (GTK3) - the render (the GTK3 spelling)
The custom widget is a gtk.Widget subclass, and the mechanism is the vfunc override: the C toolkit's virtual functions are exposed to Python as methods with the do_ prefix, and the subclass implements the ones it needs - the do_expose_event is the paint handler (the same Cairo drawing the drawing area used, but on the widget's own window), the do_size_allocate is the reaction to the layout assigning the widget its allocation, and the input vfuncs (do_button_press_event, do_key_press_event) are the event handlers. The __init__ calls the base gtk.Widget.__init__ first (the widget is created in the base state), then sets the widget's own state - the size request, the flags. The WIDGET_DRAWING_AREA flag is the key: it gives the widget its own window (the drawing surface) and routes the expose to it, which is what makes a plain Widget subclass drawable. The return False from the expose follows the event-signal contract - the default handling proceeds, the propagation is the normal case. The subclass is the extension point: the toolkit's widget is the base with its vfuncs, and the subclass is the app's behavior layered on the base's structure.
Size Negotiation
import gtk
class Sized(gtk.Widget):
def __init__(self):
gtk.Widget.__init__(self)
self.set_flags(gtk.WIDGET_DRAWING_AREA)
# the size request: how big the widget WANTS to be
# (the layout asks, the widget answers, the container
# decides the actual allocation)
def do_size_request(self, requisition):
# requisition is the out-parameter: the width and
# height the widget requests
requisition.width = 150
requisition.height = 60
# the size allocate: the layout has DECIDED the size -
# the widget is placed and sized, react to it
def do_size_allocate(self, allocation):
# allocation.x/y/width/height is the assigned box
# the widget's self.allocation is updated by the base;
# the override reacts (redraw, reposition internal state)
self.queue_draw()
# the simple alternative: set_size_request in __init__
# fixes the minimum; the do_size_request is for the
# DYNAMIC request (the size depends on the content)
The size negotiation is the two-phase contract between the widget and the layout. The request phase - the do_size_request override fills the requisition (the width and height the widget wants) - is the widget's answer to the layout's "how big do you need to be," and it is where a content-dependent size is computed (a label-sized-to-its-text, a widget sized to its data). The allocate phase - the do_size_allocate override, called when the layout has decided the widget's actual box - is the reaction: the widget's allocation is set by the base, and the override redraws (queue_draw) or repositions its internal state for the new size. The simple case is the set_size_request in the __init__ (the fixed minimum, the layout gives at least that), and the override is for the dynamic case where the request depends on the widget's content or state. The discipline: the request is the want, the allocation is the got, and the widget renders to the allocation (the self.allocation.width/height), never to the request - the window may be resized, the allocation changes, and the render follows the allocation.
Input Handling on the Custom Widget
import gtk
import gdk
class Clickable(gtk.Widget):
def __init__(self):
gtk.Widget.__init__(self)
self.set_flags(gtk.WIDGET_DRAWING_AREA)
self.set_can_focus(True)
# the event mask: the input events the widget receives
self.set_events(gdk.EventMask.BUTTON_PRESS_MASK |
gdk.EventMask.BUTTON_RELEASE_MASK |
gdk.EventMask.POINTER_MOTION_MASK)
self.value = 0.0
def do_button_press_event(self, event):
# event.x/y is the position in the widget
self.value = event.x / float(self.allocation.width)
self.queue_draw()
return True # handled: the press is consumed
def do_motion_notify_event(self, event):
self.value = event.x / float(self.allocation.width)
self.queue_draw()
return True
def do_expose_event(self, event):
cr = self.window.cairo_create()
w, h = self.allocation.width, self.allocation.height
cr.set_source_rgb(0.95, 0.95, 0.95)
cr.rectangle(0, 0, w, h)
cr.fill()
# the indicator at the current value:
x = self.value * w
cr.set_source_rgb(0.8, 0.2, 0.2)
cr.rectangle(x - 2, 0, 4, h)
cr.fill()
return False
# the input vfuncs follow the event contract: the return True
# consumes the event (no propagation), the state is updated
# and the queue_draw repaints - the same two-step as the
# drawing area, on the widget's own window
The input handling on a custom widget is the same contract as the event signals, on the widget's own vfuncs: the do_button_press_event and do_motion_notify_event overrides receive the GdkEvent (the event.x/y is the position in the widget's coordinates), update the widget's state, call queue_draw, and return True to consume the event. The setup is the set_can_focus (the widget can take the keyboard focus, for the key events) and the set_events mask (the opt-in to the button and motion events, the drawing chapter's rule). The interaction is the pointer-driven value - the press and the motion set the value from the x position, the expose renders the indicator at the value - which is the custom slider's whole behavior in three vfuncs. The pattern generalizes: any custom interactive widget is the state, the input vfuncs that update it (consuming the events), and the expose that renders it, on the widget's own window with the drawing-area flag and the event mask set.
The Complete Example: The Thermometer
import gtk
import cairo
class Thermometer(gtk.Widget):
"""A vertical thermometer: set the value, it renders."""
def __init__(self, lo=0.0, hi=100.0):
gtk.Widget.__init__(self)
self.set_flags(gtk.WIDGET_DRAWING_AREA)
self.set_size_request(60, 200)
self.lo, self.hi = lo, hi
self._value = lo
self.connect("configure-event", lambda w, e: self.queue_draw())
def set_value(self, v):
self._value = max(self.lo, min(self.hi, v))
self.queue_draw()
def get_value(self):
return self._value
def do_expose_event(self, event):
cr = self.window.cairo_create()
w, h = self.allocation.width, self.allocation.height
# the tube
cr.set_source_rgb(0.9, 0.9, 0.9)
cr.rectangle(10, 10, w - 20, h - 20)
cr.fill_preserve()
cr.set_source_rgb(0.3, 0.3, 0.3)
cr.stroke()
# the fill: from the bottom, to the value
frac = (self._value - self.lo) / (self.hi - self.lo)
fill_h = (h - 20) * frac
cr.set_source_rgb(0.85, 0.15, 0.15)
cr.rectangle(12, 12 + (h - 20) - fill_h,
w - 24, fill_h)
cr.fill()
return False
def main():
win = gtk.Window()
thermo = Thermometer()
win.add(thermo)
win.connect("delete-event", lambda *a: gtk.main_quit())
win.show_all()
import random
def tick():
thermo.set_value(random.random() * 100)
return True
gtk.timeout_add(200, tick)
gtk.main()
if __name__ == "__main__":
main()
The complete example is the custom widget as a component: the Thermometer is a Widget subclass with its own window (the drawing-area flag), a size request (the 60x200 tube), the set_value/get_value as the public API (the state, clamped to the range, the queue_draw on change), and the do_expose_event that renders the tube and the fill as a function of the value. The configure-event connection re-queues the draw on a resize (the allocation changed, the render must follow). The driver - the window with the thermometer, the delete-event quit, the timeout_add that randomizes the value every 200 milliseconds - shows the widget as a self-contained component with a clean API: the app sets the value, the widget renders it, and the render is the pure function of the state the drawing chapter's discipline requires. The custom widget is where the toolkit's vocabulary ends and the app's own controls begin, and the structure - the do_ vfuncs, the state, the render, the public API - is the pattern for every one of them.
The Working Rules
- Call the base
__init__first, then set the widget's state. Thegtk.Widget.__init__(self)creates the widget in the base state; the flags, the size, and the connections follow. - Set the
WIDGET_DRAWING_AREAflag for a drawable widget. The flag gives the widget its own window and routes the expose to it - without it, the plainWidgetsubclass has no surface to draw on. - Render to the allocation, not the request. The
self.allocation.width/heightis the size the layout assigned, and it changes on resize - the render follows the allocation, and theconfigure-event(or thedo_size_allocate) re-queues the draw. - The input vfuncs consume the events they handle (
return True) and update the state plusqueue_draw- the two-step, the same as the drawing area, on the widget's own window. - The public API is the state accessors. The
set_value/get_value(or the domain's equivalent) are the interface the app uses, and the render is the internal detail the API drives - the widget is a component with a state and a view of it.
Summary
The custom widget is a gtk.Widget subclass using the vfunc override: the do_expose_event is the paint handler (Cairo on the widget's own window), the do_size_request/do_size_allocate are the size negotiation (the want, then the got - the render follows the allocation), and the input vfuncs (do_button_press_event, the motion) are the event handlers that update the state and consume the events. The setup is the base __init__ first, the WIDGET_DRAWING_AREA flag for the drawing window, the size request, the set_can_focus and the event mask for the input, and the configure-event re-queue for the resize. The thermometer example is the pattern as a component - the state with its clamped set_value/get_value API, the expose that renders the tube and the fill as a pure function of the value, the driver that sets the value on a timer - and the discipline is the one the drawing chapter established: the state is the source, the render is the derived view, and the public API is the state accessors the app drives.