App Part 4: Multiple Document Interface
First a bit of background... Every window has a Client Area, this is where most programs draw images, place controls etc... the Client Area is not seperate from the window itself, it is simply a smaller specialised region of it. Sometimes a window can be all client area, and nothing else, sometimes the client area is smaller to make room for menus, titles, scrollbars, etc...
In MDI terms, your main window is called the Frame, this is probably the
only window you would have in a SDI (Single Document Interface) program.
In MDI there is an additional window, called the MDI Client Window
which is a child of your Frame window.
Unlike the Client Area it is a complete and seperate window all on it's
own, it has a client area of it's own and probably a few pixels for a border.
You never directly handle messages for the MDI Client, it is done by the
pre-defined windows class
When it comes to the windows which actually display your document or whatever your program displays, you send a message to the MDI Client to tell it to create a new window of the type you've specified. The new window is created as a child of the MDI Client, not of your Frame window. This new window is an MDI Child. The MDI Child is a child of the MDI Client, which in turn is a child of the MDI Frame (Getting dizzy yet?). To make matters worse, the MDI Child will probably have child windows of its own, for instance the edit control in the example program for this section.
You are responsable for writing two (or more) Window Procedures. One, just like always, for your main window(the Frame). And one more for the MDI Child. You may also have more than one type of Child, in which case, you'll want a seperate window procedure for each type.
If I've thoroughly confused you now talking about MDI Clients and things, this diagram may clear things up a little better:
Getting Started with MDI
MDI requires a few subtle changes throughout a program, so please read through this section carefully... chances are that if your MDI program doesn't work or has strange behaviour it's because you missed one of the alterations from a regular program.
MDI Client Window
Before we create our MDI window we need to make a change to the default message processing
that goes on in our Window Procedure... since we're creating a Frame window that will
host an MDI Client, we need to change the
ELSE RETURN DefFrameProc(hwnd, g_hMDIClient, msg, wParam, lParam);
The next step is to create the MDI Client window itself, as a child of our frame window.
We do this in
VAR ccs : CLIENTCREATESTRUCT;
ccs.hWindowMenu := GetSubMenu(GetMenu(hwnd), 2); ccs.idFirstChild := ID_MDI_FIRSTCHILD; g_hMDIClient := CreateWindowEx(WS_EX_CLIENTEDGE, "mdiclient", "", WS_CHILD BOR WS_CLIPCHILDREN BOR WS_VSCROLL BOR WS_HSCROLL BOR WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hwnd, CAST(HMENU,IDC_MAIN_MDI), Instance, ADR(ccs));
The menu handle is the handle to the popup menu that the MDI client will add items to representing each window that is created, allowing the user to select the window they want to activate from the menu, we'll add functionality shortly to handle this case. In this example it's the 3rd popup (index 2) since I've added Edit and Window to the menu after File.
Now to get this menu to work properly we need to add some special handling to our
| WM_COMMAND : CASE LOWORD(wParam) OF | ID_FILE_EXIT : PostMessage(hwnd, WM_CLOSE, 0, 0); (* ... handle other regular command IDs ... *) (* Handle MDI Window commands *) ELSE IF LOWORD(wParam) >= ID_MDI_FIRSTCHILD THEN DefFrameProc(hwnd, g_hMDIClient, WM_COMMAND, wParam, lParam); ELSE hChild := CAST(HWND,SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,0)); IF hChild#NIL THEN SendMessage(hChild, WM_COMMAND, wParam, lParam); END; END; END;
I've added an
If it isn't one of the Window IDs then I get the handle to the active child window and forward the message to it for processing. This allows you to delegate responsibility to the Child windows for performing certain actions, and allows different child windows to handle commands in different ways if so desired. In the example I only handle commands that are global to the program in the Frame window procedure, and send the commands which affect a certain document or child window on to the child window itself for processsing.
Since we're building on the last example, the code to size the MDI client is the same as the code to resize the edit control in the last example, that takes into account the size and position of the tool and status bars so they don't overlap the MDI client window.
We also need to modify our message loop a little...
WHILE GetMessage( Msg, NIL, 0, 0) DO IF NOT(TranslateMDISysAccel(g_hMDIClient, Msg)) THEN FUNC TranslateMessage(Msg); FUNC DispatchMessage(Msg); END; END;
We've added an extra step (
Child Window Class
In addition to the main window of the program (the Frame window) we need to create new window classes for each type of child window we want. For example you might have one to display text, and one to display a picture or graph. In this example we'll only be creating one child type, which will be just like the editor program in the previous examples.
PROCEDURE SetUpMDIChildWindowClass() : BOOL; VAR wc : WNDCLASS; BEGIN wc.style := CS_HREDRAW BOR CS_VREDRAW; wc.lpfnWndProc := MDIChildWndProc; wc.cbClsExtra := 0; wc.cbWndExtra := 0; wc.hInstance := Instance; wc.hIcon := LoadIcon(NIL, IDI_APPLICATION^); wc.hCursor := LoadCursor(NIL, IDC_ARROW^); wc.hbrBackground := CAST(HBRUSH,COLOR_3DFACE+1); wc.lpszMenuName := NIL; wc.lpszClassName := ADR(g_szChildClassName); IF RegisterClass(wc)=0 THEN MessageBox(NIL, "Could Not Register Child Window", "Oh Oh...", MB_ICONEXCLAMATION BOR MB_OK); RETURN FALSE; ELSE RETURN TRUE; END; END SetUpMDIChildWindowClass;
This is basically identical to registering our regular frame window, there are no particularly special flags here for use with MDI. We've set the menu as NIL, and the window procedure to point to the child window procedure which we will write next.
MDI Child Procedure
The window procecure for an MDI child is much like any other with a few small exceptions.
First of all, default messages are passed to
In this particular case, we also want to disable the Edit and Window menu's when they
aren't needed (just because it's a nice thing to do), so we handle
To be even more complete, we can disable the Close and Save File menu items as well, since they aren't going to be any good with no windows to act on. I've disabled all these items by default in the resource so that I don't need to add extra code to do it when the application first starts up.
PROCEDURE MDIChildWndProc(hwnd : HWND; msg : UINT; wParam : WPARAM; lParam : LPARAM): LRESULT [EXPORT, OSCall]; VAR hfDefault : HFONT; hEdit : HWND; rcClient : RECT; hMenu, hFileMenu : HMENU; EnableFlag : UINT; BEGIN CASE msg OF | WM_CREATE : (* Create Edit Control *) hEdit := CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", WS_CHILD BOR WS_VISIBLE BOR WS_VSCROLL BOR WS_HSCROLL BOR ES_MULTILINE BOR ES_AUTOVSCROLL BOR ES_AUTOHSCROLL, 0, 0, 100, 100, hwnd, CAST(HMENU,IDC_CHILD_EDIT), Instance, NIL); IF hEdit=NIL THEN MessageBox(hwnd, "Could not create edit box.", "Error", MB_OK BOR MB_ICONERROR); PostQuitMessage(0); ELSE hfDefault := GetStockFont(DEFAULT_GUI_FONT); SendMessage(hEdit, WM_SETFONT, CAST(WPARAM,hfDefault),0); END; | WM_MDIACTIVATE: hMenu := GetMenu(g_hMainWindow); IF hwnd = CAST(HWND,lParam) THEN (* being activated, enable the menus *) EnableFlag := MF_ENABLED; ELSE (* being de-activated, gray the menus *) EnableFlag := MF_GRAYED; END; EnableMenuItem(hMenu, 1, MF_BYPOSITION BOR EnableFlag); EnableMenuItem(hMenu, 2, MF_BYPOSITION BOR EnableFlag); hFileMenu := GetSubMenu(hMenu, 0); EnableMenuItem(hFileMenu, ID_FILE_SAVEAS, MF_BYCOMMAND BOR EnableFlag); EnableMenuItem(hFileMenu, ID_FILE_CLOSE, MF_BYCOMMAND BOR EnableFlag); EnableMenuItem(hFileMenu, ID_FILE_CLOSEALL, MF_BYCOMMAND BOR EnableFlag); DrawMenuBar(g_hMainWindow); | WM_SIZE : (* Calculate remaining height and size edit *) GetClientRect(hwnd, rcClient); hEdit := GetDlgItem(hwnd, IDC_CHILD_EDIT); SetWindowPos(hEdit, NIL, 0, 0, rcClient.right, rcClient.bottom, SWP_NOZORDER); RETURN DefMDIChildProc( hwnd, msg, wParam, lParam); | WM_COMMAND : CASE LOWORD(wParam) OF | ID_FILE_OPEN : DoFileOpen(hwnd); | ID_FILE_SAVEAS : DoFileSave(hwnd); | ID_EDIT_CUT : SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_CUT, 0, 0); | ID_EDIT_COPY : SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_COPY, 0, 0); | ID_EDIT_PASTE : SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_PASTE, 0, 0); ELSE END; ELSE RETURN DefMDIChildProc( hwnd, msg, wParam, lParam); END; RETURN 0; END MDIChildWndProc;
I've implemented the File Open and Save as commands, the
The Edit commands are easy, because the edit control has built in support for them, we just tell it what to do.
Remember I mentioned that there are little things you need to remember or your application will
behave strangely? Note that I've called
Creating and Destroying Windows
MDI Child windows are not created directly, isntead we send a
PROCEDURE CreateNewMDIChild(hMDIClient : HWND) : HWND; VAR mcs : MDICREATESTRUCT; hChild : HWND; BEGIN mcs.szTitle := ADR("[Untitled]"); mcs.szClass := ADR(g_szChildClassName); mcs.hOwner := Instance; mcs.x := CW_USEDEFAULT; mcs.y := CW_USEDEFAULT; mcs.cx := CW_USEDEFAULT; mcs.cy := CW_USEDEFAULT; mcs.style := 0; mcs.lParam := 0; hChild := CAST(HWND,SendMessage(hMDIClient, WM_MDICREATE, 0, CAST(LPARAM,ADR(mcs)))); IF hChild = NIL THEN MessageBox(hMDIClient, "MDI Child creation failed.", "Oh Oh...", MB_ICONEXCLAMATION BOR MB_OK); END; RETURN hChild; END CreateNewMDIChild;
One member of
VAR pCreateStruct : LPCREATESTRUCT; pMDICreateStruct : LPMDICREATESTRUCT;
| WM_CREATE: pCreateStruct := CAST(LPCREATESTRUCT,lParam); pMDICreateStruct := CAST(LPMDICREATESTRUCT,pCreateStruct.lpCreateParams); (* pMDICreateStruct now points to the same MDICREATESTRUCT that you sent along with the WM_MDICREATE message and you can use it to access the lParam. *)
Now we can implement the File commands on our menu in our Frame window procedure:
| ID_FILE_NEW : CreateNewMDIChild(g_hMDIClient); | ID_FILE_OPEN : hChild := CreateNewMDIChild(g_hMDIClient); IF hChild#NIL THEN DoFileOpen(hChild); END; | ID_FILE_CLOSE : hChild := CAST(HWND,SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,0)); IF hChild#NIL THEN SendMessage(hChild, WM_CLOSE, 0, 0); END;
We can also provide some default MDI processing of window arrangment for our Window menu, since MDI supports this itself it's not much work.
| ID_WINDOW_TILE : SendMessage(g_hMDIClient, WM_MDITILE, 0, 0); | ID_WINDOW_CASCADE : SendMessage(g_hMDIClient, WM_MDICASCADE, 0, 0);
Copyright © 1998-2011, Brook Miles. All rights reserved. Adapted for Modula-2 by Frank Schoonjans, with permission.