Delphi一直以丰富的第三方组件著称。在各种版本的Delphi中,第三方组件利用Delphi的设计器机制,基本上能做到所见即所得,拖拽什么到窗体上,跑起来也同样会是这个样子。这种强大的设计期能力,离不开包机制的支持。在之前的文章中我们介绍过Delphi有两种编译打包模式,一种是独立完整EXE,一种是带包。这就意味着,我们如果要写一个自定义组件,也得同样支持这两种模式。详细说来,就是我们的组件的Pascal源码,既要编译成DCU,供编译链接进完整的EXE,也要编译成BPL包,供带包编译使用。读者一定要问,如果我的组件只需要支持完整EXE模式的编译,是不是就可以不用编译成BPL包了?完整EXE模式的编译,的确只需要Pascal源码或DCU。假设你工程路径及组件源码/DCU的路径都设置好了,那么一个正确的dcc32.exe命令,就可以在脱离BPL包的情况下编译出完整的EXE文件来。但是,这种情况下,我们的组件,还能叫一个像样的“组件”吗?不信你用Delphi的IDE打开工程,如果你窗体上曾经搁了我们自己写的组件,但这组件并未编译成BPL,那么打开该窗体时,就一定会出错说某某组件找不到,哪怕这些组件对应的Pascal或DCU正完整无缺地躺在正确的目录下。为什么会这样?这就要说说Delphi设计期对包机制的依赖了。BPL本质上是DLL,二进制代码的组合块,能够被EXE或其他DLL/BPL等动态加载,而DCU是Pascal源码编译成的中间产物,并不能被动态加载,只能静态链接到EXE或DLL中。而Delphi的IDE是一个独立的大型EXE,它不能、也不应该从DCU中加载组件内容供设计期使用。常写第三方组件的朋友都知道,我们写组件的正常功能时,大多数情况并不需要考虑它究竟是在Delphi设计器里跑,还是在运行期被EXE加载了跑,只需要老老实实提供具体功能就好。这是因为,Delphi的VCL中的基类,以及Delphi的IDE自身的机制,已经将两者的区别给封装好、且对用户透明了。例如,设计器上搁一个我们写的组件,选中时四周会出八个选择点,可以拖动改变组件尺寸,这八个点,并不需要我们在组件的Paint或类似事件里自绘。我们在对象查看器里设置某个组件的Visible为False时,设计器里这个组件也不会隐藏,同样无需我们手工处理不隐藏的动作。更别说设计器窗体上不知道谁画出来的网格小点点了,反正我们没画。以上机制,都隐藏在VCL中的基类以及IDE自身的接口中。不信可以看看VCL源码中Forms.pas里的TCustomForm,它有这么个属性:property Designer: IDesignerHook
procedure TCustomForm.PaintWindow(DC: HDC); begin FCanvas.Lock; try FCanvas.Handle := DC; try if FDesigner <> nil then FDesigner.PaintGrid else Paint; finally FCanvas.Handle := 0; end; finally FCanvas.Unlock; end; end;
原来,Delphi在自己的设计器中实现了一个IDesignerHook接口,并在进入设计期时,把它塞给源于BPL且动态创建的组件。组件的VCL源码中早就写了“如果有这个接口我就用它”的机制,用它处理自画、用它处IDE消息等,双方共同配合,实现了对编程拖拽友好的设计界面。 实现第三方组件的设计期加载,使用“加载一个BPL,从其中创建组件实例,并把IDesignerHook接口塞给它”,比“编译一批DCU,再动态链接加载进Delphi的EXE中”,要简单多了。 ——后者甚至几乎不可能实现,除非还是用DCU编译成BPL加载。 以上说明了,尽管编译独立EXE,第三方组件只需要DCU,可在Delphi中要拖拽设计,还是离不开组件所在的BPL。 换句话说,组件必须同时以DCU和BPL这一静一动两种形式存在,否则功能不完整。 BPL的用处在于,在设计期,组件的BPL被Delphi的IDE加载,配合IDesignerHook接口实现设计期显示与交互。而在带包编译的运行期,组件的BPL被我们的EXE加载,在没IDesignerHook接口的情况下,实现运行期的显示与交互,和设计期会有所不同。 比如,至少没八个点拖动改变尺寸的功能。 组件编译成一个BPL后,既可以被Delphi的IDE加载到设计期,也可以被EXE加载到运行期,听起来没什么问题,至少在低版本的Delphi中一切都显得那么顺理成章。 可是,当我们的Delphi高版本开始支持新架构、新平台时,那怎么办?Delphi自从XE2支持64位以来,也有用户朋友问过,我的组件如何支持64位?起初我也把这个问题想得过于简单,认为只要处理好NativeInt/NativeUInt和Integer/Cardinal等的区别,做好组件本身的适配工作就行了。可很快就发现,这个想法远远不够。因为,组件包的DPK工程,切到64位时,根本没有安装入口!即使32位下我们的DPK能编译安装,但新建VCL工程,把工程切到64位时,Delphi的控件板上,也没有出现我们的组件。网上搜到的解决办法,是在组件源码的组件声明上头加上:[ComponentPlatformsAttribute(pidWin32 or pidWin64)] 但这一句话不过是一个自定义的Attribute,为何有这么大的魔力?我们要注意一个事实,从Delphi 5到目前最新的RAD Studio 12.2,其IDE均是在Windows上运行的32位程序。哪怕你组件再怎么适配64位,你在设计期拖拽上来的组件,其表现的本质下,仍是32位代码。Delphi的IDE设计期不能加载32位之外的BPL,也就意味着无论你打开Delphi设计32位工程,还是64位工程,抑或是设计Android、iOS工程,在设计期间跑的,全是32位Windows代码。如果我们的第三方组件只适配好了32位,没适配64位或其他平台,虽在设计期不会有问题,但运行的时候可能就挂了或直接跑不起来。Delphi设计期又只有32位版本,无从直接知晓第三方组件是否支持64位和其他平台,所以才让程序员们写上面的:[ComponentPlatformsAttribute(pidWin32 or pidWin64)]
意思是:“伙计们,你们做好64位适配后记得告诉我一声,我好把你们打包进运行期去,跑不跑得起来就完全靠你们了。” 设计期32位,运行期则可32可64也可其他平台,这是Delphi目前的特性。说特色也罢、说局限也罢,关键是要搞明白,组件怎样才算“支持64位”。完整来讲,就是要完全保证组件的行为在32位下和64位下表现相同,千万不能说“32位目前不是主流了,我的组件只要支持64位就行了”。32位和64位行为保证相同后,我们的组件包DPK被编译成32位DCU和32位BPL,再编译成64位DCU和64位BPL。32位BPL被Delphi的IDE加载参与设计(也就是Install),64位的BPL则搁那儿,等64位程序带包编译时链接使用,平时派不上用场。如果Delphi的IDE是64位的,情况就会倒过来:64位BPL被Delphi的IDE加载参与设计(也就是Install),32位的BPL反倒又搁那儿,等32位程序带包编译时链接使用,平时派不上用场了。
|