几篇有关delphi vcl消息机制的文章

Delphi对Windows消息等的封装和窗体的实现

转载,感谢作者的辛勤汗水,这是一篇很精妙的文章,看了一次,可以不用读其他相关的文章的了。

再次感谢作者。

 

开始,由VirtualAlloc想起

  我在查看VirtualAlloc这个API的时候,思绪 竟然跳到另一个地方去了。那是以前阅读VCL源码时遗留下来的问题,Classes单元的MakeObjectInstance函数调用了 VirtualAlloc,我甚是不解,为什么Delphi提供了那么多内存分配函数,而MakeObjectInstance偏偏要用系统提供的 API,更令我不解的是,之后再也不见有VirtualFree的调用,也就是说,VCL其实存在内存泄漏?这个问题我在网上也看到相关的讨论,有人认为 这的确是VCL的Bug,有人甚至修改了Classes单元,在单元的结束节处调用VirtualFree以释放以前分配的内存。

  不 过我对这个问题始终持保留态度,MakeObjectInstance是一个非常重要的函数,担负着窗口过程到对象方法的转换,Borland没有理由留 着这个“Bug”不理。

  于是我重新阅读了MakeObjectInstance这个倍受李维赞誉的函数,我想我这次是读懂了,为什么 不调用VirtualFree,因为没有必要,进程在结束的时候会毫无保留的回收所有的内存,而经由VirtualAlloc分配到的内存就保留在由 TInstanceBlock记录所组成的链表中,这个链表组成的内存并不是使用一次即弃掉的,它是可重用的,调用一次 FreeObjectInstance,那张链表便空余出TObjectInstance大小的内存,以供下一次使用。所以,这其实是一个内存池,提供了 更上一层的内存分配机制。而在结束的时候调用VirtualFree就显得没有任何必要了。

  回到上面提出的第一个问题,为什么要调用 VirtualAlloc,而不用Delphi提供的内存分配函数,如果没有看到System单元的这两个变量,我想永远也不可能找到答案:

   var

  AllocMemCount: Integer; { Number of allocated memory blocks }

  AllocMemSize: Integer; { Total size of allocated memory blocks }

  这两个变量的确是记录内存使用的总量,前提是你调用Delphi提供的内存管理函数,如果调用 Windows原生的API,则VCL是没有办法感应到的。写到这里,再看看上面的描述,也许一切都了然了。

  然而,这只是我写这篇文 章的导火线,真正原因是我读懂了MakeObjectInstance,以前的许多疑惑已经拨云见日,窗口过程到对象方法的脉络在我的脑中从未有过这么清 晰,因此欲罢不能,作此文记之。


  使用,将窗口过程转成对象方法的步骤

   从SDK的角度来讲,设置窗口过程有两种方法(我所能想到的),一是调用RegisterClass,另一个是调用SetWindowLong,第一种 用在创建窗口的时候,另一种用在改变窗口过程的时候。在Delphi中,假设你写了一个自定义窗口类,那么你可以重载WndProc,这个方法就相当于窗 口过程。可以确定,VCL在开始时肯定也是用上面所说的方法,设置窗口过程,只是后来经过一些转换,最终使窗口过程调用到对象实例的WndProc,所以 WndProc可以当成窗口过程来使用。

  这个转换的步骤从表面上看很简单,现在我们不必去深究其原理,只要知道通过下面的做法,就可 以将一个窗口过程转成对象的方法。

  首先,到Controls单元的TWinControl类,这是所有窗口的父类,转换过程就在这里 面完成。TWinControl的构造函数中写了这一句:

  constructor TWinControl.Create(AOwner: TComponent);

  begin

  ... ...

   FObjectInstance := Classes.MakeObjectInstance(MainWndProc);

   ... ...

  end;

  其中的MainWndProc就是代替窗口过程的对象方法。

  接着, 在InitWndProc有如下代码:

  function InitWndProc(HWindow: HWnd; Message, WParam,

  LParam: Longint): Longint;

  Begin

  ... ...

  SetWindowLong(HWindow, GWL_WNDPROC,

   Longint(CreationControl.FObjectInstance));

  ... ...

   end;

  InitWndProc就是刚开始的窗口过程,而调用了SetWindowLong之后,窗口过程就转成了 FobjectInstance了。而实际上最终得到调用是却是MainWndProc。

  最后,在TWinControl的析构函数 中还写了如下语句:

  destructor TWinControl.Destroy;

  begin

   ... ...

  if FObjectInstance <> nil then

   Classes.FreeObjectInstance(FObjectInstance);

  ... ...

   end;

  这是为了回收由MakeObjectInstance使用的内存,让这块内存可在下一次重用。

  上面就是 TWinControl的窗口过程到对象方法的转换步骤,这的确是很神奇的事情,它们在某些情况下是很有用的,比如TComboBox,在这个控件里面有 一个用于编辑的Edit和一个用于下拉选择的ListBox,这两个控件是在ComboBox创建的时候一起创建的,VCL没有办法对它们进行封装,但有 时候需要处理他们的消息,这时,上面的方法就派上用场了,事实上TComboBox就是运用上面的方法,将Edit和ListBox的窗口过程转换成 TcomboBox内部的方法的,有兴趣者请查阅一下VCL。

  对上面进行一次总结:

  1、 假设你通过原生的API创建了一个窗口,如果你想让这个窗口的窗口过程被指定为一个类的方法,那么可以在类的内部调用 MakeInstanceObject,传进类的一个方法(如上面的MainWndProc,当然这个方法必须是TwndMethod类型的),并保留函 数返回的指针。

  2、 调用SetWindowLong,用类保留的指针替换原来的窗口过程。到这里,窗口过程就被传进MakeInstanceObject的对象方法所代替 了。

  3、 在消毁这个类的实例时,别忘了调用FreeObjectInstance,并传回保留的指针。如果这时窗口还未消毁,还得用SetWindowLong恢 复原来的窗口过程。

  知道如何使用并不是我们的最终目的,我们要更进一步,为什么会是这样,请看下一节。


  实 现,窗口过程到对象方法的转换技术

  窗口过程实际上是一个回调函数,向API传递函数的地址,Windows保留着 这个函数地址,在适当的时候调用这个函数。那么对象方法与普通函数有什么不同呢,对于同一种调用规则来说,不同之处就是对象方法在第一个参数之前有一个隐 藏的参数,这个参数就是对象的实例(如果是C++应该叫实例指针,而Delphi的对象实例就是一个指针,只已经为大多数人所共知的事实)。

   另一方面,Windows的API使用的是Stdcall的调用规则,从机器指令的角度看,就是在Call某个函数之前,先将函数的参数从右向左地压 栈。而Delphi为了提高效率,默认使用了Register调用规则,粗略的讲就是从左向右传递参数,且前三个参数分别放在EAX,EDX,ECX寄存 器中,其后则依次入栈。若要知道详细的规则,请查看Delphi的帮助主题:Calling conventions。

  现在,如果我 们想让窗口过程流入某个对象的方法,要解决两个问题:

  1、 在进入对象方法的入口时,先将对象实例作为第一个参数传入,其次再将窗口过程的参数依次传入。对于Register调用规则来说,就是将对象实例赋值给 EAX,再将其他参数按照规则赋给相应的寄存器或者压栈。

  2、 Stdcall规则到Register规则的转换,这个不是必须的,因为Delphi也支持StdCall规则,但对Register规则来说效率更高, 另一方面Delphi对Register规则作了更多的支持,比如Published的属性就只能指定Register规则的方法。

   现在让我们围线着这两个问题开始探索VCL是如何做的。

  VCL在开始的时候同样要遵守Win32的做法,首先填充一个窗口类结构然 后注册窗口类,注意TWinControl.CreateWnd中的这一句:

  WindowClass.lpfnWndProc := @InitWndProc;

  它将窗口过程指定为InitWndProc函数。

  接下来就创建窗口类,在 TWinControl.CreateWindowHandle中:

  FHandle := CreateWindowEx(ExStyle, WinClassName, Caption, Style,X, Y, Width, Height, WndParent, 0, WindowClass.hInstance, Param);

  现在来看,一切都似乎 正常,但其实在调用CreateWindowEx的时候,事情正在稍稍发生变化。CreateWindowEx的时候系统将发送(请注意是发送而不是投 递)一个WM_CREATE消息给窗口,处理这个消息的是谁呢,正是上面看到的InitWndProc。

  有必要看一下这个函数的代 码,我顺便作了详细的注释:

01   function InitWndProc(HWindow: HWnd; Message, WParam,
02   LParam: Longint): Longint;
03   Begin
04   //CreationControl就是窗口类,TWinControl在CreateWnd的时候将Self赋给它
05   //由此可以看到VCL的窗口类是非线程安全的。
06   CreationControl.FHandle := HWindow;
07   //重设窗口过程,从此之后,这个函数再也不会得到调用了
08   SetWindowLong(HWindow, GWL_WNDPROC,
09   Longint(CreationControl.FObjectInstance));
10   if (GetWindowLong(HWindow, GWL_STYLE) and WS_CHILD <> 0) and
11    (GetWindowLong(HWindow, GWL_ID) = 0) then
12   SetWindowLong(HWindow, GWL_ID, HWindow);
13   //设置该窗的一些属性,与我们讨论的无关,可不去理会它们
14   SetProp(HWindow, MakeIntAtom(ControlAtom), THandle(CreationControl));
15   SetProp(HWindow, MakeIntAtom(WindowAtom), THandle(CreationControl));
16   //主动调用一次FobjectInstance
17   asm
18      PUSH LParam
19      PUSH WParam
20      PUSH Message
21      PUSH HWindow
22       MOV EAX,CreationControl
23      MOV CreationControl,0
24      CALL [EAX].TWinControl.FObjectInstance
25      MOV Result,EAX
26    end;
27   end;

  第6行对窗口类的Fhandle进行赋值,这么做是必要的,因为正常情况下 Fhandle只有到CreateWindowsEx返回之后才能得到赋值,在这个函数调用的过程中,系统发送WM_CREATE消息给窗口,在外部,我 们可以得到WM_CREATE的处理器进行处理,如果没有第6行的赋值,则那时我们将没有办法得到窗口句柄。我想这也是InitWndProc存在的原因 之一。

  第8行重新设置窗口过程,设置为窗口类的FobjectInstance,从此以后,窗口消息只会流到 FobjectStance指向的地方,这个函数也就作废了。

  而接下来是一段汇编代码,主要的意思是调用 FobjectInstance,18到21行传递参数(还记得STDCALL规则吗),然后24行调用FobjectInstance。这段汇编就相当 于这样的语句:

  WinControl := CreationControl;

  CreationControl := nil;

  Result := TThunkProc(WinControl.FObjectInstance)(HWindow, Message, WParam, LParam);

  其实这正是Linux版下面的做法。

  在这里我想说一下CALL指令,理解它的行为,对下文是很 有帮助的,CALL指令可以分解为两个动作:先将下一条指令的地址(EIP)压栈,然后跳转到操作数指定的地址去。与CALL对应的是RET指令,这个指 令其实就是从栈顶弹出一个值,然后跳转到这个值指明的地址去。这就是函数的原理,在函数内部,维持堆栈的平衡是非常重要的,你必须保证在RET的时候弹出 来的值正是CALL的时候压入的值,这样才能正确返回到CALL指令的下一条指令的地址,要不然执行点就不知跳到哪里去了?当然使用高级语言不用去关心这 些东西,但理解堆栈的知识仍然是非常有用的。

  在InitWndProc完成它的历史命令之后,我们可以把目光关注到 FobjectInstance这个指针去,现在它就是新的窗口过程,但是它到底指向了什么东西呢,答案就在前面看到的 MakeObjectInstance中,我们要去详细的分解这个函数的代码,不过之前我要从总体上说一下这个过程:

   FobjectInstance指向一块由MakeObjectInstance分配好的内存,这块内存存放的是一段机器指令,这段机器指令其实也是在 MakeObjectInstance写入的,当FobjectInstance得到调用时,就执行了那段指令,这段指令的任务是将对象方法(这个方法就 是传入MakeObjectInstance的那个参数,即MainWndProc)存放在ECX,然后跳转到StdWndProc 去,StdWndProc从ECX取出MainWndProc,并从这个方法中得到对象实例(对象方法其实是一个地址和一个对象实例的组合,详情请看 TMethod帮助),然后构造出一个Tmessage的结构,最后调用MainWndProc,流程完毕。

  为了让读者有一个总体的 认知,我画了下面的流程图:

  


  从 上面的分析看,至少有这么几个元素对转换过程起着至关重要的作用:

  MakeObjectInstance函数

   FObjectInstance以及其指向的内存

  StdWndProc函数

  现在我们就来详细解析它们。

   在TWinControl的构造函数中调用了MakeObjectInstance,并传入TWinControl的一个方 法:MainWndProc。MakeObjectInstance的代码是这样的:

01   function MakeObjectInstance(Method: TWndMethod): Pointer;
02   const
03   //机器指令
04    BlockCode: array[1..2] of Byte = (
05     $59, { POP ECX }
06     $E9); { JMP StdWndProc }
07    PageSize = 4096;
08   var
09    Block: PInstanceBlock;
10    Instance: PObjectInstance;
11   Begin
12   //InstFreeList指向一个TObjectInstance记录,这个记录是当前可用的
13    if InstFreeList = nil then
14    begin
15   //如果InstFreeList为空,就再创建4K的内存,这个内存格式化为一个
16   //TinstanceBlock结构。
17     Block := VirtualAlloc(nil, PageSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
18   Block^.Next := InstBlockList;
19   //对新创建的4K内存进行初始化
20     Move(BlockCode, Block^.Code, SizeOf(BlockCode));
21   Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2], @StdWndProc));
22   //TinstanceBlock里面含有314个TobjectInstance记录,对这些记录进行初始化
23    Instance := @Block^.Instances;
24    repeat
25     Instance^.Code := $E8; { CALL NEAR PTR Offset }
26     Instance^.Offset := CalcJmpOffset(Instance, @Block^.Code);
27     Instance^.Next := InstFreeList;
28     InstFreeList := Instance;
29     Inc(Longint(Instance), SizeOf(TObjectInstance));
30    until Longint(Instance) - Longint(Block) >= SizeOf(TInstanceBlock);
31    InstBlockList := Block;
32   end;
33   //将可用的TobjectInstance块返回,并让InstFreeList指向下一个可用的块
34   Result := InstFreeList;
35   Instance := InstFreeList;
36   InstFreeList := Instance^.Next;
37   //将MainWndProc保存在这里
38   Instance^.Method := Method;
39   end;

  这个函数一个非常重要的任务就是管理一个链表,这个链表的每一项有4096字节大小,每一 项可以认为是一个TinstanceBlock结构(实际上TinStanceBlock只有4092字节,即最后4个字节是没有用的)。这个链表会随着 MakeObjectInstance 的调用而增加链表项,但是不会被释放,到进程结束时由操作系统回收。InstBlockList变量指向这个链表头,可以用下图来表示:

   


   每一个TinstanceBlock的结构是这样的:

  PInstanceBlock = ^TInstanceBlock;

   TInstanceBlock = packed record

   Next: PInstanceBlock; //下一个块

    Code: array[1..2] of Byte; //机器码

   WndProcPtr: Pointer; //指针,相当于操作数

   Instances: array[0..InstanceCount] of TObjectInstance;//314个记录数组

  end;

  Code和WndProcPtr一起组成了一段 机器指令,请回头看看第20和21行,最后Code和WndProcPtr成员一起组成了类似下面这样的指令:

  POP ECX

   JMP Offset

  上面的Offset是另有用意的,它等于WndProcPtr,而Jmp的结果是跳到StdWndProc的 入口点去,为什么能够这样呢,请看第21行:

  Block^.WndProcPtr := Pointer(CalcJmpOffset(@Block^.Code[2], @StdWndProc));

   CalcJmpOffset函数如下
  function CalcJmpOffset(Src, Dest: Pointer): Longint;

  begin

   Result := Longint(Dest) - (Longint(Src) + 5);

  end;

  StdWndProc的地址减去Code[2]的地址与5的 和,为什么Code[2]还要加上5,才能被StdWndProc的地址减呢,原因是Code[2]等到$E9,后面跟一个地址(可以是绝对地址也可以是 相对地址,这里是使用相对地址)就形成了一条JMP指令,$E9占一个字节,地址是一个指针占了4个字节,所以这条指令占用了5个字节,所以 Code[2]要和5加后,被StdWndProc的地址减去后才能得到一个正确的相对地址(其实也就是StdWndProc的地址到JMP指令的距 离)。

  接下来的Instances是一个数组,共有314个,数组的每一项是一个TobjectInstance记录:

   PObjectInstance = ^TObjectInstance;

  TObjectInstance = packed record

  Code: Byte; //机器码

  Offset: Integer; //偏移,操作数

   case Integer of

   0: (Next: PObjectInstance); //可能是指向下一个记录

    1: (Method: TWndMethod); //也可能存放一个方法类型

  end;

  Code和 Offset也组成了一条机器指令,请看第25,26行,这条指令相当于:

  CALL NEAR PTR Offset

   Offset也是通过CalcJmpOffset计算得到的,它指定当前地址到Block的Code处的偏移,也就是调用ObjectInstance 所在的InstanceBlock的Code处的代码。另外,请注意这里是使用CALL而不是JMP,这是有特殊的含意的,你不妨可以思考一下,稍后我会 作解释。

  接下来是一个变体,有可能是Next指向下一个记录,也有可能是一个TwndMethod的变量。看 MakeObjectInstance的代码,在初始化Block块的时候,是将数组中所有项都设成Next的。这样看来,当一个 InstanceBlock新生成时,这个Instances数组也可以当成一个链表了,从第28行可以看出,有一个变量InstFreeList ,就指向了这个表头。

  但在34行下面的几句代码,返回了InstFreeList,并将这个记录指针的Next变成了Method, 将传进来的参数Method赋给它,最后InistFreeList指向下一个ObjectInstance。这样看来,一个InstanceBlock 是否已经用完取决于它里面的Instances数组,如果所有ObjectInstance的最后一个成员是Method,那么表示这个块已经用完了,相 反如果是Next则表示还有ObjectInstance可用。

  至此,我们可以确定,MakeObjectInstance返回的值 (被FObjectInstance所接收),就是一个TobjectInstance的指针,且里面的Method成员的值等于传进函数中的参数的值 (即MainWndProc)。

  好了,让我们对MakeObjectInstance的行为作一些总结吧:

  如果 InstFreeList变量是空,表示InstanceBlock链表没有创建或者已经没有可用的ObjectInstance项了,这时要创建一个新 的InstanceBlock记录,并对它进行初始化,接着让这个新的块作为链表头,即InstBlockList指向它,而它的Next成员则指向原来 的表头。最后,块里面第一个ObjectInstance的Method被赋值,并作为函数结果返回。而InstFreeList则指向下一个 ObjectInstance。

  如果InstanceBlock链表里面还有可用的ObjectInstance项,过程就相对简单 一点,对InstFreeList指向的ObjectInstance的Method成员赋值,并将它作为函数结果返回。InstFreeList则指向 下一个可用的ObjectInstance。这个过程可以用下面的图来分解:

  

  


  那 么是不是说明已用的ObjectInstance再也收不回来了呢,其实不是,那些ObjectInstance都保存在各个窗口类当中,如果调用了 FreeObjectInstance,则可以将这些内存回收回来,FreeObjectInstance的代码是这样的:

01  procedure FreeObjectInstance(ObjectInstance: Pointer);
02  begin
03    if ObjectInstance <> nil then
04    begin
05     //将回收的ObjectInstance的Next成员指向InstFreeList指向的内存块
06     PObjectInstance(ObjectInstance)^.Next := InstFreeList;
07     //InstFreeList指向被回收的内存
08     InstFreeList := ObjectInstance;
09    end;
10  end;


  讲完了上面的内存块管理,现在可以将普通函数到对象方法的流程走一遍,其实这一个 过程经过上面的讲解之后已经顺理成章,只要照着执行流程走下去就是

  假设窗口接收到一个消息,则窗口类中的 FObjectInstance得到调用,实际上就是执行ObjectInstance这块内存里面的指令。

   ObjectInstance的Code和Offset成员组成了这样的指令:

  CALL NEAR PTR Offset

   我们知道这条指令将使执行点跳到Offset处的代码,通过上面的分析知道Offset处的代码就在这个ObjectInstance记录所在的 InstanceBlock的Code处。另外一个非常重要的信息是Call指令调用时,会将下一条指令地址压栈,那么这里的下一条指令地址是什么呢?不 就正是下面的Method成员的地址吗。所以,我们要紧记堆栈的现场,下图是调用上面的Call指令后的堆栈:


  

  栈是向低的 地址增长的,我们假设低地址在上面,而往下则地址渐增,因此图示像上面那样,栈顶以下的第二个值是窗口过程的返回地址,想一下Windows在调用我们的 窗口过程的时候也是用Call指令的,所以当然要将Call指令的下一条指令地址压栈,这里所谓的“窗口过程的返回地址”指的就是Windows调用窗口 过程的下一条指令地址。只要我们的窗口过程最终在Ret的时候,从栈中弹出的是这个值,那么窗口过程就正确地完成它的任务了。

  回过头 来,CALL指令之后,执行点已经到InstanceBlock的Code数组处了,这个Code数组和它下面的WndProcPtr一起组成下面的指 令:

  POP ECX

  JMP Offset

  第一行,将栈顶弹出的值存入ECX,这个值当然就 是ObjectInstance.Method的地址。第二行执行一个JMP,JMP指令的一个好处就是不会对堆栈有任何影响,通过上面分析得知这次是跳 到StdWndProc的入口点去了,记住现在的堆栈:


  

  现在执行点 到了非常重要的StdWndProc处,从上面的堆栈现场看,可以认为StdWndProc就是由Windows调用的窗口过程,只是这个时候对象方法的 地址正保存在ECX中,看下面的代码:

01  { 标准窗口过程 }
02  { In ECX = 方法指针的地址 }
03  { Out EAX = 返回结果 }
04  function StdWndProc(Window: HWND; Message, WParam: Longint;
05  LParam: Longint): Longint; stdcall; assembler;
06   asm
07   XOR EAX,EAX
08   PUSH EAX
09   PUSH LParam
10   PUSH WParam
11   PUSH Message
12   MOV EDX,ESP
13   MOV EAX,[ECX].Longint[4]
14   CALL [ECX].Pointer
15   ADD ESP,12
16   POP EAX
17  end;

  第7到第11行,实际上它是在堆栈上构造一个Tmessage结构,这个结构正是 TwndMethod类型的方法所需要的唯一参数,Tmessage可简化为这样:

  TMessage = packed record

   Msg: Cardinal;

   WParam: Longint;

    LParam: Longint;

   Result: Longint);

  end;

  所以第8 行推入的Result,第9行推入的Lparam,以此类推。

  第12行将栈顶赋值给EDX,记得Register调用规则吗,EDX 正是我们看得到的第一个参数,而这个参数被赋给了一个Tmessage记录的地址(其实就是栈顶)。我们看一下现在的堆栈现场:

  


   之所以会有“上一个EBP”这一项,是在StdWndProc的ASM处有一个EBP压栈的指令,尽管这是一个非常有用的技术,但对我们的主题没有任何意 义,所以就略去不讲了。

  接着看第13行,ECX是方法指针的地址,那么[ECX]就得到方法指针本身了,而 [ECX].Longint[4]是方法指针首地址偏移4个字节处,正是方法指针对应的对象实例,为了让读者更明白,我画了下面的图揭示方法指针的内存分 布:

  


  现 在EDX存Tmessage的地址,EAX存对象实例,看看Twndmethod的声明

  TWndMethod = procedure(var Message: TMessage) of object;

  想想Register的调用规则,我们得 出结论,参数传递已经完成,接下来当然是调用对象方法,看第14行,做的就是这个事情。也就是这个时候,当初通过MakeObjectInstance传 进来的MainWndProc得到调用了,流程终于走到对象的方法去了。

  MainWndProc如何做我们大可不去理会,现在来看在 MainWndProc调用完后的第15行,栈顶加12表示栈顶的前3个值出栈(记住栈是向低处增长的),那么现在的栈顶就是Tmessage结构的 Result。再看第16行,将Result弹出给EAX,将这作为函数的返回值。看一下堆栈:

  


  在 StdWndProc的End处,先有一个Pop EBP的动作,才有一个Ret指令,所以最终能够正确的返回窗口过程。整个过程到这里结束。


   这真是一个激动人心的时刻,尽管李维的Inside VCL对于这一主题有详尽的描述,但只有自己将整个流程走通,才能真正理解这一个转换的过程。

   我们已经走得很远了,不过我们可以走得更远一些,让我们更进一步,来讨论回调函数到对象方法的转换过程吧。


  扩展,将对象方 法设为回调函数

  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调用了回调函数,执行点到了上面的代码,此时栈顶是回调函数的返回地址,下面则是回调函数所需要 的参数,那么这段指令就是将回调函数的返回地址下移一个栈值,再将对象指针存到函数返回地址原来的位置,先后两种情况的堆栈是这样的:


   

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

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

  push [ESP]

  mov [ESP+4], ObjectAddr

   jmp MethodAddr

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

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

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

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

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

01  unit HookKeyBoard;
02
03  interface
04  uses
05  Windows, Messages, Classes, Forms, Controls, CallBackToMethod;
06
07  type
08   TKeyEventEx = procedure(Sender: TObject; IsDown: Boolean;
09  ShiftState: TShiftState; Key: Word) of object;
10
11  TKeyBoardHook = class
12  private
13  HHK: HHOOK;
14  P: Pointer;
15  FActive: Boolean;
16  FKeyEvent: TKeyEventEx;
17  procedure SetActive(const Value: Boolean);
18  function KeyboardProc(code: Integer;
19  wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
20  protected
21  function DoKeyEvent(IsDown: Boolean; ShiftState: TShiftState;
22  Key: Word): Boolean; virtual;
23  public
24   constructor Create;
25   destructor Destroy; override;
26   property Active: Boolean read FActive write SetActive;
27    property OnKeyEvent: TKeyEventEx read FKeyEvent write FKeyEvent;
28  end;
29
30  implementation
31
32  uses SysUtils;
33
34  { TKeyBoardHook }
35
36  constructor TKeyBoardHook.Create;
37   var
38   M: TMethod;
39  begin
40   M.Code := @TKeyBoardHook.KeyboardProc;
41   M.Data := Self;
42   P := MakeInstruction(M);
43  end;
44
45  destructor TKeyBoardHook.Destroy;
46  begin
47   SetActive(False);
48   FreeInstruction(P);
49   inherited;
50  end;
51
52  function TKeyBoardHook.DoKeyEvent(IsDown: Boolean;
53  ShiftState: TShiftState; Key: Word): Boolean;
54  begin
55   if Assigned(FKeyEvent) then
56   FKeyEvent(Self, IsDown, ShiftState, Key);
57   Result := False;
58  end;
59
60  function TKeyBoardHook.KeyboardProc(code: Integer; wParam: WPARAM;
61  lParam: LPARAM): LRESULT;
62  var
63   IsKeyDown: Boolean;
64   ShiftState: TShiftState;
65   CharCode: Word;
66   begin
67    if code >= 0 then
68     begin
69      ShiftState := KeyDataToShiftState(lParam);
70      CharCode := LOWORD(wParam);
71      IsKeyDown := lParam and $80000000 = 0;
72      if DoKeyEvent(IsKeyDown, ShiftState, CharCode) then
73       begin
74        Result := 1;
75        Exit;
76       end;
77     end;
78      Result := CallNextHookEx(HHK, code, wParam, lParam);
79    end;
80
81    procedure TKeyBoardHook.SetActive(const Value: Boolean);
82     begin
83     if FActive <> Value then
84      begin
85       if Value then
86        begin
87         HHK := SetWindowsHookEx(WH_KEYBOARD, P, HInstance, 0);
88          if HHK = 0 then
89           raise Exception.Create('can not install a keyboard hook');
90        end
91       else
92        UnhookWindowsHookEx(HHK);
93       FActive := Value;
94      end;
95   end;
96
97  end.

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

  请用CallbackToMethod单元多测试一些例子,如果有什么错误,欢迎指正,这个转换 的功劳应该归于Savetime,我只是作了一些优化,谈不上什么创造。

 

可以参考的 文章有:

 

http://blog.csdn.net/drawwingmap/archive/2004/11/03/165431.aspx

 

 

消息中回调应用

 

http://hi.baidu.com/broland/blog/item/9d03ac1323383ac6c3fd7886.html

 

 

http://www.z6688.com/info/48701-1.htm

 

 

// 参考博客

http://blog.csdn.net/zwzgood/archive/2009/05.aspx

 

 

 

http://hi.baidu.com/hapbin/blog/item/44ce2125d789262dd5074215.html

 

 

http://blog.csdn.net/nisky/archive/2001/09/16/5338.aspx

 

 

http://www.cnblogs.com/raymond2/archive/2005/12/26/304848.html

 

 

vcl 中的代码理解vcl 中的消息 处理机制  (drawwingmap )   
delphi,一个非常优秀的开发工具,拥有强大的可视化开发环境、面向组件的快速开发模式、优秀的vcl类库、快速的代码编译器、强大的数据库和web开发能力、还有众多的第三方控件支持...(此处省略x千字...
blog.csdn.net/drawwingmap/archive/2004/11/03/165431.aspx  -2004-11-03

关于vcl 的编写 (二) 简单介绍一下vcl消息 传递  (dreamnan )   
简单介绍一下 vcl 消息传递 <?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> vcl 除了封...
blog.csdn.net/dreamnan/archive/2004/09/15/105689.aspx  -2004-09-15
深入c++ builder之编写自己的元件-深入分析vcl 继承、消息 机制(3)  (aweay )   
这篇文章提及内容可能大家已经在很多地方看到过了,作者也是如此,只不过还看了很多 vcl 源代码,加上自己实际编写元件的经验,拼凑了这么一篇文章.所以所有言论都是个人观点、经验的描述,仅供参考. <?x...
blog.csdn.net/aweay/archive/2003/06/25/17071.aspx  -2003-06-25
深入c++ builder之编写自己的元件 -深入分析vcl 继承、消息 机制(2)  (aweay )   
这篇文章提及内容可能大家已经在很多地方看到过了,作者也是如此,只不过还看了很多 vcl 源代码,加上自己实际编写元件的经验,拼凑了这么一篇文章.所以所有言论都是个人观点、经验的描述,仅供参考. 你可转...
blog.csdn.net/aweay/archive/2003/06/25/17070.aspx  -2003-06-25
深入c++ builder之编写自己的元件-深入分析vcl 继承、消息 机制(1)  (aweay )   
这篇文章提及内容可能大家已经在很多地方看到过了,作者也是如此,只不过还看了很多vcl源代码,加上自己实际编写元件的经验,拼凑了这么一篇文章.所以所有言论都是个人观点、经验的描述,仅供参考. 你可转载,...
blog.csdn.net/aweay/archive/2003/06/25/17067.aspx  -2003-06-25
关于捕获vcl 没有处理的windows消息  (tone_zrt )   
对于c++ builder的程序员来说,vcl以其灵活、高效的特点令人喜爱.因为vcl是在windows api的基础上进行了封装,同时舍弃了一些不常用的功能,所以,vcl在功能上是windows a...
blog.csdn.net/tone_zrt/archive/2001/11/23/13407.aspx  -2001-11-23
vcl消息 处理初探  (nisky )   
tobject是基类,所以我们先看一下tobject的dispatch方法.dispatch根据传入的message来寻找相应的消息处理方法,如果找不到的话,就继续向上到父类的消息处理方法表中寻找响应...
blog.csdn.net/nisky/archive/2001/09/16/5338.aspx  -2001-09-16
深入vcl 理解bcb的消息 机制(三)  (cker )   
方法3.rh指出的来自tapplication的方法不用我多废话,大家都知道tapplication在bcb中的重要性.在bcb的帮助中指出:tapplication、tscreen和tform构成了...
blog.csdn.net/cker/archive/2001/06/25/4218.aspx  -2001-06-25
深入vcl 理解bcb的消息 机制(二)  (cker )   
方法2.重载tcontrol的wndproc方法还是先谈谈vcl的继承策略.vcl中的继承链的顶部是tobject基类.一切的vcl组件和对象都继承自tobject. 打开bcb帮助查看tcontro...
blog.csdn.net/cker/archive/2001/06/16/4217.aspx  -2001-06-16


深入 vcl 理解bcb的 消息 机制 (一)  (cker )   
引子本文所谈及的技术内容都来自于internet的公开信息.由cker在闲暇之际整理后,贴出来以飴网友,姑且妄称原创. <每次在国外网站上找到精彩文章的时候,心中都会暗自叹息为什么在中文网站难以觅得这...
blog.csdn.net/cker/archive/2001/06/09/4216.aspx  

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值