關于子類化的話題雖然有些舊,但它至今仍然不失為一種開發Windows的強有力技術,在MFC的內核、甚至.NET的內核中都離不開它,希望本連載能對Windows開發的愛好者有所幫助。
原文標題:Safe Subclassing in Win32
作者:Kyle Marsh
MSDN技術組
點擊此處查看原文
摘要
本文描述了Win32環境下的子類化,描述了它是如何工作的以及實現安全的子類化必須要遵循的規則。本文涵蓋了實例子類化和全局子類化。而超類化則作為一個全局子類化的可選替代方案被介紹。
從Win16到Win32,子類化并沒有發生特別顯著的變化,但是,在Win32中,一個應用程序還是要遵守幾個新的子類化規則。其中最重要(也是最明顯的)就是一個應用程序不能子類化屬于另一個進程的窗口或者類,除非有工作區提供給應用程序使用,否則這條規則不能被打破。
子類化的定義
子
類化是這樣一種技術,它允許一個應用程序截獲發往另一個窗口的消息。一個應用程序通過截獲屬于另一個窗口的消息,從而實現增加、監視或者修改那個窗口的缺
省行為。子類化是用來改變或者擴展一個已存在的窗口的行為、而不用重新開發的有效途徑。想要獲得那些預定義控件窗口類(按鈕控件、編輯控件、列表控件、下
拉列表控件、靜態控件和滾動條控件)的功能而又要修改它們的某些行為的一個便利的方法就是對它們進行子類化。例如,對于一個在對話框中的多行編輯框來說,
當用戶按下Enter鍵時,對話框會關閉。通過對編輯控件子類化,一個應用程序就能擁有一個可以往文本中插入回車和換行,而同時又不會關閉對話框的編輯控件,應用程序不用為這個特殊的需要而去專門開發一個編輯控件。
子類化基礎
創建一個窗口的第一步是填充一個WNDCLASS 結構,并調用RegisterClass 函數來注冊一個窗口類。WNDCLASS 結構的其中一個成員是這個窗口類的窗口過程地址,當一個窗口被建立,32位版本的Microsoft Windows操作系統會取出WNDCLASS 結構中的窗口過程地址,并把它復制到一個新的窗口信息結構中。當一條消息被發往這個窗口時,Windows通過窗口信息結構中的這個地址調用這個窗口的窗口過程。為了子類化一個窗口,你要用一個新的窗口過程地址取代原窗口過程地址,從而使新的窗口過程可以接收發往原窗口過程的所有消息。
當一個應用程序子類化一個窗口時,它可對消息采取三種操作:(1)把消息傳遞給原窗口過程;(2)修改消息然后再傳遞給原窗口過程;(3)不再往下傳遞消息。
應用程序子類化一個窗口時,可以決定什么情況下對所接收的消息做出反應,應用程序可以在將消息傳遞給原窗口過程之前或(和)之后處理這條消息。
子類化的類型
有兩種子類化的類型,它們是實例子類化和全局子類化。.
實例子類化是子類化一個獨立的窗口信息結構,在實例子類化后,只有屬于一個特定的窗口實例的消息會被發送到新窗口過程。
全局子類化是替換一個窗口類的WNDCLASS 結
構中的窗口過程地址,所有在這之后使用該窗口類建立起來的窗口都具有這個被替換的窗口過程地址。全局子類化只對那些在子類化生效之后創建的窗口有效,在進
行子類化之前,如果已經存在任何用這個被全局子類化的窗口類創建的窗口,這些已經存在的窗口不會被子類化。如果應用程序想要使子類化對這些已經存在的窗口
生效,應用程序必須子類化每一個已經存在的該窗口類的實例。
Win32子類化規則
有兩條規則應用到Win32下的實例子類化和全局子類化。
子類化僅被允許用在進程內,一個應用程序不能子類化屬于另一個進程的窗口或窗口類。
這條規則的起因很簡單:Win32進程具有獨立的進程地址空間。在一個特定的進程里,一個窗口過程有一個地址,而在另一個不同的進程里,這個地址值并未指向這個窗口過程,結果就是,在一個進程中,使用從另一個進程獲得的地址替換后的地址并不能獲得期望的結果,因此32位的Windows不允許這種地址替換發生。SetWindowLong 和SetClassLong 函數中防止了這種類型的子類化發生。你不能子類化屬于另一個進程的窗口或窗口類,你能做的就到此為止。
不過,也還是有些途徑能讓你把子類化的功能用到每一個進程上。只要能得到位于某個進程地址空間里的某個函數,你就能該進程里的任何東西進行子類化。有幾個方法可以達到這個目的,其中最容易(也是最不講理的)的一個方法就是在下面這個注冊表鍵中添加一個動態鏈接庫名稱:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\APPINIT_DLLS
這個鍵導致Windows把你的DLL附加到系統里的每一個進程上。你的DLL需要一些能在它要子類化的事件發生時被喚醒的方法,通常一個WH_CBT鉤子可以實現。這個DLL可以監視HCBT_CREATEWND 事件,然后子類化它想子類化的窗口。例子程序CTL3D就使用了WH_CBT鉤子去實現它的子類化,盡管它沒有使用注冊表鍵為每個進程實現子類化,想要得到CTL3D功能的應用程序可以把他鏈接到進程里。
另一個把你的子類化代碼附加到每個進程的方法是使用一個系統鉤子。當一個系統鉤子在另一個進程的上下文中被調用時,系統就把包含這個鉤子的DLL加載到這個進程空間中。樣例CTL3D 的代碼按照使用當前線程中本地WH_CBT 鉤子的方式使用系統WH_CBT 鉤子。
第三個把子類化代碼附加到另一個進程方法更復雜:它使用OpenProcess, WriteProcessMemory和 CreateRemoteThread 函數將代碼注入其它進程。我并不推薦這個方法,并且不打算更詳細地介紹怎么實現這個方法。如果有人堅持要使用這個方法, Jeffrey Richter 曾說過他計劃在Microsoft Systems Journal 中他的一個即將開辟的Win32 Q&A專欄中描述這項技術。
現在很多Windows 3.1的應用程序子類化其它進程來擴展操作和增加一些很酷的功能。當Windows轉向一個面向對象的系統時,對象鏈接和嵌入技術(OLE)提供了更好的辦法去實現這些功能。在Windows的未來版本中,子類化其它進程可能會變得更加困難,而使用OLE也許會更容易。我推薦的是,只要可能,你應該讓你的應用程序轉向OLE,而不要子類化其它進程。
子類化操作不要直接使用原窗口過程地址。
在Win16中,一個應用程序會去使用從SetWindowLong 或 SetClassLong函數返回的窗口過程地址來直接調用窗口過程,畢竟返回值只是一個簡單的指向函數的指針,為什么不使用它呢?在Win32中,這種做法卻是個禁忌。SetWindowLong 或 SetClassLong函數的返回值可能根本不是原窗口過程的指針。Win32可能會返回指向一個數據結構的指針,該數據結構能被用來調用當前的窗口過程。這種情況發生在Windows NT中,當一個應用程序用一個非Unicode的窗口過程子類化一個Unicode窗口時,或者用一個Unicode的窗口過程子類化一個非Unicode窗口時。在這兩種情況下, 操作系統必須為窗口收到的消息執行一個Unicode和ANSI之間的轉換。如果應用程序直接使用指向這個結構的指針調用窗口過程,應用程序會立即導致一個異常。使用SetWindowLong 或 SetClassLong函數返回的窗口地址的唯一做法是將返回值作為參數調用CallWindowProc 函數
實例子類化
SetWindowLong函數用來子類化一個窗口的一個實例。應用程序必須知道子類化函數的地址,子類化函數是這樣一個函數:它用來接收從Windows發來的消息,并把消息傳遞給原窗口過程。子類化函數必須在應用程序中或DLL的模塊定義文件中導出。
應用程序子類化窗口時,使用將要被子類化的窗口的句柄、GWL_WNDPROC標志(在WINDOWS.H中定義)以及新的子類化函數地址作為參數調用函數 SetWindowLong。 函數SetWindowLong返回一個DWORD類型的值,它是窗口的原窗口過程地址,應用程序應該保存該地址以用于將截獲的消息傳遞給原窗口過程,以及將來為窗口移除子類化之用。應用程序使用原窗口過程的地址以及Windows消息所使用的hWnd, Message, wParam和lParam 參數調用函數CallWindowProc 向原窗口過程傳遞消息。通常應用程序只是簡單地把它從Windows接收來的數據傳遞給函數CallWindowProc。
應用程序同時需要原窗口過程地址來為窗口移除子類化。應用程序通過再次調用函數SetWindowLong 來為窗口移除子類化,應用程序向函數傳遞原窗口過程地址、GWL_WNDPROC 標志以及已經被子類化的窗口的句柄。
下面的代碼演示子類化一個編輯框控件以及為它移除子類化:
LONG FAR PASCAL SubClassFunc(HWND hWnd,UINT Message,WPARAM wParam,LONG lParam);
FARPROC lpfnOldWndProc;
HWND hEditWnd;
//
// Create an edit control and subclass it.
// The details of this particular edit control are not important.
//
hEditWnd = CreateWindow("EDIT", "EDIT Test",
WS_CHILD | WS_VISIBLE | WS_BORDER ,
0, 0, 50, 50,
hWndMain,NULL,hInst,NULL);
//
// Now subclass the window that was just Created.
//
lpfnOldWndProc = (FARPROC)SetWindowLong(hEditWnd,GWL_WNDPROC, (DWORD) SubClassFunc);
.
.
.
//
// Remove the subclass for the edit control.
//
SetWindowLong(hEditWnd, GWL_WNDPROC, (DWORD) lpfnOldWndProc);
//
// Here is a sample subclass function.
//
LONG FAR PASCAL SubClassFunc(HWND hWnd,
UINT Message,WPARAM wParam,LONG lParam)
{
//
// When the focus is in an edit control inside a dialog box, the
// default ENTER key action will not occur.
//
if ( Message == WM_GETDLGCODE )
return DLGC_WANTALLKEYS;
return CallWindowProc(lpfnOldWndProc, hWnd, Message, wParam,lParam);
}
潛在的缺陷
只要留意下面的保障規則,實例子類化通常是安全的。
當子類化一個窗口時,你必須了解誰在對窗口的行為負責,例如,Windows負
責它所提供的所有控件的行為,而應用程序則對它自己定義的所有窗口負責。子類化可以作用在同一進程中的任何一個窗口上,但是,當一個應用程序子類化一個不
屬于它自己負責的窗口時,應用程序必須確保子類化函數不會破壞那個窗口原有的行為。由于應用程序并未控制那個窗口,它不應該依賴任何在未來很有可能會被窗
口的擁有者改變的窗口信息。除非確切地知道窗口附加字節或窗口類附加字節的含義以及原窗口過程如何使用它們,否則一個子類化函數不應該使用
它們。退一步說,即使應用程序了解關于窗口附加字節或窗口類附加字節的每一件事,除非應用程序負責這個窗口,否則也不要使用它們。如果一個應用程序使用了
由另一個組件負責的窗口的窗口附加字節,當那個組件決定更新窗口并且改變附加字節的結構時,子類化過程很有可能會失敗。正是因為這個原因,Microsoft 建議你不要子類化控件類,因為Windows負責著這些它所提供的控件,而且下一個版本的Windows可能會改變這些控件的外觀。如果你的應用程序必須子類化一個Windows提供的控件,在新版本的Windows發布后,代碼很可能也需要更新。
由于實例子類化發生在一個窗口被創建之后,應用程序子類化窗口無法向窗口添加任何附加字節,應用程序也許需要將被子類化的窗口的實例所需的所有數據保存在窗口的屬性列表中。
SetProp函數可以把屬性附加到一個窗口。應用程序使用窗口的句柄、一個標示屬性的字符串和屬性數據的句柄作為參數調用SetProp函數。數據的句柄通常從調用LocalAlloc 或 GlobalAlloc 函數獲得。當應用程序要使用一個窗口的屬性列表中的這些數據時,可以使用窗口的句柄和標示屬性的字符串作參數調用GetProp函數,它返回用SetProp函數設置的數據的句柄。當應用程序不再需要這些數據或者當窗口將要被銷毀時,應用程序必須使用窗口的句柄和標示屬性的字符串作參數調用RemoveProp函數從屬性列表中移除這些屬性數據,該函數返回數據的句柄,然后應用程序使用該句柄調用函數LocalFree 或函數GlobalFree。 關于函數SetProp, GetProp和RemoveProp的更多信息,參見平臺SDK文檔。
當一個應用程序子類化一個已經被子類化過的窗口時,所有的子類化會被按照們被設置時的相反順序移除。
全局子類化
全局子類化類似于實例子類化。應用程序通過調用函數SetClassLong對一個窗口類進行全局子類化,就象在實例子類化中一樣,應用程序同樣需要子類化函數的地址,并且這個子類化函數必須在應用程序中或DLL的模塊定義文件中導出。
要全局子類化一個窗口類,應用程序必須擁有一個該類的窗口實例。想要獲得該類的窗口實例,大多數應用程序采
取建立一個屬于將要被全局子類化的窗口類的窗口的方法,當應用程序要移除子類化,也必須有一個窗口句柄,該句柄應該是屬于應用程序要子類化的窗口類的,因
此,為此而專門創建并保存一個窗口是個不錯的辦法。如果應用程序需要創建它所要子類化的窗口類的窗口實例,這個窗口實例通常應該是不可見的。在擁有了一個
正確類型的窗口句柄之后,應用程序可以使用該窗口句柄、GCL_WNDPROC 標志(在WINDOWS.H 中有定義)和新子類化函數的地址作為參數調用函數SetClassLong ,該函數返回一個DWORD 值,該值是該窗口類的原窗口過程地址。原窗口過程地址在全局子類化中的用處和在實例子類化中一樣,窗口消息也象在實例子類化中一樣通過調用函數CallWindowProc傳遞給原窗口過程。應用程序可以通過再次調用SetClassLong函數來從窗口類移除子類化,這時需通過傳遞參數是原窗口過程地址、GCL_WNDPROC 標志和被子類化的窗口類的窗口實例句柄。全局子類化一個控件的應用程序必須在應用程序結束時移除所做的子類化。
下面的代碼演示了全局子類化一個編輯框控件以及為它移除子類化:
LONG FAR PASCAL SubClassFunc(HWND hWnd,UINT,Message,WORD wParam,LONG lParam);
FARPROC lpfnOldClassWndProc;
HWND hEditWnd;

//
// Create an edit control and subclass it.
// Notice that the edit control is not visible.
// Other details of this particular edit control are not important.
//
hEditWnd = CreateWindow("EDIT", "EDIT Test",
WS_CHILD,
0, 0, 50, 50,
hWndMain,
NULL,
hInst,
NULL);

lpfnOldClassWndProc = (FARPROC)SetClassLong(hEditWnd, GCL_WNDPROC, (DWORD)SubClassFunc);
.
.
.
//
// To remove the subclass:
//
SetClassLong(hEditWnd, GWL_WNDPROC, (DWORD) lpfnOldClassWndProc);
DestroyWindow(hEditWnd);

潛在的缺陷
全局子類化具有和實例子類化一樣的限制,除非明確知道原窗口過程如何使用窗口類和窗口實例的附加字節,否則應用程序不應嘗試去使用它們。如果數據必須和一個窗口相關聯,可以象實例子類化中介紹的一樣,使用窗口屬性列表。
在Win32中, 全局子類化不會對任何其它進程中的窗口類或從這些類創建的窗口實例生效,這對于Win16環境是個重大的變化。在系統中,Windows分別為每個Win32進程單獨保存窗口類的信息,可以參見MSDN中的技術文章Window Classes in Win32來了解Windows在這方面的細節。目前全局子類化不能對其它進程生效,這對開發人員來講,是個有用的技術。在Win16中,
全局子類化對被子類化的窗口類的每一個窗口實例都生效:不僅僅是屬于執行了子類化操作的應用程序,還包括了屬于整個系統的,這點讓人感到失望。通常這是應
用程序不想達到的效果,所以應用程序不得不使用更不方便,不好用的方法來改變從系統窗口類創建的窗口實例行為。而現在,在Win32中,使用全局子類化卻是非常容易的。
超類化
子類化一個窗口類,導致原本屬于窗口過程的消息被發送至子類化函數,然后該子類化函數再把消息傳遞給原窗口
過程,而超類化(也被稱作窗口類克隆)是創建一個新的窗口類。這個新窗口類使用一個已經存在的窗口類的窗口過程,來為它自己添加和已經存在的窗口類一樣的
功能,超類化是基于其它窗口類的――也被稱為基類。基類常常是Windows預定義的控件類,但它也可以是任何其它窗口類。
注意 不要超類化滾動條控件類,因為Windows 使用該類名來為滾動條提供標準的行為。
超類化擁有它自己的窗口過程――超類化過程,它能起和子類化函數一樣的作用。超類化過程可以對消息實施三種動作: (1)直接將消息傳遞給原窗口過程 。(2)在傳遞給原窗口過程前修改消息。 (3)不在往下傳遞消息。超類化可以在把消息傳遞給原窗口過程之前、之后或兩者都有的情況下對消息進行操作。
和子類化函數不一樣的是,一個超類化過程也可以從Windows接收創建消息(例如WM_NCCREATE, WM_CREATE 之類的),超類化可以處理這些消息,但它必須把這些消息傳遞給原基類窗口過程,這樣基類窗口過程才能進行初始化操作
應用程序調用函數GetClassInfo 來使一個超類化基于一個基類。函數GetClassInfo 使用從基類的WNDCLASS 結構得來的值填充一個新WNDCLASS結構。然后超類化基類的應用程序把新WNDCLASS結構的hInstance域的值設置成應用程序自己的實例句柄,同時也必須把lpszClassName域的值設置成它要給該超類化的新名稱。如果基類擁有一個菜單,超類化該基類的應用程序必須提供一個新菜單,該新菜單必須和基類的菜單擁有相同的菜單標識。如果該超類化打算處理WM_COMMAND消息的,并且不再把該消息傳遞給基類的窗口過程, 那么菜單的標識可以不必和基類的一樣。函數GetClassInfo 不會返回WNDCLASS結構中域 lpszMenuName, lpszClassName, 和 hInstance 的值。
最后一個必須在超類化的WNDCLASS 結構中設置的是域lpfnWndProc,函數GetClassInfo 用原窗口過程的地址填充它。應用程序必須保存這個地址,以便能用函數CallWindowProc把消息傳遞給基類的窗口過程。應用程序要在WNDCLASS 結構中把該地址值設置成它的超類化過程的地址。這個地址并不是個過程實例地址,因為函數RegisterClass 才能得到過程實例地址。應用程序可以修改WNDCLASS 結構中其它域的值,以便符合應用程序的需要。
應用程序可以往窗口類附加字節和窗口實例附加字節后添加內容,這是因為它注冊了一個新窗口類。當應用程序做這件事時,必須遵從兩個規則: (1) 原類附加字節和窗口實例附加字節不能被子類化覆蓋,這和在實例子類化與全局子類化中的原因一樣。(2) 如果應用程序因自身需要為窗口類或窗口實例添加了附加字節,它在引用這些附加字節時,必須保持是相對于基類所使用的附加字節數來引用的。而且因為某個版本的基類所使用的附加字節數可能會與下一個版本不同,所以超類化自己的附加字節的起始偏移也因基類版本不同而不同。
當填充完WNDCLASS 結構后,應用程序應該調用函數RegisterClass 來注冊新的窗口類,現在,就可以創建并使用屬于該新窗口類的窗口實例了。
應用程序通常是在Win16環境下使用超類化,因為在Win16環境下全局子類化是令人沮喪的。現在在Win32下,
全局子類化不再令人失望,所以超類化就不再那么具有吸引力了。但在你的應用程序要改變一些窗口的行為,而這些窗口又只是從一個系統窗口類所創建的所有窗口
中的一部分時,你仍然可以發現使用超類化是很有用的,相反,對從一個系統窗口類所創建的所有窗口都有效,那是全局子類化的功能。
總結
子類化是個強大的技術,而且在Win32中的使用也沒有發生什么特別重大的改變,唯一的比較主要的變化是你不能再屬于另一個進程的窗口或窗口類,雖然有方法可以繞過這個限制,如果你確實需要這種能力,我還是建議你把你的應用程序移植到OLE,這比仍然依賴子類化更好。
好了,至此整篇文章都翻譯完了(終于趕在放假前弄完了),在Win32中安全的子類化(1)中,提供了本文的英語原文的鏈接,由于本人時間、水平有限,所以歡迎大家指正文中的錯誤和疏漏之處,謝謝!