分类: C/C++
2008-03-17 11:37:07
我想调用 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 消息路由到你的消息映射处理函数。 // 来自 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 中了。很聪明,不是吗? Hunter Gerson 好问题!这个问题把我难住了十五分钟。在 MFC 中你可以调用 AfxGetInstanceHandle()。MFC 将 HINSTANCE 存储在其 Application 对象 CWinApp::m_hInstance 中。所以如果你用的是微软 .NET 框架,你也许会想到要察看一下 Application 对象 Application.HInstance 或者 Application 属性等等。为什么不呢?因为 .NET 框架中就没有这些东西。 如果你在框架文档中搜索“hinstance”,你会发现有一个方法叫 Marshal.GetHINSTANCE。文档中是这样描述的,静态的 Marshal 方法需要一个模块(Module)作为参数,你想获得的是这个模块的 HINSTANCE。 // In C# Module m; ... IntPtr h = Marshal.GetHINSTANCE(m); 现在你应该知道如何设置该 HINSTANCE 了——那么到哪里获取模块对象呢?再说一次,你可以察看一下 Application 类,看看有没有象 Application.GetModule 之类的东西。或者也许 Application 派生于模块。可惜不是那样。难道有一个 Module 属性,也不是。嗯,应该说不完全是,有一个 Module 属性,但它不是 Application 属性,而是 Type 的属性。在 .NET 框架中,每个对象都具备一个 Type 属性,而每个 Type 都有一个 Module 属性。Type.Module 表示的是实现该类型的模块。所以获取调用模块的 HINSTANCE 可以这么做: Type t = myObj.GetType(); Module m = t.Module; IntPtr h = Marshal.GetHINSTANCE(m); 你也可以在没有对象实例的情况下用 typeof(C++)来获取类型信息,如:typeof(MyApp)。告诉你的客户一定要使用在调用模块中实现的类型。如果使用某些其它类型——例如,String 之类的框架类型——你得到的模块是错误的。 int ErrMsg::ErrorMessage(CString& msg) const { msg.LoadString(m_nErrId); msg += _T("::Error"); return -1; } 我如何用托管 C++ 重写这个函数,并用 String 替换参数中的 CString?我不知道如何声明参数,如何处理 const,以及如何从资源文件中加载托管 String。我看了文档说 String 是不能被修改的,因为它们是不可变的,但我有想修改传递的字符串。 Sumit Prakash 这个问题涉及到几个方面,所以让我们一个一个来解决。首先是 const 的声明。在 .NET 框架中是没有常量方法这种概念的,所以你要忘掉它,每办法,只能这么做。其次,如何声明新的函数。你确信你要将 CString 修改为 String,但到底用什么样的语法呢?你的函数修改传递的 CString,这就是你使用引用的原因。在 .NET 中,String 确实是不可变的。你不能修改一个 String 的内容。所有修改 String 的方法实际上都返回一个新的 String。例如: String* str = S"hello"; str = str->ToUpper(); String::ToUpper 返回一个新 String,你可以赋值给 str。如果你想修改 String,必须使用另外一个类,也就是 StringBuilder。但这里你是不需要 StringBuilder 的,因为你并不真正修改这个 String,你修改的是引用它的变量。为了弄明白这一点,考虑一下在 C# 中你的函数会是什么样子: int ErrorMessage(ref string msg) { msg = ...; return -1; } msg 参数被声明为 ref,意思是说当 ErrorMessage 修改 msg 时,它修改的是传递的变量,而非 String 对象本身,看下面代码: string str = ""; err.ErrorMessage(ref str); 现在用空串代替引用,str 引用任何 ErrorMessage 给它指定的串。所以在 C# 中,你可以用 ref 参数。但是在 C++ 中没有 ref 关键字,也没有任何托管的 __ref 关键字。C++ 不需要,因为 C++ 已经具备一个引用机制!并且编译器很灵敏,知道如何处理托管代码。你只要记住在 C++ 中,托管对象总是指针或者句柄。只要用 String* 代替 CString 即可(如果你用的 IDE 是具备 C++/CLI 的 Visual Studio 2005,可以直接用 String 代替 CString)。新的声明方法如下: int ErrMsg::ErrorMessage(String*& msg){ msg = "foo"; return -1; } 这样,新函数的参数便是一个对托管 String 指针的引用。如果你想用得暴露一点,甚至可以使用 __gc,比如: ErrorMessage(String __gc * __gc & msg); 在实际的实现中,你不必使用 __gc,因为编译器知道 String,是一个托管类。如果你使用 C++/CLI,便可以在使用引用到句柄的跟踪(tracking reference-to-handle): ErrorMessage(String^% msg); 它的意义更加明确。到此故事还没有完结,因为另外还有一个方法声明 ErrorMessage,那就是使用指针到指针的方式: int ErrMsg::ErrorMessage(String** msg){ *msg = "foo"; return -1; } 即使是在 C++ 中,指针和引用之间的差别是微小的。主要的不同是引用总是必须初始化,不能为 NULL。其它区别主要是语法上的——不论你是使用.还是->反引用。在内部看到的引用都是以指针方式实现的。在 .NET 中,没有指针。万物皆引用。或者说一切都归为一个指针,因为如果你深入到底层的话便可窥见一斑。所以不论是使用引用到指针还是指针到指针,你的 String 参数对于框架以外的世界来说都是一个引用参数。我写了一个 C# 示范程序 RetTest。(参见 Figure 3 和 Figure 4) int ErrMsg::Message1(String*& str) { int len = str->Length; ... } 这样编译没问题,但如果调用者传递 str=NULL,那么它丢出一个异常。你应该重写代码仔细处理 str=NULL 的情况,就像下面这样: int ErrMsg::Message2(String*& str) { if (str==NULL) return -1; ... } 那么,到底使用哪一个呢——指针还是饮用?我个人更喜欢引用(&),因为它反映的是 ref,看起来更简洁,反引用对象时也容易。 CString s; HINSTANCE h = ::GetModuleHandle(_T("MyLib.dll")); // use DLL''s handle s.LoadString(h, id); Figure 3 是全部的实现代码。编译后运行 RefTest 的画面如 Figure 5 所示。与往常一样,更多信息和具体实现细节请参考本文例子程序源代码。 |
作者简介 Paul DiLascia 是一名自由作家,软件咨询顾问以及大型 Web/UI 的设计师。他是《Writing Reusable Windows Code in C++》书(Addison-Wesley, 1992)的作者。业余时间他开发 PixeLib,这是一个 MFC 类库,从 Paul 的网站 可以获得这个类库。 . |
本文出自 的 期刊,可通过当地报摊获得,或者最好是 |