2008年(909)
分类:
2008-05-06 22:34:33
原著:Paul DiLascia
翻译:NorthTibet
下载源代码:CAtWork00601.exe (146KB)
原文出处:Installing
a Hook, Strings in Managed C , and More
我想调用 SetWindowsHookEx 来设置 WH_CBT 钩子,但我了解到 MFC 也安装了这个钩子,也就是在一个线程中安装了两次
WH_CBT,这样做能行吗?
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。
我正在将一个现有的 C 类库转换为托管扩展,以便能在 .NET 框架客户端使用它们。我的代码调用了 API 函数,这些函数需要当前运行模块的 HINSTANCE。我不想使用我的
DLL 的 HINSTANCE;我想让调用者提供 EXE 的 HINSTANCE,该 EXE 调用我的 DLL。我能将 HINSTANCE
声明为一个 IntPtr,但我的基于 .NET 的客户端如何让应用程序的 HINSTANCE 传递给我的函数?在 C# 中是如何做的?
// 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
之类的框架类型——你得到的模块是错误的。
Figure 1 示范了一个简单的 C# 控制台例子,ShowModule,它阐明了这一点。运行画面如 Figure 2 所示。ShowModule
显示的模块信息包括两种类型的 HINSTANCE:应用自身定义了MyApp 类,而 String[](String 数组)类型在
mscorlib 中定义。
Figure 2 模块信息
我要如何将 MFC CString 转换为托管 C 中的 String?我有一个函数是这样的:
int ErrMsg::ErrorMessage(CString& msg) const { msg.LoadString(m_nErrId); msg = _T("::Error"); return -1; }
我如何用托管 C 重写这个函数,并用 String 替换参数中的 CString?我不知道如何声明参数,如何处理 const,以及如何从资源文件中加载托管 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)
RefTest 使用了一个用 C 写的类库。ErrMsg 是一个托管类,它有两个方法,Message1 和
Message2,这两个方法将其 String 参数分别赋值为“Hello, world #1”和“Hello, world #2”,一个使用
String** 另一个使用 String*&。两种方法,不管是调用 Message1 还是 Message2,C# 调用者都必须用 ref
关键字。
“ref”对于两种情况都是必须的。如果你去掉它,便会有“error CS1503: Argument ''1'': cannot convert
from ''string'' to ''ref string''.”错误。注意:用 str=NULL 调用 Message1 是合法的。对于你的
C 函数,str 不是 NULL,它是一个空引用。如果你的函数存取传递的 String,你应该注意这一点。例如:
int ErrMsg::Message1(String*& str) { int len = str->Length; ... }
这样编译没问题,但如果调用者传递 str=NULL,那么它丢出一个异常。你应该重写代码仔细处理 str=NULL 的情况,就像下面这样:
int ErrMsg::Message2(String*& str) { if (str==NULL) return -1; ... }
那么,到底使用哪一个呢——指针还是饮用?我个人更喜欢引用(&),因为它反映的是
ref,看起来更简洁,反引用对象时也容易。
关于声明的问题讲了够多的了,下面是最后一个问题。如何加载资源串?正像你发现的,在 String 类中找不到 LoadString 方法。那是因为
.NET 框架不象 Windiws 那样处理资源,.NET 框架完全采用不同的方法,我在 2002 年 11 月的 MSDN
杂志文章中有过描述(参见:“.NET GUI Bliss: Streamline Your Code and Simplify
Localization Using an XML-Based GUI Language
Parser”),我是这样认为的:“无限的灵活性,但哪怕是一个小小的任务都很繁琐”。
.NET 的处理方式使用文本或 XML 资源文件(.resx)卫星程序集。在 .NET
中有两种资源:字符串和对象。对于字符串来说,你只要创建一个名字=值对( name=value ).txt文件,然后运行 resgen.exe。你的程序要调用 ResourceManager.GetString
来获取字符串。其它的处理包括你得编写一个将对象序列化到 .resx 文件的程序,然后在运行时调用 ResourceManger.GetObject
加载它。具体细节请参考文档或我的文章。在我的我的文章中,我编写 FileRes 类以及一个例子程序 FileResGen 来示范如何做,
FileResGen 大大简化了基于文件的资源处理,如:图像文件(.BMP, .GIF, .JPG等等)。
.NET 处理资源的方式其优点在于更容易本地化。只要翻译文本/资源并将它保存在一个子文件夹中,该文件的名字应该与语言缩写名相同——比如:en
代表 English,fr 代表 Franch,或者 kv 代表 Komi。框架会根据用户系统的 CultureInfo.CurrentUICulture
设置自动加载适当的程序集(MFC 使用类似的基于 GetSystemDefaultUILanguage
的卫星动态连接库来处理本地化)。如果你想在 .NET 领域有所作为,那么得用使用卫星程序集和 ResourceManager
重写你的库代码。但是如果本地化并不是很重要(也许你只是加载内部错误信息,这些信息用户看不到)或者工期很短,那么你仍可以按照老方法从 .RC
文件加载串资源。但你得调用 ::LoadString 或在内部使用 CString,加载字符串,然后将它拷贝到调用者的 String 对象。用
C 写这样的程序是很爽的事情!你可以直接调用 Windows APIs,不用显式使用 P/Invoke,象往常一样使用你最爱的 ATL/MFC
类。因为你要从 DLL 中加载字符串,而不是从应用程序中加载,所以唯一的诀窍是必须显式地告诉 LoadString 使用你的 DLL
HINTANCE:
CString s; HINSTANCE h = ::GetModuleHandle(_T("MyLib.dll")); // use DLL''s handle s.LoadString(h, id);
Figure 3 是全部的实现代码。编译后运行 RefTest 的画面如 Figure 5
所示。与往常一样,更多信息和具体实现细节请参考本文例子程序源代码。
Figure 5 RefTest 运行画面馆
顺祝编程愉快!
您的提问和评论可发送到 Paul 的信箱:cppqa@microsoft.com.
下载本文示例代码
C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...C At Work 专栏...