0057: Cairo I – The Basics of Drawing

Today we’re going to put the MVC series aside for a few weeks while we dive into another series on a topic near and dear to my heart… graphics. We’ll start simple and get more complicated as we go. But first, we need to look at some foundation stuff…

The Imports

To do anything with Cairo, you need to add these two lines to your list of import statements:

import cairo.Context;
import gtk.DrawingArea;

Perhaps surprisingly, you won’t need to add a lot more to this list for most Cairo operations.

The DrawingArea

Yup, it’s like an overhead projector. You prep whatever you’re going to show off to one side, then slap it onto the projector to shine it on the wall. Same thing here, and setting them up? Nothing to it:

class MyDrawingArea : DrawingArea
{
	//               R   G    B    Alpha
	float[] rgba = [0.3, 0.6, 0.2, 0.9];
	
	this()
	{
		addOnDraw(&onDraw);
		
	} // this()

Ignoring the rgbaColor array for the moment, to prepare a DrawingArea, you just:

  • instantiate it,
  • write a callback function and
  • hook up the onDraw signal to call the callback.

That’s it. But anything you plan to show with the DrawingArea ‘projector’ you’ll need to prep first. And you do that off-screen with the DrawingArea’s constant companion…

The Context

This is like a paste-up board. You take bits and pieces of images, shapes, lines, etc., mix in some color and build the image you want to show on the DrawingArea. When it’s ready, you call one of the Context’s many functions to slap your results onto the DrawingArea projector.

But to show this, we need an example, so let’s start with something simple like…

A Simple Fill

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

Let’s start by looking at the callback function… which is where the fill is done:

bool onDraw(Scoped!Context context, Widget w)
{
	context.setSourceRgba(rgba[0], rgba[1], rgba[2], rgba[3]);
	context.paint();

	return(true);
		
} // onDraw()

Here, all we do is:

  • set the color, and
  • fill the entire DrawingArea.

And that is Cairo drawing at its simplest.

A Technical Note: You’ll notice the onDraw() function’s first argument is Scoped!Context. Scoped! is a GtkD reflection of D’s scoped() function and all it means is that the Context will be destroyed automatically when we’re done with it. It has to be done this way because it’s created automatically.

Now let’s look at a second simple example…

Drawing a Line

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

The only thing different about this line-drawing example compared to the fill example is the callback function:

bool onDraw(Scoped!Context context, Widget w)
{
	context.setLineWidth(3);
	context.moveTo(100, 45);
	context.lineTo(249, 249);
	context.stroke();
		
	return(true);
		
} // onDraw()

In this operation, we do four things:

  • set the width of the line to draw,
  • move the ‘pen’ to where the line starts,
  • decide where the line will end, and
  • stroke it.

But what if you want to draw…

Multiple Lines with Rounded Ends

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

In this third, multi-line example, we just add a few preparatory commands to get the Context set up:

bool onDraw(Scoped!Context context, Widget w)
{
	context.setLineWidth(10);
	context.setLineCap(CairoLineCap.ROUND);	
	context.setLineJoin(CairoLineJoin.ROUND);
	context.moveTo(10, 10);
	context.lineTo(249, 249);
	context.lineTo(450, 15);
	context.lineTo(450, 249);
	context.stroke();
		
	return(true);
		
} // onDraw()

It’s mostly just more of what we’ve already been doing, but there are two new lines in there that make the ends and angles of the lines rounded.

So you know what all the options are for setLineCap(), here they are:

  • CairoLineCap.BUTT,
  • CairoLineCap.ROUND, and
  • CairoLineCap.SQUARE.

And the options for setLineJoin() are:

  • CairoLineJoin.MITER,
  • CairoLineJoin.ROUND, and
  • CairoLineJoin.BEVEL.

One other note: The set of lineTo() calls will draw a continuous line with three legs. If instead you want two separate lines, you would change the second lineTo() call to a moveTo() like this:

context.moveTo(10, 10);
context.lineTo(249, 249);
context.moveTo(450, 15);
context.lineTo(450, 249);
context.stroke();

Conclusion

So, to use a DrawingArea, we now know we need to instantiate it and hook up the onDraw signal. Also, we need a behind-the-scenes Context (automatically supplied in most cases) for preparing any drawing before it hits the DrawingArea.

Next time we continue with Cairo and look at rectangles. Until then…

Comments? Questions? Observations?

Did we miss a tidbit of information that would make this post even more informative? Let's talk about it in the comments.

You can also subscribe via RSS so you won't miss anything. Thank you very much for dropping by.

© Copyright 2023 Ron Tarrant