日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

Delphi動態(tài)事件深入分析

 aaie_ 2011-11-30
Delphi動態(tài)事件深入分析
Delphi動態(tài)事件深入分析
核心提示:本實驗證明了在類中方法的調(diào)用時候,所有的方法都隱含了一個Self參數(shù),并且該參數(shù)作為對象方法的第一個參數(shù)傳遞...

首先做一個空窗體,放入一Button。
在implementation下面聲明兩個方法如下:

 
//外部方法,只聲明一個參數(shù),此時按照標(biāo)準(zhǔn)的對象內(nèi)部事件方法TNotifyEvent聲明,此聲明中,Sender則對應(yīng)為產(chǎn)生該事件的對象指針。
procedureExtClick1(Sender: TObject);
begin
{asm
mov eax,[edx+8]
call showmessage
end; }
showmessage(TComponent(Sender).Name);
end;
//外部方法,聲明兩個參數(shù),用來證明,對象在調(diào)用時候會傳遞一個Self指針,此時我們假設(shè)Frm是通過類對象傳遞過來的Self指針,而Sender為產(chǎn)生該事件的對象指針
procedureExtClick(Frm: TObject;Sender: TObject);
begin
{asm
mov eax,[edx+8]
call showmessage
end; }
showmessage(TComponent(Sender).Name);
ifFrm isTForm then
TForm(Frm).Close
end;
//然后在 ‘指定調(diào)用’按扭事件中寫代碼:
procedureTForm1.Button1Click(Sender: TObject);
begin
showmessage(TComponent(Sender).Name);
end;
//很顯然運行的時候,點該按扭得到的是返回一個 消息內(nèi)容為 ‘Button1’的對話框,這是調(diào)用Form1類的對象事件觸發(fā)的方法。
//在調(diào)用 ‘調(diào)用Form類外部方法觸發(fā)事件’ Click事件中寫
procedureTForm1.Button2Click(Sender: TObject);
var
ExtClickEvent: TNotifyEvent;
begin
integer(@ExtClickEvent) := integer(@ExtClick1);
//將ExtClickEvent地址指針指向外部函數(shù)ExtClick1方法的地址
Button1.OnClick := ExtClickEvent;
//將該地址賦值給 Button1的OnClick事件替換以前的OnClick事件
end;
//另一個按扭寫代碼如下:
procedureTForm1.Button3Click(Sender: TObject);
begin
Button1.OnClick := Button1Click;//還原為對象內(nèi)觸發(fā)事件函數(shù)
end;

運行之后
點一下 ‘調(diào)用Form類外部方法觸發(fā)事件’ ,然后在點 ‘指定調(diào)用’按扭,
showmessage(TComponent(Sender).Name);返回的值是 ‘Form1’,此時是否就已經(jīng)說明了其第一個參數(shù)是否就是傳遞的一個Self指針呢。所以在調(diào)用Button.Click事件的時候傳遞過來的第一個參數(shù)為Form1內(nèi)部的Self指針,而該指針是指向Form1的。此時,我們在該函數(shù)的
Begin位置放下一個斷點,程序運行時候,此處的斷點為非可用的,如下圖:

說明程序在Begin處根本沒有處理其他任何代碼,此時,將斷點調(diào)到
showmessage(TComponent(Sender).Name);然后點 按扭 程序運行到斷點處停下
調(diào)出CPU View窗口查看代碼如下

注意 EAX,EBX,EDX,ECX的值,首先一條是

Mov eax,[eax+$08] //該條指令將對象的Name屬性值傳遞到Eax中
Call ShowMessage //此函數(shù)需要一個參數(shù),Delphi的參數(shù)傳遞規(guī)則為EAX,EDX,ECX
如此可見,沒有任何多余的處理,但是此時還不能證明Eax傳遞過來的就是類對象的Self指針

此時將 ‘調(diào)用Form類外部方法觸發(fā)事件’ Click事件中代碼的函數(shù)換成
ExtClick
既將 integer(@ExtClickEvent) := integer(@ExtClick1);
換成 integer(@ExtClickEvent) := integer(@ExtClick);
然后重新重復(fù)上面的步驟,在ExtClick的Begin處下斷點,程序運行到斷點處停下,則說明
程序在Begin時候有代碼執(zhí)行,打開CPU View查看如下:

可見在Begin之后,ShowMessage函數(shù)之前,有兩段代碼如下:
Push ebx //保存Ebx的值
Mov ebx,eax //將Eax的值暫時存放到Ebx中
然后主要看下面的showmessage(TComponent(Sender).Name);一句
可見 其匯編代碼如下:
Mov eax,[edx+$08]
Call ShowMessage
和以前相比 Mov eax,[eax+$08] 變成了 Mov eax,[edx+$08]
此時,然后運行,得到結(jié)果為TComponent(Sender).Name 的值為Button1
而下面的代碼
if Frm is TForm then
TForm(Frm).Close;
則充分證明了EAX的值是 Form1,則說明了對象方法在調(diào)用的時候會傳遞一個隱含的Self指針,而該指針的值在EAX中.
由于Delphi中參數(shù)的傳遞為
EAX 第一個參數(shù)
EDX 第二個參數(shù)
ECX 第三個參數(shù)
所以可知道,真正的觸發(fā)事件的按扭對象存放在EDX中.

所以我們可以得到如下結(jié)論
在 按扭的單擊事件中,
TNotifyEvent = procedure(Sender: TObject) of object;
其真正的實體為procedure(當(dāng)前聲明引起的對象Self,Sender: TObject)
所以 Button.OnClick的時候,其實傳遞方式如下
Button1.OnClick(Self,Sender);
其他事件方法等,依次類推.

然后根據(jù)該結(jié)論,則我們可以不在受
為Form中的某個控件對象指定事件方法的時候受到 Of Object 那個東西的限制,可以將事件方法指定到任何地方了。只要注意,該方法對應(yīng)的參數(shù)要比其事件方法(Of Object)指定的方法多一個參數(shù)聲明,則可
比如,此時,我們拿窗體關(guān)閉事件做文章:
新建一個按扭,寫代碼

 
procedureTForm1.Button4Click(Sender: TObject);
var
CloseEvent: TCloseEvent;
begin
integer(@CloseEvent) := integer(@MyCloseEvent);
self.OnClose := CloseEvent;
end;

窗體關(guān)閉的事件方法為
TCloseEvent = procedure(Sender: TObject;Var action: TCloseAction) of Object;

從上面結(jié)論我們知道可以聲明一個外部函數(shù),該外部函數(shù)的參數(shù)要比TCloseEvent的參數(shù)多一個Self指針的,所以我們聲明如下:
procedure MyCloseEvent(Frm: TForm;Sender: TObject;var Action: TCloseAction);
Frm則是外部在窗體關(guān)閉的時候,傳遞的隱含指針Self

該函數(shù)整體代碼如下:

 
procedureMyCloseEvent(Frm: TForm;Sender: TObject;varAction: TCloseAction);
begin
showmessage(Frm.Name+'窗體外部方法調(diào)用,不允許關(guān)閉窗體!');
Action := caNone;
end;

點一下,新建的按扭之后,看看是否還可以關(guān)閉窗體??!

通過匯編來處理

 
procedureTForm1.SetEvent(Event: pointer);
asm
push ebx //保護Ebx
mov ebx,eax //將當(dāng)前的eax的值,先用ebx保存起來,eax中保存的為Form的開始地
mov eax,edx //將Event指針的值給EAX
mov [ebx+$2d8],eax //將Eax的值分別寫進其高位和低位
mov eax,[edx+4]
mov [ebx+$2d4],eax
pop ebx
end;

//由于前面我們已經(jīng)證明了,在類之中的方法,其傳遞的時候,都會有一個隱含的參數(shù)Self,所以,該段匯編代碼中我們就知道了Event參數(shù)對應(yīng)應(yīng)該是Edx寄存器,而不是Eax寄存器了。然后,后面有[ebx+$2d8]這樣的內(nèi)容,這個是窗體 OnClose事件所在位置的地址。可以通過CpuView窗口查看得到,暫時沒有想到如何通過指定一個 事件名稱來得到該事件在內(nèi)存中的地址。如果這樣的話,那么則可以寫一個函數(shù)
ReSetObjEvent(EventName: string;EventValue: pointer);
先通過EventName找到事件地址,然后再通過上面的則可以寫出一個簡單通俗易懂的公用函數(shù)了。
否則只能通過傳遞地址,根據(jù)改變地址中的值來修改事件函數(shù)的指向了。如下:
寫一個專門用來重設(shè)置事件方法的函數(shù)如下:

 
procedureReSetObjEvent(OldEventAddress: Pointer;NewEventValue: pointer);
var
gg: integer;
sd: pinteger;
begin
sd := OldEvent;
gg := integer(NewEvent);
sd^:=gg;
end;

其實也就是 改變存放事件方法指針的內(nèi)存塊的數(shù)據(jù)值,使其變成另一個值。
注意,參數(shù)一指定為存放舊事件方法指針的內(nèi)存地址,所以他應(yīng)該是一個指針的指針了。
參數(shù)二指定為事件方法指針值。
調(diào)用方法如下:
比如,指定窗體的 OnClose事件方法指針為窗體類外部定義的函數(shù)。
ReSetObjEvent(@(integer(@Form1.onClose)),@MyCloseEvent)
例如:

 
procedureFrmClose(Frm: TForm;Sender: TObject;VarAction: TCloseAction);
begin
showmessage('調(diào)用外部方法,不許關(guān)閉!');
action := canone;
end;
procedureTForm1.BitBtn1Click(Sender: TObject);
begin
ReSetObjEvent(@(integer(@self.OnClose)),@frmClose);
end;

續(xù)言:
以上在Delphi7下測試通過,至于2007下,我測試,也傳遞了一個隱含參數(shù),但是該隱含參數(shù)不是Self

再論:
經(jīng)過Cnpack的劉嘯提醒之后,發(fā)現(xiàn)了Delphi7下測試通過,而2007下不通過的原因是在于D7下如下聲明:

 
procedureTForm1.Button4Click(Sender: TObject);
var
CloseEvent: TCloseEvent;
begin
integer(@CloseEvent) := integer(@MyCloseEvent);
self.OnClose := CloseEvent;
end;

此時2007下該段程序運行不能通過而D7編譯運行可以通過,實在確實是一個巧合了。
通過提示得知,TCloseEvent在Delphi中被稱為對象方法,而對象方法
在 Delphi 中用 procedure(Sender: TObject) of object; 這種格式聲明的 事件(Event) 類型實際上是同時包含有對象和函數(shù)的記錄。我們可以把一個 TNotifyEvent 的變量強制轉(zhuǎn)換成 TMethod:

 
TMethod = record
Code, Data: Pointer;
end;

例如我們聲明了一個方法 MainForm.BtnClick 并將它賦值給 btn1.OnClick 事件,實際上是將 MainForm 對象和 BtnClick 方法地址分別作為 TMethod 結(jié)構(gòu)的 Data 和 Code 成員賦值給 btn1.OnClick 事件屬性。當(dāng) btn1 按鈕調(diào)用這個 BtnClick 事件時,實際上是將 TMethod 結(jié)構(gòu)的 Data 作為第一個參數(shù)去調(diào)用 Code 函數(shù)。

我們可以編寫下面的代碼:

 
procedureMyClick(Self: TObject; Sender: TObject);
begin
// 第一個參數(shù)是虛擬的
ShowMessage(Format('Self: %d, Sender: %s', [Integer(Self), Sender.ClassName]));
end;
procedureTForm1.FormCreate(Sender: TObject);
var
M: TMethod;
begin
M.Code := @MyClick;
M.Data := Pointer(325); // 隨便取的數(shù)
btn1.OnClick := TNotifyEvent(M);
end;

這樣就可以將一個普通函數(shù)賦值給對象事件屬性了。

我們再來看看 TLanguages.Create 的代碼:

 
constructorTLanguages.Create;
type
TCallbackThunk = packedrecord
POPEDX: Byte;
MOVEAX: Byte;
SelfPtr: Pointer;
PUSHEAX: Byte;
PUSHEDX: Byte;
JMP: Byte;
JmpOffset: Integer;
end;
var
Callback: TCallbackThunk;
begin
inheritedCreate;
Callback.POPEDX := $5A;
Callback.MOVEAX := $B8;
Callback.SelfPtr := Self;
Callback.PUSHEAX := $50;
Callback.PUSHEDX := $52;
Callback.JMP := $E9;
Callback.JmpOffset := Integer(@TLanguages.LocalesCallback) - Integer(@Callback.JMP) - 5;
EnumSystemLocales(TFNLocaleEnumProc(@Callback), LCID_SUPPORTED);
end;

在 Win32 SDK 中可以查到 EnumSystemLocales 要求的回調(diào)格式是:

 
BOOL CALLBACK EnumLocalesProc(
LPTSTR lpLocaleString // pointer to locale identifier string
);

而 SysUtils 中的方法聲明:

 
TLanguages = class
...
functionLocalesCallback(LocaleID: PChar): Integer; stdcall;
...
end;

顯然,我們是無法將 LocalesCallback 這個方法直接傳遞給 EnumSystemLocales 的,因為 LocalesCallback 的函數(shù)形式聲明實際上是:
function LocalesCallback(Self: TLanguages; LocaleID: PChar): Integer; stdcall;
比 EnumLocalesProc 多出來一個參數(shù)。

所以在 TLanguages.Create 中,使用了 Callback 結(jié)構(gòu)變量來生成一小段動態(tài)代碼。這段代碼是構(gòu)造在堆棧中的(局部變量),轉(zhuǎn)換成匯編是:

 
prcoedure CallbackThunk;
asm
// 取出 lpLocaleString 參數(shù)到 EDX 寄存器
// CALLBACK EnumLocalesProc 是 stdcall 調(diào)用,參數(shù)在堆棧中
POP EDX
// 將 Self 對象傳給 EAX 寄存器
MOV EAX Self
// stdcall 調(diào)用,將 Self 作為第一個參數(shù)壓棧
PUSH EAX
// 將 lpLocaleString 作為第二個參數(shù)壓棧
PUSH EDX
// 用相對跳轉(zhuǎn)指令跳轉(zhuǎn)到 TLanguages.LocalesCallback 入口地址
JMP TLanguages.LocalesCallback
end;

將 CallbackThunk 作為臨時的回調(diào)函數(shù)傳遞給 EnumSystemLocales 是合法的。當(dāng)回調(diào)被執(zhí)行時,前面那小段代碼動態(tài)修改了堆棧的內(nèi)容,將本來只有一個參數(shù)的調(diào)用,變成了兩個參數(shù),從而實現(xiàn)了回調(diào)與對象方法的轉(zhuǎn)換。

但是,正如 Passion 在前面提到的,由于這小塊臨時代碼是放在堆棧中的,而 Win2003 的 DEP 限制了在堆棧中執(zhí)行代碼,導(dǎo)致事實上回調(diào)函數(shù)并沒有被正確地調(diào)用。

Borland 程序員也看到了這個問題,所以在 BDS 2006 中,這部分代碼的實現(xiàn)修改成:

 
var
FTempLanguages: TLanguages;
functionEnumLocalesCallback(LocaleID: PChar): Integer; stdcall;
begin
Result := FTempLanguages.LocalesCallback(LocaleID);
end;
constructorTLanguages.Create;
begin
inheritedCreate;
FTempLanguages := Self;
EnumSystemLocales(@EnumLocalesCallback, LCID_SUPPORTED);
end;

通過聲明一個臨時變量和轉(zhuǎn)換函數(shù),來取代原來的方法,就不會有 DEP 沖突了。

附帶說一下 Forms 單元中的 MakeObjectInstance。這個函數(shù)用來生成一塊動態(tài)代碼,將 Windows 的窗體消息處理過程轉(zhuǎn)換為 Delphi 的對象方法調(diào)用。在 TWinControl 等需要有消息處理支持的地方用到。該函數(shù)也是采用了前面類似的方法,不過不同的是,由于這些轉(zhuǎn)換調(diào)用是長期的,所以那些動態(tài)生成的代碼被放到了標(biāo)識為可執(zhí)行的動態(tài)空間中了,所以在 Win2003 的 DEP 下仍然可以正常工作:

 
functionMakeObjectInstance(Method: TWndMethod): Pointer;
var
...
begin
ifInstFreeList = nilthen
begin
Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
...
end;

劉嘯
例如我們聲明了一個方法 MainForm.BtnClick 并將它賦值給 btn1.OnClick 事件,實際上是將 MainForm 對象和 BtnClick 方法地址分別作為 TMethod 結(jié)構(gòu)的 Data 和 Code 成員賦值給 btn1.OnClick 事件屬性。“當(dāng) btn1 按鈕調(diào)用這個 BtnClick 事件時,實際上是將 TMethod 結(jié)構(gòu)的 Data 作為第一個參數(shù)去調(diào)用 Code 函數(shù)?!?/P>

這里關(guān)于調(diào)用的似乎值得討論一下。記得這個事件OnClick在被調(diào)用時是這么寫的:

 
ifAssigned(FOnClick) then
FOnClick(Self);

第一個參數(shù)是調(diào)用時傳入的是Button自身,也就是Button的Self,而不是原本這個Method里頭的Data吧?
我的理解是,Method的Data只是用來說明這個方法屬于哪個對象實例,但被調(diào)的時候似乎沒發(fā)揮作用。所以自行捏造一個TMethod的data部分,然后給OnClick等賦值再調(diào)用也能成功。

周勁羽

 
ifAssigned(FOnClick) then
FOnClick(Self);
這里傳入的 Self 是 TNotifyEvent 中的 Sender: TObject 參數(shù),而作為對象方法的 OnClick,實際上需要兩個參數(shù),第一個隱藏的 Self 是 OnClick 方法所從屬的對象,第二個才是 Sender。

比如 Button 調(diào)用 FOnClick 時,這個 FOnClick 指向的方法可能是從屬于某個 Form 的 OnBtnClick。類自己是不保存對象實例的,直接調(diào)用 Form.OnBtnClick 時 Self 是 Form 這個實例,而通過 Button.FOnClick 調(diào)用到 Form.OnBtnClick 方法時,OnBtnClick 的 Self 從哪里來?當(dāng)然就是用 TMethod.Data 傳過去的嘍。而這個 TMethod.Data 則是在賦值 Button.OnClick := Form.OnBtnClick 時的 Form 對象。
FOnClick時傳入的Self是作為Sender的,而BtnOnClick方法里頭所引用的Self是Form實例,后者的Self應(yīng)該是從Data里頭來的。

由上可得到一個通用函數(shù),用來動態(tài)設(shè)置對象事件:

 
procedureReSetObjEvent(OldEventAddr: pointer;NewEventValue: pointer;ReSetObject: TObject);
begin
TMethod(OldEventAddr^).Code := NewEventValue;
TMethod(OldEventAddr^).Data := ReSetObject;
end;
//參數(shù)一: 指定為 存放事件指針的內(nèi)存地址值的地址指針,所以為一個指針的指針
//參數(shù)二: 指定為新的事件函數(shù)地址指針
//參數(shù)三: 指定為重設(shè)事件的修改者,用來隱射對象方法的隱含參數(shù)Self
//調(diào)用方法:
ReSetObjEvent(@integer(@self.OnClose),@MyCloseEvent,self);
//例:
procedureMyCloseEvent(ClassSend: TObject;Sender: TObject;varAction: TCloseAction );
begin
action := canone;
showmessage(TComponent(Sender).Name+'觸發(fā),不許關(guān)閉');
showmessage(TComponent(ClassSend).Name);
end;
procedureTForm1.Button1Click(Sender: TObject);
begin
ReSetObjEvent(@integer(@self.OnClose),@MyCloseEvent,self);
end;

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多