June 1999

Drawing with metafiles

by Thomas Wieland

All graphic control components in the VCL provide the Canvas property a drawing surface for objects that draw images of themselves. In contrast to the window controls, Windows doesn't repaint such controls automatically, when, for example, the window pops up from the background. If you want to keep the content of the canvas highly dynamic, your program has to do the necessary bookkeeping of the drawing commands itself. In this article, we'll explain how to solve this problem by the metafile mechanism that easily records the necessary commandsjust like a VCR such that you only have to replay the tape, so to speak, to (re)paint the graphics.

Drawing on a canvas

The simplest graphical control is the PaintBox. It consists of hardly anything other than the canvas. In a window containing this component, you can make any drawings you want using all the operations offered by the TCanvas class. But if your window is overlapped by another for a moment, your canvas will be empty afterwards, at least in the regions that were covered. Everything you've drawn there so far will disappear. Thus, when you work with a PaintBox, you must always provide a handler for the OnPaint event. This is an encapsulation of the Windows WM_PAINT message and occurs every time the box needs to be repainted. In the worst case, this can happen quite a number of times during the runtime of your application. If your image isn't static, but has to be constructed by some calculations or from the users input, for example, you should store the rules for the drawing. Then you can perform the repainting fast enough.

Windows metafiles

In general, to save some graphics (to disk or just to memory), there are two different ways. You can save them pixel by pixel and get a bitmap. However, if you only save the rules for the drawing, (that is, the shape, filling, coordinates, etc.) you'll have a vector graphic, also known as a metafile. In C++Builder, metafiles are represented by the TMetafile class, which we'll use here. The only question is how to get the drawing from a canvas into an object of this classand vice versa.

Drawing into metafiles

The trick is done by a second class called TMetafileCanvas. This builds the bridge between the canvas and the metafile. Instances of this class can paint the contents of a metafile on a real canvas, but are also able to store the commands that make the image. The usage consists of the following steps:
bullet Declare a variable of type TMetafile* in your form class. Create a metafile object at the beginning of your application, that is, in the constructor of the form:
	aMetafile = new TMetafile;
bullet To draw something that should be saved in the metafile, you must create a metafile canvas. The simplest way to do this is with the command:
	mfCanvas = new TMetafileCanvas(aMetafile, 0);
bullet Now you can draw objects on your mfCanvas just like on any other canvas. The differences are that the picture is only created in memory and not on the screen, and that all commands are recorded.

 

bullet After your drawing is complete, you should always add the command delete mfCanvas. The recording ends when the mfCanvas object is destroyed. This is also the moment when the information is transmitted to the metafile.

 

bullet Now you may replay the recorded operations easily. Every canvaslike the one in a PaintBoxhas a Draw method that takes the metafile and the coordinates of the upper-left corner and starts rendering. For example:
	PaintBox->Canvas->Draw(0, 0, aMetafile);
When following these steps, you always begin with an empty canvas and, thus, ignore everything that might already be in the metafile. To add something to an existing content, you first draw the metafile onto the mfCanvas object before drawing anything else.

Freehand drawing in a PaintBox

Now let's take a look at a little application that makes use of this concept. The program, called MetaDraw, allows users to draw lines on a free space. They can select the color, as well as save, load, and clear the drawing.

Figure A: The MetaDraw application comprises just a PaintBox and a ToolBar.
[ Figure A ]

We create the metafile object in the constructor of the main form, like we did in step 1. In addition, we initialize a couple of internal variables:

__fastcall TDrawForm::TDrawForm(
	TComponent* Owner) : TForm(Owner)
{
	x_start = 0;
	y_start = 0;
	mouse_down = false;
	cbColor->ItemIndex = 1;

	aMetafile = new TMetafile;
	aMetafile->Width = PaintBox->Width;
	aMetafile->Height = PaintBox->Height;
	Application->ShowHint = true;
}
The crucial events of the application are the pressing of the mouse button and the mouse movement. When the users press the button, we remember the position of the pointer and move the pen there. The following code illustrates this:
void __fastcall 
	TDrawForm::PaintBoxMouseDown(
	TObject *Sender, TMouseButton Button,
	TShiftState Shift, int X, int Y)
{
	// Remember position.
	x_start = X;
	y_start = Y;
	x_prev = x_start;
	y_prev = y_start;

	// Move pen to this position.
	PaintBox->Canvas->MoveTo(X,Y);

	// Remember pressing of mouse button.
	mouse_down = true;
}
When the users move the mouse pointerwhen the OnMouseEvent occurswe delete the current line and draw a new one, so they can always see what kind of line they just drew. As this can only happen during direct user interaction, we can paint directly on the form.

The interesting part comes when the users release the mouse button. Then we regard the present act of drawing as complete and save it to our metafile. The entire routine is given in Listing A; it follows the procedure proposed above.

Listing A: OnMouseUp event

void __fastcall 
TDrawForm::PaintBoxMouseUp
	(TObject *Sender, TMouseButton Button,
	 TShiftState Shift, int X, int Y)
{
	mouse_down = false;

	TColor col;
	switch (cbColor->ItemIndex)
	{
		case 0: col = clAqua;
				break;
		// ...
		// More colors
	}

	// Create a canvas to draw into the 
	// metafile.
	mfCanvas = new TMetafileCanvas(
	aMetafile, 0);

	// Draw existing lines on the canvas.
	mfCanvas->Draw(0,0,aMetafile);

	// Draw the line.
	mfCanvas->Pen->Color = col;
	mfCanvas->MoveTo(x_start,y_start);
	mfCanvas->LineTo(X,Y);

	// Free the canvas.
	delete mfCanvas;

	// Update the window.
	PaintBox->Invalidate();
	PaintBoxPaint(Sender);
}
First, we create the metafile canvas and draw all previously saved lines on it. Then, we set the desired color of the line and add the line to the drawing. Finally, we free the canvas to transmit the additions into the metafile and update the screen by setting it to invalid.

This command forces a repainting of the box. To be precise, it causes an OnPaint event in our C++Builder program. In the handler of this event, all we have to do is replay the contents of the metafile:

void __fastcall TDrawForm::PaintBoxPaint(TObject *Sender)
{
	PaintBox->Canvas->Draw(0,0,aMetafile);
}

Conclusion

The TMetafile class has many convenient features. Certainly most important is the recording and replaying of drawing commands. As you could see from the example, you just have to draw on a metafile canvas instead of the canvas of your graphical component and you can almost forget about organizing and saving your drawing commands. The complete example application shows you even more features of TMetafile. It is, for instance, also very easy to save and load the contents of the canvas to and from disk, respectivelyas the file in the class name already suggests. Go ahead and explore the possibilities to make your programs a little bit more colorful and unique.