Some of the most interesting aspects of C++Builder are the tools it offers for the production of graphic applications. In this article, we'll demonstrate how you can use these tools quickly and simply by building a simple painter and applying some techniques of graphic rendering.
Freehand drawing
| Circle and rectangle drawing
| Filling an area with a color
| Selecting a color at a given point in an image
| Selecting a color from a palette
| Opening and saving an image file in the BMP format
| |
Now, select the Panel component on the Standard palette, and place it inside the form. This object will contain the colors used; give it a meaningful name, such as PanelColors. Click on the Align property in the Object Inspector and choose the option alBottom. Doing so will place the panel above the status bar, using all the available space.
You must place another panel on the form to contain the speed buttons. These buttons are the real heart of the program, since the user will click them to select an action to follow. Call this new panel PanelButtons, and set the Align property to alLeft.
Now you've arrived at the most important component of the application, which allows the realization of the true graphic operations. Select the PaintBox component from the System palette and place it in the form. Change the Align property to alClient, so that the component uses all the available space. (Note that this component encapsulates the TCanvas class, one of the most important classes oriented to VCL graphics. We'll discuss some of its characteristics in a moment.) To highlight the fact that the area the PaintBox component uses is dedicated to drawing, change the Cursor property to crCross. Now the mouse pointer's shape will turn into a small cross when the mouse scans above the paint box.
At this stage, you can begin adding the buttons that will affect the operations of the painter. Choose the SpeedButton component from the Additional palette and place eight buttons inside PanelButtons, as shown in Figure A.
Figure A: Eight speed buttons let the user control the painter's actions.
The first two buttons open and save the image; name these buttons sbFileOpen and sbFileSave. Click on the Glyph property to choose an image to associate with each button (the images Fileopen and Filesave in the directory \CBuilder\Images\Buttons are good for this purpose). Name the other buttons sbDrawPencil, sbFloodFill, sbPalette, sbPickColor, sbDrawRectangle, and sbDrawCircle. Note that since these six buttons represent the user's actions, you must set each button's AllowAllUp property to false and set the GroupIndex property equal to 1. Assign meaningful icons so the user will immediately recognize the buttons; the icons we used came from the source file.
The last two essential visual components for the painter are two small panels. These panels will show the primary color associated with the left button on the mouse and the secondary color associated with the right button. Create two panels inside PanelColors and call them pPenColor and pBrushColor. Size these panels to 25 by 25 pixels; and, for an improved aesthetic effect, change the BevelInner and BevelOuter properties to bvLowered.
At this point you need to add the non-visual components for opening and saving files and for selecting color. These components, which are dialog boxes, reside on the Dialog palette: OpenDialog, SaveDialog, and ColorDialog. You can position them wherever you like, since they won't appear at runtime. Your application should now appear as shown in Figure B.
Figure B: The completed application form looks like this.
Starting the application
| Recognizing and executing the graphic applications
| |
pPenColor->Color = clBlack; pBrushColor->Color = clWhite; PaintBox->Canvas->Pen->Color = pPenColor->Color; PaintBox->Canvas->Brush->Color = pBrushColor->Color;Now, the colors shown in the two small panels are really those employed by Canvas in the PaintBox. Let's move on. The user can perform six actions--draw freehand, fill an area, select a color from the palette or from the image, and draw circles and rectangles--but can do only one action at a time. Graphically, the speed buttons display this limitation by releasing each clicked button as another is clicked. This action is also handy (as you'll see) for defining the application internally.
In the Code Editor, right-click and select the first option, Open Source/Header File, from the speed menu. Given the simplicity of the situation, you can employ within MainPainterForm.h a type that contains all the possible actions, as follows:
enum TUserAction {aDrawPencil, aFloodFill,
aChooseColor, aPickColor, aDrawRectangle,
aDrawCircle, noAction};
Now,
in the public section of the TFormPainter class declaration, add a variable of
type TUserAction, as follows:
/*** class definition ***/
public:
TUserAction UserAction;
This variable will count each action as the user runs it.
How do you use UserAction? First, it must be filled when the user clicks one of
the six buttons. A good way to do this is to use a shared event handler. Select
a button on the form, then hold down the [Shift] key and select the other five
buttons. Press [F11] to open the Object Inspector. Then, select the OnClick
event, type the word SelectAction, and press [Enter]. The Code Editor
will open with the following method:
void __fastcall
TFormPainter::SelectAction(TObject *Sender)
{
}
In
this way, every time the user clicks a design button, the SelectAction method
will be invoked. You can determine which button invoked the event handler
through the Sender parameter. Filling the UserAction variable then becomes
easy:if (Sender == sbDrawPencil) UserAction = aDrawPencil; if (Sender == sbDrawCircle) UserAction = aDrawCircle;Now you have all the necessary tools to implement the various graphic operations.
A simple click
| A movement while one of the mouse buttons is held down
| |
void __fastcall
TFormPainter::PaintBoxMouseDown(
TObject *Sender,
TMouseButton Button, TShiftState Shift,
int X, int Y)
{
switch (UserAction)
{
case aDrawPencil: /*** some code ***/
break;
case aFloodFill: /*** some code ***/
break;
/*** other actions ***/
}
}
In the related case of aDrawPencil, you need to turn on a pixel in the mouse position and handle the mouse click. Fortunately, the method parameters provide all the necessary information: X and Y contain the position of the mouse, while Button contains the button pressed. To turn on a pixel, you can use the Pixels property of Canvas in PaintBox:
case aDrawPencil:
if (Button == mbLeft)
PaintBox->Canvas->Pixels [X][Y] =
pPenColor->Color;
else if (Button == mbRight)
PaintBox->Canvas->Pixels [X][Y] =
pBrushColor->Color;
break;
Have a go at compiling the code, and have some fun! When you're finished,
you'll see something a little strange: If you hold down the mouse button and
move the mouse, the application doesn't draw a continuous line of points--only
a few of the pixels turn on. This happens because you haven't yet dealt with
the management of the moving mouse. Let's fix that.
Select the PaintBox object, go again to the Object Inspector, and double-click on OnMouseMove. The PaintBoxMouseMove method that appears in the Code Editor will be invoked at runtime every time the user moves the mouse above the component. As in the last example, you must insert a switch to filter the user's actions.
For freehand drawings, however, it isn't enough to turn on just one pixel, because the mouse's movement can be so fast that the drawn line won't be continuous. To solve the problem, you can take advantage of a little trick: Draw a line between the current position of the mouse and its previous position. You can do so thanks to two methods in the TCanvas class: MoveTo and LineTo.
Unfortunately, the OnMouseMove event doesn't give you information about the previous position of the mouse. So, you'll need to add OldMouseX and OldMouseY variables to the TFormPainter class. You must initialize these variables at the press of a button and update them during the mouse's movement. You then have a situation like the following:
void __fastcall TFormPainter::PaintBoxMouseMove(
TObject *Sender,
TShiftState Shift, int X, int Y))
{
switch (UserAction)
{
case aDrawPencil:
if (Shift.Contains (ssLeft)) {
PaintBox->Canvas->MoveTo (OldMouseX,
OldMouseY);
PaintBox->Canvas->LineTo (X, Y);
OldMouseX = X;
OldMouseY = Y;
}
break;
/*** other actions ***/
}
}
First, you need to check whether a button has been clicked, in this case by
using the Contains method of the Shift set. If a button has been clicked, you
move the first point of the drawing to the last position of the mouse, thanks
to the MoveTo method. Then you trace a line to the current position using the
LineTo method. Finally, you update the variables. (In the source code, the
right mouse button is also checked.)
Now, let's look at how you fill an area. Doing so is fairly simple, since the TCanvas class provides a function. Within the PaintBoxMouseDown method, in the case relative to the flood fill, insert the following code:
case aFloodFill:
PaintBox->Canvas->FloodFill (
X, Y, PaintBox->Canvas->Pixels[X][Y],
fsSurface);
break;
The FloodFill method fills an area with the color of the brush. This method
needs to know the first point to fill, so you'll use X and Y, the coordinates
of the mouse. fsSurface specifies that the fill must occur in an area where a
color is well-defined. In this case, you'll take the color of the pixel
relative to the position of the mouse, which is indicated by PaintBox->
Canvas-> Pixels[X][Y]. As an alternative, you can use fsBorder in place of
fsSurface. When you do, you can fill the area as long as it doesn't contain the
indicated color.
case aDrawRectangle: case aDrawCircle: OldMouseX = X; OldMouseY = Y; break;In the PaintBoxMouseMove method, on the other hand, you can draw a rectangle using the Rectangle method. This method automatically uses the pen color for the rectangle's borders and the brush color for its inside. The same is true for a circle, using the Ellipse method:
case aDrawRectangle:
if (Shift.Contains (ssLeft))
PaintBox->Canvas->Rectangle (
OldMouseX, OldMouseY, X, Y)
break;
Let's move on to setting colors. When we discussed the flood fill, you
saw how to determine which color is in a given position within the drawing
area. If the user clicks the sbPickColor button, you proceed in a similar
fashion: If the left button is clicked, you assign the chosen color to the pen;
if the right button is clicked, you assign the chosen color to the brush.
Insert the following code into the PaintBoxMouseDown method:
case aPickColor:
TColor color = PaintBox->Canvas->Pixels [X][Y];
if (Button == mbLeft) {
pPenColor->Color = color;
PaintBox->Canvas->Pen->Color = color;
} else if (Button == mbRight) {
pBrushColor->Color = color;
PaintBox->Canvas->Brush->Color = color;
}
break;
To make things easier when choosing a color from a palette, you can just insert
this code directly into the SelectAction method. Fortunately, the ColorDialog
dialog box will do all the work for you. It behaves in an analogous way to the
other dialog boxes: After you execute it via the Execute method, if it's
successful, it assigns the color chosen by the user to the Color
property:
if (ColorDialog->Execute())
{
pPenColor->Color = ColorDialog->Color;
PaintBox->Canvas->Pen->Color =
colorDialog->Color;
}