April 1998

Building graphic applications

by A. Fasano

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. 

What's a painter?

A painter is a program that lets you create drawings and manipulate images. To give you a better idea, think in terms of Microsoft Paint in Windows. Starting with something simple, we'll develop a painter that contains the basic graphic operations--after that, you can expand its characteristics as you wish. Our program must support the following tasks:
bullet Freehand drawing
bullet Circle and rectangle drawing
bullet Filling an area with a color
bullet Selecting a color at a given point in an image
bullet Selecting a color from a palette
bullet Opening and saving an image file in the BMP format
Now that we've established the objectives of the application, let's take the first step in its development: designing the application.

Designing the form

To begin, start C++Builder and choose File | New Application. Save the project in a directory of your choice, renaming Unit 1 as MainPainterForm and Project1 as Painter. Then, select the form, press [F11] to move to the Object Inspector, and change the Name property to PainterForm. Next, you'll insert four elements, each of which has its own well-defined task. First, select the StatusBar component on the Win95 palette and position it inside the form (it doesn't make any difference where you click the mouse--C++Builder automatically places the status bar at the bottom of the form). The status bar will give information to the user--for example, the current position of the mouse.

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.
[ Figure A ]

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.
[ Figure B ]

Let's draw!

Finally, you can set about writing a few lines of code. You need to keep the following tasks in mind:
bullet Starting the application
bullet Recognizing and executing the graphic applications
The solution for the first task is fairly simple: For now, just assign a pen color and a brush color. To do so, select the form, go to the Object Inspector, and double-click on the OnCreate event to open the Code Editor. Insert the following lines of code into the new method:
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.

The pencil draw and the flood fill

Suppose the user clicks the sbDrawPencil button. It's possible that he'll move the mouse pointer over the paint box and, holding down either the left or right mouse button, move the mouse to draw something. In this case, you must intercept two mouse actions:
bullet A simple click
bullet A movement while one of the mouse buttons is held down
The first action doesn't create any great problems. Select the PaintBox object on the form, then double-click on the OnMouseDown event in the Object Inspector. In the code editor, you'll see the PaintBoxMouseDown method, which is invoked every time the user single-, double-, or triple-clicks above the PaintBox object. Within this method, going by the user's action (with the value of UserAction), you can have various solutions. A switch handles the task of filtering, as follows:
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.

Drawing rectangles and circles, and picking a color

Drawing circles and rectangles is really very simple, since TCanvas offers some ready-made methods. To draw, the user must click the sbDrawRectangle or sbDrawCircle button, then move the mouse while holding down the left button to indicate the initial point of the drawing. Place the following code in the PaintBoxMouseDown method:
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;
  }

Conclusion

In this article, we've shown you how to produce some simple graphic elements, primarily using the characteristics of the TCanvas class. We also began developing a drawing program, analyzing the various problems associated with it. In a future article, we'll increase this program's potential by introducing the concept of offscreen bitmaps.