Windows消息的封装之:this 在哪里(一)

开发随想 专栏收录该内容
15 篇文章 0 订阅

 

一个类的成员函数跟一般函数是不同的,而类的成员函数隐式的包含了一个this指针。

好家伙,一句废话,这是地球人都知道的东西。可是,Windows API 要求的Callback 函数都不是成员函数,而在面向对象的世界里,没有this,日子会过得很苦闷。那么多的Windows Framwork 们,是如何处理这个问题的呢?最近重新翻了一下代码,顺手做个笔记吧。以下是第一集:MFC的实现。

一、背景知识

Windows Framwork 们要解决的最基本的一个问题,就是窗口消息的封装问题。漂亮的窗体对象与消息映射,一下子就把程序员从令人眼花的switch case 中解放出来,不知道给C++程序员们在面对C程序员时带来了多少优越感。下面且让我们从故事的最开始讲起,说一说最初的故事。

Windows 是怎么找到一个窗体的窗口函数的呢?原来,在每一类窗口在系统中注册的时候,都包含了执行窗口函数的指针。这个注册工作是通过RegisterClass API 完成的。

ATOM RegisterClass(
const WNDCLASS* lpWndClass
);

这里的WNDCLASS 结构中,就包含了这个窗口函数的指针。

typedef struct _WNDCLASS {
UINT style;
WNDPROC lpfnWndProc; // 这个就是窗口函数的指针了
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS;

这个窗口函数的原型如下:

LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);

每当消息发给一个窗体的时候,系统自动调用对应的窗口函数。

二、MFC 中的消息封装

在MFC 中,承担WindowProc 任务的是AfxWndProc。这个函数在wincore.cpp。

LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
...

// all other messages route through message map
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);

...

if (pWnd == NULL || pWnd->m_hWnd != hWnd)
    return ::DefWindowProc(hWnd, nMsg, wParam, lParam);
return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}

这个函数最好玩的是这么一句:CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);

好嘛,就是这个FromHandlePermanent(),一下子把窗口类和窗口句柄关联起来了。下面的不用看了。这个FromHandlePermanent()函数怎么实现的呢?我们继续深入看看。

CWnd* PASCAL CWnd::FromHandlePermanent(HWND hWnd)
{
   CHandleMap* pMap = afxMapHWND();
   CWnd* pWnd = NULL;
if (pMap != NULL)
   {
     // only look in the permanent map - does no allocations
     pWnd = (CWnd*)pMap->LookupPermanent(hWnd);
       ASSERT(pWnd == NULL || pWnd->m_hWnd == hWnd);
   }
   return pWnd;
}

原来是查表,通过CHandleMap 来查找CWnd * 和HWND 的关联。现在我们可以差不多猜到MFC在这个问题上的完整思路了:

1、建立一个窗口句柄与窗口类指针的关联表;
2、在AfxWndProc得到系统的每次回调的时候(得到了一个新消息),在上面的关联表中查询这个得到消息的窗口句柄,到底对应哪一个窗口对象;
3、调用AfxCallWndProc,把消息投递到那个窗口对象的消息处理函数里面,从此以后的调用,this 就有了。

好了,现在剩下最后一个问题:这个表是什么时候填充数据的?这是一个很麻烦的事情,因为我们在CreateWindow API 返回的时候,才能知道一个窗口的句柄,在这之前一直是不知道的。但是,这个时候我们再来填充这个关联表,我们肯定就晚了。为什么呢?因为CreateWindow API 在调用过程中,会给新建的窗口发送WM_NCCREATE、WM_CREATE 等消息,假如等CreateWindow 函数返回,我们的窗口对象就拦截不到这些消息了。

所以,MFC使用了一些比较卑鄙的办法^_^ 它给窗口挂了一个WH_CBT 钩子。这个工作是在AfxHookWindowCreate 函数里面实现的,这个函数也在wincore.cpp里面。

void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
{
   _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
   if (pThreadState->m_pWndInit == pWnd)
      return;

if (pThreadState->m_hHookOldCbtFilter == NULL)
{
          pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,
         _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
        if (pThreadState->m_hHookOldCbtFilter == NULL)
          AfxThrowMemoryException();
}

...

   pThreadState->m_pWndInit = pWnd;
}

这个函数中,需要注意的除了挂钩之外,还把当前窗口对象的指针,保存在了pThreadState 里面。这是一个线程局部存储的数据,就是所谓的tls。这个保存起来的指针,就会在AfxCbtFilterHook 函数里面,作为窗口对象的指针用来建立句柄跟对象指针的关联。

为了处理跟输入法的兼容,AfxCbtFilterHook代码又长又臭,我就不全部贴了。贴其中最关键的内容:

    AFX_MANAGE_STATE(pWndInit->m_pModuleState);

    // the window should not be in the permanent map at this time
    ASSERT(CWnd::FromHandlePermanent(hWnd) == NULL);

    // connect the HWND to pWndInit...
    pWndInit->Attach(hWnd);

    ...

    pThreadState->m_pWndInit = NULL;

整个关联工作正是由 pWndInit->Attach(hWnd); 来实现的,关联结束之后,把pThreadState 里面的窗口指针置空,为本线程创建下一个窗口作准备。下面就是Attach 函数的实现了:

BOOL CWnd::Attach(HWND hWndNew)
{
   ASSERT(m_hWnd == NULL);     // only attach once, detach on destroy
ASSERT(FromHandlePermanent(hWndNew) == NULL);
    // must not already be in permanent map

...

   CHandleMap* pMap = afxMapHWND(TRUE); // create map if not exist
   ASSERT(pMap != NULL);

   pMap->SetPermanent(m_hWnd = hWndNew, this);

   ...

   return TRUE;
}

好了,整个流程走到这里就差不多说明白了。这些部分《深入浅出MFC》也有些语焉不详,算一个补充吧。

三、结论

MFC 基本上是通过查表来实现对象指针和窗口句柄的关联的。实话实说,这个映射兼容性很好,但是效率实在不敢恭维,每个消息都要查表,这使得整个框架的效率大受影响。正是由于这个原因,新一代的Windows Frameworks 都采用了更新的(也更猥琐)的关联方法。到底是什么方法呢?且听下回分解。

 

我的百度博客:

http://hi.baidu.com/spaceblog/blog

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值