本节书摘来自异步社区《Windows 程序设计(第3版)》一书中的第6章,第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结构了,直接在消息映射表中添加相关消息映射项即可处理它们。运行程序后,自己的类库框架开始工作了。
相关资源:C 程序设计语言(特别版)--源代码