Step 11: Moving to MDI
This chapter describes how to convert the application created in Step 10
to use the Multiple Document Interface, or MDI for short. The application in Step 10 is
what is known as a Single Document Interface, or SDI, application. That means the
application can support and display only a single document at a time.
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.
Understanding the MDI model
An MDI application functions a little differently from an SDI
application. In Step 10, the Drawing Pad application displayed a single drawing in a
window. The window that actually displayed the drawing was a client of the frame window.
The frame window managed general application tasks, such as menu handling, resizing,
painting menus and control bars, and so on. The client window managed tasks specific to
the application, such as handling mouse movements and button clicks in the client area,
painting the lines in the drawing, responding to application-specific events, and so on.
In comparison, MDI applications divide tasks up three ways instead
of two:
In this step, you'll take
the example from Step 10 and restructure it to support MDI functionality. It's not as
complicated as it may seem; most of the new classes you'll construct can be taken straight
from the existing TDrawWindow class!
Adding the MDI header files
There are a number of new header files you need to include to add
MDI capability to your application. This section describes the header files that need to
be changed or added. It also describes the classes that are defined in each header file.
Changing the resource script file
You need to change the include statement for the STEP10.RC resource
script file to include the STEP11.RC resource script file. There are only two changes you
need to make to STEP11.RC:
- Include the resource header file owl\mdi.rh.
- Add a pop-up menu called Window between the Tools menu and the Help menu. This menu
should have four items, described in
Table
11.1.
The functions that handle these events are described later on.
Replacing the frame window header file
In the place of owl\decframe.h, you need to include owl\decmdifr.h.
This header file contains the definition of the TDecoratedMDIFrame class, which is derived
from TMDIFrame and TDecoratedFrame. TMDIFrame, defined in the owl\mdi.h header file, adds
the support for containing an MDI client window to the support already provided by
TFrameWindow for command processing and keyboard navigation. MDI client windows are
discussed above. As shown in the
previous step of the tutorial, TDecoratedFrame provides the ability to support decorations
such as control bars and status bars. Since the tutorial application already supports
decorations from the previous step, you can use the decorated version of the MDI frame
window to keep this functionality.
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
owl\mdichild.h contains
the definition of the TMDIChild class, which is derived from TWindow. TMDIChild overrides
a number of TWindow's function to provide the ability to function as an MDI child.
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.
Changing the frame window
The first step in moving the drawing application to MDI is to
change the frame window. MDI applications use specialized MDI frame windows. As discussed
earlier, ObjectWindows provides two MDI frame window classes, TMDIFrame and
TDecoratedMDIFrame. Because we're using the TDecoratedMDIFrame class for the frame window,
discussion of the TMDIFrame class is left for "Window objects" of the
ObjectWindows Programmer's Guide.
Here's the constructor for TDecoratedMDIFrame:
TDecoratedMDIFrame(const char far* title,
TResId menuResId,
TMDIClient& clientWnd = *new TMDIClient,
bool trackMenuSelection = false,
TModule* module = 0);
where:
Besides adding the
owl\decmdifr.h header file, two other changes are required to use a TDecoratedMDIFrame in
the tutorial application. The first is changing the line in the TDrawApp::InitMainWindow
function where the frame window is created:
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.
Your InitMainWindow function should now look something like this:
void
TDrawApp::InitMainWindow()
{
// Create a decorated MDI frame
TDecoratedMDIFrame *frame = new TDecoratedMDIFrame("Drawing Pad",
TResId("COMMANDS"), *new TDrawMDIClient, true);
// Construct a status bar
TStatusBar* sb = new TStatusBar(frame, TGadget::Recessed);
// Construct a control bar
TControlBar *cb = new TControlBar(frame);
cb->EnableFlatStyle(); // Enable the new flat look of toolbar buttons
cb->Insert(*new TButtonGadget(CM_FILENEW, CM_FILENEW,
TButtonGadget::Command));
cb->Insert(*new TButtonGadget(CM_FILEOPEN, CM_FILEOPEN,
TButtonGadget::Command));
cb->Insert(*new TButtonGadget(CM_FILESAVE, CM_FILESAVE,
TButtonGadget::Command));
cb->Insert(*new TButtonGadget(CM_FILESAVEAS, CM_FILESAVEAS,
TButtonGadget::Command));
cb->Insert(*new TSeparatorGadget);
cb->Insert(*new TButtonGadget(CM_PENSIZE, CM_PENSIZE,
TButtonGadget::Command));
cb->Insert(*new TButtonGadget(CM_PENCOLOR, CM_PENCOLOR,
TButtonGadget::Command));
cb->Insert(*new TSeparatorGadget);
cb->Insert(*new TButtonGadget(CM_ABOUT, CM_ABOUT,
TButtonGadget::Command));
// Insert the status bar and control bar into the frame
frame->Insert(*sb, TDecoratedFrame::Bottom);
frame->Insert(*cb, TDecoratedFrame::Top);
// Set the main window and its menu
SetMainWindow(frame);
}
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:
| MDI client window are created
during the construction of the MDI frame class. This window is open as long as the frame
window is still open (in this case, for the life of the application). This window handles
the CM_FILEOPEN, CM_FILENEW, and CM_ABOUT commands. When the application is first
started up, or when there are no drawings open, the only commands that make sense are
opening drawing files, creating new drawings, and opening the About... dialog box. Other
commands available in the tutorial application, such as saving drawings, changing the pen
size or color, and so on, apply to a particular drawing, which must already be open and
displayed in a child window.
|
| MDI child windows are created by
the MDI client window in response to CM_FILENEW or CM_FILEOPEN commands handled by the
client window. In the tutorial application, MDI child windows handle the events handled by
TDrawWindow in Step 10 that aren't handled by TDrawMDIClient:
Note that each of these
commands pertains to a specific drawing or window; that is, each event only makes sense in
the context of an open drawing contained in a child window. For example, in order for the
user of the application to save a drawing, there must already be a drawing open. Contrast
this to the events handled by the MDI client window, which either open a new child window
containing a new or existing drawing or are independent of a drawing altogether.
|
The next sections discuss
how to create the MDI client and child window classes for the tutorial application.
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
The class declaration for TDrawMDIChild is very similar to the
declaration of the TDrawWindow class from Step10. Here are the changes you need to make:
- Change all occurrences of TDrawWindow to TDrawMDIChild. This includes the name of the
destructor, which otherwise doesn't change.
- Remove the CmFileNew, CmFileOpen, and CmAbout functions from the class declaration.
- The constructor for TMDIChild requires a TMDIClient reference in place of TDrawWindow's
TWindow *. This parameter indicates the parent of the MDI child window. In this case, you
want to add a TDrawMDIClient reference to the constructor and pass this to the TMDIChild
constructor. In addition, you should add a const char* for the MDI child window's
caption.
- In the response table, remove the entries for handling the CM_FILENEW, CM_FILEOPEN, and
CM_ABOUT events.
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;
Creating the TDrawMDIChild functions
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 |
There is one minor change
you need to make to the CmFileSaveAs function. Because the name of the drawing usually
changes when the user calls the File|Save As command, you need to set the caption of the
window to the file name. To do this, use the SetCaption function. This function takes a char*,
which in this case should be the FileName member of the FileData object. The CmFileSaveAs
function should now look like this:
void
TDrawMDIChild::CmFileSaveAs()
{
if (IsNewFile)
strcpy(FileData->FileName, "");
if ((TFileSaveDialog(this, *FileData)).Execute() == IDOK)
SaveFile();
SetCaption(FileData->FileName);
}
Creating the TDrawMDIChild constructor
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.
Initializing data members
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)
{
}
Initializing file information data members
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.
Creating the MDI client window class
The TDrawMDIClient class manages the multiple child windows open on
its client area and all the attendant functionality, such as creating new children,
closing windows either singly or all at one time, tiling or cascading the windows, and
arranging the icons of minimized children. TDrawMDIClient inherits a great deal of this
functionality from the TMDIClient class.
TMDIClient functionality
It is important to understand the TMDIClient class, for the main
reason that it is going to do a lot of work for you. TMDIClient is virtually derived from
the TWindow class. TMDIClient overrides two of TWindow's virtual functions, PreProcessMsg
and Create, to provide specific keyboard and menu handling functionality required by the
client window. TMDIClient also handles a number of events, which are described in Table 11.2.
The Drawing Pad application
actually only provides menu items for four of these-CM_TILECHILDREN, CM_CASCADECHILDREN,
CM_ARRANGEICONS, and CM_CLOSECHILDREN.
These response functions are simply wrappers for other TMDIClient
functions that actually perform the work necessary. Each response function calls a
function with the same name without the Cm prefix, so that CmCreateChild calls the
CreateChild function. The only exception is CmTileChildrenHoriz, which calls the
TileChildren function with the MDITILE_HORIZONTAL parameter.
Another function provided by TMDIClient is the GetActiveMDIChild
function, which returns a pointer to the active MDI child window. Note that there can only
be one active MDI child window at any time, but there is always one active MDI child
window, even if all the MDI child windows are minimized.
There is one other function to discuss, InitChild. This is the only
function in TMDIClient that you need to override in TDrawMDIClient. InitChild and
overriding it to work with TDrawMDIClient are discussed on page 75.
Data members in TDrawMDIClient
TDrawMDIClient requires a couple of new data members. These should
both be declared private.
The first is NewChildNum. The only function of this variable is to
keep track of the number of new drawing created by the CmFileNew function. This number is
used for the window caption of all new drawings. It is initialized to 0 in the
TDrawMDIClient constructor.
The second is FileData, a pointer to a TOpenSaveDialog::TData
object, just like the FileData member of TDrawMDIChild. FileData is used to hold the file
information when a user opens an existing file. It is set to 0 in the constructor.
FileData is also set to 0 once the MDI child window has been opened. As shown on page 71, the object returned by
GetFileData is assigned to the FileData member of TDrawMDIChild. The object returned by
GetFileData is actually the object (or lack thereof in the case of a new file) pointed to
by TDrawMDIClient`s FileData member.
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
There are a number of differences between the TDrawWindow version of
CmFileOpen and the TDrawMDIClient version.
Your CmFileOpen function should look something like this:
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;
}
GetFileData
The only new function required for TDrawMDIClient is GetFileData.
This function is called by TDrawMDIChild in its constructor. This function should take no
parameters and return a pointer to a TOpenSaveDialog::TData object. Its function is to
return a pointer to the object pointed to by TDrawMDIClient's FileData member. If FileData
references a valid object (that is, during a file open operation), GetFileData should
return FileData. If FileData doesn't reference a valid object (that is, during a file new
operation), GetFileData should return 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; }
Overriding InitChild
The only TMDIClient function that TDrawMDIChild overrides is the
InitChild function. InitChild takes no parameters and returns a pointer to a TMDIChild
object. The CreateChild function calls InitChild before creating a new MDI child window.
It is in InitChild that you create the TMDIChild or TMDIChild-derived object for the MDI
child window. This is the only function of TMDIClient that you'll override when you create
the TDrawMDIClient class.
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;
}
Where to find more information
MDI frame, client, and child windows are described in "Window objects" in the ObjectWindows Programmer's Guide.
|