Delphi动态事件
Delphi動態事件深入分析
核心提示:本實驗證明了在類中方法的調用時候,所有的方法都隱含了一個Self參數,并且該參數作為對象方法的第一個參數傳遞...
首先做一個空窗體,放入一Button。
在implementation下面聲明兩個方法如下:
//外部方法,只聲明一個參數,此時按照標準的對象內部事件方法TNotifyEvent聲明,此聲明中,Sender則對應為產生該事件的對象指針。
procedureExtClick1(Sender: TObject);?
begin
? {asm
??? mov eax,[edx+8]
??? call showmessage
? end; }
? showmessage(TComponent(Sender).Name);?
end;?
??
//外部方法,聲明兩個參數,用來證明,對象在調用時候會傳遞一個Self指針,此時我們假設Frm是通過類對象傳遞過來的Self指針,而Sender為產生該事件的對象指針
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;?
??
//然后在 ‘指定調用’按扭事件中寫代碼:
procedureTForm1.Button1Click(Sender: TObject);?
begin
?? showmessage(TComponent(Sender).Name);?
end;?
??
//很顯然運行的時候,點該按扭得到的是返回一個 消息內容為 ‘Button1’的對話框,這是調用Form1類的對象事件觸發的方法。
??
//在調用 ‘調用Form類外部方法觸發事件’ Click事件中寫
procedureTForm1.Button2Click(Sender: TObject);?
var
? ExtClickEvent: TNotifyEvent;?
begin
? integer(@ExtClickEvent) := integer(@ExtClick1);??
//將ExtClickEvent地址指針指向外部函數ExtClick1方法的地址
? Button1.OnClick := ExtClickEvent;?
//將該地址賦值給 Button1的OnClick事件替換以前的OnClick事件
end;?
//另一個按扭寫代碼如下:
procedureTForm1.Button3Click(Sender: TObject);?
begin
? Button1.OnClick := Button1Click;//還原為對象內觸發事件函數
end;
運行之后
點一下 ‘調用Form類外部方法觸發事件’ ,然后在點 ‘指定調用’按扭,
showmessage(TComponent(Sender).Name);返回的值是 ‘Form1’,此時是否就已經說明了其第一個參數是否就是傳遞的一個Self指針呢。所以在調用Button.Click事件的時候傳遞過來的第一個參數為Form1內部的Self指針,而該指針是指向Form1的。此時,我們在該函數的
Begin位置放下一個斷點,程序運行時候,此處的斷點為非可用的,如下圖:
說明程序在Begin處根本沒有處理其他任何代碼,此時,將斷點調到
showmessage(TComponent(Sender).Name);然后點 按扭 程序運行到斷點處停下
調出CPU View窗口查看代碼如下
注意 EAX,EBX,EDX,ECX的值,首先一條是
Mov? eax,[eax+$08] //該條指令將對象的Name屬性值傳遞到Eax中
Call?? ShowMessage //此函數需要一個參數,Delphi的參數傳遞規則為EAX,EDX,ECX
如此可見,沒有任何多余的處理,但是此時還不能證明Eax傳遞過來的就是類對象的Self指針
此時將 ‘調用Form類外部方法觸發事件’ Click事件中代碼的函數換成
ExtClick
既將?? integer(@ExtClickEvent) := integer(@ExtClick1);
換成?? integer(@ExtClickEvent) := integer(@ExtClick);
然后重新重復上面的步驟,在ExtClick的Begin處下斷點,程序運行到斷點處停下,則說明
程序在Begin時候有代碼執行,打開CPU View查看如下:
可見在Begin之后,ShowMessage函數之前,有兩段代碼如下:
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]
此時,然后運行,得到結果為TComponent(Sender).Name 的值為Button1
而下面的代碼
if Frm is TForm then
TForm(Frm).Close;
則充分證明了EAX的值是 Form1,則說明了對象方法在調用的時候會傳遞一個隱含的Self指針,而該指針的值在EAX中.
由于Delphi中參數的傳遞為
EAX? 第一個參數
EDX? 第二個參數
ECX? 第三個參數
所以可知道,真正的觸發事件的按扭對象存放在EDX中.
所以我們可以得到如下結論
在 按扭的單擊事件中,
TNotifyEvent = procedure(Sender: TObject) of object;
其真正的實體為procedure(當前聲明引起的對象Self,Sender: TObject)
所以 Button.OnClick的時候,其實傳遞方式如下
Button1.OnClick(Self,Sender);
其他事件方法等,依次類推.
然后根據該結論,則我們可以不在受
為Form中的某個控件對象指定事件方法的時候受到 Of Object 那個東西的限制,可以將事件方法指定到任何地方了。只要注意,該方法對應的參數要比其事件方法(Of Object)指定的方法多一個參數聲明,則可
比如,此時,我們拿窗體關閉事件做文章:
新建一個按扭,寫代碼
procedureTForm1.Button4Click(Sender: TObject);?
var
? CloseEvent: TCloseEvent;?
begin
? integer(@CloseEvent) := integer(@MyCloseEvent);?
? self.OnClose := CloseEvent;?
end;
窗體關閉的事件方法為
TCloseEvent = procedure(Sender: TObject;Var action: TCloseAction) of Object;
從上面結論我們知道可以聲明一個外部函數,該外部函數的參數要比TCloseEvent的參數多一個Self指針的,所以我們聲明如下:
procedure MyCloseEvent(Frm: TForm;Sender: TObject;var Action: TCloseAction);
Frm則是外部在窗體關閉的時候,傳遞的隱含指針Self
該函數整體代碼如下:
procedureMyCloseEvent(Frm: TForm;Sender: TObject;varAction: TCloseAction);?
begin
? showmessage(Frm.Name+'窗體外部方法調用,不允許關閉窗體!');?
? Action := caNone;?
end;
點一下,新建的按扭之后,看看是否還可以關閉窗體!!
通過匯編來處理
procedureTForm1.SetEvent(Event: pointer);?
asm
? push ebx??????????????? //保護Ebx
? mov ebx,eax??? //將當前的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;
//由于前面我們已經證明了,在類之中的方法,其傳遞的時候,都會有一個隱含的參數Self,所以,該段匯編代碼中我們就知道了Event參數對應應該是Edx寄存器,而不是Eax寄存器了。然后,后面有[ebx+$2d8]這樣的內容,這個是窗體 OnClose事件所在位置的地址。可以通過CpuView窗口查看得到,暫時沒有想到如何通過指定一個 事件名稱來得到該事件在內存中的地址。如果這樣的話,那么則可以寫一個函數
ReSetObjEvent(EventName: string;EventValue: pointer);
先通過EventName找到事件地址,然后再通過上面的則可以寫出一個簡單通俗易懂的公用函數了。
否則只能通過傳遞地址,根據改變地址中的值來修改事件函數的指向了。如下:
寫一個專門用來重設置事件方法的函數如下:
procedureReSetObjEvent(OldEventAddress: Pointer;NewEventValue: pointer);?
var
? gg: integer;?
? sd: pinteger;?
begin
sd := OldEvent;?
? gg := integer(NewEvent);?
? sd^:=gg;?
end;
其實也就是 改變存放事件方法指針的內存塊的數據值,使其變成另一個值。
注意,參數一指定為存放舊事件方法指針的內存地址,所以他應該是一個指針的指針了。
????? 參數二指定為事件方法指針值。
調用方法如下:
比如,指定窗體的 OnClose事件方法指針為窗體類外部定義的函數。
? ReSetObjEvent(@(integer(@Form1.onClose)),@MyCloseEvent)
例如:
procedureFrmClose(Frm: TForm;Sender: TObject;VarAction: TCloseAction);?
begin
?? showmessage('調用外部方法,不許關閉!');?
?? action := canone;?
end;?
??
procedureTForm1.BitBtn1Click(Sender: TObject);?
begin
? ReSetObjEvent(@(integer(@self.OnClose)),@frmClose);?
end;
續言:
? 以上在Delphi7下測試通過,至于2007下,我測試,也傳遞了一個隱含參數,但是該隱含參數不是Self
再論:
? 經過Cnpack的劉嘯提醒之后,發現了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) 類型實際上是同時包含有對象和函數的記錄。我們可以把一個 TNotifyEvent 的變量強制轉換成 TMethod:
TMethod = record
? Code, Data: Pointer;?
end;
例如我們聲明了一個方法 MainForm.BtnClick 并將它賦值給 btn1.OnClick 事件,實際上是將 MainForm 對象和 BtnClick 方法地址分別作為 TMethod 結構的 Data 和 Code 成員賦值給 btn1.OnClick 事件屬性。當 btn1 按鈕調用這個 BtnClick 事件時,實際上是將 TMethod 結構的 Data 作為第一個參數去調用 Code 函數。
我們可以編寫下面的代碼:
procedureMyClick(Self: TObject; Sender: TObject);?
begin
? // 第一個參數是虛擬的
? 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); // 隨便取的數
? btn1.OnClick := TNotifyEvent(M);?
end;
這樣就可以將一個普通函數賦值給對象事件屬性了。
我們再來看看 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 要求的回調格式是:
BOOL CALLBACK EnumLocalesProc(?
??? LPTSTR lpLocaleString???????? // pointer to locale identifier string
);
而 SysUtils 中的方法聲明:
TLanguages = class
? ...?
? functionLocalesCallback(LocaleID: PChar): Integer; stdcall;?
? ...?
end;
顯然,我們是無法將 LocalesCallback 這個方法直接傳遞給 EnumSystemLocales 的,因為 LocalesCallback 的函數形式聲明實際上是:
function LocalesCallback(Self: TLanguages; LocaleID: PChar): Integer; stdcall;
比 EnumLocalesProc 多出來一個參數。
所以在 TLanguages.Create 中,使用了 Callback 結構變量來生成一小段動態代碼。這段代碼是構造在堆棧中的(局部變量),轉換成匯編是:
prcoedure CallbackThunk;?
asm
? // 取出 lpLocaleString 參數到 EDX 寄存器
? // CALLBACK EnumLocalesProc 是 stdcall 調用,參數在堆棧中
? POP EDX?
? // 將 Self 對象傳給 EAX 寄存器
? MOV EAX Self?
? // stdcall 調用,將 Self 作為第一個參數壓棧
? PUSH EAX?
? // 將 lpLocaleString 作為第二個參數壓棧
? PUSH EDX?
? // 用相對跳轉指令跳轉到 TLanguages.LocalesCallback 入口地址
? JMP TLanguages.LocalesCallback?
end;
將 CallbackThunk 作為臨時的回調函數傳遞給 EnumSystemLocales 是合法的。當回調被執行時,前面那小段代碼動態修改了堆棧的內容,將本來只有一個參數的調用,變成了兩個參數,從而實現了回調與對象方法的轉換。
但是,正如 Passion 在前面提到的,由于這小塊臨時代碼是放在堆棧中的,而 Win2003 的 DEP 限制了在堆棧中執行代碼,導致事實上回調函數并沒有被正確地調用。
Borland 程序員也看到了這個問題,所以在 BDS 2006 中,這部分代碼的實現修改成:
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;
通過聲明一個臨時變量和轉換函數,來取代原來的方法,就不會有 DEP 沖突了。
附帶說一下 Forms 單元中的 MakeObjectInstance。這個函數用來生成一塊動態代碼,將 Windows 的窗體消息處理過程轉換為 Delphi 的對象方法調用。在 TWinControl 等需要有消息處理支持的地方用到。該函數也是采用了前面類似的方法,不過不同的是,由于這些轉換調用是長期的,所以那些動態生成的代碼被放到了標識為可執行的動態空間中了,所以在 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 結構的 Data 和 Code 成員賦值給 btn1.OnClick 事件屬性。“當 btn1 按鈕調用這個 BtnClick 事件時,實際上是將 TMethod 結構的 Data 作為第一個參數去調用 Code 函數。”
這里關于調用的似乎值得討論一下。記得這個事件OnClick在被調用時是這么寫的:
ifAssigned(FOnClick) then
? FOnClick(Self);
第一個參數是調用時傳入的是Button自身,也就是Button的Self,而不是原本這個Method里頭的Data吧?
我的理解是,Method的Data只是用來說明這個方法屬于哪個對象實例,但被調的時候似乎沒發揮作用。所以自行捏造一個TMethod的data部分,然后給OnClick等賦值再調用也能成功。
周勁羽
ifAssigned(FOnClick) then
? FOnClick(Self);
這里傳入的 Self 是 TNotifyEvent 中的 Sender: TObject 參數,而作為對象方法的 OnClick,實際上需要兩個參數,第一個隱藏的 Self 是 OnClick 方法所從屬的對象,第二個才是 Sender。
比如 Button 調用 FOnClick 時,這個 FOnClick 指向的方法可能是從屬于某個 Form 的 OnBtnClick。類自己是不保存對象實例的,直接調用 Form.OnBtnClick 時 Self 是 Form 這個實例,而通過 Button.FOnClick 調用到 Form.OnBtnClick 方法時,OnBtnClick 的 Self 從哪里來?當然就是用 TMethod.Data 傳過去的嘍。而這個 TMethod.Data 則是在賦值 Button.OnClick := Form.OnBtnClick 時的 Form 對象。
FOnClick時傳入的Self是作為Sender的,而BtnOnClick方法里頭所引用的Self是Form實例,后者的Self應該是從Data里頭來的。
由上可得到一個通用函數,用來動態設置對象事件:
procedureReSetObjEvent(OldEventAddr: pointer;NewEventValue: pointer;ReSetObject: TObject);?
begin
TMethod(OldEventAddr^).Code := NewEventValue;?
TMethod(OldEventAddr^).Data := ReSetObject;?
end;?
//參數一: 指定為 存放事件指針的內存地址值的地址指針,所以為一個指針的指針
//參數二: 指定為新的事件函數地址指針
//參數三: 指定為重設事件的修改者,用來隱射對象方法的隱含參數Self
??
//調用方法:
? ReSetObjEvent(@integer(@self.OnClose),@MyCloseEvent,self);?
??
//例:
??
procedureMyCloseEvent(ClassSend: TObject;Sender: TObject;varAction: TCloseAction );?
begin
??? action := canone;?
??? showmessage(TComponent(Sender).Name+'觸發,不許關閉');?
??? showmessage(TComponent(ClassSend).Name);?
end;?
??
procedureTForm1.Button1Click(Sender: TObject);?
begin
? ReSetObjEvent(@integer(@self.OnClose),@MyCloseEvent,self);?
end;
總結
以上是生活随笔為你收集整理的Delphi动态事件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 然而毕博淮安的毕博淮安
- 下一篇: 飞鸽传书 获得磁盘的C#描述信息