[教程] QPlugins 插件引擎教程 – 让 QPlugins 协助你解耦程序

程序就是一堆面条,理顺了,好用又好看,如果缠在一起,那就会煮成一坨面疙瘩了。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”);

 

 

分享到: