[Contents] [Prev] [Next]

Step 13: Moving the DOC/View application to MDI

The Doc/View model is much more useful when it is used in a multiple-document interface (MDI) application. The ability to have multiple child windows in a frame lets you open more than one view for a document. You can find the source for Step 13 in the files STEP13.CPP, STEP13.RC, STEP13DV.CPP, and STEP13DV.RC in the directory EXAMPLES\OWL\TUTORIAL.

In Step 13, you'll add MDI capability to the application. This requires new functionality in the TDrawDocument and TDrawView classes. In addition, you'll add new features such as the ability to delete or modify an existing line and the ability to undo changes. You'll also create a new view class called TDrawListView to take advantage of the ability to display multiple views. TDrawListView shows an alternate view of the drawing stored in TDrawDocument, displaying it as a list of line information.

Supporting MDI in the application

STEP13.CPP contains the code for the application object and the definition of the main window. The application object provides a framework for the Doc/View classes defined in STEP13DV.CPP. This section discusses the changes to the TDrawApp class that are required to provide MDI support for your Doc/View application. The OwlMain function remains unchanged.

Changing to a decorated MDI frame

To support an MDI application, you need to change the TDecoratedFrame you've been using to a TDecoratedMDIFrame. Then, inside the decorated MDI frame, you need to create an MDI client window with the class TMDIClient. To easily locate the client window later, add a TMDIClient * to your TDrawApp class. Call the pointer Client. This client window contains the MDI child windows that display the various views.

The parameters for the constructor in this case are different from the parameters used in creating the decorated MDI frame used in Step 11.

The window constructor should look like this:

TDecoratedMDIFrame* frame = new TDecoratedMDIFrame("Drawing Pad", 0,
                                 *(Client = new TMDIClient)true);

Changing the hint mode

You might have noticed in Step 12 that the hint text for control bar buttons didn't appear until you actually press the button. You can change the hint mode so that the text shows up when you just run the mouse over the top of the button.

To make this happen, call the control bar's SetHintMode function with the TGadgetWindow::EnterHints parameter:

cb->SetHintMode(TGadgetWindow::EnterHints);
This causes hints to be displayed when the cursor is over a button, even if the button isn't pressed. You can reset the hint mode by calling SetHintMode with the TGadgetWindow::PressHints parameter. You can also turn off menu tracking altogether by calling SetHintMode with the TGadgetWindow::NoHints parameter.

Setting the main window's menu

You need to change the SetMenuDescr call a little. The COMMANDS menu resource has been expanded to provide placeholder menus for the document manager's and views' menu descriptors. Also, the decorated MDI frame provides window management functions, such as cascading or tiling child windows, arranging the icons of minimized child windows, and so on.

The call to the SetMenuDescr function should now look like this:

GetMainWindow()->SetMenuDescr(TMenuDescr("COMMANDS"));

Setting the document manager

You also need to change how you create the document manager in an MDI application. The only change you need to make in this case is to change the dmSDI flag to dmMDI. You need to keep the dmMenu flag:

SetDocManager(new TDocManager(dmMDI | dmMenu));

InitInstance function

You need to make one change to the InitInstance function: remove the call to CmFileNew. This makes the frame open with no untitled documents. In the SDI application, opening the frame with an untitled document was OK. If the user opened a file, the untitled document was replaced by the new document. But in an MDI application, if the user opens an existing document, the untitled document remains open, requiring the user to close it before it'll go away.

Opening a new view

When you open a new view, you must provide a window for the view. In Step 12, EvNewView used the same client window again and again for every document and view. In an MDI application, you can open numerous windows in the EvNewView function. Each window you open inside the client area should be a TMDIChild. You can place your view inside the TMDIChild object by calling the view's GetWindow function for the child's client window.

Once you've created the TMDIChild object, you need to set its menu descriptor, but only if the view has a menu descriptor itself. After setting the menu descriptor, call the MDI child's Create function.

The EvNewView function should now look something like this:

void
TDrawApp::EvNewView(TView& view)
{
  TMDIChild* child = new TMDIChild(*Client, 0, view.GetWindow());
  if (view.GetViewMenu())
    child->SetMenuDescr(*view.GetViewMenu());
  child->Create();
}

Modifying drag and drop

In the SDI version of the tutorial application, you had to check to make sure the user didn't drop more than one file into the application area. But in MDI, if the user drops in more than one file, you can open them all, with each document in a separate window. Here's how to implement the ability to open multiple files dropped into your application:

  1. Find the number of files dropped into the application. Use the DragQueryFileCount function. Use a for loop to iterate through the files.
  2. For each file, get the length of its path and allocate a char array with enough room. Call the DragQueryFile function with the file's index (which you can track using the loop counter), the char array, and the length of the path.
  3. Once you've got the file name, you can call the document manager's MatchTemplate function to get the proper template for the file type. This is done the same way as in Step 12.
  4. Once you've located a template, call the template's CreateDoc function with the file path as the parameter to the function. This creates a new document and its corresponding view, and opens the file into the document.
  5. Once all the files have been opened, call the DragFinish function. This function releases the memory that Windows allocates during drag and drop operations.
Here's how the new EvDropFiles function should look:

void
TDrawApp::EvDropFiles(TDropInfo dropInfo)
{
 int fileCount = dropInfo.DragQueryFileCount();
  for (int index = 0; index < fileCount; index++) {
    int fileLength = dropInfo.DragQueryFileNameLen(index)+1;
    char* filePath = new char [fileLength];
    dropInfo.DragQueryFile(index, filePath, fileLength);
    TDocTemplate* tpl = GetDocManager()->MatchTemplate(filePath);
    if (tpl)
      tpl->CreateDoc(filePath);
    delete filePath;
  }
  dropInfo.DragFinish();
}

Closing a view

In Step 12, when you wanted to close a view, you had to remove the view as a client window, restore the main window's menu, and reset the main window's caption. You no longer need to do any of this, because these tasks are handled by the MDI window classes. Here's how your EvCloseView function should look:

void
TDrawApp::EvCloseView(TView& /*view*/)
{  // nothing needs to be done here for MDI
}

Changes to TDrawDocument and TDrawView

You need to make the following changes in the TDrawDocument and TDrawView classes. These changes include defining new events, adding new event-handling functions, adding document property functions, and more.

Defining new events

First you need to define three new events to support the new features in the TDrawDocument and TDrawView classes. These view notification events are vnDrawAppend, vnDrawDelete, and vnDrawModify. These events should be const ints, and defined as offsets from the predefined value vnCustomBase. Using vnCustomBase ensures that your new events don't overlap any ObjectWindows events.

Next, use the NOTIFY_SIG macro to specify the signature of the event-handling function. The NOTIFY_SIG macro takes two parameters, the event name (such as vnDrawAppend or vnDrawDelete) and the parameter type to be passed to the event-handling function. The size of the parameter type can be no larger than a long; if the object being passed is larger than a long, you must pass it by pointer. In this case, the parameter is just an unsigned int to pass the index of the affected line to the event-handling function. The return value of the event-handling function is always void.

Lastly, you need to define the response table macro for each of these events. By convention, the macro name uses the event name, in all uppercase letters, preceded by EV_VN_. Use the #define macro to define the macro name. To define the macro itself, use the VN_DEFINE macro. Here's the syntax for the VN_DEFINE macro:

VN_DEFINE(eventName, functionName, paramSize)
where:

The full definition of the new events should look something like this:

const int vnDrawAppend = vnCustomBase+0;
const int vnDrawDelete = vnCustomBase+1;
const int vnDrawModify = vnCustomBase+2;

NOTIFY_SIG(vnDrawAppend, unsigned int)
NOTIFY_SIG(vnDrawDelete, unsigned int)
NOTIFY_SIG(vnDrawModify, unsigned int)

#define EV_VN_DRAWAPPEND VN_DEFINE(vnDrawAppend, VnAppend, int)
#define EV_VN_DRAWDELETE VN_DEFINE(vnDrawDelete, VnDelete, int)
#define EV_VN_DRAWMODIFY VN_DEFINE(vnDrawModify, VnModify, int)

Changes to TDrawDocument

TDrawDocument adds some new protected data members:

The TDrawDocument constructor should be modified to initialize UndoLine to 0 and UndoState to UndoNone. The TDrawDocument destructor is modified to delete UndoLine.

You need to modify the Open function slightly to read the file information string from the document file and use it to initialize the FileInfo member. If the document doesn't have a valid document path, initialize FileInfo using the string resource IDS_FILEINFO.

Modify the AddLine function to notify any other views when a line has been added to the drawing. You can use the NotifyViews function with the vnDrawAppend event. The second parameter to the NotifyViews call should be the new line's array index. You also need to set UndoState to UndoAppend. The AddLine function should now look like this:

int
TDrawDocument::AddLine(TLine& line)
{
  int index = Lines->GetItemsInContainer();
  Lines->Add(line);
  SetDirty(true);
  NotifyViews(vnDrawAppend, index);
  UndoState = UndoAppend;
  return index;
}

Property functions

Every document has a list of properties. Each property has an associated value, defined as an enum, by which it is identified. The list of enums for a derived document object should always end with the value NextProperty. The list of enums for a derived document object should always start with the value PrevProperty, which should be set to the NextProperty member of the base class, minus 1.

Each property also has a text string describing the property contained in an array called PropNames and an int containing implementation-defined flags in an array called PropFlags. The property's enum value can be used in an array index to locate the property string or flag for a particular property.

TDrawDocument adds two new properties to its document properties list: LineCount and Description. The enum definition should look like this:

enum {
  PrevProperty = TFileDocument::NextProperty-1,
  LineCount,
  Description,
  NextProperty,
};
By redefining PrevProperty and NextProperty, any class that's derived from your document class can create new properties without overwriting the properties you've defined.

TDrawDocument also adds an array of static char strings. This array contains two strings, each containing a text description of one of the new properties. The array definition should look like this:

static char* PropNames[] = {
  "Line Count",
  "Description",
};
Lastly, TDrawDocument adds an array of ints called PropFlags, which contains the same number of array elements as PropNames. Each array element contains one or more document property flags ORed together, and corresponds to the property in PropNames with the same array index. The PropFlags array definition should look like this:

static int PropFlags[] = {
  pfGetBinary|pfGetText, // LineCount
  pfGetText,             // Description
};
TDrawDocument overrides a number of the TDocument property functions to provide access to the new properties. You can find the total number of properties for the TDrawDocument class by calling the PropertyCount function. PropertyCount returns the value of the property enum NextProperty, minus 1.

You can find the text name of any document property using the PropertyName function. PropertyName returns a char *, a string containing the property name. It takes a single int parameter, which indicates the index of the parameter for which you want the name. If the index is less than or equal to the enum PrevProperty, you can call the TFileDocument function PropertyName. This returns the name of a property defined in TFileDocument or its base class TDocument. If the index is greater than or equal to NextProperty, you should return 0; NextProperty marks the last property in the document class. If the index has the same or greater value than NextProperty, the index is too high to be valid. As long as the index is greater than PrevProperty but less than NextProperty, you should return the string from the PropNames array corresponding to the index. The code for this function should look like this:

const char*
TDrawDocument::PropertyName(int index)
{
  if (index <= PrevProperty)
    return TFileDocument::PropertyName(index);
  else if (index < NextProperty)
    return PropNames[index-PrevProperty-1];
  else
    return 0;
}
The FindProperty function is essentially the opposite of the PropertyName function. FindProperty takes a single parameter, a const char *. It tries to match the string passed in with the name of each document property. If it successfully matches the string with a property name, it returns an int containing the index of the property. The code for this function should look like this:

int
TDrawDocument::FindProperty(const char far* name)
{
  for (int i=0; i < NextProperty-PrevProperty-1; i++)
    if (strcmp(PropNames[i], name) == 0)
      return i+PrevProperty+1;
  return 0;
}
The PropertyFlags function takes a single int parameter, which indicates the index of the parameter for which you want the property flags. These flags are returned as an int. If the index is less than or equal to the enum PrevProperty, you can call the TFileDocument function PropertyName. This returns the name of a property defined in TFileDocument or its base class TDocument. If the index is greater than or equal to NextProperty, you should return 0; NextProperty marks the last property in the document class. If the index has the same or greater value than NextProperty, the index is too high to be valid. As long as the index is greater than PrevProperty but less than NextProperty, you should return the member of the PropFlags array corresponding to the index. The code for this function should look like this:

int
TDrawDocument::PropertyFlags(int index)
{
  if (index <= PrevProperty)
    return TFileDocument::PropertyFlags(index);
  else if (index < NextProperty)
    return PropFlags[index-PrevProperty-1];
  else
    return 0;
}
The last property function is the GetProperty function, which takes three parameters. The first parameter is an int, the index of the property you want. The second parameter is a void *. This should be a block of memory that is used to hold the property information. The third parameter is an int and indicates the size in bytes of the block of memory.

There are three possibilities the GetProperty function should handle:

The code for the GetProperty function should look like this:

int
TDrawDocument::GetProperty(int prop, void far* dest, int textlen)
{
  switch(prop)
  {
    case LineCount:
    {
      int count = Lines->GetItemsInContainer();
      if (!textlen) {
        *(int far*)dest = count;
        return sizeof(int);
      }
      return wsprintf((char far*)dest, "%d", count);
    }
    case Description:
      char* temp = new char[textlen]; // need local copy for medium model
      int len = FileInfo.copy(temp, textlen);
      strcpy((char far*)dest, temp);
      return len;
  }
  return TFileDocument::GetProperty(prop, dest, textlen);
}

New functions in TDrawDocument

Step 13 adds a number of new functions to TDrawDocument. These functions let you modify the document object by deleting lines, modifying lines, clearing the document, and undoing changes.

The first new function is DeleteLine. As its name implies, the purpose of this function is to delete a line from the document. DeleteLine takes a single int parameter, which gives the array index of the line to be deleted.

  1. Delete should check that the index passed in to it is valid. You can check this by calling the GetLine function and passing the index to GetLine. If the index is valid, GetLine returns a pointer to a line object. Otherwise, it returns 0.
  2. Once you have determined the index is valid, you should set UndoLine to the line to be deleted and set UndoState to UndoDelete. This saves the old line in case the user requests an undo of the deletion.
  3. You should then detach the line from the document using the container class Detach function. This function takes a single int parameter, the array index of the line to be deleted.
  4. Turn the IsDirty flag on by calling the SetDirty function.
  5. Lastly, notify the views that the document has changed by calling the NotifyViews function. Pass the vnDrawDelete event as the first parameter of the NotifyViews call and the array index of the line as the second parameter.
The code for the DeleteLine function should look like this:

void
TDrawDocument::DeleteLine(unsigned int index)
{
  const TLine* oldLine = GetLine(index);
  if (!oldLine)
    return;
  delete UndoLine;
  UndoLine = new TLine(*oldLine);
  Lines->Detach(index);
  SetDirty(true);
  NotifyViews(vnDrawDelete, index);
  UndoState = UndoDelete;
}
The ModifyLine function takes two parameters, a TLine & and an int. The int is the array index of the line to be modified. The affected line is replaced by the TLine &.

  1. As with the DeleteLine function, you need to set up the undo data members before replacing the line. Copy the line to be replaced to UndoLine and set UndoState to UndoModify. You also need to set UndoIndex to the index of the affected line.
  2. Set the line to the TLine object passed into the function.
  3. Turn the IsDirty flag on by calling the SetDirty function.
  4. Lastly, notify the views that the document has changed by calling the NotifyViews function. Pass the vnDrawModify event as the first parameter of the NotifyViews call and the array index of the line as the second parameter.
The code for this function should look like this:

void
TDrawDocument::ModifyLine(TLine& line, unsigned int index)
{
  delete UndoLine;
  UndoLine = new TLine((*Lines)[index]);
  SetDirty(true);
  (*Lines)[index] = line;
  NotifyViews(vnDrawModify, index);
  UndoState = UndoModify;
  UndoIndex = index;
}
The Clear function is fairly straightforward. It flushes the TLines array referenced by Lines, then forces the views to update by calling NotifyViews with the vnRevert parameter. When the views are updated, there's no data in the document, causing the views to clear their windows. The function should look something like this:

void
TDrawDocument::Clear()
{
  Lines->Flush();
  NotifyViews(vnRevert, true);
}
The Undo function has three different types of operations to undo: append, delete, and modify. It determines which type of operation it needs to undo by the value of the UndoState variable:

Here's how the code for the Undo function should look:

void
TDrawDocument::Undo()
{
  switch (UndoState) {
    case UndoAppend:
      DeleteLine(Lines->GetItemsInContainer()-1);
      return;
    case UndoDelete:
      AddLine(*UndoLine);
      delete UndoLine;
      UndoLine = 0;
      return;
    case UndoModify:
      TLine* temp = UndoLine;
      UndoLine = 0;
      ModifyLine(*temp, UndoIndex);
      delete temp;
  }
}
Each operation uses one of these new modification functions. That way, each undo operation can itself be undone.

Changes to TDrawView

TDrawView modifies a number of its functions, including deleting the GetPenSize function. This function should be moved to the TLine class, so that the pen size is set in the line itself. You can call the TLine::GetPenSize function from the CmPenSize function. The same thing should be done with the CmPenColor function; move the functionality of this function to the TLine::GetPenColor function. You can call the TLine::GetPenColor function from the CmPenColor function.

To accommodate the new editing functionality in the TDrawDocument and TDrawView classes, you need to add menu choices for Undo and Clear. These choices should post the events CM_CLEAR and CM_UNDO. The menu requires a change in the menu resource to group the menus properly. The call should look like this:

SetViewMenu(new TMenuDescr(IDM_DRAWVIEW));
You can redefine the right button behavior by changing the EvRButtonDown function (there are now two other ways to change the pen size, the Tools|Pen Size menu command and the Pen Size control bar button). You can use the right mouse button as a shortcut for an undo operation. The EvRButtonDown function should look like this:

void
TDrawView::EvRButtonDown(uint, TPoint&)
{
  CmUndo();
}

New functions in TDrawView

Step 13 adds a number of new functions to TDrawDocument. These functions implement an interface to access the new functionality in TDrawDocument.

You need to override the TView virtual function GetViewName. The document manager calls this function to determine the type of view. This function should return a const char * referencing a string containing the view name. This function should look like this:

const char far* GetViewName() { return StaticName(); }
After adding the new menu items Clear and Undo to the Edit menu, you need to handle the events CM_CLEAR and CM_UNDO. Add the following lines to your response table:

EV_COMMAND(CM_CLEAR, CmClear),
EV_COMMAND(CM_UNDO, CmUndo),
You also need functions to handle the CM_CLEAR and CM_UNDO events. If the view receives a CM_CLEAR message, all it needs to do is to call the document's Clear function:

void
TDrawView::CmClear()
{
  DrawDoc->Clear();
}
If the view receives a CM_UNDO message, all it needs to do is to call the document's Undo function:

void
TDrawView::CmUndo()
{
  DrawDoc->Undo();
}
The other new events the view has to handle are the view notification events, vnDrawAppend, vnDrawDelete, and vnDrawModify. You should add the response table macros for these events to the view's response table:

DEFINE_RESPONSE_TABLE1(TDrawView, TWindowView)
  EV_VN_DRAWAPPEND,
  EV_VN_DRAWDELETE,
  EV_VN_DRAWMODIFY,
END_RESPONSE_TABLE;
The event-handling functions for these macros are VnAppend, VnDelete, and VnModify. All three of these functions return a bool and take a single parameter, an int indicating which line in the document is affected by the event.

The VnAppend function gets notification that a line was appended to the document. It then draws the new line in the view's window. It should create a device context, get the line from the document, call the line's Draw function with the device context object as the parameter, then return true. The code for this function looks like this:

bool
TDrawView::VnAppend(unsigned int index)
{
  TClientDC dc(*this);
  const TLine* line = DrawDoc->GetLine(index);
  line->Draw(dc);
  return true;
}
The VnModify function forces a repaint of the entire window. It might seem more efficient to just redraw the affected line, but you would need to paint over the old line, repaint the new line, and restore any lines that might have crossed or overlapped the affected line. It is actually more efficient to invalidate and repaint the entire window. So the code for the VnModify function should look like this:

bool
TDrawView::VnModify(unsigned int /*index*/)
{
  Invalidate();  // force full repaint
  return true;
}
The VnDelete function also forces a repaint of the entire window. This function faces the same problem as VnModify; simply erasing the line will probably affect other lines. The code for the VnDelete function should look like this:

bool
TDrawView::VnDelete(unsigned int /*index*/)
{
  Invalidate();  // force full repaint
  return true;
}

TDrawListView

The purpose of the TDrawListView class is to display the data contained in a TDrawDocument object as a list of lines. Each line will display the color values for the line, the pen size for the line, and the number of points that make up the line. TDrawListView will let the user modify a line by changing the pen size or color. The user can also delete a line.

TDrawListView is derived from TView and TListBox. TView gives TDrawListView the standard view capabilities. TListBox provides the ability to display the information in the document object in a list.

Creating the TDrawListView class

The TDrawListView constructor takes two parameters, a TDrawDocument & (a reference to the view's associated document) and a TWindow * (a pointer to the parent window). The parent window defaults to 0 if no value is supplied. The constructor passes the first parameter to the TView constructor and initializes the DrawDoc member to point at the document passed as the first parameter.

TDrawListView has two data members, one protected TDrawDocument * called DrawDoc and one public int called CurIndex. DrawDoc serves the same purpose in TDrawListView as it did in TDrawView, namely to reference the view's associated document object. CurIndex contains the array index of the currently selected line in the list box.

The TDrawListView constructor also calls the TListBox constructor. The first parameter of the TListBox constructor is passed the parent window parameter of the TDrawListView constructor. The second parameter of the TListBox constructor is a call to the TView function GetNextViewId. This function returns a static unsigned that is used as the list box identifier. The view identifier is set in the TView constructor. The coordinates and dimensions of the list box are all set to 0; the dimensions are filled in when the TDrawListView is set as a client in an MDI child window.

The constructor also sets some window attributes, including the Attr.Style attribute, which has the WS_BORDER and LBS_SORT attributes turned off, and the Attr.AccelTable attribute, which is set to the IDA_DRAWLISTVIEW accelerator resource defined in STEP13DV.RC.

The constructor also sets up the menu descriptor for TDrawListView. Because TDrawListView has a different function from TDrawView, it requires a different menu. Compare the menu resource for TDrawView and the menu resource for TDrawListView.

Here's the code for the TDrawListView constructor:

TDrawListView::TDrawListView(TDrawDocument& doc,TWindow *parent)
  : TView(doc), TListBox(parent, GetNextViewId(), 0,0,0,0), DrawDoc(&doc)
{
  Attr.Style &= ~(WS_BORDER | LBS_SORT);
  Attr.AccelTable = IDA_DRAWLISTVIEW;
  SetViewMenu(new TMenuDescr(IDM_DRAWLISTVIEW));
}
TDrawListView has no dynamically allocated data members. The destructor therefore does nothing.

Naming the class

Like the TDrawView class, TDrawListView should define the function StaticName to return the name of the view class. Here's how the StaticName function might look:

static const char far* StaticName() {return "DrawList View";}

Overriding TView and TWindow virtual functions

The document manager calls the view function GetViewName to determine the type of view. You need to override this function, which is declared virtual function in TView. This function should return a const char * referencing a string containing the view name. This function should look like this:

const char far* GetViewName() { return StaticName(); }
The document manager calls the view function GetWindow to get the window associated with a view. You need to override this function also, which is declared virtual function in TView. It should return a TWindow * referencing the view's window. This function should look like this:

TWindow* GetWindow() { return (TWindow*) this; }
You also need to supply a version of the CanClose function. This function should call the TListBox version of CanClose and also call the document's CanClose function. This function should look like this:

bool CanClose() {return TListBox::CanClose() && Doc->CanClose();}
You also need to provide a version of the Create function. You can call the TListBox version of Create to actually create the window. But you also need to load the data from the document into the TDrawListView object. To do this, call the LoadData function. You'll define the LoadData function in the next section of this step. The Create function should look something like this:

bool
TDrawListView::Create()
{
  TListBox::Create();
  LoadData();
  return true;
}

Loading and formatting data

You need to provide functions to load data from the document object to the view document and to format the data for display in the list box. These functions should be protected so that only the view can call them.

The first function is LoadData. To load data into the list box, you need to first clear the list of any items that might already be in it. For this, you can call the ClearList function, which is from the TListBox base class. After that, get lines from the document and format each line until the document runs out of lines. You can tell when there are no more lines in the document; the GetLine function returns 0. Lastly, set the current selection index to 0 using the SetSelIndex function. This causes the first line in the list box to be selected. The code for the LoadData function looks something like this:

void
TDrawListView::LoadData()
{
  ClearList();
  int i = 0;
  const TLine* line;
  while ((line = DrawDoc->GetLine(i)) != 0)
    FormatData(line, i++);
  SetSelIndex(0);
}
The FormatData function takes two parameters. The first parameter is a const TLine * that references the line to modified or added to the list box. The second parameter contains the index of the line to modified.

The code for FormatData should look something like this:

void
TDrawListView::FormatData(const TLine* line, int unsigned index)
{
  char buf[80];
  TColor color(line->QueryColor());
  wsprintf(buf, "Color = R%d G%d B%d, Size = %d, Points = %d",
           color.Red(), color.Green(), color.Blue(),
           line->QueryPenSize(), line->GetItemsInContainer());

  DeleteString(index);
  InsertString(buf, index);
  SetSelIndex(index);
}

Event handling in TDrawListView

Here's the response table for TDrawListView:

DEFINE_RESPONSE_TABLE1(TDrawListView, TListBox)
  EV_COMMAND(CM_PENSIZE, CmPenSize),
  EV_COMMAND(CM_PENCOLOR, CmPenColor),
  EV_COMMAND(CM_CLEAR, CmClear),
  EV_COMMAND(CM_UNDO, CmUndo),
  EV_COMMAND(CM_DELETE, CmDelete),
  EV_VN_ISWINDOW,
  EV_VN_COMMIT,
  EV_VN_REVERT,
  EV_VN_DRAWAPPEND,
  EV_VN_DRAWDELETE,
  EV_VN_DRAWMODIFY,
END_RESPONSE_TABLE;
This response table is similar to TDrawView's response table in some ways. The two views share some events, such as the CM_PENSIZE and CM_PENCOLOR events and the vnDrawAppend and vnDrawModify view notification events.

But each view also handles events that the other view doesn't. This is because each view has different capabilities. For example, the TDrawView class handles a number of mouse events, whereas TDrawListView handles none. That's because it makes no sense in the context of a list box to handle the mouse events; those events are used when drawing a line in the TDrawView window.

TDrawListView handles the CM_DELETE event, whereas TDrawView doesn't. This is because, in the TDrawView window, there's no way for the user to indicate which line should be deleted. But in the list box, it's easy: just delete the line that's currently selected in the list box.

TDrawListView also handles the vnIsWindow event. The vnIsWindow message is a predefined ObjectWindows event, which asks the view if its window is the same as the window passed with the event.

The CmPenSize function is more complicated in the TDrawListView class than in the TDrawView class. This is because the TDrawListView class doesn't maintain a pointer to the current line the way TDrawView does. Instead, you have to get the index of the line that's currently selected in the list box and get that line from the document. Then, because the GetLine function returns a pointer to a const object, you have to make a copy of the line, modify the copy, then call the document's ModifyLine function. Here's how the code for this function should look:

void
TDrawListView::CmPenSize()
{
  int index = GetSelIndex();
  const TLine* line = DrawDoc->GetLine(index);
  if (line) {
    TLine* newline = new TLine(*line);
    if (newline->GetPenSize())
      DrawDoc->ModifyLine(*newline, index);
    delete newline;
  }
}
The interesting aspect of this function comes in the ModifyLine call. When the user changes the pen size using this function, the pen size in the view isn't changed at this time. But when the document changes the line in the ModifyLine call, it posts a vnDrawModify event to all of its views:

NotifyViews(vnDrawModify, index);
This notifies all the views associated with the document that a line has changed. All views then call their VnModify function and update their displays from the document. This way, any change made in one view is automatically reflected in other open views. The same holds true for any other functions that modify the document's data, such as CmPenColor, CmDelete, CmUndo, and so on.

The CmPenColor function looks nearly same as the CmPenSize function, except that, instead of calling the line's GetPenSize function, it calls GetPenColor:

void
TDrawListView::CmPenColor()
{
  int index = GetSelIndex();
  const TLine* line = DrawDoc->GetLine(index);
  if (line) {
    TLine* newline = new TLine(*line);
    if (newline->GetPenColor())
      DrawDoc->ModifyLine(*newline, index);
    delete newline;
  }
}
The CM_DELETE event indicates that the user wants to delete the line that is currently selected in the list box. The view needs to call the document's DeleteLine function, passing it the index of the currently selected line. This function should look like this:

void
TDrawListView::CmDelete()
{
  DrawDoc->DeleteLine(GetSelIndex());
}
You also need functions to handle the CM_CLEAR and CM_UNDO events for TDrawListView. If the user chooses the Clear menu command, the view receives a CM_CLEAR message. All it needs to do is call the document's Clear function:

void
TDrawListView::CmClear() {
  DrawDoc->Clear();
}
If the user chooses the Clear menu command, the view receives a CM_UNDO message. All it needs to do is call the document's Undo function:

void
TDrawListView::CmUndo()
{
  DrawDoc->Undo();
}
These functions are identical to the TDrawView versions of the same functions. That's because these operation rely on TDrawDocument to actually make the changes to the data.

Like the TDrawView class, TDrawListView's VnCommit function always returns true. In a more complex application, this function would add any cached data to the document, but in this application, the data is added to the document as each line is drawn.

The VnRevert function calls the LoadData function to revert the list box display to the data contained in the document:

bool
TDrawListView::VnRevert(bool /*clear*/)
{
  LoadData();
  return true;
}
The VnAppend function gets a single unsigned int parameter, which gives the index number of the appended line. You need to get the new line from the document by calling the document's GetLine function. Call the FormatData function with the line and the line index passed into the function. After formatting the line, set the selection index to the new line and return. The function should look like this:

bool
TDrawListView::VnAppend(unsigned int index)
{
  const TLine* line = DrawDoc->GetLine(index);
  FormatData(line, index);
  SetSelIndex(index);
  return true;
}
The VnDelete function takes a single int parameter, the index of the line to be deleted. To remove the line from the list box, call the TListBox function DeleteString:

bool
TDrawListView::VnDelete(unsigned int index)
{
  DeleteString(index);
  HandleMessage(WM_KEYDOWN,VK_DOWN); // force selection
  return true;
}
The call to HandleMessage ensures that there is an active selection in the list box after the currently selected string is deleted.

The VnModify function takes a single int parameter, the index of the line to be modified. You need to get the line from the document using the GetLine function. Call FormatData with the line and its index:

bool
TDrawListView::VnModify(unsigned int index)
{
  const TLine* line = DrawDoc->GetLine(index);
  FormatData(line, index);
  return true;
}

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]