Using Cairo With wxPython
Update: wxPython versions 2.8.9 and later now include the wx.lib.wxcairo module, which provides an easier means of cairo integration (including cross-platform support, for example).
Introduction
wxPython has a new wxGraphicsContext ("wxGC") class (and related classes) which provide a nice, modern paths-based drawing API. On wxMSW, this uses GDIPlus calls for rendering, on wxGTK, cairo is used. wxGraphicsContext is nice 'n all, but cairo itself provides a lot more flexibility than is exposed in the wxGraphicsContext interface. It would be nice to be able to draw on wx-windows using the cairo API via the python-cairo bindings (pycairo). This wiki-page shows how this can be done.
A few of the extra things you can do with cairo include switching anti-aliasing off/on (for performance reasons) and rendering to a host of other targets like SVG, PDF, Bitmap etc.
What Objects are Involved
Besides wxPython, this requires ctypes and pycairo. I've tested it using Python-2.5, wxPythonGTK-2.8.7.1 and pycairo-1.4.7. The method demonstrated here only works with wxGTK. It is possible to install cairo/pycairo on Windows, in which case a cairo context could be constructed. I havn't attempted this yet.
Process Overview
The wxGC provides a GetNativeContext() method which returns a pointer to the native cairo_t structure (on wxGTK anyway). This is wrapped into a SWIG-Object by wxPython. Our task is to extract the raw pointer from the swig-object and, using ctypes, wrap it into a Pycairo_Context python object which pycairo can manipulate. The pycairo module provides "hand-coded" wrappers for the cairo structures into python extension types.
Cairo contexts are reference counted objects. The Pycairo_Context wrapper will decrement the context ref-count when it is destroyed, or if allocation fails. Thus, we must increment the context ref-count whenever we pass it into a Pycairo_Context object, to balance this out. The wxGC would also hold references to the context. Adding references to the context requires a ctypes call to libcairo, since this function is not exposed in pycairo. The Context_FromSWIGObject function is the useful thing here; pass it the output from gc.GetNativeContext() and it returns a Pycairo_Context object exposing the cairo API. Note, you can mix wxGC and cairo drawing calls. This is handy because wxGC does offer some nice features of it's own, like the DrawLines() method, for easy/fast polyline drawing. It will be good when more of the dc.DrawXXXList methods of wxDC is ported across to wxGraphicsContext.
The Code...
1 import ctypes
2 import cairo
3 from ctypes.util import find_library
4
5 cairo_dll = ctypes.CDLL(find_library("cairo"))
6
7 # Pycairo's API representation (from pycairo.h)
8 class Pycairo_CAPI(ctypes.Structure):
9 _fields_ = [
10 ('Context_Type', ctypes.py_object),
11 ('Context_FromContext', ctypes.PYFUNCTYPE(ctypes.py_object,
12 ctypes.c_void_p,
13 ctypes.py_object,
14 ctypes.py_object)),
15 ('FontFace_Type', ctypes.py_object),
16 ('FontFace_FromFontFace', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)),
17 ('FontOptions_Type', ctypes.py_object),
18 ('FontOptions_FromFontOptions', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)),
19 ('Matrix_Type', ctypes.py_object),
20 ('Matrix_FromMatrix', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)),
21 ('Path_Type', ctypes.py_object),
22 ('Path_FromPath', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)),
23 ('Pattern_Type', ctypes.py_object),
24 ('SolidPattern_Type', ctypes.py_object),
25 ('SurfacePattern_Type', ctypes.py_object),
26 ('Gradient_Type', ctypes.py_object),
27 ('LinearGradient_Type', ctypes.py_object),
28 ('RadialGradient_Type', ctypes.py_object),
29 ('Pattern_FromPattern', ctypes.c_void_p),
30 ('ScaledFont_Type', ctypes.py_object),
31 ('ScaledFont_FromScaledFont', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)),
32 ('Surface_Type', ctypes.py_object),
33 ('ImageSurface_Type', ctypes.py_object),
34 ('PDFSurface_Type', ctypes.py_object),
35 ('PSSurface_Type', ctypes.py_object),
36 ('SVGSurface_Type', ctypes.py_object),
37 ('Win32Surface_Type', ctypes.py_object),
38 ('XlibSurface_Type', ctypes.py_object),
39 ('Surface_FromSurface', ctypes.PYFUNCTYPE(ctypes.py_object, ctypes.c_void_p)),
40 ('Check_Status', ctypes.PYFUNCTYPE(ctypes.c_int, ctypes.c_int))]
41
42 # look up the API
43 ctypes.pythonapi.PyCObject_Import.restype = ctypes.POINTER(Pycairo_CAPI)
44 pycairo_api = ctypes.pythonapi.PyCObject_Import("cairo", "CAPI").contents;
45
46 ContextType = pycairo_api.Context_Type
47
48 def Context_FromSWIGObject(swigObj):
49 ptr = ctypes.c_void_p(int(swigObj))
50 #increment the native context's ref count, since the Pycairo_Context decrements it
51 #when it is finalised.
52 cairo_dll.cairo_reference(ptr)
53 return pycairo_api.Context_FromContext(ptr, ContextType, None)
54
55 if __name__=="__main__":
56 import wx
57 import math
58
59 class myframe(wx.Frame):
60 def __init__(self):
61 wx.Frame.__init__(self, None, -1, "test", size=(500,400))
62 self.Bind(wx.EVT_PAINT, self.OnPaint)
63
64 def OnPaint(self, event):
65 dc = wx.PaintDC(self)
66 w,h = dc.GetSizeTuple()
67 gc = wx.GraphicsContext.Create(dc)
68 nc = gc.GetNativeContext()
69 ctx = Context_FromSWIGObject(nc)
70
71 #wxGC drawing calls
72 gc.SetPen(wx.Pen("navy", 2))
73 gc.SetBrush(wx.Brush("pink"))
74 gc.DrawRectangle(w/4.,h/4.,w/2.,h/2.)
75
76 #cairo drawing calls
77 ctx.arc(2.*w/3,2.*h/3.,min(w,h)/4. - 10,0, math.pi*2)
78 ctx.set_source_rgba(0,1,1,0.5)
79 ctx.fill_preserve()
80 ctx.set_source_rgb(1,0.5,0)
81 ctx.stroke()
82
83 app = wx.App()
84 f = myframe()
85 f.Show()
86 app.MainLoop()
Comments
...