问:我想调用 SetWindowsHookEx 来设置一个 WH_CBT 挂钩以进行某些操作。但是我知道 MFC 也安装了该挂钩。这意味着,在一个线程中 WH_CBT 安装了两次。我能这样做吗?1
答:简单的回答是可以的。只要遵循正确的步骤,您就可以安装若干个相同类型的挂钩。Windows® 挂钩旨在以与子类派生类似的雏菊链 (daisy-chain) 方式进行操作。要安装一个挂钩,可以使用挂钩类型以及指向挂钩过程的指针来调用 SetWindowsHookEx。SetWindowsHookEx 返回以前挂钩的句柄:
HHOOK hOldHook; // global ... hOldHook = SetWindowsHookEx(WH_CBT, MyCbtProc, ...);
现在,无论挂钩事件何时发生,Windows 都会调用挂钩过程。当您的过程处理完事件之后,应该通过调用 CallNextHookEx(还有别的吗?)来调用下一个挂钩:
LRESULT CALLBACK MyCbtProc(int code, ...) { if (code==/* whatever */) { // do something } return CallNextHookEx(hOldHook, code, ...); }
当然,没有人强制您调用 CallNextHookEx,但如果不调用,您的应用程序很可能中断。对于基于计算机的训练(computer-based training,CBT)挂钩,MFC 使用它来捕获窗口创建。无论它何时创建一个窗口,Windows 都使用 HCBT_CREATEWND 调用 CBT 挂钩。MFC 通过子类派生该窗口并将其附加到它的 CWnd 对象来处理 HCBT_CREATEWND。详细的代码有点复杂,以下是简化的代码:
// simplified from 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 挂钩时,它将 HWND 作为 WPARAM 传递。但是,MFC 如何才能知道附加哪个 CWnd 派生的对象呢?方法是设置一个全局变量。要创建一个窗口,必须调用 CWnd::Create 或 CWnd::CreateEx。前者调用后者,因此所有路径都通过 CWnd::CreateEx。在创建窗口之前,CWnd::CreateEx 安装 CBT 挂钩并设置一个全局变量。其代码如下所示:
// simplified from 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 or WM_GETMINMAXINFO 的任何消息之前),Windows 使用 HCBT_CREATEWND 调用 CBT 挂钩。然后,_AfxCbtFilterHook 获取控制权并子类派生窗口,并且将其连接到它的 CWnd。因为之前将 CWnd 指针存储在 pThreadState->m_pWndInit 中,所以 MFC 知道使用哪个 CWnd。非常聪明,不是吗?
当 _AfxCbtFilterHook 将控制权返回给 Windows 后,Windows 会将 WM_GETMINMAXINFO 和 WM_CREATE 消息发送到您的窗口,MFC 现在以正规方式处理它,方法是将控制权路由到 OnGetMinMaxInfo 和 OnCreate 处理程序。仅仅由于 HWND 现在附加到它的 CWnd 对象,这才成为可能。当 ::CreateWindowEx 将控制权返回给 CWnd::CreateEx 时,CWnd::CreateEx 调用 AfxUnhookWindowCreate 以移除 CBT 挂钩,并设置 pThreadState->m_pWndInit = NULL。这样处理 CBT 挂钩的唯一原因是要捕获窗口创建,以便 MFC 可以将 CWnd 连接到它的 HWND。该挂钩仅在调用 ::CreateWindowEx 的期间内存在。
聪明的读者可能想知道 MFC 如此麻烦的原因 — 为什么不简单地用 CWnd::CreateEx 附加并子类派生窗口呢?那样做可能非常奏效,但会有一个问题。CWnd 对象将错过发送自 ::CreateWindowEx 的任何消息 — 例如,WM_GETMINMAXINFO、WM_NCCREATE 和 WM_CREATE。问题是,::CreateWindow Ex 不允许您在创建窗口时指定窗口过程。然后,您必须对其进行子类派生。但是到那时为止,已经错过了几个消息。为了处理所有消息(包括早期创建消息),MFC 必须在消息发送之前连接窗体对象。进行此操作的唯一方式是使用一个 CBT 挂钩,原因是 Windows 只在它创建窗口后且在为它发送任何消息前调用 CBT 挂钩。因此,CBT 挂钩旨在捕获窗口创建,以便在窗口接收任何消息之前将 CWnd 对象连接到其 HWND。
虽然这里介绍的内容超出您问题的答案范围,但是了解 MFC 何时、何地以及为什么使用它的 CBT 挂钩是有好处的。我还想展示 MFC 如何使用线程全局变量 m_pWndInit 将 CWnd 传递给挂钩函数。通常,您需要进行一些熟悉的操作。SetWindowsHookEx 没有可以传递到挂钩过程的 void* 参数。如果您的挂钩过程需要数据形式的信息,则传递它的唯一方式是通过一个全局变量。在多数情况下,使用一个常规的静态变量就可以了;如果您的数据不是特定于线程的,则无需使用特定于线程的全局变量。MFC 之所以使用线程状态,是因为它可以维护每个线程的单个窗口映射。窗口映射保留线程的所有 CWnd;它将每个 CWnd 与其 HWND 链接在一起。
但对于多个挂钩而言,只要您记住调用 ::CallNextHook,就可以安装任意数量的挂钩。您不会妨碍到 MFC。
问:我正在将一个现有的 C++ 类库转换为托管扩展,以便将其公开给基于 .NET Framework 的客户端。其中的一些代码调用 API 函数,这些函数需要当前运行模块的 HINSTANCE。我不想使用 DLL 的 HINSTANCE;我希望调用方提供 EXE 的 HINSTANCE 来调用我的 DLL。我可以将 HINSTANCE 参数声明为一个 IntPtr,但基于 .NET 的客户端如何使该应用程序的 HINSTANCE 传到我的函数中呢?例如,它们如何用 C# 完成它呢?2
问得好!这个问题我足足考虑了 15 分钟。在 MFC 中,您可以调用 AfxGetInstanceHandle()。MFC 使用 CWinApp::m_hInstance 将 HINSTANCE 存储在它的 Application 对象中。因此,如果要使用 Microsoft® .NET Framework,您应该考虑看看 Application.HInstance 的 Application 对象,或者一个 Application 属性,或者一些类似的内容。为什么不呢?因为它并不存在!
如果在 Framework 文档中搜索“hinstance”,您会发现有一个方法 Marshal.GetHINSTANCE。嗯,这听起来很有希望了。该静态 Marshal 方法需要您想获取其 HINSTANCE 的模块:
// In C# Module m; ... IntPtr h = Marshal.GetHINSTANCE(m);
现在您知道如何获取 HINSTANCE 了,但是从何处获取该模块呢?您可能想再次查看 Application 类以获取诸如 Application.GetModule 这样的内容。或许从模块派生 Application 也是有意义的。唉,都不行。嗯,也许有一个 Module 属性。不,也不能确定。有一个 Module 属性,但它不是 Application 的属性,而是一个 Type 属性。在 .NET Framework 中,每个对象有一个 Type,而且每个 Type 有一个 Module。Type.Module 表示实现该类型的任何模块。因此,要获取调用模块的 HINSTANCE,您的客户端可以如下编码:
Type t = myObj.GetType(); Module m = t.Module; IntPtr h = Marshal.GetHINSTANCE(m);
您也可以使用 typeof(在 C++ 中是 –typeof)获取不具有对象实例的类型,就像在 typeof(MyApp) 中一样。告诉您的客户一定要使用在调用模块中实现的类型。如果使用其他某种类型(例如,诸如 String 的一种 Framework 类型),您将得到错误的模块。
以下代码显示一个简单的 C# 控制台应用程序 ShowModule,它说明了这一点。
using System; using System.Reflection; using System.Runtime.InteropServices; class MyApp { // main entry point static void Main(string[] args) { ShowModule(typeof(MyApp)); // show module for MyApp (me) ShowModule(args.GetType()); // show module for String[] (CLR) } // Helper to display module info and HINSTANCE given Type. static void ShowModule(Type t) { Module m = t.Module; Console.WriteLine("Module info for type {0}:", t); Console.WriteLine(" Name = {0}", m.Name); Console.WriteLine(" FullyQualifiedName = {0}", m.FullyQualifiedName); Console.WriteLine(" HINSTANCE = 0x{0:x}\n", (int)Marshal.GetHINSTANCE(m)); } } |
图 2 显示它的运行。ShowModule 显示模块信息,这些信息包括用于以下两种类型的 HINSTANCE:在应用程序本身中定义的 MyApp 类,以及在 mscorlib 中定义的 String[](String 数组)类型。
图 2 模块信息
问:如何在托管 C++ 中将一个 MFC CString 转换为 String?我有以下 C++ 函数:
int ErrMsg::ErrorMessage(CString& msg) const { msg.LoadString(m_nErrId); msg += _T("::Error"); return -1; }
如何使用托管 C++ 重新编写它,并在参数列表中使用 String 替换 CString?我不知道如何声明这些参数,如何处理 const,以及如何加载资源文件的托管 String。我知道 String 是无法修改的,因为它们是不可变的,但我想修改传递的字符串。 3
答:这里有若干个问题,让我来逐一说明。首先是 const 声明。由于 .NET Framework 中并没有 const 方法的概念,因此您可以忽略这一点。对不起,这正是它的方式。
接下来,如何声明您的新函数。您知道,想将 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++ 中,托管对象始终是指针或句柄。将 CString 替换为 String*(如果您要使用具有 C++/CLI 的 Visual Studio® 2005,则替换为 String^)。新声明如下所示:
int ErrMsg::ErrorMessage(String*& msg){ msg = "foo"; return -1; }
即,新函数获取对(托管)String 指针的引用。如果您想显式进行此操作,甚至可以使用 __gc,就像在 ErrorMessage(String __gc * __gc & msg) 中一样。实际上,您不需要 __gc,因为编译器知道 String 是一个托管类。如果要使用 C++/CLI,您将使用一个引用到句柄 (reference-to-handle) 的跟踪,就像在ErrorMessage(String^% msg) 中一样。令人惊讶的是,它非常有意义。但是这并不是全部,因为您还能够以另一种方式声明 ErrorMessage。您可以使用指针到指针 (pointer-to-pointer) 的方式,如下所示:
int ErrMsg::ErrorMessage(String** msg){ *msg = "foo"; return -1; }
即使是在 C++ 中,引用和指针之间的差别也是微小的。主要的不同在于,引用必须进行初始化而不能为 NULL。其它区别主要是语法上的 — 是使用 . 还是使用 -> 取消引用该对象。在内部,引用是作为指针实现的。.NET 中没有指针。一切都是引用。如果您进行深入的探究,也可以说一切都是指针。所以不论是使用引用到指针还是指针到指针,您的 String 参数对于 Framework 以外的世界来说都是一个 ref 参数。我编写了一个 C# 示范程序 RefTest(请参见以下代码)。
Mylib.cpp
#using <mscorlib.dll> #include <atlstr.h> // CString using namespace System; using namespace System::Runtime::InteropServices; namespace MyLib { public __gc class ErrMsg { public: // reference-to-pointer int Message1(String*& str) { str = "Hello, world #1"; return 0; } // pointer-to-pointer int Message2(String** str) { *str = "Hello, world #2"; return 0; } // Load string from DLL resource file. // Note explicit module handle. int GetString(String*& str, int id) { CString s; if (s.LoadString(::GetModuleHandle(_T("MyLib.dll")), id)) { str = s; // Copy to caller's string. Compiler knows what to do. return 0; } // Note: need to box id sinve String::Format expects an // Object. str = String::Format("Error: String {0} not found.", __box(id)); return -1; } }; } |
MyLib.rc
using System;
using MyLib; // C++ lib
class MyApp {
// main entry point
static int Main(string[] args) {
ErrMsg em = new ErrMsg();
String str = null;
// Test declaration 1: reference-to-pointer
em.Message1(ref str);
Console.WriteLine("Message1: str = {0}", str);
// Test declaration 2: pointer-to-pointer
em.Message2(ref str);
Console.WriteLine("Message2: str = {0}", str);
// Test old-style resource strings: There are only 2.
for (int i=1; i<=3; i++) {
em.GetString(ref str, i);
Console.WriteLine("Resource String #{0} = {1}", i, str);
}
return 0;
}
}
RefTest 使用了一个用 C++ 编写的库类 ErrMsg。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,它是一个对 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 Framework 处理资源的方式与 Windows 不一样。.NET Framework 采用一个完全不同的方法,我在 MSDN®Magazine 2002 年 11 月的文章中(请参阅 .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 加载它。具体细节请参考本文或我的 GUI 文章。其中,我说明了如何编写 FileRes 类以及 FileResGen 程序,它们可以大大简化基于文件的资源(例如,诸如 .BMP、.GIF 和 .JPG 等图像文件)的处理。
.NET 处理资源的优势在于更容易本地化。只需翻译文本/资源并将它保存在一个子文件夹中,该文件夹的名称应该与地区名相同 — 例如,en 代表 English,fr 代表 Franch,或者 kv 代表 Komi。Framework 会根据用户的 CultureInfo.CurrentUICulture 自动加载适当的程序集(MFC 的操作类似于基于 GetSystemDefaultUILanguage 的附属 DLL 的操作。)如果要在 .NET 领域有所作为,您应该重新编写库,以便使用附属程序集和 ResourceManager。但是,如果本地化不很重要(也许您只是加载用户看不到的内部错误信息)或者项目时间很短,那么您仍可以从 .RC 文件加载旧式字符串资源。但您必须调用 ::LoadString 或在内部使用 CString,以便加载字符串,然后将它复制到调用方的 String 对象。用 C++ 编写这样的程序是件很奇妙的事情!您可以直接调用 Windows 而无需显式使用 P/Invoke,并且象往常一样使用您最爱的 ATL/MFC 类。因为您要从 DLL 中加载字符串,而不是从调用应用程序加载,所以唯一的诀窍是,您必须显式通知 LoadString 使用 DLL 的 HINSTANCE:
CString s; HINSTANCE h = ::GetModuleHandle(_T("MyLib.dll"));
// use DLL's handle s.LoadString(h, id);
图 5 显示了编译并运行 RefTest 的结果。和
以前一样,您可以从 MSDN Magazine Web 站点下载 RefTest 和其他所有代码。
图 5 运行中的 RefTest
祝大家编程愉快!
请将有关 Paul 的问题和意见发送到 cppqa@microsoft.com。
Paul DiLascia 是一名自由软件顾问以及自由 Web/UI 设计师。他撰写了 Windows++: Writing Reusable Windows Code in
C++ (Addison-Wesley, 1992) 一书。Paul 在业余时间开发 PixieLib,它
是一个 MFC 类库,您可以从他的 Web 站点 www.dilascia.com 访问该类库。
1 | Ken Dang |
2 | Hunter Gerson |
3 | Sumit Prakash |
欢迎访问最专业的网吧论坛,无盘论坛,网吧经营,网咖管理,网吧专业论坛
https://bbs.txwb.com
关注天下网吧微信/下载天下网吧APP/天下网吧小程序,一起来超精彩
|
本文来源:vczx 作者:佚名