摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P907
图 19-2 所示的 MDIDEMO 程序演示了编写 MDI 应用程序的基本要求。
/*----------------------------------------------------------
MDIDEMO.C -- Multiple-Document Interface Demonstration
(c) Charles Petzold, 1998
----------------------------------------------------------*/
#include
#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:
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:
// Changes 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 (excerpts)
// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.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 (excerpts)
// Microsoft Visual C++ 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,Wolrd!”,另一种显示一系列随机矩形。(在源代码清单和标识名称中,它们被称为 Hello 文档和 Rect 文档。)这两种文档窗口有不同的菜单。显示“Hello,World!”的文档窗口有一个菜单,可以让你改变文本的颜色。
我们首先来看 MDIDEMO.RC 资源脚本。这个资源脚本定义了程序所使用的三个菜单模板。
当没有文档窗口时,程序显示 MdiMenuInit 菜单。这个菜单仅仅让你建立一个新文档,或是退出程序。
MdiMenuHello 菜单是跟显示“Hello,World!”的文档窗口相联系的。File 子菜单可以打开任何一种格式的新文档、关闭活动文档和退出程序。Color 子菜单可以让你设置文本颜色。Window 子菜单提供选项让你可以把文档窗口按层叠或平铺的方式来排列、排列文档图标和关闭所有窗口。这个子菜单还会按照文档窗口的建立顺序把它们列出来。
MdiMenuRect 菜单是和随机矩形文档联系在一起的。它跟 MdiMenuHello 菜单一样,只是不包括 Color 子菜单。
跟往常一样,RESOURCE.H 头文件定义了所有的菜单标识符。此外,在 MDIDEMO.C 中还定义了以下三个常数:
#define INIT_MENU_POS 0
#define HELLO_MENU_POS 2
#define RECT_MENU_POS 1
这些标识符指定 Windows 子菜单在三个菜单模板中的位置。程序需要这些信息来告诉客户窗口在哪儿放置文档列表。当然,MdiMenuInit 菜单没有 Window 子菜单,所以我告诉它列表应该放在第一个子菜单的后面(位置 0)。然而,列表实际上是永远不会在那出现的。(等我在随后讨论程序时,你会明白我们为什么需要它。)
在 MDIDEMO.C 中定义的 IDM_FIRSTCHILD 标识符并不对应一个菜单项。这个标识符给将要出现在 Window 子菜单中的列表内的第一个文档窗口的。这个标识符应该比其他所有菜单 DI 都大。
在 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 函数三次就得到了 Window 子菜单的句柄,文档列表会被添加在这些子菜单之后。这些句柄也被保存在全局变量中。LoadAccelerators 函数加载键盘加速键表。
WinMain 调用 CreateWindow 创建框架窗口。在 FrameWndProc 的 WM_CREATE 处理过程中,框架窗口创建客户窗口。这会再调用另一个 CreateWindow。窗口类被设为 MDICLIENT,也就是为 MDI 客户窗口预先注册的类。Windows 中很多对 MDI 的支持都被封装在 MDICLIENT 窗口类中。客户窗口过程是介于框架窗口和各种文档窗口之间的中间层。当调用 CreateWindow 创建客户窗口时,最后一个参数必须设为指向 CLIENTCREATESTRUCT 结构的指针。这个结构有两个字段,定义如下。
回到 WinMain 中,MDIDEMO 显示新创建的框架窗口并进入消息循环。这个消息循环跟普通循环有一点不同:在调用 GetMessage 从消息队列中得到一条消息后,MDI 程序会把消息传给 TranslateMDISysAccel(以及 TranslateAccelerator,如果程序像 MDIDEMO 一样有菜单加速键的话)。
TranslateMDISysAccel 函数把对应到特殊的 MDI 加速键(如 Ctrl+F6)的任何按键都翻译成 WM_SYSCOMMAND 消息。如果 TranslateMDISysAccel 或 TranslateAccelerator 返回 TRUE(表示一个消息被这些函数中的一个翻译过了),就不要再调用 TranslateMessage 和 DispatchMessage。
注意,hwndClient 和 hwndFrame 这两个窗口句柄被分别传递给 TranslateMDISysAccel 和 TranslateAccelerator。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 菜单的 Window 子菜单下的。
在该菜单中最多可以列出九个文档,每个前面都有从 1 到 9 的带下划线的数字,如果有多于九个文档窗口被创建,列表后面会跟着一个 More Windows 菜单项。该菜单项会调出一个对话框,显示一个列出了所有文档窗口的列表框。这个文档列表的维护是 WIndows 支持的最棒的功能之一。
让我们在关注子文档窗口之前,继续讨论 FrameWndProc 的消息处理。
当你从 File 菜单选择 Close 选项时,MDIDEMO 会关闭活动的子窗口。它通过向客户窗口发送 WM_MDIGETEACTIVE 消息来获得活动子窗口的句柄。如果子窗口确实对 WM_QUERYENDSESSION 消息做出了响应,MDIDEMO 就给客户窗口发送 WM_MDIDESTROY 消息来关闭子窗口。
对 File 菜单由 Exit 选项的处理,只需要框架窗口给它自己发一个 WM_CLOSE 消息即可。
处理 Window 子菜单的 Tile(平铺)、Cascade(层叠)和 Arrange Icons(排列图标)选项也很方便,只需要给客户窗口发送 WM_MDITILE、WM_MDICASCADE 和 WM_MDIICONARRANGE 消息即可。
Close All(关闭全部)选项有点复杂。FrameWndProc 调用 EnumChildWindows,传给它一个指向 CloseEnumProc 函数的指针。这个函数给每个子窗口发送 WM_MDIRESTORE 消息,然后再发送 WM_QUERYENDSESSION,然后可能还会发送 WM_MDIDESTROY 消息。对于图标标题窗口则不用这么做,这种窗口在用 GW_OWNER 参数调用 GetWindows 时会返回一个非 NULL 值。
你会注意到 FrameWndProc 不处理任何代表某种颜色被 Color 菜单选中了的 WM_COMMAND 消息,这些消息是文档窗口应该负责的。因为这个原因,FrameWndProc 把所有没处理的 WM_COMMAND 消息都转发给活动的子窗口,以便子窗口能够处理跟它们的窗口相关的那些消息。
所有框架窗口选择不予处理的消息都必须传给 DefFrameProc。这些函数替换了框架窗口的 DefWindowProc 函数。就算框架窗口拦截了 WM_MENUCHAR、WM_SETFOCUS 或是 WM_SIZE 消息,它们也必须传给 DefFrameProc。
未处理的 WM_COMMAND 消息也必须传给 DefFrameProc。特别是 FrameWndProc 不会处理任何由于从 Window 子菜单的列表中选择其中的一个文档而产生的 WM_COMMAND 消息。(这些选项的 wParam 的值以 IDM_FIRSTCHILD 开头。)这些消息会被传给 DefFrameProc,并在那里进行处理。
注意,框架窗口不需要维护它所创建的文档窗口的窗口句柄列表。如果需要这些句柄(如处理菜单中的 Close All 选项时),它们能用 EnumChildWindows 得到。
现在让我们来看 HelloWndProc,它是用来显示“Hello,World!”的子文档的窗口过程。
跟其他任何用于多个窗口的窗口类一样,在窗口过程(或窗口过程调用的任何函数)中定义的静态变量都会被所有基于该窗口类而创建的窗口共享。
每个窗口自己都有的数据必须用非静态变量的方法来存放。一种技巧是用窗口属性。另一种方法是利用预留的内存,而这也正是我所采用的方法。该内存是在用来注册窗口类的 WNDCLASS 结构的 cbWndExtra 字段中定义非 0 值而预留的。
在 MDIDEMO 中,我用这个空间来存储指向一个内存块的指针,该内存的大小是 HELLODATA 结构的大小。HelloWndProc 在 WM_CREATE 消息中分配这块内存,初始化其中的两个字段(它们表示当前被选定的菜单项和文本颜色),并用 SetWindowLong 来存储这个指针。
当处理改变文本颜色的 WM_COMMAND 消息时(请记得这些消息是从框架窗口过程产生的),HelloWndProc 用 GetWindowLong 来得到指向含有 HELLODATA 结构的内存块的指针。用这个结构,HelloWndProc 会取消选中已选定的菜单项,再选中所选择的菜单项,并保存新颜色。
只要窗口变成活动或非活动状态(看 lParam 是不是包含窗口的句柄),文档窗口过程就会收到 WM_MDIACTIVEATE 消息。你应该记得 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 的值分别是将变成非活动窗口和活动窗口的窗口句柄。窗口过程首先收到其 lParam 被设为该窗口的句柄的 WM_MDIACTIVATE 消息。当窗口被销毁时,在窗口过程最后收到的消息中,lParam 被设为另一个值。当用户从一个文档切换到另一个文档时,第一个文档收到一条 WM_MDIACTIVATE 消息,其 wParam 被设为第一个窗口的句柄,这时候窗口过程把菜单设成 MdiMenuInit。第二个文档窗口收到一条 WM_MDIACTIVATE 消息,其 lParam 被设成第二个窗口的句柄。这时候窗口过程根据情况把菜单设成 MdiMenuHello 或 MdiMenuRect。如果所有的窗口都被关闭了,菜单就设成 MdiMenuInit。
你会记得当用户从 File 菜单中选择 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 所涉及的开销很类似,但它稍简单一些(也就是没有牵涉到菜单选项,窗口也不需要用户确认是否能被关闭),所以我不必再讨论它。但是请注意,RectWndProc 处理完 WM_SIZE 后是跳出的,所以该消息会被传给 DefMDIChildProc。
在 WinMain 中,MDIDEMO 用 LoadMenu 来加载资源脚本中定义的三个菜单。通常,Windows 会在菜单所依属的窗口被销毁的时候销毁菜单。Init 菜单就是这样处理的。但是,不依属某一窗口的菜单需要被显示地销毁。为此,MDIDEMO 在 WinMain 的最后两次调用 DestroyMenu 来删除 Hello 和 Rect 菜单。