《Windows 程序设计(第3版)》——6.2 窗口句柄映射

本节书摘来自异步社区《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;
};

《Windows 程序设计(第3版)》——6.2 窗口句柄映射

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。

《Windows 程序设计(第3版)》——6.2 窗口句柄映射

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节。

上一篇:aswing学习笔记2-不规则外框-请教思路


下一篇:民间高手重新设计 Windows 10 资源监视器:一眼就中毒