首页 > 代码库 > Delphi对象变成Windows控件的前世今生(关键是句柄和回调函数)

Delphi对象变成Windows控件的前世今生(关键是句柄和回调函数)

----------------------------------------------------------------------
第一步,准备工作:预定义一个全局Win控件变量,以及一个精简化的Win控件类
var
CreationControl: TWinControl = nil; // 定义全局变量,用来表示每次刚创建的Win控件

TWinControl = class(TControl)
private
FDefWndProc: Pointer; // 记录原有的窗口过程,但只有真正创建句柄的时候才会记录。只有Windows控件才有默认窗口处理过程,而TControl有FWindowProc,不是一回事
FObjectInstance: Pointer; // 普通指针(连函数指针都不是)。当转发消息的时候,使用这个普通窗口函数地址(不是类窗口函数地址)。控件创建的时候就会做转换。
FHandle: HWnd; // Windows窗口的真实句柄
FParentWindow: HWnd; // 父窗口的句柄也要记录下来。父控件(类,具有许多额外功能)与父句柄(Windows指针,特简单)不是一回事。这个属性在一般VCL控件里根本用不到,只有ActiveX可能用到
protected
procedure MainWndProc(var Message: TMessage); // 非虚函数,调用WindowProc函数,不希望被覆盖(如果要覆盖就覆盖WndProc函数,而且这也不是唯一的办法)
procedure WndProc(var Message: TMessage); override; // 虚函数,处理少部分消息,最后调用父类同名函数
// 创建和销毁窗口句柄,按调用顺序排列:
procedure CreateHandle; virtual; // 虚函数,关键入口,被UpdateShowing和HandleNeeded调用,事实是子类从来没有被覆盖。
procedure CreateWnd; virtual; // 虚函数,注册窗口类。很多子类都覆盖它,为的是加上一些额外的功能,比如TEdit
procedure CreateParams(var Params: TCreateParams); virtual; // 第一次出现。只有windows控件才需要准备一大堆内容
procedure CreateWindowHandle(const Params: TCreateParams); virtual; // 虚函数,简单函数,调用API, 看名字就很清楚功能。子类有时候覆盖它,TEdit和TMemo
end;

----------------------------------------------------------------------
第二步,调用控件构造函数,申请Delphi控件对象的内存空间。此时这个内存中的控件:
1. 没有Windows句柄,
2. 预备了一个MakeObjectInstance转换后的窗口回调函数指针FObjectInstance,它封装了MainWndProc函数(或者说,它就是MainWndProc函数)。MainWndProc封装了程序员要用到的窗口回调函数WndProc。但这步仅仅是预备窗口函数指针FObjectInstance,并没有做任何使用和设置,使它与一个Windows窗口联系起来。
到这步,在内存中还仅仅是简单的Delphi内存对象,并没有把它与Windows操作系统联系起来使之真正成为一个Windows窗口对象。

TButton.Create;
调用inherited Create(AOwner);

TWinControl.Create;
调用inherited Create(AOwner);
调用FObjectInstance := Classes.MakeObjectInstance(MainWndProc); // 全局函数,把类函数指针MainWndProc转换成 普通指针(连函数指针都不是)。注意只有Windows控件才有这项

----------------------------------------------------------------------
第三步,依次调用函数,注册Windows窗口类,使之与当前Delphi对象联系起来(其实是Delphi对象包含它,因为Delphi对象包括了许多其它内容),最关键的有:
0. 在CreateHandle中(即入口函数),它会调用CreateWnd函数,而CreateHandle本身又会被UpdateShowing和HandleNeeded调用,其中UpdateShowing会被TWinControl.UpdateControlState;调用,UpdateControlState会被TWinControl.InsertControl调用,InsertControl会被TControl.SetParent调用,详情见:
http://www.cnblogs.com/findumars/p/3917061.html
http://www.cnblogs.com/findumars/p/3667031.html
1. 在CreateWnd中,根据Delphi控件的值,准备Params
2. 在CreateWnd中,强行取消注册当前Delphi类(比如TButton),然后设置窗口函数Params.WindowClass.lpfnWndProc := @InitWndProc;
3. 在CreateWnd中,重新注册了Windows窗口Windows.RegisterClass(Params.WindowClass)
4. 在CreateWnd中,执行CreationControl := Self; 此时这个CreationControl就是代表Delphi内存控件
5. 在CreateWnd中,执行CreateWindowHandle(Params); 真正创建Windows窗口,并立即给这个窗口发送WM_NCCREATE消息,在函数返回之前,就跳转到回调函数InitWndProc里执行(即后面的6~10),然后才将其句柄赋值给Delphi控件属性FHandle(这个赋值其实多余,去掉赋值照样没问题,因为在回调函数里已经赋值了)。
注意1,通过实验发现,在CreateWindowEx这个WINAPI返回之前,就已经发送了WM_NCCREATE消息,因此WINAPI返回之前就会执行InitWndProc回调函数。可以这样理解:CreateWindowEx函数的内部实现就是先创造FHandle,然后就是SendMessage(FHandle, WM_NCCREATE),回调函数会立刻工作,而此时还没有跳出CreateWindowEx函数呢,因为后面还有两个消息要发送,外加其它善后事宜。
我的理解是,只要成功创建了这个windows窗口就会有句柄(这是将来消息找到这个窗口的唯一依据),不管这个windows窗口是否显示,更不管它是否与Delphi对象相联系,Windows都会给它发送WM_NCCREATE消息。注意这个Windows的窗口函数在注册Windows窗口类的时候就已经存在了(即InitWndProc),所以一定可以执行和处理这个消息。
注意2,由于在回调函数里已经给FHandle属性赋值了,所以FHandle := CreateWindowEx(ExStyle...),这里的FHanle赋值可以去掉,运行几个demo都正常。但是百思不得其解的是,把InitWndProc的CreationControl.FHandle := HWindow;屏蔽掉,留下FHandle := CreateWindowEx(ExStyle...)却始终报错A call to an OS function failed。经过检测,发现此时CreateWindowEx的返回值为0,不懂为什么。
6. 在InitWndProc中,当第一个消息(WM_NCCREATE)来的时候,就执行CreationControl.FHandle := HWindow;,这样当前Delphi控件第一次有了句柄(最关键的第一步)。
注意,必须执行这一步,如果屏蔽这句话就会出现A call to an OS function failed的错误。即使想了个花招(这招可以使主Form和Button正常创建,然后用Button动态创建TEdit,且Edit.tag=100,这样可以专用测试),if (CreationControl.tag<>100) CreationControl.FHandle := HWindow; 也不行。报错的语句显然是if FHandle = 0 then RaiseLastOSError; 通过单步测试,此时InitWndProc仍可正常执行,但不知道为什么FHandle := CreateWindowEx(ExStyle...)的返回值就变0了。
7. 在InitWndProc中,重新设置以HWindow代表的Windows窗口实例(也就是Delphi控件实例)的窗口函数为预设的FObjectInstance,这样当前Delphi控件的窗口回调函数就是FObjectInstance了,即指向Delphi类的虚函数WndProc了(最关键的第二步)。
8. 在InitWndProc中,对回调函数所需要的4个参数依次压栈,使之符合Windows标准回调函数的stdcall口味
9. 在InitWndProc中,将CreationControl的地址值转移到EAX,并将CreationControl清空,即CreationControl代表的Delphi控件实例的临时任务完成了,准备让下一个新的Delphi控件实例使用
10.在InitWndProc中,使用EAX到内存中找到当前Delphi控件,把它转化成TWinControl,然后直接调用它的FObjectInstance函数处理消息,参数就是刚才压栈的那些参数,这样第一个消息就处理完毕了。处理这个消息的目的有多个,都十分重要:
1)记录windows控件的句柄到Delphi对象的属性里
2)把这个Windows窗口的回调函数替换为Delphi对象的FObjectInstance,使之间接调用Delphi对象的虚函数WndProc,方便程序员改写
3)用三种方法在全局记录这个windows句柄的ID
4) 上述三个主要目的已经达到,所以尽管WM_NCCREATE消息本身没什么用(一般情况下,因为程序员仍可改写),但消息来了必须处理,所以通过变换手段之后,使用新的回调函数FObjectInstance对消息进行处理。如果程序员也需要使用WM_NCCREATE消息执行某些逻辑,仍可在WndProc和动态函数中正常执行。有一个疑问是,如果屏蔽这段汇编就会出错,错误停留在TWinControl.DefaultHandler的CallWindowProc(FDefWndProc,FHandle,Msg,WParam,LParam);处;把这段汇编改成CALL DefWindowProc也是一样的错误,原因可能是消息必须处理?
11.在CreateHandle中,以当前Delphi控件的FHandle属性为依据,调用SetWindowPos显示了这个Windows窗口,对于一般程序员的理解,就是显示了这个Delphi控件
需要强调的是,以上11个步骤,每次生成TButton实例都要这样来一遍,尤其是第二步重新改写Delphi类(比如TButton)的回调函数,然后重新注册,重新替换回调函数为FObjectInstance。

实际调用关系如下:
TWinControl.CreateHandle;
调用CreateWnd;
调用SetWindowPos(FHandle,SWP_NOMOVE + SWP_NOSIZE + SWP_NOACTIVATE);

TWinControl.CreateWnd;
申请Params: TCreateParams;
调用CreateParams(Params);
调用FDefWndProc := Params.WindowClass.lpfnWndProc; // 更改之前,先记录到Delphi的类属性
调用Params.WindowClass.lpfnWndProc := @InitWndProc; // 更改为Delphi的全局函数,参数一致。
调用CreationControl := Self; // 全局变量,只此一处使用,记录下来以供InitWndProc使用。注意,每次的Self值是不同的,实际上是不同的Delphi对象的地址值。

TWinControl.CreateParams;
申请Params: TCreateParams; // 是一个Record,即在栈上分配内存。出了这个函数,这部分内存就被收回。
调用CreateParams(Params); // 虚函数,类函数,就这一处被调用。
调用Params.WindowClass.lpfnWndProc := @DefWindowProc; // API,某个Delphi的默认窗口函数,会很快被替换掉。这只是给类的窗口函数,但对于每个实例,它们的每个窗口函数都被换掉了。

TWinControl.CreateWindowHandle;
调用FHandle := CreateWindowEx(ExStyle, WinClassName, Caption, Style, X, Y, Width, Height, WndParent, 0, WindowClass.hInstance, Param);
这个API会发送 WM_NCCREATE, WM_NCCALCSIZE, 和 WM_CREATE,实际上执行了前两个消息对应的函数,最后一个消息的功能被构造函数替代了。
创建后取得窗口句柄,存储在Delphi对象的FHandle属性里

全局函数 InitWndProc(HWindow: HWnd; Message, WParam, LParam: Longint): Longint;
调用CreationControl.FHandle := HWindow;
调用SetWindowLong(HWindow, GWL_WNDPROC, Longint(CreationControl.FObjectInstance)); // 使用事先准备好的FObjectInstance作为普通窗口函数地址
调用
PUSH LParam // 压栈4个格子
PUSH WParam
PUSH Message
PUSH HWindow
MOV EAX,CreationControl // 把刚才创建的控件地址放到EAX寄存器里。混用汇编和Delphi,直接引用Delphi变量,给下一个函数准备参数。
MOV CreationControl,0 // 用完以后立刻清空,准备让下一个新的Control使用
CALL [EAX].TWinControl.FObjectInstance // 根据寄存器里的内存地址在内存中找到控件,转化为Win控件,并调用它的窗口函数,参数就在栈里
MOV Result,EAX // 处理完(WM_NCCREATE)消息后,把结果传回来

到这里CreationControl和它的窗口函数都被替换了。留下的是一个Delphi对象,有了正确的Handle值,并有了单独的窗口函数(FObjectInstance指向MainWndProc指向WndProc)。甚至还使用FDefWndProc记录了默认窗口函数地址。

-----------------------------------------------------------
第四步,善后工作

TWinControl.Destroy;
调用if FObjectInstance <> nil then Classes.FreeObjectInstance(FObjectInstance); // 全局函数,释放窗口函数的内存

Delphi对象变成Windows控件的前世今生(关键是句柄和回调函数)