[QPlugins] DLL 中插件提供窗口服务指南

QPlugins 为了简化大家开发带有窗体的插件,提供了 IQFormService 接口的封装实现。要使用该接口,需要按以下步骤执行:

【宿主程序】

1、如果宿主程序是 VCL 程序,在某个模块的 uses 小节,加入 qplugins.vcl.messages 单元,以便引入对 Windows 消息的扩展支持服务;如果宿主程序是 FMX 程序,则引用 qplugins.fmx.messages 单元。

2、重新编译宿主程序;

【插件】

1、VCL 窗体在窗体实现单元的 uses 里,加入 qplugins 和 qplugins.vcl.formsvc,FMX 窗体在窗体实现单元的 uses 里,加入 qplugins 和 qplugins.fmx.formsvc。

2、在窗体的 initialization 小节,加入对 RegisterFormService 的调用来注册窗体服务,该函数的声明如下:

/// <summary>注册一个IQFormService服务</summary>
/// <param name="APath">服务注册的父结点,用户可以自定义,推荐/Services/Forms</param>
/// <param name="AName">服务的名称</param>
/// <param name="AClass">窗体类型</param>
/// <param name="AMultiInstance">是否是多实例</param>
procedure RegisterFormService(const APath, AName: QStringW; AClass: TFormClass;AMultiInstance: Boolean = True); overload;
/// <summary>注册一个单实例服务</summary>
/// <param name="APath">服务注册的父结点,用户可以自定义,推荐/Services/Forms</param>
/// <param name="AName">服务的名称</param>
/// <param name="AForm">共享的窗体实例</param>
procedure RegisterFormService(const APath, AName: QStringW;AForm: TForm); overload;

两个重载,第一个使用窗体类型注册,可以注册单实例或多实例服务的窗体(取决 于AMutiInstance参数),第二个则在系统启动时,直接启动一个全局共享的单实例窗体服务,这个实例在整个程序运行阶段都是存在,除非取消服务注册。

3、在窗体的 finalization 小节,调用 UnregisterServices 来取消窗体服务的注册;

4、重新编译插件

【已知问题】

FMX 插件退出时会有异常,其它工作正常,而根据目前能够调试到的信息看,应该是 Delphi 自己的Bug。由此,程序也就无法动态链接 FMX 插件,在关闭时,可以调用 TerminateProcess 来强制结束进程,以避免程序退出时,由于无法卸载 FMX 的 DLL 插件造成的异常提示。

【常见问题及解答】

1、如何在窗体显示前,给窗体服务传递参数?

要传递参数给服务窗体,需要提供服务的窗体实现 IQCustomAction 接口,然后通过 IQCustomAction 服务 DoAction 接口函数,执行调用。该函数的唯一参数是一个 IQParams 的参数列表,具体如何解释该参数,由服务窗体和调用服务的单元实现。具体可以查看 FormService 示例中的 CustomAction 的实现:

[调用端示例]

procedure TForm1.Button4Click(Sender: TObject);
var
  AFormService: IQFormService;
  AButtonAction: IQCustomAction;
  AParams: TQParams;
begin
  if Supports(PluginsManager.ByPath('/Services/Docks/Forms/CustomAction'),
    IQFormService, AFormService) then
  begin
    if Supports(AFormService, IQCustomAction, AButtonAction) then
    begin
      AParams := TQParams.Create;
      AParams.Add('Caption', ptUnicodeString).AsString := '点我看看';
      AParams.Add('Message', ptUnicodeString).AsString := '这是宿主程序传来的值哟!';
      AButtonAction.DoAction(AParams);
    end;
    AFormService.ShowModal(nil);
  end;
end;

[插件端示例]

...
TForm6 = class(TForm, IQCustomAction)
    ...
  private
    { Private declarations }
    FClickMsg: String;
    function DoAction(AParams: IQParams; AResult: IQParams): Boolean;
  public
    { Public declarations }
  end;

var
  Form6: TForm6;

implementation

{$R *.dfm}

function TForm6.DoAction(AParams, AResult: IQParams): Boolean;
var
  ACaption, AMsg: IQParam;
begin
  ACaption := AParams.ByName('Caption');
  AMsg := AParams.ByName('Message');
  if Assigned(ACaption) then
    Button1.Caption := ParamAsString(ACaption);
  if Assigned(AMsg) then
    FClickMsg := ParamAsString(AMsg);
  Result := True;
end;

2、为什么服务明明注册是的多实例的,但调用 PluginsManager.ByPath 返回的服务的 IsMultiInstance 测试结果是单实例的?

通过 ByPath 或直接 PluginsManager as 得到的实例永远是单实例的。这是因为 ByPath 会在获取服务实例时,调用多实例服务的 GetInstance 返回一个新的实例,而这个实例是一个单实例的对象(也就是调用它的 GetInstance 接口,永远返回的是自身)。如果要访问原始的多实例服务,请使用 FindService 方法,它返回的是原始的多实例的服务自身。

3、主程序和插件是不是必需引用 qplugins.xxx.messages 单元?

如果你的宿主程序和插件都是VCL,并且共享相同的rtl+vcl 的 BPL 库,那么不需要。否则,你应当会需要它(当然你可以再造一遍车),以便提供统一的消息处理。简单总结几种情况:

  • 主程序是VCL,插件可能是 FMX 的;
  • 主程序是VCL,但是不带包编译的;
  • 主程序是VCL,但插件可能有不带包编译的;
  • 主程序是VCL,但插件和主程序不是同一版本的IDE编译的;
  • 主程序是FMX,插件可能是 VCL 的;
  • 主程序是FMX,但是不带包编译的;
  • 主程序是FMX,但插件可能是不带包编译的

4、为什么我在多实例服务返回的结果上调用 Show 时会一闪而过?

这是多实例的服务返回的是一个新的单实例的服务用来提供后续的实际服务,所以,如果你没有保存下返回的服务接口对象,则由于接口会在你函数退出时完成后自动减少引用计数,造成提供服务的实例被释放造成的。要解决这一问题,只需要设置一个 IQFormService(C++ Builder 为 _di_IQFormService)的变量,保存这个返回的单实例服务就可以了。

分享到: