Создание системы расширения на библиотеке Qt +12



Плагины(Расширения)


Расширения это совместно используемая динамическая библиотека предназначенная для загрузки в процессе исполнения основного приложения, которая обязательно должна реализовывать хотя бы один специальный интерфейс.

Расширения делятся на два типа:

  • Для Qt
  • Для собственных приложений

Разберём как создать свою систему расширений и сами расширения для него.

Связь с расширением осуществляется с помощью интерфейса (сигналы, слоты и методы класса). Расширение загружается приложением при помощи класса QPluginLoader. Для загрузки расширения используется метод instance(), который создаёт объект расширения и возвращает указатель на него. Для выгрузки расширения используется метод unload().

Часть 1


В первом примере создадим расширение которое будет использовать функцию(алгоритм, формулу) из расширения.

Визуальная схема проекта будет выглядеть следующим образом.



Этап 1:


Первым этапом создадим класс интерфейсов наследуемый от QObject, в качестве интерфейса будет метод который принимает переменную типа QString и возвращает эту же строку в верхнем регистре. С помощью макроса Q_DECLARE_INTERFACE, задаём идентификотор интерфейсов, компилятор с генерирует метаинформацию для строки-идентификатор. Данный модуль является протоколом общения между плагином и основной программой и будет использоваться в проекте плагина и в основном проекте.

Класс будет выглядеть следующем образом.

//---------------------------------------------------
#ifndef INTERFACE_H
#define INTERFACE_H
//-------------------------------------------------------
#include <QObject>
//-------------------------------------------------------
class interface : public QObject
{
public:
        /// \brief виртуальный деструктор
        virtual ~interface() = default;
        /// \brief Интерфейс расширения
        virtual QString getUpString(QString str) = 0;
};
//----------------------------------------------------------------
Q_DECLARE_INTERFACE(interface, "com.mysoft.Application.interface")
//----------------------------------------------------------------
#endif // INTERFACE_H
//----------------------------------------------------------------


Этап 2:


Создадим базовое приложение которое будет загружать расширение. По нажатию кнопку будет происходить поиск расширения и загрузка его в систему. Далее через интерфейс будем задействовать нашу функцию.

Базовое приложение:

mainproject.h

//---------------------------------------------------
#ifndef MAINPROJECT_H
#define MAINPROJECT_H
//-------------------------------------------------------
#include <QWidget>
#include <QPluginLoader>
#include <QDir>
#include "interface.h"
//-------------------------------------------------------
namespace Ui {
class mainProject;
}
//-------------------------------------------------------
class mainProject : public QWidget
{
        Q_OBJECT
public:
   /// \brief конструктор
   explicit mainProject(QWidget *parent = nullptr);
   /// \brief деструктор
   ~mainProject();
private slots:
    /// \brief Поиск плагина
    void on_searchPlugin_clicked();
    /// \brief Использования интерфейса
    void on_getUp_clicked();
private:
    Ui::mainProject *ui;
    interface *pluginObject; ///< Указатель на объект плагина
};
//-------------------------------------------------------
#endif // MAINPROJECT_H
//-------------------------------------------------------

mainproject.cpp

//---------------------------------------------------
#include "mainproject.h"
#include "ui_mainproject.h"
//-------------------------------------------------------
mainProject::mainProject(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::mainProject)
{
        ui->setupUi(this);
}
//-------------------------------------------------------
mainProject::~mainProject()
{
        delete ui;
}
//-------------------------------------------------------
void mainProject::on_searchPlugin_clicked()
{
        QStringList listFiles;
        QDir dir(QApplication::applicationDirPath() + "/Plugins/");
        // Поиск всех файлов в папке "Plugins"
        if(dir.exists())
           listFiles = dir.entryList(QStringList("*"), QDir::Files);
        // Проход по всем файлам
        for(QString str: listFiles)
        {
          QPluginLoader loader(dir.absolutePath() + "/" +str);
          QObject *pobj = 0;
          // Загрузка плагина
          pobj = qobject_cast<QObject*>(loader.instance());
          if(!pobj)
              continue;
          pluginObject = 0;
          // Получения интерфейсов
          pluginObject = qobject_cast<interface *>(pobj);
          // Проверка тот ли загружен плагин
          if(pluginObject)
           {
              ui->label->setText("Расширение найдено");
              break;
           }
        }
}
//-------------------------------------------------------
void mainProject::on_getUp_clicked()
{
        QString tmp;
        tmp = ui->lineEdit->text();
        // использование интерфейса getUpString()
        tmp = pluginObject->getUpString(tmp);
        ui->label_2->setText(tmp);
}
//-------------------------------------------------------


Этап 3:


Создание расширения, первое что нужно сделать это в pro файле изменить типа собираемого проекта, для этого нужно добавить следующую строку TEMPLATE = lib, и задать конфигурацию проекта под расширения CONFIG += plugin.

upperstringplugin.pro

#-------------------------------------------------
#
# Project created by QtCreator 2019-04-03T11:35:18
#
#-------------------------------------------------
QT       += core
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = upperStringPlugin
TEMPLATE = lib
CONFIG += plugin
DESTDIR = ../Plugins
DEFINES += QT_DEPRECATED_WARNINGS
CONFIG += c++11
SOURCES +=         upperstringplugin.cpp
HEADERS +=         upperstringplugin.h     	  interface.h

Далее создаём класс будущего расширения, класс должен быть унаследован от класса интерфейсов. Макрос Q_INTERFACES, нужен что бы компилятор с генерировал всю необходимую мета информацию для расширения. Макрос Q_PLUGIN_METADATA(), задаёт точку входа в расширение и доступ для библиотеки Qt. Также нужно создать файл inteface.json с метаинформацией(файл должен находиться в корне проекта), в нашем случае там нет информации поэтому просто запишем пустые кавычки {} в файл.

upperstringplugin.h

//---------------------------------------------------
#ifndef UPPERSTRINGPLUGIN_H
#define UPPERSTRINGPLUGIN_H
//---------------------------------------------------
#include "interface.h"
//---------------------------------------------------
class upperStringPlugin : public interface
{
        Q_OBJECT
        Q_INTERFACES(interface)
        Q_PLUGIN_METADATA(IID "com.mysoft.Application.interface" FILE "interface.json")
public:
        explicit upperStringPlugin();
        ~upperStringPlugin();
        // interface interface
public:
        QString getUpString(QString str);
};
//---------------------------------------------------
#endif // UPPERSTRINGPLUGIN_H
//---------------------------------------------------

upperstringplugin.cpp

//---------------------------------------------------
#include "upperstringplugin.h"
//---------------------------------------------------
upperStringPlugin::upperStringPlugin()
{}
//---------------------------------------------------
upperStringPlugin::~upperStringPlugin()
{}
//---------------------------------------------------
QString upperStringPlugin::getUpString(QString str)
{
   return str.toUpper();
}
//---------------------------------------------------

На выходе компиляции проекта мы получим файл с расширением .so, данный файл перемещаем в папку «Plugins» главного проекта и запускаем его. В данном случае расширение загружается в основную программу и создаётся единственный объект расширения. При попытки заново использовать функцию instance(), функция вернёт указатель на уже созданный объект расширения.

Выполнение программы



Часть 2


Усложним нашу задачу, теперь нам нужно что бы расширение представляло собой виджет и что бы была возможность создавать несколько таких виджетов. Основная программа будет принимать сообщения от плагинов и отправлять обратно ответ. Создадим новые проекты, на первом этапе нам понадобиться два класса интерфейсов, один будет отвечать за загрузку расширения и создания виджета, другой за работу самого виджета.

Схема проекта будет выглядеть следующим образом:



Этап 1:


Первый класс interface будет иметь две функции, получить названия плагина и получить виджет плагина. Название плагина будем хранить для индентификации его в системе. Виджет плагина мы будем добавлять в MDI окна основного приложения.

Второй класс является самим графическим виджетом, он унаследован от QWidget, здесь мы прописали необходимые нам функции, виджет будет получать сообщение и отправлять в основную программу.

interface.h

//-------------------------------------------------------------------------
#ifndef INTERFACE_H
#define INTERFACE_H
//-------------------------------------------------------------------------
#include <QWidget>
class QString;
//-------------------------------------------------------------------------
class interface : public QObject
{
public:
        /// \brief Деструктор
        virtual ~interface(){}
        /// \brief Получить название плагина
        virtual QString getNamePlugin() = 0;
        /// \brief Получить новый виджет
        virtual QObject *getPluginWidget() = 0;
};
//-------------------------------------------------------------------------
class interfaceWidget: public QWidget
{
public:
        /// \brief Деструктор
        virtual ~interfaceWidget() = default;
signals:
        /// \brief Сигнал отправляет текст из расширения
        virtual void signal_writeText(QString str) = 0;
public slots:
        /// \brief Слот получает текст в плагин
        virtual void slot_getText(QString str) = 0;
};
//-------------------------------------------------------------------------
Q_DECLARE_INTERFACE(interface, "com.mysoft.Application.interface")
//-------------------------------------------------------------------------
#endif // INTERFACE_H
//-------------------------------------------------------------------------

Этап 2:


Основная программа состоит из MDI окна, в котором есть основной виджет для принятия сообщений от плагинов и дополнительные окна, которые динамически появляются по мере вызова плагинов.

При создания виджета плагина мы соединяем сигнал от плагина со слотом и с помощью функции sender() мы получаем указатель на плагин который прислал сообщение. Созданный виджет мы помещаем в MDI окно, а сам объект плагина можно выгрузить из системы.

mainproject.h

//------------------------------------------------
#ifndef MAINPROJECT_H
#define MAINPROJECT_H
//------------------------------------------------
#include <QMainWindow>
#include <QDir>
#include <QPluginLoader>
#include "interface.h"
//------------------------------------------------
namespace Ui {
class mainProject;
}
//------------------------------------------------
typedef struct str_plugin
{
        QString namePlugin;     ///< Имя плагина
        QString dirPlugin;      ///< Расположение плагина
}TSTR_PLUGIN;
//------------------------------------------------
class mainWidget;
//------------------------------------------------
class mainProject : public QMainWindow
{
        Q_OBJECT
public:
        explicit mainProject(QWidget *parent = nullptr);
        ~mainProject();
private slots:
        void on_action_triggered();
        /// \brief Слот Запуска плагина
        void slot_showPlugin();
        /// \brief Функция получения текста от плагина и отправляет ему ответ
        void slot_getTextFromPlugin(QString str);
private:
        Ui::mainProject *ui;
        mainWidget *widget;     ///< Основное окно
        QVector<TSTR_PLUGIN > vecPlugin;        ///< Вектор плагинов
};
//------------------------------------------------
#endif // MAINPROJECT_H
//------------------------------------------------

mainproject.cpp

//------------------------------------------------
#include "mainproject.h"
#include "ui_mainproject.h"
#include "mainwidget.h"
#include <QMdiSubWindow>
//------------------------------------------------
mainProject::mainProject(QWidget *parent) :
        QMainWindow(parent),
        ui(new Ui::mainProject)
{
        ui->setupUi(this);
        QMdiSubWindow *sWPS = new QMdiSubWindow;
        widget = new mainWidget();
        sWPS->setWidget(widget);
        ui->mdiArea->addSubWindow(sWPS);
}
//------------------------------------------------
mainProject::~mainProject()
{
        delete ui;
}
//------------------------------------------------
void mainProject::on_action_triggered()
{
        ui->menu_2->clear();
        QStringList listFiles;
        QDir dir(QApplication::applicationDirPath() + "/Plugins/");
        if(dir.exists())
        {
            listFiles = dir.entryList(QStringList("*"), QDir::Files);
        }
        for(QString str: listFiles)
        {
            QPluginLoader loader(dir.absolutePath() + "/" +str);
            QObject *pobj = 0;
            pobj = qobject_cast<QObject*>(loader.instance());
            if(!pobj)
              continue;
            interface *plW = 0;
            plW = qobject_cast<interface *>(pobj);
            if(!plW)
                continue;
            QString namePlugin = plW->getNamePlugin();
            QAction *action = new QAction(namePlugin);
            ui->menu_2->addAction(action);
            connect(action, SIGNAL(triggered()), this, SLOT(slot_showPlugin()));
            TSTR_PLUGIN plug;
            plug.namePlugin = namePlugin;
            plug.dirPlugin = dir.absolutePath() + "/" +str;
            vecPlugin.push_back(plug);
            delete plW;
        }
}
//------------------------------------------------
void mainProject::slot_showPlugin()
{
        QObject *pobj = sender();
        QAction *action = qobject_cast<QAction *>(pobj);
        QString namePlugin = action->iconText();
        for(int i = 0; i < vecPlugin.size(); i++)
        {
                if(namePlugin == vecPlugin[i].namePlugin)
                {
                   QMdiSubWindow *sWPS = new QMdiSubWindow;
                   ui->mdiArea->addSubWindow(sWPS);
                   sWPS->setAttribute(Qt::WA_DeleteOnClose, true);
                   QPluginLoader loader(vecPlugin[i].dirPlugin);
                   QObject *pobj = qobject_cast<QObject*>(loader.instance());
                   if(!pobj)
                      continue;
                   interface *plW = qobject_cast<interface *>(pobj);
                   if(!plW)
                      continue;
                   QObject *ob = plW->getPluginWidget();
                    if(!ob)
                      continue;
                    interfaceWidget *interFaceW = dynamic_cast<interfaceWidget *>(ob);
                      if(!interFaceW)
                       continue;
                    sWPS->setWidget(interFaceW);
                    sWPS->show();
                    QSize size = interFaceW->minimumSize();
                    size.setHeight(size.height() + 20);
                    size.setWidth(size.width() + 20);
                    sWPS->resize(size);
                    loader.unload();
                    connect(interFaceW, SIGNAL(signal_writeText(QString)), this, SLOT(slot_getTextFromPlugin(QString)));
                }
        }
}
//------------------------------------------------
void mainProject::slot_getTextFromPlugin(QString str)
{
        //Получение указателя на отправителя сообщения
        QObject *pobj = sender();
        interfaceWidget *pPlug =  dynamic_cast<interfaceWidget *>(pobj);
        widget->slot_getText("Получено сообщение от плагина");
        widget>slot_getText(str);
        widget->slot_getText("Отправлен ответ");
        widget>slot_getText("------------------------------");
        pPlug->slot_getText("Сообщение доставлено");
}
//------------------------------------------------

Основное окно, принимает сообщение и отображает его.

mainwidget.h

//----------------------------------------------------------
#ifndef MAINWIDGET_H
#define MAINWIDGET_H
//----------------------------------------------------------
#include <QWidget>
//----------------------------------------------------------
namespace Ui {
class mainWidget;
}
//----------------------------------------------------------
class mainWidget : public QWidget
{
        Q_OBJECT
public:
        explicit mainWidget(QWidget *parent = nullptr);
        ~mainWidget();
public slots:
        /// \brief Слот принимает сообщения от плагинов
        void slot_getText(QString str);
private:
        Ui::mainWidget *ui;
};
//----------------------------------------------------------
#endif // MAINWIDGET_H
//----------------------------------------------------------

mainwidget.cpp

//----------------------------------------------------------
#include "mainwidget.h"
#include "ui_mainwidget.h"
//----------------------------------------------------------
mainWidget::mainWidget(QWidget *parent) :
        QWidget(parent),
        ui(new Ui::mainWidget)
{
        ui->setupUi(this);
}
//----------------------------------------------------------
mainWidget::~mainWidget()
{
        delete ui;
}
//----------------------------------------------------------
void mainWidget::slot_getText(QString str)
{
        ui->textEdit->append(str);
}
//----------------------------------------------------------

Этап 2:


Создаём плагин, его идея состоит в том что он является фабрикой для создания виджета.

plugin.h

//-------------------------------------------------
#ifndef PLUGIN_H
#define PLUGIN_H
//-------------------------------------------------
#include "interface.h"
#include "texttranferwidget.h"
//-------------------------------------------------
class plugin : public interface
{
	Q_OBJECT
	Q_INTERFACES(interface)
	Q_PLUGIN_METADATA(IID "com.mysoft.Application.interface" FILE "interface.json")

public:
	explicit plugin();
	~plugin();

	// interface interface
public:
	/// \brief Получить название плагина
	QString getNamePlugin();
	/// \brief Получить новый виджет
	QObject *getPluginWidget();
};
//-------------------------------------------------
#endif // PLUGIN_H
//-------------------------------------------------

plugin.cpp

//-------------------------------------------------
#include "plugin.h"
//-------------------------------------------------
plugin::plugin()
{
}
//-------------------------------------------------
plugin::~plugin()
{
}
//-------------------------------------------------
QString plugin::getNamePlugin()
{
	return "Тестовый плагин1";
}
//-------------------------------------------------
QObject *plugin::getPluginWidget()
{
	textTranferWidget *widget = new textTranferWidget();
	return qobject_cast<QObject *>(widget);
}
//-------------------------------------------------

Виджет создаваемый плагином.

texttranferwidget.h

//-------------------------------------------------------------------
#ifndef TEXTTRANFERWIDGET_H
#define TEXTTRANFERWIDGET_H
//-------------------------------------------------------------------
#include "interface.h"
//-------------------------------------------------------------------
namespace Ui {
class textTranferWidget;
}
//-------------------------------------------------------------------
class textTranferWidget : public interfaceWidget
{
	Q_OBJECT

public:
	/// \brief Конструктор
	explicit textTranferWidget();
	/// \brief Деструктор
	~textTranferWidget();
private:
	Ui::textTranferWidget *ui;

	// interfaceWidget interface
signals:
	/// \brief Сигнал отправляет текст из расширения
	void signal_writeText(QString str);

public slots:
	/// \brief Слот получает текст в плагин
	void slot_getText(QString str);
private slots:
	void on_pushButton_clicked();
};
//-------------------------------------------------------------------
#endif // TEXTTRANFERWIDGET_H
//-------------------------------------------------------------------

texttranferwidget.cpp

//-------------------------------------------------------------------
#include "texttranferwidget.h"
#include "ui_texttranferwidget.h"
//-------------------------------------------------------------------
textTranferWidget::textTranferWidget() :
	ui(new Ui::textTranferWidget)
{
	ui->setupUi(this);
}
//-------------------------------------------------------------------
textTranferWidget::~textTranferWidget()
{
	delete ui;
}
//-------------------------------------------------------------------
void textTranferWidget::slot_getText(QString str)
{
	ui->textEdit->append(str);
}
//-------------------------------------------------------------------
void textTranferWidget::on_pushButton_clicked()
{
		emit signal_writeText(ui->lineEdit->text());
}
//-------------------------------------------------------------------

Вывод основной программы:

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (15):

  1. aol-nnov
    /#20054808

    У меня вопросы :)


    Имена классов с маленькой буквы… ну, такое…
    Зачем интерфейс — наследник куобжекта, если в нем только pure virtual функции?
    В составе примеров куте есть отличный plug and paint на эту тему :)

    • CIUK
      /#20057930

      Первая статья и всё такое, извиняюсь:). А причём тут plug and paint? вы мне предлагаете рисовать весь виджет(все кнопки, весь функционал?), Зачем если виджет несёт за собой много полезных функций, и второе я добавляю свой функционал с помощью виртуализации и третье я могу легко работать с этим виджетом, скрывать когда мне нужно удалять, загружать заново. Вы не поняли суть стать.

      • aol-nnov
        /#20057940

        промахнулся с веткой, ответ рядом :/

    • CIUK
      /#20058166

      Наследник QObject нужен для сохранения системы сигналов и слотов(по необходимости), если не нужны сигналы можно и не наследоваться от него

      • aol-nnov
        /#20058180

        Ваша идиллическая картина рушится в следующий момент после возникновения необходимости реализации нескольких таких интерфейсов в одном плагине. (Почему? потому что возникает diamond problem)

        Лучше так:

        class Plugin : public QObject, public Interface, public Interface2 {}
        


        а из интерфейсов убрать наследование от куобжекта, тем более, оно там ни к чему, на самом деле :)

        • CIUK
          /#20058238

          Насчёт убрать QObject да возможно, просто привычно работать с объектами.
          Во второй части статьи, создаётся виджет с интерфейсами, я могу прописать сколько хочу интерфейсов, в том числе сигналы и слоты и передавать любые данные, хоть структурные.

        • CIUK
          /#20058244

          Плагин служит лишь фабрикой для создания виджета, где я могу прописать сколько угодно интерфейсов

        • CIUK
          /#20058254

          И там нету diamond problem, плагин и созданный виджет не связаны между собой.

          • aol-nnov
            /#20058260

            «там» — нет. я написал, при каких условиях она может возникнуть (и это совсем не надуманная проблема).

        • BeardedBeaver
          /#20060082

          Я вам больше скажу, Qt просто не даст отнаследовать класс от более, чем одного QObject-based класса.

          • aol-nnov
            /#20060102

            об этом я автору и пытаюсь донести!

            а «не даст отнаследовать класс от более, чем одного QObject-based класса» — это та самая «diamond problem» и конкретно к куте отношения не имеет :)

            • CIUK
              /#20060196

              Зачем вы ее упоминали тогда?

              • aol-nnov
                /#20060320

                У меня создается впечатление, что милорд читает мои ответы через строку.

                Diamond problem упоминается здесь потому, что она есть в с++ независимо от того, пишет ли автор с применением куте или на голом с++. Надеюсь, так понятнее выразил свою мысль.

  2. aol-nnov
    /#20057938

    > А причём тут plug and paint

    он тут при том, что это пример плагинной архитектуры программы от создателей куте. И не имеет некоторых недостатков, присущих данной статье. Только лишь.

  3. AN3333
    /#20057948

    .