19.2 MDI 实现范例

摘录于《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!”的文档窗口有一个菜单,可以让你改变文本的颜色。

19.2.1  三个菜单

        我们首先来看 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 都大。

19.2.2  程序初始化

        在 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 结构的指针。这个结构有两个字段,定义如下。

  • 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。

        注意,hwndClient 和 hwndFrame 这两个窗口句柄被分别传递给 TranslateMDISysAccel 和 TranslateAccelerator。WinMain 函数通过用 GW_CHILD 参数调用 GetWindow 来得到 hwndClient 窗口句柄。

19.2.3  创建子窗口

        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 支持的最棒的功能之一

19.2.4  更多框架窗口消息的处理

        让我们在关注子文档窗口之前,继续讨论 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 得到。

19.2.5  子文档窗口

        现在让我们来看 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。

19.2.6  清理

        在 WinMain 中,MDIDEMO 用 LoadMenu 来加载资源脚本中定义的三个菜单。通常,Windows 会在菜单所依属的窗口被销毁的时候销毁菜单。Init 菜单就是这样处理的。但是,不依属某一窗口的菜单需要被显示地销毁。为此,MDIDEMO 在 WinMain 的最后两次调用 DestroyMenu 来删除 Hello 和 Rect 菜单。

你可能感兴趣的:(《Windows,程序设计》学习之旅,windows,编程,多文档界面)