Mono.Cairo
Cairo is a low-level 2D vector drawing library. Various rendering backends (XRender, Win32) are already supported and more (ie. glitz – OpenGL hardware accelerated backend) are on the way. The Mono.Cairo.dll assembly exposes the Cairo API to managed applications. The mapping is a pretty straightforward one, and the programming model is very close to the OpenGL model (although much simpler)
More Resources
- Mono.Cairo Tutorial - An in-depth guide to getting started with Cairo and Mono.
- Mono.Cairo Cookbook - Some short recipes to help spice up your Cairo usage.
Using Cairo with Gtk/Gdk
A core class that does all the drawing is the Cairo.Context
class. A Context
is always attached to a Surface
. Surface
can be window on the screen, an off-screen buffer or a static file on the disk.
To use Cairo in Gdk/Gtk applications, a Context
with a Gdk.Drawable
target surface is needed. It can be obtained using two methods:
- Beginning in Gtk# 2.8, the Gdk.CairoHelper class provides cairo access to Gdk drawables. You can create a cairo context for a given drawable:
Cairo.Context context = Gdk.CairoHelper.Create(drawable);
- A second choice (recommended for older versions of Gtk) is to use a function provided in the Mono.Cairo samples. This will work on all platforms and with older Gdk versions. The code in question resides in sysdraw.cs and you can simply download & use this file in your project.
The best place to create and use the Context
is the ExposeEvent for the given widget. Usually you’ll want to use the Gtk.DrawingArea for this task. An example implementation of the Expose event method:
void OnDrawingAreaExposed (object o, ExposeEventArgs args)
{
DrawingArea area = (DrawingArea) o;
using (Cairo.Context g = Gdk.CairoHelper.Create (area.GdkWindow)) {
// Perform some drawing
g.GetTarget ().Dispose ();
}
}
Notice that Surface
(the target the Context
is actually drawing to), as well as the Context
itself are manually disposed before we leave the function. This is required for the time being since garbage collecting is not yet supported in Mono.Cairo.
Drawing simple primitives
Cairo drawing model works very much like a plotting machine. An abstract pen moves around the Surface
area drawing lines and curves. The basic functions to handle the “plotting” are: MoveTo, LineTo, CurveTo. These functions take PointD objects as the arguments. PointD
is a two-dimensional coordinate where X
and Y
are expressed as double
.
Context.MoveTo (PointD coordinate)
will position the cursor/pen at the given coordinateContext.LineTo (PointD coordinate)
will make a straight line from the current pen position to the given coordinate. After calling this function the pen is located at the given coordinate.Context.CurveTo (PointD coordinate1, PointD coordinate2, PointD coordinate3)
draws a curved line from the current pen position to the coordinate3 position. Coordinates 1 & 2 act as control points and affect the shape of the curve (Bezier).
Having this in mind, we can draw a square using the following instructions:
PointD p1,p2,p3,p4;
p1 = new PointD (10,10);
p2 = new PointD (100,10);
p3 = new PointD (100,100);
p4 = new PointD (10,100);
// g is a Graphics object
g.MoveTo (p1);
g.LineTo (p2);
g.LineTo (p3);
g.LineTo (p4);
g.LineTo (p1);
g.ClosePath ();
However, our plotter analogy ends here – nothing has been drawn to the surface yet. At this point we only have an outlined path, which we made sure is closed (thanks to ClosePath
). To actually draw the path we need to call the Stroke
method (g.Stroke ()
). To fill the path one would call g.Fill ()
.
Notice, that the Stroke
and Fill
methods don’t take any arguments. The color for the stroke/fill, pen width and other interesting parameters are taken from the Cairo.Context
at the moment of stroking/filling. In other words we can say that Context
class acts like a state machine.
Here is a complete implementation of the ExposeEvent
that draws a black square with a red border inside a Gtk.DrawingArea:
void OnDrawingAreaExposed (object o, ExposeEventArgs args)
{
DrawingArea area = (DrawingArea) o;
Cairo.Context g = Gdk.CairoHelper.Create (area.GdkWindow);
PointD p1,p2,p3,p4;
p1 = new PointD (10,10);
p2 = new PointD (100,10);
p3 = new PointD (100,100);
p4 = new PointD (10,100);
g.MoveTo (p1);
g.LineTo (p2);
g.LineTo (p3);
g.LineTo (p4);
g.LineTo (p1);
g.ClosePath ();
g.Color = new Color (0,0,0);
g.FillPreserve ();
g.Color = new Color (1,0,0);
g.Stroke ();
g.GetTarget ().Dispose ();
g.Dispose ();
}
I used FillPreserve
method instead of Fill
because the latter destroys the current path. If you want to keep the path use StrokePreserve
and FillPreserve
.
Take a look at the Graphics class members for other functions used to outline paths (ie. ArcTo
, Rectangle
).
Saving and restoring the Cairo state
As you have already noticed, most of the drawing parameters are controlled in a state-based manner. Various Graphics
properties you can set include:
- Color – to set the stroke/fill color. Color values (Red, Green, Blue, Alpha) are expressed in a 0 - 1 range (as
double
). - LineWidth – to control the width of the stroke line.
- LineCap – controls the line capping (round, square, etc.)
This state-based approach is far more convenient than specifying all drawing parameters in a single function call (like it’s done ie. in the low-level Gdk drawing methods). However, once you started creating your own custom drawing functions, you’ll notice that it’s hard to control all the state modifications spanned across multiple methods. In most cases you will not want to care about certain state modifiers assuming they’re unset.
Cairo provides us with methods to control the state stack. The respective Graphics
members are Save and Restore.
Context.Save
will copy the current state and push the copy on the top of the stack. Context.Restore
will pop one state back from the stack. Clearly all the state-altering calls placed inside Save
/Restore
parenthesis are local.
It’s a good programming practice to place all state modifications inside Save
/Restore
brackets. This way you can easily control the Cairo state and work on a “clean” (unpolluted) state in higher-level functions.
For example, you might create yourself the following function to draw a triangle:
void DrawTriangle (Cairo.Context g, double x, double y, bool fill)
{
g.Save ();
g.MoveTo (new PointD (x - 10, y + 10));
g.LineTo (new PointD (x, y - 10));
g.LineTo (new PointD (x + 10, y + 10));
g.LineTo (new PointD (x - 10, y + 10));
g.ClosePath ();
g.Color = new Color (1,0,0);
if (fill)
g.Fill ();
else
g.Stroke ();
g.Restore ();
}
In this example, the state we have when entering the function remains same when leaving the function. The Color
modification we perform is local.
Filling shapes with gradients
In most cases the Color
property is used as the fill/stroke solid color. You can, however, use a pattern as the fill/stroke pen. Pattern
can be a gradient, a bitmap or a surface part. To draw using a pattern, you need to set the Graphics.Pattern
property to a valid Pattern
object. I will discuss only gradients here.
A handy subclass of the Pattern
class is the Gradient
class. Moreover, Gradient
has two more sub-classes – LinearGradient
and RadialGradient
. Gradients are built using the color-stops – a concept known from image editing software.
To create a simple black-to-white gradient you might write:
Cairo.Gradient pat = new Cairo.LinearGradient (0, 0, 100, 100);
pat.AddColorStop (0, new Cairo.Color (0,0,0));
pat.AddColorStop (1, new Cairo.Color (1,1,1));
gr.Pattern = pat;
The four parameters (double) given to LinearGradient
constructor specify a gradient vector (x0, y0, x1, y1). The first parameter given to AddColorStop
is the offset (0 to 1). The point located at x0, y0 will have the color where offset = 0, while point located at x1,y1 will have a color of offset = 1. Other values are interpolated between the two. You can add as many color stops as you need. Bear in mind that colorstops can be used as a method to perform alpha blending (just use the four-params constructor for Color
).
Here is a sample ExposeEvent
that draws some smooth shapes.
void OnDrawingAreaExposed (object o, ExposeEventArgs args)
{
DrawingArea area = (DrawingArea) o;
Cairo.Context g = Gdk.CairoHelper.Create (area.GdkWindow);
// Shape
g.MoveTo (new PointD (100,200));
g.CurveTo (new PointD (100,100), new PointD (100,100),
new PointD (200,100));
g.CurveTo (new PointD (200,200), new PointD (200,200),
new PointD (100,200));
g.ClosePath ();
// Save the state to restore it later. That will NOT save the path
g.Save ();
Cairo.Gradient pat = new Cairo.LinearGradient (100,200, 200, 100);
pat.AddColorStop (0, new Cairo.Color (0,0,0,1));
pat.AddColorStop (1, new Cairo.Color (1,0,0,1));
g.Pattern = pat;
// Fill the path with pattern
g.FillPreserve ();
// We "undo" the pattern setting here
g.Restore ();
// Color for the stroke
g.Color = new Color (0,0,0);
g.LineWidth = 3;
g.Stroke ();
g.GetTarget().Dispose ();
g.Dispose ();
}
Hints
- Don’t try to keep
Cairo.Context
across multiple expose events, this will not work due to double-buffering - Don’t try to use Cairo in threads other than the main (Gdk) thread.
- If you need to draw sharp (crisp) 1 pixel lines, add 0.5 to the coordinates. This is related to how the anti-aliasing works. Normally a 1px line is drawn “between” two pixels, which means that two points get 1/2 of the color value (blur)
- Color (and Alpha) values are expressed in a 0-1 range not 0-255 range.
- Don’t forget to manually dispose the
Context
and the targetSurface
at the end of the expose event. Automatic garbage collecting is not yet 100% working in Cairo.