Delphi动态事件如何进行深入分析与优化?
- 内容介绍
- 文章标签
- 相关推荐
本文共计3299个文字,预计阅读时间需要14分钟。
[Delphi] view plain copy print?首先创建一个窗体如下:然后单元中如下代码:在Implementation下面声明两个方法如下://外部方法,只声明一个参数//按照标准对象内部事件方法TNotifyEvent声明
[delphi] view plain copy print?- 首先做一个窗体如下
- 然后单元中如下代码:
- 在implementation下面声明两个方法如下:
- //外部方法,只声明一个参数,此时按照标准的对象内部事件方法TNotifyEvent声明,此声明中,Sender则对应为产生该事件的对象指针。
- procedureExtClick1(Sender:TObject);
- begin
- {asm
- moveax,[edx+8]
- callshowmessage
- end;}
- showmessage(TComponent(Sender).Name);
- end;
- //外部方法,声明两个参数,用来证明,对象在调用时候会传递一个Self指针,此时我们假设Frm是通过类对象传递过来的Self指针,而Sender为产生该事件的对象指针
- procedureExtClick(Frm:TObject;Sender:TObject);
- begin
- {asm
- moveax,[edx+8]
- callshowmessage
- end;}
- showmessage(TComponent(Sender).Name);
- ifFrmisTFormthen
- 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);然后点按扭程序运行到断点处停下
- 调出CPUView窗口查看代码如下
- 注意EAX,EBX,EDX,ECX的值,首先一条是
- Moveax,[eax+$08]//该条指令将对象的Name属性值传递到Eax中
- CallShowMessage//此函数需要一个参数,Delphi的参数传递规则为EAX,EDX,ECX
- 如此可见,没有任何多余的处理,但是此时还不能证明Eax传递过来的就是类对象的Self指针
- 此时将‘调用Form类外部方法触发事件’Click事件中代码的函数换成
- ExtClick
- 既将integer(@ExtClickEvent):=integer(@ExtClick1);
- 换成integer(@ExtClickEvent):=integer(@ExtClick);
- 然后重新重复上面的步骤,在ExtClick的Begin处下断点,程序运行到断点处停下,则说明
- 程序在Begin时候有代码执行,打开CPUView查看如下:
- 可见在Begin之后,ShowMessage函数之前,有两段代码如下:
- Pushebx//保存Ebx的值
- Movebx,eax//将Eax的值暂时存放到Ebx中
- 然后主要看下面的showmessage(TComponent(Sender).Name);一句
- 可见其汇编代码如下:
- Moveax,[edx+$08]
- CallShowMessage
- 和以前相比Moveax,[eax+$08]变成了Moveax,[edx+$08]
- 此时,然后运行,得到结果为TComponent(Sender).Name的值为Button1
- 而下面的代码
- ifFrmisTFormthen
- TForm(Frm).Close;
- 则充分证明了EAX的值是Form1,则说明了对象方法在调用的时候会传递一个隐含的Self指针,而该指针的值在EAX中.
- 由于Delphi中参数的传递为
- EAX第一个参数
- EDX第二个参数
- ECX第三个参数
- 所以可知道,真正的触发事件的按扭对象存放在EDX中.
- 所以我们可以得到如下结论
- 在按扭的单击事件中,
- TNotifyEvent=procedure(Sender:TObject)ofobject;
- 其真正的实体为procedure(当前声明引起的对象Self,Sender:TObject)
- 所以Button.OnClick的时候,其实传递方式如下
- Button1.OnClick(Self,Sender);
- 其他事件方法等,依次类推.
- 然后根据该结论,则我们可以不在受
- 为Form中的某个控件对象指定事件方法的时候受到OfObject那个东西的限制,可以将事件方法指定到任何地方了。只要注意,该方法对应的参数要比其事件方法(OfObject)指定的方法多一个参数声明,则可
- 比如,此时,我们拿窗体关闭事件做文章:
- 新建一个按扭,写代码
- procedureTForm1.Button4Click(Sender:TObject);
- var
- CloseEvent:TCloseEvent;
- begin
- integer(@CloseEvent):=integer(@MyCloseEvent);
- self.OnClose:=CloseEvent;
- end;
- 窗体关闭的事件方法为
- TCloseEvent=procedure(Sender:TObject;Varaction:TCloseAction)ofObject;
- 从上面结论我们知道可以声明一个外部函数,该外部函数的参数要比TCloseEvent的参数多一个Self指针的,所以我们声明如下:
- procedureMyCloseEvent(Frm:TForm;Sender:TObject;varAction:TCloseAction);
- Frm则是外部在窗体关闭的时候,传递的隐含指针Self
- 该函数整体代码如下:
- procedureMyCloseEvent(Frm:TForm;Sender:TObject;varAction:TCloseAction);
- begin
- showmessage(Frm.Name+‘窗体外部方法调用,不允许关闭窗体!‘);
- Action:=caNone;
- end;
- 点一下,新建的按扭之后,看看是否还可以关闭窗体!!
- 通过汇编来处理
- procedureTForm1.SetEvent(Event:pointer);
- asm
- pushebx//保护Ebx
- movebx,eax//将当前的eax的值,先用ebx保存起来,eax中保存的为Form的开始地
- moveax,edx//将Event指针的值给EAX
- mov[ebx+$2d8],eax//将Eax的值分别写进其高位和低位
- moveax,[edx+4]
- mov[ebx+$2d4],eax
- popebx
- 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)ofobject;这种格式声明的事件(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;
- 在Win32SDK中可以查到EnumSystemLocales要求的回调格式是:
- BOOLCALLBACKEnumLocalesProc(
- LPTSTRlpLocaleString//pointertolocaleidentifierstring
- );
- 而SysUtils中的方法声明:
- TLanguages=class
- ...
- functionLocalesCallback(LocaleID:PChar):Integer;stdcall;
- ...
- end;
- 显然,我们是无法将LocalesCallback这个方法直接传递给EnumSystemLocales的,因为LocalesCallback的函数形式声明实际上是:
- functionLocalesCallback(Self:TLanguages;LocaleID:PChar):Integer;stdcall;
- 比EnumLocalesProc多出来一个参数。
- 所以在TLanguages.Create中,使用了Callback结构变量来生成一小段动态代码。这段代码是构造在堆栈中的(局部变量),转换成汇编是:
- prcoedureCallbackThunk;
- asm
- //取出lpLocaleString参数到EDX寄存器
- //CALLBACKEnumLocalesProc是stdcall调用,参数在堆栈中
- POPEDX
- //将Self对象传给EAX寄存器
- MOVEAXSelf
- //stdcall调用,将Self作为第一个参数压栈
- PUSHEAX
- //将lpLocaleString作为第二个参数压栈
- PUSHEDX
- //用相对跳转指令跳转到TLanguages.LocalesCallback入口地址
- JMPTLanguages.LocalesCallback
- end;
- 将CallbackThunk作为临时的回调函数传递给EnumSystemLocales是合法的。当回调被执行时,前面那小段代码动态修改了堆栈的内容,将本来只有一个参数的调用,变成了两个参数,从而实现了回调与对象方法的转换。
- 但是,正如Passion在前面提到的,由于这小块临时代码是放在堆栈中的,而Win2003的DEP限制了在堆栈中执行代码,导致事实上回调函数并没有被正确地调用。
- Borland程序员也看到了这个问题,所以在BDS2006中,这部分代码的实现修改成:
- 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;
本文共计3299个文字,预计阅读时间需要14分钟。
[Delphi] view plain copy print?首先创建一个窗体如下:然后单元中如下代码:在Implementation下面声明两个方法如下://外部方法,只声明一个参数//按照标准对象内部事件方法TNotifyEvent声明
[delphi] view plain copy print?- 首先做一个窗体如下
- 然后单元中如下代码:
- 在implementation下面声明两个方法如下:
- //外部方法,只声明一个参数,此时按照标准的对象内部事件方法TNotifyEvent声明,此声明中,Sender则对应为产生该事件的对象指针。
- procedureExtClick1(Sender:TObject);
- begin
- {asm
- moveax,[edx+8]
- callshowmessage
- end;}
- showmessage(TComponent(Sender).Name);
- end;
- //外部方法,声明两个参数,用来证明,对象在调用时候会传递一个Self指针,此时我们假设Frm是通过类对象传递过来的Self指针,而Sender为产生该事件的对象指针
- procedureExtClick(Frm:TObject;Sender:TObject);
- begin
- {asm
- moveax,[edx+8]
- callshowmessage
- end;}
- showmessage(TComponent(Sender).Name);
- ifFrmisTFormthen
- 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);然后点按扭程序运行到断点处停下
- 调出CPUView窗口查看代码如下
- 注意EAX,EBX,EDX,ECX的值,首先一条是
- Moveax,[eax+$08]//该条指令将对象的Name属性值传递到Eax中
- CallShowMessage//此函数需要一个参数,Delphi的参数传递规则为EAX,EDX,ECX
- 如此可见,没有任何多余的处理,但是此时还不能证明Eax传递过来的就是类对象的Self指针
- 此时将‘调用Form类外部方法触发事件’Click事件中代码的函数换成
- ExtClick
- 既将integer(@ExtClickEvent):=integer(@ExtClick1);
- 换成integer(@ExtClickEvent):=integer(@ExtClick);
- 然后重新重复上面的步骤,在ExtClick的Begin处下断点,程序运行到断点处停下,则说明
- 程序在Begin时候有代码执行,打开CPUView查看如下:
- 可见在Begin之后,ShowMessage函数之前,有两段代码如下:
- Pushebx//保存Ebx的值
- Movebx,eax//将Eax的值暂时存放到Ebx中
- 然后主要看下面的showmessage(TComponent(Sender).Name);一句
- 可见其汇编代码如下:
- Moveax,[edx+$08]
- CallShowMessage
- 和以前相比Moveax,[eax+$08]变成了Moveax,[edx+$08]
- 此时,然后运行,得到结果为TComponent(Sender).Name的值为Button1
- 而下面的代码
- ifFrmisTFormthen
- TForm(Frm).Close;
- 则充分证明了EAX的值是Form1,则说明了对象方法在调用的时候会传递一个隐含的Self指针,而该指针的值在EAX中.
- 由于Delphi中参数的传递为
- EAX第一个参数
- EDX第二个参数
- ECX第三个参数
- 所以可知道,真正的触发事件的按扭对象存放在EDX中.
- 所以我们可以得到如下结论
- 在按扭的单击事件中,
- TNotifyEvent=procedure(Sender:TObject)ofobject;
- 其真正的实体为procedure(当前声明引起的对象Self,Sender:TObject)
- 所以Button.OnClick的时候,其实传递方式如下
- Button1.OnClick(Self,Sender);
- 其他事件方法等,依次类推.
- 然后根据该结论,则我们可以不在受
- 为Form中的某个控件对象指定事件方法的时候受到OfObject那个东西的限制,可以将事件方法指定到任何地方了。只要注意,该方法对应的参数要比其事件方法(OfObject)指定的方法多一个参数声明,则可
- 比如,此时,我们拿窗体关闭事件做文章:
- 新建一个按扭,写代码
- procedureTForm1.Button4Click(Sender:TObject);
- var
- CloseEvent:TCloseEvent;
- begin
- integer(@CloseEvent):=integer(@MyCloseEvent);
- self.OnClose:=CloseEvent;
- end;
- 窗体关闭的事件方法为
- TCloseEvent=procedure(Sender:TObject;Varaction:TCloseAction)ofObject;
- 从上面结论我们知道可以声明一个外部函数,该外部函数的参数要比TCloseEvent的参数多一个Self指针的,所以我们声明如下:
- procedureMyCloseEvent(Frm:TForm;Sender:TObject;varAction:TCloseAction);
- Frm则是外部在窗体关闭的时候,传递的隐含指针Self
- 该函数整体代码如下:
- procedureMyCloseEvent(Frm:TForm;Sender:TObject;varAction:TCloseAction);
- begin
- showmessage(Frm.Name+‘窗体外部方法调用,不允许关闭窗体!‘);
- Action:=caNone;
- end;
- 点一下,新建的按扭之后,看看是否还可以关闭窗体!!
- 通过汇编来处理
- procedureTForm1.SetEvent(Event:pointer);
- asm
- pushebx//保护Ebx
- movebx,eax//将当前的eax的值,先用ebx保存起来,eax中保存的为Form的开始地
- moveax,edx//将Event指针的值给EAX
- mov[ebx+$2d8],eax//将Eax的值分别写进其高位和低位
- moveax,[edx+4]
- mov[ebx+$2d4],eax
- popebx
- 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)ofobject;这种格式声明的事件(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;
- 在Win32SDK中可以查到EnumSystemLocales要求的回调格式是:
- BOOLCALLBACKEnumLocalesProc(
- LPTSTRlpLocaleString//pointertolocaleidentifierstring
- );
- 而SysUtils中的方法声明:
- TLanguages=class
- ...
- functionLocalesCallback(LocaleID:PChar):Integer;stdcall;
- ...
- end;
- 显然,我们是无法将LocalesCallback这个方法直接传递给EnumSystemLocales的,因为LocalesCallback的函数形式声明实际上是:
- functionLocalesCallback(Self:TLanguages;LocaleID:PChar):Integer;stdcall;
- 比EnumLocalesProc多出来一个参数。
- 所以在TLanguages.Create中,使用了Callback结构变量来生成一小段动态代码。这段代码是构造在堆栈中的(局部变量),转换成汇编是:
- prcoedureCallbackThunk;
- asm
- //取出lpLocaleString参数到EDX寄存器
- //CALLBACKEnumLocalesProc是stdcall调用,参数在堆栈中
- POPEDX
- //将Self对象传给EAX寄存器
- MOVEAXSelf
- //stdcall调用,将Self作为第一个参数压栈
- PUSHEAX
- //将lpLocaleString作为第二个参数压栈
- PUSHEDX
- //用相对跳转指令跳转到TLanguages.LocalesCallback入口地址
- JMPTLanguages.LocalesCallback
- end;
- 将CallbackThunk作为临时的回调函数传递给EnumSystemLocales是合法的。当回调被执行时,前面那小段代码动态修改了堆栈的内容,将本来只有一个参数的调用,变成了两个参数,从而实现了回调与对象方法的转换。
- 但是,正如Passion在前面提到的,由于这小块临时代码是放在堆栈中的,而Win2003的DEP限制了在堆栈中执行代码,导致事实上回调函数并没有被正确地调用。
- Borland程序员也看到了这个问题,所以在BDS2006中,这部分代码的实现修改成:
- 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;

