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.
The parameters for the constructor in this case are different from the parameters used in creating the decorated MDI frame used in Step 11.
TDecoratedMDIFrame* frame = new TDecoratedMDIFrame("Drawing Pad", 0, *(Client = new TMDIClient)true);
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.
The call to the SetMenuDescr function should now look like this:
GetMainWindow()->SetMenuDescr(TMenuDescr("COMMANDS"));
SetDocManager(new TDocManager(dmMDI | dmMenu));
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(); }
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(); }
void TDrawApp::EvCloseView(TView& /*view*/) { // nothing needs to be done here for MDI }
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:
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)
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; }
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:
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); }
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.
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 &.
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:
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.
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(); }
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 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.
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.
static const char far* StaticName() {return "DrawList View";}
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; }
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); }
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; }