| This chapter covers the basics of graphics programming in the
VCL. The VCL encapsulates the Windows Graphics Device Interface, or GDI. GDI
programming can be a subtle and dangerous process. The VCL tames this technology
and makes it extremely easy to use. In the following pages you will learn
about:
- The TCanvas object
- Painting shapes on the screen
- Working with colors
- Working with bitmaps
- Working with metafiles
- Drawing fractals
- Working with fonts
It's important to understand that the basic graphics functionality presented
here is a far cry from the sophisticated tools you find in the DirectX programs
seen in Chapter 28, "Game Programming." This is just the basic functionality
needed to present standard Windows programs. However, the material presented in
this chapter is useful in most standard Windows programs, and it is part of the
core knowledge that all VCL programmers should possess. In particular, it shows
how the VCL encapsulates and simplifies Windows GDI programming.
Graphics.Hpp
The core of the VCL graphics support is found in Graphics.Hpp. The
following objects can be found in this file:
| Object |
Description |
| TCanvas |
This is the basic graphics object used to paint shapes on a
form or other surface. It is the primary wrapper around the GDI. |
| TBrush |
This is the object used to designate the color or pattern
that fills in the center of shapes such as rectangles or ellipses. |
| TPen |
This is the object used for drawing lines, and as the
outline for rectangles or ellipses. |
| TPicture |
This is the generalized, high-level VCL wrapper around
"pictures" such as bitmaps, metafiles, or icons. |
| TMetaFileCanvas |
This is the drawing surface for a metafile. |
| TMetaFile |
This is the VCL wrapper around a windows metafile. |
| TBitmap |
This is the VCL wrapper around bitmaps. |
| TIcon |
This is the VCL wrapper around icons. |
| TGraphicsObject |
This is the base class for TBrush, TFont,
and TPen. |
| TGraphic |
This is the base class for TMetaFile, TBitmap,
and Ticon. |
Many of these objects are explored in the next few pages. In particular, I
demonstrate how to use bitmaps and pens and show how to draw basic geometric
shapes to the screen. You also see how to work with bitmaps and metafiles.
After the TFont object, the next most important graphics object in
the VCL is TCanvas. This is a wrapper around the Windows GDI, or
Graphics Device Interface. The GDI is the subsystem Windows programmers use to
paint pictures and other graphics objects. Most of the content of
Graphics.Hpp is aimed at finding ways to simplify the GDI so that it is
relatively easy to use.
Two other important graphics-based objects not found in Graphics.Hpp
are TImage and TPaintBox. Both of these components are covered
in this chapter.
The TColor Type
Almost all the graphics objects use the simple TColor type. This is
one of the building blocks on which the whole graphics system is laid.
For most practical purposes, a variable of type TColor is synonymous
with the built in Windows type COLORREF. However, the actual
declaration for TColor looks like this:
enum TColor {clMin=-0x7fffffff-1, clMax=0x7fffffff};
If you know the COLORREF type, you can see that it is similar to the
TColor type. In a sense, the TColor type is nothing but a set
of
predefined COLORREFs.
The Windows palette system enables you to define three different colors, Red,
Green, and Blue, where each color has 255 different shades. These colors are
specified in the last three bytes of the 4-byte long value used to represent a
variable of type TColor.
Here is what the three primary colors look like:
Canvas->Brush->Color = 0x000000FF; // Red
Canvas->Brush->Color = 0x0000FF00; // Green
Canvas->Brush->Color = 0x00FF0000; // Blue
You worked with these colors, and combinations of these colors, in the
RGBShape program from Chapter 2, "Basic Facts about C++Builder."
Of course, it's not convenient to have to write out these numbers directly in
hex. Instead, you can use the RGB macro:
Canvas->Brush->Color = RGB(255, 0, 0); // Red
Canvas->Brush->Color = RGB(0, 255, 0); // Green
Canvas->Brush->Color = RGB(0, 0, 255); // Blue
You can, of course, combine red, green, and blue to produce various shades:
RGB(255, 0, 255); // Purple
RGB(255, 255, 0); // Yellow
RGB(127, 127, 127); // Gray
The VCL also provides a series of constants you can use to specify common
colors. clBlack, for instance, has the same internal number you would
obtain from the following code:
COLORREF clBlack = RGB(0, 0, 0);
Here are some declarations from GRAPHICS.HPP:
#define clBlack TColor(0)
#define clMaroon TColor(128)
#define clGreen TColor(32768)
#define clOlive TColor(32896)
If you want to experiment with this system, you can create a new project in
BCB, drop a button on the main form and run some changes on a method that looks
like this:
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Canvas->Brush->Color = 0x0000FFFF;
Canvas->Rectangle(0, 0, 200, 200);
}
This code will draw a bright yellow rectangle in the upper right corner of
the main form. Try changing the values of the Brush color to
0x00FF0000, to RGB(255, 0, 255), to clBlue, and so on. If
you get tired of drawing rectangles, you can switch to ellipses instead:
Canvas->Ellipse(0, 0, 200, 200);
I talk more about drawing shapes with a canvas later in the chapter.
Most of the time you will pick colors from the Color property editor
provided by most components. For instance, you can double click the Color
property for a TForm object to bring up a TColorDialog.
However, there are times when you need to work with the TColor type
directly, which is why I have explained the topic in some detail.
The Canvas Object
All forms, and many components, have a canvas. You can think of a canvas as
being the surface on which graphics objects can paint. In short, the metaphor
used by the developers here is of a painter's canvas.
This TCanvas object is brought into the core component hierarchy
through aggregation, which means in many cases that you access its features via
a field of an object such as a TForm or TImage object. For
instance, you can write the following:
Form1->Canvas->TextOut(1, 1, "Hello from the canvas");
This statement writes the words "Hello from the canvas" in the
upper-left corner of a form. Notice that you do not have to access or reference
a device context directly in order to use this method.
Of course, most of the time you are accessing Form1 from inside of
Form1, so you can write
Canvas->TextOut(1, 1, "Hello from the canvas");
However, you cannot write
Form1->TextOut(1, 1, "Hello from the canvas");
This is because the Canvas object is aggregated into Form1.
TCanvas is not brought in through multiple inheritance. Furthermore,
the aggregation does not attempt to wrap each of the methods of TCanvas
inside methods of TForm. Aggregation is discussed in more detail in
Chapters 19, "Inheritance," and 20, "Encapsulation."
The Canvas object has several key methods that all VCL programmers
should know:
| Arc |
Draw an arc |
| Chord |
A closed figure showing the intersection of a line and an
ellipse |
| CopyRect |
Copy an area of one canvas to another |
| Draw |
Draw a bitmap or other graphic on a canvas |
| Ellipse |
Draw an ellipse |
| FillRect |
Fill a rectangle |
| FloodFill |
Fill an enclosed area |
| FrameRect |
Draw a border around a rectangle |
| LineTo |
Draw a line |
| MoveTo |
Draw a line |
| Pie |
Draw a pie-shaped object |
| Polygon |
Draw a multisided object |
| PolyLine |
Connect a set of points on the canvas |
| Rectangle |
Draw a rectangle |
| RoundRect |
Draw a rectangle with rounded corners |
| StretchDraw |
Same as Draw, but stretches the object to fill an
area |
| TextHeight |
The height of a string in the current font |
| TextOut |
Output text |
| TextRect |
Output text in a defined area |
| TextWidth |
The width of a string in the current font |
The following properties of the Canvas object are important:
Font
Brush
Pen
Pixels
The following events can be important to some very technical programmers:
OnChange
OnChanging
If you know the Windows API, many of these methods will be familiar to you.
The big gain from using the Canvas object rather than the raw Windows
GDI calls is that the resources you use will be managed for you automatically.
In particular, you never need to obtain a device context, nor do you have to
select an object into a device context.
In some cases, you will get better performance if you write directly to the
Windows GDI. However, it's a mistake to assume that you will always get better
by doing so. For instance, the VCL graphics subsystem will cache and share
resources in a sophisticated manner that would be very difficult to duplicate in
your own code. My personal opinion is that you should use the VCL graphics
routines whenever possible, and only turn to the raw Windows API when you run up
against an area of graphics not covered by the VCL.
The DrawShapes program demonstrates how easy it is to use the TCanvas
object in a program. This application delineates the outlines of a simple paint
program that has the capability to draw lines, rectangles, and ellipses. A
screen shot of the program is shown in Figure 7.1, and the source for the
program appears in Listings 7.1 and 7.2.
FIGURE 7.1. The DrawShapes program shows how
to create the lineaments of a simple paint program.
Listing 7.1. The header file for the DrawShapes
program declares an enumerated type and several simple fields.
///////////////////////////////////////
// Main.cpp
// DrawShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Menus.hpp>
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
#include <vcl\Dialogs.hpp>
enum TCurrentShape {csLine, csRectangle, csEllipse};
class TForm1 : public TForm
{
__published:
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *Open1;
TMenuItem *Save1;
TMenuItem *N1;
TMenuItem *Rectangle1;
TMenuItem *Shapes1;
TMenuItem *Rectangle2;
TMenuItem *Ellipse1;
TMenuItem *Colors1;
TMenuItem *Brush1;
TMenuItem *Pen1;
TPanel *Panel1;
TSpeedButton *SpeedButton1;
TSpeedButton *SpeedButton2;
TSpeedButton *SpeedButton3;
TSpeedButton *SpeedButton4;
TMenuItem *Lines1;
TColorDialog *ColorDialog1;
TSpeedButton *SpeedButton5;
void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseMove(TObject *Sender, TShiftState Shift, int X,
int Y);
void __fastcall SpeedButton1Click(TObject *Sender);
void __fastcall Brush1Click(TObject *Sender);
void __fastcall Pen1Click(TObject *Sender);
void __fastcall SpeedButton5Click(TObject *Sender);
private:
TRect FShapeRect;
TColor FPenColor;
TColor FBrushColor;
bool FDrawing;
TCurrentShape FCurrentShape;
int FPenThickness;
void __fastcall DrawShape();
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 7.2. The main form for the DrawShapes
program shows how to draw shapes on a canvas.
///////////////////////////////////////
// Main.cpp
// DrawShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FDrawing= False;
FCurrentShape = csRectangle;
FBrushColor = clBlue;
FPenColor = clYellow;
FPenThickness = 1;
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
Canvas->Pen->Mode = pmCopy;
DrawShape();
}
void __fastcall TForm1::DrawShape()
{
Canvas->Brush->Color = FBrushColor;
Canvas->Pen->Color = FPenColor;
Canvas->Pen->Width = FPenThickness;
switch (FCurrentShape)
{
case csLine:
Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
case csRectangle:
Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
case csEllipse:
Canvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
default:
;
}
}
void __fastcall TForm1::FormMouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
Canvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
FCurrentShape = TCurrentShape(dynamic_cast<TSpeedButton *>(Sender)->Tag);
}
void __fastcall TForm1::Brush1Click(TObject *Sender)
{
ColorDialog1->Color = FBrushColor;
if (ColorDialog1->Execute())
FBrushColor = ColorDialog1->Color;
}
void __fastcall TForm1::Pen1Click(TObject *Sender)
{
ColorDialog1->Color = FPenColor;
if (ColorDialog1->Execute())
FPenColor = ColorDialog1->Color;
}
void __fastcall TForm1::SpeedButton5Click(TObject *Sender)
{
FPenThickness = dynamic_cast<TSpeedButton *>(Sender)->Tag;
}
This program enables you to draw lines, rectangles, and ellipses on the main
form of the application. You can select the type of shape you want to work with
through the menus or by clicking on speed buttons. The program also lets you set
the color of the shapes you want to draw and specify the width and color of the
border around the shape.
Pens and Brushes
When setting the color of a shape, you need to know that the filled area
inside a shape gets set to the color of the current Brush for the
canvas. The line that forms the border for the shape is controlled by the
current Pen.
Here is how to set the current color and width of the Pen:
Canvas->Pen->Color = clBlue;
Canvas->Pen->Width = 5;
If you want to change the color of the Brush, you can write the
following code:
Canvas->Brush->Color = clYellow;
I don't work with it in this program, but you can assign various styles to a
Brush, as defined by the following enumerated type:
TBrushStyle { bsSolid, bsClear, bsHorizontal,
bsVertical,
bsFDiagonal, bsBDiagonal, bsCross, bsDiagCross };
By default, a Brush has the bsSolid type.
To set the style of a Brush, you would write code that looks like
this:
Canvas->Brush->Style = bsSolid;
Brushes also have Color and Bitmap properties. The
Bitmap property can be set to a small external bitmap image that
defines the pattern for Brush.
There is only one method of the TBrush object that you would be
likely to use. This method is called Assign, and it is used when you
want to copy the characteristics of one brush to another brush.
Pens have all the same properties as Brushes, but they add
a Width and Mode property. The Width property defines
the width of the Pen in pixels, and the Mode property defines
the type
of operation to use when painting the Pen to the screen. These
logical operations will be discussed further in just a few moments.
Rubber Banding
Before reading this section, fire up the DrawShapes program and practice
drawing ellipses and rectangles onscreen so you can see how the rubber-band
technique works. If for some reason you can't run the DrawShapes program, open
up Windows Paint and draw some squares or circles by first clicking the
appropriate icon from the Tools menu. Watch the way these programs create an
elastic square or circle that you can drag around the desktop. Play with these
shapes as you decide what dimensions and locations you want for your geometric
figures.
These tools appear to be difficult for a programmer to create, but thanks to
the Windows API, the code is relatively trivial. Following are the main steps
involved, each of which is explained in-depth later in this section:
- 1. When the user clicks the left mouse button, DrawShapes
"memorizes" the x and y coordinates of the WM_LBUTTONDOWN event.
2. As the user drags the mouse across the screen with the left button
still down, DrawShapes draws a square or circle each time it gets a
WM_MOUSEMOVE message. Just before painting each new shape, the program
blanks out the previous square or circle. The dimensions of the new shape are
calculated by combining the coordinates of the original WM_LBUTTONDOWN
message with the current coordinates passed in the WM_MOUSEMOVE
message.
3. When the user generates a WM_LBUTTONUP message, DrawShapes
paints the final shape in the colors and pen size specified by the user.
Although this description obviously omits some important details, the
outlines of the algorithm should take shape in your mind in the form of only a
few relatively simple, logical strokes. Things get a bit more complicated when
the details are mulled over one by one, but the fundamental steps should be
relatively clear.
-
NOTE: In the previous numbered
list, I give the names of the messages that generate VCL events. For instance,
a WM_LBUTTONDOWN message generates an OnMouseDown event, the
WM_MOUSEMOVE message generates an OnMouseMove event, and so
on. I'm referring to the underlying messages because it is a good idea to
remember the connection between VCL events and their associated Windows
messages.
Zooming in on the details, here's a look at the response to a
WM_LBUTTONDOWN message:
void __fastcall TForm1::FormMouseDown(TObject *Sender,
TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
FormMouseDown saves the location on which the mouse was clicked in
the first two fields of a TRect structure called FShapeRect.
FShapeRect is declared as a field of TForm1. I set the
Right field of the TRect structure to a large negative number so I
know that this is the first time that I need to start tracking the user's mouse
movements. This is necessary because the very first shape I draw needs to be
treated as a special case.
The last thing done in the FormMouseDown method is to set a private
variable of TForm1 called FDrawing to True. This lets
the program know that the user has started a drawing operation.
After the left mouse button is pressed, the program picks up all
WM_MOUSEMOVE messages that come flying into DrawShape's ken:
void __fastcall TForm1::FormMouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
Canvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
The first line of the function uses one of several possible techniques for
checking to see if the left mouse button is down. If the button isn't down, the
function ignores the message. If it is down, the function gets the device
context, sets the Pen's drawing mode to pmNotXor, memorizes
the current dimensions of the figure, draws it, and releases the device context.
The Mode property of a TPen object sets the current drawing
mode in a manner similar to the way the last parameter in BitBlt sets
the current painting mode. You can achieve the same effect by directly calling
the Windows API function called SetROP2. In this case, DrawShape
uses the logical XOR and NOT operations to blit the
elastic image to the screen. This logical operation is chosen because it paints
the old shape directly on top of the original image, thereby effectively erasing
each shape:
- If you XOR a square to the screen, the square will show up
clearly.
- If you XOR that same image again in the same location, the image
will disappear.
Such are the virtues of simple logical operations in graphics mode.
-
NOTE: Aficionados of graphics
logic will note that the logical operation employed by DrawShapes is
a variation on the exclusive OR (XOR) operation. This
variation ensures that the fill in the center of the shape to be drawn won't
blot out what's beneath it. The Microsoft documentation explains the
difference like this:
R2_XOR: final pixel = pen ^ screen pixel
R2_NOTXORPEN : final pixel = ~(pen ^ screen pixel)
This code tests to see whether the pixels to be XORed belong to a
pen. Don't waste too much time worrying about logical operations and how they
work. If they interest you, fine; if they don't, that's okay. The subject
matter of this book is programming, not logic.
If you were working directly in the Windows API, you would work with a
constant called RT_NOTXORPEN rather than pmNotXor. I have to
confess that the VCL's tendency to rename constants used by the Windows API is
not a very winning trait. Granted, the people in Redmond who came up with many
of those identifiers deserve some terrible, nameless, fate, but once the
damage had been done it might have been simpler to stick with the original
constants. That way people would not have to memorize two sets of identifiers,
one for use with the VCL, and the other for use with the Windows API. You
cannot use the Windows constants in place of the VCL constants, as the various
identifiers do not map down to the same value.
Despite these objections, I still think it is wise to use the VCL rather than
writing directly to the Windows API. The VCL is much safer and much easier to
use. The performance from most VCL objects is great, and in many cases it will
be better than what most programmers could achieve writing directly to the
Windows API.
Notice that FormMouseMove calls DrawShape twice. The first
time, it passes in the dimensions of the old figure that needs to be erased.
That means it XORs the same image directly on top of the original
image, thereby erasing it. Then FormMouseMove records the location of
the latest WM_MOUSEMOVE message and passes this new information to
DrawShape, which paints the new image to the screen. This whole process is
repeated over and over again (at incredible speeds) until the user lifts the
left mouse button.
In the DrawImage function, Metaphor first checks to see
which shape the user has selected and then proceeds to draw that shape to the
screen using the current pen and fill color:
void __fastcall TForm1::DrawShape()
{
Canvas->Brush->Color = FBrushColor;
Canvas->Brush->Style = bsSolid;
Canvas->Pen->Color = FPenColor;
Canvas->Pen->Width = FPenThickness;
switch (FCurrentShape)
{
case csLine:
Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
case csRectangle:
Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
case csEllipse:
Canvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
default:
;
}
}
This code sets the current Pen and Brush to the values
chosen by the user. It then uses a switch statement to select the
proper type of shape to draw to the screen. Most of these private variables such
as FPenColor are set by allowing the user to make selections from the
menu. To see exactly how this works, you can study the code of the application.
Notice that when drawing these shapes, there is no need to track the HDC of
the current Canvas. One of the primary goals of the TCanvas
object is to completely hide the HDC from the user. I discuss this matter in
more depth in the next section of the chapter, "To GDI or not to GDI."
The final step in the whole operation occurs when the user lifts his finger
off the mouse:
void __fastcall TForm1::FormMouseUp(TObject *Sender,
TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
Canvas->Pen->Mode = pmCopy;
DrawShape();
}
This code performs the following actions:
- A flag is set stating that the user has decided to stop drawing.
- The final dimensions of the shape are recorded.
- The mode for the Pen is switched from pmNotXor to the
default value, which is pmCopy.
- The final image is painted to the screen.
The code that paints the final shape takes into account the colors and the
pen thickness that the user selected with the menus.
Well, there you have it. That's how you draw shapes to the screen using the
rubber-band technique. Overall, if you take one thing at a time, the process
isn't too complicated. Just so you can keep those steps clear in your mind, here
they are again:
- Remember where the WM_LBUTTONDOWN message took place.
- Draw the shape each time you get a WM_MOUSEMOVE message.
- Draw the final shape when you get a WM_LBUTTONUP message.
That's all there is to it.
To GDI or not to GDI
The VCL will never cut you off completely from the underlying Windows API
code. If you want to work at the Windows API level, you can do so. In fact, you
can often write code that mixes VCL and raw Windows API code.
If you want to access the HDC for a window, you can get at it through the
Handle property of the canvas:
MyOldPenHandle = SelectObject(Canvas->Handle,
MyPen->Handle);
In this case you are copying the Handle of a Pen object
into the HDC of the TCanvas object.
Having free access to the Handle of the TCanvas object can
be useful at times, but the longer I use the VCL, the less inclined I am to use
it. The simple truth of the matter is that I now believe that it is best to let
an object of some sort handle all chores that require serious housekeeping. This
course of action allows me to rely on the object's internal logic to correctly
track the resources involved.
If you are not using the VCL, whenever you select something into an HDC, you
need to keep track of the resources pumped out of the HDC by the selection. When
you are done, you should then copy the old Handle back into the HDC. If
you accidentally lose track of a resource, you can upset the balance of the
entire operating system. Clearly, this type of process is error-prone and best
managed by an object that can be debugged once and reused many times. My
options, therefore, are to either write the object myself or use the existing
code found in the VCL. In most cases, I simply take the simplest course and use
the excellent TCanvas object provided by the VCL.
When I do decide to manage an interaction with the GDI myself, I often prefer
to get hold of my own HDC and ignore the TCanvas object altogether. The
reason I take this course is simply that the TCanvas object will
sometimes maintain the Canvas's HDC on its own, and I therefore can't
have complete control over what is happening to it.
Here is a simple example of how to use the GDI directly inside a VCL program:
HDC DC = GetDC(Form1->Handle);
HFONT OldFont = SelectObject(DC, Canvas->Font->Handle);
TextOut(DC, 1, 100, "Text", 4);
SelectObject(DC, OldFont);
ReleaseDC(Form1->Handle, DC);
If you look at the code shown here, you will see that I get my own DC by
calling the Windows API function GetDC. I then select a new font into
the DC. Notice that I use the VCL TFont object. I usually find it
easier to manage Fonts, Pens, and Brushes with VCL
objects than with raw Windows API code. However, if you want to create your own
fonts with raw Windows API code, you are free to do so.
Here is an example of creating a VCL Font object from scratch:
TFont *Font = new TFont();
Font->Name = "Time New Roman";
Font->Size = 25;
Font->Style = TFontStyles() << fsItalic << fsBold;
HDC DC = GetDC(Form1->Handle);
HFONT OldFont = SelectObject(DC, Font->Handle);
TextOut(DC, 1, 100, "Text", 4);
SelectObject(DC, OldFont);
ReleaseDC(Form1->Handle, DC);
delete Font;
This code allocates memory for a Font object, assigns some values to
its key properties, and then copies the Handle of the Font
object into the current DC. Notice that I still have to save the old Font
handle and copy it back into the DC when I am done with it. This is the type of
operation that I prefer to have handled by an object. When I am done with the
example code shown here, I delete the Font object I created.
Examples such as the ones you have just seen show that you can use the
Windows API by itself inside a VCL graphics operation, or you can combine VCL
code with raw GDI code. The course you choose will be dictated by your
particular needs. My suggestion, however, is to use the VCL whenever possible,
and to fall back on the Windows API only when strictly necessary. The primary
reason for this preference is that it is safer to use the VCL than to write
directly to the Windows API. It is also easier to use the VCL than the raw
Windows API, but that argument has a secondary importance in my mind.
Using TImage, Saving and Loading Bitmaps
If you work with the DrawShapes program for awhile, you will find that it has
several weaknesses that cry out for correction. In particular, the program
cannot save an image to disk, and it cannot repaint the current image if you
temporarily cover it up with another program.
Fixing these problems turns out to be remarkably simple. The key
functionality to add to the program is all bound up in a single component called
TImage. This control provides you with a Canvas on which you
can draw, and gives you the ability to convert this Canvas into a
bitmap.
The BitmapShapes program, shown in Listing 7.3, shows how to proceed in the
creation of an updated DrawShapes program that can save files to disk, and can
automatically redraw an image if you switch back to the main form from another
program.
Listing 7.3. The main module for the BitmapShapes
program.
///////////////////////////////////////
// Main.cpp
// DrawShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FDrawing= False;
FCurrentShape = csRectangle;
FBrushColor = clBlue;
FPenColor = clYellow;
FPenThickness = 1;
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
Image1->Canvas->Pen->Mode = pmCopy;
DrawShape();
}
void __fastcall TForm1::DrawShape()
{
Image1->Canvas->Brush->Color = FBrushColor;
Image1->Canvas->Brush->Style = bsSolid;
Image1->Canvas->Pen->Color = FPenColor;
Image1->Canvas->Pen->Width = FPenThickness;
switch (FCurrentShape)
{
case csLine:
Image1->Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
Image1->Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
case csRectangle:
Image1->Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
case csEllipse:
Image1->Canvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
default:
;
}
}
void __fastcall TForm1::FormMouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
Image1->Canvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
FCurrentShape = TCurrentShape(dynamic_cast<TSpeedButton *>(Sender)->Tag);
}
void __fastcall TForm1::Brush1Click(TObject *Sender)
{
ColorDialog1->Color = FBrushColor;
if (ColorDialog1->Execute())
FBrushColor = ColorDialog1->Color;
}
void __fastcall TForm1::Pen1Click(TObject *Sender)
{
ColorDialog1->Color = FPenColor;
if (ColorDialog1->Execute())
FPenColor = ColorDialog1->Color;
}
void __fastcall TForm1::SpeedButton5Click(TObject *Sender)
{
FPenThickness = dynamic_cast<TSpeedButton *>(Sender)->Tag;
}
void __fastcall TForm1::Save1Click(TObject *Sender)
{
if (SaveDialog1->Execute())
{
Image1->Picture->SaveToFile(SaveDialog1->FileName);
}
}
void __fastcall TForm1::Open1Click(TObject *Sender)
{
if (OpenDialog1->Execute())
{
Image1->Picture->LoadFromFile(OpenDialog1->FileName);
}
}
void __fastcall TForm1::Exit1Click(TObject *Sender)
{
Close();
}
The key change from the DrawShapes program is that all of the Canvas
operations are performed on the Canvas of a TImage control
rather than directly on the Canvas of a form:
Image1->Canvas->MoveTo(FShapeRect.Left,
FShapeRect.Top);
Image1->Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
A TImage control is designed explicitly for the type of operations
undertaken by this program. In particular, it maintains an internal bitmap into
which images are automatically drawn.
When it comes time to save the picture you have created, you can do so with
one
line of code:
void __fastcall TForm1::Save1Click(TObject *Sender)
{
if (SaveDialog1->Execute())
{
Image1->Picture->SaveToFile(SaveDialog1->FileName);
}
}
Loading a file into memory from disk is also a simple one-line operation:
void __fastcall TForm1::Open1Click(TObject *Sender)
{
if (OpenDialog1->Execute())
{
Image1->Picture->LoadFromFile(OpenDialog1->FileName);
}
}
Both of the preceding calls assume that you have dropped a TOpenDialog
onto the main form of your application. The TOpenDialog object is
extremely easy to use, so you should have no trouble learning how to use it from
the online help.
-
NOTE: When I opt not to explain
an object such as TOpenDialog, my intention is not to ignore the
needs of this book's readers, but rather to avoid inserting boring,
repetitious information into the book. If you really crave a reference other
than the online help for this kind of information, you should check the
bookstore for introductory texts that cover this kind of material.
If you want a bit more flexibility, and desire to have a separate TBitmap
object which you can use for your own purposes, it is easy to create one from
the image you created with this program. To proceed, just create a TBitmap
object, and then use the Assign method to copy the picture from the
TImage control to your bitmap:
Graphics::TBitmap *Bitmap = new Graphics::TBitmap();
Bitmap->Assign(Image1->Picture->Graphic);
Bitmap->SaveToFile(SaveDialog1->FileName);
delete Bitmap;
In this example I save the bitmap to file and then delete the object when I
am through with it. In the context of the current program, this doesn't make a
great deal of sense. However, code like this demonstrates some of the
functionality of the TBitmap object. I qualify the reference to
TBitmap with Graphics.Hpp, because there are two TBitmap
declarations in the header files included in most BCB programs.
Working with Metafiles
Bitmaps are very convenient and are often the best way to work with graphic
images. However, BCB also lets you work with enhanced metafiles.
Metafiles are collections of shapes drawn into a device context. Internally,
they are little more than a list of GDI calls that can be played back on demand
to create a picture. Metafiles have two big advantages over bitmaps:
- They are very small. The same image that might take hundreds of KB to save
in bitmaps can often be saved in a metafile of just 10 or 20 thousand bytes.
- Because metafiles consist of a series of shapes, you can, at least
potentially, iterate through the list of shapes in a picture and edit them one
at a time. This can give you a very powerful ability to precisely edit the
contents of a picture.
The disadvantage of metafiles is that they must consist only of a series of
GDI calls. As a result, you can't easily transform a photograph or a scanned
image into a metafile. Metafiles are best defined as a simple way of preserving
text, or as a simple means of storing a series of fairly simple shapes such as a
line drawing. In the hands of a good artist, however, you can get metafiles to
show some fairly powerful images. (See Figure 7.2.)
FIGURE 7.2. A grayscale version of a
colorful metafile that ships with Microsoft Office shows some of the power of
this technology.
There are two types of metafiles available to Windows users. One has the
extension .WMF, and is primarily designed for use in the 16-bit world.
The second type of file has the extension .EMF, for Enhanced Metafile.
These latter types of files are designed for use in the 32-bit world. I find
them much more powerful than their 16-bit cousins.
The code shown in the forthcoming MetaShapes program is designed to work only
with enhanced metafiles. BCB will save a file as a regular metafile if you give
it an extension of .WMF, and it will save it as an enhanced metafile if
you give it an extension of .EMF. When using BCB, it is best to stick
with enhanced metafiles. If you have some .WMF files you want to use
with BCB, you might want to find a third-party tool that will convert your
.WMF into an .EMF.
The MetaShapes program is very similar to the DrawShapes and BitmapShapes
programs. It has the same capabilities as the BitmapShapes program in terms of
its capability to save and preserve images, only it works with metafiles rather
than bitmaps. Metafiles are many times smaller than standard .BMP
files.
There is no TImage control for metafiles, so you have to do a little
more work to make this system function properly. In particular, you have to
explicitly open a metafile and then draw directly into it, as shown in Listing
7.4 and 7.5.
Listing 7.4. The header for the MetaShapes program.
///////////////////////////////////////
// Main.h
// MetaShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Menus.hpp>
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
#include <vcl\Dialogs.hpp>
enum TCurrentShape {csLine, csRectangle, csEllipse};
class TForm1 : public TForm
{
__published:
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *Open1;
TMenuItem *Save1;
TMenuItem *N1;
TMenuItem *Exit1;
TMenuItem *Shapes1;
TMenuItem *Rectangle2;
TMenuItem *Ellipse1;
TMenuItem *Colors1;
TMenuItem *Brush1;
TMenuItem *Pen1;
TPanel *Panel1;
TSpeedButton *SpeedButton1;
TSpeedButton *SpeedButton2;
TSpeedButton *SpeedButton3;
TSpeedButton *SpeedButton4;
TMenuItem *Lines1;
TColorDialog *ColorDialog1;
TSpeedButton *SpeedButton5;
TMenuItem *New1;
TPaintBox *PaintBox1;
TSaveDialog *SaveDialog1;
TOpenDialog *OpenDialog1;
void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseMove(TObject *Sender, TShiftState Shift, int X,
int Y);
void __fastcall SpeedButton1Click(TObject *Sender);
void __fastcall Brush1Click(TObject *Sender);
void __fastcall Pen1Click(TObject *Sender);
void __fastcall SpeedButton5Click(TObject *Sender);
void __fastcall New1Click(TObject *Sender);
void __fastcall Exit1Click(TObject *Sender);
void __fastcall PaintBox1Paint(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
void __fastcall Save1Click(TObject *Sender);
void __fastcall Open1Click(TObject *Sender);
private:
TRect FShapeRect;
TColor FPenColor;
TColor FBrushColor;
bool FDrawing;
TCurrentShape FCurrentShape;
int FPenThickness;
TMetafile* MyMetafile;
TMetafileCanvas* MyCanvas;
void __fastcall DrawShape();
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 7.5. The MetaShapes program shows how to
draw into a metafile and save it to disk.
///////////////////////////////////////
// Main.cpp
// MetaShapes Example
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
FDrawing= False;
FCurrentShape = csRectangle;
FBrushColor = clBlue;
FPenColor = clYellow;
FPenThickness = 1;
MyMetafile = new TMetafile;
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
}
void __fastcall TForm1::FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
void __fastcall TForm1::FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
PaintBox1->Canvas->Pen->Mode = pmCopy;
MyCanvas->Pen->Mode = pmCopy;
DrawShape();
}
}
void __fastcall TForm1::DrawShape()
{
PaintBox1->Canvas->Brush->Color = FBrushColor;
PaintBox1->Canvas->Brush->Style = bsSolid;
PaintBox1->Canvas->Pen->Color = FPenColor;
PaintBox1->Canvas->Pen->Width = FPenThickness;
MyCanvas->Brush->Color = FBrushColor;
MyCanvas->Brush->Style = bsSolid;
MyCanvas->Pen->Color = FPenColor;
MyCanvas->Pen->Width = FPenThickness;
switch (FCurrentShape)
{
case csLine:
MyCanvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
MyCanvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
PaintBox1->Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
PaintBox1->Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
case csRectangle:
PaintBox1->Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
MyCanvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
case csEllipse:
PaintBox1->Canvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
MyCanvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
break;
default:
;
}
}
void __fastcall TForm1::FormMouseMove(TObject *Sender,
TShiftState Shift, int X, int Y)
{
if (FDrawing)
{
PaintBox1->Canvas->Pen->Mode = pmNotXor;
MyCanvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
void __fastcall TForm1::SpeedButton1Click(TObject *Sender)
{
FCurrentShape = TCurrentShape(dynamic_cast<TSpeedButton *>(Sender)->Tag);
}
void __fastcall TForm1::Brush1Click(TObject *Sender)
{
ColorDialog1->Color = FBrushColor;
if (ColorDialog1->Execute())
FBrushColor = ColorDialog1->Color;
}
void __fastcall TForm1::Pen1Click(TObject *Sender)
{
ColorDialog1->Color = FPenColor;
if (ColorDialog1->Execute())
FPenColor = ColorDialog1->Color;
}
void __fastcall TForm1::SpeedButton5Click(TObject *Sender)
{
FPenThickness = dynamic_cast<TSpeedButton *>(Sender)->Tag;
}
void __fastcall TForm1::New1Click(TObject *Sender)
{
delete MyCanvas;
delete MyMetafile;
MyMetafile = new TMetafile;
MyCanvas = new TMetafileCanvas(MyMetafile, 0);
InvalidateRect(Handle, NULL, True);
}
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
delete MyCanvas;
if (MyMetafile)
PaintBox1->Canvas->Draw(0,0,MyMetafile);
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
MyCanvas->Draw(0, 0, MyMetafile);
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete MyCanvas;
}
void __fastcall TForm1::Exit1Click(TObject *Sender)
{
Close();
}
void __fastcall TForm1::Save1Click(TObject *Sender)
{
if (SaveDialog1->Execute())
{
delete MyCanvas;
MyMetafile->SaveToFile(SaveDialog1->FileName);
MyCanvas = new TMetafileCanvas(MyMetafile, 0);
MyCanvas->Draw(0, 0, MyMetafile);
}
}
void __fastcall TForm1::Open1Click(TObject *Sender)
{
if (OpenDialog1->Execute())
{
FShapeRect = Rect(0, 0, 0, 0);
delete MyCanvas;
TFileStream *Stream = new TFileStream(OpenDialog1->FileName, fmOpenRead);
MyMetafile->LoadFromStream(Stream);
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
MyCanvas->Draw(0, 0, MyMetafile);
PaintBox1->Canvas->Draw(0, 0, MyMetafile);
Stream->Free();
}
}
This program works just like the BitmapShapes program shown previously, in
that it lets you draw colored shapes to the screen and then save them to file. A
screenshot of the program is shown in Figure 7.3.
FIGURE 7.3. The MetaShapes program in
action.
When working with metafiles, you need to create both a TMetafile
object and a TMetafileCanvas object.
MyMetafile = new TMetafile;
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
Notice that the TMetafileCanvas is explicitly associated with a
particular TMetafile object. Specifically, the TMetaFile
object is passed in as the first parameter of the TMetaFileCanvas
object's constructor. These two classes are designed to work in tandem, and both
pieces must be present if you want to create a metafile.
Here is a simple example of how to draw into a TMetafile canvas:
MyCanvas->Ellipse(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
This code looks exactly like any other call to the TCanvas::Ellipse,
only this time I am writing into a TMetafileCanvas rather than a form's
Canvas.
-
NOTE: The VCL preserves the
Canvas metaphor in a wide variety of contexts, such as when you are
working with metafiles or bitmaps. This enables you to learn one set of
commands, and to then apply them to a wide variety of objects. As you will see
later in the book, this type of functionality derives in part from a judicious
and intelligent use of polymorphism.
Basing a suite of objects on one model enables users to quickly get up to
speed on new technologies. The underlying code for creating bitmaps is very
different from the code for creating metafiles. But the TCanvas
object enables you to treat each operation as if it were nearly identical.
People who are interested in object design should contemplate the elegance of
this implementation.
If you only want to display a metafile, you can work solely with the
TMetafile object. However, if you want to create metafiles, to draw images
into a metafile, you need a TMetafileCanvas object. You can draw
directly into a TMetafileCanvas, but when you want to display the image
to the user, you need to delete the object so that its contents will be
transferred into a metafile that can be displayed for the user:
delete MyCanvas;
PaintBox1->Canvas->Draw(0,0,MyMetafile);
In this code I am using a PaintBox as the surface on which to
display the metafile. The code first deletes the TMetafileCanvas,
thereby transferring the painting from the TMetafileCanvas to the
TMetafile. This implementation is, I suppose, a bit awkward, but once you
understand the principle involved, it should not cause you any serious
difficulty.
There is no particular connection between metafiles and the TPaintBox
object. In fact, I could just as easily have painted directly into a TForm.
The reason I chose TPaintBox is that it enables me to easily define a
subsection of a form that can be used as a surface on which to paint. In
particular, part of the form in the MetaShapes program is covered by a
TPanel object. To make sure that the user can see the entire canvas on
which he will be painting, I used a TPaintBox.
If you are interested in these matters, you might want to open up
EXTCTRLS.HPP and compare the declarations for TPaintBox and
TImage. Both of these controls are descendants of TGraphicControl,
and both offer similar functionality. The big difference between them is that
TImage has an underlying bitmap, while TPaintBox has a
simpler, sparer architecture.
By now it has probably occurred to you that there is no simple means for
displaying a metafile to the user at the time it is being created. In
particular, you can't show it to the user without first deleting the
TMetafileCanvas. To avoid performing this action too often, I simply record
the user's motions into both the TMetafileCanvas and the Canvas
for the main form:
PaintBox1->Canvas->Brush->Color = FBrushColor;
... // Code ommitted
MyCanvas->Brush->Color = FBrushColor;
...// Code ommitted
switch (FCurrentShape)
{
case csLine:
MyCanvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
MyCanvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
PaintBox1->Canvas->MoveTo(FShapeRect.Left, FShapeRect.Top);
PaintBox1->Canvas->LineTo(FShapeRect.Right, FShapeRect.Bottom);
break;
... // etc
This duplication of code, while bothersome, is not really terribly costly in
terms of what it is adding to the size of my executable. Needless to say, I use
this technique so that the user can see what he or she is painting.
If the user flips away from the program and covers it with another
application, I need to repaint the image when the user flips back to
MetaShapes:
void __fastcall TForm1::PaintBox1Paint(TObject *Sender)
{
delete MyCanvas;
PaintBox1->Canvas->Draw(0,0,MyMetafile);
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
MyCanvas->Draw(0, 0, MyMetafile);
}
As you can see, this code deletes the TMetafileCanvas and then
paints the current image to the screen. I then create a new TMetafileCanvas
and paint the contents of the current metafile into it:
MyCanvas->Draw(0, 0, MyMetafile);
This same process occurs when I load a metafile from disk:
void __fastcall TForm1::Open1Click(TObject *Sender)
{
if (OpenDialog1->Execute())
{
delete MyCanvas;
TFileStream *Stream = new TFileStream(OpenDialog1->FileName, fmOpenRead);
MyMetafile->LoadFromStream(Stream);
MyCanvas = new TMetafileCanvas(MyMetafile, PaintBox1->Canvas->Handle);
MyCanvas->Draw(0, 0, MyMetafile);
PaintBox1->Canvas->Draw(0, 0, MyMetafile);
}
}
In this code, I delete the contents of any current TMetafileCanvas,
not because I want to draw it, but because I want to create a new, blank surface
on which to paint. I then load a metafile from disk and create a new
TMetafileCanvas into which I can paint the contents of the freshly loaded
metafile:
MyCanvas->Draw(0, 0, MyMetafile);
Finally, the program plays the contents of the metafile back to the user:
PaintBox1->Canvas->Draw(0, 0, MyMetafile);
When you are playing back fairly large metafiles, you can sometimes see the
shapes being painted to the screen one at a time. This helps to illustrate the
fact that metafiles are literally just lists of GDI calls with their related
parameters and device contexts carefully preserved. There are calls available
that enable you to walk through these lists and delete or modify individual
items. However, I do not take the code in this program quite that far. If you
master this process, however, you have the rudiments of a CAD or sophisticated
drawing program on your hands.
The only thing left to discuss is saving metafiles to disk, which turns out
to be a simple process:
void __fastcall TForm1::Save1Click(TObject *Sender) {
if (SaveDialog1->Execute()) { delete MyCanvas;
MyMetafile->SaveToFile(SaveDialog1->FileName); MyCanvas = new
TMetafileCanvas(MyMetafile, 0); MyCanvas->Draw(0, 0, MyMetafile); } }
This code deletes the current TMetafileCanvas, thereby assuring that
the TMetafile object is up-to-date. It then makes a call to the
TMetafile::SaveToFile method. Finally, it creates a new TMetafileCanvas
and draws the contents of the current metafile into it, which enables the user
to continue editing the picture after the save.
That is all I'm going to say about metafiles. Admittedly, this is a somewhat
esoteric subject from the point of view of many programmers. However, metafiles
are a useful tool that can be a great help if you want to save text, or a series
of images, in a small, compact file.
When I talk about saving text to a metafile, remember that all calls to draw
text on the screen are simply calls into the Windows GDI. You can therefore save
these calls in a metafile. Many fancy programs that enable you to quickly scroll
through large volumes of text are using metafiles as part of their underlying
technology.
Working with Fonts
The raw Windows API code for working with fonts is quite complex. The VCL,
however, makes fonts easy to use.
You have already seen an example of using the TFont object. The
basic idea is simply that a TFont object has eight key properties
called Color, Handle, Height, Name,
Pitch, PixelsPerInch, Size, and Style. It is a
simple matter to create a TFont object and change its properties:
TFont *MyFont = new TFont();
MyFont->Name = "BookwomanSH";
MyFont->Size = 24;
MyFont->Style = TFontStyles() << fsBold;
Form1->Font = MyFont;
In this case, the programmer has no obligation to delete the TFont
object, because its ownership is assumed by the form in the last line. If you
did not pass the ownership of the object over to the Form, you would
need to explicitly delete the Font when you were through with it:
delete MyFont;
Most of the time, if you want to give the user the capability to select a
font at runtime, you should use the TFontDialog from the dialogs page
of the
Component Palette. However, you can iterate through all the fonts on your
system by using the Fonts field of the TScreen object.
The TScreen object is used primarily for keeping track of the active
forms in your application and the current cursor. However, you can also use it
for iterating through all the available fonts on the system. These fonts are
kept in a TStringList object that is accessible through a property
called Fonts:
ListBox1->Items = Screen->Fonts;
The screen object is created automatically by the VCL, and is available to
all applications as a global object.
Besides the screen fonts, some programmers might also be interested in
accessing the available printer fonts. These can be accessed through an object
called TPrinter, which is kept in a module called PRINTERS.HPP:
TPrinter *Printer = new TPrinter();
ListBox2->Items = Printer->Fonts;
delete Printer;
The BCBFonts program from this book's CD-ROM gives a simple example of how to
iterate through all the fonts on the system so the user can pick and choose from
them. The source code for the program is shown in Listing 7.6.
Listing 7.6. The main module for the BCBFonts program
shows how to view a list of the currently available fonts.
///////////////////////////////////////
// Main.cpp
// Project: BCBFonts
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <vcl\printers.hpp>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
ListBox1->Items = Screen->Fonts;
TPrinter *Printer = new TPrinter();
ListBox2->Items = Printer->Fonts;
delete Printer;
Panel4->Font->Size = TrackBar1->Position;
ListBox1->ItemIndex = 0;
Panel4->Caption = ListBox1->Items->Strings[0];
}
void __fastcall TForm1::ListBox1Click(TObject *Sender)
{
if (ListBox1->ItemIndex != -1)
{
Panel4->Font->Name = ListBox1->Items->Strings[ListBox1->ItemIndex];
Panel4->Caption = Panel4->Font->Name;
}
}
void __fastcall TForm1::TrackBar1Change(TObject *Sender)
{
Panel4->Font->Size = TrackBar1->Position;
}
This program has two list boxes. At program startup, the first list box is
initialized to display all the fonts currently available on the system. The
right-hand list box shows all the fonts currently available on the printer. As a
rule, you don't need to worry about coordinating the two sets of fonts, because
Windows will handle that problem for you automatically. However, if you are
having trouble getting the right fonts on your printer when working with a
particular document, you might want to compare these two lists and see if any
obvious problems meet the eye.
If you select a font from the first list box in the BCBFonts program, its
name will appear in the caption of a TPanel object shown at the bottom
of the program. The Caption for the panel will use the font whose name
is currently being displayed, as shown in Figure 7.4. At the bottom of the
BCBFonts program is the TTrackBar object, which enables you to change
the size of the current font.
FIGURE 7.4. The BCBFonts program gives
samples of all the fonts on the system.
Working with Fractals
The graphics code shown so far in this section has been fairly trivial. The
last program I show in this chapter ups the ante a bit by showing how you can
create fractals, using the VCL graphics objects. This code should make it clear
that the VCL tools are quite powerful, and can be put to good use even in fairly
graphics-intensive programs.
The sample program on the CD, Fractals, is where you can find all of the code
discussed in this section of the chapter. The source for the program is shown in
Listings 7.7 through 7.13.
Listing 7.7. This program shows how to use fractals to
draw various shapes.
///////////////////////////////////////
// Main.cpp
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "Ferns.h"
#include "Squares1.h"
#include "Mandy.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//----------------------------------
void __fastcall TForm1::Ferns1Click(TObject *Sender)
{
FernForm->ShowModal();
}
void __fastcall TForm1::Squares1Click(TObject *Sender)
{
SquaresForm = new TSquaresForm(this);
SquaresForm->ShowModal();
SquaresForm->Free();
}
void __fastcall TForm1::Mandelbrot1Click(TObject *Sender)
{
MandelForm = new TMandelForm(this);
MandelForm->ShowModal();
delete MandelForm;
}
//----------------------------------
Listing 7.8. This header is for a form that draws a
fractal fern.
///////////////////////////////////////
// Ferns.h
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef FernsH
#define FernsH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
class TFernForm : public TForm
{
__published:
TTimer *Timer1;
void __fastcall Timer1Timer(TObject *Sender);
void __fastcall FormResize(TObject *Sender);
private:
void DoPaint();
float x, y;
int MaxIterations;
int Count;
int MaxX;
int MaxY;
public:
virtual __fastcall TFernForm(TComponent* Owner);
};
extern TFernForm *FernForm;
#endif
Listing 7.9. This form draws a fractal that looks
like a fern.
///////////////////////////////////////
// Ferns.cpp
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Ferns.h"
#pragma resource "*.dfm"
TFernForm *FernForm;
typedef float TDAry[4];
TDAry a = {0, 0.85, 0.2, -0.15};
TDAry b = {0, 0.04, -0.26, 0.28};
TDAry c = {0, -0.04, 0.23, 0.26};
TDAry d = {0.16, 0.85, 0.22, 0.24};
TDAry e = {0, 0, 0, 0};
TDAry f = {0, 1.6, 1.6, 0.44};
__fastcall TFernForm::TFernForm(TComponent* Owner)
: TForm(Owner)
{
Count = 0;
x = 0;
y = 0;
}
void TFernForm::DoPaint()
{
int k;
float TempX, TempY;
k = random(100);
if ((k > 0) && (k <= 85))
k = 1;
if ((k > 85) && (k <= 92))
k = 2;
if (k > 92)
k = 3;
TempX = a[k] * x + b[k] * y + e[k];
TempY = c[k] * x + d[k] * y + f[k];
x = TempX;
y = TempY;
if ((Count >= MaxIterations) || (Count != 0))
Canvas->Pixels[(x * MaxY / 11 + MaxX / 2)]
[(y * - MaxY / 11 + MaxY)] = clGreen;
Count = Count + 1;
}
void __fastcall TFernForm::Timer1Timer(TObject *Sender)
{
int i;
if (Count > MaxIterations)
{
Invalidate();
Count = 0;
}
for (i = 0; i <= 200; i++)
DoPaint();
}
void __fastcall TFernForm::FormResize(TObject *Sender)
{
MaxX = Width;
MaxY = Height;
MaxIterations = MaxY * 50;
Listing 7.10. The header for the Squares form.
///////////////////////////////////////
// Squares.h
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef Squares1H
#define Squares1H
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#define BoxCount 25
class TSquaresForm : public TForm
{
__published:
TTimer *Timer1;
void __fastcall Timer1Timer(TObject *Sender);
void __fastcall FormShow(TObject *Sender);
void __fastcall FormHide(TObject *Sender);
private:
void DrawSquare(float Scale, int Theta);
public:
virtual __fastcall TSquaresForm(TComponent* Owner);
};
extern TSquaresForm *SquaresForm;
#endif
Listing 7.11. A form that draws a hypnotic series of
squares.
///////////////////////////////////////
// Squares.cpp
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <math.h>
#pragma hdrstop
#include "Squares1.h"
#pragma resource "*.dfm"
TColor Colors[BoxCount];
typedef TPoint TSquarePoints[5];
TSquarePoints Square =
{{-100, -100},{100, -100},{100, 100},
{-100, 100},{-100, -100}};
TSquaresForm *SquaresForm;
__fastcall TSquaresForm::TSquaresForm(TComponent* Owner)
: TForm(Owner)
{
int X;
Randomize;
Colors[1] = TColor(RGB(random(255), random(255), random(255)));
for (X = 2; X <= BoxCount; X++)
Colors[X] = TColor(Colors[X-1] + RGB(random(64), random(64), random(64)));
}
void TSquaresForm::DrawSquare(float Scale, int Theta)
{
int i;
float CosTheta, SinTheta;
TSquarePoints Path;
CosTheta = Scale * cos(Theta * M_PI / 180); // precalculate rotation &
scaling
SinTheta = Scale * sin(Theta * M_PI / 180);
for (i = 0; i <= 4; i++)
{
Path[i].x = (Square[i].x * CosTheta + Square[i].y * SinTheta);
Path[i].y = (Square[i].y * CosTheta - Square[i].x * SinTheta);
}
Canvas->Polyline(Path, 4);
}
void __fastcall TSquaresForm::Timer1Timer(TObject *Sender)
{
int i;
float Scale = 1.0;
int Theta = 0;
SetViewportOrgEx(Canvas->Handle, ClientWidth / 2, ClientHeight / 2, NULL);
Canvas->Pen->Color = clWhite;
for (i = 1; i <= BoxCount; i++)
{
DrawSquare(Scale, Theta);
Theta = Theta + 10;
Scale = Scale * 0.85;
Canvas->Pen->Color = Colors[i];
}
for (i = BoxCount - 1; i >= 1; i--)
Colors[i] = Colors[i - 1];
Colors[0] = TColor(RGB(Colors[0] + random(64),
Colors[0] + random(64),
Colors[0] + random(64)));
}
void __fastcall TSquaresForm::FormShow(TObject *Sender)
{
Timer1->Enabled = True;
}
void __fastcall TSquaresForm::FormHide(TObject *Sender)
{
Timer1->Enabled = False;
}
Listing 7.12. The header for the form that draws the
Mandelbrot set.
///////////////////////////////////////
// Mandy.h
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MandyH
#define MandyH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
class TMandelForm : public TForm
{
__published:
TTimer *Timer1;
void __fastcall FormMouseDown(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormMouseUp(TObject *Sender, TMouseButton Button,
TShiftState Shift, int X, int Y);
void __fastcall FormResize(TObject *Sender);
void __fastcall Timer1Timer(TObject *Sender);
void __fastcall FormDblClick(TObject *Sender);
void __fastcall FormMouseMove(TObject *Sender, TShiftState Shift, int X, int
Y);
private:
int FDepth;
float FXRange; // The width and height of the
float FYRange; // mandlebrot plane. Starts at 3.
int FScrOrgX;
int FScrOrgY;
int FScrMaxX;
int FScrMaxY;
float FBaseOrgX;
float FBaseOrgY;
bool FQuitDrawing;
void GetOriginsAndWidths(float &XOrg, float &YOrg, float &XMax, float &YMax);
float Distance(float X, float Y);
void Calculate(float X, float Y, float &XIter, float &YIter);
int GetColor(int Steps);
TRect FShapeRect;
bool FDrawing;
void SetBoundary(int ScrX, int ScrY, int ScrX1, int ScrY1);
void SetMouseDownPos(int ScrX, int ScrY);
void SetMouseUpPos(int ScrX1, int ScrY1);
bool Run();
public: // User declarations
virtual __fastcall TMandelForm(TComponent* Owner);
void __fastcall DrawShape();
};
extern TMandelForm *MandelForm;
#endif
Listing 7.13. A simple form for drawing the
Mandelbrot set.
///////////////////////////////////////
// Mandy.cpp
// Project: Fractals
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <math.h>
#pragma hdrstop
#include "Mandy.h"
#pragma resource "*.dfm"
TMandelForm *MandelForm;
__fastcall TMandelForm::TMandelForm(TComponent* Owner)
: TForm(Owner)
{
FXRange = 3;
FYRange = 3;
FBaseOrgX = -2.25;
FBaseOrgY = -1.5;
Width = 550;
Height = 400;
FDrawing = False;
}
void TMandelForm::GetOriginsAndWidths(float &XOrg, float &YOrg, float &XMax,
float &YMax)
{
float VOrgX, VOrgY, VMaxX, VMaxY;
float XPercent, YPercent;
float MaxXPercent, MaxYPercent;
VOrgX = FScrOrgX;
VOrgY = FScrOrgY;
VMaxX = FScrMaxX;
VMaxY = FScrMaxY;
XPercent = VOrgX / Width;
YPercent = VOrgY / Height;
MaxXPercent = VMaxX / Width;
MaxYPercent = VMaxY / Height;
XOrg = (XPercent * FXRange) + FBaseOrgX;
YOrg = (YPercent * FYRange) + FBaseOrgY;
XMax = (MaxXPercent * FXRange) + FBaseOrgX;
YMax = (MaxYPercent * FYRange) + FBaseOrgY;
FBaseOrgX = XOrg;
FBaseOrgY = YOrg;
FXRange = XMax - XOrg;
FYRange = YMax - YOrg;
}
float TMandelForm::Distance(float X, float Y)
{
if ((X != 0.0) && (Y != 0.0))
return sqrt(pow(X, 2) + pow(Y, 2));
else
if (X == 0.0)
return abs(Y);
else
return abs(X);
};
void TMandelForm::Calculate(float X, float Y,
float &XIter, float &YIter)
{
float XTemp, YTemp;
XTemp = pow(XIter, 2) - pow(YIter, 2) + X;
YTemp = 2 * (XIter * YIter) + Y;
XIter = XTemp;
YIter = YTemp;
}
// Steps won't be larger than FDepth.
int TMandelForm::GetColor(int Steps)
{
int TopVal= 16777215; // RGB(255,255,255)
float Variation;
int Val;
Variation = TopVal / FDepth;
Val = Variation * Steps;
return Val;
}
void TMandelForm::SetBoundary(int ScrX, int ScrY, int ScrX1, int ScrY1)
{
FScrOrgX = ScrX;
FScrOrgY = ScrY;
FScrMaxX = ScrX1;
FScrMaxY = ScrY1;
};
void TMandelForm::SetMouseDownPos(int ScrX, int ScrY)
{
FScrOrgX = ScrX;
FScrOrgY = ScrY;
}
void TMandelForm::SetMouseUpPos(int ScrX1, int ScrY1)
{
FScrMaxX = ScrX1;
FScrMaxY = ScrY1;
if ((FScrMaxX - FScrOrgX) > 10)
Run();
}
void __fastcall TMandelForm::DrawShape()
{
Canvas->Rectangle(FShapeRect.Left, FShapeRect.Top,
FShapeRect.Right, FShapeRect.Bottom);
}
void __fastcall TMandelForm::FormMouseDown(TObject *Sender, TMouseButton
Button,
TShiftState Shift, int X, int Y)
{
if (Shift.Contains(ssRight))
FQuitDrawing = True;
else
SetMouseDownPos(X, Y);
FShapeRect.Left = X;
FShapeRect.Top = Y;
FShapeRect.Right = - 32000;
FDrawing = True;
}
void __fastcall TMandelForm::FormMouseUp(TObject *Sender, TMouseButton
Button,
TShiftState Shift, int X, int Y)
{
FDrawing = False;
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
Canvas->Pen->Mode = pmCopy;
DrawShape();
SetMouseUpPos(X, Y);
}
bool TMandelForm::Run()
{
int i, j, Steps;
float XStep, YStep, XPos, YPos, XOrg, YOrg;
float XMax, YMax, XIter, YIter;
bool Done;
if (FDepth < 1)
FDepth = 50;
InvalidateRect(Handle, NULL, True);
GetOriginsAndWidths(XOrg, YOrg, XMax, YMax);
XStep = (XMax - XOrg) / Width;
YStep = (YMax - YOrg) / Height;
for (i = 0; i <= Width; i++)
for (j = 0; j <= Height; j++)
{
XPos = XOrg + i * XStep;
YPos = YOrg + j * YStep;
XIter = 0.0;
YIter = 0.0;
Steps =0;
Done = False;
do {
Calculate(XPos, YPos, XIter, YIter);
Steps++;
if (Distance(XIter, YIter) >= 2.0)
Done = True;
if (Steps == FDepth)
Done = True;
} while (!Done);
if (Steps < FDepth)
SetPixel(Canvas->Handle, i, j, GetColor(Steps));
Application->ProcessMessages();
if (FQuitDrawing)
break;
}
return True;
}
void __fastcall TMandelForm::FormResize(TObject *Sender)
{
SetBoundary(0, 0, ClientWidth, ClientHeight);
}
void __fastcall TMandelForm::Timer1Timer(TObject *Sender)
{
Timer1->Enabled = False;
Run();
}
void __fastcall TMandelForm::FormDblClick(TObject *Sender)
{
FQuitDrawing = True;
}
void __fastcall TMandelForm::FormMouseMove(TObject *Sender, TShiftState
Shift,
int X, int Y)
{
if (FDrawing)
{
Canvas->Pen->Mode = pmNotXor;
if (FShapeRect.Right != -32000)
DrawShape();
FShapeRect.Right = X;
FShapeRect.Bottom = Y;
DrawShape();
}
}
//----------------------------------
When this program is launched, you see the opening form shown in Figure 7.5.
The menu for this form will let you select secondary forms that draw fractal
ferns (Figure 7.6), squares (Figure 7.7), and a rather hastily thrown together
version of the Mandelbrot set (Figure 7.8).
FIGURE 7.5. The main form for the Fractals
program features a bitmap made in Caligari's TrueSpace program.
FIGURE 7.6. A fractal fern drawn with VCL
code.
FIGURE 7.7. These squares are animated and
appear to be swirling away from the user.
FIGURE 7.8. You can draw a square on the
form with the mouse to zoom in on any part of the Mandelbrot set.
This book is not about mathematics, so I will not give an in-depth explanation
of any of these forms. Instead, I will simply give a quick overview of each of
the major forms from the program. The key point to grasp is simply that you can
use the VCL to create programs that create rather elaborate graphics.
The Fern form uses a global variable called Count to check
the depth of detail to which the fern is drawn. When the detail reaches a
certain point, the image is erased, and the drawing is begun anew.
The output for the Fern program is handled by a single routine found in the
DoPaint method that draws a point to the screen:
Canvas->Pixels[(x * MaxY / 11 + MaxX / 2)]
[(y * - MaxY / 11 + MaxY)] = clGreen;
A Timer component dropped on the form calls the DoPaint
method at periodic intervals. By laboriously drawing pixels to the screen, one
at a time, the DoPaint method slowly builds up a fractal image of a
fern. Needless to say, it is the array-based math in the program that calculates
where to draw the pixels so that the fern takes on a "life of its own." The
Squares form creates the illusion that it is animating a set of squares by
rotating them one inside the next, so that they appear to be receding into the
distance as they spin away from the user. In fact, the code only calculates how
to draw the squares once, and then animates the palette with which the squares
are colored:
for (i = 1; i <= BoxCount; i++)
{
DrawSquare(Scale, Theta);
Theta = Theta + 10;
Scale = Scale * 0.85;
Canvas->Pen->Color = Colors[i];
}
for (i = BoxCount - 1; i >= 1; i--)
Colors[i] = Colors[i - 1];
Colors[0] = TColor(RGB(Colors[0] + random(64),
Colors[0] + random(64),
Colors[0] + random(64)));
The first loop shown here draws each square in a different color from an
array of 25 different colors:
#define BoxCount 25
TColor Colors[BoxCount];
The second loop shown above shifts each color up the array by one notch. The
final statement sets the first element in the array to a new color.
When you watch the program in action, each color appears to be racing away
from the user down through the swirling maze of squares. Of course, the squares
themselves aren't really moving; it's just that the colors assigned to them
change with each iteration of the main loop in the program.
The Mandy form uses standard code to draw the Mandelbrot set. The
user can then left-click at one point and drag a square across a portion of the
set that he or she would look to see in more detail. When the user lets go of
the left mouse button, the highlighted part of the image will be redrawn at a
high magnification.
The whole point of the Mandelbrot set, of course, is that there is no end to
the degree of detail with which you can view it. Of course, in this version, if
you zoom in close enough, some of the detail gets lost. You could add code to
the program that would fix this problem, but the current implementation should
provide enough fun for most users.
I provide only minimal code that checks all the possible errors the user
could make, such as clicking once on the form in the same spot:
void TMandelForm::SetMouseUpPos(int ScrX1, int ScrY1)
{
FScrMaxX = ScrX1;
FScrMaxY = ScrY1;
if ((FScrMaxX - FScrOrgX) > 10)
Run();
}
Here you can see that I check to make sure there is at least 10 pixels of
difference between the location where the user clicked the mouse and the
location where he let go of the mouse. This helps to protect the user from
feeding invalid data to the routines that calculate the area to draw. However,
this code is not foolproof, and you might find yourself in a situation where the
code is taking a ridiculously long time to finish its calculations. In that
case, you can just double-click the form to stop the drawing.
You have already seen the rubber band technology used to draw a square on the
form that delineates the next frame to be viewed. The code that performs that
function for the MandyForm is taken verbatim from the DrawShapes
program shown earlier in this chapter.
Summary
In this chapter you have had an overview of graphics programming with the
VCL. I have made no attempt to cover all facets of this subject, but hopefully
you now have enough information to meet most of your needs.
Key subjects covered in this chapter include the TCanvas object,
used for drawing shapes or words on a form or component surface. You also saw
the TFont, Tbrush, and TPen objects, which define the
characteristics of the shapes or letters drawn on a component. The TMetaFile
and TBitmap objects were also introduced, though I never did do
anything with the simple and easy-to-use TIcon object. The last section
of the chapter had a bit of fun drawing flashy images to the screen.
One related subject that was not covered in this chapter is the
TImageList object. If you are interested in this subject, you might want to
look at the KdAddExplore program found on the CD-ROM that accompanies this book.
The program should be in the Chap14 directory. The TImageList
component provides a means of storing a list of images in memory while taking up
the minimum possible amount of system resources. It is much less expensive to
store five images in an image list than it would be to use five separate bitmaps
in your program.
|