A New Multiple Document Interface API Simplifies MDI Application Development

Charles Petzold

The Multiple Document Interface (MDI) is a specification for applications written for the Microsoft WindowsÔ environment or OS/2 Presentation Manager programs. The specification describes a window structure and user interface that allow the user to work with multiple documents in a single application (such as text documents in a word processing program or spreadsheets in a spreadsheet program). Just as Windows1 maintains multiple application windows within a single screen, an MDI application maintains multiple document windows within a single client area.

The MDI specification dates back to Windows Version 2.0 and was first used in Microsoft Excel for Windows. Windows programmers who wrote MDI applications before the release of Windows Version 3.0 must be commended for their bravery and stamina. I am certainly not one of them. When I first saw a beta version of Microsoft Excel, I was intrigued by several aspects of the program that appeared to exhibit nonstandard behavior for a Windows application. Using SYMDEB, I spent most of a day tracing through the Microsoft Excel code attempting to determine what magic its programmers had performed. It turned out not to be magic at all, but simply an enormous amount of work.

With Windows 3.0, however, much of that work has already been done for the applications developer. Windows 3.0 includes one new window class, four new function calls, two new data structures, and eleven new messages specifically designed to simplify MDI applications.

The Elements of MDI

The Multiple Document Interface is described in the Systems Application Architecture, Common User Access: Advanced Interface Design Guide (IBM, 1989). The main application window of an MDI program is conventional: it has a title bar, a menu, a sizing border, a system menu icon, and minimize/maximize icons. However, the client area (called a workspace) is not directly used to display program output. This workspace contains zero or more child windows, each of which displays a document.

These document child windows look like normal application windows, except they do not have a menu. The menu on the main application window applies to the document windows.

Only one document window is active at any time. The active window appears in front of all the other document windows and is indicated by a highlighted title bar. All the document child windows are clipped to the workspace area and never appear outside the application window. Figure 1 shows several documents in Microsoft Excel.

At first, MDI seems a fairly straightforward job for the Windows programmer. All you need to do is create a WS_CHILD or WS_POPUP window for each document, making the program's main application window the parent of the document window. But with a little exploration of an MDI application such as Microsoft Excel, you'll find some complications that require some difficult code.

The first intricacy is that an MDI document window can be maximized. The title bar of the document window (normally used to show the filename of the document in the window) disappears, and the filename appears appended to the application name in the application window's title bar. The system menu icon of the document window becomes the first item in the top-level menu of the application window. The icon to restore the size of the document window becomes the last item in the top-level menu and appears to the far right.

Second, although some MDI programs don't allow this, a document window should also be minimizable. Its icon should appear at the bottom of the workspace. (Generally an MDI application will use different icons for the main application window and each type of document.)

The third complication is that the system keyboard accelerator to close a document window is the same as that used to close the main window, but it uses the Ctrl key rather than Alt. That is, Alt-F4 closes the application window while Ctrl-F4 closes the document window. In addition, Ctrl-F6 switches among the child document windows within the active MDI application. Alt-Spacebar invokes the system menu of the main window, as usual. Alt-Minus invokes the system menu of the active child document window.

Fourth, when using the cursor keys to move among items on the menu in an MDI application, control passes from the application system menu, to the currently active document system menu, then back to the first item on the menu bar. (Standard windows pass control from the system menu directly to the first item on the menu bar.)

Fifth, if the application is capable of supporting several different types of child windows (for example, the sheet and chart documents in Microsoft Excel), the menu should reflect the operations associated with that type of document. This requires that the program change the menu when a different document window becomes active. In addition, when no document window exists, the menu should be stripped down to only those operations involved in opening a new document.

Finally, the top-level menu bar has an item called "Window." By convention, this is the last item on the top-level menu except for Help. The "Window" submenu generally has options to arrange the document windows within the workspace. Document windows can be cascaded from the upper left or tiled so that each document window is fully visible. This submenu also has a list of all the document windows. Selecting one moves that document window to the foreground.

All these aspects of MDI are supported in Windows 3.0. Some overhead is required, of course (as will be shown in a sample program), but it's not anything like the code you'd have to write to support all these features directly.

Windows Version 3.0 and MDI

Some new terminology is necessary when discussing Windows 3.0 MDI support. (I'm afraid this terminology will be a little confusing to OS/2 Presentation Manager programmers. The use of the terms frame window and client window in a Windows MDI program isn't the same as in Presentation Manager.)

The main application window is called the frame window. Just as in a conventional Windows program, this is a window in the WS_OVERLAPPEDWINDOW style.

An MDI application also creates a client window based on the predefined window class MDICLIENT. The client window is created by a call to CreateWindow using the WS_CHILD style. The last parameter to CreateWindow is a pointer to a small structure of type CLIENTCREATESTRUCT (see Figure 2). This client window covers the client area of the frame window and is responsible for much of the MDI support. The color of this client window is the system color COLOR_APPWORKSPACE.

The document windows, called child windows, are created by initializing a structure of type MDICREATESTRUCT (see Figure 2) and sending the client window a WM_MDICREATE message with a pointer to this structure. The document windows are children of the client window, which in turn is a child of the frame window. The parent-child hierarchy is shown in Figure 3.

You need a window class (and window procedure) for the frame window and each type of child window supported by the application. You don't need a window procedure for the client window because the window class is preregistered.

Sample Program

The Windows 3.0 Software Development Kit (SDK) includes a sample program called MULTIPAD that demonstrates how to write an MDI program. However, MULTIPAD contains quite a bit of code that has nothing to do with MDI. It might be easier for you to get a better feel for MDI programming by examining a smaller program that does little except demonstrate the MDI features. The components of this program, called MDIDEMO, are shown in Figure 4. To compile MDIDEMO, you need Microsoft C Version 6.0 and the Windows 3.0 SDK.

MDIDEMO (see Figure 5) supports two types of extremely simple document windows: one displays "Hello, World!" in the center of its client area and the other displays a series of random rectangles. (In the source code listings and identifier names, these are referred to as the Hello document and the Rect document.) Different menus are associated with these two types of document windows (compare Figures 5 and 6). The document window that displays "Hello, World!" has a menu that allows changing the color of the text.

Three Menus

Let's turn first to the MDIDEMO.RC resource script (see Figure 4), which defines three menu templates used by the program. The program displays the MdiMenuInit menu when no document windows are present. This menu simply allows the creation of a new document or exit from the program.

The MdiMenuHello menu is associated with the document window that displays "Hello, World!" The File submenu allows the opening of a new document of either type, the closing of the active document, and exiting from the program. The Color submenu lets you set the text color. The Window submenu has options to arrange the document windows in cascade or tile fashion, to arrange the document icons, and to close all the windows. This submenu will also list all the document windows as they are created.

The MdiMenuRect menu is associated with the random rectangle document. This menu is the same as the MdiMenuHello menu except that it does not include the Color submenu.

The MDIDEMO.H header file (see Figure 4) defines all the menu identifiers as well as three constants:

#define INIT_MENU_POS 0

#define HELLO_MENU_POS 2

#define RECT_MENU_POS 1

These identifiers indicate the position of the "Window" submenu in each of the three menu templates. This information is needed by the program to inform the client window where the document list is to appear. Of course, the MdiMenuInit menu doesn't have a "Window" submenu, so I've indicated that the list should be appended to the first submenu (position 0). The list will never actually be viewed there, however. You'll see why when I discuss the program.

The IDM_FIRSTCHILD identifier doesn't correspond to a menu item. This is the identifier that will be associated with the first document window in the list that will appear in the "Window" submenu. You should choose this identifier to be greater than all the other menu IDs.

Program Initialization

In MDIDEMO.C (see Figure 4), WinMain begins by registering window classes for the frame window and the two child windows. The window procedures are called FrameWndProc, HelloWndProc, and RectWndProc. Normally, different icons should be associated with these window classes. For simplicity, I've used the standard IDI_APPLICATION icon and no icon for the Rect window. (Note that I've defined the hbrBackground field of the WNDCLASS structure for the frame window class to be the COLOR_APPWORKSPACE system color. This is not entirely necessary because the client area of the frame window is covered up by the client window, and the client window is this color anyway. However, using this color looks a little better when the window is first displayed.) The lpszMenuName field is set to NULL for each of these three window classes. For the Hello and Rect child window classes, this is normal. For the frame window class, I have chosen to indicate the menu handle in the CreateWindow function when creating the frame window.

The window classes for the Hello and Rect child windows allocate extra space for each window using a nonzero value as the cbWndExtra field of the WNDCLASS structure. This space will contain a local memory handle that will reference a block of memory used to store information unique to each document window. This block of memory is the size of the HELLODATA or RECTDATA structures defined near the top of MDIDEMO.C.

Next, WinMain uses LoadMenu to load the three menus and saves their handles in global variables. Three calls to the GetSubMenu function obtain handles to the "Window" submenu on which the document list will be appended. These handles are also saved in global variables.

A call to CreateWindow in WinMain creates the frame window. During the processing of WM_CREATE in FrameWndProc, the frame window creates the client window. This involves another call to CreateWindow. The window class is set to MDICLIENT, which is the preregistered class for MDI client windows. The last parameter to CreateWindow must be set to a pointer to a structure of type CLIENTCREATESTRUCT. This structure has two fields, hWindowMenu and idFirstChild. The first field, hWindowMenu, is the handle of the submenu on which the document list will be appended. In MDIDEMO, this is hMenuInitWindow, which was obtained during WinMain. You'll see later how the menu is changed. The second field, idFirstChild, is the menu ID to be associated with the first document window in the document list. This is simply IDM_FIRSTCHILD.

Back in WinMain, MDIDEMO displays the newly created frame window and enters the message loop. The message loop is a little different from usual-after obtaining the message from the message queue with a call to GetMessage, an MDI program passes the message to TranslateMDISysAccel. This function translates any keystroke that may correspond to the special MDI accelerators (such as Ctrl-F6) into a WM_SYSCOMMAND message. If the function TranslateMDISysAccel returns TRUE (indicating that a message was translated), TranslateMessage and DispatchMessage are not called. MDIDEMO uses its own accelerators also, so the message loop should call the TranslateMessage and DispatchMessage functions only if neither TranslateAccelerator nor TranslateMDISysAccel returns TRUE.

Creating the Child Windows

The bulk of FrameWndProc is devoted to processing WM_COMMAND messages that signal menu selections. As usual, the wParam parameter to FrameWndProc contains the menu ID number.

For wParam values of IDM_NEWHELLO and IDM_NEWRECT, FrameWndProc must create a new document window. This is done by initializing the fields of an MDICREATESTRUCT structure (most of which correspond to CreateWindow parameters) and sending the client window a WM_MDICREATE message with lParam set to a pointer to this structure. The client window then creates the child document window.

Normally, the szTitle field of the MDICREATESTRUCT structure would be the file name corresponding to the document. The style field can be set to the window styles WS_HSCROLL or WS_VSCROLL or both to include scroll bars in the document window. The style field can also include WS_MINIMIZE or WS_MAXIMIZE to display the document window initially in a minimized or maximized state. The lParam field of the MDICREATESTRUCT structure provides a way for the frame window and the child window to share some variables. This could be set to a local or global memory handle that references a block of memory containing a structure. During the WM_CREATE message in the child document window, lParam is a pointer to a CREATESTRUCT structure; the lpCreateParams field of this structure is a pointer to the MDICREATESTRUCT structure used to create the window.

On receipt of the WM_MDICREATE message, the client window creates the child document window and adds the title of the window to the bottom of the submenu specified in the MDICLIENTSTRUCT structure. When the MDIDEMO program creates its first document window, the submenu used is the File submenu of the MdiMenuInit menu. We'll see later how this document list gets moved to the Window submenu of the MdiMenuHello and MdiMenuRect menus.

The menus lists as many as nine documents, each preceded by an underlined number (1 to 9). If more than nine document windows are created, this list is followed by a "More windows..." item on the menu. This item invokes a dialog box containing a list box that lists all the document windows (see Figure 7). The automatic maintenance of this document list is one of the nicest features of the Windows MDI support.

More Frame Window Message Processing

Let's continue with FrameWndProc message processing before turning to the child document windows. When you select Close from the File menu, MDIDEMO closes the active child window, and it obtains the handle to the active child window by sending the client window a WM_MDIGETACTIVE message. If the child window responds affirmatively to a WM_QUERYENDSESSION message, then MDIDEMO sends the client window a WM_MDIDESTROY message to close the child window.

Processing the Exit option from the File menu requires only that the frame window procedure send itself a WM_CLOSE message. Processing the Tile, Cascade, and Arrange Icons options from the Window submenu is a snap, requiring only that the WM_MDITILE, WM_MDICASCADE, and WM_MDIICONARRANGE messages be sent to the client window.

The Close All option is more complex. FrameWndProc calls EnumChildWindows to enumerate the child windows through the callback function CloseEnumProc. This function sends the child a WM_MDIRESTORE message and then WM_QUERYENDSESSION. If the child window responds affirmatively to a WM_QUERYENDSESSION message, CloseEnumProc closes the child window by sending the WM_MDIDESTROY message to the client window. However, the icon title windows (the text beneath each document icon) must not be destroyed. You can check for these windows by a non-NULL return from GetWindow with a GW_OWNER parameter.

You'll notice that FrameWndProc does not process any of the WM_COMMAND messages signaling that one of the colors has been selected from the Color menu. These messages are really the responsibility of the document window. For this reason, FrameWndProc sends all unprocessed WM_COMMAND messages to the active child window so that the child window can process those messages that pertain to its window.

All the messages that the frame window procedure chooses not to process must be passed to DefFrameProc. This is one of the new MDI functions in Windows 3.0. It replaces DefWindowProc in the frame window procedure. Even if a frame window procedure traps the WM_MENUCHAR, WM_NEXTMENU, WM_SIZE, or WM_SETFOCUS messages, they must also be passed to DefFrameProc.

Unprocessed WM_COMMAND messages must also be passed to DefFrameProc. In particular, FrameWndProc does not process any of the WM_COMMAND messages resulting from the user selecting one of the documents from the list in the Window submenu. (The wParam values for these options begin with IDM_FIRSTCHILD.) These messages are passed to DefFrameProc and processed there.

Notice that the frame window does not need to maintain a list of window handles of all the document windows it creates. If these handles are ever needed (such as when processing the Close All option from the menu), they can be obtained using GetWindow.

Child Document Windows

Now let's look at HelloWndProc, which is the window procedure used for the child document windows that display "Hello, World!"

As with any window class used for more than one window, static variables defined in the window procedure (or any function called from the window procedure) are shared by all windows based on that window class.

Data that is unique to each window must be stored using a method other than static variables. One such technique uses window properties. Another approach (the one I employed) uses memory space that is reserved by defining a nonzero value in the cbWndExtra field of the WNDCLASS structure used to register the window class.

In MDIDEMO, I use this space to store a local memory handle that references a block of memory the size of the HELLODATA structure. HelloWndProc allocates this memory during the WM_CREATE message, locks it, initializes the two fields (which indicate the currently checked menu item and the text color), unlocks the block, and stores the local memory handle using SetWindowWord.

When processing a WM_COMMAND message for changing the text colors (recall that these messages originate in the frame window procedure), HelloWndProc uses GetWindowWord to obtain a handle to the memory block containing the HELLODATA structure. Using this structure, HelloWndProc unchecks the checked menu item, checks the selected menu item, and saves the new color.

A document window procedure receives the WM_MDIACTIVATE message whenever the window becomes active or inactive (indicated by a TRUE or FALSE value in wParam). You'll recall that MDIDEMO has three different menus: MdiMenuInit, used when no documents are present; MdiMenuHello, used when a Hello document window is active; and MdiMenuRect, used when a Rect document window is active.

The WM_MDIACTIVATE message provides an opportunity for the document window to change the menu. If wParam is TRUE (meaning the window is becoming active), HelloWndProc changes the menu to MdiMenuHello. If wParam is FALSE, HelloWndProc changes the menu to MdiMenuInit.

HelloWndProc changes the menu by sending a WM_MDISETMENU message to the client window. The client window processes this message by removing the document list from the current menu and appending it to the new menu. This is how the document list is transferred from the MdiMenuInit menu (which was in effect when the first document is created) to the MdiMenuHello menu. The SetMenu function should not be used to change a menu in an MDI application.

Another little chore involves the checkmarks on the Color submenu. Program options such as these should be unique to each document. For example, you should be able to set blue text in one window and red text in another. The menu checkmarks should reflect the option chosen in the active window (see Figure 8). For this reason, HelloWndProc unchecks the selected menu item when the window is becoming inactive and checks the appropriate item when the window is becoming active.

The window procedure gets the first WM_MDIACTIVATE message with wParam set to TRUE when the window is first created, and the last (with wParam set to FALSE) when the window is destroyed. When the user switches from one document to another, the first document window receives a WM_MDIACTIVATE message with wParam set to FALSE (at which time it sets the menu to MdiMenuInit) and the second document window receives a WM_MDIACTIVATE message with wParam set to TRUE (at which time it sets the menu to MdiMenuHello or MdiMenuRect as appropriate). If all the windows are closed, the menu is left as MdiMenuInit.

You'll recall that FrameWndProc sends the child window a WM_QUERYENDSESSION when the user selects Close or Close All from the menu. HelloWndProc processes the WM_QUERYENDSESSION and WM_CLOSE messages by displaying a message box and asking the user if the window can be closed. (In a real program, this message box would ask if a file needed to be saved.) If the user indicates that the window should not be closed, the window procedure returns zero. During the WM_DESTROY message, HelloWndProc frees the local memory block allocated during the WM_CREATE message.

All unprocessed messages must be passed on to DefMDIChildProc (not DefWindowProc) for default processing. The messages shown below must be passed to DefMDIChildProc whether the child window procedure does something with them or not.

WM_CHILDACTIVATE

WM_GETMINMAXINFO

WM_MENUCHAR

WM_MOVE

WM_NEXTMENU

WM_SETFOCUS

WM_SIZE

WM_SYSCOMMAND

RectWndProc is fairly similar to HelloWndProc, but it's a little simpler (no menu options are involved and the window does not verify with the user if it can be closed), so I needn't discuss it. But note that RectWndProc breaks after processing WM_SIZE so it is passed to DefMDIChildProc.

Finishing Up

I mentioned earlier that the MDI support in Windows 3.0 involves one new window class, two new data structures, four new function calls, and eleven new messages. The window class, MDICLIENT, is used for creating the client window. The two new data structures, CLIENTCREATESTRUCT and MDICREATESTRUCT, are used for creating the client window and the child document windows.

I discussed three of the four new function calls: TranslateMDISysAccel, DefFrameProc, and DefMDIChildProc. The fourth new function call is ArrangeIconicWindows, which performs the same function in an MDI application as the WM_MDIICONARRANGE message.

MDIDEMO demonstrates nine of the eleven new messages. These messages are:

WM_MDIACTIVATE

WM_MDICASCADE

WM_MDICREATE

WM_MDIDESTROY

WM_MDIGETACTIVE

WM_MDIICONARRANGE

WM_MDIRESTORE

WM_MDISETMENU

WM_MDITILE

The other two messages are WM_MDIMAXIMIZE and WM_MDINEXT. But because Windows takes care of all the maximizing and switching of child windows, applications usually don't have to use these messages.

One aspect of the Multiple Document Interface described in the IBM book about Common User Access (CUA), and used in Microsoft Excel, is splitting windows in half or into quadrants using small controls to the top and left of the scroll bars. This is not part of the MDI support in Windows 3.0. However, all other features of MDI described in CUA are provided. The new features described here make MDI accessible to Windows programmers and are implemented in a way that illustrates the power of the object-oriented architecture of Windows.