纯Win32 MDI应用程序






4.08/5 (12投票s)
用一个简单的例子解释了 Windows 中的 MDI 支持。
引言
本文解释了如何使用纯 Win32 创建一个基本的 MDI 应用程序。作为此示例一部分创建的 MDI 子窗口显示了有关系统的以下基本信息:计算机名称、操作系统版本和服务包以及 CPU 数量。
本文的其他功能包括
- 打开子窗口时更新菜单
- 创建多部分状态栏
- 添加和删除系统托盘图标
- 使用 Mailslot 进行进程间通信
纯 Win32 MDI 创建
MDI 框架窗口在 WinMain()
中创建。在 MDI 框架窗口的 WM_CREATE
中,MDI 客户端区域的创建如下。所有子窗口都将在该客户端区域内浮动。
CLIENTCREATESTRUCT MDIClientCreateStruct;
// Structure to be used for MDI client area
switch(message)
{
case WM_CREATE:
// On creation of main frame, create the MDI client area
MDIClientCreateStruct.hWindowMenu = NULL;
MDIClientCreateStruct.idFirstChild = IDM_FIRSTCHILD;
ghMDIClientArea = CreateWindow(TEXT("MDICLIENT"), // predefined value for
// MDI client area
NULL, // no caption required
WS_CHILD | WS_CLIPCHILDREN | WS_VISIBLE,
0, // No need to give any x/y or height/width.
0,
0,
0,
hwnd,
NULL,
ghInstance,
(void*) &MDIClientCreateStruct);
MDICLIENT 用作第一个参数,并且是 Windows MDI 支持的一部分预定义的。类型为 CLIENTCREATESTRUCT
的变量作为指针传递,作为 CreateWindow
的最后一个参数。CLIENTCREATESTRUCT
有两个元素:一个是应用程序窗口菜单的句柄,第二个是第一个子窗口创建的起始标识符(后续窗口将在此基础上递增)。
显示系统信息的 MDI 子窗口是在主框架窗口过程 SigmaFrameWndProc
中接收到命令消息 ID_INFORMATION_SYSTEMINFORMATION
时创建的。在此,在堆上创建了一个 CSystemInfo
对象。然后使用此 g_pSystemInfo
对象创建窗口并填充树形控件。
CSystemInfo::CreateSystemInfoWindow()
注册子窗口并创建 MDI 子窗口。代码片段如下所示。
MDICREATESTRUCT MDIChildCreateStruct;
MDIChildCreateStruct.szClass = TEXT("SigmaSystemInfoWnd");
MDIChildCreateStruct.szTitle = TEXT("System Information");
MDIChildCreateStruct.hOwner = ghInstance;
MDIChildCreateStruct.x = CW_USEDEFAULT;
MDIChildCreateStruct.y = CW_USEDEFAULT;
MDIChildCreateStruct.cx = CW_USEDEFAULT;
MDIChildCreateStruct.cy = CW_USEDEFAULT;
MDIChildCreateStruct.style = 0;
MDIChildCreateStruct.lParam = 0;
//
m_hwndSystemInformation = (HWND) SendMessage(ghMDIClientArea,
WM_MDICREATE,
0,
(LPARAM) (LPMDICREATESTRUCT) &MDIChildCreateStruct);
// return if its not possible to create the child window
if(NULL == m_hwndSystemInformation)
{
return 0;
}
MDICREATESTRUCT
用于设置子窗口参数。将其作为 SendMessage
的最后一个参数传递。SendMessage
用于向主框架窗口的客户端区域发送 WM_MDICREATE
消息。
类及其功能
CSystemInfo
– 此类通过CSystemInfoData
和CSystemInfoView
类分别保存表示数据和视图的成员元素。在此类中创建子窗口。CSystemInfoData
– 此类存储应在树形控件中显示的数据。CSystemInfoView
–此类用于初始化树形控件,并将CSystemInfoData
中可用数据填充到树形控件中。
创建菜单并在子窗口活动时更改菜单
框架窗口菜单由资源表示:IDR_MAINFRAME_MENU
。此菜单在创建主框架窗口期间,通过 WinMain
本身的以下调用创建和加载,如下所示。
ghMainFrameMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MAINFRAME_MENU));
ghSysInfoMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_SYSINFO_MENU));
DWORD derror = GetLastError();
//Create the main MDI frame window
ghwndMainFrame = CreateWindow(gszSigmaFrameClassName,
gszAppName,
WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
CW_USEDEFAULT, // allows system choose an x position
CW_USEDEFAULT, // allows system choose a y position
CW_USEDEFAULT, // width, CW_USEDEFAULT allows system to
// choose height and width
CW_USEDEFAULT, // height, CW_USEDEFAULT ignores heights
// as this is set by setting
// CW_USEDEFAULT in width above.
NULL, // handle to parent window
ghMainFrameMenu, // handle to menu
hInstance, // handle to the instance of module
NULL); // Long pointer to a value to be passed
// to the window through the
// CREATESTRUCT structure passed in the
// lParam parameter the WM_CREATE message
子窗口菜单由 IDR_SYSINFO_MENU
表示。在子窗口过程 (SigmaSystemInfoWndProc
) 的 WM_MDIACTIVATE
下,添加了修改子窗口菜单和窗口菜单的代码。当焦点从打开的子窗口丢失时,它将设置框架窗口菜单。为了使此激活和去激活可见,需要有多个子窗口。在调用 SendMessage()
和 WM_MDISETMENU
后,需要调用 DrawMenuBar()
。一旦子窗口被打开,通过调用下面的 EnableMenuItem()
,其相应的菜单项就会被灰显。
case WM_MDIACTIVATE:
{
HWND hwndClient = GetParent(hWnd);
HWND hwndFrame = GetParent(hwndClient);
HMENU hSysInfoWindowMenu = GetSubMenu(ghSysInfoMenu, SIGMA_SYSINFO_WINDOW_MENU_POS) ;
// Set the system info menu when getting activated
if (lParam == (LPARAM) hWnd)
{
SendMessage(hwndClient,
WM_MDISETMENU,
(WPARAM) ghSysInfoMenu,
(LPARAM) hSysInfoWindowMenu);
//Gray out the system information menu item
EnableMenuItem(ghSysInfoMenu, ID_INFORMATION_SYSTEMINFORMATION, MF_GRAYED);
}
// Set the frame window menu when losing focus
if (lParam != (LPARAM) hWnd)
{
SendMessage(hwndClient,
WM_MDISETMENU,
(WPARAM) ghMainFrameMenu,
(LPARAM) NULL) ;
}
// call DrawMenuBar after the menu items are set
DrawMenuBar(hwndFrame);
状态栏和 MDI 客户端区域的重新排列
状态栏通过在 WinMain()
中调用 CreateStatusBar()
来创建。通过调用 CreateWindowEx
并将类名设置为 STATUSCLASSNAME
来创建状态栏,如下所示。
//Create the status bar
ghwndStatusBar = CreateWindowEx(0, // extended not required
STATUSCLASSNAME, // status bar class name, equivalent to
// "msctls_statusbar32"
"", //caption not required
WS_CHILD | WS_VISIBLE,
-100, // x
-100, // y
10, // width
10, // height
ghwndMainFrame,
NULL,
(HINSTANCE) GetWindowLong (ghwndMainFrame, GWL_HINSTANCE),
NULL);
通过调用 SendMessage(ghwndStatusBar
, SB_SETPARTS, (WPARAM)nParts, (LPARAM)lpParts) 将状态栏划分为三个不同的部分。确切的调用顺序将在下面详细说明。
//Create parts to the status bar
RECT rcClient;
LPINT lpParts = NULL;
int nWidth = 0;
int nParts = SIGMA_STATUSBAR_PARTS;
// Get the coordinates of the parent window's client area.
GetClientRect(ghwndMainFrame, &rcClient);
// Allocate an array for holding the right edge coordinates.
HLOCAL hloc = NULL;
hloc = LocalAlloc(LHND, sizeof(int) * nParts);
lpParts = (int *)LocalLock(hloc);
// Calculate the right edge coordinate for each part, and
// copy the coordinates to the array.
nWidth = rcClient.right / nParts;
for (int i = 0; i < nParts; i++)
{
lpParts[i] = nWidth;
nWidth += nWidth;
}
// Create status bar parts.
SendMessage(ghwndStatusBar, SB_SETPARTS, (WPARAM)nParts, (LPARAM)lpParts);
// Free the array
LocalUnlock(hloc);
LocalFree(hloc);
创建状态栏后,重新排列客户端区域,以防止客户端区域覆盖状态栏。这在框架窗口调整大小时也需要完成。在框架窗口的 WM_SIZE
处理中,不要调用 DefFrameProc
,因为调用 DefFrameProc
会导致 MDI 客户端区域的大小调整并覆盖状态栏。
// re-arrange the client area so that the status bar is always visible
RECT rectStatusBar;
GetClientRect(ghwndStatusBar, &rectStatusBar); // get status bar client area
giStatusBarHeight = rectStatusBar.bottom;
MoveWindow(ghMDIClientArea,
0,
0,
rcClient.right, //width
rcClient.bottom - giStatusBarHeight, //height
true);
可以通过调用 SendMessage
和 SB_SETTEXT
将文本设置到特定的状态栏部分,如下所示。
// Set a message in first part (zero based index) of the status bar
// Each part can have maximum of 127 characters
SendMessage(ghwndStatusBar, SB_SETTEXT,
0, // Part 1 of status based on Zero based index
(LPARAM)"Application started, no error detected.");
添加系统托盘图标
系统托盘图标通过在 WinMain()
中调用 AddSysTrayIcon()
来创建。调用 Shell_NotifyIcon()
将图标添加到系统托盘。
NOTIFYICONDATA nid = {0};
nid.cbSize = sizeof(nid);
nid.uID = SIGMA_SYSTRAY_ICON_ID; // 0 to 12 are reserved and should not be used.
nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE;
nid.hIcon = LoadIcon(ghInstance, MAKEINTRESOURCE(IDI_SIGMA_MAIN_ICON));
strcpy(nid.szTip, "Sigma");
nid.hWnd = ghwndMainFrame;
nid.uCallbackMessage = WM_SIGMA_SYSTRAY_MSG;
//Add the notification to the tray
Shell_NotifyIcon(NIM_ADD, &nid);
在关闭应用程序时,通过调用 Shell_NotifyIcon
(
NIM_DELETE, &nid)
删除系统托盘图标。
使用 Mailslot 进行 IPC
Mailslot 是一种进程间通信机制。其工作方式是,一个进程打开一个邮件槽,第二个进程将其消息写入其中。由第一个进程读取第二个进程写入邮件槽的消息。在此示例中,通过在 WinMain()
中调用 SetupMailslot()
来设置邮件槽。要查看邮件槽通信的实际效果,请注册我另一篇文章中的服务。此服务文章可在以下位置找到:SigmaService.aspx。服务启动后,它将持续在状态栏的第三部分更新此客户端应用程序。服务会用服务的确切状态更新状态栏。
以下是在邮件槽中获取消息所需的步骤:
- 创建邮件槽
- 获取邮件槽信息
- 读取邮件槽消息
以下是客户端获取邮件槽消息的调用。
#define MAILSLOTNAME \\\\.\\mailslot\\sigmamain
.
.
ghSlot = CreateMailslot(TEXT(MAILSLOTNAME),
0, // no maximum message size
MAILSLOT_WAIT_FOREVER, // no time-out for operations
(LPSECURITY_ATTRIBUTES) NULL); // default security
.
.
bResult = GetMailslotInfo(ghSlot, // mailslot handle
(LPDWORD) NULL, // no maximum message size
&dw_MsgSize, // size of next message
&dw_MsgCount, // number of messages
(LPDWORD) NULL); // no read time-out
.
.
// if there are any messages then we read the mailslot
DWORD dw_MsgSize = 0;
DWORD dw_MsgCount = 0;
DWORD dw_MsgRead = 0;
LPTSTR lpszBuffer;
BOOL bResult;
HANDLE hEvent;
OVERLAPPED ov;
hEvent = CreateEvent(NULL, FALSE, FALSE, TEXT("SigmaMailSlot"));
ov.Offset = 0;
ov.OffsetHigh = 0;
ov.hEvent = hEvent;
bResult = GetMailslotInfo(ghSlot, // mailslot handle
(LPDWORD) NULL, // no maximum message size
&dw_MsgSize, // size of next message
&dw_MsgCount, // number of messages
(LPDWORD) NULL); // no read time-out
while (dw_MsgCount != 0) // retrieve all messages
{
// memory for the message.
lpszBuffer = (LPTSTR) GlobalAlloc(GPTR,
dw_MsgSize);
if( NULL == lpszBuffer )
return FALSE;
lpszBuffer[0] = '\0';
bResult = ReadFile(ghSlot,
lpszBuffer,
dw_MsgSize,
&dw_MsgRead,
&ov);
//Write or display the message in lpszBuffer
}
//Call GetMailSlotInfo() again and repeat the above loop to get further messages.
将消息发布到邮件槽需要以下步骤:
- 打开邮件槽文件
- 写入邮件槽
以下是完成此操作所需的调用。
LPTSTR lpszSlotName = TEXT("\\\\.\\mailslot\\sigmamain");
.
.
g_hFile = CreateFile(lpszSlotName,
GENERIC_WRITE,
FILE_SHARE_READ,
(LPSECURITY_ATTRIBUTES) NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
(HANDLE) NULL);
.
.
bResult = WriteFile(g_hFile,
lpszMessage,
(DWORD) lstrlen(lpszMessage) + 1, // add null termination
&dw_MsgWrittenLen,
(LPOVERLAPPED) NULL);
其他说明
- 调用
InitCommonControlsEx()
来初始化通用控件。这对于创建树形控件是必需的。 - 在框架窗口的
WM_SIZE
处理中,不要调用DefFrameProc
,因为调用DefFrameProc
会导致 MDI 客户端区域的大小调整并覆盖状态栏。 - 此应用程序仅在 XP SP3 上进行了测试。
环境
VC++ 6.0, UNICODE, C++, 和 XP SP3。
历史
2009 年 5 月 18 日
- 已更新以下附加功能
- 打开子窗口时更新菜单
- 创建多部分状态栏
- 添加和删除系统托盘图标
- 使用 Mailslot 进行进程间通信