In the sense that it's used here, document doesn't have the same meaning you might be used to. Instead of a paper document or a word-processing document, a document refers to any set of data that your application displays and manipulates. In the case of the tutorial application, documents are the drawing files that the application creates. Converting the application to use MDI adds the ability to support multiple drawings open at the same time in multiple child windows.
In comparison, MDI applications divide tasks up three ways instead of two:
Menu item text | Command identifier |
---|---|
Cascade | CM_CASCADECHILDREN |
Tile | CM_TILECHILDREN |
Arrange Icons | CM_ARRANGEICONS |
Close All | CM_CLOSECHILDREN |
Adding the MDI client and child header files
You need to add the owl\mdi.h and owl\mdichild.h header files. owl\mdi.h contains the definition of the TMDIFrame and TMDIClient classes. TMDIClient provides the functionality necessary for managing MDI child windows. MDI child windows are the windows that the user of your application actually works with and that display the data contained in each document. TMDIClient provides the ability to
You usually derive new classes from both TMDIClient and TMDIChild to provide the specific functionality required by your application. Creating new classes from TMDIClient and TMDIChild to support the Drawing Pad application is discussed later in this step.
Here's the constructor for TDecoratedMDIFrame:
TDecoratedMDIFrame(const char far* title, TResId menuResId, TMDIClient& clientWnd = *new TMDIClient, bool trackMenuSelection = false, TModule* module = 0);where:
TDecoratedMDIFrame *frame = new TDecoratedMDIFrame("Drawing Pad", TResId("COMMANDS"), *new TDrawMDIClient, true);
As before, the frame window caption is Drawing Pad. The frame window is initialized with the COMMANDS menu resource. The client window is a new TDrawMDIClient, which is a TMDIClient-derived class that you'll define a little bit later in this step. The final parameter indicates that menu tracking should be on for this window. The module parameter is left to its default value of 0.
The second change is removing the AssignMenu call at the end of the InitMainWindow function of Step 10. This call is no longer necessary because the menu resource is set up by the second parameter of the TDecoratedMDIFrame constructor.
These are the only changes necessary to the TDrawApp class to support MDI functionality.
Creating the MDI window classes
The functionality contained in the TDrawWindow class in the previous step needs to be divided up into two classes in the MDI model. The reason for this is that there are two windows that handle messages and user input:
Creating the MDI child window class
You need to create a class declaration for the TDrawMDIChild class, along with defining the functions for the class. You can reuse most of the class declaration for TDrawWindow from Step 10, along with most of the functions with only a few changes. Declaring the TDrawMDIChild class
Your class declaration should look something like this:
class TDrawMDIChild : public TMDIChild { public: TDrawMDIChild(TDrawMDIClient& parent, const char* title = 0); ~TDrawMDIChild() { delete DragDC; delete Line; delete Lines; delete FileData; } protected: TDC *DragDC; TPen *Pen; TLines *Lines; TLine *Line; // To hold a single line at a time that later gets // stuck in Lines TOpenSaveDialog::TData *FileData; bool IsDirty, IsNewFile; void GetPenSize(); // GetPenSize always calls Line->SetPen(). // Override member function of TWindow bool CanClose(); // Message response functions void EvLButtonDown(uint, TPoint&); void EvRButtonDown(uint, TPoint&); void EvMouseMove(uint, TPoint&); void EvLButtonUp(uint, TPoint&); void Paint(TDC&, bool, TRect&); void CmFileSave(); void CmFileSaveAs(); void CmPenSize(); void CmPenColor(); void SaveFile(); void OpenFile(); DECLARE_RESPONSE_TABLE(TDrawMDIChild); }; DEFINE_RESPONSE_TABLE1(TDrawMDIChild, TWindow) EV_WM_LBUTTONDOWN, EV_WM_RBUTTONDOWN, EV_WM_MOUSEMOVE, EV_WM_LBUTTONUP, EV_COMMAND(CM_FILESAVE, CmFileSave), EV_COMMAND(CM_FILESAVEAS, CmFileSaveAs), EV_COMMAND(CM_PENSIZE, CmPenSize), EV_COMMAND(CM_PENCOLOR, CmPenColor), END_RESPONSE_TABLE;
Just about all of the functions in TDrawMDIChild can be carried over from the TDrawWindow class. The only thing you need to do is change the class identifier in the function declarations from TDrawWindow to TDrawMDIChild. For example, the declaration for the EvLButtonDown function changes from this:
void TDrawWindow::EvLButtonDown(uint, TPoint& point) { }to this:
void TDrawMDIChild::EvLButtonDown(uint, TPoint& point) { }Change the class identifiers for the following functions:
GetPenSize, EvLButtonDown, EvMouseMove, Paint, CmFileSaveAs, CmPenColor, OpenFile | CanClose, EvRButtonDown, EvLButtonUp, CmFileSave, CmPenSize, SaveFile |
void TDrawMDIChild::CmFileSaveAs() { if (IsNewFile) strcpy(FileData->FileName, ""); if ((TFileSaveDialog(this, *FileData)).Execute() == IDOK) SaveFile(); SetCaption(FileData->FileName); }
The main difference between TDrawMDIChild and the TDrawWindow class, other than the fact that TDrawMDIChild has three fewer functions than TDrawWindow, is in the constructor.
Like TDrawWindow, TDrawMDIChild contains the device context object that displays the drawing and manages the arrays that contain the line drawing information. It also contains the IsDirty flag, setting it to false when the drawing is first created or opened and setting it to true when the drawing is modified. So the variables that contain the data for these functions-DragDC, Line, Lines, and IsDirty-need to be initialized in the TDrawMDIChild constructor. This looks just the same as their initialization in the TDrawWindow class.
DragDC = 0; Lines = new TLines(5, 0, 5); Line = new TLine(TColor::Black, 1); IsDirty = false;There are some notable changes from TDrawWindow's constructor here, however. First, the Init function is no longer called. TMDIChild does not provide an Init function. Instead, you should just call the base class constructor in the TDrawMDIChild initialization list, like so:
TDrawMDIChild::TDrawMDIChild(TDrawMDIClient& parent, const char* title) : TMDIChild(parent, title) { }
You can no longer simply initialize the IsNewFile variable to true, assuming that you are creating a new drawing whenever you create a window. In earlier steps this was a valid assumption: when the window was created, it hadn't opened a file yet, but was available to be drawn in. The IsNewFile flag was only set to false once a drawing had either been saved to a file or an existing drawing had been opened from a file into a window that had already been created.
In this case, the MDI client parent window will handle the file creation and opening operations. It then creates a child window to contain the new or existing drawing. The child window has to find out from the parent whether this is a new drawing or an existing drawing opened from a file.
For the same reason, the MDI child window does not necessarily create the TOpenSaveDialog::TData referenced by the FileData member. The TDrawMDIClient class has a function (or will have, when you get around to creating it) called GetFileData. This function takes no parameters and returns a pointer to a TOpenSaveDialog::TData object. If the MDI client window is creating the child window in response to a CM_FILEOPEN event, it creates a new TOpenSaveDialog::TData object containing the information about the file to be opened. GetFileData returns a pointer to that object. But if the client window is creating the child window in response to a CM_FILENEW event, TDrawMDIClient doesn't create a TOpenSaveDialog::TData object and GetFileData returns 0.
So the MDI child can find out whether this is a new drawing or not by testing the return value of GetFileData. If GetFileData returns a valid object, then it should assign the pointer to this object to its FileData member and set IsNewFile to false. It can then call the OpenFile function to load the drawing just as it did before. If GetFileData doesn't return a valid object (that is, it returns 0), the MDI child should set IsNewFile to true and create a new TOpenSaveDialog::TData object. The file name in the new object is set in the CmFileSaveAs function, just as it was in previous steps.
The constructor for TDrawMDIChild should look something like this:
TDrawMDIChild::TDrawMDIChild(TDrawMDIClient& parent, const char* title) : TMDIChild(parent, title) { DragDC = 0; Lines = new TLines(5, 0, 5); Line = new TLine(TColor::Black, 1); IsDirty = false; // If the parent returns a valid FileData member, this is an open operation // Copy the parent's FileData member, since that'll go away if(FileData = parent.GetFileData()) { // Not a new file IsNewFile = false; OpenFile(); } // But if the parent returns 0, this is a new operation else { // This is a new file IsNewFile = true; // Create a new FileData member FileData = new TOpenSaveDialog::TData(OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, "Point Files (*.PTS)|*.pts|", 0, "", "PTS"); } }Note that, in the case of an open operation, the child assigns the pointer returned by GetFileData to its FileData member. Once this is done, the child takes over responsibility for the TOpenSaveDialog::TData object, including responsibility for cleaning it up. Since this is already done in the destructor, you don't have to do anything else.
The Drawing Pad application actually only provides menu items for four of these-CM_TILECHILDREN, CM_CASCADECHILDREN, CM_ARRANGEICONS, and CM_CLOSECHILDREN.
Data members in TDrawMDIClient
TDrawMDIClient requires a couple of new data members. These should both be declared private.
Adding response functions
In addition to the events handled by TMDIClient, TDrawMDIClient also handles the events formerly handled by TDrawWindow and not handled by TDrawMDIChild-CM_FILENEW, CM_FILEOPEN, and CM_ABOUT. The CmAbout response function is mostly unchanged from the TDrawWindow version, other than changing the class specifier. On the other hand, the CmFileNew and CmFileOpen functions must be substantially changed.CmFileNew
The CmFileNew function is actually simplified from its TDrawWindow version. It no longer has to deal with flushing the line arrays, invalidating the window, and setting flags. Instead it sets FileData to 0 so that the MDI child object can tell that it is displaying a new drawing, increments NewChildNum, then calls CreateChild. CreateChild is the function that actually creates and displays the new MDI child window. It is discussed in more detail in the discussion of the InitChild function.
The CmFileNew function should now look something like this:
void TDrawMDIClient::CmFileNew() { FileData = 0; NewChildNum++; CreateChild(); }CmFileOpen
void TDrawMDIClient::CmFileOpen() { // Create FileData. FileData = new TOpenSaveDialog::TData(OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, "Point Files (*.PTS)|*.pts|", 0, "", "PTS"); // As long as the file open operation goes OK... if ((TFileOpenDialog(this, *FileData)).Execute() == IDOK) // Create the child window. CreateChild(); // FileData is no longer needed. FileData = 0; }
The actual function definition is very simple and can be inlined by defining the function inside the class declaration. Your GetFileData function should look something like this:
TOpenSaveDialog::TData *GetFileData() { return FileData ? FileData : 0; }
The InitChild function for TDrawMDIClient is fairly straightforward. If FileData is 0, you should create a character array to contain a default window title. This can be initialized using the value of NewChildNum so that each new drawing has a different title.
Then you should create a TMDIChild* and create a new TDrawMDIChild object. The constructor for TDrawMDIChild takes two parameters, a reference to a TDrawMDIClient object for its parent window and a const char* containing the MDI child window's caption. In this case, the first parameter should be the dereferenced this pointer. The second parameter should be either the FileName member of the FileData object if FileData references a valid object or the character array you created earlier if not.
Once the MDI child object has been created, you need to call the SetIcon function for the object. SetIcon associates an icon resource with the function's object. This icon is displayed in the client area when the child window is minimized. You can set the icon to the icon provided for the tutorial application called IDI_TUTORIAL.
The last step of the function is to return the TMDIChild pointer. Your InitChild function should look something like this: TMDIChild* TDrawMDIClient::InitChild() { char title[15]; if(!FileData) wsprintf(title, "New drawing %d", NewChildNum); TMDIChild* child = new TDrawMDIChild(*this, FileData ? FileData->FileName : title); child->SetIcon(GetApplication(), TResId("IDI_TUTORIAL")); return child; }