多文档界面(Multiple-Document Interface, MDI)是针对处理文档的应用,这套规范描述了如何通过一个窗口结构和用户界面来让用户在单个应用程序中处理多个文档(如字处理软件中的文本文档,还有电子表格程序中的电子表格)。简单来说,一个MDI程序要在一个客户区内维护多个文档窗口,这就好比Windows在一个屏幕内管理多个程序窗口。Windows上的第一个MDI程序是最早版本的Windows版的 Excel。不过,其他MDI软件很快应运而生。
本章学习知识概要:
MDI的概念
MDI示例
18.1 MDI的概念
文件,它比老式的图元文件格式有很多改进。
本节必须掌握的知识点:
MDI的构成元素
MDI 支持
18.1.1 MDI的构成元素
MDI程序的主应用窗口和传统程序一样:有标题栏、菜单.、大小可调的边框、系统菜单图标以及最大化、最小化、关闭按钮。不同的是,客户区并不直接显示程序输出,这个区域也常称为“工作区” 。工作区中可以有多个子窗口(也叫文档窗口)也可以没有子窗口,每个子窗口用于显示一个文档。
这些子窗口的外观和通常的应用程序窗口以及MDI程序的主应用窗口很类似。它们也 拥有标题栏、可调边框、系统菜单图标和最大化、最小化、关闭按钮,甚至还可以有滚动条。不同的地方在于这些文档窗口自己没有菜单。主窗口上的菜单会应用于这些子窗口。
任何时候只能有一个文档窗口是活动窗口,它的标题栏会呈现为高亮,并且活动窗口会排列在其他文档窗口的上面。所有的文档窗口都被限定在工作区内,不会在主窗口之外显示。
对Windows程序员来说,MDI初看起来好像很简单直观。只需要为每个文档创建一个 WS_CHILD窗口。并把程序的主窗口设成它们的父窗口即可。但是随着深入了解现有的 MDI应用程序,就会发现有些应用程序需要复杂的编程。
● MDI文档窗口可以最小化,变成位于工作区底部的个带图标的小标题栏。通常, MDI程序的主窗口和每一种文档窗口会使用不同的图标。
● MDI文档窗口可以最大化。这时,文档窗口的标题栏(通常用于显示窗口里文档的 文件名)会消失,文件名则将附加在其主窗口标题栏的应用程序名之后。它的系统菜单图标会出现在主窗口顶级菜单的最前面。文档窗口的关闭按钮则相应地出现在主窗口顶级菜单的最后面,靠最右端的地方。
●关闭文档窗口的系统快捷键和关闭主窗口的快捷键类似,只是用Ctrl键替换了Alt 键。也就是说,A11-F4关闭整个程序,而Ctrl-F4只关闭当前文档窗口。此外,Ctrl-F6 可以用于在多个子文档窗口之间进行切换。Alt-空格键可以调出主窗口的系统菜 。Alt-(减号)可以调出当前活动子文档窗口的系统菜单。
●通常,使用光标键在菜单上移动时,焦点会从系统菜单转到菜单栏的第一项上。 对于MDI程序来说,焦点则会从主窗口系统菜单转到活动文档窗口的系统菜单, 进而到菜单栏的第一项上。
●如果程序能支持多种类型的子窗口(例如Excel中的工作表和图表文档),那么菜单要能够反映出与对应文档类型相符的操作。这就需要在切换到一个不同类型的文档窗口时更新菜单。此外,如果没有打开的文档,主窗口菜单则要缩减到只显示必要的操作,如打开或者创建新文档。
●顶级菜单栏有一项是【窗口】。根据惯例,它应该是顶级菜单栏中除【帮助】外的最后一项。【窗口】子菜单通常提供用于在客户区中安排文档窗口位置的功能。多个文档窗口可以从左上角开始“堆叠”起来显示,也可以“平铺”显示以便每个文档窗口都能看到。这个子菜单下还有一个列表,包含了所有文档窗口,在其 中选取就可以将对应的文档窗口提到前台来。
所有的这些MDI的特性Windows 98都支持。当然了,写程序时还是需要一些额外的工作的(后面的范例程序中会看到)。不过,这比起完全自己写代码来支持所有这些要容易得多。
18.1.2 MDI 支持
在介绍MDI支持之前需要先了解一些新术语。主应用程序窗口被称作“框架窗口”。和一般的Windows程序一样,这是一个WS_OVERLAPPEDWINDOW样式的窗口。
MDI程序还会创建一个基于预定义的窗口类MDICLIENT的“客户窗口”。客户窗口是通过用这个窗口类调用CreateWindow并传入WS_CHILD样式创建的。CreateWindow的最后一个参数是一个指向CLIENTCREATESTRUCT结构的指针。创建的客户窗口占据整个框架窗口的客户区,它负责支持大部分的MDI行为。客户窗口的颜色使用系统颜色 COLOR_APP WORKSPACE。
你可能已经注意到,文档窗口也被称为“子窗口”。创建它们需要初始化一个 MDICREATESTRUCT结构,然后向客户窗口发送一个WM_MDICREATE消息,并用一个指针指向这个被创建的结构。
■框架窗口
●本身是一个普通的主窗口,其客户区被特殊的窗口覆盖,并不直接显示程序的输出。其客户区也被称为“工作区”。
●默认的消息处理函数是DefFrameProc,而不是DefWindowProc。
■客户窗口
●系统预定义的窗口类,类名“MDICLIENT”,负责各个MDI子窗口的管理。
●窗口过程系统己经预先注册,用户程序不需要窗口过程。
■文档窗口:也称为子窗口,用于显示一个文档。
文档窗口是客户窗口的子窗口,客户窗口则是框架窗口的子窗口。各窗口的父子关系如图19-1所示。
图18-1 MDI程序窗口的层次结构
框架窗口需要一个对应的窗口类(和窗口过程),每个类型的子窗口也是一样的。而客户窗口不需要窗口过程,因为它的窗口类已经预先注册好。
Windows 98的MDI支持包括一个窗口类、五个函数、两个数据结构以及十二个消息。 前面已经提到了窗口类MDICLIENT和数据结构CLIENTCREATESTRUCT以及 MDICREATESTRUCT。五个函数里有两个是用来替代在MDI应用程序中的 DefWindowProc的。对所有未处理的消息,不再调用DefWindowProc,而是在框架窗口过程中调用DefFrameProc,在子窗口窗口过程中调用DefMDIChildProc。另一个MDI特有的函数是TranslateMDISysAccel,它和TranslateAccelerator的使用方法一样,后者在第九章中已经介绍过。MDI支持还包括ArrangeIconicWindows,不过有一个特殊的MDI消息使我们可以不用这个函数了。
第五个MDI函数是CreateMDIWindow,用于在一个独立的执行线程中创建子窗口。在单线程程序中不需要这个函数。
在即将给出的范例程序中,我们会演示十二个MDI消息中的九个(其他三个不常用)。这些消息都有一个前缀WM_MDI。框架窗口向客户窗口发送这些消息来进行子窗口的操作或 者获取子窗口的信息。(例如,框架窗口向客户窗口发送WM_MDICREATE消息来创建子窗口。)WM_MDIACTIVE消息略有不同。框架窗口可以向客户窗口发送这个消息来激活某个子窗口,而客户窗口既会把这个消息发送给被激活的子窗口,也会发送给从活动状态进入后台的那个子窗口,来通知它们这个变化。
18.2 MDI示例
文件,它比老式的图元文件格式有很多改进。
本节必须掌握的知识点:
第146练:MDI多文档界面
18.2.1 第146练:MDI多文档界面
MDIDEMO.C
/*------------------------------------------------------------------------
146 WIN32 API 每日一练
第146个例子MDIDEMO.C:MDI多文档界面
LoadMenu 函数
GetSubMenu函数
CLIENTCREATESTRUCT结构
MDICREATESTRUCT结构
EnumChildWindows函数
IsWindow函数
GetWindow函数
GetProcessHeap函数
HeapAlloc函数
GetWindowLong函数
HeapFree函数
(c) www.bcdaren.com 编程达人
-----------------------------------------------------------------------*/
#include <windows.h>
#include "resource.h"
//3个子菜单资源ID
#define INIT_MENU_POS 0
#define HELLO_MENU_POS 2
#define RECT_MENU_POS 1
#define IDM_FIRSTCHILD 50000 //第一个MDI子窗口的子窗口标识符
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);
//每个Hello子窗口存储私有数据的结构
typedef struct tagHELLODATA
{
UINT iColor; //颜色菜单ID
COLORREF clrText; //RGB
}HELLODATA,*PHELLODATA;
//每个Rect子窗口存储私有数据的结构
typedef struct tagRECTDATA
{
short cxClient; //宽
short cyClient; //高
}RECTDATA,* PRECTDATA;
//全局变量
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,LPSTR lpCmdLine,int nShowCmd)
{
HACCEL hAccel;
HWND hwndFrame,hwndClient;
MSG msg;
WNDCLASS wndclass;
hInst = hInstance;
//注册框架窗口类
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;//在FrameWndProc的CreateWindow中指定
wndclass.lpszClassName = szFrameClass;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL,TEXT("This program requires windows
NT!"),szAppName,MB_ICONERROR);
return 0;
}
//注册hello子窗口类
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = HelloWndProc;
wndclass.cbClsExtra = 0;
//窗口实例之后要分配的额外字节数--32位值
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;//在HelloWndProc的WM_MDIACTIVATE中指定
wndclass.lpszClassName = szHelloClass;
RegisterClass(&wndclass);
//注册Rect子窗口类
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;//在RectWndProc的WM_MDIACTIVATE中指定
wndclass.lpszClassName = szRectClass;
RegisterClass(&wndclass);
//获取三个菜单资源句柄
hMenuInit = LoadMenu(hInstance,TEXT("MdiMenuInit"));
hMenuHello = LoadMenu(hInstance,TEXT("MdiMenuHello"));
hMenuRect = LoadMenu(hInstance,TEXT("MdiMenuRect"));
//上述三个菜单模板中的Window窗口子菜单句柄,文档列表会被加在这些子菜单之下
//菜单资源属于框架窗口,子窗口不可以用于菜单
//指定菜单中从零开始的相对位置
hMenuInitWindow = GetSubMenu(hMenuInit,INIT_MENU_POS);
hMenuHelloWindow = GetSubMenu(hMenuHello,HELLO_MENU_POS);
hMenuRectWindow = GetSubMenu(hMenuRect,RECT_MENU_POS);
//加载加速键
hAccel = LoadAccelerators(hInstance,szAppName);
//创建主框架窗口
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,nShowCmd);
ShowWindow(hwndFrame,nShowCmd);
UpdateWindow(hwndFrame);
while (GetMessage(&msg,NULL,0,0))
{
//不是MDI子窗口或者加速键消息时,管理子窗口的消息都由客户窗口处理
if (!TranslateMDISysAccel(hwndClient,&msg)
&& !TranslateAccelerator(hwndFrame,hAccel,&msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
//手动销毁hMenuHello和hMenuRect子菜单,hMenuInit菜单自动销毁
DestroyMenu(hMenuHello);
DestroyMenu(hMenuRect);
return msg.wParam;
}
//框架窗口过程
LRESULT CALLBACK FrameWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{//客户窗口(静态的),因为默认的窗口过程需要传入此参数。
static HWND hwndClient;
CLIENTCREATESTRUCT clientcreate;//创建的第一个MDI子窗口结构
HWND hwndChild;
MDICREATESTRUCT mdicreate;//创建MDI子窗口结构
switch (message)
{
case WM_CREATE:
//填充CLIENTCREATESTRUCT结构体,并根据该结构体来创建客户窗口---即子窗口的父窗口
//文档列表要添加在其后的子菜单句柄
clientcreate.hWindowMenu = hMenuInitWindow;
//创建的第一个MDI子窗口的子窗口标识符,其他MDI子窗口增加标识符
clientcreate.idFirstChild = IDM_FIRSTCHILD;
hwndClient = CreateWindow(TEXT("MDICLIENT"), NULL,
WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE,
0, 0, 0, 0,
hwnd, (HMENU)1, hInst, //菜单ID为1
(LPVOID)&clientcreate);
return 0;
case WM_COMMAND:
switch (LOWORD(wParam))//菜单ID
{
case IDM_FILE_NEWHELLO: //创建Hello子窗口
mdicreate.szClass = szHelloClass;
//当子窗口最大化时,会把该标题加在框架窗口标题后面
mdicreate.szTitle = TEXT("Hello");
mdicreate.hOwner = hInst; //注意,这里是hInst,而不是hwnd
mdicreate.x = CW_USEDEFAULT;
mdicreate.y = CW_USEDEFAULT;
mdicreate.cx = CW_USEDEFAULT;
mdicreate.cy = CW_USEDEFAULT;
mdicreate.style = 0;
mdicreate.lParam = 0;
/*这个lParam可以给框架窗口和子窗口提供共享某些变量的方法。
用法:在HelloWndProc的WM_CREATE消息的lParam字段(是个CREATESTRUCT结构),而这个结构的lpCreateParams字段是一个指向用来创建窗口的MDICREATESTRUCT结构的指针,可从指针中取出mdicreate.lParam出来。*/
//发送WM_MDICREATE消息给“客户窗口”以便让其根据传入的mdicreate
//信息创建hello子窗口
hwndChild = (HWND)SendMessage(hwndClient, WM_MDICREATE, 0,
(LPARAM)(LPMDICREATESTRUCT)&mdicreate);
return 0;
case IDM_FILE_NEWRECT: //创建RECT子窗口
mdicreate.szClass = szRectClass;
//当子窗口最大化时,会把该标题加在框架窗口标题后面
mdicreate.szTitle = TEXT("Rectangles");
mdicreate.hOwner = hInst; //注意,这里是hInst,而不是hwnd
mdicreate.x = CW_USEDEFAULT;
mdicreate.y = CW_USEDEFAULT;
mdicreate.cx = CW_USEDEFAULT;
mdicreate.cy = CW_USEDEFAULT;
mdicreate.style = 0;
mdicreate.lParam = 0; //与Hello子窗口相同的处理
//发送WM_MDICREATE消息给“客户窗口”以便让其根据传入的mdicreate
//信息创建Rect子窗口
hwndChild = (HWND)SendMessage(hwndClient, WM_MDICREATE, 0,
(LPARAM)(LPMDICREATESTRUCT)&mdicreate);
return 0;
//关闭当前活动的“文档窗口”(单个),而不是所有文档窗口
case IDM_FILE_CLOSE:
//先获取当前活动的“文档窗口”
hwndChild = (HWND)SendMessage(hwndClient,
WM_MDIGETACTIVE, 0, 0);
//如果该文档窗口选择“确定”,表示允许关闭该窗口,
//则向客户窗口发送“销毁”消息。
if (SendMessage(hwndChild, WM_QUERYENDSESSION, 0, 0))
SendMessage(hwndClient, WM_MDIDESTROY,
(WPARAM)hwndChild, 0);//wParam为要关闭的子窗口句柄
return 0;
case IDM_APP_EXIT:
SendMessage(hwnd, WM_CLOSE, 0, 0);
return 0;
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:
//这里枚举各子窗口的目的是为了,确定是否关闭。
//这里的第1个参数为hwndClient,表示只枚举客户窗口的所有子窗口
EnumChildWindows(hwndClient, CloseEnumProc, 0);
return 0;
default:
//将其他菜单消息传给当前活动的子窗口
hwndChild = (HWND)SendMessage(hwndClient,
WM_MDIGETACTIVE, 0, 0);
if (IsWindow(hwndChild)) //判断窗口是否存在
{
//Color菜单中的消息,转发送给Hello子窗口自己去处理,
//本例中框架窗口不处理该消息,因为不同的窗口要显示不同颜色的字体
SendMessage(hwndChild, WM_COMMAND, wParam, lParam);
}
break; //然后,交给DefFrameProc去处理。
}
break;
case WM_QUERYENDSESSION:
case WM_CLOSE: //试图关联所有的子窗口
SendMessage(hwnd, WM_COMMAND, IDM_WINDOW_CLOSEALL, 0);
//如果客户窗口上仍有文档窗口,则return不去退出程序
if (NULL != GetWindow(hwndClient, GW_CHILD))
return 0; //这里return 0,表示不退出程序。
//如果客户窗口上己经没有文档窗口了,则break,
//交由DefFrameProc去最后销毁程序。
break;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
//将未处理的消息交给DefFrameProc(注意:不是DefWindowProc)
//所有框架不处理的消息,都必须传给DefFrameProc。
//此外,WM_MENUCHAR、WM_SETFOCUS、WM_SIZE消息,在框架窗口过程中处理完后
//也必须再交给DefFrameProc去进一步处理。
return DefFrameProc(hwnd, hwndClient, message, wParam, lParam);
}
resource.h
//{{NO_DEPENDENCIES}}
// 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
// 新对象的下一组默认值
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 105
#define _APS_NEXT_COMMAND_VALUE 40020
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
MDIDEMO.rc
// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/
//
// 从 TEXTINCLUDE 2 资源生成。
//
#include "winres.h"
/
#undef APSTUDIO_READONLY_SYMBOLS
/
// 中文(简体,中国) 资源
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
LANGUAGE 4, 2
#ifdef APSTUDIO_INVOKED
/
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/
//
// Menu
//
MDIMENUINIT MENU
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
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
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 &Icon", IDM_WINDOW_ARRANGE
MENUITEM "Close &All", IDM_WINDOW_CLOSEALL
END
END
/
//
// Accelerator
//
MDIDEMO ACCELERATORS
BEGIN
VK_F4, IDM_WINDOW_TILE, VIRTKEY, SHIFT, NOINVERT
VK_F5, IDM_WINDOW_CASCADE, VIRTKEY, SHIFT, NOINVERT
END
#endif // 中文(简体,中国) 资源
/
#ifndef APSTUDIO_INVOKED
/
//
// 从 TEXTINCLUDE 3 资源生成。
//
/
#endif // 不是 APSTUDIO_INVOKED
运行结果:
图18-2 MDI多文档界面
总结
■实例MDIDEMO.C 包含三个菜单。
我们首先来看MDIDEMO.RC资源脚本。这个资源脚本定义了程序所使用的三个菜单模板。
当没有文档窗口时,程序显示MdiMenuInit菜单。这个菜单仅仅让你建立一个新文档, 或是退出程序。
MdiMenuHello菜单是跟显示“Hello, World! ”的文档窗口相联系的。File子菜单可以打开任何一种格式的新文档、关闭活动文档和退出程序。Color子菜单可以让你设置文本颜色。Window子菜单提供选项让你可以把文档窗口按层叠或平铺的方式来排列、排列文档图标和关闭所有窗口。这个子菜单还会按照文档窗口的建立顺序把它们列出来。
MdiMenuRect菜单是和随机矩形文档联系在一起的。它跟MdiMenuHello菜单一样,只是不包括Color子菜单。
跟往常一样,RESOURCE.H头文件定义了所有的菜单标识符。此外,在MDIDEMO.C 中还定义了以下三个常数:
#define INIT_MENO_POS 0
#define HELLO_MENU_POS 2
#define RECT_MENU_POS 1
这些标识符指定Window子菜单在三个菜单模板中的位置。程序需要这些信息来告诉客户窗口在哪儿放置文档列表。当然,MdiMenuInit菜单没有Window子菜单,所以我们告诉它列表应该放在第一个子菜单的后面(位置0)。然而,列表实际上是永远不会在那出现的。
在MDIDEMO.C中定义的IDM_FIRSTCHILD标识符并不对应一个菜单项。这个标识符是给将要出现在Window子菜单中的列表内的第一个文档窗口的。这个标识符应该比其他所有菜单ID都大。
■菜单控制
●在MDI程序中,可以根据激活的子窗口而切换框架窗口的菜单。并且,可以将文档窗口列表添加到菜单中去。所添加的菜单项命令是又框架对应的缺省消息处理函数完成的。
1.为每种窗口类准备一套菜单资源。
2.装载菜单,得到菜单句柄。
3.框架在建立时,使用框架菜单的句柄作为参数。
4.子窗口在激活时,加载自己菜单到框架窗口。
5.失去焦点时,还原框架菜单。
●使用向MDI“客户窗口”发送WM_MDISETMENU或WM_MDISETMENU消息。
case WM_MDIACTIVATE: //wParam为菜单句柄,lParam为欲添加窗口列表子菜单句柄
//激活时,设置框架菜单
if (lParam == (LPARAM) hwnd)
SendMessage (hwndClient, WM_MDISETMENU,
(WPARAM) hMenuHello, (LPARAM) hMenuHelloWindow) ;
//失去焦点时,将框架菜单还原
if (lParam != (LPARAM) hwnd)
SendMessage (hwndClient, WM_MDISETMENU, (WPARAM) hMenuInit,
(LPARAM) hMenuInitWindow) ;
DrawMenuBar (hwndFrame) ;
//注: hwndFrame的得到方法:
//hwndClient = GetParent (hwnd) ;
//hwndFrame = GetParent (hwndClient) ;
return 0 ;
■程序初始化
在MDIDEMO.C中,WinMain一开始就为框架窗口和两个子窗口注册了窗口类,它们的窗口过程分别叫FrameWndProc、HelloWndProc和RectWndProc。一般情况下,这些窗口类会有不同的图标。为了简单起见,我们让框架窗口和子窗口都使用标准的 IDI_APPLICATION 图标。
●框架窗口:先注册一个MDI框架窗口类,并提供MDI框架窗口的窗口过程。
//MDI框架窗口的消息处理函数
LRESULT CALLBACK FrameWndProc (HWND, UINT, WPARAM, LPARAM) ;
{
……
//其他消息交给MDI框架缺省的处理函数,第2个参数是客户窗口的句柄
return DefFrameProc(hwnd,hwndClient,message,wParam,lParam);
}
●客户窗口的建立:在主框架窗口WM_CREATE消息中创建
case WM_CREATE:
hInst = ((LPCREATESTRUCT)lParam)->hInstance;
//填充CLIENTCREATESTRUCT结构体,并根据该结构体来创建客户窗口
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,
(LPVOID)&clientcreate);
【注意】客户窗口的大小没有关系。MDI客户区窗口建立后,通过向它发送消息管理子窗口的建立、销毁、排列等。
●文档窗口(也叫子窗口)的建立:在主框架窗口的主菜单项中创建。
case IDM_FILE_NEWHELLO: //创建Hello子窗口
mdicreate.szClass = szHelloClass; //MDI子窗口的类名称
mdicreate.szTitle = TEXT("Hello"); //当最大化时该标题加在框架标题后面
mdicreate.hOwner = hInst; //注意,这里是hInst,而不是hwnd
mdicreate.x = CW_USEDEFAULT;
mdicreate.y = CW_USEDEFAULT;
mdicreate.cx = CW_USEDEFAULT;
mdicreate.cy = CW_USEDEFAULT;
mdicreate.style = 0;
mdicreate.lParam = 0;
//发送WM_MDICREATE消息给“客户窗口”以便让其根据传入的
//mdicreate信息创建hello子窗口
hwndChild = (HWND)SendMessage(hwndClient, //MDI客户区窗口句柄
WM_MDICREATE, 0,
(LPARAM)(LPMDICREATESTRUCT)&mdicreate);
【注意】我们将框架窗口类的WNDCLASS结构中的hbrBackground字段定义为 COLOR_APPWORKSPACE系统颜色。这不是必要的,因为框架窗口的客户区会被客户窗口盖住,而客户窗口的颜色也是这个颜色。但是用这个颜色会使框架窗口第一次显示时更好看一些。
这三个窗口类的IpszMenuName字段都被设为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。
■消息循环中处理针对MDI的加速键
回到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_NEWR£CT 时,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菜申的Hie子菜单。我们会在后面看到这个文档列表进怎样被挪到MdiMenuHello和MdiMenuRect菜单的Window子菜单下的。
在该菜单中最多可以列出九个文档,每个前面都有从1到9的带下划线的数字,如果有多于九个文档窗口被创建,列表后面会跟着一个More Windows菜单项。该菜单项会调出一个对话框,显示一个列出了所有文档窗口的列表框。这个文档列表的维护是Windows MDI支持的最棒的功能之一。
■命令的流向
框架窗口在收到WM_COMMAND等通知消息后,应该给当前激活的MDI窗口提供处理机会。
case WM_COMMAND:
switch (LOWORD (wParam))
{
//针对框架的命令
case IDM_FILE_NEWHELLO:
//...
return 0;
//针对MDI子窗口管理的命令
case IDM_WINDOW_TILE:
SendMessage (hwndClient, WM_MDITILE, 0, 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 ; //跳出针对WM_COMMAND的case分支,又DefFrameProc处理剩下的命令
让我们在关注子文档窗口之前,继续讨论FrameWndProc的消息处理。
当你从File菜单选择Close选项时,MDIDEMO会关闭活动的子窗口。它通过向客户 窗口发送WM_MD1GETACTIVE消息来获得活动子窗口的句柄。如果子窗口确实对 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参数调用GetWindow时会返回一个非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得到。
■子窗口的管理
●子窗口排列——给MDI客户区窗口发控制消息即可。
case WM_COMMAND:
switch (LOWORD (wParam))
{
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;
//...
//...
}
break;
●当前子窗口的关闭
关闭当前激活窗口时,先向该窗口发送查询消息:WM_QUERYENDSESSION。子窗口的消息处理循环中响应此消息,作关闭前的一些处理,若能关闭,返回真否则返回假。框架窗口中根据此返回值决定是否关闭窗口。
【框架窗口命令处理中】
case IDM_FILE_CLOSE:
//获得当前激活窗口
hwndChild = (HWND) SendMessage (hwndClient, WM_MDIGETACTIVE, 0, 0);
//询问通过后,销毁窗口
if (SendMessage (hwndChild, WM_QUERYENDSESSION, 0, 0))
SendMessage (hwndClient, WM_MDIDESTROY, (WPARAM) hwndChild, 0);
return 0;
【子窗口的消息处理函数中】
LRESULT CALLBACK HelloWndProc (HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch (message)
{
//...
//...
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
}
return DefMDIChildProc (hwnd, message, wParam, lParam) ;
}
●关闭所有子窗口
当使用命令方式关闭所有子窗口时,需要枚举所有子窗口进行关闭。
【框架窗口响应命令】
case IDM_WINDOW_CLOSEALL:
//针对所有子窗口执行CloseEnumProc
EnumChildWindows (hwndClient, CloseEnumProc, 0) ;
return 0 ;
在WinMain中,MDIDEMO用LoadMenu来加载资源脚本中定义的三个菜单。通常 WindoWs会在菜单所依属的窗口被销毁的时候销毁菜单。Init菜单就是这样处理的。但是,不依属某一窗口的菜单需要被显式地销毁。为此,MDIDEMO在WinMain的最后两次调用 DestroyMenu 来删除 Hello 和 Rect 菜单。
【枚举函数】
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 ;
}
现在让我们来看HelloWndProc,它是用来显示“Hello, World! ”的子文档窗口的窗口过程。
跟其他任何用于多个窗口的窗口类一样,在窗口过程(或窗口过程调用的任何函数)中定义的静态变量都会被所有基于该窗口类而创建的窗口共享。
每个窗口自己独有的数据必须用非静态变量的方法来存放。一种技巧是用窗口属性。 另一种方法是利用预留的内存,而这也正是我所采用的方法。该内存是在用来注册窗口类的WNDCLASS结构的cbWndExtra字段中定义非0值而预留的。
在MDIDEMO中,我们用这个空间来存储指向一个内存块的指针,该内存的大小是 HELLODATA结构的大小。HelloWndProc在WM_CREATE消息中分配这块内存,初始化其中的两个字段(它们表示当前被选定的菜单项和文本颜色),并用SetWindowLong来存储这个指针。
当处理改变文本颜色的WM_COMMAND消息时(请记得这些消息是从框架窗口过程产生的),HelloWndProc用GetWindowLong来得到指向含有HELLODATA结构的内存块的指针。用这个结构,HelloWndProc会取消选中已选定的菜单项,再选中所选择的菜单项,并保存新颜色。
只要窗口变成活动或非活动状态(看lParam是不是包含窗口的句柄文档窗口过程就会收到WM_MDIACTIVATE消息。你应该记得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。