本节书摘来自异步社区《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所示。
这个例子主要用到了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结构了,直接在消息映射表中添加相关消息映射项即可处理它们。运行程序后,自己的类库框架开始工作了。