多文档界面(MDI)是Microsoft Windows文档处理应用程序的一种规范,该规范描术了窗口结构和允许用户在单个应用程序中使用多个文档的用户界面(如字处理程序中的本文文档和电子表格程序中的电子表格)。简单地说,就像Windows在一个屏幕上维护多个应用程序窗口一样,MDI应用程序在一个客户区内维护多个文档窗口。Windows中的第一个MDI应用程序是Windows下的Microsoft Excel 的第一个版本,紧接着又出现了许多其的应用程序。
尽管MDI规范随着Windows2.0的推出已经很普及中,但在那时,MDI应用程序写起来很困难,并且需要一些非常复杂的程序设计工作。从Windows3.0起,其中许多工作就都已经为您做好了。Windows95中增强的支持也已经被添加进Windows98和Microsoft Windows NT中。
MDI的组成
MDI程序的主应用程序窗口是很相似的:有一个标题栏、一个菜单、一个缩放边框、一个系统菜单图标,以及最大化/最小化/关闭按钮。客户区经常被称为“工作空间”,它不直接用于显示程序输出。这个工作空间包括零个或多个子窗口,每个窗口都显示一个文档。
这些子窗口看起来与通常的应用程序窗口及MDI程序的主窗口很相似。它们有一个标题栏、一个缩放边框、一个系统菜单图标、以及及最大化/最小化/关闭按钮,可能还包括滚动条。但是文档窗口没有菜单,主应用程序窗口上的菜单适用于文档窗口。
在任何时候都只能有一个文档窗口是活动的(加亮标题栏来表示),它出现在其他所有文档窗口之前。所有文档窗口之前。所有文档窗口都由工作空间区域加以剪裁,面不会出现在应用程序窗口这处。
对Windows程序员来说,初看起来MDI似乎相当简单。需要程序员做序员做的工作好像就是为每个文档创建一个WS_CHILD窗口,并使程序的主应用程序窗口成为文档窗口的父窗口。但对现有的MDI应用程序稍加研究,就会发现一些导致编码困难的复杂问题,例如:
1、MDI文档窗口可以被最小化。它的图标出现在工作空间的底部的短标题栏中。一般来说,MDI应用程序可以将不同的图标分别用于主应用程序窗口和每一类文档应用。
2、MDI文档窗口可以被最大化。在这种情况下,文档窗口的标题栏(一般用来显示窗口中文档的文件名)消失,文件名出现在应用程序窗口标题栏的应用程序名之后,文档窗口的系统菜单图标成为应用程序窗口的顶层菜单中的第一项。关闭文档窗中按钮变成顶层菜单中的最后一项,且出现在最右边。
3、用以关闭文档窗口的系统键盘加速键与关闭主窗口的系统键盘加速键一样,只是Ctrl键代替了Alt键。这也就是说,Alt+F4用于关闭应用程序窗口,而Ctrl+F4用于关闭文档窗口。此外,Ctrl+F6可以在活动MDI应用程序的子文档窗口之间切换。与平时一样,Alt+空格键激活主窗口的系统菜单,Alt+-(减号)激活活动子文档窗口的系统菜单。
4、当使用光标键在菜单顶间移动时,控制通常从系统菜单转到菜单栏中的第一项。在MDI应用程序中,近代制是从应用程序系统菜单黑心到活动文档系统菜单,然后再转到菜单中的第一项。
5、如果应用程序能够支持若干种类型的子窗口(如 Microsoft Excel 中的工作表和图表文档),好么菜单应能反映出与这种类型的文档有关的操作。这就要求当不同的文本窗口变成活动窗口时,程序能更换菜单。此外,当没有文档窗口存在时,菜单应该被缩减到只剩下与打开新文档有关的操作。
6、顶层菜单上有一个被叫做“窗口(Window)”的菜单项。按照习惯,这是顶层菜单上“帮助(Help)”这前的那一项,即倒数第二项。“窗口”子菜单上通常包含一些在工作空间内安排文档窗口的选项。文档窗口可以从左上方开始“层叠”,也可以并排“平铺”以便使每一个文档窗口都是完全可见的。这个子菜单同时了包含一个所有文档窗口的列表。从中先择一个文档窗口,就可以把此文档窗口移到前台,使之成为活动窗口。
Windows98支持MDI的所有这些方面。当然,需要您做一些工作(如下面的示例程序所示),但是,这绝不是要您编写大量代码来直技支持所有这些功能。
MDI支持
探讨Windows的MDI支持时需要引人一些新术语。主应用程序窗口称为“框架窗口”,就像传统的Windows程序一样,它是WS_OVERLAPPEDWINDOW风格的窗口。
MDI应用程序还根据预定义的窗口类MDICLIENT创建“客户窗口”,这一客户窗口是用这种窗口类和WS_CHILD风格调用CreateWindow来创建的。这一调用的最后一个参数是指向一个CLIENTCREATESTRUCT类型的结构的指针。这个客户窗口覆盖框架窗口的客户区,并提供许多MDI支持。此客户窗口的颜色是系统颜色COLOR_APWORKSPACE。
文档窗口被称为“子窗口”。通过初始化一个MDICREATESTRUCT类型的结构,以一个指向此结构的指针为参数将消息WM_MDICREATE发送给客户窗口,就可以创建这些文档窗口。
文档窗口是客户窗口的子窗口,而客户窗口又是框架窗口的子窗口。父子窗口层次结构如图所示。
您需要框架窗口的窗口类(及窗口过程)和一个由应用程序支持的每类子窗口的窗口类(及窗口过程)。由于已经先注册了窗口类,所以不需要客户窗口的窗口过程。
Windows98的MDI支持包括1个窗口类、5个函数、两个数据结构和12个消息。前面已经提到了MDI窗口类,即MDICLIENT,以及数据结构CLIENTCREATESTRUCT和MDICREATESTRUCT。在MDI应用程序中,这5个函数中的两个用于取代有DefWindow-Proc:不再将DefWindowProc调用用于所有未处理的消息,而是由框架窗口过程调用Def-FrameProc,子窗口过程调用DefMDIChildProc。另一个MDI特有的函数TranslateMDl-SysAceel与第十章中讨论的TranslateAccelerator的使用方式相同。MDl支持也包括Ar-rangeIconicWindows函数,但有一个专用的MDI消息使得此函数对MDI程序来说不再必要。
第5个MDI函数是CreateMDIWindow,它使得子窗口可以在单独的执行线程中被创建。在单线程的程序中不需要这个函数,我会演示这一点。
在下面的程序中,我将演示12个MDI消息中的9个(其他3个消息一般不用),这些消息的前缀是WM_MDI。框架窗口向客户窗口发送其中某个消息,以便在子窗口上完成一项操作或者获取关于子窗口的信息(例如,框架窗口发送一个WM_MDICREATE消息给客户窗口,以创建子窗口)。消息WM_MDIACHVATE是一特例:框架窗口可以将该消息发送给客户窗口以激活一个子窗口,而客户窗口也把该消息发送给将被激活或被取消激活的子窗口,以便通知它们这一变化。
示例程序
MDI的示例程序
程序19-1所示的MDIDEMO程序说明了编写MDI应用程序的基本方法。
MDIDEMO.C
/*--------------------------------------------------
MDIDEMO.C-Multipis-Document lnterface Demonstration
(C)CharlesPetzold,1998
--------------------------------------------------*/
#include<windows.h>
#include "resource.h"
#define INIT_MENU_POS
#define HELLO_MENU_POS
#define RECT_MENU_POS
#define IDM_FIRSTCHILD 50000
LRESULT CALLBACK FrameWndProc (HWND,UINT,WPARAM,LPARM);
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");
HINSTANCH hlnst;
HMENU hMenulnit,hMenuHello,hMenuRect;
HMENU hMenulnitWindow,hMenuHelloWindow,hMenuRectWindow;
int WINAPI WinNain(HINSTANCE hinstance,HLNSTANCE hPrEVLNSTANCE,PSTR szCmdLine,int iCmdShow)
{
HACCEL hAccel;
HWND hwndFrame,hwndClient;
MSG msg;
WNDCLASS wndclass;
hlnst=hlnstance;
//Register the frame window class
wndclass.styte=CS_HREDRAW|CS_VREDRAW;
wndclass.lpfnWndProc =FrarneWndProc;
wndclass.cbCIsExtra =0;
wndclass.cbWndExtra =0;
wndclass.hlnstance =hlnstance;
wndclass.hlcon =Loadlcon(NULL,IDI_APPLICATlON);
wndclass.hCursor =LoadCursor(NULL,IDC_ARROW);
wndclass.hblBackground=(HBRUSH)(COLOR_APPWORKSPACE+1);
wndclass.lpszMenuName=NULL;
wndclass.lpszClassName=szFrameClass;
if(!RegisteClass(&wndclass))
{
MessageBox(NULL,TEXT("This program requires Windows NT!")
szAppName,MB_ICONERROR);
returm 0;
}
//Register the Hello child window class
wndclass.style=CS_HREDRAW|CS_VREDRAW;
wndclass.lpfnWndProc =HelloWndProc;
wndclass.cbClsExtra =0;
wndclass.cbWndExtra =sizeof(HANDLE);
wndclass.hlnstance =hlnstance;
wndclass.hlcon =Loadlcon(NULL,IDI_APPLICATION);
wndclass.hCursor =LoadCursor(NULL,IDC_ARROW);
wndclass.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
wnddass.lpszMenuName=NULL;
wndclass.lpszClassName=szHelloClass;
RegisterClass(&wndclass);
//Register the Rect child window class
wndclass.style=CS_HREORAW | CS_VREDRAW;
wndclass.lpfnWndProc =RectWndProc;
wndclass.cbClsExtra =0;
wndclass.cbWndExtra =sizeof(HANDLE);
wndclass.hlnstance =hlnstance;
wndclass.hlcon =Loadlcon(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
hMenulnit=LoadMenu(hlnstance,TEXT("MdiMenulnit"));
hMenuHello=LoadMenu(hlnstance,TEXT("MdiMenuHello"));
hMenuRect=LoadMenu(hlnstance,TEXT("MdiMenuRect"));
hMenulnitWindow=GetSubMenu(hMenulnit,INIT_MENU_POS);
hMenuHelloWindow=GetSubMenu(hMenuHello,HELL0_MENU_POS);
hMenuRectWindow:GetSubMenu(hMenuRect,RECT_MENU_POS);
//Load accelerator table
hAccel=LoadAccelerators(hlnstance,szAppName);
//Create the frame window
hwndFrame=CreateWindow(szFrameClass,TEXT("MDI Demonstration"),
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
CW_USEDEFAULT,CW_USEOEFAULT,
CW_USEDEFAULT,CW_USEDEFAULT,
NULL,hMenulnit,hlnstance,NULL);
hwndClient=GetWindow(hwndFrame,GW_CHILD);
ShowWindow(hwndFrame,lCmdShow);
UpdateWindow(hwndFrarne);
//Enter the modified message loop
while(GetMessage(&msg,NULL,0,O))
{
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;
MDICREATESTRLICT mdicreate;
switch(message)
{
case WM_CREATE: //Create the client window
clientcreate.hWindowMenu=hMenulnitWindow;
clientcreate.idFirstChild= IDM_FIRSTCHILD:
hwndClient=CreateWindow(TEXT("MDICLIENT"),NULL,
WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE,
0,0,0,0,hwnd,(HMENU)l,hlnst,
(PSTR)&clientcreate);
return 0;
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDM_FILE_NEWHELLO: //Create a Hello child window
mdicreate.szClass=szHelloClass;
rndicreate.szTitle=TEXT("Hello");
mdicreate.hOwner=hlnst;
mdicreate.X =CW_USEDEFAULT;
mdicreate.Y =CW_USEDEFAULT;
mdicreate.CX =CW_USEDEFAULT;
mdicreate.cy =CW_USEDEFAULT;
mdicreate.style =0;
mdicreate.lParan=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=hlnst;
mdicreale.X =CW_USEDEFAULT;
mdicreale.Y =CW_USEDEFAULT;
mdicreale.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 lDM_WINDOW_TILE:
SendMessage(hwndClient,WM_MDITILE,0,0);
return 0;
case IDM_WINDOW_CASCADE:
SendMessage(hwndClient,WM_MDICASCADE,0,0);
return 0;
case IDM_WlNDOW_ARRANGE:
SendMessage(hwndClient,WM_MDIICONARRANGE,0,0);
return 0;
case IDM_WlNDOW_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 lhen to DefFraneProc
}
break;
case WM_QUERYENDSESSlON:
case WM_CLOSE: //Attempt to close alI 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 CALL BACK 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 clrTextArmy[]= {RGB(0,0,0),RGB(255,0,0),
RGB(0,2.55,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,O,(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);
hManu=GetMenu(hwndFrame);
CheckMenultem(hMenu,pHelloData->iColor,MF_UNCHECKED);
pHelloData->iColor=wParam;
CheckMenultem(hMenu,pHelloData->iColor,MF_CHECKED);
pHelloData->clrText=ctrTextArray[wParam_IDM_COLOR_BLACK];
InvalidateRect(hwnd,NULL,FALSE);
}
return 0;
case WM_PAlNT:
//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(IParam==(LPARAM)hwnd)
SendMessage(hwndClient,WM_MDISETMENU,(WPARAM)hMenuHello,(LPARAM)hMenuHelloWindow);
//Check or uncheck menu item
pHelloData=(PHELLODATA)GetWindowLong(hwnd,0);
CheckMenultem(hMenuHello,pHelloData->iColor,
(lParam==(LPARAM)hwnd)?MF_CHECKED:MF_UNCHECKED);
//Set the lnit menu if losing focus
if(lParam!=(LPARAM)hwnd)
SendMessage(hwndClient,WM_MDISETMENU,(WPARAM)hMenulnit,(LPARAM)hMenulnitWindow);
DrawMenuBar(hwndFrame);
return 0;
if(IDOK != MessageBox(hwnd,TEXT("OK to close window?"),
TEXT("Hello"), MB_ICONQUESTION | MB_OKCANCEL))
return 0;
break; //l.e.,call DefMDIChildProc
case WM_DESTROY:
pHelloData=(PHELLODATA)GetWindowLong(hwnd,0);
HeapFree(GetProcessHeap(),0,pHelloData);
return 0;
}
//Pass unprocessed message to DefMDIChikProc
return DeIMDIChildProc(hwnd,message,wParam,IPamm);
}
LRESULT CAllBACK RectWndProc(HWND hwnd,UINT message,
WPARAM wParam,LPARAM lParam)
{
static HWND hwndClient,hwndFrame;
HBRUSH hBrush;
HDC hdc;
PRECTDATA pRectData;
PAlNTSTRUCT ps;
int xLeft,xRight,yTop,yBottom;
short nRed,nGreen,nBlue;
switch(message)
{
case WM_CREATE:
//Allocate memory for window private data
pRectData=(PRECTDATA)HeapAlloc(GelProcessHeap(),HEAP_ZERO_MEMORY,sizeof(RECIDATA));
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 DelMDIChildProc
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;
caseWM_MDIACTNATE: //Set the appropriate menu
if(lParam==(LPARAM)hwnd)
SendMessage(hwndClient,WM_MDISETMENU,(WPARAM)hMenuRect,(LPARAM)hMenuRectWindow);
else
SendMessage(hwndClient,WM_MDISETMENU,(WPARAM)hMenulnit,(LPARAM)hMenulnitWindow);
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 DelMDIChildProc(hwnd,message,wParam,IParam);
}
MDIDEMO.RC(摘录)
//Microsoft Developer Studio generated resource scnpt.
#include "resource.h"
#include "afxres.h"
//Menu
MDIMENUINIT MENU DlSCARDABLE
BEGIN
POPUP "&File"
BEGIN
MENUITEM "New &Hello", IDM_FILE_NEWHELL0
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_NEWHELL0
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", lDM_FILE_NEWHELLO
MENUITEM "New &Rectangle", lDM_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_WlNDOW_CLOSEALL
END
END
//Accelerator
MOlDEMO ACCELERATORS DlSCAROABLE
BEGIN
VK_F4, IDM_WINDOW_TILE, VIRTKEY,SHIFT,NOINVERT
VK_F5, IDM_WINDOW_CASCADE, VIRTKEY,SHIFT,NOINVERT
END
RESOURCE.H(摘录)
//Microsoft Devetoper Studio generated include file.
//Used by MDIDemo.rc
#define IDM_FILE_NEWHELLO 40001
#define IDM_FILE_NEWRECT 40002
#define IDM_APP_EXIT 40003
#define lDM_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资源描述文件,它定义了程序使用的三个菜单模板。
当文档窗口不存在时,程序显示MdiMenulnit菜单,这个菜单只允许用户创建新文档或退出程序。
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子菜单,所以如前所述,文档列表应附加在第一个子菜单中(位置O)。不过,实际上永远不会在此看到文档列表(在后面讨论 此程序时,您可以发现这样做的原因)。
定义在MDIDEMO.C中的IDM_FIRSTCHllD标识符不对应于菜单项,它与出现在Windows子菜单上的文档 列表中的第一个文档窗口相关联。这个识别符的值应当大于所有其他菜单ID的值。
程序初始化
在MDIDEMO.C中,WinMain是从注册框架窗口和两个子窗口的窗口类开始的。窗口过程是FrameWndProc、 HelloWndProc和RectWndProc。一般来说,这些窗口类应该与不同的图标相关联。为了简单起见,我们将标准IDI_APPLICATION图标用于框架窗口和子窗口。
注意,我们已经将框架窗口类的WNDCLASS结构的hbrBackgwtmd字段定义为COLOR_APPWORKSPACE系统颜 色。由于框架窗口的客户区被客户窗口所覆盖,并且客户窗口具有这种颜色,所以上面的定义不是完全必要的。但是,在最初显示框架窗口时。使用这种颜色似乎要好一些。
这三种窗口类中的lpszMenuName字段都设置为NULL。对Hello和Rect子窗口类来说,这是很自然的。对于框架窗口类,我在创建框架窗口时在CreateWindow函数中给出菜单句柄。
Hello和Rect子窗口的窗口类将WNDCLASS结构中的cbWndExtra字段置为非零值来为每个窗口分配额外空 间,这个空间将用于存储指向一个内存块的指针(HEILODATA和RECTDATA结构的大小定义在MDIDEMO.C的开 始处),这个内存块被用于存储每个文档窗口特有的信息。
下一步,WinMain使用LoadMenu加载三个菜单,并把它们的句柄存储到全局变量中。调用三次 GetSubMenu函数可以获得Windows子菜单(文档列表将加在它上面)的句柄,同样也会把这些函数存储到全局 变量中。LoadAccelemtors函数加载加速键表。
在WinMain中调用CreateWindow创建框架窗口。在.FrameWndProc中处理WM_CREATE消息期间,框架窗口创建客户窗口。这项操作涉及到再一次调用函数CreateWindow。窗口类被设置为MDICLIENT,它是预先注 册的MDI客户区窗口类。在Windows中许多对MDI的支持被放人了MDICLIENT窗口类中。客户区窗口过程作为 框架窗口和不同文档窗口的中间层。当调用CreateWindow创建客户区窗口时,最后一个参数必须被设置为 指向CLIENTCREATESTRUCT类型结构的指针。这个结构有两个字段,描述如下:
1、hWindowMenu是要加入文档列表的子菜单的句柄。在MDIDEMO中,它是hMenuInitWindow,是在WinMain期间获得的。后面将看到如何修改此菜单。
2、idFirstChild是与文档列表中的第一个文档窗口相关联的菜单ID。它就是IDM_FIRSTCHILD。
再让我们回过头来看看WinMain。MDIDEMO显示新创建的框架窗口,并进入消息循环。消息循环与正常 的循环稍有不同:在调用GetMessage从消息队列中获得消息之后,MDI程序把该消息传送给 TranslateMDISysAccel(以及TranslateAccelerator,如果像MDIDEMO程序一样,程序本身也有菜单加速键的话)。
TranslateMDISysAccel函数把可能对应特定MDI加速键(例如Ctrl_F6)的击键转换成WM_SYSCOMMAND消 息。如果TranslateMDISysAccel或TranslateAccelerator都返回TRUE(表明某个消息已被这些函数之一转换 ),就不能调用TranslateMessage和DispatchMessage。
注意传递到TranslateMDISysAccel和TranslateAccelemtor的两个窗口句柄: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参数设置为指向这个结构的指针,然后由客户窗口创 建于窗口。(也可以使用CreateMDlWindow函数。)
MDICREATESTRUCT结构中的szTitle字段一般是对应于文档的文件名。风格字段设置为窗口风格WS_ HSCROLL、WS_VSCROLL或这两者的组合,以便在文档窗口中包括滚动条。风格字段也可以包括WS_ MINIMIZE或WS_MAXIMIZE,以便在最初时以最小化或最大化状态显示文档窗口。
MDICREATESTRUCT结构的lParam字段为框架窗口和子窗口共享某些变量提供了一种方法。这个域可以设置为含有一个结构的内存块的内存句柄。在子文档窗口的WM_CREATE消息期间,lParam是一个指向CREATESTRUCT结构的指针,这个结构的lpCreatePamms字段是一个指向用于创建窗口的MDICREATESTRUCT结 构的指针。
客户窗口一旦接收到WM_MDICREATE消息就创建一个子文档窗口,并把窗口标题加到用于创建客户窗口的MDICLIENTSTRUCT结构中所指定的子菜单的底部。当MDIDEMO程序创建它的第一个文档窗口时,这个子菜 单就是MdiMenuInit菜单中的File子菜单。后面将看到这个文档列表将如何被移到MdiMenuHeilo和 MdiMenuRect菜单的Windows子菜单中。
菜单上可以列出9个文档,每个文档的前面是带有下划线的数字1~9。如果创建的文档窗口多于9个, 则这个清单后跟有More Windows菜单项。该项激活带有列表框的对话框,列表框列出了所有文档。这种文 档列表的维护是Windows MDl支持的最好的特性之一。
关于框架窗口的消息处理
在把注意力转移到子文档窗口之前,我们先继续讨论FmmeWndProc的消息处理。当从File菜单中选择Close时,MDIDEMO关闭活动子窗口。它通过把WM_MDIGETACTIVE消息发送给客户 窗口,而获得活动子窗口的句柄。如果子窗口以WM_QUERYENDSKSSION消息来响应,那么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_SIETFOCUS或WM_SIZE消息,这些消息也要被送到DefFrameProc中。
所有未经处理的WM_COMMAND消息也必须送给DefFrameProc。具体地说,FrameWndProc并不处理任何WM_COMMAND消息,即使这些消息是用户在Windows子菜单的文档列表中选择文档时产生的(这些选项的wParam值是以IDM_FIRSTCHILD开始的)。这些消息要传送到DefFrameProc,并在那里进行处理。
注意框架窗口并不需要维护它所创建的所有文档窗口的窗口句柄清单。如果需要这些句柄(如处理菜单上的Close Au选项时),可以使用EnumChildWindows得到它们。
子文档窗口
现在看一下HelloWndProe,它是用于显示"Hello,World!”的子文档窗口的窗口过程。与用于多个窗口的窗口类一样,所有在窗口过程(或从该窗口过程中调用的任何函数)中定义的静态变 量由基于该窗口类的所有窗口共享。
只有对于每个唯一于窗口的数据才必须采用非静态变量的方法来存储。这样的技术要用到窗口属性。另一种方法(我使用的方法)是使用预留的内存空间。可以在注册窗口类时将WNDCLASS结构的cbWndExtra字 段设置为非零值,以便预留这部分内存空间。
MDIDEMO程序使用这个内存空间来保存一个指针,这个指针指向一块与HELLODATA结构大小相同的内存 块。在处理WM_CRF_ATE消息时,HelloWndPmc分配这块内存,初始化它的两个字段(它们用于指定当前选中 的菜单项和文本颜色),并用SetWindowLong将内存指针保存到预留的空间中。
当处理改变文本颜色的WM_COMMAND消息(回忆一下,这些消息来自框架窗口过程)时,HelloWndProc使 用GetWindowLong获得包含HELLODATA结构的内存块的指针。利用这个结构,HeLloWndProe清除原来对菜单 项的选择,设置所选菜单项为选中状态,并保存新的颜色。
当窗口变成活动窗口或非活动窗口时,文档窗口过程都会收到WM_MDIACTIV。ATE消息(lParam的值是 否是这个窗口的句柄表明了该窗口是活动的还是非活动的)。您也许还能记起MDIDEMO程序中有三个不同的 菜单:当无文档时为MdiMenuInit;当Hello文档窗口是活动窗口时为MdiMenuHeUo;当Rect文档窗口为活动 窗口时为MdiMen_uRect。
WM_MDIACTIVATE消息为文档窗口提供了一个修改菜单的机会。如果IParam中含有本窗口的句柄(意味 着本窗口将变成活动的),那么HelloWndProc就将菜单改为MdiMenutteUoo如果IPamm中包含另一个窗口的句 柄,那么HelloWndProc将菜单改为MdiMenuInit。
HelloWndProc通过把WM_MDISETMENU消息发送给客户窗口来修改菜单,客户窗口通过从当前菜单上删 除文档列表并把它添加到一个新的菜单上来处理该消息。这就是文档列表从MdiMenulnit菜单(它在创建第 一个文档时有效)传送到MdiMenuHello菜单中的方法。在MDI应用程序中不要用SetMenu函数更改菜单。
另一项工作涉及到Color子菜单上的复选标记。像这样的程序选项对每个文档来说都是不同的,例如, 可以在一个窗口中设置黑色文本,在另一个窗口中设置红色文本。菜单复选标记应能反映出活动窗口中选 定的选项。由于这种原因,HelloWndProc在窗口变成非活动窗口时清除选中菜单项的复选标记,而当窗口 变成活动窗口时设置适当菜单项的复选标记。
WM_MDIACTIVATE的wParam和IParam值分别是取消激活窗口和激活窗口的句柄。窗口过程得到的第一个 WM_MDIACTIVATE消息的IParam参数被设置为当前窗口的句柄。而当窗口被消除时,窗口过程得到的最后一个消息的IParam参数被设置为另一个值。当用户从一个文档切换到另一个文档时,前一个文档窗口收到一个WM_MDIACTIVATE消息,其lParam参数为第一个窗口的句柄(此时,窗口过程将菜单设置为MdiMenuInit),后一个文档窗口收到一个WM_MDIACTIVATE消息,其lParam参数是第二个窗口的句柄(此时 ,窗口过程将菜单设置为MdiMenuHello或MdiMenuRect中适当的那个)。如果所有的窗口都关闭了,剩下的菜单就是MdiMenuInit。
当用户从菜单中选择Close或Close All时,FrameWndProc给子窗口发送一个WM_QIJERYENDSESSION消息。 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菜单。