分享
 
 
 

安装钩子,托管C++中的字符串以其它

王朝vc·作者佚名  2006-01-17
窄屏简体版  字體: |||超大  

C++ At Work 专栏...

安装钩子,托管C++中的字符串及其它

原著:Paul DiLascia

翻译:NorthTibet

下载源代码:CAtWork00601.exe (146KB)

原文出处:Installing a Hook, Strings in Managed C++, and More

我想调用 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。

我正在将一个现有的 C++ 类库转换为托管扩展,以便能在 .NET 框架客户端使用它们。我的代码调用了 API 函数,这些函数需要当前运行模块的 HINSTANCE。我不想使用我的

DLL 的 HINSTANCE;我想让调用者提供 EXE 的 HINSTANCE,该 EXE 调用我的 DLL。我能将 HINSTANCE

声明为一个 IntPtr,但我的基于 .NET 的客户端如何让应用程序的 HINSTANCE 传递给我的函数?在 C# 中是如何做的?

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

之类的框架类型——你得到的模块是错误的。

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

是不能被修改的,因为它们是不可变的,但我有想修改传递的字符串。

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

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.

作者简介

Paul DiLascia 是一名自由作家,软件咨询顾问以及大型 Web/UI 的设计师。他是《Writing Reusable

Windows Code in C++》书(Addison-Wesley, 1992)的作者。业余时间他开发 PixeLib,这是一个 MFC

类库,从 Paul 的网站 http://www.dilascia.com 可以获得这个类库。

.

本文出自 MSDN Magazine

January 2006 期刊,可通过当地报摊获得,或者最好是

订阅

本文由 VCKBASE MTT 翻译

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有