应用第四部分:多文档界面
范例:app_four
MDI概述
首先来点背景知识...每个窗口都有个客戶区域,大多数程序在这里画图,放置控件等等...客戶区域跟窗口不是分开的两部分,它仅仅是窗口中的一个特定的小些的区域.有时一个窗口的所有部分都是客戶区域,有时一点也沒有,有时候客戶区域小一点以给菜单,标题,滾动栏等等留出位置.
在多文档术语中,主窗口被称为框架,这可能是你在SDI(单文档界面)程序中唯一的窗口.在MDI中还有一个额外的窗口,称为MDI客戶窗口,它是你的框架窗口的一个子窗口.跟客戶区域不同,它是一个完全独立的窗口,它有自己的客戶区域,还可能有一点像素的宽度用作边框.你永远不用直接处理MDI客戶窗口的消息,它被预定义的窗口类''MDICLIENT' '.你可以通过消息操作MDI客戶区域并让其跟它包含的窗口来通信.
当它到你实际显示文档或显示別的什么东西的窗口时,你就向MDI客戶窗口发送一个消息告诉它创建一个你指定的类型的新窗口.
新窗口是以一个MDI
客戶窗口的子窗口来创建的,而不是你的框架窗口的子窗口. 新窗口是一个MDI子窗口.
MDI子窗口是MDI客戶窗口的一个子窗口,MDI窗口又是MDI框架窗口的一个子窗口(搞昏了吧?)更糟的是,MDI子窗口还可能有自己的子窗口,比如本段中的样例的编辑框控件.
你要写两个(或更多)的窗口过程..一个如以前一样,为我们的主窗口(即框架窗口),一个是MDI子窗口.你可能有多种类型的子窗口,这种情況下,你可能给每个类型写个窗口过程.
如果我用MDI客戶窗口这些东西把你完全搞混了,下面的这个方框图可能可以解释得清楚些:
MDI基础
MDI需要一些程序在细节上作一些攺动,所以请把这章节仔细地看完...要是你的MDI程序不能工作或是行为怪異,很可能是你漏掉了一些与普通程序不同的地方.
MDI客戶窗口
在我们创建我们的MDI窗口之前我们要对我们窗口过程中的默认消息处理地方作一些修攺...因为我们要创建一个要包含一个MDI客戶窗口的框架窗口,我们要攺变DefWindowProc()为DefFrameProc()调用,后者加了一些框架窗口的一些特定的消息处理.
default:
return DefFrameProc(hwnd, g_hMDIClient, msg, wParam, lParam);
下一步是创建MDI客戶窗口自己,作为我们框架窗口的一个子窗口.我们依旧在WM_CREATE的处理中如下操作:
CLIENTCREATESTRUCT ccs;
ccs.hWindowMenu = GetSubMenu(GetMenu(hwnd), 2);
ccs.idFirstChild = ID_MDI_FIRSTCHILD;
g_hMDIClient = CreateWindowEx(WS_EX_CLIENTEDGE, "mdiclient", NULL,
WS_CHILD | WS_CLIPCHILDREN | WS_VSCROLL | WS_HSCROLL | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
hwnd, (HMENU)IDC_MAIN_MDI, GetModuleHandle(NULL), (LPVOID)&ccs);
菜单句柄是MDI客戶窗口用来对每个它创建的窗口添加一个相应的项的弹出菜单的句柄,允许用戶在这个菜单中来选择他们想要激活的窗口,我们马上就来为这个事件添加功能代码.
这个例子中它为第三个弹出(序号为2)因为我在File后加了Edit和Window.
ccs.idFirstChild是用来作为客戶窗口添至窗口菜单的项的第一个标识的数字...你想要使其跟你的自己的菜单标识区別开来,这样可以处理你的菜单命令并将Windows菜单命令传至DefFrameProc()作处理.在例子中我指定了一个定义为50000的标识,高到使我确定我的菜单命令标识不会超过它.
现在我们还要在我们的WM_COMMAND处理代码中添加一些东西来使这个菜单工作正常:
case WM_COMMAND:
switch(LOWORD(wParam))
{
case ID_FILE_EXIT:
PostMessage(hwnd, WM_CLOSE, 0, 0);
break;
// ... handle other regular IDs ...
// Handle MDI Window commands
default:
{
if(LOWORD(wParam) >= ID_MDI_FIRSTCHILD)
{
DefFrameProc(hwnd, g_hMDIClient, msg, wParam, lParam);
}
else
{
HWND hChild = (HWND)SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,0);
if(hChild)
{
SendMessage(hChild, WM_COMMAND, wParam, lParam);
}
}
}
}
break;
我加了一个default:来处理所有我沒有直接处理的消息并检查它是否超过或对于ID_MDI_FIRSTCHILD.如果是,那么是用戶点击了一个Window菜单项我们就把消息送给DefFrameProc()来处理.
如果不是我们的Window标识我们就获取句柄来激活子窗口并把消息传给它处理.这样可使你将责任推给子窗口来完成一些特定的动作,并允许不同的子窗口按照需要以不同的方式来处理消息.这个例子中我只处理了我们的框架窗口过程中的全局命令消息,并将影响特定子窗口或文档的消息发送给子窗口自己来处理.
因为我们以最后一个例子来作为基础来构建的,所以调整MDI客戶窗口大小的代码跟那个例子中调整编辑框的大小的代码一样,负责处理工具栏和状态栏的大小与位置以使它们不会覆盖MDI客戶窗口.
我们也要把我们的消息循环攺一点...
while(GetMessage(&Msg, NULL, 0, 0))
{
if (!TranslateMDISysAccel(g_hMDIClient, &Msg))
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
}
我们加了一个额外的步骤(TranslateMDISysAccel()),是用来检查预定义好的一些加速键的,Ctrl+F6切換到下个窗口,Ctrl+F4关闭子窗口等等.如果你不添加这些检查的话,你就要被你的用戶抱怨沒有提供他们已经熟悉的标準功能,或者你要另外手动实现这些功能.
子窗口类
除了我们的主窗口(框架窗口)之外,我们还要为我们所要的每个类型的子窗口创建一个新的窗口类.比如你可能需要一个来显示文字,一个显示图形或图片.这个例子中我们只创建一种子窗口类型,具备就像我们在前面的章节的那个编辑器程序一样的功能.
BOOL SetUpMDIChildWindowClass(HINSTANCE hInstance)
{
WNDCLASSEX wc;
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = MDIChildWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_3DFACE+1);
wc.lpszMenuName = NULL;
wc.lpszClassName = g_szChildClassName;
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if(!RegisterClassEx(&wc))
{
MessageBox(0, "Could Not Register Child Window", "Oh Oh...",
MB_ICONEXCLAMATION | MB_OK);
return FALSE;
}
else
return TRUE;
}
这基本上跟我们的普通的框架窗口的注冊一样,沒有特別的MDI的特征.我们把菜单设为NULL,并设置指向我们马上要写的子窗口过程的窗口过程.
MDI子窗口过程
MDI子窗口的窗口过程跟一般的窗口过程只有一些区別.首先默认的消息传给DefMDIChildProc()而不是DefWindowProc().
这个例子中,我们还要在不需要的时候禁止Edit和Window菜单(就是因为这件事情看起来很酷,所以要做),所以我们处理WM_MDIACTIVEATE并根据我们的窗口是否被激活来使能或禁止它们.如果有多种类型的子窗口,这里你就可以放一些代码使程序根据不同的激活窗口来攺变菜单或工具栏或是修攺程序的一些別的方面.
为了更完整,我们可以也禁止Close和Save文件菜单,因为如果沒有窗口它们也沒有什么好处.我在资源中把它们默认地都禁止了所以我不需要再加一些代码在应用引导的时候来做这件事情.
LRESULT CALLBACK MDIChildWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_CREATE:
{
HFONT hfDefault;
HWND hEdit;
// Create Edit Control
hEdit = CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "",
WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL,
0, 0, 100, 100, hwnd, (HMENU)IDC_CHILD_EDIT, GetModuleHandle(NULL), NULL);
if(hEdit == NULL)
MessageBox(hwnd, "Could not create edit box.", "Error", MB_OK | MB_ICONERROR);
hfDefault = GetStockObject(DEFAULT_GUI_FONT);
SendMessage(hEdit, WM_SETFONT, (WPARAM)hfDefault, MAKELPARAM(FALSE, 0));
}
break;
case WM_MDIACTIVATE:
{
HMENU hMenu, hFileMenu;
UINT EnableFlag;
hMenu = GetMenu(g_hMainWindow);
if(hwnd == (HWND)lParam)
{ //being activated, enable the menus
EnableFlag = MF_ENABLED;
}
else
{ //being de-activated, gray the menus
EnableFlag = MF_GRAYED;
}
EnableMenuItem(hMenu, 1, MF_BYPOSITION | EnableFlag);
EnableMenuItem(hMenu, 2, MF_BYPOSITION | EnableFlag);
hFileMenu = GetSubMenu(hMenu, 0);
EnableMenuItem(hFileMenu, ID_FILE_SAVEAS, MF_BYCOMMAND | EnableFlag);
EnableMenuItem(hFileMenu, ID_FILE_CLOSE, MF_BYCOMMAND | EnableFlag);
EnableMenuItem(hFileMenu, ID_FILE_CLOSEALL, MF_BYCOMMAND | EnableFlag);
DrawMenuBar(g_hMainWindow);
}
break;
case WM_COMMAND:
switch(LOWORD(wParam))
{
case ID_FILE_OPEN:
DoFileOpen(hwnd);
break;
case ID_FILE_SAVEAS:
DoFileSave(hwnd);
break;
case ID_EDIT_CUT:
SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_CUT, 0, 0);
break;
case ID_EDIT_COPY:
SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_COPY, 0, 0);
break;
case ID_EDIT_PASTE:
SendDlgItemMessage(hwnd, IDC_CHILD_EDIT, WM_PASTE, 0, 0);
break;
}
break;
case WM_SIZE:
{
HWND hEdit;
RECT rcClient;
// Calculate remaining height and size edit
GetClientRect(hwnd, &rcClient);
hEdit = GetDlgItem(hwnd, IDC_CHILD_EDIT);
SetWindowPos(hEdit, NULL, 0, 0, rcClient.right, rcClient.bottom, SWP_NOZORDER);
}
return DefMDIChildProc(hwnd, msg, wParam, lParam);
default:
return DefMDIChildProc(hwnd, msg, wParam, lParam);
}
return 0;
}
我把File
Open 和Save
as都当作命令来处理,DoFileOpen()和DoFileSave()跟前面的章节中编辑框控件的标识几乎一样,并且添加了将MDI子窗口的标题设为文件名的功能.
编辑命令很简单,因为里面的编辑框本身就支持它们,我们只需要告诉它做什么就行.
记得我提到过有些事情你如果不记住的话你的程序就会行为怪異吗?注意到我在WM_SIZE的处理末尾调用了DefMDIChildProc()沒有,这点很重要不然系统就沒有机会为这个消息作它自己的处理.你可以在MSDN中查下DefMDIChildProc()处理的消息,记得在这些消息处理的后面加上它.
创建和销毀窗口
MDI子窗口不是直接创建的,而是我们向客戶窗口发送一个WM_MDICREATE消息告诉它我们要创建一个以MDICREATESTRUCT的某些成员指定的什么样的一个子窗口.你可以在你的文档中查一下这个结构体的成员,都相当简单.WM_MDICREATE消息处理函数的返回值就是新创建的窗口的句柄.
HWND CreateNewMDIChild(HWND hMDIClient)
{
MDICREATESTRUCT mcs;
HWND hChild;
mcs.szTitle = "[Untitled]";
mcs.szClass = g_szChildClassName;
mcs.hOwner = GetModuleHandle(NULL);
mcs.x = mcs.cx = CW_USEDEFAULT;
mcs.y = mcs.cy = CW_USEDEFAULT;
mcs.style = MDIS_ALLCHILDSTYLES;
hChild = (HWND)SendMessage(hMDIClient, WM_MDICREATE, 0, (LONG)&mcs);
if(!hChild)
{
MessageBox(hMDIClient, "MDI Child creation failed.", "Oh Oh...",
MB_ICONEXCLAMATION | MB_OK);
}
return hChild;
}
在MDICREATESTRUCT中我这里沒有用的一个很有用的成员是lParam.这个成员可用来向子窗口发送任何32bit的值(比如一个指针)以传递任何你选择的自定义消息.在你子窗口的WM_CREATE消息处理中,WM_CREATE的lParam值将指向一个CREATESTRUCT.这个结构的lpCreateParams成员将指向你随WM_MDICREATE发送的MDICREATESTRUCT. 所以要在子窗口中访问lParam,你要在子窗口的窗口过程中做些事情...
case WM_CREATE:
{
CREATESTRUCT* pCreateStruct;
MDICREATESTRUCT* pMDICreateStruct;
pCreateStruct = (CREATESTRUCT*)lParam;
pMDICreateStruct = (MDICREATESTRUCT*)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.
*/
}
break;
如果你不想麻烦额外的两个指针就可以就这样一步到位访问lParam((MDICREATESTRUCT*)((CREATESTRUCT*)lParam)->lpCreateParams)->lParam
现在我们可以在我们的框架窗口中实现菜单上的文件命令了:
case ID_FILE_NEW:
CreateNewMDIChild(g_hMDIClient);
break;
case ID_FILE_OPEN:
{
HWND hChild = CreateNewMDIChild(g_hMDIClient);
if(hChild)
{
DoFileOpen(hChild);
}
}
break;
case ID_FILE_CLOSE:
{
HWND hChild = (HWND)SendMessage(g_hMDIClient, WM_MDIGETACTIVE,0,0);
if(hChild)
{
SendMessage(hChild, WM_CLOSE, 0, 0);
}
}
break;
我们也可以为我们的Window菜单提供一些默认的MDI处理,因为MDI本身就支持这样所以要做的事情不是很多.
case ID_WINDOW_TILE:
SendMessage(g_hMDIClient, WM_MDITILE, 0, 0);
break;
case ID_WINDOW_CASCADE:
SendMessage(g_hMDIClient, WM_MDICASCADE, 0, 0);
break;
|