65.9K
CodeProject 正在变化。 阅读更多。
Home

纯Win32 MDI应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.08/5 (12投票s)

2009年4月19日

CPOL

5分钟阅读

viewsIcon

87183

downloadIcon

6458

用一个简单的例子解释了 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 – 此类通过 CSystemInfoDataCSystemInfoView 类分别保存表示数据和视图的成员元素。在此类中创建子窗口。
  • 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);

可以通过调用 SendMessageSB_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。服务启动后,它将持续在状态栏的第三部分更新此客户端应用程序。服务会用服务的确切状态更新状态栏。

以下是在邮件槽中获取消息所需的步骤:

  1. 创建邮件槽
  2. 获取邮件槽信息
  3. 读取邮件槽消息

以下是客户端获取邮件槽消息的调用。

#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.

将消息发布到邮件槽需要以下步骤:

  1. 打开邮件槽文件
  2. 写入邮件槽

以下是完成此操作所需的调用。

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 进行进程间通信
© . All rights reserved.