CTK完整教學(OSGI for C++ 實現 C++ Qt 模組化)

CTK完整教學(OSGI for C++ 實現 C++ Qt 模組化)

Qt模組化開發框架介紹

最後更新 2022/3/24 上午12:32
来唧唧歪歪
預計閱讀 37 分鐘
分類
前端
標籤
外掛化 模組化 CTK OSGI Qt

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 框架中。

介面類別沒有什麼變化,實現類別少了註冊的程式碼,建構函式也無參數,註冊的過程放在了啟動類別裡。

  1. 實現類別

.h

HelloImpl( );

.cpp

HelloImpl::HelloImpl( )
{
     qDebug()<<"this is imp";
}
  1. 啟動類別

.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&)的外掛程式,從而這裡工廠根據執行的不同外掛程式名稱返回了不同的服務實現。

服務工廠的作用:

  1. 在服務中可以知道是哪個其他外掛程式在使用它;

  2. 懶漢式使用服務,需要的時候才 new;

  3. 其他外掛程式使用有服務工廠和使用無服務工的服務,沒有任何區別,程式碼都一樣;

  4. 可根據需要建立多種實現的服務,就是:多種服務對應一個外掛程式。

介面類別

#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

繼續探索

延伸閱讀

更多文章
同分類 2023/10/16

.NET工具箱:開源、免費的純前端工具網站,帶你探索10大工具分類和73個實時在線小工具

Dotnet工具箱是一個純前端的、開源和免費的工具網站,週末我參考了開源專案it-tools,對網站界面文字進行了中文化,並重新部署了網站。該網站共有10大工具分類,提供了73個實時在線小工具。使用Vue3開發的Dotnet工具箱具有獨特的特色,本文詳細介紹了其中一些特色工具,並簡單分享了如何部署自己的工具網站。如果你對工具網站感興趣,不妨來了解一下Dotnet工具箱吧!

繼續閱讀