Chapter 18  The Multiple Document Interface (MDI)
ÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ

The Multiple Document Interface (MDI) is a specification for applications
that handle documents in Microsoft Windows. The specification describes a
window structure and user interface that allow the user to work with
multiple documents within a single application (such as text documents in a
word processing program or spreadsheets in a spreadsheet program). Simply
put, just as Windows maintains multiple application windows within a single
screen, an MDI application maintains multiple document windows within a
single client area. The first MDI application for Windows was the first
Windows version of Microsoft Excel. Both the Program Manager and File
Manager in Windows 3 are MDI applications.

Although the MDI specificiation has been around since Windows 2, at that
time MDI applications were difficult to write and required some very
intricate programming work. With Windows 3, however, much of that work has
already been done for you. Windows 3 includes one new window class, four new
functions, two new data structures, and eleven new messages for the specific
purpose of simplifying MDI applications.

THE ELEMENTS OF MDI

The Multiple Document Interface is described in the CUA Advanced Interface
Design Guide. 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. The client area, however, is often called
a "workspace" and is not directly used to display program output. This
workspace contains zero or more child windows, each of which displays a
document.

These child windows look much like normal application windows. They have a
title bar, a sizing border, a system menu icon, minimize/maximize icons, and
possibly scroll bars. None of the document windows has a menu, however. The
menu on the main application window applies to the document windows.

At any one time, only one document window is active (indicated by a
highlighted title bar) and appears in front of all the other document
windows. All the document child windows are clipped to the workspace area
and never appear outside the application window.

At first, MDI seems a fairly straightforward job for the Windows programmer.
All you need to do is create a WS_CHILD 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 the Windows 3 File
Manager, you'll find some complications that require difficult code.

  þ   An MDI document window can be minimized. Its icon appears at the
      bottom of the workspace. (Generally an MDI application will use
      different icons for the main application window and each type of
      document window.)

  þ   An MDI document window can be maximized. In this case, 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.

  þ   The system keyboard accelerator to close a document window is the same
      as that to close the main window, except using 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.

  þ   When using the cursor keys to move among items on the menu, control
      normally passes from the system menu to the first item on the menu
      bar. In an MDI application, control passes from the application system
      menu to the active document system menu to the first item on the menu
      bar.

  þ   If the application is capable of supporting several types of child
      windows (for example, the worksheet and chart documents in Microsoft
      Excel), then 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.

  þ   The top-level menu bar has an item called Window. By convention, this
      is the last item on the top-level menu bar 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 of these aspects of MDI are supported in Windows 3. Some overhead is
required of course (as will be shown in a sample program), but it's not
anywhere close to the amount of code you'd have to write to support all
these features directly.


WINDOWS 3 AND MDI

Some new terminology is necessary when approaching the Windows 3 MDI
support. The main application window is called the "frame window." Just as
in a conventional Windows program, this is a window of 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 this window class and the WS_CHILD style. The last
parameter to CreateWindow is a pointer to a small structure of type
CLIENTCREATESTRUCT. 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 are called "child windows." You create these windows by
initializing a structure of type MDICREATESTRUCT 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
18-1 on the following page.

  (Figure 18-1. may be found in the printed book.)

You need a window class (and window procedure) for the frame window and for
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.

I mentioned earlier that the MDI support of Windows 3 includes one new
window class, four new functions, two new data structures, and eleven new
messages. I've already mentioned the new window class, which is MDICLIENT,
and the new data structures, CLIENTCREATESTRUCT and MDICREATESTRUCT. Two of
the four new functions replace DefWindowProc in MDI applications: Rather
than call DefWindowProc for all unprocessed messages, a frame window
procedure calls DefFrameProc and a child window procedure calls
DefMDIChildProc. Another new function, TranslateMDISysAccel, is used in the
same way as TranslateAccelerator, which I discussed in Chapter 9. The fourth
new function is ArrangeIconicWindows, but one of the special MDI messages
makes this function unnecessary for MDI programs.

In the sample program coming up, I'll demonstrate nine of the eleven MDI
messages. (The other two are not normally required.) These messages begin
with the prefix WM_MDI. A frame window sends one of these messages to the
client window to perform an operation on a child window or to obtain
information about a child window. (For example, a frame window sends an
WM_MDICREATE message to a client window to create a child window.) The
WM_MDIACTIVATE message is an exception: While a frame window can send this
message to the client window to activate one of the child windows, the
client window also sends the message to the child windows being activated
and deactivated to inform them of this change.


THE SAMPLE PROGRAM

The Windows 3 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 18-2.

 MDIDEMO.MAK

#-----------------------
# MDIDEMO.MAK make file
#-----------------------

mdidemo.exe : mdidemo.obj mdidemo.def mdidemo.res
     link mdidemo, /align:16, NUL, /nod slibcew libw, mdidemo
     rc mdidemo.res

mdidemo.obj : mdidemo.c mdidemo.h
     cl -c -Gsw -Ow -W2 -Zp mdidemo.c

mdidemo.res : mdidemo.rc mdidemo.h
     rc -r mdidemo.rc

 MDIDEMO.C

/*--------------------------------------------------------
   MDIDEMO.C -- Multiple Document Interface Demonstration
                (c) Charles Petzold, 1990
  --------------------------------------------------------*/

#include 
#include 
#include "mdidemo.h"

long FAR PASCAL FrameWndProc  (HWND, WORD, WORD, LONG) ;
BOOL FAR PASCAL CloseEnumProc (HWND, LONG) ;
long FAR PASCAL HelloWndProc  (HWND, WORD, WORD, LONG) ;
long FAR PASCAL RectWndProc   (HWND, WORD, WORD, LONG) ;

          // structure for storing data unique to each Hello child window



typedef struct
     {
     short    nColor ;
     COLORREF clrText ;
     }
     HELLODATA ;

typedef HELLODATA NEAR *NPHELLODATA ;

          // structure for storing data unique to each Rect child window

typedef struct
     {
     short cxClient ;
     short cyClient ;
     }
     RECTDATA ;

typedef RECTDATA NEAR *NPRECTDATA ;

          // global variables

char   szFrameClass [] = "MdiFrame" ;
char   szHelloClass [] = "MdiHelloChild" ;
char   szRectClass  [] = "MdiRectChild" ;
HANDLE hInst ;
HMENU  hMenuInit, hMenuHello, hMenuRect ;
HMENU  hMenuInitWindow, hMenuHelloWindow, hMenuRectWindow ;

int PASCAL WinMain (HANDLE hInstance, HANDLE hPrevInstance,
                    LPSTR lpszCmdLine, int nCmdShow)
     {
     HANDLE   hAccel ;
     HWND     hwndFrame, hwndClient ;
     MSG      msg ;
     WNDCLASS wndclass ;

     hInst = hInstance ;

     if (!hPrevInstance)
          {
                    // Register the frame window class

          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = FrameWndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = 0 ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = COLOR_APPWORKSPACE + 1 ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szFrameClass ;

          RegisterClass (&wndclass) ;

                    // Register the Hello child window class

          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = HelloWndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = sizeof (LOCALHANDLE) ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szHelloClass ;

          RegisterClass (&wndclass) ;

                    // Register the Rect child window class

          wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
          wndclass.lpfnWndProc   = RectWndProc ;
          wndclass.cbClsExtra    = 0 ;
          wndclass.cbWndExtra    = sizeof (LOCALHANDLE) ;
          wndclass.hInstance     = hInstance ;
          wndclass.hIcon         = NULL ;
          wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
          wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;
          wndclass.lpszMenuName  = NULL ;
          wndclass.lpszClassName = szRectClass ;

          RegisterClass (&wndclass) ;
          }
               // Obtain handles to three possible menus and submenus

     hMenuInit  = LoadMenu (hInst, "MdiMenuInit") ;
     hMenuHello = LoadMenu (hInst, "MdiMenuHello") ;
     hMenuRect  = LoadMenu (hInst, "MdiMenuRect") ;

     hMenuInitWindow  = GetSubMenu (hMenuInit,   INIT_MENU_POS) ;
     hMenuHelloWindow = GetSubMenu (hMenuHello, HELLO_MENU_POS) ;
     hMenuRectWindow  = GetSubMenu (hMenuRect,   RECT_MENU_POS) ;

               // Load accelerator table
     hAccel = LoadAccelerators (hInst, "MdiAccel") ;

               // Create the frame window

     hwndFrame = CreateWindow (szFrameClass, "MDI Demonstration",
                               WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
                               CW_USEDEFAULT, CW_USEDEFAULT,
                               CW_USEDEFAULT, CW_USEDEFAULT,
                               NULL, hMenuInit, hInstance, NULL) ;

     hwndClient = GetWindow (hwndFrame, GW_CHILD) ;

     ShowWindow (hwndFrame, nCmdShow) ;
     UpdateWindow (hwndFrame) ;

               // Enter the modified message loop

     while (GetMessage (&msg, NULL, 0, 0))
          {
          if (!TranslateMDISysAccel (hwndClient, &msg) &&
              !TranslateAccelerator (hwndFrame, hAccel, &msg))
               {
               TranslateMessage (&msg) ;
               DispatchMessage (&msg) ;
               }
          }
     return msg.wParam ;
     }

long FAR PASCAL FrameWndProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
     static HWND        hwndClient ;
     CLIENTCREATESTRUCT clientcreate ;
     FARPROC            lpfnEnum ;
     HWND               hwndChild, hwndNext ;
     MDICREATESTRUCT    mdicreate ;

     switch (message)
          {
          case WM_CREATE :         // Create the client window

               clientcreate.hWindowMenu  = hMenuInitWindow ;
               clientcreate.idFirstChild = IDM_FIRSTCHILD ;

               hwndClient = CreateWindow ("MDICLIENT", NULL,
                              WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE,
                              0, 0, 0, 0, hwnd, 1, hInst,
                              (LPSTR) &clientcreate) ;
               return 0 ;
          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDM_NEWHELLO :      // Create a Hello child window

                         mdicreate.szClass = szHelloClass ;
                         mdicreate.szTitle = "Hello" ;
                         mdicreate.hOwner  = hInst ;
                         mdicreate.x       = CW_USEDEFAULT ;
                         mdicreate.y       = CW_USEDEFAULT ;
                         mdicreate.cx      = CW_USEDEFAULT ;
                         mdicreate.cy      = CW_USEDEFAULT ;
                         mdicreate.style   = 0 ;
                         mdicreate.lParam  = NULL ;

                         hwndChild = SendMessage (hwndClient, WM_MDICREATE,
0,
                                        (LONG) (LPMDICREATESTRUCT)
&mdicreate) ;
                         return 0 ;

                    case IDM_NEWRECT :       // Create a Rect child window

                         mdicreate.szClass = szRectClass ;
                         mdicreate.szTitle = "Rectangles" ;
                         mdicreate.hOwner  = hInst ;
                         mdicreate.x       = CW_USEDEFAULT ;
                         mdicreate.y       = CW_USEDEFAULT ;
                         mdicreate.cx      = CW_USEDEFAULT ;
                         mdicreate.cy      = CW_USEDEFAULT ;
                         mdicreate.style   = 0 ;
                         mdicreate.lParam  = NULL ;

                         hwndChild = SendMessage (hwndClient,  WM_MDICREATE,
0,
                                        (LONG) (LPMDICREATESTRUCT)
&mdicreate) ;
                         return 0 ;

                    case IDM_CLOSE :         // Close the active window

                         hwndChild = LOWORD (SendMessage (hwndClient,
                                                  WM_MDIGETACTIVE, 0, 0L)) ;

                         if (SendMessage (hwndChild, WM_QUERYENDSESSION, 0,
0L))
                              SendMessage (hwndClient, WM_MDIDESTROY,
                                           hwndChild, 0L) ;
                         return 0 ;

                    case IDM_EXIT :          // Exit the program

                         SendMessage (hwnd, WM_CLOSE, 0, 0L) ;
                         return 0 ;
                                   // Messages for arranging windows
                    case IDM_TILE :
                         SendMessage (hwndClient, WM_MDITILE, 0, 0L) ;
                         return 0 ;

                    case IDM_CASCADE :
                         SendMessage (hwndClient, WM_MDICASCADE, 0, 0L) ;
                         return 0 ;

                    case IDM_ARRANGE :
                         SendMessage (hwndClient, WM_MDIICONARRANGE, 0, 0L)
;
                         return 0 ;

                    case IDM_CLOSEALL :      // Attempt to close all
children

                         lpfnEnum = MakeProcInstance (CloseEnumProc, hInst)
;
                         EnumChildWindows (hwndClient, lpfnEnum, 0L) ;
                         FreeProcInstance (lpfnEnum) ;
                         return 0 ;

                    default :           // Pass to active child

                         hwndChild = LOWORD (SendMessage (hwndClient,
                                                WM_MDIGETACTIVE, 0, 0L)) ;

                        if (IsWindow (hwndChild))
                             SendMessage (hwndChild, WM_COMMAND,
                                          wParam, lParam) ;

                        break ;        // and then to DefFrameProc
                    }
               break ;

          case WM_QUERYENDSESSION :
          case WM_CLOSE :                    // Attempt to close all
children

               SendMessage (hwnd, WM_COMMAND, IDM_CLOSEALL, 0L) ;

               if (NULL != GetWindow (hwndClient, GW_CHILD))
                    return 0 ;

               break ;   // i.e., call DefFrameProc ;

          case WM_DESTROY :
               PostQuitMessage (0) ;
               return 0 ;
          }
               // Pass unprocessed messages to DefFrameProc (not
DefWindowProc)
     return DefFrameProc (hwnd, hwndClient, message, wParam, lParam) ;
     }

BOOL FAR PASCAL CloseEnumProc (HWND hwnd, LONG lParam)
     {
     if (GetWindow (hwnd, GW_OWNER))         // check for icon title
          return 1 ;

     SendMessage (GetParent (hwnd), WM_MDIRESTORE, hwnd, 0L) ;

     if (!SendMessage (hwnd, WM_QUERYENDSESSION, 0, 0L))
          return 1 ;

     SendMessage (GetParent (hwnd), WM_MDIDESTROY, hwnd, 0L) ;
          return 1 ;
     }

long FAR PASCAL HelloWndProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
     static COLORREF clrTextArray [] = { RGB (0,   0, 0), RGB (255, 0,   0),
                                         RGB (0, 255, 0), RGB (  0, 0, 255),
                                         RGB (255, 255, 255) } ;
     static HWND     hwndClient, hwndFrame ;
     HDC             hdc ;
     HMENU           hMenu ;
     LOCALHANDLE     hHelloData ;
     NPHELLODATA     npHelloData ;
     PAINTSTRUCT     ps ;
     RECT            rect ;

     switch (message)
          {
          case WM_CREATE :
                         // Allocate memory for window private data

               hHelloData = LocalAlloc (LMEM_MOVEABLE | LMEM_ZEROINIT,
                                        sizeof (HELLODATA)) ;

               npHelloData = (NPHELLODATA) LocalLock (hHelloData) ;
               npHelloData->nColor  = IDM_BLACK ;
               npHelloData->clrText = RGB (0, 0, 0) ;
               LocalUnlock (hHelloData) ;
               SetWindowWord (hwnd, 0, hHelloData) ;

                         // Save some window handles

               hwndClient = GetParent (hwnd) ;
               hwndFrame  = GetParent (hwndClient) ;
               return 0 ;
          case WM_COMMAND :
               switch (wParam)
                    {
                    case IDM_BLACK :
                    case IDM_RED :
                    case IDM_GREEN :
                    case IDM_BLUE :
                    case IDM_WHITE :
                                   // Change the text color

                         hHelloData  = GetWindowWord (hwnd, 0) ;
                         npHelloData = (NPHELLODATA) LocalLock (hHelloData)
;

                         hMenu = GetMenu (hwndFrame) ;

                         CheckMenuItem (hMenu, npHelloData->nColor,
                                               MF_UNCHECKED) ;
                         npHelloData->nColor = wParam ;
                         CheckMenuItem (hMenu, npHelloData->nColor,
                                               MF_CHECKED) ;

                         npHelloData->clrText =
                               clrTextArray [wParam - IDM_BLACK] ;

                         LocalUnlock (hHelloData) ;
                         InvalidateRect (hwnd, NULL, FALSE) ;
                    }
               return 0 ;

          case WM_PAINT :
                         // Paint the window

               hdc = BeginPaint (hwnd, &ps) ;

               hHelloData  = GetWindowWord (hwnd, 0) ;
               npHelloData = (NPHELLODATA) LocalLock (hHelloData) ;
               SetTextColor (hdc, npHelloData->clrText) ;
               LocalUnlock (hHelloData) ;

               GetClientRect (hwnd, &rect) ;

               DrawText (hdc, "Hello, World!", -1, &rect,
                         DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_MDIACTIVATE :

                         // Set the Hello menu if gaining focus
               if (wParam == TRUE)
                    SendMessage (hwndClient, WM_MDISETMENU, 0,
                                 MAKELONG (hMenuHello, hMenuHelloWindow)) ;

                         // check or uncheck menu item

               hHelloData  = GetWindowWord (hwnd, 0) ;
               npHelloData = (NPHELLODATA) LocalLock (hHelloData) ;
               CheckMenuItem (hMenuHello, npHelloData->nColor,
                              wParam ? MF_CHECKED : MF_UNCHECKED) ;
               LocalUnlock (hHelloData) ;

                         // Set the Init menu if losing focus

               if (wParam == FALSE)
                    SendMessage (hwndClient, WM_MDISETMENU, 0,
                                 MAKELONG (hMenuInit, hMenuInitWindow)) ;

               DrawMenuBar (hwndFrame) ;
               return 0 ;

          case WM_QUERYENDSESSION :
          case WM_CLOSE :
               if (IDOK != MessageBox (hwnd, "OK to close window?", "Hello",
                                       MB_ICONQUESTION | MB_OKCANCEL))
                    return 0 ;

               break ;   // i.e., call DefMDIChildProc

          case WM_DESTROY :
               hHelloData = GetWindowWord (hwnd, 0) ;
               LocalFree (hHelloData) ;
               return 0 ;
          }
               // Pass unprocessed message to DefMDIChildProc

     return DefMDIChildProc (hwnd, message, wParam, lParam) ;
     }

long FAR PASCAL RectWndProc (HWND hwnd, WORD message, WORD wParam, LONG
lParam)
     {
     static HWND hwndClient, hwndFrame ;
     HPEN        hBrush ;
     HDC         hdc ;
     LOCALHANDLE hRectData ;
     NPRECTDATA  npRectData ;
     PAINTSTRUCT ps ;
     short       xLeft, xRight, yTop, yBottom, nRed, nGreen, nBlue ;
     switch (message)
          {
          case WM_CREATE :
                         // Allocate memory for window private data

               hRectData = LocalAlloc (LMEM_MOVEABLE | LMEM_ZEROINIT,
                                       sizeof (RECTDATA)) ;

               SetWindowWord (hwnd, 0, hRectData) ;

                         // Start the timer going

               SetTimer (hwnd, 1, 250, NULL) ;

                         // Save some window handles

               hwndClient = GetParent (hwnd) ;
               hwndFrame  = GetParent (hwndClient) ;
               return 0 ;

          case WM_SIZE :           // Save the window size

               hRectData  = GetWindowWord (hwnd, 0) ;
               npRectData = (NPRECTDATA) LocalLock (hRectData) ;

               npRectData->cxClient = LOWORD (lParam) ;
               npRectData->cyClient = HIWORD (lParam) ;

               LocalUnlock (hRectData) ;

               break ;        // WM_SIZE must be processed by
DefMDIChildProc

          case WM_TIMER :          // Display a random rectangle

               hRectData  = GetWindowWord (hwnd, 0) ;
               npRectData = (NPRECTDATA) LocalLock (hRectData) ;

               xLeft   = rand () % npRectData->cxClient ;
               xRight  = rand () % npRectData->cxClient ;
               yTop    = rand () % npRectData->cyClient ;
               yBottom = rand () % npRectData->cyClient ;
               nRed    = rand () & 255 ;
               nGreen  = rand () & 255 ;
               nBlue   = rand () & 255 ;

               hdc = GetDC (hwnd) ;
               hBrush = CreateSolidBrush (RGB (nRed, nGreen, nBlue)) ;
               SelectObject (hdc, hBrush) ;
               Rectangle (hdc, min (xLeft, xRight), min (yTop, yBottom),
                               max (xLeft, xRight), max (yTop, yBottom)) ;

               ReleaseDC (hwnd, hdc) ;
               DeleteObject (hBrush) ;
               LocalUnlock (hRectData) ;
               return 0 ;

          case WM_PAINT :          // Clear the window

               InvalidateRect (hwnd, NULL, TRUE) ;
               hdc = BeginPaint (hwnd, &ps) ;
               EndPaint (hwnd, &ps) ;
               return 0 ;

          case WM_MDIACTIVATE :    // Set the appropriate menu
               if (wParam == TRUE)
                    SendMessage (hwndClient, WM_MDISETMENU, 0,
                                 MAKELONG (hMenuRect, hMenuRectWindow)) ;
               else
                    SendMessage (hwndClient, WM_MDISETMENU, 0,
                                 MAKELONG (hMenuInit, hMenuInitWindow)) ;

               DrawMenuBar (hwndFrame) ;
               return 0 ;

          case WM_DESTROY :
               hRectData = GetWindowWord (hwnd, 0) ;
               LocalFree (hRectData) ;
               KillTimer (hwnd, 1) ;
               return 0 ;
          }
               // Pass unprocessed message to DefMDIChildProc

     return DefMDIChildProc (hwnd, message, wParam, lParam) ;
     }

 MDIDEMO.RC

/*----------------------------
   MDIDEMO.RC resource script
  ----------------------------*/

#include 
#include "mdidemo.h"
MdiMenuInit MENU
     {
     POPUP "&File"
          {
          MENUITEM "New &Hello",             IDM_NEWHELLO
          MENUITEM "New &Rectangles",        IDM_NEWRECT
          MENUITEM SEPARATOR
          MENUITEM "E&xit",                  IDM_EXIT
          }
     }

MdiMenuHello MENU
     {
     POPUP "&File"
          {
          MENUITEM "New &Hello",             IDM_NEWHELLO
          MENUITEM "New &Rectangles",        IDM_NEWRECT
          MENUITEM "&Close",                 IDM_CLOSE
          MENUITEM SEPARATOR
          MENUITEM "E&xit",                  IDM_EXIT
          }
     POPUP "&Color"
          {
          MENUITEM "&Black",                 IDM_BLACK
          MENUITEM "&Red",                   IDM_RED
          MENUITEM "&Green",                 IDM_GREEN
          MENUITEM "B&lue",                  IDM_BLUE
          MENUITEM "&White",                 IDM_WHITE
          }
     POPUP "&Window"
          {
          MENUITEM "&Cascade\tShift+F5",     IDM_CASCADE
          MENUITEM "&Tile\tShift+F4",        IDM_TILE
          MENUITEM "Arrange &Icons",         IDM_ARRANGE
          MENUITEM "Close &All",             IDM_CLOSEALL
          }
     }

MdiMenuRect MENU
     {
     POPUP "&File"
          {
          MENUITEM "New &Hello",             IDM_NEWHELLO
          MENUITEM "New &Rectangles",        IDM_NEWRECT
          MENUITEM "&Close",                 IDM_CLOSE
          MENUITEM SEPARATOR
          MENUITEM "E&xit",                  IDM_EXIT
          }
     POPUP "&Window"
          {
          MENUITEM "&Cascade\tShift+F5",     IDM_CASCADE
          MENUITEM "&Tile\tShift+F4",        IDM_TILE
          MENUITEM "Arrange &Icons",         IDM_ARRANGE
          MENUITEM "Close &All",             IDM_CLOSEALL
          }
     }

MdiAccel ACCELERATORS
     {
     VK_F5, IDM_CASCADE, VIRTKEY, SHIFT
     VK_F4, IDM_TILE,    VIRTKEY, SHIFT
     }

 MDIDEMO.H

/*-----------------------
   MDIDEMO.H header file
  -----------------------*/

#define INIT_MENU_POS     0
#define HELLO_MENU_POS    2
#define RECT_MENU_POS     1

#define IDM_NEWHELLO     10
#define IDM_NEWRECT      11
#define IDM_CLOSE        12
#define IDM_EXIT         13

#define IDM_BLACK        20
#define IDM_RED          21
#define IDM_GREEN        22
#define IDM_BLUE         23
#define IDM_WHITE        24

#define IDM_TILE         30
#define IDM_CASCADE      31
#define IDM_ARRANGE      32
#define IDM_CLOSEALL     33

#define IDM_FIRSTCHILD  100

 MDIDEMO.DEF

;------------------------------------
; MDIDEMO.DEF module definition file
;------------------------------------

NAME           MDIDEMO

DESCRIPTION    'MDI Demonstration (c) Charles Petzold, 1990'
EXETYPE        WINDOWS
STUB           'WINSTUB.EXE'
CODE           PRELOAD MOVEABLE DISCARDABLE
DATA           PRELOAD MOVEABLE MULTIPLE
HEAPSIZE       1024
STACKSIZE      8192
EXPORTS        FrameWndProc
               CloseEnumProc
               HelloWndProc
               RectWndProc

MDIDEMO 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. The document window that displays "Hello, World!" has a menu that
allows you to change the color of the text.

Three Menus

Let's turn first to the MDIDEMO.RC resource script. The resource script
defines three menu templates used by the program.

The program displays the MdiMenuInit menu when no document windows are
present. This menu simply allows creating a new document or exiting the
program.

The MdiMenuHello menu is associated with the document window that displays
"Hello, World!" The File submenu allows opening a new document of either
type, closing the active document, and exiting the program. The Color
submenu lets you set the text color. The Window submenu has options for
arranging the document windows in a cascaded or tiled fashion, arranging the
document icons, and closing 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
is the same as the MdiMenuHello menu except that it does not include the
Color submenu.

The MDIDEMO.H header file 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 this is needed when
I discuss the program later.)

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, 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 the purpose of
simplicity, I've simply used the standard IDI_APPLICATION icon for the frame
and child.

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 has this color
anyway. However, using this color looks a little better when the frame
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've 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 be used to store a local memory handle
that will reference a block of memory (the size of the HELLODATA or RECTDATA
structures defined near the top of MDIDEMO.C) used to store information
unique to each document window.

Next, WinMain uses LoadMenu to load the three menus and save their handles
in global variables. Three calls to the GetSubMenu function obtain handles
to the Window submenu to which the document list will be appended. These are
also saved in global variables. The LoadAccelerators function loads the
accelerator table.

A call to CreateWindow in WinMain creates the frame window. During the WM-
_CREATE processing 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 is the handle of the submenu to 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.

  þ   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 differs a little from a normal loop:
After obtaining the message from the message queue with a call to
GetMessage, an MDI program passes the message to TranslateMDISysAccel (and
TranslateAccelerator if, like the MDIDEMO program, the program also has menu
accelerators).

The TranslateMDISysAccel function translates any keystrokes that may
correspond to the special MDI accelerators (Ctrl-F6, for example) into a
WM_SYSCOMMAND message. If neither TranslateMDISysAccel nor
TranslateAccelerator returns TRUE (indicating that a message was translated
by one of these functions), do not call TranslateMessage and
DispatchMessage.

Notice the two different window handles passed to TranslateMDISysAccel and
TranslateAccelerator: hwndClient and hwndFrame, respectively. The WinMain
function obtains the hwndClient window handle by calling GetWindow with the
GW_CHILD parameter.



CREATING THE CHILDREN

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 involves 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
filename 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 initially display the document window 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 field 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, and 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 used to create the client
window. When the MDIDEMO program creates its first document window, this 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.

Up to nine documents can be listed on the menu, each preceded by an
underlined number from 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 with a list box that lists all the document
windows. The maintenance of this document list is one of the nicest features
of the Windows 3 MDI support.


MORE FRAME WINDOW MESSAGE PROCESSING

Let's continue with FrameWndProc message processing before turning our
attention to the child document windows.

When you select Close from the File menu, MDIDEMO closes the active child
window. 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 a little more complex. FrameWndProc calls
EnumChildWindows, passing a pointer referencing the CloseEnumProc function.
This function sends a WM_MDIRESTORE message to each child window, followed
by a WM_QUERYENDSESSION and (possibly) a WM_MDIDESTROY message. This is not
done for the icon title window, indicated by a non-NULL return of GetWindow
with the GW_OWNER parameter.

You'll notice that FrameWndProc does not process any of the WM_COMMAND
messages that signal one of the colors being 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 messages that the frame window procedure chooses not to process must be
passed to DefFrameProc. This is one of the new MDI functions. It replaces
DefWindowProc in the frame window procedure. Even if a frame window
procedure traps the WM_MENUCHAR, WM_NEXTMENU, WM_SETFOCUS, or WM_SIZE
messages, these also must be passed to DefFrameProc.

Unprocessed WM_COMMAND messages must 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 document windows it creates. If ever these handles are needed
(such as when processing the Close All option from the menu), they can be
obtained using EnumChildWindows.


THE 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 created 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 involves window properties. Another
approach (the one I used) uses memory space 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 the MDIDEMO program has three different menus:
MdiMenuInit for when no documents are present, MdiMenuHello for when a Hello
document window is active, and MdiMenuRect for 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 is
in effect when the first document is created) to the MdiMenuHello menu. Do
not use the SetMenu function to change a menu in an MDI application.

Another little chore involves the checkmarks on the Color submenu. Program
options such as this should be unique to each document. For example, you
should be able to set black text in one window and red text in another. The
menu checkmarks should reflect the option chosen in the active window. 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 gets the last message 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 whether the window can be closed. (In a real
program, this message box would ask whether a file needed to be saved.) If
the user indicates that the window should not be closed, the window
procedure returns 0.

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. Several messages must be passed to
DefMDIChildProc whether the child window procedure does something with them
or not. These are:  WM_CHILDACTIVATE, WM_GETMINMAXINFO, WM_MENUCHAR,
WM_MOVE, WM- _SETFOCUS, WM_SIZE, and WM_SYSCOMMAND.

RectWndProc is fairly similar to HelloWndProc in much of the overhead
involved, but it's a little simpler (no menu options are involved and the
window does not verify with the user whether 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.


THE POWER OF WINDOW PROCEDURES

Much of the support in Windows 3 for the Multiple Document Interface is
encapsulated in the MDICLIENT window class. I think this clearly illustrates
the power of the object-oriented architecture of Windows. The client window
procedure serves as an intermediary layer between the frame window and the
various document windows.

Now let's look at another powerful feature of Windows--dynamic link
libraries.