Delphi動(dòng)態(tài)事件深入分析
核心提示:本實(shí)驗(yàn)證明了在類(lèi)中方法的調(diào)用時(shí)候,所有的方法都隱含了一個(gè)Self參數(shù),并且該參數(shù)作為對(duì)象方法的第一個(gè)參數(shù)傳遞...
首先做一個(gè)空窗體,放入一Button。
在implementation下面聲明兩個(gè)方法如下:
//外部方法,只聲明一個(gè)參數(shù),此時(shí)按照標(biāo)準(zhǔn)的對(duì)象內(nèi)部事件方法TNotifyEvent聲明,此聲明中,Sender則對(duì)應(yīng)為產(chǎn)生該事件的對(duì)象指針。 |
procedureExtClick1(Sender: TObject); |
showmessage(TComponent(Sender).Name); |
//外部方法,聲明兩個(gè)參數(shù),用來(lái)證明,對(duì)象在調(diào)用時(shí)候會(huì)傳遞一個(gè)Self指針,此時(shí)我們假設(shè)Frm是通過(guò)類(lèi)對(duì)象傳遞過(guò)來(lái)的Self指針,而Sender為產(chǎn)生該事件的對(duì)象指針 |
procedureExtClick(Frm: TObject;Sender: TObject); |
showmessage(TComponent(Sender).Name); |
//然后在 ‘指定調(diào)用’按扭事件中寫(xiě)代碼: |
procedureTForm1.Button1Click(Sender: TObject); |
showmessage(TComponent(Sender).Name); |
//很顯然運(yùn)行的時(shí)候,點(diǎn)該按扭得到的是返回一個(gè) 消息內(nèi)容為 ‘Button1’的對(duì)話(huà)框,這是調(diào)用Form1類(lèi)的對(duì)象事件觸發(fā)的方法。 |
//在調(diào)用 ‘調(diào)用Form類(lèi)外部方法觸發(fā)事件’ Click事件中寫(xiě) |
procedureTForm1.Button2Click(Sender: TObject); |
ExtClickEvent: TNotifyEvent; |
integer(@ExtClickEvent) := integer(@ExtClick1); |
//將ExtClickEvent地址指針指向外部函數(shù)ExtClick1方法的地址 |
Button1.OnClick := ExtClickEvent; |
//將該地址賦值給 Button1的OnClick事件替換以前的OnClick事件 |
procedureTForm1.Button3Click(Sender: TObject); |
Button1.OnClick := Button1Click;//還原為對(duì)象內(nèi)觸發(fā)事件函數(shù) |
運(yùn)行之后
點(diǎn)一下 ‘調(diào)用Form類(lèi)外部方法觸發(fā)事件’ ,然后在點(diǎn) ‘指定調(diào)用’按扭,
showmessage(TComponent(Sender).Name);返回的值是 ‘Form1’,此時(shí)是否就已經(jīng)說(shuō)明了其第一個(gè)參數(shù)是否就是傳遞的一個(gè)Self指針呢。所以在調(diào)用Button.Click事件的時(shí)候傳遞過(guò)來(lái)的第一個(gè)參數(shù)為Form1內(nèi)部的Self指針,而該指針是指向Form1的。此時(shí),我們?cè)谠摵瘮?shù)的
Begin位置放下一個(gè)斷點(diǎn),程序運(yùn)行時(shí)候,此處的斷點(diǎn)為非可用的,如下圖:
說(shuō)明程序在Begin處根本沒(méi)有處理其他任何代碼,此時(shí),將斷點(diǎn)調(diào)到
showmessage(TComponent(Sender).Name);然后點(diǎn) 按扭 程序運(yùn)行到斷點(diǎn)處停下
調(diào)出CPU View窗口查看代碼如下
注意 EAX,EBX,EDX,ECX的值,首先一條是
Mov eax,[eax+$08] //該條指令將對(duì)象的Name屬性值傳遞到Eax中
Call ShowMessage //此函數(shù)需要一個(gè)參數(shù),Delphi的參數(shù)傳遞規(guī)則為EAX,EDX,ECX
如此可見(jiàn),沒(méi)有任何多余的處理,但是此時(shí)還不能證明Eax傳遞過(guò)來(lái)的就是類(lèi)對(duì)象的Self指針
此時(shí)將 ‘調(diào)用Form類(lèi)外部方法觸發(fā)事件’ Click事件中代碼的函數(shù)換成
ExtClick
既將 integer(@ExtClickEvent) := integer(@ExtClick1);
換成 integer(@ExtClickEvent) := integer(@ExtClick);
然后重新重復(fù)上面的步驟,在ExtClick的Begin處下斷點(diǎn),程序運(yùn)行到斷點(diǎn)處停下,則說(shuō)明
程序在Begin時(shí)候有代碼執(zhí)行,打開(kāi)CPU View查看如下:
可見(jiàn)在Begin之后,ShowMessage函數(shù)之前,有兩段代碼如下:
Push ebx //保存Ebx的值
Mov ebx,eax //將Eax的值暫時(shí)存放到Ebx中
然后主要看下面的showmessage(TComponent(Sender).Name);一句
可見(jiàn) 其匯編代碼如下:
Mov eax,[edx+$08]
Call ShowMessage
和以前相比 Mov eax,[eax+$08] 變成了 Mov eax,[edx+$08]
此時(shí),然后運(yùn)行,得到結(jié)果為T(mén)Component(Sender).Name 的值為Button1
而下面的代碼
if Frm is TForm then
TForm(Frm).Close;
則充分證明了EAX的值是 Form1,則說(shuō)明了對(duì)象方法在調(diào)用的時(shí)候會(huì)傳遞一個(gè)隱含的Self指針,而該指針的值在EAX中.
由于Delphi中參數(shù)的傳遞為
EAX 第一個(gè)參數(shù)
EDX 第二個(gè)參數(shù)
ECX 第三個(gè)參數(shù)
所以可知道,真正的觸發(fā)事件的按扭對(duì)象存放在EDX中.
所以我們可以得到如下結(jié)論
在 按扭的單擊事件中,
TNotifyEvent = procedure(Sender: TObject) of object;
其真正的實(shí)體為procedure(當(dāng)前聲明引起的對(duì)象Self,Sender: TObject)
所以 Button.OnClick的時(shí)候,其實(shí)傳遞方式如下
Button1.OnClick(Self,Sender);
其他事件方法等,依次類(lèi)推.
然后根據(jù)該結(jié)論,則我們可以不在受
為Form中的某個(gè)控件對(duì)象指定事件方法的時(shí)候受到 Of Object 那個(gè)東西的限制,可以將事件方法指定到任何地方了。只要注意,該方法對(duì)應(yīng)的參數(shù)要比其事件方法(Of Object)指定的方法多一個(gè)參數(shù)聲明,則可
比如,此時(shí),我們拿窗體關(guān)閉事件做文章:
新建一個(gè)按扭,寫(xiě)代碼
procedureTForm1.Button4Click(Sender: TObject); |
integer(@CloseEvent) := integer(@MyCloseEvent); |
self.OnClose := CloseEvent; |
窗體關(guān)閉的事件方法為
TCloseEvent = procedure(Sender: TObject;Var action: TCloseAction) of Object;
從上面結(jié)論我們知道可以聲明一個(gè)外部函數(shù),該外部函數(shù)的參數(shù)要比TCloseEvent的參數(shù)多一個(gè)Self指針的,所以我們聲明如下:
procedure MyCloseEvent(Frm: TForm;Sender: TObject;var Action: TCloseAction);
Frm則是外部在窗體關(guān)閉的時(shí)候,傳遞的隱含指針Self
該函數(shù)整體代碼如下:
procedureMyCloseEvent(Frm: TForm;Sender: TObject;varAction: TCloseAction); |
showmessage(Frm.Name+'窗體外部方法調(diào)用,不允許關(guān)閉窗體!'); |
點(diǎn)一下,新建的按扭之后,看看是否還可以關(guān)閉窗體??!
通過(guò)匯編來(lái)處理
procedureTForm1.SetEvent(Event: pointer); |
mov ebx,eax //將當(dāng)前的eax的值,先用ebx保存起來(lái),eax中保存的為Form的開(kāi)始地 |
mov eax,edx //將Event指針的值給EAX |
mov [ebx+$2d8],eax //將Eax的值分別寫(xiě)進(jìn)其高位和低位 |
//由于前面我們已經(jīng)證明了,在類(lèi)之中的方法,其傳遞的時(shí)候,都會(huì)有一個(gè)隱含的參數(shù)Self,所以,該段匯編代碼中我們就知道了Event參數(shù)對(duì)應(yīng)應(yīng)該是Edx寄存器,而不是Eax寄存器了。然后,后面有[ebx+$2d8]這樣的內(nèi)容,這個(gè)是窗體 OnClose事件所在位置的地址。可以通過(guò)CpuView窗口查看得到,暫時(shí)沒(méi)有想到如何通過(guò)指定一個(gè) 事件名稱(chēng)來(lái)得到該事件在內(nèi)存中的地址。如果這樣的話(huà),那么則可以寫(xiě)一個(gè)函數(shù)
ReSetObjEvent(EventName: string;EventValue: pointer);
先通過(guò)EventName找到事件地址,然后再通過(guò)上面的則可以寫(xiě)出一個(gè)簡(jiǎn)單通俗易懂的公用函數(shù)了。
否則只能通過(guò)傳遞地址,根據(jù)改變地址中的值來(lái)修改事件函數(shù)的指向了。如下:
寫(xiě)一個(gè)專(zhuān)門(mén)用來(lái)重設(shè)置事件方法的函數(shù)如下:
procedureReSetObjEvent(OldEventAddress: Pointer;NewEventValue: pointer); |
其實(shí)也就是 改變存放事件方法指針的內(nèi)存塊的數(shù)據(jù)值,使其變成另一個(gè)值。
注意,參數(shù)一指定為存放舊事件方法指針的內(nèi)存地址,所以他應(yīng)該是一個(gè)指針的指針了。
參數(shù)二指定為事件方法指針值。
調(diào)用方法如下:
比如,指定窗體的 OnClose事件方法指針為窗體類(lèi)外部定義的函數(shù)。
ReSetObjEvent(@(integer(@Form1.onClose)),@MyCloseEvent)
例如:
procedureFrmClose(Frm: TForm;Sender: TObject;VarAction: TCloseAction); |
showmessage('調(diào)用外部方法,不許關(guān)閉!'); |
procedureTForm1.BitBtn1Click(Sender: TObject); |
ReSetObjEvent(@(integer(@self.OnClose)),@frmClose); |
續(xù)言:
以上在Delphi7下測(cè)試通過(guò),至于2007下,我測(cè)試,也傳遞了一個(gè)隱含參數(shù),但是該隱含參數(shù)不是Self
再論:
經(jīng)過(guò)Cnpack的劉嘯提醒之后,發(fā)現(xiàn)了Delphi7下測(cè)試通過(guò),而2007下不通過(guò)的原因是在于D7下如下聲明:
procedureTForm1.Button4Click(Sender: TObject); |
integer(@CloseEvent) := integer(@MyCloseEvent); |
self.OnClose := CloseEvent; |
此時(shí)2007下該段程序運(yùn)行不能通過(guò)而D7編譯運(yùn)行可以通過(guò),實(shí)在確實(shí)是一個(gè)巧合了。
通過(guò)提示得知,TCloseEvent在Delphi中被稱(chēng)為對(duì)象方法,而對(duì)象方法
在 Delphi 中用 procedure(Sender: TObject) of object; 這種格式聲明的 事件(Event) 類(lèi)型實(shí)際上是同時(shí)包含有對(duì)象和函數(shù)的記錄。我們可以把一個(gè) TNotifyEvent 的變量強(qiáng)制轉(zhuǎn)換成 TMethod:
例如我們聲明了一個(gè)方法 MainForm.BtnClick 并將它賦值給 btn1.OnClick 事件,實(shí)際上是將 MainForm 對(duì)象和 BtnClick 方法地址分別作為 TMethod 結(jié)構(gòu)的 Data 和 Code 成員賦值給 btn1.OnClick 事件屬性。當(dāng) btn1 按鈕調(diào)用這個(gè) BtnClick 事件時(shí),實(shí)際上是將 TMethod 結(jié)構(gòu)的 Data 作為第一個(gè)參數(shù)去調(diào)用 Code 函數(shù)。
我們可以編寫(xiě)下面的代碼:
procedureMyClick(Self: TObject; Sender: TObject); |
ShowMessage(Format('Self: %d, Sender: %s', [Integer(Self), Sender.ClassName])); |
procedureTForm1.FormCreate(Sender: TObject); |
M.Data := Pointer(325); // 隨便取的數(shù) |
btn1.OnClick := TNotifyEvent(M); |
這樣就可以將一個(gè)普通函數(shù)賦值給對(duì)象事件屬性了。
我們?cè)賮?lái)看看 TLanguages.Create 的代碼:
constructorTLanguages.Create; |
TCallbackThunk = packedrecord |
Callback: TCallbackThunk; |
Callback.SelfPtr := Self; |
Callback.JmpOffset := Integer(@TLanguages.LocalesCallback) - Integer(@Callback.JMP) - 5; |
EnumSystemLocales(TFNLocaleEnumProc(@Callback), LCID_SUPPORTED); |
在 Win32 SDK 中可以查到 EnumSystemLocales 要求的回調(diào)格式是:
BOOL CALLBACK EnumLocalesProc( |
LPTSTR lpLocaleString // pointer to locale identifier string |
而 SysUtils 中的方法聲明:
functionLocalesCallback(LocaleID: PChar): Integer; stdcall; |
顯然,我們是無(wú)法將 LocalesCallback 這個(gè)方法直接傳遞給 EnumSystemLocales 的,因?yàn)?LocalesCallback 的函數(shù)形式聲明實(shí)際上是:
function LocalesCallback(Self: TLanguages; LocaleID: PChar): Integer; stdcall;
比 EnumLocalesProc 多出來(lái)一個(gè)參數(shù)。
所以在 TLanguages.Create 中,使用了 Callback 結(jié)構(gòu)變量來(lái)生成一小段動(dòng)態(tài)代碼。這段代碼是構(gòu)造在堆棧中的(局部變量),轉(zhuǎn)換成匯編是:
// 取出 lpLocaleString 參數(shù)到 EDX 寄存器 |
// CALLBACK EnumLocalesProc 是 stdcall 調(diào)用,參數(shù)在堆棧中 |
// 將 Self 對(duì)象傳給 EAX 寄存器 |
// stdcall 調(diào)用,將 Self 作為第一個(gè)參數(shù)壓棧 |
// 將 lpLocaleString 作為第二個(gè)參數(shù)壓棧 |
// 用相對(duì)跳轉(zhuǎn)指令跳轉(zhuǎn)到 TLanguages.LocalesCallback 入口地址 |
JMP TLanguages.LocalesCallback |
將 CallbackThunk 作為臨時(shí)的回調(diào)函數(shù)傳遞給 EnumSystemLocales 是合法的。當(dāng)回調(diào)被執(zhí)行時(shí),前面那小段代碼動(dòng)態(tài)修改了堆棧的內(nèi)容,將本來(lái)只有一個(gè)參數(shù)的調(diào)用,變成了兩個(gè)參數(shù),從而實(shí)現(xiàn)了回調(diào)與對(duì)象方法的轉(zhuǎn)換。
但是,正如 Passion 在前面提到的,由于這小塊臨時(shí)代碼是放在堆棧中的,而 Win2003 的 DEP 限制了在堆棧中執(zhí)行代碼,導(dǎo)致事實(shí)上回調(diào)函數(shù)并沒(méi)有被正確地調(diào)用。
Borland 程序員也看到了這個(gè)問(wèn)題,所以在 BDS 2006 中,這部分代碼的實(shí)現(xiàn)修改成:
FTempLanguages: TLanguages; |
functionEnumLocalesCallback(LocaleID: PChar): Integer; stdcall; |
Result := FTempLanguages.LocalesCallback(LocaleID); |
constructorTLanguages.Create; |
EnumSystemLocales(@EnumLocalesCallback, LCID_SUPPORTED); |
通過(guò)聲明一個(gè)臨時(shí)變量和轉(zhuǎn)換函數(shù),來(lái)取代原來(lái)的方法,就不會(huì)有 DEP 沖突了。
附帶說(shuō)一下 Forms 單元中的 MakeObjectInstance。這個(gè)函數(shù)用來(lái)生成一塊動(dòng)態(tài)代碼,將 Windows 的窗體消息處理過(guò)程轉(zhuǎn)換為 Delphi 的對(duì)象方法調(diào)用。在 TWinControl 等需要有消息處理支持的地方用到。該函數(shù)也是采用了前面類(lèi)似的方法,不過(guò)不同的是,由于這些轉(zhuǎn)換調(diào)用是長(zhǎng)期的,所以那些動(dòng)態(tài)生成的代碼被放到了標(biāo)識(shí)為可執(zhí)行的動(dòng)態(tài)空間中了,所以在 Win2003 的 DEP 下仍然可以正常工作:
functionMakeObjectInstance(Method: TWndMethod): Pointer; |
Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); |
劉嘯
例如我們聲明了一個(gè)方法 MainForm.BtnClick 并將它賦值給 btn1.OnClick 事件,實(shí)際上是將 MainForm 對(duì)象和 BtnClick 方法地址分別作為 TMethod 結(jié)構(gòu)的 Data 和 Code 成員賦值給 btn1.OnClick 事件屬性?!爱?dāng) btn1 按鈕調(diào)用這個(gè) BtnClick 事件時(shí),實(shí)際上是將 TMethod 結(jié)構(gòu)的 Data 作為第一個(gè)參數(shù)去調(diào)用 Code 函數(shù)?!?/P>
這里關(guān)于調(diào)用的似乎值得討論一下。記得這個(gè)事件OnClick在被調(diào)用時(shí)是這么寫(xiě)的:
ifAssigned(FOnClick) then |
第一個(gè)參數(shù)是調(diào)用時(shí)傳入的是Button自身,也就是Button的Self,而不是原本這個(gè)Method里頭的Data吧?
我的理解是,Method的Data只是用來(lái)說(shuō)明這個(gè)方法屬于哪個(gè)對(duì)象實(shí)例,但被調(diào)的時(shí)候似乎沒(méi)發(fā)揮作用。所以自行捏造一個(gè)TMethod的data部分,然后給OnClick等賦值再調(diào)用也能成功。
周勁羽
ifAssigned(FOnClick) then |
這里傳入的 Self 是 TNotifyEvent 中的 Sender: TObject 參數(shù),而作為對(duì)象方法的 OnClick,實(shí)際上需要兩個(gè)參數(shù),第一個(gè)隱藏的 Self 是 OnClick 方法所從屬的對(duì)象,第二個(gè)才是 Sender。
比如 Button 調(diào)用 FOnClick 時(shí),這個(gè) FOnClick 指向的方法可能是從屬于某個(gè) Form 的 OnBtnClick。類(lèi)自己是不保存對(duì)象實(shí)例的,直接調(diào)用 Form.OnBtnClick 時(shí) Self 是 Form 這個(gè)實(shí)例,而通過(guò) Button.FOnClick 調(diào)用到 Form.OnBtnClick 方法時(shí),OnBtnClick 的 Self 從哪里來(lái)?當(dāng)然就是用 TMethod.Data 傳過(guò)去的嘍。而這個(gè) TMethod.Data 則是在賦值 Button.OnClick := Form.OnBtnClick 時(shí)的 Form 對(duì)象。
FOnClick時(shí)傳入的Self是作為Sender的,而B(niǎo)tnOnClick方法里頭所引用的Self是Form實(shí)例,后者的Self應(yīng)該是從Data里頭來(lái)的。
由上可得到一個(gè)通用函數(shù),用來(lái)動(dòng)態(tài)設(shè)置對(duì)象事件:
procedureReSetObjEvent(OldEventAddr: pointer;NewEventValue: pointer;ReSetObject: TObject); |
TMethod(OldEventAddr^).Code := NewEventValue; |
TMethod(OldEventAddr^).Data := ReSetObject; |
//參數(shù)一: 指定為 存放事件指針的內(nèi)存地址值的地址指針,所以為一個(gè)指針的指針 |
//參數(shù)二: 指定為新的事件函數(shù)地址指針 |
//參數(shù)三: 指定為重設(shè)事件的修改者,用來(lái)隱射對(duì)象方法的隱含參數(shù)Self |
ReSetObjEvent(@integer(@self.OnClose),@MyCloseEvent,self); |
procedureMyCloseEvent(ClassSend: TObject;Sender: TObject;varAction: TCloseAction ); |
showmessage(TComponent(Sender).Name+'觸發(fā),不許關(guān)閉'); |
showmessage(TComponent(ClassSend).Name); |
procedureTForm1.Button1Click(Sender: TObject); |
ReSetObjEvent(@integer(@self.OnClose),@MyCloseEvent,self); |