《Windows 程序设计(第3版)》——6.5 消息处理

本节书摘来自异步社区《Windows 程序设计(第3版)》一书中的第6章,第6.5节,作者:王艳平 , 张铮著,更多章节内容可以访问云栖社区“异步社区”公众号查看

6.5 消息处理

6.5.1 使用消息映射宏
Windows统一用WPARAM和LPARAM两个参数来描述消息的附加信息,例如WM_ CREATE消息的LPARAM参数是指向CREATESTRUCT结构的指针,WPARAM参数没有被使用;WM_LBUTTONDOWN消息的WPARAM参数指定了各虚拟键的状态(UINT类型),LPARAM参数指定了鼠标的坐标位置(POINT类型)。很明显,消息附加参数的类型并不是完全相同的,如果CWnd类也定义一种统一形式的成员来处理所有的消息,将会丧失消息映射的灵活性。

消息映射项AFX_MSGMAP_ENTRY的pfn成员记录了消息映射表中消息映射函数的地址,但它却无法反映出该消息处理函数的类型。试想,CWnd对象的WindowProc函数在调用消息映射表中的函数响应Windows消息时,它如何能够知道向这个函数传递什么参数呢?又如何能够知道该函数是否有返回值呢?所以,仅仅在消息映射表项中记录下消息处理函数的地址是不够的,还应该想办法记录下函数的类型,以便框架程序能够正确地调用它。消息映射项的nSig成员是为达到这个目的而被添加到AFX_MSGMAP_ENTRY结构中的,它的不同取值代表了消息处理函数不同的返回值、函数名和参数列表。

我们可以使用下面一组枚举类型的数据来表示不同的函数类型。

#ifndef __AFXMSG_H__      // _AFXMSG_.H文件。请创建一个这样的文件
#define __AFXMSG_H__

enum AfxSig // 函数签名标识
{
  AfxSig_end = 0,  // 结尾标识

  AfxSig_vv,    // void (void),比如,void OnPaint()函数
  AfxSig_vw,    // void (UINT),比如,void OnTimer(UINT nIDEvent)函数
  AfxSig_is,    // int  (LPTSTR),比如,BOOL OnCreate(LPCREATESTRUCT)函数
};
#endif // __AFXMSG_H__

虽然要定义的数字签名远远超过3个,但仅仅是为了做实验,所以有这几个已经够了。我们可以认为数字签名中的v代表void,w代表UINT,i代表int,s代表指针。有了这些全局变量的声明,在初始化消息映射表时,就能够记录下消息处理函数的类型。比如,CWnd类中处理WM_TIMER消息的函数是:

void OnTimer(UINT nIDEvent);
相关的消息映射项就应该初始化为这个样子:

{ WM_TIMER, 0, 0, 0, AfxSig_vw, (AFX_PMSG)(AFX_PMSGW)(void (CWnd::*)(UINT))&OnTimer },

请注意上面对OnTimer函数类型的转化顺序。在_AFXWIN.H文件中有对AFX_PMSGW宏的定义,应当把它添加到定义CWnd类的地方。

typedef void (CWnd::*AFX_PMSGW)(void);  // 与AFX_PMSG宏相似,但这个宏仅用于CWnd的派生类

首先程序将OnTimer函数转化成“void (CWnd::)(UINT)”类型,再转化成“void (CWnd::) (void)”类型,最后转化成“void (CCmdTarget::*)(void)”类型。

当对应的窗口接收到WM_TIMER消息时,框架程序就会去调用映射项成员pfn指向的函数,即OnTimer函数。但是,在调用之前,框架程序必须把这个AFX_PMSG类型的函数转化成“void (CWnd::*)(UINT)”类型。为了使这一转化方便地进行,下面再定义一个名称为Message MapFunctions的联合。

union MessageMapFunctions      //   _AFXIMPL.H文件
{
  AFX_PMSG pfn;

  void (CWnd::*pfn_vv)(void);
  void (CWnd::*pfn_vw)(UINT);
  int (CWnd::*pfn_is)(LPTSTR);
};

下面的代码演示了如何调用消息映射表中的函数OnTimer,其中lpEntry变量是查找到的指向类中AFX_MSGMAP_ENTRY对象的指针。

union MessageMapFunctions mmf;
mmf.pfn = lpEntry->pfn;
if(lpEntry->nSig == AfxSig_vw)
{
  (this->*mmf.pfn_vw)(wParam);      // 调用消息映射表中的函数
}

CWnd类中为绝大部分Windows消息都安排了消息处理函数。作为示例,我们现在仅处理下面几个消息。

afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);  // WM_CREATE消息
afx_msg void OnPaint();              // WM_PAINT消息
afx_msg void OnClose();            // WM_CLOSE消息
afx_msg void OnDestroy();            // WM_DESTROY消息
afx_msg void OnNcDestroy();            // WM_NCDESTROY消息
afx_msg void OnTimer(UINT nIDEvent);        // WM_TIMER消息

在CWnd类的实现文件中,这些消息处理函数的默认实现代码如下。

int CWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
  return Default();
}
void CWnd::OnPaint()
{
  Default();
}
void CWnd::OnClose()
{
  Default();
}
void CWnd::OnDestroy()
{
  Default();
}
void CWnd::OnNcDestroy()
{
  CWinThread* pThread = AfxGetThread();
  if(pThread != NULL)   
  {
    if(pThread->m_pMainWnd == this)
    {
      if(pThread == AfxGetApp())  // 要退出消息循环?
      {
        ::PostQuitMessage(0); 
      }
      pThread->m_pMainWnd = NULL;
    }
  }

  Default();
  Detach();
  // 给子类做清理工作的一个机会
  PostNcDestroy();
}
void CWnd::OnTimer(UINT nIDEvent)
{
  Default();
}

请注意,只有在类的消息映射表中添加成员函数与特定消息的关联之后,消息到达时框架程序才会调用它们。上面这些消息处理函数除了OnNcDestroy函数做一些额外的工作外,其他函数均是直接调用DefWindowProc函数做默认处理,所以CWnd类的消息映射表中应该有这么一项(说明CWnd类要处理WM_NCDESTROY消息)。

{ WM_NCDESTROY, 0, 0, 0, AfxSig_vv, (AFX_PMSG)(AFX_PMSGW)(int (CWnd::*)(void))&OnNcDestroy },

为了方便向消息映射表中添加消息映射项,再在AFXMSG.H文件中为各类使用的消息映射项定义几个消息映射宏。

#define ON_WM_CREATE() \
  { WM_CREATE, 0, 0, 0, AfxSig_is, \
    (AFX_PMSG)(AFX_PMSGW)(int (CWnd::*)(LPCREATESTRUCT))&OnCreate },
#define ON_WM_PAINT() \
  { WM_PAINT, 0, 0, 0, AfxSig_vv, \
    (AFX_PMSG)(AFX_PMSGW)(int (CWnd::*)(HDC))&OnPaint },
#define ON_WM_CLOSE() \
  { WM_CLOSE, 0, 0, 0, AfxSig_vv, \
    (AFX_PMSG)(AFX_PMSGW)(int (CWnd::*)(void))&OnClose },
#define ON_WM_DESTROY() \
  { WM_DESTROY, 0, 0, 0, AfxSig_vv, \
    (AFX_PMSG)(AFX_PMSGW)(int (CWnd::*)(void))&OnDestroy },
#define ON_WM_NCDESTROY() \
  { WM_NCDESTROY, 0, 0, 0, AfxSig_vv, \
    (AFX_PMSG)(AFX_PMSGW)(int (CWnd::*)(void))&OnNcDestroy },
#define ON_WM_TIMER() \
  { WM_TIMER, 0, 0, 0, AfxSig_vw, \
    (AFX_PMSG)(AFX_PMSGW)(void (CWnd::*)(UINT))&OnTimer },

对消息映射宏的定义大大简化了用户使用消息映射的过程。比如,CWnd类要处理WM_NCDESTROY消息,以便在窗口完全销毁前做一些清理工作,CWnd的消息映射表就应该如下这样编写。

// 初始化消息映射表      // WINCORE.CPP文件
BEGIN_MESSAGE_MAP(CWnd, CCmdTarget)
ON_WM_NCDESTROY()
END_MESSAGE_MAP()

现在,各窗口的消息都被发送到了对应CWnd对象的WindowProc函数,而每个要处理消息的类也都拥有了自己的消息映射表,剩下的事情是WindowProc函数如何将接收到的消息交给映射表中记录的具体的消息处理函数,这就是下一小节要解决的问题。

6.5.2 消息的分发机制
根据处理函数和处理过程的不同,框架程序主要处理如下3类消息。

(1)Windows消息,前缀以“WM”打头,WM_COMMAND例外。这是通常见到的WM CREATE、WM_PAINT等消息。对于这类消息我们安排一个名称为OnWndMsg的虚函数来处理。

(2)命令消息,它是子窗口控件或菜单送给父窗口的WM_COMMAND消息。虽然现在还没有讲述子窗口控件,但菜单总用过吧。这一类消息用名为OnCommand的虚函数来处理。

(3)通知消息,它是通用控件送给父窗口的WM_NOFITY消息。这个消息以后再讨论,这里仅安排一个什么也不做的OnNotify虚函数响应它。

处理这3类消息的函数定义如下。

class CWnd : public CCmdTarget
{
...  // 其他成员
protected:
  virtual BOOL OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult);
  virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam);
  virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
};

为了将CWnd对象接收到的消息传递给上述3个虚函数,应当如下所示改写WindowProc的实现代码。

LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
  LRESULT lResult;
  if(!OnWndMsg(message, wParam, lParam, &lResult))
    lResult = DefWindowProc(message, wParam, lParam);
  return lResult;
}

OnWndMsg函数的返回值说明了此消息有没有被处理。如果没有处理WindowProc发过来的消息,OnWndMsg返回FALSE,WindowProc函数则调用CWnd类的成员函数DefWindowProc做默认处理。最后一个参数pResult用于返回消息处理的结果。

OnWndMsg函数会进而将接收到的消息分发给OnCommand和OnNotify函数。现在先写下这两个函数的实现代码。

BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)
{ 
  return FALSE;
}
BOOL CWnd::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{ 
  return FALSE; 
}

这节我们重点谈论OnWndMsg函数的实现过程,所以让处理命令消息和通知消息的函数仅返回FALSE即可。

假如用户从CWnd类派生了自己的窗口类CMyWnd,然后把要处理的消息写入CMyWnd类的消息映射表中。CWnd::OnWndMsg函数接收到CMyWnd类感兴趣的消息以后如何处理呢?它调用GetMessageMap虚函数得到自己派生类(CMyWnd类)的消息映射表的地址,然后遍历此表中所有的消息映射项,查找CMyWnd类为当前消息提供的消息处理函数,最后调用它。

要想遍历消息映射表查找处理指定消息的消息映射项,用一个简单的循环即可。下面的AfxFindMessageEntry函数具有此功能。

// 声明函数的代码在_AFXWIN.H文件中(CWnd类下面),实现代码在WINCORE.CPP文件中
const AFX_MSGMAP_ENTRY* AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry, 
                      UINT nMsg, UINT nCode, UINT nID)
{
  while(lpEntry->nSig != AfxSig_end)
  {
    if(lpEntry->nMessage == nMsg && lpEntry->nCode == nCode &&
              (nID >= lpEntry->nID && nID <= lpEntry->nLastID))
      return lpEntry;
    lpEntry++;
  }
  return NULL;
}

此函数的第一个参数是消息映射表的地址,后面几个参数指明了要查找的消息映射项。查找成功函数返回消息映射项的地址。有了这个地址,系统就可以调用用户提供的消息处理函数了。具体实现代码如下面的OnWndMsg函数所示。

BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
  LRESULT lResult = 0;

  // 将命令消息和通知消息交给指定的函数处理
  if(message == WM_COMMAND)
  {
    if(OnCommand(wParam, lParam))
    {
      lResult = 1;
      goto LReturnTrue;
    }
    return FALSE;
  }
  if(message == WM_NOTIFY)
  {
    NMHDR* pHeader = (NMHDR*)lParam;
    if(pHeader->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))
      goto LReturnTrue;
    return FALSE;
  }

  // 在各类的消息映射表中查找合适的消息处理函数,找到的话就调用它
  const AFX_MSGMAP* pMessageMap;
  const AFX_MSGMAP_ENTRY* lpEntry;
  for(pMessageMap = GetMessageMap(); pMessageMap != NULL; pMessageMap = pMessageMap->pBaseMap)
  {
    ASSERT(pMessageMap != pMessageMap->pBaseMap);
    if((lpEntry = AfxFindMessageEntry(pMessageMap->pEntries, message, 0, 0)) != NULL)
        goto LDispatch;
  }
  return FALSE;

LDispatch:
  union MessageMapFunctions mmf;
  mmf.pfn = lpEntry->pfn;
  switch(lpEntry->nSig)
  {
  default:
    return FALSE;
  case AfxSig_vw:
    (this->*mmf.pfn_vw)(wParam);
    break;
  case AfxSig_vv:
    (this->*mmf.pfn_vv)();
    break;
  case AfxSig_is:
    (this->*mmf.pfn_is)((LPTSTR)lParam);
    break;  
  }

LReturnTrue:
  if(pResult != NULL)
    *pResult = lResult;
  return TRUE;
}

OnWndMsg函数为所有的Windows消息查找消息处理函数,如果找到就调用它们。但是它不处理命令消息(WM_COMMAND)和通知消息(WM_NOTIFY)。事实上,这两个消息最终会被传给CCmdTarget类,由这个类在自己的派生类中查找合适的消息处理函数。这也是CCmdTarget类居于消息处理顶层的原因。为了使CWinThread及其派生类有机会响应命令消息和通知消息,也要让CWinThread类从CCmdTarget类继承,而不从CObject类继承。

6.5.3 消息映射应用举例
到此,框架程序已经有能力创建并管理窗口了。下面举一个具体的例子来强化对本章内容的理解。例子的源代码在配套光盘的06Meminfo工程下,它的用途是实时显示电脑内存的使用情况,运行效果如图6.5所示。

screenshot

这个例子主要用到了GlobalMemoryStatus函数。这个函数能够取得当前系统内物理内存和虚拟内存的使用情况,其原型如下。

void GlobalMemoryStatus(LPMEMORYSTATUS );
其参数是指向MEMORYSTATUS结构的指针,GlobalMemoryStatus会将当前的内存使用信息返回到这个结构中。

typedef struct _MEMORYSTATUS { 
    DWORD dwLength;       // 本结构的长度。不用你在调用GlobalMemoryStatus之前设置
    DWORD dwMemoryLoad;    // 已用内存的百分比
    SIZE_T dwTotalPhys;       // 物理内存总量
    SIZE_T dwAvailPhys;       // 可用物理内存
    SIZE_T dwTotalPageFile;     // 交换文件总的大小
    SIZE_T dwAvailPageFile;     // 交互文件中空闲部分大小
    SIZE_T dwTotalVirtual;     // 用户可用的地址空间
    SIZE_T dwAvailVirtual;      // 当前空闲的地址空间
} MEMORYSTATUS, *LPMEMORYSTATUS;

MEMORYSTATUS结构反映了调用发生时内存的状态,所以它能够实时监测内存。

06Meminfo实例的实现原理很简单,在处理WM_CREATE消息时安装一个间隔为0.5 s的定时器,然后在WM_TIMER消息到来时调用GlobalMemoryStatus函数获取内存使用信息并更新客户区显示。

原理虽然简单,但目的是介绍框架程序是怎样工作的,所以其应该将更多的注意力放在CWnd类处理消息的方式上。下面具体讲述程序的编写过程。

创建一个名为06Meminfo的空Win32 Application工程,更换VC++使用的默认运行期库,使它支持多线程(见3.1.5小节)。为了使用自己设计的框架程序,必须把COMMON目录下的.CPP文件全部添加到工程中,然后再从CWinApp类继承自己的应用程序类,从CWnd类继承自己的窗口类。

具体的程序代码在Meminfo.h和Meminfo.cpp两个文件中。在工程中通过菜单命令“File/New...”新建它们,文件内容如下。

// -----------------------------------------------Meminfo.h文件-------------------------------------------------//
#include "../common/_afxwin.h"

class CMyApp : public CWinApp
{
public:
  virtual BOOL InitInstance();
};

class CMainWindow : public CWnd
{
public:
  CMainWindow();
protected:
  char m_szText[1024];  // 客户区文本缓冲区
  RECT m_rcInfo;    // 文本所在方框的大小

protected:
  virtual void PostNcDestroy();
  afx_msg BOOL OnCreate(LPCREATESTRUCT);
  afx_msg void OnPaint();
  afx_msg void OnTimer(UINT nIDEvent);
  DECLARE_MESSAGE_MAP()
};
//------------------------------------------------ Meminfo.cpp文件-----------------------------------------------//
#include "Meminfo.h"
#include "resource.h"
#define IDT_TIMER  101

CMyApp theApp;

BOOL CMyApp::InitInstance()
{
  m_pMainWnd = new CMainWindow;
  ::ShowWindow(*m_pMainWnd, m_nCmdShow);
  ::UpdateWindow(*m_pMainWnd);
  return TRUE;
}

CMainWindow::CMainWindow()
{
  m_szText[0] = '\0';
  LPCTSTR lpszClassName = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW, 
            ::LoadCursor(NULL, IDC_ARROW), (HBRUSH)(COLOR_3DFACE+1), 
            AfxGetApp()->LoadIcon(IDI_MAIN));
  CreateEx(WS_EX_CLIENTEDGE, lpszClassName, 
    "内存使用监视器", WS_OVERLAPPEDWINDOW, 
    CW_USEDEFAULT, CW_USEDEFAULT, 300, 230, NULL, NULL);
}

// CMainWindow类的消息映射表
BEGIN_MESSAGE_MAP(CMainWindow, CWnd)
ON_WM_CREATE()
ON_WM_PAINT()
ON_WM_TIMER()
END_MESSAGE_MAP()

BOOL CMainWindow::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
  // 设置显示文本所在方框的大小
  ::GetClientRect(m_hWnd, &m_rcInfo);
  m_rcInfo.left = 30;
  m_rcInfo.top = 20;
  m_rcInfo.right = m_rcInfo.right - 30;
  m_rcInfo.bottom = m_rcInfo.bottom - 30;

  // 安装定时器
  ::SetTimer(m_hWnd, IDT_TIMER, 500, NULL);

  // 将窗口提到最顶层
  ::SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0, 0, 0,
            SWP_NOMOVE | SWP_NOREDRAW | SWP_NOSIZE);
  return TRUE;
}
void CMainWindow::OnTimer(UINT nIDEvent)
{
  if(nIDEvent == IDT_TIMER)
  {
    char szBuff[128];
    MEMORYSTATUS ms;
    
    // 取得内存状态信息
    ::GlobalMemoryStatus(&ms);

    // 将取得的信息放入缓冲区m_szText中
  
    m_szText[0] = '\0';
    wsprintf(szBuff, "\n 物理内存总量: %-5d MB", ms.dwTotalPhys/(1024*1024));
    strcat(m_szText, szBuff);
    wsprintf(szBuff, "\n 可用物理内存: %-5d MB", ms.dwAvailPhys/(1024*1024));
    strcat(m_szText, szBuff);
    
    wsprintf(szBuff, "\n\n 虚拟内存总量: %-5d MB", ms.dwTotalVirtual/(1024*1024));
    strcat(m_szText, szBuff);
    wsprintf(szBuff, "\n 可用虚拟内存: %-5d MB", ms.dwAvailVirtual/(1024*1024));
    strcat(m_szText, szBuff);
    
    wsprintf(szBuff, "\n\n 内存使用率:   %d%%", ms.dwMemoryLoad);
    strcat(m_szText, szBuff);
    
    // 无效显示文本的区域,以迫使系统发送WM_PAINT消息,更新显示信息
    ::InvalidateRect(m_hWnd, &m_rcInfo, TRUE);
  }  
}
void CMainWindow::OnPaint()
{
  PAINTSTRUCT ps;
  HDC hdc = ::BeginPaint(m_hWnd, &ps);

  // 设置背景为透明模式
  ::SetBkMode(hdc, TRANSPARENT);

  // 创建字体
  // CreateFont函数用指定的属性创建一种逻辑字体。这个逻辑字体能够被选入到任何设备中
  HFONT hFont = ::CreateFont(12, 0, 0, 0, FW_HEAVY, 0, 0, 0, ANSI_CHARSET, \
    OUT_TT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, \
    VARIABLE_PITCH | FF_SWISS, "MS Sans Serif" );
  // 创建画刷
  HBRUSH hBrush = ::CreateSolidBrush(RGB(0xa0, 0xa0, 0xa0));
  // 将它们选入到设备环境中
  HFONT hOldFont = (HFONT)::SelectObject(hdc, hFont);
  HBRUSH hOldBrush = (HBRUSH)::SelectObject(hdc, hBrush);

  // 设置文本颜色
  ::SetTextColor(hdc, RGB(0x32, 0x32, 0xfa));
  // 画一个圆角矩形
  ::RoundRect(hdc, m_rcInfo.left, m_rcInfo.top, m_rcInfo.right, m_rcInfo.bottom, 5, 5);
  
  // 绘制文本
  ::DrawText(hdc, m_szText, strlen(m_szText), &m_rcInfo, 0);
  
  // 清除资源
  ::DeleteObject(::SelectObject(hdc, hOldFont));
  ::DeleteObject(::SelectObject(hdc, hOldBrush));
  ::EndPaint(m_hWnd, &ps);
}
void CMainWindow::PostNcDestroy()
{
  delete this;
}

程序很简单,仅处理WM_CREATE、WM_PAINT和WM_TIMER3个消息。这次我们不必再使用长长的switch/case结构了,直接在消息映射表中添加相关消息映射项即可处理它们。运行程序后,自己的类库框架开始工作了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值