本节书摘来自异步社区《Windows 程序设计(第3版)》一书中的第6章,第6.2节,作者:王艳平 , 张铮著,更多章节内容可以访问云栖社区“异步社区”公众号查看
6.2 窗口句柄映射
6.2.1 向CWnd对象分发消息
一个线程中可能(很可能)有不止一个窗口,因此也会有多个对应的CWnd对象。每个CWnd对象只响应发送给本窗口的消息,那么,如何将线程接受到的消息交给不同的CWnd对象呢?本节就着重解决这个问题。
Windows是通过窗口函数将消息发送给应用程序的。窗口函数的第一个参数hWnd指示了接收此消息的窗口,我们只能通过窗口句柄hWnd的值找到对应的CWnd对象的地址。这就要求:
(1)只安排一个窗口函数。窗口函数的作用仅仅是找到处理该消息的CWnd对象的地址,再把它交给此CWnd对象。增加窗口函数对寻找CWnd对象不会有帮助,因为窗口函数的参数是固定的。
(2)记录窗口句柄到CWnd对象指针的映射关系。
窗口函数是全局函数,将它命名为AfxWndProc,其实现代码在CWnd类的实现文件WINCORE.CPP中。假设CWnd类用于接收消息的成员函数的名称是WindowProc,则AfxWndProc的伪代码如下。
LRESULT __stdcall AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
CWnd* pWnd = ... // 通过hWnd找到对应的CWnd指针
ASSERT(pWnd != NULL);
ASSERT(pWnd->m_hWnd == hWnd);
... // 将消息交给CWnd对象处理return pWnd->WindowProc(nMsg, wParam, lParam);
}
AfxWndProc是程序中所有窗口的消息处理函数,它先找到管理窗口的CWnd对象,再将消息交给该对象处理,并返回消息的处理结果。图6.1显示了此函数的功能。
解决(2)问题,只要使用CHandleMap类就可以了。由于Windows为每个线程维护一个消息队列,如图6.1所示,线程1执行过程中消息处理函数AfxWndProc只能收到本线程中的窗口发来的消息,所以窗口的句柄映射应该是线程私有的。CWnd类对象和它所控制的窗口都在同一个模块中,因此窗口句柄映射是模块线程私有的。所以最终我们将记录窗口句柄映射的CHandleMap对象定义在模块线程状态类AFX_MODULE_THREAD_STATE中。
class AFX_MODULE_THREAD_STATE : public CNoTrackObject // _AFXSTAT_.H文件
{
... // 其他成员
// 窗口句柄映射
CHandleMap* m_pmapHWND;
};
m_pmapHWND指针所指向的CHandleMap对象记录了本模块内当前线程的窗口句柄映射,这里的当前线程是指访问此变量的线程。下面的函数afxMapHWND用于访问当前线程中窗口句柄映射。
CHandleMap* afxMapHWND(BOOL bCreate = FALSE) // 定义在WINCORE.CPP文件
{
AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
if(pState->m_pmapHWND == NULL && bCreate)
{
pState->m_pmapHWND = new CHandleMap();
}
return pState->m_pmapHWND;
}
系统需要访问当前线程的窗口句柄映射时,只要调用afxMapHWND函数即可。如果仅仅是查询,就将bCreate参数的值设置为FALSE;如果是向映射中添加新项,就要将TRUE传给bCreate参数,此时,afxMapHWND会检查当前线程中的CHandleMap对象是否创建,如果没有就创建它。
CWnd类提供以下4个成员函数来管理窗口句柄映射,这些函数都是先调用afxMapHWND函数得到CHandleMap指针,然后再进行相关操作。
class CWnd : public CCmdTarget // _AFXWIN.H文件
{
... // 其他成员
static CWnd* FromHandle(HWND hWnd);
static CWnd* FromHandlePermanent(HWND hWnd);
BOOL Attach(HWND hWndNew);
HWND Detach();
}
给定窗口句柄hWnd,FromHandle和FromHandlePermanent函数都会试图返回指向CWnd对象的指针。如果没有CWnd对象附加到此窗口句柄上,FromHandle函数会创建一个临时的CWnd对象,并附加到hWnd上,而FromHandlePermanent函数只返回NULL。但是,我们的CHandleMap类并没有实现自动创建临时对象的功能,所以这两个函数的功能没有区别。函数的实现代码如下。
CWnd* CWnd::FromHandle(HWND hWnd) // WINCORE.CPP文件
{
CHandleMap* pMap = afxMapHWND(TRUE); // 如果不存在则创建一个CHandleMap对象
ASSERT(pMap != NULL);
return (CWnd*)pMap->FromHandle(hWnd);
}
CWnd* CWnd::FromHandlePermanent(HWND hWnd)
{
CHandleMap* pMap = afxMapHWND();
CWnd* pWnd = NULL;
if(pMap != NULL)
{
// 仅仅在永久映射(非临时映射)中查找——不创建任何新的CWnd对象
pWnd = (CWnd*)pMap->LookupPermanent(hWnd);
}
return pWnd;
}
这两个函数的实现不与任何CWnd类的对象有关,而且又是负责查询全局(相对于线程)窗口句柄映射的,所以将它们声明为static类型,作为全局函数来使用。
Attach函数附加一个窗口句柄到当前CWnd对象,即添加一对映射项;Detach函数将窗口句柄从当前CWnd对象分离,即移除一对映射项。这些操作都是在永久映射中进行的,其实现代码如下。
BOOL CWnd::Attach(HWND hWndNew) // WINCORE.CPP文件
{
ASSERT(m_hWnd == NULL); // 仅仅附加一次
ASSERT(FromHandlePermanent(hWndNew) == NULL); // 必须没有在永久映射中
if(hWndNew == NULL)
return FALSE;
CHandleMap* pMap = afxMapHWND(TRUE); // 如果不存在则创建一个CHandleMap对象
ASSERT(pMap != NULL);
pMap->SetPermanent(m_hWnd = hWndNew, this); // 添加一对映射
return TRUE;
}
HWND CWnd::Detach()
{
HWND hWnd = m_hWnd;
if(hWnd != NULL)
{
CHandleMap* pMap = afxMapHWND(); // 如果不存在不去创建
if(pMap != NULL)
pMap->RemoveHandle(hWnd);
m_hWnd = NULL;
}
return hWnd;
}
每创建一个窗口,就调用Attach函数将新的窗口句柄附加到CWnd对象,在此窗口销毁的时候再调用Detach函数取消上面的附加行为。这样,在整个窗口的生命周期内,就会存在一个此窗口句柄hWnd到CWnd对象指针pWnd的映射项,在消息处理函数AfxWndProc中,能够轻易地完成图6.1所示的消息分发的功能,如下代码所示。
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); // 通过hWnd找到对应的CWnd指针;
return pWnd->WindowProc(nMsg, wParam, lParam); // 将消息交给CWnd对象处理
6.2.2 消息的传递方式
线程状态类_AFX_THREAD_STATE中,一个很重要的成员的是m_lastSendMsg,这个MSG类型的变量记录了上一次线程收到的消息,也可以说是当前正在处理的消息。维护这个成员的值是很有用的,它提供了一种向CWnd对象发送消息的方法。我们在任何时候都可以通过下面的语句得到当前正在处理的消息。
_AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
MSG msg = pThreadState->m_lastSendMsg; // 变量msg为当前正在处理的消息
用这种方式传递消息避开了使用函数参数的烦琐,而且维护m_lastSendMsg的值也比较容易。线程收到的所有消息都会首先到达消息处理函数AfxWndProc,在消息处理函数将消息交给CWnd对象之前更新当前线程私有变量m_lastSendMsg的值即可。下面是这一过程的具体实现。
// 这两个函数的声明代码在_AFXWIN.H文件中(CWnd类下面),实现代码在WINCORE.CPP文件中
LRESULT __stdcall AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
ASSERT(pWnd != NULL);
ASSERT(pWnd->m_hWnd == hWnd);
return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}
LRESULT AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,
WPARAM wParam = 0, LPARAM lParam = 0)
{
_AFX_THREAD_STATE* pThreadState = AfxGetThreadState();
// 因为可能会发生嵌套调用,所以要首先保存旧的消息,在函数返回时恢复
MSG oldState = pThreadState->m_lastSendMsg;
// 更新本线程中变量m_lastSendMsg的值
pThreadState->m_lastSendMsg.hwnd = hWnd;
pThreadState->m_lastSendMsg.message = nMsg;
pThreadState->m_lastSendMsg.wParam = wParam;
pThreadState->m_lastSendMsg.lParam = lParam;
// 处理接收到的消息
// 将消息交给CWnd对象
LRESULT lResult;
lResult = pWnd->WindowProc(nMsg, wParam, lParam); // 下面要讲述成员函数WindowProc
// 消息处理完毕,在返回处理结果以前恢复m_lastSendMsg的值
pThreadState->m_lastSendMsg = oldState;
return lResult;
}
添加AfxCallWndProc函数是为了让用户能够直接向CWnd对象发送消息。此函数在将消息传给CWnd对象前会更新线程私有数据m_lastSendMsg的值,所以在CWnd对象处理消息的过程中变量m_lastSendMsg就是当前正在处理的消息。这样,消息就以两种不同的方式传给了CWnd对象:第一种方式是通过CWnd::WindowProc函数的3个参数(沿这条线路传递的消息将是以后介绍的重点);第二种方式是通过线程私有数据m_lastSendMsg。
如果你不熟悉消息处理函数的工作机制,可能会以为AfxCallWndProc函数中局部变量oldState的值将永远是0,也就是说在CWnd对象处理完消息以后恢复变量m_lastSendMsg的值是没有必要的。可是事实并不是这样。
在CWnd对象处理消息的过程中,可能会因为调用了某个函数而促使AfxCallWndProc再次被调用,这就会发生函数的嵌套调用。比如在处理一个消息时,又用SendMessage函数向当前窗口发了一条消息,SendMessage会一直等到消息处理完毕才返回。于是程序的执行流程转向对AfxCallWndProc函数的调用,如图6.2所示。这次首先保存旧的消息就有用了,因为这个旧消息就是促使SendMessage函数被调用的消息,而不再是0。
CWnd类的成员函数WindowProc是为了实现CWnd类而添加的,并不是用户可以使用的接口函数,所以将它的保护类型设为protected。为了使全局函数AfxCallWndProc能够访问CWnd类的受保护成员,将它声明为CWnd类的友元函数,相关代码如下。
class CWnd : public CCmdTarget
{
... // 其他成员
protected:
// 处理Windows消息
virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
protected:
// 分发消息的实现
friend LRESULT AfxCallWndProc(CWnd*, HWND, UINT, WPARAM, LPARAM);
};
为了使程序通过编译,要先写出此函数的最简单的实现,比如
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) // WINCORE.CPP文件
{ return 0; }
究竟WindowProc是如何处理消息的,见6.5节。