[Contents] [Prev] [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:

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:

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:

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:

Using the array classes

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

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:

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:

      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:



      [Contents] [Prev] [Next]