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:
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.
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.
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.
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 |
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.
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.
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:
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:
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:
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:
That's all there is to it.
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.
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.
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:
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.
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.
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.
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.
©Copyright, Macmillan Computer Publishing. All rights reserved.