赞
踩
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结构中的,它的不同取值代表了消息处理函数不同的返回值、函数名和参数列表。
可以使用下面一组枚举类型的数据来表示不同的函数类型。
// _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)函数
};
虽然要定义的数字签名远远超过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)
”类型。为了使这一转化方便地进行,下面再定义一个名称为MessageMapFunctions的联合。
// _AFXIMPL.H文件
union MessageMapFunctions
{
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函数如何将接收到的消息交给映射表中记录的具体的消息处理函数,这就是下一小节要解决的问题。
根据处理函数和处理过程的不同,框架程序主要处理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类继承。
到此,框架程序已经有能力创建并管理窗口了。做一个实时显示电脑内存的使用情况的窗口。
这个例子主要用到了GlobalMemoryStatus函数。这个函数能够取得当前系统内物理内存和虚拟内存的使用情况,其原形如下:
void GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer);
其参数是指向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.5s的定时器,然后在WM_TIMER消息到来时调用GlobalMemoryStatus函数获取内存使用信息并更新客户区显示。
原理虽然简单,但目的是介绍框架程序是怎样工作的,所以应该将更多的注意力放在CWnd类处理消息的方式上。下面具体讲述程序的编写过程。
创建一个名为06Meminfo的空Win32 Application工程,更换VC++使用的默认运行期库,使它支持多线程(见3.1.5小节)。为了使用自己设计的框架程序,必须把COMMON目录下的.CPP文件全部添加到工程中,然后再从CWinApp类继承自己的应用程序类,从CWnd类继承自己的窗口类。
具体的程序代码在Meminfo.h和Meminfo.cpp两个文件中。在工程中通过菜单命令“File/New…”新建它们,文件内容如下。
//Meminfo.h文件 #include "_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); // 创建字体 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_TIMER三个消息。这次不必再使用长长的switch/case结构了,直接在消息映射表中添加相关消息映射项即可处理它们。运行程序,自己的类库框架开始工作了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。