CTK 框架实际应用比较可靠,但网上资料很少。本教程围绕 CTK Plugin Framework,探索 C++ 中的模块化技术,并能够基于 CTK 快速搭建 C++ 组件化框架,避免后来的人走弯路。本教程的源码下载地址:项目源代码。
CTK 介绍
CTK 为支持生物医学图像计算的公共开发包,其全称为 Common Toolkit。CTK 插件框架的设计有很大的灵感来自 OSGi 并且使得应用程序由许多不同的组件组合成一个可扩展模型。这个模型允许通过那些组件间共享对象的服务通信。
当前,CTK 工作的主要范围包括:
DICOM:提供了从 PACS 和本地数据库中查询和检索的高级类。包含 Qt 部件,可以轻松地设置服务器连接,并发送查询和查看结果。
DICOM Application Hosting:目标是创建 DICOM Part 19 Application Hosting specifications 的 C++ 参考实现。它提供了用于创建主机和托管应用程序的基础设。
Widgets:用于生物医学成像应用的 Qt Widgets 集合。
Plugin Framework:用于 C++ 的动态组件系统,以 OSGi 规范为模型。它支持一个开发模型,在这个模型中,应用程序(动态地)由许多不同(可重用的)组件组成,遵循面向服务的方法。
Command Line Interfaces:一种允许将算法编写为自包含可执行程序的技术,可以在多个终端用户应用程序环境中使用,而无需修改。
CTK Plugin Framework
CTK Plugin Framework 架构策略
- Qt Creator 的可扩展性。
Qt Creator 通过一种简单、优雅的方式来实现可扩展性,它使用一个通用的 QObject 池来实现某些可用的接口。同时,通过使用嵌入式文本文件(.pluginspec 文件)来向插件添加元数据(例如:Name、Version 等)。
其实严格来说,CTK Plugin Framework 同时借鉴了 OSGi 和 Qt Creator 的思想。
- Qt 提供了 Qt Plugin System 和 Qt Service Framework。
Qt Plugin System 提供了两套用于创建插件的 API,高级 API 用于扩展 Qt 本身(例如:自定义数据库驱动、图像格式、文本编解码、自定义样式等),低级 API 用于扩展 Qt 应用程序。 对于 Qt Service Framework 来说,它能使服务的开发和访问方式变得更加容易。Qt 服务提供者可以与特定于平台的服务进行交互,而无需向客户端公开平台的细节。每个服务都通过 QObject 指针公开,这意味着客户端可以通过 Qt MetaObject 系统与服务对象进行交互。
- CTK Plugin Framework 的架构策略是什么?
CTK Plugin Framework 是基于 Qt Plugin System 和 Qt Service Framework 实现的,并且它还添加了以下特性来增强这两个系统:
- 插件元数据(由 MANIFEST.MF 文件提供);
- 一个定义良好的插件生命周期和上下文;
- 综合服务发现和注册;
- ……
注意: 在 Qt Plugin System 中,插件的元数据由 JSON 文件提供。
CTK Plugin Framework 的核心架构主要包含两个组件:Plugin System 本身和 Service Registry。然而,这两个组件是相互关联的,它们在 API 级别上的组合使得系统更加全面、灵活。
- Plugin System
CTK Core 依赖于 QtCore 模块,因此 CTK Plugin Framework 基于 Qt Plugin System。Qt API 允许在运行时加载和卸载插件,这个功能在 CTK Plugin Framework 中得到了加强,以支持透明化延迟加载和解决依赖关系。
插件的元数据被编译进插件内部,可以通过 API 进行提取。此外,插件系统还使用 SQLite 缓存了元数据,以避免应用程序加载时间问题。另外,Plugin System 支持通过中央注册中心使用服务。
- Service Registry
Qt Service Framework 是 Qt Mobility 项目发布的一个 Qt 解决方案,这种服务框架允许“声明式服务”(Getting Started with OSGi: Introducing Declarative Services )和按需加载服务实现。为了启用动态(非持久性)服务,Qt Mobility 服务框架可以与 Service Registry 一起使用,类似于 OSGi Core Specifications 中描述的一样。
CTK Plugin Framework 优点
由于 CTK Plugin Framework 基于 OSGi,因此它继承了一种非常成熟且完全设计的组件系统,这在 Java 中用于构建高度复杂的应用程序,它将这些好处带给了本地(基于 Qt 的)C++ 应用程序。以下内容摘自 Benefits of Using OSGi,并适应于 CTK Plugin Framework:
- 降低复杂性
使用 CTK Plugin Framework 开发意味着开发插件,它们隐藏了内部实现,并通过定义良好的服务来和其它插件通信。隐藏内部机制意味着以后可以自由地更改实现,这不仅有助于 Bug 数量的减少,还使得插件的开发变得更加简单,因为只需要实现已经定义好的一定数量的功能接口即可。
- 复用
标准化的组件模型,使得在应用程序中使用第三方组件变得非常容易。
- 现实情况
CTK Plugin Framework 是一个动态框架,它可以动态地更新插件和服务。在现实世界中,有很多场景都和动态服务模型相匹配。因此,应用程序可以在其所属的领域中重用 Service Registry 的强大基元(注册、获取、用富有表现力的过滤语言列表、等待服务的出现和消失)。这不仅节省了编写代码,还提供了全局可见性、调试工具以及比为专用解决方案实现的更多的功能。在这样的动态环境下编写代码听起来似乎是个噩梦,但幸运的是,有支持类和框架可以消除大部分(如果不是全部的话)痛苦。
- 开发简单
CTK Plugin Framework 不仅仅是组件的标准,它还指定了如何安装和管理组件。这个 API 可以被插件用来提供一个管理代理,这个管理代理可以非常简单,如命令 shell、图形桌面应用程序、Amazon EC2 的云计算接口、或 IBM Tivoli 管理系统。标准化的管理 API 使得在现有和未来的系统中集成 CTK Plugin Framework 变得非常容易。
- 动态更新
OSGi 组件模型是一个动态模型,插件可以在不关闭整个系统的情况下被安装、启动、停止、更新和卸载。
- 自适应
OSGi 组件模型是从头设计的,以允许组件的混合和匹配。这就要求必须指定组件的依赖关系,并且需要组件在其可选依赖性并不总是可用的环境中生存。Service Registry 是一个动态注册表,其中插件可以注册、获取和监听服务。这种动态服务模型允许插件找出系统中可用的功能,并调整它们所能提供的功能。这使得代码更加灵活,并且能够更好地适应变化。
- 透明性
插件和服务是 CTK 插件环境中的一等公民。管理 API 提供了对插件的内部状态的访问,以及插件之间的连接方式。可以停止部分应用程序来调试某个问题,或者可以引入诊断插件。
- 版本控制
在 CTK Plugin Framework 中,所有的插件都经过严格的版本控制,只有能够协作的插件才会被连接在一起。
- 简单
CTK 插件相关的 API 非常简单,核心 API 不到 25 个类。这个核心 API 足以编写插件、安装、启动、停止、更新和卸载它们,并且还包含了所有的监听类。
- 懒加载
懒加载是软件中一个很好的点,OSGi 技术有很多的机制来保证只有当类真正需要的时候才开始加载它们。例如,插件可以用饿汉式启动,但是也可以被配置为仅当其它插件使用它们时才启动。服务可以被注册,但只有在使用时才创建。这些懒加载场景,可以节省大量的运行时成本。
- 非独占性
CTK Plugin Framework 不会接管整个应用程序,你可以选择性地将所提供的功能暴露给应用程序的某些部分,或者甚至可以在同一个进程中运行该框架的多个实例。
- 非侵入
在一个 CTK 插件环境中,不同插件均有自己的环境。它们可以使用任何设施,框架对此并无限制。CTK 服务没有特殊的接口需求,每个 QObject 都可以作为一个服务,每个类(也包括非 QObject)都可以作为一个接口。
CTK 编译
使用 cmake 编译出与系统版本相应的动态库。参见CTK 编译教程(64 位环境 Windows + Qt + MinGW 或 MSVC + CMake)。
使用 CTKWidgets
新项目-Application(Qt)-Qt Console Application,项目名称为 UseCTKWidgets,pro 文件的代码:
QT += core gui widgets
TARGET = UseCTKWidgets
TEMPLATE = app
# CTK 安装路径
CTK_INSTALL_PATH = $$PWD/../../CTKInstall
# CTK 相关库所在路径(例如:CTKCore.lib、CTKWidgets.lib)
CTK_LIB_PATH = $$CTK_INSTALL_PATH/lib/ctk-0.1
# CTK 相关头文件所在路径(例如:ctkPluginFramework.h)
CTK_INCLUDE_PATH = $$CTK_INSTALL_PATH/include/ctk-0.1
# 相关库文件(CTKCore.lib、CTKWidgets.lib)
LIBS += -L$$CTK_LIB_PATH -lCTKCore -lCTKWidgets
INCLUDEPATH += $$CTK_INCLUDE_PATH
主函数 main 加载部件,代码如下:main.cpp
#include <QApplication>
#include <QFormLayout>
#include <QVBoxLayout>
#include <ctkCheckablePushButton.h>
#include <ctkCollapsibleButton.h>
#include <ctkColorPickerButton.h>
#include <ctkRangeWidget.h>
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
// 可折叠按钮
ctkCollapsibleButton* buttons = new ctkCollapsibleButton("Buttons");
// 可勾选按钮
ctkCheckablePushButton* checkablePushButton = new ctkCheckablePushButton();
checkablePushButton->setText("Checkable");
// 颜色拾取器
ctkColorPickerButton* colorPickerButton = new ctkColorPickerButton();
colorPickerButton->setColor(QColor("#9e1414"));
ctkCollapsibleButton* sliders = new ctkCollapsibleButton("Sliders");
QFormLayout* buttonsLayout = new QFormLayout;
buttonsLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
buttonsLayout->addRow("ctkCheckablePushButton", checkablePushButton);
buttonsLayout->addRow("ctkColorPickerButton", colorPickerButton);
buttons->setLayout(buttonsLayout);
QVBoxLayout* topLevelLayout = new QVBoxLayout();
topLevelLayout->addWidget(buttons);
topLevelLayout->addWidget(sliders);
QFormLayout* slidersLayout = new QFormLayout;
ctkRangeWidget* rangeWidget = new ctkRangeWidget();
slidersLayout->addRow("ctkRangeWidget", rangeWidget);
sliders->setLayout(slidersLayout);
QWidget topLevel;
topLevel.setLayout(topLevelLayout);
topLevel.show();
return app.exec();
}
项目代码:UseCTKWidgets
初步使用 CTK Plugin Framework
项目结构
由于每一个插件都要建一个子项目,本项目刚开始创建时在 QtCreator 中选择新建-其他项目-子目录项目,新建项目名称为SampleCTK,然后建立主程序入口项目,这里建立一个控制台项目,取名叫 App。
更改项目输出路径:app.pro
DESTDIR = $$OUT_PWD/../bin
主函数中加载插件,启动框架:main.cpp
#include <QCoreApplication>
#include "ctkPluginFrameworkFactory.h"
#include "ctkPluginFramework.h"
#include "ctkPluginException.h"
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
app.setApplicationName("SampleCTK");//给框架创建名称,Linux下没有会报错
ctkPluginFrameworkFactory frameWorkFactory;
QSharedPointer<ctkPluginFramework> framework = frameWorkFactory.getFramework();
try {
// 初始化并启动插件框架
framework->init();
framework->start();
qDebug() << "CTK Plugin Framework start ...";
} catch (const ctkPluginException &e) {
qDebug() << "Failed to initialize the plugin framework: " << e.what();
qDebug() << e.message() << e.getType();
}
return app.exec();
}
如果想把 CTK 初始化、插件安装启动、获取等操作封装成一个类,那么要注意:需要把 CTK 相关的变量定义成类属性,不能是局部变量,否则会出现各种问题如获取不了服务、服务引用为空等。
没有报错的话及表示插件加载成功!
其中 QSharedPointer framework 这个对象比较有意思,既可以作为对象也可以作为对象指针,但要作为插件框架使用必须要用指针方法调用,所以代码里使用“->”。
项目加载 CTK 框架插件
项目 SampleCTK 新建文本文件 CTK,然后更改扩展名为 pri。文件加载 CTK 安装目录及源代码目录,编译出的动态库就可以当普通动态库使用加载了,CTK.pri 里面加载代码为:
# CTK 安装路径
CTK_INSTALL_PATH = $$PWD/../../CTKInstall
# CTK 插件相关库所在路径(例如:CTKCore.lib、CTKPluginFramework.lib)
CTK_LIB_PATH = $$CTK_INSTALL_PATH/lib/ctk-0.1
# CTK 插件相关头文件所在路径(例如:ctkPluginFramework.h)
CTK_INCLUDE_PATH = $$CTK_INSTALL_PATH/include/ctk-0.1
# CTK 插件相关头文件所在路径(主要因为用到了 service 相关东西)
CTK_INCLUDE_FRAMEWORK_PATH = $$PWD/../../CTK-master/Libs/PluginFramework
LIBS += -L$$CTK_LIB_PATH -lCTKCore -lCTKPluginFramework
INCLUDEPATH += $$CTK_INCLUDE_PATH \
$$CTK_INCLUDE_FRAMEWORK_PATH
将 CTK.pri 文件的内容引入 pro 文件:app.pro
include($$PWD/../CTK.pri)
CTK 插件的接口处理
CTK 框架由一个一个可分离的插件组成,框架对插件识别有一定要求,目前网上很多一整块扔出来对新人不太友好,博主这里讲解是尽量拆。单个插件最基本的格式要求分成 Activator,qrc 文件,以及 MANIFEST.MF,以 say Hello 模块 HelloCTK 为例。
Activator 注册器
每个插件都有自己的注册器 Activator。
右键项目选择新建子项目-其他项目-Empty qmake Project,项目名称为 HelloCTK,pro 文件中添加代码:
QT += core
QT -= gui
TEMPLATE = lib
CONFIG += plugin
TARGET = HelloCTK
DESTDIR = $$OUT_PWD/../bin/plugins
include($$PWD/../CTK.pri)
生成的插件名(TARGET)不要有下划线,因为 CTK 会默认将插件名中的下划线替换成点号,最后后就导致找不到插件。
项目中添加 C++类 HelloActivator,代码如下:
hello_activator.h
#ifndef HELLO_ACTIVATOR_H
#define HELLO_ACTIVATOR_H
#include <QObject>
#include "ctkPluginActivator.h"
class HelloActivator : public QObject, public ctkPluginActivator
{
Q_OBJECT
Q_INTERFACES(ctkPluginActivator)
Q_PLUGIN_METADATA(IID "HELLO_CTK")
//向Qt的插件框架声明,希望将xxx插件放入到框架中。
public:
void start(ctkPluginContext* context);
void stop(ctkPluginContext* context);
};
#endif // HELLO_ACTIVATOR_H
hello_activator.cpp
#include "hello_activator.h"
#include <QDebug>
void HelloActivator::start(ctkPluginContext* context)
{
qDebug() << "HelloCTK start";
}
void HelloActivator::stop(ctkPluginContext* context)
{
qDebug() << "HelloCTK stop";
Q_UNUSED(context)
//Q_UNUSED,如果一个函数的有些参数没有用到、某些变量只声明不使用,但是又不想编译器、编辑器报警报,其他没有什么实际性作用
}
activator 是标准的 Qt 插件类,它实现 ctkPluginActivator 的 start、stop 函数并对外提供接口。我这里是 Qt5 的版本,所以使用 Q_PLUGIN_METADATA 申明插件,Qt4 需要用自己的方法实现插件。
qrc 文件
创建插件的资源文件,格式如下:
<RCC>
<qresource prefix="/HelloCTK/META-INF">
<file>MANIFEST.MF</file>
</qresource>
</RCC>
插件加载后会寻找同名前缀/META-INF,所以前缀格式固定,将 MANIFEST.MF 文件添加进来
MENIFEST.MF 文件内容如下:
可直接在 MF 文件里添加自己特有的元数据
Plugin-SymbolicName:HelloCTK
Plugin-Version:1.0.0
Plugin-Number:100 #元数据
注意:Plugin-SymbolicName 要满足这里的前缀是:TARGET/META-INF 格式。TARGET 的名字最好和工程名一致,不然可能出现 device not open 错误。
文件包含 ctk 插件的基本信息,只要 ctk 框架正常识别到文件中 Plugin-SymbolicName 等信息,则判定它是一个 ctk 插件,能够正常调用 activator 中的 start、stop 函数。这个文件需要拷到插件生成路径下,pro 文件中添加代码:
file.path = $$DESTDIR
file.files = MANIFEST.MF
INSTALLS += file
CTK 插件启用
根据以上步骤,一个 CTK 插件接口定义基本完成,我们在 App 项目下调用观察插件是否能够正常加载。main 函数中框架启动成功后添加以下代码:
QString dir = QCoreApplication::applicationDirPath();
dir += "/plugins/HelloCTK.dll";
qDebug() << dir;
QUrl url = QUrl::fromLocalFile(dir);
QSharedPointer<ctkPlugin> plugin;
try
{
plugin = framework->getPluginContext()->installPlugin(url);
}catch(ctkPluginException e){
qDebug() << e.message() << e.getType();
}
try{
plugin->start(ctkPlugin::START_TRANSIENT);
}catch(ctkPluginException e){
qDebug() << e.message() << e.getType();
}
控制台打印输出:
"C:/d/mmm/qt/ctk/CTK-examples/build-SampleCTK-Desktop_Qt_5_15_1_MSVC2019_64bit-Release/bin/plugins"
HelloCTK start
成功调用 HelloCTK 中 start 内打印输出,则表明 ctk 插件接口正常定义并能成功加载。其中 start(ctkPlugin::START_TRANSIENT)表示立即启用插件,不设置参数的话加载后也不会立即打印输出。
基本使用 CTK Plugin Framework
CTK 插件间通信
CTK 框架插件化开发实现功能的隔离,插件通信需要参照固定标准,这里介绍两种插件间通信的方法。
通信方法一. 注册接口调用
注册接口调用
函数接口
接口就是纯虚函数类,也就是最终的服务的前身。
上面我们已经编译出需要的动态库,首先确定我们需要插件向外部暴露的功能有什么,比如这里我们需要说”Hello,CTK!”的操作,定义头文件如下:hello_service.h
#ifndef HELLO_SERVICE_H
#define HELLO_SERVICE_H
#include <QtPlugin>
class HelloService
{
public:
virtual ~HelloService() {}
virtual void sayHello() = 0;
};
#define HelloService_iid "org.commontk.service.demos.HelloService"
Q_DECLARE_INTERFACE(HelloService, HelloService_iid)
//此宏将当前这个接口类声明为接口,后面的一长串就是这个接口的唯一标识。
#endif // HELLO_SERVICE_H
Q_DECLARE_INTERFACE 将接口类向 Qt 系统申明,然后添加它的实现对象:
接口的实现
插件就是实现这个接口类的实现类,所以理论上有多少个实现类就有多少个插件。
hello_impl.h
#ifndef HELLO_IMPL_H
#define HELLO_IMPL_H
#include "hello_service.h"
#include <QObject>
class ctkPluginContext;
class HelloImpl : public QObject, public HelloService
{
Q_OBJECT
Q_INTERFACES(HelloService)
/*
此宏与Q_DECLARE_INTERFACE宏配合使用。
Q_DECLARE_INTERFACE:声明一个接口类
Q_INTERFACES:当一个类继承这个接口类,表明需要实现这个接口类
*/
public:
HelloImpl(ctkPluginContext* context);
void sayHello() Q_DECL_OVERRIDE;
};
#endif // HELLO_IMPL_H
hello_impl.cpp
#include "hello_impl.h"
#include <ctkPluginContext.h>
#include <QtDebug>
HelloImpl::HelloImpl(ctkPluginContext* context)
{
context->registerService<HelloService>(this);
}
void HelloImpl::sayHello()
{
qDebug() << "Hello,CTK!";
}
这仍是 Qt 的插件定义格式,但是不会作为插件导出,外部功能接口可以自定义。
服务注册(Activator 注册服务)
激活类里有一个独占智能指针,指向接口类【使用多态,指针都指向父类】,然后在 start 里 new 一个实现类,注册这个实现类为服务,功能是实现接口类的接口,然后将智能指针指向这个实现类。可以理解为以后向框架索取这个服务的时候,实际获取的就是这个 new 出来的实现类。如果不用智能指针,就需要在 stop 里手动 delete 这个实现类。
每个插件都有自己的注册器 Activator,功能节接口完成后,在插件启动时注册到 ctk 框架的服务中,代码如下:hello_activator.cpp
#include "hello_activator.h"
#include "hello_impl.h"
void HelloActivator::start(ctkPluginContext* context)
{
s.reset(new HelloImpl(context));
//调用注册服务context->registerService<HelloService>(this);
}
接口调用
CTK 插件启用后,就可以调用接口。
主函数框架及插件加载完成后,即可调用插件接口,代码如下:main.cpp
#include "../HelloCTK/hello_service.h"
// 获取服务引用
ctkServiceReference reference = context->getServiceReference<HelloService>();
if (reference) {
// 获取指定 ctkServiceReference 引用的服务对象
HelloService* service = qobject_cast<HelloService *>(context->getService(reference));
if (service != Q_NULLPTR) {
// 调用服务
service->sayHello();
}
}
在获取服务的时候,有两个重载方式【可直接使用的】
1、HelloService* service = context->getService<HelloService>(reference);
2、HelloService* service = qobject_cast<HelloService*>(context->getService(reference));
服务就是根据接口的实例,每生成一个服务就会调用一次注册器的 start。把接口当做类,服务是根据类 new 出的对象,插件就是动态库 dll。
项目代码:SampleCTK
优化解耦(实现类和激活类分离)
编写插件主要有 3 个步骤:接口类、实现类、激活类。不在实现类的构造函数里注册服务,降低耦合性,接口类就只做接口声明,实现类就只实现接口,激活类就负责将服务整合到 ctk 框架中。
接口类没有什么变化,实现类少了注册的代码,构造函数也无参数,注册的过程放在了激活类里。
- 实现类
.h
HelloImpl( );
.cpp
HelloImpl::HelloImpl( )
{
qDebug()<<"this is imp";
}
- 激活类
.cpp
void HelloActivator::start(ctkPluginContext* context)
{
HelloImpl* helloImpl = new HelloImpl(context);
context->registerService<HelloService>(helloImpl);
s.reset(helloImpl);
}
接口、插件、服务的关系
1、1 对 1
1 个接口类由 1 个类实现,输出 1 个服务和 1 个插件。
上面项目为典型 1 对 1 关系。
2、多对 1
1 个类实现了多个接口类,输出多个服务和 1 个插件,无论想使用哪个服务最终都通过这同一个插件来实现。
实现类,实现多个接口。
#include "greet_impl.h"
#include <QtDebug>
GreetImpl::GreetImpl()
{
}
void GreetImpl::sayHello()
{
qDebug() << "Hello,CTK!";
}
void GreetImpl::sayBye()
{
qDebug() << "Bye,CTK!";
}
获取不同服务
// 获取服务引用
ctkServiceReference ref = context->getServiceReference<HelloService>();
if (ref) {
HelloService* service = qobject_cast<HelloService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->sayHello();
}
ref = context->getServiceReference<ByeService>();
if (ref) {
ByeService* service = qobject_cast<ByeService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->sayBye();
}
具体实现参见项目:PluginAndService/MultipleInterfaces
3、1 对多
1 接口由多个个类实现,也就是某一个问题提供了多种解决思路,输出 1 个服务和多个插件,通过 ctkPluginConstantsSERVICE_RANKING 和 ctkPluginConstantsSERVICE_ID 来调用不同的插件。这里虽然有两个插件,但都是被编译到同一个 dll 中的。服务的获取策略如下:容器会返回排行最低的服务,返回注册时 SERVICE_RANKING 属性值最小的服务。如果有多个服务的排行值相等,那么容器将返回 PID 值最小的那个服务。
某插件每次调用另一个插件的时候,只会生成一个实例,然后把实例存到内存当中,不会因为多次调用而生成多个服务实例。
在使用 1 接口 2 插件的时候,虽然是两个插件,也会有两个激活类【从原理上来讲 1 个激活类就行了,但是在 start 里注册两次】,其中的 IID 只能有一个。从 Qt 插件基础上来说,一个 dll 只能有一个 IID。
多个实现类
,实现 1 个接口。
welcome_ctk_impl.cpp
#include "welcome_ctk_impl.h"
#include <QtDebug>
WelcomeCTKImpl::WelcomeCTKImpl()
{
}
void WelcomeCTKImpl::welcome()
{
qDebug() << "Welcome CTK!";
}
welcome_qt_impl.cpp
#include "welcome_qt_impl.h"
#include <QtDebug>
WelcomeQtImpl::WelcomeQtImpl()
{
}
void WelcomeQtImpl::welcome()
{
qDebug() << "Welcome Qt!";
}
对应的多个激活类
welcome_ctk_activator.cpp
#include "welcome_ctk_impl.h"
#include "welcome_ctk_activator.h"
#include <QtDebug>
void WelcomeCTKActivator::start(ctkPluginContext* context)
{
ctkDictionary properties;
properties.insert(ctkPluginConstants::SERVICE_RANKING, 2);
properties.insert("name", "CTK");
m_pImpl = new WelcomeCTKImpl();
context->registerService<WelcomeService>(m_pImpl, properties);
}
void WelcomeCTKActivator::stop(ctkPluginContext* context)
{
Q_UNUSED(context)
delete m_pImpl;
}
welcome_qt_activator.cpp
#include "welcome_qt_impl.h"
#include "welcome_qt_activator.h"
#include <QtDebug>
void WelcomeQtActivator::start(ctkPluginContext* context)
{
ctkDictionary properties;
properties.insert(ctkPluginConstants::SERVICE_RANKING, 1);
properties.insert("name", "Qt");
m_pImpl = new WelcomeQtImpl();
context->registerService<WelcomeService>(m_pImpl, properties);
}
void WelcomeQtActivator::stop(ctkPluginContext* context)
{
Q_UNUSED(context)
delete m_pImpl;
}
获取服务
// 1. 获取所有服务
QList<ctkServiceReference> refs = context->getServiceReferences<WelcomeService>();
foreach (ctkServiceReference ref, refs) {
if (ref) {
qDebug() << "Name:" << ref.getProperty("name").toString()
<< "Service ranking:" << ref.getProperty(ctkPluginConstants::SERVICE_RANKING).toLongLong()
<< "Service id:" << ref.getProperty(ctkPluginConstants::SERVICE_ID).toLongLong();
WelcomeService* service = qobject_cast<WelcomeService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->welcome();
}
}
// 2. 使用过滤表达式,获取感兴趣的服务
refs = context->getServiceReferences<WelcomeService>("(&(name=CTK))");
foreach (ctkServiceReference ref, refs) {
if (ref) {
WelcomeService* service = qobject_cast<WelcomeService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->welcome();
}
}
// 3. 获取某一个服务(由 Service Ranking 和 Service ID 决定)
ctkServiceReference ref = context->getServiceReference<WelcomeService>();
if (ref) {
WelcomeService* service = qobject_cast<WelcomeService *>(context->getService(ref));
if (service != Q_NULLPTR)
service->welcome();
}
具体实现参见项目:PluginAndService/OneInterface
通信方法二. 事件监听
CTK 框架中的事件监听,即观察者模式流程上是这样:接收者注册监听事件->发送者发送事件->接收者接收到事件并响应;相比调用插件接口,监听事件插件间依赖关系更弱,不用指定事件的接收方和发送方是谁。
要使用 CTK 框架的事件服务,准备工作应该从 cmake 开始,编译出支持事件监听的动态库,名称为 liborg_commontk_eventadmin.dll。现在要完成的内容是,从上面生成的主窗体中,以事件监听的方式调用一个子窗体。
1、通信主要用到了 ctkEventAdmin 结构体,主要定义了如下接口:
postEvent:类通信形式异步发送事件
sendEvent:类通信形式同步发送事件
publishSignal:信号与槽通信形式发送事件
unpublishSignal:取消发送事件
subscribeSlot:信号与槽通信形式订阅时间,返回订阅的 ID
unsubscribeSlot:取消订阅事件
updateProperties:更新某个订阅 ID 的主题
2、通信的数据是:ctkDictionary
其实就是个 hash 表:typedef QHash<QString,QVariant> ctkDictionary
事件监听
具体项目:EventAdmin/SendEvent
加载 EventAdmin 动态库
添加动态库可以使用 ctkPluginFrameworkLauncher,代码如下:main.cpp
// 获取插件所在位置
// 在插件的搜索路径列表中添加一条路径
ctkPluginFrameworkLauncher::addSearchPath("../../../../CTKInstall/lib/ctk-0.1/plugins");
// 设置并启动 CTK 插件框架
ctkPluginFrameworkLauncher::start("org.commontk.eventadmin");
……
// 停止插件
ctkPluginFrameworkLauncher::stop();
事件注册监听(接收插件)
首先编写我们需要的接收者模块,并注册监听事件,这里我们新建一个模块 BlogEventHandler,模块的接口处理参见上面“CTK 插件的接口处理”。插件部分代码如下:
blog_event_handler.h
#ifndef BLOG_EVENT_HANDLER_H
#define BLOG_EVENT_HANDLER_H
#include <QObject>
#include <service/event/ctkEventHandler.h>
// 事件处理程序(或订阅者)
class BlogEventHandler : public QObject, public ctkEventHandler
{
Q_OBJECT
Q_INTERFACES(ctkEventHandler)
public:
// 处理事件
void handleEvent(const ctkEvent& event) Q_DECL_OVERRIDE
{
QString title = event.getProperty("title").toString();
QString content = event.getProperty("content").toString();
QString author = event.getProperty("author").toString();
qDebug() << "EventHandler received the message, topic:" << event.getTopic()
<< "properties:" << "title:" << title << "content:" << content << "author:" << author;
}
};
#endif // BLOG_EVENT_HANDLER_H
与上面自定义接口不同,这里我们实例化 ctkEventHandler 对象,并实现 handleEvent 接口。构造函数中注册的服务对象是 ctkEventHandler,在注册时指定触发的事件,当事件触发时调用该对象的 handleEvent 实现指定操作。
事件发送(发送插件)
监听对象完成后调用比较简单,代码如下:blog_manager.cpp
#include "blog_manager.h"
#include <service/event/ctkEventAdmin.h>
#include <QtDebug>
BlogManager::BlogManager(ctkPluginContext* context)
: m_pContext(context)
{
}
// 发布事件
void BlogManager::publishBlog(const Blog& blog)
{
ctkServiceReference ref = m_pContext->getServiceReference<ctkEventAdmin>();
if (ref) {
ctkEventAdmin* eventAdmin = m_pContext->getService<ctkEventAdmin>(ref);
ctkDictionary props;
props["title"] = blog.title;
props["content"] = blog.content;
props["author"] = blog.author;
ctkEvent event("org/commontk/bloggenerator/published", props);
qDebug() << "Publisher sends a message, properties:" << props;
eventAdmin->sendEvent(event);
}
}
项目代码:EventAdmin/SendEvent
事件发送方式(类通信、信号槽通信)
1、类通信
原理就是直接将信息使用 CTK 的 eventAdmin 接口 send/post 出去。
上面项目为典型类通信。
2、信号槽通信
原理是将 Qt 自己的信号与 CTK 的发送事件绑定、槽与事件订阅绑定。
接收槽
void BlogEventHandlerUsingSlotsActivator::start(ctkPluginContext* context)
{
m_pEventHandler = new BlogEventHandlerUsingSlots();
ctkDictionary props;
props[ctkEventConstants::EVENT_TOPIC] = "org/commontk/bloggenerator/published";
ctkServiceReference ref = context->getServiceReference<ctkEventAdmin>();
if (ref) {
ctkEventAdmin* eventAdmin = context->getService<ctkEventAdmin>(ref);
eventAdmin->subscribeSlot(m_pEventHandler, SLOT(onBlogPublished(ctkEvent)), props, Qt::DirectConnection);
}
}
发送信号
BlogManagerUsingSignals::BlogManagerUsingSignals(ctkPluginContext *context)
{
ctkServiceReference ref = context->getServiceReference<ctkEventAdmin>();
if (ref) {
ctkEventAdmin* eventAdmin = context->getService<ctkEventAdmin>(ref);
// 使用 Qt::DirectConnection 等同于 ctkEventAdmin::sendEvent()
eventAdmin->publishSignal(this, SIGNAL(blogPublished(ctkDictionary)), "org/commontk/bloggenerator/published", Qt::DirectConnection);
}
}
具体项目:项目代码:EventAdmin/SignalSlot
二者的区别
1、通过 event 事件通信,是直接调用 CTK 的接口,把数据发送到 CTK 框架;通过信号槽方式,会先在 Qt 的信号槽机制中转一次,再发送到 CTK 框架。故效率上来讲,event 方式性能高于信号槽方式。
2、两种方式发送数据到 CTK 框架,这个数据包含:主题+属性。主题就是 topic,属性就是 ctkDictionary。 一定要注意 signal 方式的信号定义,参数不能是自定义的,一定要是 ctkDictionary,不然会报信号槽参数异常错误。
3、两种方式可以混用,如发送 event 事件,再通过槽去接收;发送 signal 事件,再通过 event 是接收。
4、同步:sendEvent、QtDirectConnection;异步:postEvent、QtQueuedConnection
这里的同步是指:发送事件之后,订阅了这个主题的数据便会处理数据【handleEvent、slot】,处理的过程是在发送者的线程完成的。可以理解为在发送了某个事件之后,会立即执行所有订阅此事件的回调函数。
异步:发送事件之后,发送者便会返回不管,订阅了此事件的所有插件会根据自己的消息循环,轮到了处理事件后才会去处理。不过如果长时间没处理,CTK 也有自己的超时机制。如果事件处理程序花费的时间比配置的超时时间长,那么就会被列入黑名单。一旦处理程序被列入黑名单,它就不会再被发送任何事件。
插件依赖
插件加载时一般根据首字母大小自动加载,所以在插件启用时,某个插件还没有被调用,所以发送事件没有接收方,这样就要考虑到插件依赖关系,在 MANIFEST.MF 中添加依赖:
Plugin-SymbolicName:Plugin-xxx-1
Plugin-Version:1.0.0
Require-Plugin:Plugin-xxx-2; plugin-version="[1.0,2.0)"; resolution:="mandatory"
Plugin-xxx-2:为需要依赖的插件名【就是另一个插件在 MANIFEST.MF 里的 Plugin-SymbolicName】;
[1.0,2.0):为 Plugin-xxx-2 的版本,这里是左闭右开区间,默认是 1.0,;
resolution:有两个选择,optional、mandatory。前者是弱依赖,就算依赖的插件没有,当前插件也能正常使用,后者是强依赖,如果没有依赖的插件,就当前插件就不能被 start。
这样就向框架申明了,该插件加载时需要先加载 Plugin-xxx-2 插件,所有用户插件都应该有这样一份申明。
具体实现参见项目:RequirePlugin
插件元数据
获取 MANIFEST.MF 中的数据
QHash<QString, QString> headers = plugin->getHeaders();
ctkVersion version = ctkVersion::parseVersion(headers.value(ctkPluginConstants::PLUGIN_VERSION));
QString name = headers.value(ctkPluginConstants::PLUGIN_NAME);
具体实现参见项目:GetMetaData
高级使用 CTK Plugin Framework
CTK 服务工厂
注册服务的时候能够用服务工厂来注册,访问服务 getService 中的 plugin 参数是执行 ctkPluginContext::getService(const ctkServiceReference&)的插件,从而这里工厂根据执行的不同插件名称返回了不同的服务实现。
服务工厂的作用:
在服务中可以知道是哪个其他插件在使用它;
懒汉式使用服务,需要的时候才 new;
其他插件使用有服务工厂和使用无服务工的服务,没有任何区别,代码都一样;
可根据需要创建多种实现的服务,就是:多种服务对应一个插件。
接口类
#ifndef HELLO_SERVICE_H
#define HELLO_SERVICE_H
#include <QtPlugin>
class HelloService
{
public:
virtual ~HelloService() {}
virtual void sayHello() = 0;
};
#define HelloService_iid "org.commontk.service.demos.HelloService"
Q_DECLARE_INTERFACE(HelloService, HelloService_iid)
#endif // HELLO_SERVICE_H
多实现类
#ifndef HELLO_IMPL_H
#define HELLO_IMPL_H
#include "hello_service.h"
#include <QObject>
#include <QtDebug>
// HelloWorld
class HelloWorldImpl : public QObject, public HelloService
{
Q_OBJECT
Q_INTERFACES(HelloService)
public:
void sayHello() Q_DECL_OVERRIDE {
qDebug() << "Hello,World!";
}
};
// HelloCTK
class HelloCTKImpl : public QObject, public HelloService
{
Q_OBJECT
Q_INTERFACES(HelloService)
public:
void sayHello() Q_DECL_OVERRIDE {
qDebug() << "Hello,CTK!";
}
};
#endif // HELLO_IMPL_H
服务工厂类
#ifndef SERVICE_FACTORY_H
#define SERVICE_FACTORY_H
#include <ctkServiceFactory.h>
#include <ctkPluginConstants.h>
#include <ctkVersion.h>
#include "hello_impl.h"
class ServiceFactory : public QObject, public ctkServiceFactory
{
Q_OBJECT
Q_INTERFACES(ctkServiceFactory)
public:
ServiceFactory() : m_counter(0) {}
// 创建服务对象
QObject* getService(QSharedPointer<ctkPlugin> plugin, ctkServiceRegistration registration) Q_DECL_OVERRIDE {
Q_UNUSED(registration)
qDebug() << "Create object of HelloService for: " << plugin->getSymbolicName();
m_counter++;
qDebug() << "Number of plugins using service: " << m_counter;
QHash<QString, QString> headers = plugin->getHeaders();
ctkVersion version = ctkVersion::parseVersion(headers.value(ctkPluginConstants::PLUGIN_VERSION));
QString name = headers.value(ctkPluginConstants::PLUGIN_NAME);
QObject* hello = getHello(version);
return hello;
}
// 释放服务对象
void ungetService(QSharedPointer<ctkPlugin> plugin, ctkServiceRegistration registration, QObject* service) Q_DECL_OVERRIDE {
Q_UNUSED(plugin)
Q_UNUSED(registration)
Q_UNUSED(service)
qDebug() << "Release object of HelloService for: " << plugin->getSymbolicName();
m_counter--;
qDebug() << "Number of plugins using service: " << m_counter;
}
private:
// 根据不同的版本,获取不同的服务
QObject* getHello(ctkVersion version) {
if (version.toString().contains("alpha")) {
return new HelloWorldImpl();
} else {
return new HelloCTKImpl();
}
}
private:
int m_counter; // 计数器
};
#endif // SERVICE_FACTORY_H
可以根据插件,获取不同的服务。若主框架【main.cpp】的 symbolicName 是 system.plugin
激活类
#ifndef HELLO_ACTIVATOR_H
#define HELLO_ACTIVATOR_H
#include <ctkPluginActivator.h>
#include <ctkPluginContext.h>
#include "hello_service.h"
#include "service_factory.h"
class HelloActivator : public QObject, public ctkPluginActivator
{
Q_OBJECT
Q_INTERFACES(ctkPluginActivator)
Q_PLUGIN_METADATA(IID "HELLO")
public:
// 注册服务工厂
void start(ctkPluginContext* context) {
ServiceFactory *factory = new ServiceFactory();
context->registerService<HelloService>(factory);
}
void stop(ctkPluginContext* context) {
Q_UNUSED(context)
}
};
#endif // HELLO_ACTIVATOR_H
插件中访问服务
// 访问服务
ctkServiceReference reference = context->getServiceReference<HelloService>();
if (reference) {
HelloService* service = qobject_cast<HelloService *>(context->getService(reference));
if (service != Q_NULLPTR) {
service->sayHello();
}
}
具体实现参见项目:ServiceFactory
CTK 事件监听
CTK 一共有三种事件可以监听:框架事件、插件事件、服务事件。但是这些事件只有再变化时才能监听到,如果已经变化过后,进入一个稳定的状态,这时才去监听,那么是无法监听到的。
框架事件
针对整个框架的,相当于只有一个,因为框架只有一个,但是这里有个问题,就是监听这个事件是在框架初始化之后的,所以根本没法监听到框架事件的初始化,只能监听到结束的事件。类型有
FRAMEWORK_STARTED
PLUGIN_ERROR
PLUGIN_WARNING
PLUGIN_INFO
FRAMEWORK_STOPPED
FRAMEWORK_STOPPED_UPDATE
FRAMEWORK_WAIT_TIMEDOUT
服务事件
在创建、回收插件时的事情,主要体现在服务的注册和注销。类型有
REGISTERED
MODIFIED
MODIFIED_ENDMATCH
UNREGISTERING
插件事件
在安装、启动插件的过程中呈现的,主要就是插件的一个状态的变化。类型有
INSTALLED
RESOLVED
LAZY_ACTIVATION
STARTING
STARTED
STOPPING
STOPPED
UPDATED
UNRESOLVED
UNINSTALLED
监听例子
监听类,event_listener.h
#ifndef EVENT_LISTENER_H
#define EVENT_LISTENER_H
#include <QObject>
#include <ctkPluginFrameworkEvent.h>
#include <ctkPluginEvent.h>
#include <ctkServiceEvent.h>
class EventListener : public QObject
{
Q_OBJECT
public:
explicit EventListener(QObject *parent = Q_NULLPTR);
~EventListener();
public slots:
// 监听框架事件
void onFrameworkEvent(const ctkPluginFrameworkEvent& event);
// 监听插件事件
void onPluginEvent(const ctkPluginEvent& event);
// 监听服务事件
void onServiceEvent(const ctkServiceEvent& event);
};
#endif // EVENT_LISTENER_H
event_listener.cpp
#include "event_listener.h"
EventListener::EventListener(QObject *parent)
: QObject(parent)
{
}
EventListener::~EventListener()
{
}
// 监听框架事件
void EventListener::onFrameworkEvent(const ctkPluginFrameworkEvent& event)
{
if (!event.isNull()) {
QSharedPointer<ctkPlugin> plugin = event.getPlugin();
qDebug() << "FrameworkEvent: [" << plugin->getSymbolicName() << "]" << event.getType() << event.getErrorString();
} else {
qDebug() << "The framework event is null";
}
}
// 监听插件事件
void EventListener::onPluginEvent(const ctkPluginEvent& event)
{
if (!event.isNull()) {
QSharedPointer<ctkPlugin> plugin = event.getPlugin();
qDebug() << "PluginEvent: [" << plugin->getSymbolicName() << "]" << event.getType();
} else {
qDebug() << "The plugin event is null";
}
}
// 监听服务事件
void EventListener::onServiceEvent(const ctkServiceEvent &event)
{
if (!event.isNull()) {
ctkServiceReference ref = event.getServiceReference();
QSharedPointer<ctkPlugin> plugin = ref.getPlugin();
qDebug() << "ServiceEvent: [" << event.getType() << "]" << plugin->getSymbolicName() << ref.getUsingPlugins();
} else {
qDebug() << "The service event is null";
}
}
启用监听,main.cpp:
// 事件监听
EventListener listener;
context->connectFrameworkListener(&listener, SLOT(onFrameworkEvent(ctkPluginFrameworkEvent)));
context->connectPluginListener(&listener, SLOT(onPluginEvent(ctkPluginEvent)));
// 过滤 ctkEventAdmin 服务
// QString filter = QString("(%1=%2)").arg(ctkPluginConstants::OBJECTCLASS).arg("org.commontk.eventadmin");
context->connectServiceListener(&listener, "onServiceEvent"); //, filter);
具体实现参见项目:EventListener
CTK 服务追踪
服务追踪:如果想在 B 插件里使用 A 服务,可以专门写一个类继承 ctkServiceTracker,在这个类里完成对 A 服务的底层操作,然后在 B 插件里通过这个类提供的接口来使用回收 A 服务。
理论上 ctkServiceTracker 和 A 服务应该是一起的,这里有点像服务工厂。优点就是获取服务的代码简单,不用各种判断空指针。
服务 A
服务 A 实现类,log_impl.cpp
#include "log_impl.h"
#include <QtDebug>
LogImpl::LogImpl()
{
}
void LogImpl::debug(QString msg)
{
qDebug() << "This is a debug message: " << msg;
}
服务 A 激活类,log_activator.cpp
#include "log_impl.h"
#include "log_activator.h"
#include <ctkPluginContext.h>
#include <QtDebug>
void LogActivator::start(ctkPluginContext* context)
{
m_pPlugin = new LogImpl();
context->registerService<LogService>(m_pPlugin);
}
void LogActivator::stop(ctkPluginContext* context)
{
Q_UNUSED(context)
delete m_pPlugin;
m_pPlugin = Q_NULLPTR;
}
服务 A 的服务追踪类
追踪类,建立时机:
1、可以在封装 A 服务的时候就建立,作为一种工具向外提供,但是不应该被编译进插件中,它并不是插件的功能而是访问插件的工具;
2、也可以在 B 插件中建立,完全和 A 服务独立开,作为访问 A 服务的一种手段;
3、单独建立一个空工程,为项目中的所有服务建立对应的追踪类,然后放在同一个文件夹中,其他想要的自己使用就行。
注意:B 插件如果想要使用 A 服务,需要 service_tracker.h、service_tracker.cpp、A 服务的接口类。
本例采用第二种。
service_tracker.h
#ifndef SERVICE_TRACKER_H
#define SERVICE_TRACKER_H
#include <ctkPluginContext.h>
#include <ctkServiceTracker.h>
#include "../Log/log_service.h"
class ServiceTracker : public ctkServiceTracker<LogService *>
{
public:
ServiceTracker(ctkPluginContext* context) : ctkServiceTracker<LogService *>(context) {}
~ServiceTracker() {}
protected:
// 在 Service 注册时访问
LogService* addingService(const ctkServiceReference& reference) Q_DECL_OVERRIDE {
qDebug() << "Adding service:" << reference.getPlugin()->getSymbolicName();
// return ctkServiceTracker::addingService(reference);
LogService* service = (LogService*)(ctkServiceTracker::addingService(reference));
if (service != Q_NULLPTR) {
service->debug("Ok");
}
return service;
}
void modifiedService(const ctkServiceReference& reference, LogService* service) Q_DECL_OVERRIDE {
qDebug() << "Modified service:" << reference.getPlugin()->getSymbolicName();
ctkServiceTracker::modifiedService(reference, service);
}
void removedService(const ctkServiceReference& reference, LogService* service) Q_DECL_OVERRIDE {
qDebug() << "Removed service:" << reference.getPlugin()->getSymbolicName();
ctkServiceTracker::removedService(reference, service);
}
};
#endif // SERVICE_TRACKER_H
插件 B
插件 B 实现类, login_impl.cpp
#include "login_impl.h"
#include "service_tracker.h"
LoginImpl::LoginImpl(ServiceTracker *tracker)
: m_pTracker(tracker)
{
}
bool LoginImpl::login(const QString& username, const QString& password)
{
LogService* service = (LogService*)(m_pTracker->getService());
if (QString::compare(username, "root") == 0 && QString::compare(password, "123456") == 0) {
if (service != Q_NULLPTR)
service->debug("Login successfully");
return true;
} else {
if (service != Q_NULLPTR)
service->debug("Login failed");
return false;
}
}
插件 B 激活类,login_activator.cpp
#include "login_impl.h"
#include "login_activator.h"
#include "service_tracker.h"
#include <ctkPluginContext.h>
void LoginActivator::start(ctkPluginContext* context)
{
// 开启服务跟踪器
m_pTracker = new ServiceTracker(context);
m_pTracker->open();
m_pPlugin = new LoginImpl(m_pTracker);
m_registration = context->registerService<LoginService>(m_pPlugin);
}
void LoginActivator::stop(ctkPluginContext* context)
{
Q_UNUSED(context)
// 注销服务
m_registration.unregister();
// 关闭服务跟踪器
m_pTracker->close();
delete m_pPlugin;
m_pPlugin = Q_NULLPTR;
}
使用插件 B
// 获取插件所在位置
QString path = QCoreApplication::applicationDirPath() + "/plugins";
// 遍历路径下的所有插件
QDirIterator itPlugin(path, QStringList() << "*.dll" << "*.so", QDir::Files);
while (itPlugin.hasNext()) {
QString strPlugin = itPlugin.next();
try {
// 安装插件
QSharedPointer<ctkPlugin> plugin = context->installPlugin(QUrl::fromLocalFile(strPlugin));
// 启动插件
plugin->start(ctkPlugin::START_TRANSIENT);
qDebug() << "Plugin start ...";
} catch (const ctkPluginException &e) {
qDebug() << "Failed to install plugin" << e.what();
return -1;
}
}
// 获取服务引用
ctkServiceReference reference = context->getServiceReference<LoginService>();
if (reference) {
// 获取指定 ctkServiceReference 引用的服务对象
LoginService* service = qobject_cast<LoginService *>(context->getService(reference));
if (service != Q_NULLPTR) {
// 调用服务
service->login("root", "123456");
}
}
具体参见项目:ServiceTracker