当前位置: 首页 > 工具软件 > CTK > 使用案例 >

CTK框架使用简明教程

曾昂然
2023-12-01

CTK框架实际应用比较可靠,但网上资料很少。本教程围绕 CTK,探索 C++ 中的模块化技术,并能够基于 CTK 快速搭建 C++ 组件化框架,避免后来的人走弯路。本教程的源码下载地址:项目源代码。

CTK介绍

CTK 为支持生物医学图像计算的公共开发包,其全称为 Common Toolkit。CTK插件框架的设计有很大的灵感来自OSGi并且使得应用程序由许多不同的组件组合成一个可扩展模型。这个模型允许通过那些组件间共享对象的服务通信。

当前,CTK 工作的主要范围包括:

1.DICOM:提供了从 PACS 和本地数据库中查询和检索的高级类。包含 Qt 部件,可以轻松地设置服务器连接,并发送查询和查看结果。

2.DICOM Application Hosting:目标是创建 DICOM Part 19 Application Hosting specifications 的 C++ 参考实现。它提供了用于创建主机和托管应用程序的基础设。

3.Widgets:用于生物医学成像应用的 Qt Widgets 集合。

4.Plugin Framework:用于 C++ 的动态组件系统,以 OSGi 规范为模型。它支持一个开发模型,在这个模型中,应用程序(动态地)由许多不同(可重用的)组件组成,遵循面向服务的方法。

5.Command Line Interfaces:一种允许将算法编写为自包含可执行程序的技术,可以在多个终端用户应用程序环境中使用,而无需修改。

使用 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)。

项目结构

由于每一个插件都要建一个子项目,本项目刚开始创建时在QtCreator中选择新建-其他项目-子目录项目,新建项目名称为ctkExample,然后建立主程序入口项目,这里建立一个控制台项目,取名叫console。

更改项目输出路径:console.pro

DESTDIR = $$PWD/../bin

主函数中加载插件,启动框架:main.cpp

#include <QCoreApplication>
#include "ctkPluginFrameworkFactory.h"
#include "ctkPluginFramework.h"
#include "ctkPluginException.h"
#include <QDebug>
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
a.setApplicationName("ctkExample");
//给框架创建名称,Linux下没有这步会报错
ctkPluginFrameworkFactory factory;//插件工厂类
QSharedPointer<ctkPluginFramework> framework;
framework = factory.getFramework();
try{
framework->init();
framework->start();
}
catch(const ctkPluginException& e){
qDebug() << "framework init fail";
qDebug() << e.message() << e.getType();
}
return a.exec();
}

如果想把CTK初始化、插件安装启动、获取等操作封装成一个类,那么要注意:需要把CTK相关的变量定义成类属性,不能是局部变量,否则会出现各种问题如获取不了服务、服务引用为空等。

没有报错的话及表示插件加载成功!

其中QSharedPointer framework这个对象比较有意思,既可以作为对象也可以作为对象指针,但要作为插件框架使用必须要用指针方法调用,所以代码里使用“->”。

项目加载CTK框架插件

复制CTK安装目录到项目根目录,编译出的动态库就可以当普通动态库使用加载了,pro里面加载代码为:

LIBS += -L$$PWD/../libs/ctk-0.1 -lCTKCore
LIBS += -L$$PWD/../libs/ctk-0.1/ -lCTKPluginFramework
INCLUDEPATH += $$PWD/../includes/ctk-0.1
DEPENDPATH += $$PWD/../libs/ctk-0.1

CTK插件的接口处理

CTK框架由一个一个可分离的插件组成,框架对插件识别有一定要求,目前网上很多一整块扔出来对新人不太友好,博主这里讲解是尽量拆。单个插件最基本的格式要求分成Activator,qrc文件,以及MANIFEST.MF,我们以生成一个主界面模块MainWindow为例。

Activator注册器

每个插件都有自己的注册器Activator。

右键项目选择新建子项目-其他项目-Empty qmake Project,项目名称为MainWindow,pro文件中添加代码:

TEMPLATE = lib
TARGET = MainWindow
DESTDIR = $$PWD/../bin/plugins/$$TARGET
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
#加载ctk库
LIBS += -L$$PWD/../libs/ctk-0.1 -lCTKCore
LIBS += -L$$PWD/../libs/ctk-0.1/ -lCTKPluginFramework
INCLUDEPATH += $$PWD/../includes/ctk-0.1
#INCLUDEPATH += $$PWD/../libs/ctk-0.1
DEPENDPATH += $$PWD/../libs/ctk-0.1

生成的插件名(TARGET)不要有下划线,因为CTK会默认将插件名中的下划线替换成点号,最后后就导致找不到插件。

项目中添加C++类MainWindowActivator,代码如下:

mainwindowactivator.h

#ifndef MAINWINDOWACTIVATOR_H
#define MAINWINDOWACTIVATOR_H
#include <QObject>
#include "ctkPluginActivator.h"
class MainWindowActivator : public QObject, public ctkPluginActivator
{
public:
Q_OBJECT
Q_PLUGIN_METADATA(IID "MainWindow")
//向Qt的插件框架声明,希望将xxx插件放入到框架中。
Q_INTERFACES(ctkPluginActivator)
public:
MainWindowActivator();
void start(ctkPluginContext *context);
void stop(ctkPluginContext *context);
};
#endif // MAINWINDOWACTIVATOR_H

mainwindowactivator.cpp

#include "mainwindowactivator.h"
#include <QDebug>
MainWindowActivator::MainWindowActivator()
{
}
void MainWindowActivator::start(ctkPluginContext *context)
{
qDebug() << "mainwindow start";
}
void MainWindowActivator::stop(ctkPluginContext *context)
{
qDebug() << "my plugin stop";
Q_UNUSED(context)
/*
Q_UNUSED,如果一个函数的有些参数没有用到、某些变量
只声明不使用,但是又不想编译器、
编辑器报警报,其他没有什么实际性作用
*/
}

activator是标准的Qt插件类,它实现ctkPluginActivator的start、stop函数并对外提供接口。我这里是Qt5的版本,所以使用Q_PLUGIN_METADATA申明插件,Qt4需要用自己的方法实现插件。

qrc文件

创建插件的资源文件,格式如下:

<RCC>
<qresource prefix="/MainWindow/META-INF">
<file>MANIFEST.MF</file>
</qresource>
</RCC>

插件加载后会寻找同名前缀/META-INF,所以前缀格式固定,将MANIFEST.MF文件添加进来

MENIFEST.MF文件内容如下:

可直接在MF文件里添加自己特有的元数据

Plugin-SymbolicName:MainWindow
Plugin-Version:1.0.0
Plugin-Number:100 #元数据

注意:Plugin-SymbolicName要满足这里的前缀是:TARGET/META-INF格式。TARGET的名字最好和工程名一致,不然可能出现device not open错误。

获取MANIFEST.MF中的数据

QHash<QString, QString> headers = plugin->getHeaders();
ctkVersion version = ctkVersion::parseVersion(headers.value (ctkPluginConstants::PLUGIN_VERSION));
QString name = headers.value(ctkPluginConstants::PLUGIN_NAME);

文件包含ctk插件的基本信息,只要ctk框架正常识别到文件中Plugin-SymbolicName等信息,则判定它是一个ctk插件,能够正常调用activator中的start、stop函数。这个文件需要拷到插件生成路径下,pro文件中添加代码:

file.path = $$DESTDIR
file.files = MANIFEST.MF
INSTALLS += file

CTK插件启用

根据以上步骤,一个CTK插件接口定义基本完成,我们在console项目下调用观察插件是否能够正常加载。main函数中框架启动成功后添加以下代码:

QString dir = QCoreApplication::applicationDirPath();
dir += "/plugins/MainWindow/libMainWindow.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/ctkExample/bin/plugins/ MainWindow / MainWindow.dll"
mainwindow start

成功调用MainWindow中start内打印输出,则表明ctk插件接口正常定义并能成功加载。其中start(ctkPlugin::START_TRANSIENT)表示立即启用插件,不设置参数的话加载后也不会立即打印输出。

CTK插件间通信

CTK框架插件化开发实现功能的隔离,插件通信需要参照固定标准,这里介绍两种插件间通信的方法。以上面的MainWindow为例,主程序中以接口调用的方法弹出插件中的界面。由于涉及到Qt的Widget界面,请先将main函数中的QCoreApplication改为QApplication。

通信方法1-注册接口调用

函数接口

接口就是纯虚函数类,也就是最终的服务的前身。

上面我们已经编译出需要的动态库,首先确定我们需要插件向外部暴露的功能有什么,比如这里我们需要弹出窗口界面的操作,定义头文件如下:imainwindow.h

#ifndef IMAINWINDOW_H
#define IMAINWINDOW_H
#include <QObject>
class iMainWindow
{
public:
virtual void popMainWindow() = 0;
};
Q_DECLARE_INTERFACE(iMainWindow, "interface_mainwindow")
//此宏将当前这个接口类声明为接口,
后面的一长串就是这个接口的唯一标识。
#endif // IMAINWINDOW_H

Q_DECLARE_INTERFACE将接口类向Qt系统申明,然后添加它的实现对象:

接口的实现

插件就是实现这个接口类的实现类,所以理论上有多少个实现类就有多少个插件。

#ifndef MAINWINDOWPLUGIN_H
#define MAINWINDOWPLUGIN_H
#include <QObject>
#include "../includes/imainwindow.h"
#include "ctkPluginContext.h"
#include "mainwindowdlg.h"
class MainWindowPlugin : public QObject, public iMainWindow
{
Q_OBJECT
Q_INTERFACES(iMainWindow)
/*
此宏与Q_DECLARE_INTERFACE宏配合使用。
Q_DECLARE_INTERFACE:声明一个接口类
Q_INTERFACES:当一个类继承这个接口类,
表明需要实现这个接口类
*/
public:
MainWindowPlugin(ctkPluginContext *context);
virtual void popMainWindow();
private:
ctkPluginContext *m_context;
MainWindowDlg* m_windowDlg;
};
#endif // MAINWINDOWPLUGIN_H

mainwindowplugin.cpp

#include "mainwindowplugin.h"
MainWindowPlugin::MainWindowPlugin(ctkPluginContext *context)
:m_context(context)
{
m_windowDlg = new MainWindowDlg;
}
void MainWindowPlugin::popMainWindow()
{
m_windowDlg->show();
}

这仍是Qt的插件定义格式,但是不会作为插件导出,外部功能接口可以自定义。

服务注册(Activator注册服务)

激活类里有一个独占智能指针,指向接口类【使用多态,指针都指向父类】,然后在start里new一个实现类,注册这个实现类为服务,功能是实现接口类的接口,然后将智能指针指向这个实现类。可以理解为以后向框架索取这个服务的时候,实际获取的就是这个new出来的实现类。如果不用智能指针,就需要在stop里手动delete这个实现类。

每个插件都有自己的注册器Activator,功能节接口完成后,在插件启动时注册到ctk框架的服务中,代码如下:mainwindowactivator.cpp

void MainWindowActivator::start(ctkPluginContext *context)
{
qDebug() << "mainwindow start";
m_plugin = new MainWindowPlugin(context);
ctkDictionary dic;
context->registerService<iMainWindow>(m_plugin, dic);
}

接口调用

CTK插件启用后,就可以调用接口。

主函数框架及插件加载完成后,即可调用插件接口,代码如下:main.cpp

#include "../includes/imainwindow.h"
……
ctkPluginContext* context = framework-> getPluginContext ();
ctkServiceReference ref =context->getServiceReference <iMainWindow>();
iMainWindow* mainWindow;
if(ref)
mainWindow = context->getService<iMainWindow>(ref);
if(mainWindow)
mainWindow->popMainWindow();

在获取服务的时候,有两个重载方式【可直接使用的】

1、iMainWindow* ret = context->getService <iMainWindow > (reference);
2、iMainWindow* ret = qobject_cast <iMainWindow*> (context->getService(reference));

服务就是根据接口的实例,每生成一个服务就会调用一次注册器的start。把接口当做类,服务是根据类new出的对象,插件就是动态库dll。

小结:接口、插件、服务的关系

1、1对1

1个接口类由1个类实现,输出1个服务和1个插件。

2、多对1

1个类实现了2个接口类,输出2个服务和1个插件,无论想使用哪个服务最终都通过这同一个插件来实现。

3、1对多

1接口由2个类实现,也就是某一个问题提供了2种解决思路,输出1个服务和2个插件,通过ctkPluginConstants::SERVICE_RANKING和ctkPluginConstants::SERVICE_ID来调用不同的插件。这里虽然有两个插件,但都是被编译到同一个dll中的。

某插件每次调用另一个插件的时候,只会生成一个实例,然后把实例存到内存当中,不会因为多次调用而生成多个服务实例。

在使用1接口2插件的时候,虽然是两个插件,也会有两个激活类【从原理上来讲1个激活类就行了,但是在start里注册两次】,其中的IID只能有一个。从Qt插件基础上来说,一个dll只能有一个IID。

通信方法2-事件监听

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动态库

添加动态库可以使用ctkPluginFrameworkLauncher,代码如下:main.cpp

ctkPluginFrameworkLauncher::addSearchPath ("../libs/ctk-0.1/plugins");
ctkPluginFrameworkLauncher::start("org.commontk .eventadmin");
……
// 停止插件
ctkPluginFrameworkLauncher::stop();

需要在框架加载前调用。

事件注册监听(接收插件)

首先编写我们需要的接收者模块,并注册监听事件,这里我们新建一个模块Client1,模块的接口处理参见上面“CTK插件的接口处理”。插件部分代码如下:

client1plugin.h

#ifndef CLIENT1PLUGIN_H
#define CLIENT1PLUGIN_H
#include <QObject>
#include "ctkPluginContext.h"
#include "service/event/ctkEventAdmin.h"
#include "service/event/ctkEventHandler.h"
#include "client1dlg.h"
class Client1Plugin : public QObject, public
ctkEventHandler
{
Q_OBJECT
Q_INTERFACES(ctkEventHandler)
public:
Client1Plugin(ctkPluginContext *context);
protected:
void handleEvent(const ctkEvent& event);
signals:
void openDlg();
public slots:
void onOpenDlg();
private:
void registToMainWindow();
ctkPluginContext *m_context;
Client1Dlg* m_clientDlg;
};
#endif

#include "client1plugin.h"
#include <service/event/ctkEventConstants.h>
Client1Plugin::Client1Plugin(ctkPluginContext *context)
:m_context(context)
{
m_clientDlg = new Client1Dlg;
connect(this, SIGNAL(openDlg()), this, SLOT(onOpenDlg()),
Qt::QueuedConnection);
//注册监听信号"zhimakaimen"
ctkDictionary dic;
dic.insert(ctkEventConstants::EVENT_TOPIC, "zhimakaimen");
context->registerService<ctkEventHandler>(this, dic);
}
void Client1Plugin::handleEvent(const ctkEvent& event)
{
//接收监听事件接口
if(event.getTopic() == "zhimakaimen")
{
emit openDlg();
//这里用了信号槽异步,因为线程中不能调用界面元素
}
}

与上面自定义接口不同,这里我们实例化ctkEventHandler对象,并实现handleEvent接口。构造函数中注册的服务对象是ctkEventHandler,在注册时指定触发的事件,当事件触发时调用该对象的handleEvent实现指定操作。

事件发送(发送插件)

监听对象完成后调用比较简单,比如我们直接在窗体的菜单栏中,点击actoin调用,代码如下:mainwindowdlg.cpp

#include "mainwindowdlg.h"
#include "ui_mainwindowdlg.h"
#include "service/event/ctkEvent.h"
#include "service/event/ctkEventAdmin.h"
#include "service/event/ctkEventHandler.h"
MainWindowDlg::MainWindowDlg(ctkPluginContext * context , QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindowDlg),
m_context(context)
{
ui->setupUi(this);
QAction* action = new QAction("用户1");
ui->menubar->addAction(action);
connect(action, SIGNAL(triggered(bool)), this , SLOT (action_clicked()));
}
MainWindowDlg::~MainWindowDlg()
{
delete ui;
}
void MainWindowDlg::action_clicked()
{
//获取事件服务接口
ctkServiceReference ref;
ctkEventAdmin* eventAdmin;
ref = m_context->getServiceReference<ctkEventAdmin>();
if(ref)
{
eventAdmin = m_context->getService<ctkEventAdmin>(ref);
m_context->ungetService(ref);
}
//发送事件
ctkDictionary message;
if(eventAdmin)
eventAdmin->postEvent(ctkEvent("zhimakaimen", message));
}

事件发送二种方式:类通信、信号槽通信

1、类通信

原理就是直接将信息使用CTK的eventAdmin接口send/post出去。

2、信号槽通信

原理是将Qt自己的信号与CTK的发送事件绑定、槽与事件订阅绑定。

二种方式的区别:

1、通过event事件通信,是直接调用CTK的接口,把数据发送到CTK框架;通过信号槽方式,会先在Qt的信号槽机制中转一次,再发送到CTK框架。故效率上来讲,event方式性能高于信号槽方式。

2、两种方式发送数据到CTK框架,这个数据包含:主题+属性。主题就是topic,属性就是ctkDictionary。 一定要注意signal方式的信号定义,参数不能是自定义的,一定要是ctkDictionary,不然会报信号槽参数异常错误。

3、两种方式可以混用,如发送event事件,再通过槽去接收;发送signal事件,再通过event是接收。

4、同步:sendEvent、Qt::DirectConnection;异步:postEvent、Qt::QueuedConnection

这里的同步是指:发送事件之后,订阅了这个主题的数据便会处理数据【handleEvent、slot】,处理的过程是在发送者的线程完成的。可以理解为在发送了某个事件之后,会立即执行所有订阅此事件的回调函数。

异步:发送事件之后,发送者便会返回不管,订阅了此事件的所有插件会根据自己的消息循环,轮到了处理事件后才会去处理。不过如果长时间没处理,CTK也有自己的超时机制。如果事件处理程序花费的时间比配置的超时时间长,那么就会被列入黑名单。一旦处理程序被列入黑名单,它就不会再被发送任何事件。

插件依赖

插件加载时一般根据首字母大小自动加载,所以在Client1启用时,MainWindow还没有被调用,所以发送的”event/registAction”事件没有接收方,这样就要考虑到插件依赖关系,在MANIFEST.MF中添加依赖:

Plugin-SymbolicName:Client1
Plugin-Version:1.0.0
Require-Plugin:MainWindow; plugin-version="[1.0,2.0)" ; resolution ="mandatory"

MainWindow:为需要依赖的插件名【就是另一个插件在MANIFEST.MF里的Plugin-SymbolicName】;

[1.0,2.0):为MainWindow的版本,这里是左闭右开区间,默认是1.0,;

resolution:有两个选择,optional、mandatory。前者是弱依赖,就算依赖的插件没有,当前插件也能正常使用,后者是强依赖,如果没有依赖的插件,就当前插件就不能被start。

这样就向框架申明了,该插件加载时需要先加载MainWindow插件,所有用户插件都应该有这样一份申明

 类似资料: