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

简介: 一个线程中可能(很可能)有不止一个窗口,因此也会有多个对应的CWnd对象。每个CWnd对象只响应发送给本窗口的消息,那么,如何将线程接受到的消息交给不同的CWnd对象呢?本节就着重解决这个问题。

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

screenshot

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。

screenshot

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

相关文章
|
1月前
|
存储 安全 数据安全/隐私保护
Windows部署WebDAV服务并映射到本地盘符实现公网访问本地存储文件
Windows部署WebDAV服务并映射到本地盘符实现公网访问本地存储文件
270 0
|
存储 分布式计算 安全
VMware 安装CentOS7配置环境、安装虚拟机、选择cd/dvd的方式安装系统、系统安装引导界面、需要定制化的内容、配置磁盘分区、修改主机名、网络配置、修改windows的主机映射文件(host
调整时间差、安装GHOME(图形化界面的方式)注意图上标注的点击顺序、添加boot、添加swap交换分区、配置根(/)目录、编辑VMware的网络配置、Windows的网络配置、虚拟机网络IP修改地址配置、修改主机名和hosts文件、配置Linux克隆机主机名称映射hosts文件,打开/etc/hosts、关闭 kdump本身虚拟机内存就不够,他会吃掉一部分内存,我们尽量省一点、是否打开安全协议(开启与否都可以)、安装时间比较长大概需要10几分钟(设置root用户密码,一定要设置)、创建一个普通用户(可以不
VMware 安装CentOS7配置环境、安装虚拟机、选择cd/dvd的方式安装系统、系统安装引导界面、需要定制化的内容、配置磁盘分区、修改主机名、网络配置、修改windows的主机映射文件(host
|
消息中间件 安全 API
C#实现操作Windows窗口句柄:SendMessage/PostMessage发送系统消息、事件和数据【窗口句柄总结之二】
SendMessage/PostMessage API 可以实现发送系统消息,这些消息可以定义为常见的鼠标或键盘事件、数据的发送等各种系统操作......
3737 1
C#实现操作Windows窗口句柄:SendMessage/PostMessage发送系统消息、事件和数据【窗口句柄总结之二】
|
5月前
|
存储 安全 API
3.5 Windows驱动开发:应用层与内核层内存映射
在上一篇博文`《内核通过PEB得到进程参数》`中我们通过使用`KeStackAttachProcess`附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这次我们将实现一个更加有趣的功能,在某些情况下应用层与内核层需要共享一片内存区域通过这片区域可打通内核与应用层的隔离,此类功能的实现依附于MDL内存映射机制实现。
56 0
3.5 Windows驱动开发:应用层与内核层内存映射
|
6月前
|
存储 缓存 分布式数据库
[笔记]Windows核心编程《十七》内存映射文件(二)
[笔记]Windows核心编程《十七》内存映射文件(二)
|
6月前
|
缓存 Java 编译器
[笔记]Windows核心编程《十七》内存映射文件(一)
[笔记]Windows核心编程《十七》内存映射文件
|
API C# Windows
C#实现操作Windows窗口句柄:常用窗口句柄相关API、Winform中句柄属性和Process的MainWindowHandle问题【窗口句柄总结之三】
本篇主要介绍一些与窗口句柄相关的一些API,比如设置窗口状态、当前激活的窗口、窗口客户区的大小、鼠标位置、禁用控件等,以及介绍Winform中的句柄属性,便于直接获取控件或窗体句柄,以及不推荐...
1683 0
C#实现操作Windows窗口句柄:常用窗口句柄相关API、Winform中句柄属性和Process的MainWindowHandle问题【窗口句柄总结之三】
|
C# Windows 容器
C#面向对象程序设计课程实验二: 实验名称:Windows 窗体程序
C#面向对象程序设计课程实验二: 实验名称:Windows 窗体程序
C#面向对象程序设计课程实验二: 实验名称:Windows 窗体程序
|
存储 缓存 编译器
[笔记]Windows核心编程《十七》内存映射文件
Windows核心编程《十七》内存映射文件
317 0
[笔记]Windows核心编程《十七》内存映射文件
|
API C# Windows
C#实现操作Windows窗口句柄:遍历、查找窗体和控件【窗口句柄最全总结之一】
C#对Windows窗口或窗口句柄的操作,都是通过 P/Invoke Win32 API 实现的,DllImport引入Windows API操作窗口(句柄),可以实现枚举已打开的窗口、向窗口...
2057 0
C#实现操作Windows窗口句柄:遍历、查找窗体和控件【窗口句柄最全总结之一】