blog

枪手亨利

博客园 首页 新随笔 联系 订阅 管理

我想调用 SetWindowsHookEx 来设置 WH_CBT 钩子,但我了解到 MFC 也安装了这个钩子,也就是在一个线程中安装了两次 WH_CBT,这样做能行吗?
Ken Dang
答案是肯定的。只要遵循正确的步骤,你可以安装几个相同类型的钩子。Windows 的钩子是被设计用于一系列类似子类化这样的操作。为了安装钩子,得调用 SetWindowsHookEx 函数,参数为钩子类型和指向钩子过程的指针。SetWindowsHookEx 返回一个指向旧钩子的句柄:
HHOOK hOldHook; // 全局
...
hOldHook = SetWindowsHookEx(WH_CBT, MyCbtProc, ...);

  现在只要发生有钩子事件,Windows 便调用你的钩子过程。当你的过程处理完该事件,则应该用 CallNextHookEx 调用下一个钩子:

LRESULT CALLBACK
MyCbtProc(int code, ...)
{
    if (code==/* whatever */) {
    // do something
    }

    return CallNextHookEx(hOldHook, code, ...);
}

  当然,没有人强迫你调用 CallNextHookEx,但是如果不调用,那么你的程序可能会垮掉。MFC 使用 CBT 钩子来监视窗口的创建。只要一创建窗口,Windows 都会用 HCBT_CREATEWND 调用此 CBT 钩子。MFC 通过子类化窗口来处理 HCBT_CREATEWND,并将它附属到其 CWnd 对象。具体细节比较复杂,这里仅给出一个简版的代码:

// 来自  wincore.cpp 的简化代码
LRESULT _AfxCbtFilterHook(int code, WPARAM wp, ...)
{
    if (code==HCBT_CREATEWND) {
       CWnd* pWndInit = pThreadState->m_pWndInit;
       HWND hWnd = (HWND)wp;
       pWndInit->Attach(hWnd);
       SetWindowLongPtr(hWnd, GWLP_WNDPROC, &AfxWndProcafxWndProc);
    }
    return CallNextHookEx(...);
}

  这里是去粗取精后的代码,MFC 将窗口对象附属到其 HWND 并通过安装 AfxWndProc 对之进行子类化处理。正是通过这种方式,MFC 将 C++ 窗口对象与它们的 HWNDs 联系起来。AfxWndProc 过程的作用是(通过非常曲折的途径)将 WM_XXX 消息路由到你的消息映射处理函数。
  当 Windows 调用 CBT 钩子时,它用 WPARAM 传递 HWND。但 MFC 是如何知道要附属哪个 CWnd 派生对象呢?通过一个全局变量。为了创建窗口,你必须调用 CWnd::Create 或 CWnd::CreateEx。前者调用后者,所以不管怎样都要经过 CWnd::CreateEx 调用。在创建窗口之前, CWnd::CreateEx 安装 CBT 钩子并设置全局变量。代码是这样的:

// 来自  wincore.cpp 的简化代码
BOOL CWnd::CreateEx(...)
{
    AfxHookWindowCreate(this);
    ::CreateWindowEx(...);
    AfxUnhookWindowCreate();
    return TRUE;
}

AfxHookWindowCreate 安装 CBT 钩子 _AfxCbtFilterHook。它还在线程状态中保存窗口对象指针,pThreadState->m_pWndInit。

void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
{
    _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
    pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(
    WH_CBT, _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
    pThreadState->m_pWndInit = pWnd;
}

  考虑到线程状态是一个保存线程级全局变量的地方。所以这个动作点到为止。你的程序调用 CWnd::Create 或者 CWnd::CreateEx。CWnd::CreateEx 安装 CBT 钩子,将一个全局指针赋值给所创建的 CWnd,并且最终调用 ::CreateWindowEx 来真正创建窗口。在创建窗口之后,发送 WM_CREATE 或 WM_GETMINMAXINFO 之类的窗口消息之前—— Windows 用 HCBT_CREATEWND 调用 CBT 钩子。然后 _AfxCbtFilterHook 获得控制并子类化该窗口并将它连接到其 CWnd,MFC 知道使用哪个 CWnd,因为它之前已经将 CWnd 指针保存在 pThreadState->m_pWndInit 中了。很聪明,不是吗?
  在 _AfxCbtFilterHook 将控制返回  Windows 之后,通过将控制交给 OnGetMinMaxInfo 和 OnCreate 处理例程,Windows 向你的窗口发送 WM_GETMINMAXINFO 和 WM_CREATE 消息,MFC 按常规方式处理它们。这是必由之路,因为 HWND 已经被附属到其 CWnd 对象。当 ::CreateWindowEx 将控制返回给 CWnd::CreateEx 的时候,CWnd::CreateEx 调用 AfxUnhookWindowCreate 删除 CBT 钩子并将 pThreadState->m_pWndInit 置为 NULL。之所以要这样处理 CBT,其唯一的理由就是为了监控窗口的创建,以便 MFC 能将 CWnd 连接到它们的 HWNDs,这个钩子只为 ::CreateWindowEx 调用过程而存在。
  机敏的读者可能会问:为什么 MFC 要费那么大的劲——为什么不在 CWnd::CreateEx 中直接附属并子类化窗口?那样做也行得通,只是会有一个问题。CWnd 对象将错过任何从 ::CreateWindowEx 发送的消息——如:WM_GETMINMAXINFO、WM_NCCREATE 以及 WM_CREATE。这个问题的来由是这样的:在创建窗口时 ::CreateWindowEx 无法让你指定窗口过程。你必须在之后进行子类化。但是另一方面,几个消息都已发出。为了处理这些消息,包括早先创建的消息,MFC 不得不在消息被发送之前连接到窗口对象。唯一途径就是使用 CBT 钩子,因为 Windows 正是在它创建窗口之后,发送任何消息之前调用该 CBT 钩子。所以说到底,CBT 钩子的目的是监视窗口的创建,以便在该窗口接收到任何消息之前将 CWnd 对象连接到它们的 HWNDs。
  其实要回答你的问题不需要这么罗嗦,之所以讲这么多主要是为了更好地理解什么时候,在哪里使用 CBT 钩子以及 MFC 为什么使用 CBT 钩子。我还想向你展示 MFC 如何用线程全局变量 m_pWndInit 向钩子函数传递 CWnd 对象。你会经常遇到类似的处理。SetWindowsHookEx 不具备 void* 类型的参数向钩子函数传递信息。如果你的钩子过程需要这种形式的数据,唯一的方式是通过全局变量。其它的大多数情况只要一个常规静态变量即可;你不需要线程专用的全局变量,除非你的数据是线程专用的。MFC 使用线程状态,因为 它为每个线程维护一个单独的窗口映射。该窗口映射为线程保存所有的 CWnds 对象;从而我们能将每个 CWnd 与其 HWND 连接。
  至于多个钩子的情况,只要你愿意,安装多少个钩子都没关系,只是你要记得调用 ::CallNextHook 函数,这样便不会妨碍 MFC。



posted on 2006-01-02 08:18  henry  阅读(7949)  评论(0)    收藏  举报