程序就是一堆面条,理顺了,好用又好看,如果缠在一起,那就会煮成一坨面疙瘩了。QPlugins 虽然是一个插件引擎,但是记住我们的理念,插件即服务,服务也就是插件一种插接方式。
首先,我们了解的第一个基于 QPlugins 的 Demo 位于 DockForms 里的 InProcess 目录下。它的目标是将不同单元的窗体嵌入到主窗体的 TPageControl 的不同页面中,而不需要引用主窗体的任何东西。
使用 QPlugins 插件引擎所要做的第一件事:确定服务的接口方式。
如在 了解 QPlugins 的整体架构 一节中所说的,我们通过 IQPluginsManager 接口来管理一切服务和通知,而每个插件或主程序本身提供一个或多个服务。而服务的调用者与服务之间要如何交互呢?这是在将 QPlugins 插件引擎引入到你程序时,需要考虑的第一个问题:
- 使用一个 Interface 接口定义
此方式的优点是参数形式统一明确,而且参数类型一般是系统提供的内置类型,效率较高,调用时与传统习惯比较一致。 - 使用 IQService 的 Execute 函数来执行
此方式的优点是不需要预先定义接口,但仍然要知道需要传递的参数的类型。此类调用通过 IQParams 接口来将参数传递给服务的提供者,而服务的提供者通过 IQParams 来得到相关的参数数据,并执行服务。在本示例中,我们使用将演示这一用法。
好了,由于本示例使用的第二种方式来调用,也就是说,我们不知道提供服务接口的GUID编码,那么我们就需要知道怎么找到这个服务。QPlugins 提供了一个 ByPath 函数来让我们通过路径来查找服务。我们来看 ByPath 的声明:
function ByPath(APath: PWideChar): IQService; stdcall;
实际上,这个函数是由 IQServices 接口规定的,APath 约定了服务提供路径,如本示例中的 “Services/Docks”。
为了更好的说明这一点,我们来看我们示例中的代码:
procedure TForm1.FormCreate(Sender: TObject); var ARoot: IQServices; I: Integer; ATabSheet: TTabSheet; AParams: IQParams; begin ARoot := PluginsManager.ByPath('Services/Docks') as IQServices; if Assigned(ARoot) then begin AParams := TQParams.Create; AParams.Add('Parent', ptUInt64); for I := 0 to ARoot.Count - 1 do begin ATabSheet := TTabSheet.Create(PageControl1); ATabSheet.PageControl := PageControl1; ATabSheet.Caption := ARoot[I].Name; AParams[0].AsInt64 := IntPtr(ATabSheet); ARoot[I].Execute(AParams, nil); end; end; end;
首先,在继续之前,我们了解下这里的一个约定,所有要嵌入的子窗体必需将自己注册到 “Services/Docks” 服务列表(IQServices)结点下,做为子服务,以便主程序进行调用。
接下来,各个子窗体的实现单元,自己在 initialization 单元中,通过 RegisterService 将自己注册到 PluginsManager 的 “Service/Docks”下面,做为一个子服务。
好了,现在我们来看上面的代码做了什么:
- 检查 “Service/Docks” 这个根服务结点是否存在,如果不存在,说明没有任何子服务注册,就不需要继续了;
- 创建一个 IQParams 接口的实例 AParams,添加一个名为 Parent 的参数,考虑到64位编译的支持,这里类型定义为了ptUInt64(64位无符号整数)。这个 Parent 参数,用来传递给子窗体,告诉它要嵌入的目标控件地址(TWinControl 的子类)。
- 循环遍历每一个注册的子服务,调用其Execute方法,并将 AParams 做为参数传递进去,以执行服务。
好了,主窗体单元所要做的一切都已OK了,我们接下来实现子窗体的服务了。因为所有的窗体嵌入这块,实际上可以共用同样的代码,所以我们将其提炼一下,将其写到了 dockservice 单元。我们来看它的源码:
unit dockservice; interface uses classes, qstring, qplugins, controls; type TDockService = class(TQService) private FControlClass: TControlClass; public function Execute(AParams: IQParams; AResult: IQParams): Boolean; override; stdcall; property ControlClass: TControlClass read FControlClass write FControlClass; end; const IDockServices: TGuid = '{9DDD6DD9-3053-4EE2-90D5-759267DBB10C}'; procedure RegisterDock(AClass: TControlClass); implementation { TDockService } function TDockService.Execute(AParams, AResult: IQParams): Boolean; var AParent: TWinControl; AControl: TControl; begin AParent := Pointer(AParams[0].AsInt64); AControl := ControlClass.Create(AParent); AControl.HostDockSite := AParent; AControl.Visible := True; AControl.Align := alClient; Result := True; end; procedure RegisterDock(AClass: TControlClass); var AParent: IQServices; AService: TDockService; begin AParent := PluginsManager.ById(IDockServices) as IQServices; AService := TDockService.Create(NewId, AClass.ClassName); AService.ControlClass := AClass; AParent.Add(AService); end; procedure RegisterClass; begin PluginsManager.Services.Add(TQServices.Create(IDockServices, 'Docks')); end; initialization RegisterClass; end.
很短的一段代码,TDockService 继承自 TQService,并重载了 Execute 方法,以便提供服务的实现。另外,定义了一个 ControlClass 属性,来指定要嵌入的控件类型。剩下还有两个方法:
- RegisterDock 是一个简单的二次封装,让用户直接调用并传递要嵌入的控件类型就OK,不去再关心注册服务的问题。
- RegisterClass 用来在 “Serivces” 下,添加一个名为 “Docks” 的子结点。好吧,回过头看前面,知道 “Services/Docks” 来自于那里了吧:)
最后,我们在 initialization 里注册调用 RegisterClass 来初始化注册 “Service/Docks” 这个服务列表。
好了,现在我们随便设计两个窗体单元,在 Unit2 和 Unit3 中,窗体上你随心放置一些组件用于演示的目的。然后我们引入 qdockservices 单元,然后添加 initialization 小节,加入 RegisterDock 的调用来注册窗体类别,示例如下:
uses dockservice; {$R *.dfm} initialization RegisterDock(TForm2); end.
好吧,我们的第一个基于 QPlugins 松散耦合,面向服务的程序就此诞生了。现在我们 F9 运行它,可以看到两个窗体被正确的嵌入到相应的位置了:
简单总结一下 QPlugins 程序编写的几个步骤:
【服务的消费者】-本示例中是主窗体单元
1、通过路径或ID来查找服务(本示例通过路径);
2、通过接口或者服务的 Execute 方法来调用服务(本示例调用 Execute 方法);
【服务的提供者】-本示例中为各个子窗体实现单元
1、实现 IQService 接口和自己特定的接口(本示例没有特定的接口,直接继承 TQService 并实现了Execute 方法);
2、在单元的 initialization 小节中,注册自己到编写的服务路径下(本示例中的 “Services/Docks”);