将Delphi的对象方法设为回调函数

摘要:
一时兴起,为了实现更好的通用性和封装性,我们需要使用类方法作为回调函数来查找好文章并转发摘录。如果有一种方法可以将普通回调函数转换为对象,那就太好了。事实上,VCL的MakeObjectInstance函数为我们开创了先例。虽然它只转换窗口回调函数,但我们也可以模仿普通回调函数。声明指向此内存块的指针,并将其作为回调函数传递给API。

心血来潮,为了实现更好的通用性和封装性,需要把类方法作为回调函数,搜得一篇好文,节选转发。命名似乎应该是MethodToCallback才合适,可惜调试时总是报错,debugging。


Win32的API有一些需要回调函数,说白了就是函数指针,比如钩子,列举窗口等等。如果我们要对这些技术进行面向对象的封装,就要遇到一些难题。拿钩子来说,假设我们要封装一个键盘钩子,设计一个TKeyboard Hook类,并提供一个Active属性,如果Active属性为True,就调用SetWindowsHookEx安装一个键盘钩子,如果Active为False,就调用UnhookWindowsHookEx卸载键盘钩子,一切看起来都很好,但是调用SetWindowHookEx时需要提供一个HOOKPROC类型的回调函数,而我们并不能用一个对象的方法去作为回调函数传进去。如果有一种方法,能将普通的回调函数转换成对象的方法,那将是很棒的事情,其实VCL的MakeObjectInstance函数已经为我们开了先河,尽管它只是转换了窗口的回调函数,但对于一般的回调函数,我们同样可以仿照着做。

上文中提到过在同一种调用规则下,Win32的API与对象方法之间的差别,仅有的一点就是多了个Self的隐藏参数。由于MakeObjectInstance只是针对窗口的回调函数,参数是确定的,所以可以多做一些功夫,把StdCall转成Register调用规则。但扩展到所有的回调函数,情况就复杂得多了,你不知道这个回调函数的参数个数,因此没法进行调用规则的转换。既然如此,我们退一步,让对象方法必须也是StdCall调用规则,作这一让步并不需要付出多大的代价,你只需要把这个对象方法作为中转站,在方法里面调用Register版的方法即可,而剩下的事情由编译器帮我们做就行了。

基本的原理与上文的描述是很相似的,即提供一个内存块,内存块中保留着一段机器指令,这段指令最终能够调用到对象的指定方法。声明一个指向这个内存块的指针,将它作为回调函数传进API中。

在我即将完成这个有趣的事情而感到兴奋时,我看到网上已经有人实现了这样的转换,那就是大富翁的SaveTime,我在他的2004学习笔记中看到了“让类成员函数成为Windows回调函数的方法”,原来在两年多前就有人完成了这样的事情,看来我的此举是有些多余了,我认真看了Savetime的实现方法,基本的思路是差不多的,不过他写到内存块中的机器指令似乎不是很好,他的指令是这样:

MOV EAX, [ESP];//栈顶的值存到EAX中,此时栈顶的值即是回调函数返回地址

PUSH EAX;//将EAX入栈,

MOV EAX, ObjectAddr;

MOV [ESP+4], EAX;//将对象地址作为对象方法的第一个参数

JMP FunctionAddr;//跳到对象方法去

这段指令实现的功能与我原来想的一样,我们知道在调用API时,要先将参数从右到左的入栈,然后调用函数。我们假设Windows调用了回调函数,执行点到了上面的代码,此时栈顶是回调函数的返回地址,下面则是回调函数所需要的参数,那么这段指令就是将回调函数的返回地址下移一个栈值,再将对象指针存到函数返回地址原来的位置,先后两种情况的堆栈是这样的:

将Delphi的对象方法设为回调函数第1张

如图2所示,此时已经完成了调用对象方法所需要的一切工作,接下来跳到对象方法的入口点去就行了。

这段代码的思路是正确的,不过我认为有一点值得考虑,就是EAX,如果之前EAX的值是有用的,那么执行这段指令之后,它的值就被破坏了,最好的情况就是不要使用寄存器,我将指令优化了一下,成了下面这样子:

push[ESP]

mov[ESP+4], ObjectAddr

jmpMethodAddr

现在只需要三条指令就可以完成了,现实的功能是一样,从机器指令的大小来算,Savetime的需要18字节,而我的指令只需要16字节,所以在空间方面也有所减少。由此看来,我所做的并非无用功呀,呵呵!

至此已经万事具备,应该将代码列出来了,我写了一个CallbackToMethod的单元,这个单元具有一定的通用性,可以应用到你需要的地方去,请看下面的代码:

01unitCallBackToMethod;
02
03{*******************************************
04*brief:回调函数转对象方法的实现
05*autor:linzhenqun
06*date:2006-12-18
07*email:linzhengqun@163.com
08********************************************}
09{
10说明:本单元的实现方法是一种比较安全的方式,其中不破坏任何寄存器的值,并且
11指令的大小只有16字节。
12使用:下面是推荐的使用方法
131.在类中保存一个指针成员P:Pointer
142.在类的构造函数中创建指令块:
15var
16M:TMethod;
17begin
18M.Code:=@MyMethod;
19M.Data:=Self;
20P:=MakeInstruction(M);
21end;
223.调用需要回调函数的API时,直接传进P即可,如:
23HHK:=SetWindowsHookEx(WH_KEYBOARD,P,HInstance,0);
244.在类的析构函数中释放指令块
25FreeInstruction(P);
26注意:作为回调函数的对象方法必须是StdCall调用规则
27}
28
29interface
30
31(*创建回调函数转对象方法的指令块*)
32functionMakeInstruction(Method:TMethod):Pointer;
33(*消毁指令块*)
34procedureFreeInstruction(P:Pointer);
35
36implementation
37
38usesSysUtils;
39
40type
41{
42指令块中的内容相当于下面的汇编代码:
43----------------------------------
44push[ESP]
45mov[ESP+4],ObjectAddr
46jmpMethodAddr
47----------------------------------
48}
49PInstruction=^TInstruction;
50TInstruction=packedrecord
51Code1:array[0..6]ofbyte;
52Self:Pointer;
53Code2:byte;
54Method:Pointer;
55end;
56
57functionMakeInstruction(Method:TMethod):Pointer;
58const
59Code:array[0..15]ofbyte=
60($FF,$34,$24,$C7,$44,$24,$04,$00,$00,$00,$00,$E9,$00,$00,$00,$00);
61var
62P:PInstruction;
63begin
64New(P);
65Move(Code,P^,SizeOf(Code));
66P^.Self:=Method.Data;
67P^.Method:=Pointer(Longint(Method.Code)-(Longint(P)+SizeOf(Code)));
68Result:=P;
69end;
70
71procedureFreeInstruction(P:Pointer);
72begin
73Dispose(P);
74end;
75
76end.

60行是机器指令,实现的功能就是注释中的汇编,请不要被这些数字吓倒,只要先写好汇编,用CPU窗口一查就知道了,至少我就是这么做的。

在上文中曾说到封装一个键盘钩子,下面就是一个简单的实现版本:

01unitHookKeyBoard;
02
03interface
04uses
05Windows,Messages,Classes,Forms,Controls,CallBackToMethod;
06
07type
08TKeyEventEx=procedure(Sender:TObject;IsDown:Boolean;
09ShiftState:TShiftState;Key:Word)ofobject;
10
11TKeyBoardHook=class
12private
13HHK:HHOOK;
14P:Pointer;
15FActive:Boolean;
16FKeyEvent:TKeyEventEx;
17procedureSetActive(constValue:Boolean);
18functionKeyboardProc(code:Integer;
19wParam:WPARAM;lParam:LPARAM):LRESULT;stdcall;
20protected
21functionDoKeyEvent(IsDown:Boolean;ShiftState:TShiftState;
22Key:Word):Boolean;virtual;
23public
24constructorCreate;
25destructorDestroy;override;
26propertyActive:BooleanreadFActivewriteSetActive;
27propertyOnKeyEvent:TKeyEventExreadFKeyEventwriteFKeyEvent;
28end;
29
30implementation
31
32usesSysUtils;
33
34{TKeyBoardHook}
35
36constructorTKeyBoardHook.Create;
37var
38M:TMethod;
39begin
40M.Code:=@TKeyBoardHook.KeyboardProc;
41M.Data:=Self;
42P:=MakeInstruction(M);
43end;
44
45destructorTKeyBoardHook.Destroy;
46begin
47SetActive(False);
48FreeInstruction(P);
49inherited;
50end;
51
52functionTKeyBoardHook.DoKeyEvent(IsDown:Boolean;
53ShiftState:TShiftState;Key:Word):Boolean;
54begin
55ifAssigned(FKeyEvent)then
56FKeyEvent(Self,IsDown,ShiftState,Key);
57Result:=False;
58end;
59
60functionTKeyBoardHook.KeyboardProc(code:Integer;wParam:WPARAM;
61lParam:LPARAM):LRESULT;
62var
63IsKeyDown:Boolean;
64ShiftState:TShiftState;
65CharCode:Word;
66begin
67ifcode>=0then
68begin
69ShiftState:=KeyDataToShiftState(lParam);
70CharCode:=LOWORD(wParam);
71IsKeyDown:=lParamand$80000000=0;
72ifDoKeyEvent(IsKeyDown,ShiftState,CharCode)then
73begin
74Result:=1;
75Exit;
76end;
77end;
78Result:=CallNextHookEx(HHK,code,wParam,lParam);
79end;
80
81procedureTKeyBoardHook.SetActive(constValue:Boolean);
82begin
83ifFActive<>Valuethen
84begin
85ifValuethen
86begin
87HHK:=SetWindowsHookEx(WH_KEYBOARD,P,HInstance,0);
88ifHHK=0then
89raiseException.Create('cannotinstallakeyboardhook');
90end
91else
92UnhookWindowsHookEx(HHK);
93FActive:=Value;
94end;
95end;
96
97end.

代码中没有作什么注释,那不是我们的重点。可以覆盖DoKeyEvent方法,以实现功能更丰富的键盘钩子类。

免责声明:文章转载自《将Delphi的对象方法设为回调函数》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇32位linux系统操作大于2G文件方法ShowDoc速记下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Delphi的TService的輸入桌面切換(服务程序)(windows登录界面如何截图)(使用了OpenDesktop和GetThreadDesktop等API)

dfm: object CopyDeskService: TCopyDeskServiceOldCreateOrder = FalseOnCreate = ServiceCreateOnDestroy = ServiceDestroyAllowPause = FalseDisplayName = 'Copy Desk Service'Interactive...

单片机中的ROM,RAM和FLASH的作用

ROM,RAM和FLASH的区别,下面主要是具体到他们在单片机中的作用。 一、ROM,RAM和FLASH在单片中的作用ROM——存储固化程序的(存放指令代码和一些固定数值,程序运行后不可改动)c文件及h文件中所有代码、全局变量、局部变量、’const’限定符定义的常量数据、startup.asm文件中的代码(类似ARM中的bootloader或者X86中的...

栅格化

OpenGL 栅格化 栅格化是将凹多边形或自相交多边形分割成凸多边形的过程。由于OpenGL只接受渲染凸多边形,那些非凸多边形在渲染之前必须栅格化。 左边为凹四边形,中间为有洞多边形,右边为自相交图形 概述 栅格化的基本过程是将非凸多边形的所有顶点发送到栅格器而不是直接发送到OpenGL渲染管线,然后由栅格器对多边形栅格化。最后,当栅格过程结束,栅格器...

delphi Drag and Drop sample 鼠标拖放操作实例

Drag and Drop is a common operation that makes the interface user friendly: a user can drag/drop information to controls instead of having to type etc. The following sample explai...

delphi ehLib 安装包下载及安装方法

1.下载安装包,这里提供一个百度云盘共享链接,D7-XE8都有:https://pan.baidu.com/s/1DTlxok4RiSmDokuabnGvQw2.添加环境变量,菜单"Tools"->"Options"->左侧"Environment Options"->"delphi Options"->"Library"->...

Win32编程

    Win32编程 此资料为ITjob软件开发教程网提供,特此分享,互相学习! C/C++/VC/MFC技术交流群:95453496 一、Win32编程基本概念 1、消息驱动 在介绍Windows消息驱动概念之前,我们首先来回顾面向过程的程序结构:main()程序有明显的开始、中间过程和结束点,程序是围绕这个过程编写好相关的子过程,再把这些子过程串联...