Back Home Up Next

Step 6: Painting the window and adding a menu

There are a few flaws with the application from Step 5. The biggest problem is that the drawing window doesn't know how to paint itself. To see this for yourself, try drawing a line in the window, minimizing the application, then restoring it. The line you drew is gone.You can find the source for Step 6 in the files STEP06.CPP and STEP06.RC in the directory EXAMPLES\OWL\TUTORIAL.

Another problem is that the only way the user can access the application is with the mouse. The user can either press the left button to draw a line or the right button to change the pen size.

In Step 6, you'll make it possible for the application to remember the contexts of the window and redraw it. You'll also add some menus to increase the number of ways the user can access the application.

Repainting the window

There are two problems that must be dealt with when you're trying to paint the window:

There must be a way to remember what was displayed in the window.
There must be a way to redraw the window.

Storing the drawing

In the earlier steps of the tutorial application, the line in the window was drawn as the user moved the mouse while holding the left mouse button. This approach is fine for drawing the line, but doesn't store the points in the line for later use.

Because the line is composed of a number of points in the window, you can store each point in the ObjectWindows TPoint class. And because each line is composed of multiple points, you need an array of TPoint objects to store a line. Instead of attempting to allocate, manage, and update an array of TPoint objects from scratch, the tutorial application uses the Borland container class TArray to define a data type called TPoints. It also uses the Borland container class TArrayIterator to define an iterator called TPointsIterator. The definitions of these two types look like this:


typedef  TArray<TPoint>  TPoints;

typedef  TArrayIterator<TPoint>  TPointsIterator;
     

The TDrawWindow class adds a TPoints object in which it can store the points in the line. It actually uses a TPoints *, a protected member called Line, which is set to point to a TPoints array created in the constructor. The constructor now looks something like this:

TDrawWindow::TDrawWindow(TWindow  *parent)
{
    Init(parent,  0,  0);
    DragDC  =  0;
    PenSize  =  1;
    Pen  =  new  TPen(TColor::Black,  PenSize);
    Line  =  new  TPoints(10,  0,  10);
}
     

TPoints

The Borland C++ container class library and the TArray and TArrayIterator classes are explained in detail in Chapter 1 of the Class Libraries Guide. For now, here's a simple explanation of how the TPoints and TPointsIterator container classes are used in the tutorial application. To use the TArray and TArrayIterator classes, you must include the header file classlib\arrays.h.

The TArray constructor takes three parameters, all ints:

The first parameter represents the upper boundary of the array; that is, how high the array count can go.
The second parameter represents the lower boundary of the array; that is, the number at which the array count begins. This parameter defaults to 0, matching the C and C++ convention of starting arrays at member 0.
The third parameter represents the array delta. The array delta is the number of members that are added when the array grows too large to contain all the members of the array.

Here's the statement that allocates the initial array of points in the TDrawWindow constructor:


Line  =  new  TPoints(10,  0,  10);
     

The array of points is created with room for ten members, beginning at 0. Once ten objects are stored in the array, attempting to add another object adds room for ten new members to the array. This lets you start with a small conservative array size, but also alleviates one of the main problems normally associated with static arrays, which is running out of room and having to reallocate and expand the array.

Once you've created an array, you need to be able to manipulate it. The TArray class (and, by extension, the TPoints class) provides a number of functions to add members, delete members, clear the array, and the like. The tutorial application uses only a small number of the functions provided. Here's a short description of each function:

The Add function adds a member to the array. It takes a single parameter, a reference to an object of the array type. For example, adding a TPoint object to a TPoints array would look something like this:
//  Construct  a  TPoints  array  (an  array  of  TPoint  objects)
TPoints  Points(10,  0,  10);

//  Construct  a  TPoint  object
TPoint  p(3,4);

//  Add  the  TPoint  object  p  to  the  array
Points.Add(p);
          
The Flush function clears all the members of an array and resets the number of array members back to the initial array size. It takes no parameters. To clear the array in the previous sample code, the function call would look something like this:
//  Clear  all  members  in  the  Points  array

Points.Flush();
          
The GetItemsInContainer function returns the total number of items in the container. Note that this number indicates the number of actual objects added to the container, not the space available. For example, even though the container may have enough room for 30 objects, it might only contain 23 objects. In this case, GetItemsInContainer would return 23.

TPointsIterator

Iterators-in this case the TPointsIterator type-let you move through the array, accessing a single member of the array at a time. An iterator constructor takes a single parameter, a reference to a TArray of objects (the type of objects in the array is set up by the definition of the iterator). Here's what an iterator looks like when it's set up using the Line member of the TDrawWindow class:


TPointsIterator  i(*Line);
     

Note that Line is dereferenced because the iterator constructor takes a TPoints & for its parameter, and Line is a TPoints *. Dereferencing the pointer makes Line comply with the iterator constructor type requirements.

Once you've created an iterator, you can use it to access each object in the array, one at a time, starting with the first member. In the tutorial application, the iterator isn't used very much and you won't learn much about the possibilities of an iterator from it. But the tutorial does use two properties of iterators that require a note of explanation:

You can move through the objects in the array using the ++ operator on the iterator. This returns a reference to the current object and increments the iterator to the next object in the array. The order in which it performs these two actions depends on whether you use the ++ operator as a prefix or postfix operator. Using it as a prefix operator (for example, ++i) increments the iterator to the next object, then returns a reference to that object. Using it as a postfix operator (for example, i++) returns a reference to the current object, then increments the iterator to the next object. When you attempt to increment the iterator past the last member of the array, the iterator is set to 0. You can use this as a test in any Boolean conditional. For example:

TPointsIterator  i(*Line);
while(i)
    i++;
          
You can also access the current object with the Current function. Calling the current function returns a reference to the current object. You can then perform operations on the object as if it were a regular instance of the object. For example, you can test a point accessed by an iterator against the value of another point:
TPointsIterator  i(*Line);
TPoint  tmp(5,  6);
if  (i.Current()  ==  tmp)
    return  true;
else
    return  false;
      

Using the array classes

Once the Line array is created in the TDrawWindow constructor, it is accessed in four main places:

The EvLButtonDown function. The array is flushed at the beginning of the function before the screen is invalidated. The beginning point of the line is then inserted towards the end of the function. The EvLButtonDown function should look something like this:
void
TDrawWindow::EvLButtonDown(uint,  TPoint&  point)
{
    Line->Flush();
    Invalidate();
    if  (!DragDC)  {
        SetCapture();
        DragDC  =  new  TClientDC(*this);
        DragDC->SelectObject(*Pen);
        DragDC->MoveTo(point);
        Line->Add(point);
    }
}
          
The EvMouseMove function. Each point in the line is added to the array as the user draws in the window. The EvMouseMove function should look something like this:
void
TDrawWindow::EvMouseMove(uint,  TPoint&  point)
{
    if  (DragDC)  {
        DragDC->LineTo(point);
        Line->Add(point);
    }
}
          
The Paint function. This function is described in the next section.
The CmFileNew function. This function is described in Implementing the event handlers.

Paint function

In standard C Windows programs, if you need to repaint a window manually, you catch the WM_PAINT messages and do whatever you need to do to repaint the screen. This might lead you to think that the proper way to repaint the window in the TDrawWindow class is to add the EV_WM_PAINT macro to the class' response table and set up a function called EvPaint.

You can do this if you want. However, a better way is to override the TWindow function Paint. TDrawWindow's base class TWindow actually does quite a bit of work in its EvPaint function. It sets up the BeginPaint and EndPaint calls, creates a device context for the window, and so on.

Paint is a virtual member of the TWindow class. TWindow's EvPaint calls it in the middle of its processing. The default Paint function doesn't do anything. You can use it to provide the special processing required to draw a line from a TPoints array.

Here is the signature of the Paint function. This is added to the TDrawWindow class:

void  Paint(TDC&,  bool,  TRect&);
    

where:

The first parameter is the device context set up by the calling function. This is the device context you should use when working.
If the second parameter is true, you are supposed to clear the device context before painting the window. If it's false, you are supposed to paint over what is already contained in the window.
The third parameter indicates the invalid area of the device context that needs to be repainted.

In the current case, you always want to clear the window. You can also assume that the entire area of the drawing needs to be repainted. The Paint function implements this basic algorithm:

  1. Create an iterator to go through the points in the line.
  2. Select the pen into the device context passed into the Paint function.
  3. If this is the first point in the array, set the current point to the coordinates contained in the current array member.
  4. While there are still points left in the array, draw lines from the current point to the point contained in the current array member. The TDrawWindow::Paint function now looks something like this:
    void
    TDrawWindow::Paint(TDC&  dc,  bool,  TRect&)
    {
        bool  first  =  true;
        TPointsIterator  i(*Line);
    
        dc.SelectObject(*Pen);
    
        while  (i)  {
          TPoint  p  =  i++;
    
          if  (!first)
            dc.LineTo(p);
          else  {
            dc.MoveTo(p);
            first  =  false;
          }
        }
    }
              

    Menu commands

    There are a number of steps you need to perform to add a menu choice and its corresponding event handler to your application:

    1. Define the event identifier for the menu choice. By convention, this identifier is all capital letters, and begins with CM_. For example, the identifier for the File Open menu choice is CM_FILEOPEN.
    2. Add the appropriate menu resource to your resource file.
    3. Add an event-handling function for the menu choice to your class. The ObjectWindows 2.5 convention is to name this function the same name as the event identifier, except omitting the underscore and using initial capital letters and lowercase letters for the rest. For example, the function that handles the CM_FILEOPEN event is named CmFileOpen.
    4. Add an EV_COMMAND macro to your class' response table, associating the event identifier with the event-handling function. This macro takes two parameters; the first is the event identifier and the second is the name of the event-handling function. For example, the response table entry for the File Open menu choice looks like this:
      
      EV_COMMAND(CM_FILEOPEN,  CmFileOpen),
                      
    5. The EV_COMMAND macro requires the signature of the event-handling function to take no parameters and return void. So the signature of the event-handling function for the File Open menu choice looks like this:
      
      void  CmFileOpen();
                        

      Adding event identifiers

      You need to add identifiers for each of these menu choices. Here's the definition of the event identifiers:

      #define  CM_FILENEW     201
      #define  CM_FILEOPEN    202
      #define  CM_FILESAVE    203
      #define  CM_FILESAVEAS  204
      #define  CM_ABOUT       205
                        

      These identifiers are contained in the file STEP06.RC. The ObjectWindows style places the definitions of identifiers in the resource script file, instead of a header file. This cuts down on the number of source files required for a project, and also makes it easier to maintain the consistency of identifier values between the resources and the application source code.

      The actual resource definitions in the resource file are contained in a block contained in an #ifndef/#endif block, like so:

      #ifdef  RC_INVOKED
          //  Resource  definitions  here.
          
      #endif
                        

      RC_INVOKED is defined by all resource compilers, but not by C++ compilers. The resource information is never seen during C++ compilation. Identifier definitions should be placed outside this #ifndef/#endif block, usually at the beginning of the file.

      Adding menu resources

      For now, you want to add five menu choices to the application:

      File | New
      File | Open
      File | Save
      File | Save As
      Help | About

      Each of these menu choices needs to associated with the correct event identifier; that is, the File Open menu choice should send the CM_FILEOPEN event.

      The menu resource is attached to the application in the InitMainWindow function. You need to call the main window's AssignMenu function. To get the main window, you can call the GetMainWindow function. The InitMainWindow function should look like this:

      void  InitMainWindow()
      {
          SetMainWindow(new  TFrameWindow(0,  "Drawing  Pad",  new  TDrawWindow));
          GetMainWindow()->AssignMenu("COMMANDS");
      }
                        

      Adding response table entries

      Each event identifier needs to be associated with its corresponding handler. To do this, add the following lines to the response table:

      EV_COMMAND(CM_FILENEW,  CmFileNew),
      EV_COMMAND(CM_FILEOPEN,  CmFileOpen),
      EV_COMMAND(CM_FILESAVE,  CmFileSave),
      EV_COMMAND(CM_FILESAVEAS,  CmFileSaveAs),
      EV_COMMAND(CM_ABOUT,  CmAbout),
                        

      Adding event handlers

      Now you need to add a function to handle each of the events you've just added to the response table. Because these functions will eventually grow rather large, you should declare them in the class declaration and define them outside the class declaration.

      The declarations of these function should look something like this:

      void  CmFileNew();
      void  CmFileOpen();
      void  CmFileSave();
      void  CmFileSaveAs();
      void  CmAbout();
                        

      Implementing the event handlers

      The last step in implementing the event handlers is defining the functions. For now, leave the implementation of these functions to a bare minimum. Most of them can just pop up a message box saying that the function has not yet been implemented. The functions that are set up this way are CmFileOpen, CmFileSave, CmFileSaveAs, and CmAbout. Here's how these functions look:

      void
      TDrawWindow::CmFileOpen()
      {
          MessageBox("Feature  not  implemented",  "File  Open",  MB_OK);
      }
                        

      The only function that's implemented in this step is the CmFileNew function. That's because it's very easy to set up. All that needs to be done is to clear the array of points and erase the window. The CmFileNew function looks like this:

      void
      TDrawWindow::CmFileNew()
      {
          Line->Flush();
          Invalidate();
      }
                        

      Where to find more information

      Here's a guide to where you can find more information on the topics introduced in this step:

      Event handling is discussed in "Event handling" of the ObjectWindows Programmer's Guide.
      Window classes are discussed in "Windows objects" of the ObjectWindows Programmer's Guide.
      Menus and menu objects are explained in "Menu objects" of the ObjectWindows Programmer's Guide.
      The Borland C++ container class library and the TArray and TArrayIterator classes are explained in Chapter 1 of the Class Libraries Guide.
 


Copyright © 1998-2001 Yura Bidus. All rights reserved.