多重文件接口(MDI)是Microsoft Windows文件处理应用程序的一种规范,该规范描述了窗口结构和允许使用者在单个应用程序中使用多个文件的使用者接口(如文书处理程序中的文字文件和电子表格程序中的电子表格)。简单地说,就像Windows在一个屏幕上维护多个应用程序窗口一样,MDI应用程序在一个显示区域内维护多个文件窗口。Windows中的第一个MDI应用程序是Windows下的Microsoft Excel的第一个版本。紧接着又出现了许多其它的应用程序。
MDI 概念
尽管MDI规范随着Windows 2.0的推出已经很普及,但在那时,MDI应用程序写起来很困难,并且需要一些非常复杂的程序设计工作。从Windows 3.0起,其中许多工作就都由Windows为您做好了。Windows 95中增强的支持也已经被添加进Windows 98和Microsoft Windows NT中。
MDI 的组成
MDI程序的主应用程序窗口是很普通的:它有一个标题列、一个菜单、一个缩放边框、一个系统菜单图标和最大化/最小化/关闭按钮。显示区域经常被称为「工作空间」,它不直接用于显示程序输出。这个工作空间包括零个或多个子窗口,每个窗口都显示一个文件。
这些子窗口看起来与通常的应用程序窗口以及MDI程序的主窗口很相似。它们有一个标题列、一个缩放边框、一个系统菜单图标和最大化/最小化/关闭按钮,可能还包括滚动条。但是文件窗口没有菜单,主应用程序窗口上的菜单适用于文件窗口。
在任何时候都只能有一个文件窗口是活动的(加亮标题列来表示),它出现在其它所有文件窗口之前。所有文件窗口都由工作空间区域加以剪裁,而不会出现在应用程序窗口之外。
初看起来,对Windows程序写作者来说,MDI似乎是相当简单。需要程序写作者做的工作好像就是为每个文件建立一个WS_CHILD窗口,并使程序的主应用程序窗口成为文件窗口的父窗口。但对现有的MDI应用程序稍加研究,就会发现一些导致程序写作困难的复杂问题。例如:
- MDI文件窗口可以最小化。它的图示出现在工作空间的底部。一般来说,MDI应用程序可以将不同的图示分别用于主应用程序窗口和每一类文件应用。
- MDI文件窗口可以最大化。在这种情况下,文件窗口的标题列(一般用来显示窗口中文件的文件名称)消失,文件名称出现在应用程序窗口标题列的应用程序名称之后,文件窗口的系统菜单图标成为应用程序窗口的顶层菜单中的第一项。关闭文件窗口按钮变成顶层菜单中的最后一项,且出现在最右边。
- 用以关闭文件窗口的系统键盘快捷键与关闭主窗口的系统键盘快捷键一样,只是Ctrl键代替了Alt键。这也就是说,Alt+F4用于关闭应用程序窗口,而Ctrl+F4用于关闭文件窗口。此外,Ctrl+F6可以在活动MDI应用程序的子文件窗口之间切换。与平时一样,Alt+空格键启动主窗口的系统菜单,Alt+-(减号)启动活动子文件窗口的系统菜单。
- 当使用光标键在菜单项间移动时,控件权通常从系统菜单转到菜单列中的第一项。在MDI应用程序中,控件权是从应用程序系统菜单转到活动文件系统菜单,然后再转到菜单列中的第一项。
- 如果应用程序能够支持若干种型态的子窗口(如Microsoft Excel中的工作表和图表文件),那么菜单应能反映出与这种型态的文件有关的操作。这就要求当不同的文字窗口变成活动窗口时,程序能更换菜单。此外,当没有文件窗口存在时,菜单应该被缩减到只剩下与打开新文件有关的操作。
- 顶层菜单上有一个叫做「窗口(Window)」的菜单项。按照习惯,这是顶层菜单上「Help」之前的那一项,即倒数第二项。「窗口」子菜单上通常包含在工作空间内安排文件窗口的选项。文件窗口可以从左上方开始「平铺」或「层迭」。在前一种方式下,可以完整地看到每一个文件窗口。这个子菜单同时也包含所有文件窗口的列表。从中选择一个文件窗口,就可以把此文件窗口移到前景。
Windows 98支持MDI的所有这些方面。当然,需要您做一些工作(如下面的范例程序所示),但是,这远不是要您程序写作来直接支持所有这些功能。
MDI支援
探讨Windows的MDI支持时需要发表一些新术语。主应用程序窗口称为「框架窗口」,就像传统的Windows程序一样,它是WS_OVERLAPPEDWINDOW样式的窗口。
MDI应用程序还根据预先定义的窗口类别MDICLIENT建立「客户窗口」,这一客户窗口是用这种窗口类别和WS_CHILD样式呼叫CreateWindow来建立的。这一呼叫的最后一个参数是指向一个CLIENTCREATESTRUCT型态的结构的指针。这个客户窗口覆盖框架窗口的显示区域,并提供许多MDI支持。此客户窗口的颜色是系统颜色COLOR_APPWORKSPACE。
文件窗口被称为「子窗口」。通过初始化一个MDICREATESTRUCT型态的结构,以一个指向此结构的指针为参数将消息WM_MDICREATE发送给客户窗口,就可以建立这些文件窗口。
文件窗口是客户窗口的子窗口,而客户窗口又是框架窗口的子窗口。父-子窗口分层结构如图19-1所示。
图19-1 Windows MDI应用程序的父-子层次图 |
您需要框架窗口的窗口类别(及窗口消息处理程序)和一个由应用程序支持的每类子窗口的窗口类别(及窗口消息处理程序)。由于已经预先注册了窗口类别,所以不需要客户窗口的窗口消息处理程序。
Windows 98的MDI支持包括一个窗口类别、五个函数、两个数据结构和12个消息。前面已经提到了MDI窗口类别,即MDICLIENT,以及数据结构CLIENTCREATESTRUCT和MDICREATESTRUCT。在MDI应用程序中,这五个函数中的两个用于取代DefWindowProc:不再将DefWindowProc呼叫用于所有未处理的消息,而是由框架窗口过程调用DefFrameProc,子窗口过程调用DefMDIChildProc。另一个MDI特有的函数TranslateMDISysAccel与第十章中讨论的TranslateAccelerator的使用方式相同。MDI支持也包括ArrangeIconicWindows函数,但有一条专用的MDI消息使得此函数对MDI程序来说不再必要。
第五个MDI函数是CreateMDIWindow,它使得子窗口可以在单独的线程中被建立。这个函数不需要在单线程的程序中,我会展示这一点。
在下面的程序中,我将展示12条MDI消息中的9条(其它三个消息一般不用),这些消息的前缀是WM_MDI。框架窗口向客户窗口发送其中某个消息,以便在子窗口上完成一项操作或者取得关于子窗口的信息(例如,框架窗口发送一个WM_MDICREATE消息给客户窗口,以建立子窗口)。消息WM_MDIACTIVATE消息有点特别:框架窗口可以发送这个消息给客户窗口来启动一个子窗口,而客户窗口也把这个消息发送给将被启动或者失去活动的子窗口,以便通知它们这一变化。
程序19-1 MDIDEMO程序说明了编写MDI应用程序的基本方法。
程序19-1 MDIDEMO
MDIDEMO.C /*--------------------------------------------------------------------------- MDIDEMO.C -- Multiple-Document Interface Demonstration (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #define INIT_MENU_POS 0 #define HELLO_MENU_POS 2 #define RECT_MENU_POS 1 #define IDM_FIRSTCHILD 50000 LRESULT CALLBACK FrameWndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK CloseEnumProc (HWND, LPARAM) ; LRESULT CALLBACK HelloWndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK RectWndProc (HWND, UINT, WPARAM, LPARAM) ; // structure for storing data unique to each Hello child window typedef struct tagHELLODATA { UINT iColor ; COLORREF clrText ; } HELLODATA, * PHELLODATA ; // structure for storing data unique to each Rect child window typedef struct tagRECTDATA { short cxClient ; short cyClient ; } RECTDATA, * PRECTDATA ; // global variables TCHAR szAppName[] = TEXT ("MDIDemo") ; TCHAR szFrameClass[] = TEXT ("MdiFrame") ; TCHAR szHelloClass[] = TEXT ("MdiHelloChild") ; TCHAR szRectClass[] = TEXT ("MdiRectChild") ; HINSTANCE hInst ; HMENU hMenuInit, hMenuHello, hMenuRect ; HMENU hMenuInitWindow, hMenuHelloWindow, hMenuRectWindow ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HACCEL hAccel ; HWND hwndFrame, hwndClient ; MSG msg ; WNDCLASS wndclass ; hInst = hInstance ; // 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 = (HBRUSH) (COLOR_APPWORKSPACE + 1) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szFrameClass ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } // Register the Hello child window class wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = HelloWndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = sizeof (HANDLE) ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) 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 (HANDLE) ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szRectClass ; RegisterClass (&wndclass) ; // Obtain handles to three possible menus & submenus hMenuInit = LoadMenu (hInstance, TEXT ("MdiMenuInit")) ; hMenuHello = LoadMenu (hInstance, TEXT ("MdiMenuHello")) ; hMenuRect = LoadMenu (hInstance, TEXT ("MdiMenuRect")) ; hMenuInitWindow = GetSubMenu (hMenuInit, INIT_MENU_POS) ; hMenuHelloWindow = GetSubMenu (hMenuHello, HELLO_MENU_POS) ; hMenuRectWindow = GetSubMenu (hMenuRect, RECT_MENU_POS) ; // Load accelerator table hAccel = LoadAccelerators (hInstance, szAppName) ; // Create the frame window hwndFrame = CreateWindow (szFrameClass, TEXT ("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, iCmdShow) ; 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) ; } } // Clean up by deleting unattached menus DestroyMenu (hMenuHello) ; DestroyMenu (hMenuRect) ; return msg.wParam ; } LRESULT CALLBACK FrameWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HWND hwndClient ; CLIENTCREATESTRUCT clientcreate ; HWND hwndChild ; MDICREATESTRUCT mdicreate ; switch (message) { case WM_CREATE: // Create the client window clientcreate.hWindowMenu = hMenuInitWindow ; clientcreate.idFirstChild = IDM_FIRSTCHILD ; hwndClient = CreateWindow ( TEXT ("MDICLIENT"), NULL, WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU) 1, hInst, (PSTR) &clientcreate) ; return 0 ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDM_FILE_NEWHELLO: // Create a Hello child window mdicreate.szClass = szHelloClass ; mdicreate.szTitle = TEXT ("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 = 0 ; hwndChild = (HWND) SendMessage (hwndClient, WM_MDICREATE, 0, (LPARAM) (LPMDICREATESTRUCT) &mdicreate) ; return 0 ; case IDM_FILE_NEWRECT: // Create a Rect child window mdicreate.szClass = szRectClass ; mdicreate.szTitle = TEXT ("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 = 0 ; hwndChild = (HWND) SendMessage (hwndClient, WM_MDICREATE, 0, (LPARAM) (LPMDICREATESTRUCT) &mdicreate) ; return 0 ; case IDM_FILE_CLOSE: // Close the active window hwndChild = (HWND) SendMessage (hwndClient, WM_MDIGETACTIVE, 0, 0) ; if (SendMessage (hwndChild, WM_QUERYENDSESSION, 0, 0)) SendMessage (hwndClient, WM_MDIDESTROY, (WPARAM) hwndChild, 0) ; return 0 ; case IDM_APP_EXIT:// Exit the program SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ; // messages for arranging windows case IDM_WINDOW_TILE: SendMessage (hwndClient, WM_MDITILE, 0, 0) ; return 0 ; case IDM_WINDOW_CASCADE: SendMessage (hwndClient, WM_MDICASCADE, 0, 0) ; return 0 ; case IDM_WINDOW_ARRANGE: SendMessage (hwndClient, WM_MDIICONARRANGE, 0, 0) ; return 0 ; case IDM_WINDOW_CLOSEALL: // Attempt to close all children EnumChildWindows (hwndClient, CloseEnumProc, 0) ; return 0 ; default: // Pass to active child... hwndChild = (HWND) SendMessage (hwndClient, WM_MDIGETACTIVE, 0, 0) ; 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_WINDOW_CLOSEALL, 0) ; 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 CALLBACK CloseEnumProc (HWND hwnd, LPARAM lParam) { if (GetWindow (hwnd, GW_OWNER)) // Check for icon title return TRUE ; SendMessage (GetParent (hwnd), WM_MDIRESTORE, (WPARAM) hwnd, 0) ; if (!SendMessage (hwnd, WM_QUERYENDSESSION, 0, 0)) return TRUE ; SendMessage (GetParent (hwnd), WM_MDIDESTROY, (WPARAM) hwnd, 0) ; return TRUE ; } LRESULT CALLBACK HelloWndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM 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 ; PHELLODATA pHelloData ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_CREATE: // Allocate memory for window private data pHelloData = (PHELLODATA) HeapAlloc (GetProcessHeap (), HEAP_ZERO_MEMORY, sizeof (HELLODATA)) ; pHelloData->iColor = IDM_COLOR_BLACK ; pHelloData->clrText = RGB (0, 0, 0) ; SetWindowLong (hwnd, 0, (long) pHelloData) ; // Save some window handles hwndClient = GetParent (hwnd) ; hwndFrame = GetParent (hwndClient) ; return 0 ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDM_COLOR_BLACK: case IDM_COLOR_RED: case IDM_COLOR_GREEN: case IDM_COLOR_BLUE: case IDM_COLOR_WHITE: // Change the text color pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; hMenu = GetMenu (hwndFrame) ; CheckMenuItem (hMenu, pHelloData->iColor, MF_UNCHECKED) ; pHelloData->iColor = wParam ; CheckMenuItem (hMenu, pHelloData->iColor, MF_CHECKED) ; pHelloData->clrText = clrTextArray[wParam - IDM_COLOR_BLACK] ; InvalidateRect (hwnd, NULL, FALSE) ; } return 0 ; case WM_PAINT: // Paint the window hdc = BeginPaint (hwnd, &ps) ; pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; SetTextColor (hdc, pHelloData->clrText) ; GetClientRect (hwnd, &rect) ; DrawText (hdc, TEXT ("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 (lParam == (LPARAM) hwnd) SendMessage (hwndClient, WM_MDISETMENU,(WPARAM) hMenuHello, (LPARAM) hMenuHelloWindow) ; // Check or uncheck menu item pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; CheckMenuItem (hMenuHello, pHelloData->iColor, (lParam == (LPARAM) hwnd) ? MF_CHECKED : MF_UNCHECKED) ; // Set the Init menu if losing focus if (lParam != (LPARAM) hwnd) SendMessage (hwndClient, WM_MDISETMENU, (WPARAM) hMenuInit,(LPARAM) hMenuInitWindow) ; DrawMenuBar (hwndFrame) ; return 0 ; case WM_QUERYENDSESSION: case WM_CLOSE: if (IDOK != MessageBox (hwnd, TEXT ("OK to close window?"), TEXT ("Hello"), MB_ICONQUESTION | MB_OKCANCEL)) return 0 ; break ; // i.e., call DefMDIChildProc case WM_DESTROY: pHelloData = (PHELLODATA) GetWindowLong (hwnd, 0) ; HeapFree (GetProcessHeap (), 0, pHelloData) ; return 0 ; } // Pass unprocessed message to DefMDIChildProc return DefMDIChildProc (hwnd, message, wParam, lParam) ; } LRESULT CALLBACK RectWndProc ( HWND hwnd, UINT message,WPARAM wParam, LPARAM lParam) { static HWND hwndClient, hwndFrame ; HBRUSH hBrush ; HDC hdc ; PRECTDATA pRectData ; PAINTSTRUCT ps ; int xLeft, xRight, yTop, yBottom ; short nRed, nGreen, nBlue ; switch (message) { case WM_CREATE: // Allocate memory for window private data pRectData = (PRECTDATA) HeapAlloc (GetProcessHeap (), HEAP_ZERO_MEMORY, sizeof (RECTDATA)) ; SetWindowLong (hwnd, 0, (long) pRectData) ; // Start the timer going SetTimer (hwnd, 1, 250, NULL) ; // Save some window handles hwndClient = GetParent (hwnd) ; hwndFrame = GetParent (hwndClient) ; return 0 ; case WM_SIZE: // If not minimized, save the window size if (wParam != SIZE_MINIMIZED) { pRectData = (PRECTDATA) GetWindowLong (hwnd, 0) ; pRectData->cxClient = LOWORD (lParam) ; pRectData->cyClient = HIWORD (lParam) ; } break ; // WM_SIZE must be processed by DefMDIChildProc case WM_TIMER: // Display a random rectangle pRectData = (PRECTDATA) GetWindowLong (hwnd, 0) ; xLeft = rand () % pRectData->cxClient ; xRight = rand () % pRectData->cxClient ; yTop = rand () % pRectData->cyClient ; yBottom = rand () % pRectData->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) ; 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 (lParam == (LPARAM) hwnd) SendMessage (hwndClient, WM_MDISETMENU, (WPARAM) hMenuRect, (LPARAM) hMenuRectWindow) ; else SendMessage (hwndClient, WM_MDISETMENU, (WPARAM) hMenuInit, (LPARAM) hMenuInitWindow) ; DrawMenuBar (hwndFrame) ; return 0 ; case WM_DESTROY: pRectData = (PRECTDATA) GetWindowLong (hwnd, 0) ; HeapFree (GetProcessHeap (), 0, pRectData) ; KillTimer (hwnd, 1) ; return 0 ; } // Pass unprocessed message to DefMDIChildProc return DefMDIChildProc (hwnd, message, wParam, lParam) ; }
MDIDEMO.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" / // Menu MDIMENUINIT MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "New &Hello", IDM_FILE_NEWHELLO MENUITEM "New &Rectangle", IDM_FILE_NEWRECT MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END END MDIMENUHELLO MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "New &Hello", IDM_FILE_NEWHELLO MENUITEM "New &Rectangle", IDM_FILE_NEWRECT MENUITEM "&Close", IDM_FILE_CLOSE MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Color" BEGIN MENUITEM "&Black", IDM_COLOR_BLACK MENUITEM "&Red", IDM_COLOR_RED MENUITEM "&Green", IDM_COLOR_GREEN MENUITEM "B&lue", IDM_COLOR_BLUE MENUITEM "&White", IDM_COLOR_WHITE END POPUP "&Window" BEGIN MENUITEM "&Cascade\tShift+F5", IDM_WINDOW_CASCADE MENUITEM "&Tile\tShift+F4", IDM_WINDOW_TILE MENUITEM "Arrange &Icons", IDM_WINDOW_ARRANGE MENUITEM "Close &All", IDM_WINDOW_CLOSEALL END END MDIMENURECT MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "New &Hello", IDM_FILE_NEWHELLO MENUITEM "New &Rectangle", IDM_FILE_NEWRECT MENUITEM "&Close", IDM_FILE_CLOSE MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Window" BEGIN MENUITEM "&Cascade\tShift+F5", IDM_WINDOW_CASCADE MENUITEM "&Tile\tShift+F4", IDM_WINDOW_TILE MENUITEM "Arrange &Icons", IDM_WINDOW_ARRANGE MENUITEM "Close &All", IDM_WINDOW_CLOSEALL END END / // Accelerator MDIDEMO ACCELERATORS DISCARDABLE BEGIN VK_F4, IDM_WINDOW_TILE, VIRTKEY, SHIFT, NOINVERT VK_F5, IDM_WINDOW_CASCADE, VIRTKEY, SHIFT, NOINVERT END
RESOURCE.H (摘录) // Microsoft Developer Studio generated include file. // Used by MDIDemo.rc #define IDM_FILE_NEWHELLO 40001 #define IDM_FILE_NEWRECT 40002 #define IDM_APP_EXIT 40003 #define IDM_FILE_CLOSE 40004 #define IDM_COLOR_BLACK 40005 #define IDM_COLOR_RED 40006 #define IDM_COLOR_GREEN 40007 #define IDM_COLOR_BLUE 40008 #define IDM_COLOR_WHITE 40009 #define IDM_WINDOW_CASCADE 40010 #define IDM_WINDOW_TILE 40011 #define IDM_WINDOW_ARRANGE 40012 #define IDM_WINDOW_CLOSEALL 40013
MDIDEMO支持两种型态的非常简单的文件窗口:第一种窗口在它的显示区域中央显示"Hello, World!",另一种窗口显示一系列随机矩形(在原始码列表和标识符名中,它们分别叫做「Hello」文件和「Rect」文件)。这两类文件窗口的菜单不同,显示"Hello, World!"的文件窗口有一个允许使用者修改文字颜色的菜单。
三个菜单
现在让我们先看看MDIDEMO.RC资源描述文件,它定义了程序所使用的三个菜单模板。
当文件窗口不存在时,程序显示MdiMenuInit菜单,这个菜单只允许使用者建立新文件或退出程序。
MdiMenuHello菜单与显示「Hello, World!」的文件窗口相关联。「File」子菜单允许使用者打开任何一类新文件、关闭活动文件或退出程序。「Color」子菜单允许使用者设定文字颜色。Window子菜单包括以平铺或者重迭的方式安排文件窗口、安排文件图标或关闭所有窗口等选项,这个子菜单也列出了它们建立的所有文件窗口。
MdiMenuRect菜单与随机矩形文件相关联。除了不包含「Color」子菜单外,它与MdiMenuHello菜单一样。
RESOURCE.H表头文件定义所有的菜单标识符。另外,以下三个常数定义在MDIDEMO.C中:
#define INIT_MENU_POS 0 #define HELLO_MENU_POS 2 #define RECT_MENU_POS 1
这些标识符说明每个菜单模板中Windows子菜单的位置。程序需要这些信息来通知客户窗口文件列表应出现在哪里。当然,MdiMenuInit菜单没有Windows子菜单,所以如前所述,文件列表应附加在第一个子菜单中(位置0)。不过,实际上永远不会在此看到文件列表(在后面讨论此程序时,您可以发现这样做的原因)。
定义在MDIDEMO.C中的IDM_FIRSTCHILD标识符不对应于菜单项,它与出现在Windows子菜单上的文件列表中的第一个文件窗口相关联。这个标识符的值应当大于所有其它菜单ID的值。
程序初始化
在MDIDEMO.C中,WinMain是从注册框架窗口和两个子窗口的窗口类别开始的。窗口消息处理程序是FrameWndProc、HelloWndProc和RectWndProc。一般来说,这些窗口类别应该与不同的图标相关联。为了简单起见,我们将标准IDI_APPLICATION图标用于框架窗口和子窗口。
注意,我们已经定义了框架窗口类别的WNDCLASS结构的hbrBackground字段为COLOR_APPWORKSPACE系统颜色。由于框架窗口的显示区域被客户窗口所覆盖并且客户窗口具有这种颜色,所以上面的定义不是绝对必要的。但是,在最初显示框架窗口时,使用这种颜色似乎要好一些。
这三种窗口类别中的lpszMenuName字段都设定为NULL。对「Hello」和「Rect」子窗口类别来说,这是很自然的。对于框架窗口类别,我在建立框架窗口时在CreateWindow函数中给出菜单句柄。
「Hello」和「Rect」子窗口的窗口类别将WNDCLASS结构中的cbWndExtra字段设为非零值来为每个窗口配置额外空间,这个空间将用于储存指向一个内存块的指针(HELLODATA和RECTDATA结构的大小定义在MDIDEMO.C的开始处),这个内存块被用于储存每个文件窗口特有的信息。
下一步,WinMain用LoadMenu载入三个菜单,并把它们的句柄储存到整体变量中。呼叫三次GetSubMenu函数可获得Windows子菜单(文件列表将加在它上面)的句柄,同样也把它们储存到整体变量中。LoadAccelerators函数加载加速键表。
在WinMain中呼叫CreateWindow建立框架窗口。在FrameWndProc中WM_CREATE消息处理期间,框架窗口建立客户窗口。这项操作涉及到再一次呼叫函数CreateWindow。窗口类别被设定为MDICLIENT,它是预先注册的MDI显示区域窗口类别。在Windows中许多对MDI的支持被放入了MDICLIENT窗口类别中。显示区域窗口消息处理程序作为框架窗口和不同文件窗口的中间层。当呼叫CreateWindow建立显示区域窗口时,最后一个参数必须被设定为指向CLIENTCREATESTRUCT型态结构的指针。这个结构有两个字段:
- hWindowMenu是要加入文件列表的子菜单的句柄。在MDIDEMO中,它是hMenuInitWindow,是在WinMain期间获得的。后面将看到如何修改此菜单。
- idFirstChild是与文件列表中的第一个文件窗口相关联的菜单ID。它就是IDM_FIRSTCHILD.
再让我们回过头来看看WinMain。MDIDEMO显示新建立的框架窗口并进入消息循环。消息循环与正常的循环稍有不同:在呼叫GetMessage从消息队列中获得消息之后,MDI程序把这个消息传送给了TranslateMDISysAccel(以及TranslateAccelerator,如果像MDIDEMO程序一样,程序本身也有菜单快捷键的话)。
TranslateMDISysAccel函数把可能对应特定MDI快捷键(例如Ctrl-F6)的按键转换成WM_SYSCOMMAND消息。如果TranslateMDISysAccel或TranslateAccelerator都传回TRUE(表示某个消息已被这些函数之一转换),就不能呼叫TranslateMessage和DispatchMessage。
注意传递到TranslateMDISysAccel和TranslateAccelerator的两个窗口句柄:hwndClient和hwndFrame。WinMain函数通过用GW_CHILD参数呼叫GetWindow获得hwndClient窗口句柄。
建立子窗口
FrameWndProc的大部分工作是用于处理通知菜单选择的WM_COMMAND消息。与平时一样,FrameWndProc中wParam参数的低字组包含着菜单ID。
在菜单ID的值为IDM_FILE_NEWHELLO和IDM_FILE_NEWRECT的情况下,FrameWndProc必须建立一个新的文件窗口。这涉及到初始化MDICREATESTRUCT结构中的字段(大多数字段对应于CreateWindow的参数),并将消息WM_MDICREATE发送给客户窗口,消息的lParam参数设定为指向这个结构的指针。然后由客户窗口建立子文件窗口。(也可以使用CreateMDIWindow函数。)
MDICREATESTRUCT结构中的szTitle字段一般是对应于文件的文件名称。样式字段设定为窗口样式WS_HSCROLL、WS_VSCROLL或这两者的组合,以便在文件窗口中包括滚动条。样式字段也可以包括WS_MINIMIZE或WS_MAXIMIZE,以便在最初时以最小化或最大化状态显示文件窗口。
MDICREATESTRUCT结构的lParam字段为框架窗口和子窗口共享某些变量提供了一种方法。这个字段可以设定为含有一个结构的内存块的内存句柄。在子文件窗口的WM_CREATE消息处理期间,lParam是一个指向CREATESTRUCT结构的指针,这个结构的lpCreateParams字段是一个指向用于建立窗口的MDICREATESTRUCT结构的指针。
客户窗口一旦接收到WM_MDICREATE消息就建立一个子文件窗口,并把窗口标题加到用于建立客户窗口的MDICLIENTSTRUCT结构中所指定的子菜单的底部。当MDIDEMO程序建立它的第一个文件窗口时,这个子菜单就是「MdiMenuInit」菜单中的「File」子菜单。后面将看到这个文件列表将如何移到「MdiMenuHello」和「MdiMenuRect」菜单的「Windows」子菜单中。
菜单上可以列出9个文件,每个文件的前面是带有底线的数字1至9。如果建立的文件窗口多于9个,则这个清单后跟有「More Windows」菜单项。该项启动带有清单方块的对话框,清单方块列出了所有文件。这种文件列表的维护是Windows MDI支持的最好特性之一。
关于框架窗口的消息处理
在把注意力转移到子文件窗口之前,我们先继续讨论FrameWndProc的消息处理。
当从「File」菜单中选择「Close」时,MDIDEMO关闭活动子窗口。它通过把WM_MDIGETACTIVE消息发送给客户窗口,而获得活动子窗口的句柄。如果子窗口以WM_QUERYENDSESSION消息来响应,那么MDIDEMO将WM_MDIDESTROY消息发送给客户窗口,从而关闭子窗口。
处理「File」菜单中的「Exit」选项只需要框架窗口消息处理程序给自己发送一个WM_CLOSE消息。
处理Window子菜单的「Tile」、「Cascade」和「Arrange」选项是极容易的,只需把消息WM_MDITILE、WM_MDICASCADE和WM_MDIICONARRANGE发送给客户窗口。
处理「Close All」选项要稍微复杂一些。FrameWndProc呼叫EnumChildWindows,传送一个引用CloseEnumProc函数的指标。此函数把WM_MDIRESTORE消息发送给每个子窗口,紧跟着发出WM_QUERYENDSESSION和WM_MDIDESTROY。对图标平铺窗口来说并不就此结束,用GW_OWNER参数呼叫GetWindow时,传回的非NULL值可以显示出这一点。
FrameWndProc没有处理任何由「Color」菜单中对颜色的选择所导致的WM_COMMAND消息,这些消息应该由文件窗口负责处理。因此,FrameWndProc把所有未经处理的WM_COMMAND消息发送到活动子窗口,以便子窗口可以处理那些与它们有关的消息。
框架窗口消息处理程序不予处理的所有消息都要送到DefFrameProc,它在框架窗口消息处理程序中取代了DefWindowProc。即使框架窗口消息处理程序拦截了WM_MENUCHAR、WM_SETFOCUS或WM_SIZE消息,这些消息也要被送到DefFrameProc中。
所有未经处理的WM_COMMAND消息也必须送给DefFrameProc。具体地说,FrameWndProc并不处理任何WM_COMMAND消息,即使这些消息是使用者在Windows子菜单的文件列表中选择文件时产生的(这些选项的wParam值是以IDM_FIRSTCHILD开始的)。这些消息要传送到DefFrameProc,并在那里进行处理。
注意框架窗口并不需要维护它所建立的所有文件窗口的窗口句柄清单。如果需要这些窗口句柄(如处理菜单上的「Close All」选项时),可以使用EnumChildWindows得到它们。
子文件窗口
现在看一下HelloWndProc,它是用于显示「Hello, World!」的子文件窗口的窗口消息处理程序。
与用于多个窗口的窗口类别一样,所有在窗口消息处理程序(或从该窗口消息处理程序中呼叫的任何函数)中定义的静态变量由依据该窗口类别建立的所有窗口共享。
只有对于每个唯一于窗口的数据才必须采用非静态变量的方法来储存。这样的技术要用到窗口属性。另一种方法(我使用的方法)是使用预留的内存空间;可以在注册窗口类别时将WNDCLASS结构的cbWndExtra字段设定为非零值以便预留这部分内存空间。
MDIDEMO程序使用这个内存空间来储存一个指标,这个指标指向一块与HELLODATA结构大小相同的内存块。在处理WM_CREATE消息时,HelloWndProc配置这块内存,初始化它的两个字段(它们用于指定目前选中的菜单项和文字颜色),并用SetWindowLong将内存指针储存到预留的空间中。
当处理改变文字颜色的WM_COMMAND消息(回忆一下,这些消息来自框架窗口消息处理程序)时,HelloWndProc使用GetWindowLong获得包含HELLODATA结构的内存块的指针。利用这个结构,HelloWndProc清除原来对菜单项的选择,设定所选菜单项为选中状态,并储存新的颜色。
当窗口变成活动窗口或不活动的时候,文件窗口消息处理程序都会收到WM_MDIACTIVATE消息(lParam的值是否为这个窗口的句柄表示了该窗口是活动的还是不活动的)。您也许还能记起MDIDEMO程序中有三个不同的菜单:当无文件时为MdiMenuInit;当「Hello」文件窗口是活动窗口时为MdiMenuHello;当「Rect」文件窗口为活动窗口时为MdiMenuRect。
WM_MDIACTIVATE消息为文件窗口提供了一个修改菜单的机会。如果lParam中含有本窗口的句柄(意味着本窗口将变成活动的),那么HelloWndProc就将菜单改为MdiMenuHello。如果lParam中包含另一个窗口的句柄,那么HelloWndProc将菜单改为MdiMenuInit。
HelloWndProc经由把WM_MDISETMENU消息发送给客户窗口来修改菜单,客户窗口透过从目前菜单上删除文件列表并把它添加到一个新的菜单上来处理这个消息。这就是文件列表从MdiMenuInit菜单(它在建立第一个文件时有效)传送到MdiMenuHello菜单中的方法。在MDI应用程序中不要用SetMenu函数改变菜单。
另一项工作涉及到「Color」子菜单上的选中旗标。像这样的程序选项对每个文件来说都是不同的,例如,可以在一个窗口中设定黑色文字,在另一个窗口中设定红色文字。菜单选中旗标应能反映出活动窗口中选择的选项。由于这种原因,HelloWndProc在窗口变成非活动窗口时清除选中菜单项的选中旗标,而当窗口变成活动窗口时设定适当菜单项的选中旗标。
WM_MDIACTIVATE的wParam和lParam值分别是失去活动和被启动窗口的句柄。窗口消息处理程序得到的第一个WM_MDIACTIVATE消息的lParam参数被设定为目前窗口的句柄。而当窗口被消除时,窗口消息处理程序得到的最后一个消息的lParam参数被设定为另一个值。当使用者从一个文件切换到另一个文件时,前一个文件窗口收到一个WM_MDIACTIVATE消息,其lParam参数为第一个窗口的句柄(此时,窗口消息处理程序将菜单设定为MdiMenuInit);后一个文件窗口收到一个WM_MDIACTIVATE消息,其lParam参数是第二个窗口的句柄(此时,窗口消息处理程序将菜单设定为MdiMenuHello或MdiMenuRect中适当的那个)。如果所有的窗口都关闭了,剩下的菜单就是MdiMenuInit。
当使用者从菜单中选择「Close」或「Close All」时,FrameWndProc给子窗口发送一个WM_QUERYENDSESSION消息。HelloWndProc将显示一个消息框并询问使用者是否要关闭窗口,以此来处理WM_QUERYENDSESSION和WM_CLOSE消息(在真实的应用程序中,消息框会询问是否需要储存文件)。如果使用者表示不能关闭窗口,那么窗口消息处理程序传回0。
在WM_DESTROY消息处理期间,HelloWndProc释放在WM_CREATE期间配置的内存块。
所有未经处理的消息必须传送到用于内定处理的DefMDIChildProc(不是DefWindowProc)。不论子窗口消息处理程序是否使用了这些消息,有几个消息必须被传送给DefMDIChildProc。这些消息是:WM_CHILDACTIVATE、WM_GETMINMAXINFO、WM_MENUCHAR、WM_MOVE、WM_SETFOCUS、WM_SIZE和WM_SYSCOMMAND。
RectWndProc与HelloWndProc非常相似,但是它比HelloWndProc要简单一些(不含菜单选项并且无需使用者确认是否关闭窗口),所以这里不对它进行讨论了。但应该注意到,在处理WM_SIZE之后RectWndProc使用了「break」叙述,所以WM_SIZE消息被传给DefMDIChildProc。
结束处理
在WinMain中,MDIDEMO使用LoadMenu加载资源描述档中定义的三个菜单。一般说来,当菜单所在的窗口被清除时,Windows也要清除与之关联的菜单。对于Init菜单,应该清除那些没有联系到窗口的菜单。由于这个原因,MDIDEMO在WinMain的末尾呼叫了两次DestroyMenu来清除「Hello」和「Rect」菜单。