我想調用 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 的信箱:[email protected].
本文配套源碼