Safe Subclassing in Win32 - Win32中的安全子類化 from MSDN.net 2003
Kyle Marsh
Microsoft Developer Network Technology Group
Created: January 25, 1994
摘要
這篇文章描述了Win32®環(huán)境下的子類化(subclassing)技術(shù),它怎樣實(shí)現(xiàn),以及為了使子類化安全而必須遵循的規(guī)則(rules)。整篇文章覆蓋了實(shí)例(instance)子類化和全局(global)子類化。超類化(superclassing)被描述為全局子類化的一個可選(alternative)部分。
子類化從Win16到Win32沒有發(fā)生顯著的(dramatically)變化。但是在Win32中有些新的子類化規(guī)則應(yīng)用程序必須遵循。其中最重要的(也是最顯而易見的)是一個application不能子類化一個屬于其他進(jìn)程的窗口和類。這個規(guī)則不能被打破,但這里有某些workarounds(這個詞不會翻譯)應(yīng)用程序可以使用。
Subclassing的定義
子類化是一種允許一個application截獲送往其他窗口的消息的技術(shù)。一個application可以通過截獲供其他窗口使用的消息來增加,監(jiān)視,或者修改一個窗口的默認(rèn)行為(behavior)。子類化對于改變或擴(kuò)展一個窗口的行為是一種快捷高效的方法,而你并不需要重新開發(fā)這個窗口。子類化默認(rèn)的控件窗口類(按鈕控件,編輯控件,列表控件,組合框控件,靜態(tài)控件和滾動條控件)是一種獲得控件的功能并修改它的行為的方便方法。例如,如果一個多行(multiline)編輯控件包含在一個對話框中,并且用戶按下了ENTER鍵,則對話框?qū)⒈魂P(guān)閉。通過子類化這個編輯控件,當(dāng)用戶按下ENTER鍵時(shí),一個程序可以插入一個回車符號并且文本換行而對話框沒有退出。為了一個應(yīng)用程序的這個需要,通過子類化技術(shù),并不需要重新開發(fā)一個編輯控件。
基礎(chǔ)
創(chuàng)建一個窗口的第一步就是填充WNDCLASS 結(jié)構(gòu)并調(diào)用RegisterClass函數(shù)注冊(register)一個窗口類。WNDCLASS結(jié)構(gòu)的;一個元素就是窗口過程(window procedure)的地址。當(dāng)一個窗口被創(chuàng)建,32位版本的微軟Windows™操作系統(tǒng)就讀取WNDCLASS結(jié)構(gòu)中的窗口過程地址并且拷貝其到新窗口的信息結(jié)構(gòu)(information structure)。當(dāng)一個消息被發(fā)送給該窗口,Windows通過保存在窗口的信息結(jié)構(gòu)中的地址調(diào)用相應(yīng)的窗口過程。要子類化一個窗口,你可以通過用新的窗口過程地址替換原來的窗口過程地址來使一個新的窗口過程接收發(fā)送給原始窗口的所有消息。
當(dāng)一個應(yīng)用程序子類化了一個窗口,它可以對消息執(zhí)行三種動作:(1)傳遞這個消息到原始窗口過程;(2)修改這個消息并傳遞它到原始窗口過程;(3)不繼續(xù)傳送該消息。
一個子類化了一個窗口的應(yīng)用程序可以決定什么時(shí)候?qū)λ邮盏降南⒆鞒龇磻?yīng)。這個程序可以在發(fā)送該消息到原始窗口過程之前或者之后,或者之前并且之后處理該消息(The application can process the message before, after or both before and after passing the message to the original window procedure.)
Types of Subclassing子類化的類型
子類化有兩種類型,分別使實(shí)例子類化(instance subclassing)和全局子類化(global subclassing)
例子類化是指子類化一個獨(dú)立窗口的信息結(jié)構(gòu)。使用實(shí)例子類化,只有特定窗口實(shí)例的消息會被發(fā)送到新的窗口過程(it only substitutes the address of window procedure in the window’s information structure, not WNDCLASS structure. We can see from the Topic “The Basics”,each window has its own information structure.)
全局子類化是指替換一個窗口類的WNDCLASS結(jié)構(gòu)中的窗口過程地址.所有后來被創(chuàng)建的這種窗口類的窗口擁有被替換了的窗口過程地址。全局子類化只對子類化發(fā)生后創(chuàng)建的窗口有影響。在子類化時(shí),如果任何該窗口類的窗口已經(jīng)存在,則存在的窗口不受全局子類化的影響。如果程序需要影響已經(jīng)存在的窗口的行為,這個程序必須子類化每個存在的該窗口類的實(shí)例。
Win32 子類化規(guī)則
在Win32中,有兩種子類化規(guī)則適(應(yīng))用于(apply to)實(shí)例和全局子類化。
子類化只允許在一個進(jìn)程中發(fā)生,一個程序不能子類化屬于另一個進(jìn)程的窗口或類。
這條規(guī)則的原因很簡單:Win32進(jìn)程擁有獨(dú)立的地址空間。一個窗口過程擁有其自己的地址。在一個不同的進(jìn)程,窗口進(jìn)程擁有不同的地址。作為結(jié)果,從一個進(jìn)程中替換一個來自另一個進(jìn)程的地址不能帶來期望的結(jié)果,所以32位版本的Windows不允許這種替換發(fā)生(也就是說(that is),從一個不同的進(jìn)程中進(jìn)行子類化)。SetWindowLong和SetClassLong函數(shù)不允許這種類型的子類化。你不能子類化其他進(jìn)程中的窗口或者類。
然而,仍然有某些方法可以使你能對任何進(jìn)程增加子類化功能。一旦你得到一個進(jìn)程的地址空間內(nèi)的一個函數(shù),你可以子類化該進(jìn)程的任何部分。有少數(shù)幾個方法可以達(dá)到這個目的。最簡單(也使最無禮的)方法(approach)是向注冊表的下面這個主鍵添加一個DLL(Dynamic-link library)的名字。
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\ CurrentVersion\Windows\APPINIT_DLLS
這個主鍵導(dǎo)致Windows添加你的DLL到系統(tǒng)中的所有進(jìn)程。在任何你的DLL想要子類化的事件發(fā)生后,你的DLL可能需要某種方法喚醒。WH_CBT鉤子通常可以做到這點(diǎn)。DLL可以監(jiān)視HCBT_CREATEWND事件,然后子類化期望的窗口。CTL3D例子程序使用WH_CBT鉤子來進(jìn)行子類化,不過它沒有包含子類化任何進(jìn)程的注冊表入口。應(yīng)用程序想要實(shí)現(xiàn)CTL3D的可以鏈接它到自己的進(jìn)程中。
另一種添加你的子類化代碼(subclassing code)到任意進(jìn)程的方法是使用一個系統(tǒng)范圍的鉤子。當(dāng)一個系統(tǒng)范圍的鉤子被從另一進(jìn)程的上下文中調(diào)用,系統(tǒng)就把包含這個鉤子的DLL載入該進(jìn)程。CTL3D代碼以同本地WH_CBT鉤子同樣的方式處理系統(tǒng)范圍的WH_CBT鉤子(In fact we called them global hook and thread-specific hook) 。
第三中方法非常復(fù)雜:它包括使用OpenProcess, WriteProcessMemory, and CreateRemoteThread函數(shù)來向其他進(jìn)程注入代碼。我不推薦這種方法,也不想詳細(xì)討論怎么實(shí)現(xiàn)它。對于堅(jiān)持想用這種方法的開發(fā)人員,Jeffrey Richter(我最崇拜的Windows編程大師,技術(shù)作家,《windows核心編程》的作者)告訴我他正打算在他最近的在Microsoft Systems Journal 中的Win32 Q&A(Questions and Answers)專欄中描述這個技術(shù)。
今天,許多Windows 3.1程序子類化其他進(jìn)程來增強(qiáng)該進(jìn)程并增加某些非??岬墓δ堋?/span>Windows正向面向?qū)ο笙到y(tǒng)發(fā)展,對象鏈接和嵌入(OLE)提供了一個實(shí)現(xiàn)該功能更好的方法。在未來版本的Windows中,子類化其他進(jìn)程可能會變得更難,而使用OLE可能變得更容易。如果可能,我推薦你將你的程序轉(zhuǎn)換為OLE,而不是子類化其他進(jìn)程。
子類化的進(jìn)程可能不直接使用原始的窗口進(jìn)程。
在Win16時(shí)代,一個程序可以直接通過 SetWindowLong or SetClassLong的返回值來調(diào)用原始的窗口過程。畢竟,這兩個函數(shù)的返回值就是一個函數(shù)指針,所以為什么不直接調(diào)用它?在Win32時(shí)代,這是絕對不能做的事(definitive no-no). 從SetWindowLong and GetClassLong返回的值可能根本就不是指向之前的窗口過程的地址的指針。這發(fā)生在Window NT™中,當(dāng)一個應(yīng)用程序用一個非Unicode的窗口過程子類化一個Unicode™窗口,或者說一個擁有Unicode窗口過程的非Unicode窗口。在這種情況下,操作系統(tǒng)必須為窗口接收的消息執(zhí)行一個Unicode和ANSI之間的轉(zhuǎn)換。如果一個程序使用指向結(jié)構(gòu)的指針調(diào)用窗口過程,程序?qū)⒘⒓串a(chǎn)生一個異常。使用從SetWindowLong or SetClassLong返回的值調(diào)用窗口過程的唯一方法就是把該返回值作為一個參數(shù)傳給CallWindowProc。
實(shí)例子類化(subclassing a window)
SetWindowLong 用來子類化一個窗口實(shí)例。程序必須擁有子類華函數(shù)(subclass function)的地址。子類化函數(shù)是指從窗口接收消息并傳遞給原始窗口過程的函數(shù)。子類化函數(shù)必須被導(dǎo)出到程序或DLL的模塊定義文件中(The subclass function must be exported in the application's or the DLL's module definition file.).
想要子類化某個窗口的程序使用該窗口的句柄,GWL_WNDPROC選項(xiàng)(在WINDOWS.H中定義), 新的子類化函數(shù)地址來調(diào)用SetWindowLong函數(shù)。SetWindowLong 返回一個DWORD值,這是該窗口的原始窗口過程的地址。程序必須保存這個地址來傳遞截獲的消息給原始窗口過程并且用來從該窗口中移除子類化。通過使用原始窗口過程的地址和窗口消息中的hWnd, Message, wParam, lParam參數(shù)來調(diào)用CallWindowProc ,程序可以將消息發(fā)送給原始的窗口過程。通常,程序只簡單的傳送它從Windows接收到的參數(shù)給CallWindowProc.
程序同樣需要原始窗口過程地址來從窗口移除子類化。程序通過再次調(diào)用SetWindowLong 來從窗口移除子類化。程序傳遞原始窗口過程地址,GWL_WNDPROC 選項(xiàng)和被子類化的窗口句柄句柄給SetWindowLong 函數(shù)。
下面的代碼例子子類化一個編輯控件(edit control)并在之后移除子類化。
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);
}
Potential pitfalls潛在的缺陷
實(shí)例子類化普通情況下是安全的,但注意下面的規(guī)則可以確保安全。
當(dāng)子類化一個窗口,你必須知道由誰來對該窗口的行為負(fù)責(zé)。例如,Windows對它提供的所有控件負(fù)責(zé),而程序?qū)λx的所有窗口負(fù)責(zé)。子類化可對同一進(jìn)程中的任意窗口進(jìn)行,然而,當(dāng)一個程序?qū)σ粋€它不負(fù)責(zé)的窗口進(jìn)行子類化,這個程序必須保證子類化函數(shù)不會破壞該窗口的原始行為(original behavior)。因?yàn)檫@個程序并不控制該窗口,所以不能依賴于任何關(guān)于該窗口的信息,因?yàn)閷υ摯翱谪?fù)責(zé)的組件可能在未來改變。一個子類化函數(shù)不應(yīng)該使用窗口中的額外窗口字節(jié)(extra window bytes)和類字節(jié)(class bytes),除非它確切了解這些字節(jié)的含義和原始窗口過程如何使用它們。即使這個程序?qū)︻~外窗口字節(jié)和類字節(jié)很了解,它也不應(yīng)該使用它們,除非該程序決定更新(update)這個窗口并改變這些額外字節(jié)的某些方面,否則子類化過程很有可能失敗。因?yàn)檫@個原因,Microsoft建議你不要子類化控件類(control classes).Windows對它提供的控件負(fù)責(zé),而控件的某些方面可能隨著Windows版本的改變而改變。如果你的程序必須子類化一個Windows提供的控件,當(dāng)新的Windows版本發(fā)布(release)時(shí),你也許得更新你的代碼。
因?yàn)閷?shí)例子類化(instance subclassing)發(fā)生在窗口被創(chuàng)建后,子類化窗口的程序不能向該窗口增加任何額外字節(jié)(extra bytes)。程序應(yīng)該將需要存儲的數(shù)據(jù)放在被子類化的窗口的屬性列表中(property list)。
可以設(shè)置一個窗口的屬性。程序使用窗口句柄,一個標(biāo)識屬性的字符串,以及一個指向數(shù)據(jù)的句柄來調(diào)用SetProp 函數(shù)。指向數(shù)據(jù)的句柄通常通過調(diào)用LocalAlloc or GlobalAlloc 來得到。當(dāng)一個程序需要使用窗口的屬性列表中的數(shù)據(jù),它可以用該窗口的句柄以及標(biāo)識該屬性的字符串作為參數(shù)來調(diào)用GetProp 函數(shù)。GetProp 返回由SetProp設(shè)置的指向數(shù)據(jù)的句柄,當(dāng)程序使用完這些數(shù)據(jù),或者當(dāng)窗口即將被銷毀,程序必須調(diào)用RemoveProp 來從窗口的屬性列表中移除這些屬性,參數(shù)是窗口的句柄和屬性的字符串標(biāo)識。RemoveProp 返回?cái)?shù)據(jù)的句柄,此時(shí)程序用這些句柄來調(diào)用LocalFree or GlobalFree以釋放內(nèi)存。
如果一個程序子類化一個已經(jīng)子類化了的窗口,則移除子類化時(shí)必須以相反的順序進(jìn)行,即后子類化的先移除。
全局子類化(Subclassing a window class)
全局子類化與實(shí)例子類化相似。程序調(diào)用SetClassLong 來全局的子類化一個窗口類(window class)。與實(shí)例子類化一樣,程序需要子類化函數(shù)的地址,并且子類化函數(shù)必須在程序或DLL的模塊定義文件中導(dǎo)出。
要全局子類化一個窗口類,程序必須擁有該窗口類的一個窗口的句柄。要得到期望的窗口類的窗口句柄,多數(shù)程序建立一個相應(yīng)類的窗口。當(dāng)程序要移除子類化,它需要一個指向它子類化了的窗口類的窗口句柄,因此,為此目的建立并維護(hù)一個窗口是最好的技術(shù)(technique)。如果程序建立一個它想子類化的類的窗口,一般會把該窗口隱藏。在得到正確的窗口類的窗口句柄后,程序用該窗口句柄,GCL_WNDPROC 選項(xiàng)(defined in WINDOWS.H),以及子類化函數(shù)的地址來調(diào)用SetClassLong. SetClassLong 返回一個DWORD值,這是該窗口類的原始窗口過程地址。此時(shí)通過調(diào)用CallWindowProc,程序可以將消息發(fā)送給原始窗口過程。程序可以通過再次調(diào)用SetClassLong移除子類化,只需要向開始那樣,只是把子類化函數(shù)地址換成原始窗口過程地址。
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);

潛在的缺陷
全局子類化和實(shí)例子類化擁有一樣。程序不應(yīng)該嘗試(attempt to)使用窗口類或窗口的額外字節(jié)(extra bytes)。除非它確切(exactly)知道原始過程怎樣使用它們。如果必須在窗口上附著數(shù)據(jù),應(yīng)該向?qū)嵗宇惢菢邮褂么翱趯傩粤斜怼?/font>
在Win32中,全局子類化不影響其他進(jìn)程的類或之前從這些窗口類建立的窗口。這是從Win16環(huán)境過來的非常重要的一個變化。Windows為系統(tǒng)中每個不同的Win32進(jìn)程保持單獨(dú)的窗口類信息。要想了解Windows這方面的細(xì)節(jié),請參閱MSDN庫中"Window Classes in Win32"技術(shù)文章。現(xiàn)在全局子類化不會影響其他進(jìn)程,這成為了開發(fā)者有用的技術(shù)。在Win16中,全局子類化不被鼓勵使用,因?yàn)樗绊懥俗宇惢拇翱陬惖乃写翱冢还馐菆?zhí)行子類化的程序,而是整個系統(tǒng)。這不是程序通常想要的,所以程序只能使用不方便和并不強(qiáng)大(less powerful)的方法來改變系統(tǒng)類創(chuàng)建的窗口的行為。在Win32中使用全局子類化變得非常的簡單。
Superclassing超類化
子類化一個窗口類導(dǎo)致到窗口過程的消息被發(fā)送到子類化函數(shù)(subclass function)。子類化函數(shù)然后把該消息傳遞給原始窗口過程。超類化Superclassing(also known as class cloning)建立一個新的窗口類。新的窗口類使用存在的類的窗口過程,來使新的窗口類具有存在的類的功能(functionality)。超類化使基于其他的窗口類的,已經(jīng)存在的類被稱為base class。
注意
不要超類化滾動條(scroll bar)控件類,因?yàn)?/span>Windows使用該類的名字來正確的處理滾動條的行為。
超類化擁有它自己的窗口過程――超類化過程,它能起和子類化函數(shù)一樣的作用。超類化過程可以對消息實(shí)施三種動作: (1)直接將消息傳遞給原窗口過程 。(2)在傳遞給原窗口過程前修改消息。 (3)不在往下傳遞消息。超類化可以在把消息傳遞給原窗口過程之前、之后或兩者都有的情況下對消息進(jìn)行操作。
和子類化函數(shù)不一樣的是,一個超類化過程也可以從Windows接收創(chuàng)建消息(例如WM_NCCREATE, WM_CREATE 之類的),超類化可以處理這些消息,但它必須把這些消息傳遞給原基類窗口過程,這樣基類窗口過程才能進(jìn)行初始化操作
應(yīng)用程序調(diào)用函數(shù)GetClassInfo 來使一個超類化基于一個基類。函數(shù)GetClassInfo 使用從基類的WNDCLASS 結(jié)構(gòu)得來的值填充一個新WNDCLASS結(jié)構(gòu)。然后超類化基類的應(yīng)用程序把新WNDCLASS結(jié)構(gòu)的hInstance域的值設(shè)置成應(yīng)用程序自己的實(shí)例句柄,同時(shí)也必須把lpszClassName域的值設(shè)置成它要給該超類化的新名稱。如果基類擁有一個菜單,超類化該基類的應(yīng)用程序必須提供一個新菜單,該新菜單必須和基類的菜單擁有相同的菜單標(biāo)識。如果該超類化打算處理WM_COMMAND消息的,并且不再把該消息傳遞給基類的窗口過程, 那么菜單的標(biāo)識可以不必和基類的一樣。函數(shù)GetClassInfo 不會返回WNDCLASS結(jié)構(gòu)中域 lpszMenuName, lpszClassName, 和 hInstance 的值。
最后一個必須在超類化的WNDCLASS 結(jié)構(gòu)中設(shè)置的是域lpfnWndProc,函數(shù)GetClassInfo 用原窗口過程的地址填充它。應(yīng)用程序必須保存這個地址,以便能用函數(shù)CallWindowProc把消息傳遞給基類的窗口過程。應(yīng)用程序要在WNDCLASS 結(jié)構(gòu)中把該地址值設(shè)置成它的超類化過程的地址。這個地址并不是個過程實(shí)例地址,因?yàn)楹瘮?shù)RegisterClass 才能得到過程實(shí)例地址。應(yīng)用程序可以修改WNDCLASS 結(jié)構(gòu)中其它域的值,以便符合應(yīng)用程序的需要。
應(yīng)用程序可以往窗口類附加字節(jié)和窗口實(shí)例附加字節(jié)后添加內(nèi)容,這是因?yàn)樗粤艘粋€新窗口類。當(dāng)應(yīng)用程序做這件事時(shí),必須遵從兩個規(guī)則: (1) 原類附加字節(jié)和窗口實(shí)例附加字節(jié)不能被子類化覆蓋,這和在實(shí)例子類化與全局子類化中的原因一樣。(2) 如果應(yīng)用程序因自身需要為窗口類或窗口實(shí)例添加了附加字節(jié),它在引用這些附加字節(jié)時(shí),必須保持是相對于基類所使用的附加字節(jié)數(shù)來引用的。而且因?yàn)槟硞€版本的基類所使用的附加字節(jié)數(shù)可能會與下一個版本不同,所以超類化自己的附加字節(jié)的起始偏移也因基類版本不同而不同。
當(dāng)填充完WNDCLASS 結(jié)構(gòu)后,應(yīng)用程序應(yīng)該調(diào)用函數(shù)RegisterClass 來注冊新的窗口類,現(xiàn)在,就可以創(chuàng)建并使用屬于該新窗口類的窗口實(shí)例了。
應(yīng)用程序通常是在Win16環(huán)境下使用超類化,因?yàn)樵?/span>Win16環(huán)境下全局子類化是令人沮喪的?,F(xiàn)在在Win32下,全局子類化不再令人失望,所以超類化就不再那么具有吸引力了。但在你的應(yīng)用程序要改變一些窗口的行為,而這些窗口又只是從一個系統(tǒng)窗口類所創(chuàng)建的所有窗口中的一部分時(shí),你仍然可以發(fā)現(xiàn)使用超類化是很有用的,相反,對從一個系統(tǒng)窗口類所創(chuàng)建的所有窗口都有效,那是全局子類化的功能。
總結(jié)
子類化是個強(qiáng)大的技術(shù),而且在Win32中的使用也沒有發(fā)生什么特別重大的改變,唯一的比較主要的變化是你不能再屬于另一個進(jìn)程的窗口或窗口類,雖然有方法可以繞過這個限制,如果你確實(shí)需要這種能力,我還是建議你把你的應(yīng)用程序移植到OLE,這比仍然依賴子類化更好。
Send feedback to Microsoft
© 2003 Microsoft Corporation. All rights reserved.
發(fā)表于 @ 2006年03月18日 20:30:00
(#)