Qt插件机制介绍及实现

创建应用程序主窗口

创建Qt项目

$ mkdir ImageView
$ touch main.cpp mainwindow.h mainwindow.cpp
$ qmake -project
$ ls
mainwindow.h ImageView.pro main.cpp mainwindow.cpp

编辑项目文件ImageView.pro

$ vim ImageView.pro

修改后项目文件内容如下:

#项目模板
TEMPLATE = app
#生成目标
TARGET = ImageView
INCLUDEPATH += .
#向qmake声明应用程序依赖widgets模块
greaterThan(QT_MAJOR_VERSION,4): QT += widgets
#引入头文件
HEADERS += mainwindow.h
SOURCES += main.cpp mainwindow.cpp

mainwindow.cpp

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QMenuBar>
#include <QToolBar>
#include <QAction>
#include <QGraphicsScene>
#include <QGraphicsView>
#include <QGraphicsPixmapItem>

//创建新的窗口类
class MainWindow : public QMainWindow
{
    Q_OBJECT
    
public:
    explicit MainWindow(QWidget *parent=nullptr);
    ~MainWindow();

private:
    void initUI();
    void createActions();
    void showImage(QString);

private slots:
    void openImage();

private:
    QMenu *fileMenu;
    QMenu *editMenu;

    QToolBar *fileToolBar;
    QToolBar *editToolBar;

    QGraphicsScene *imageScene;
    QGraphicsView *imageView;

    QAction *openAction;

    QString currentImagePath;
    QGraphicsPixmapItem *currentImage;
};

#endif // MAINWINDOW_H

main.cpp

#include <QApplication>
#include "mainwindow.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MainWindow window;
    window.setWindowTitle("ImageView");
    window.show();
    return app.exec();
}

mainwindow.cpp

#include <QApplication>
#include <QFileDialog>
#include <QMessageBox>
#include <QPixmap>
#include <QKeyEvent>
#include <QDebug>
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
    , fileMenu(nullptr)
    , editMenu(nullptr)
    , currentImage(nullptr)
{
    initUI();
}

MainWindow::~MainWindow()
{
}

void MainWindow::initUI()
{
    this->resize(800, 600);
    // 创建菜单
    fileMenu = menuBar()->addMenu("&File");
    editMenu = menuBar()->addMenu("&Edit");

    // 创建工具栏
    fileToolBar = addToolBar("File");
    editToolBar = addToolBar("Edit");


    // 图片显示区域
    imageScene = new QGraphicsScene(this);
    imageView = new QGraphicsView(imageScene);
    setCentralWidget(imageView);

    createActions();
}

void MainWindow::createActions()
{

    // 创建动作,用于打开图片
    openAction = new QAction("&Open", this);
    fileMenu->addAction(openAction);


    // 将动作添加至工具栏
    fileToolBar->addAction(openAction);

    // 连接信号和槽
    connect(openAction, SIGNAL(triggered(bool)), this, SLOT(openImage()));

}

void MainWindow::openImage()
{
    qDebug() << "slot openImage is called.";
    QFileDialog dialog(this);
    dialog.setWindowTitle("Open Image");
    dialog.setFileMode(QFileDialog::ExistingFile);
    dialog.setNameFilter(tr("Images (*.png *.bmp *.jpg)"));
    QStringList filePaths;
    if (dialog.exec()) {
        filePaths = dialog.selectedFiles();
        showImage(filePaths.at(0));
    }
}

void MainWindow::showImage(QString path)
{
    imageScene->clear();
    imageView->resetTransform();
    QPixmap image(path);
    currentImage = imageScene->addPixmap(image);
    imageScene->update();
    imageView->setSceneRect(image.rect());
    currentImagePath = path;
}

编译运行

主窗口程序编写完成后,编译测试

$ qmake -makefile
$ make
$ ./ImageView

主窗口成功运行由于上述代码并非本文主要说明对象,因此不做过多解释。
接下来,使用Qt插件机制,实现图片模糊操作。

插件接口

首先是一些必须安装的依赖项:

Qt插件机制是一种强大的方法,它使Qt应用程序更具可扩展性。使用此机制抽象出一种可以轻松添加新功能的方法。
完成后,只需要注意功能的名称和操作,就可以添加一个新功能。
第一步是找出一个接口,以便在应用程序和插件之间提供一个公共协议,这样就可以加载和调用插件,而不管它们是如何实现的。在C++中,接口是一个带有纯虚成员函数的类。对于插件,处理动作名和操作,因此,创建editor_plugin_interface.h文件,在editor_plugin_interface.h中声明接口:

#ifndef EDITOR_PLUGIN_INTERFACE_H
#define EDITOR_PLUGIN_INTERFACE_H
#include <QObject>
#include <QString>
//引入opencv库方便编辑操作
#include "opencv2/opencv.hpp"
class EditorPluginInterface
{
public:
    virtual ~EditorPluginInterface(){};
    virtual QString name()=0;
    virtual void edit(const cv::Mat &input,cv::Mat &output)=0;
};
#define EDIT_PLUGIN_INTERFACE_IID "com.kdr2.editorplugininterface"
Q_DECLARE_INTERFACE(EditorPluginInterface,EDIT_PLUGIN_INTERFACE_IID);
#endif // EDITOR_PLUGIN_INTERFACE_H

声明一个名为EditorPluginInterface的类,它是接口类。在这个类中,除了一个虚的空析构函数,可以看到两个纯虚成员函数:name和edit函数。name函数返回一个QString,它将是编辑操作的名称。edit函数以Mat的两个引用作为其输入和输出函数,用于编辑操作。每个插件都是这个接口的一个子类,这两个函数的实现将决定操作名和编辑操作。
在类声明之后,定义一个唯一的标识字符串com.kdr2.editorplugininterface作为接口的ID。这个ID在应用程序范围内必须是唯一的,也就是说,如果编写其他接口,则必须为它们使用不同的ID。然后,使用Q_DECLARE_INTERFACE宏将接口的类名与定义的唯一标识符相关联,这样Qt的插件系统就可以在加载插件之前识别该接口的插件。至此,编辑功能的界面已经确定。编写一个插件来实现这个接口。

实现插件

编写Qt插件,应该从头开始一个新的Qt项目。考虑到主要目的是引入Qt库的插件机制,将使用OpenCV库中的一个简单函数进行简单的编辑以保持代码清晰。这里,调用OpenCV库中的模糊函数来模糊图像。
将插件命名BlurPlugin,然后从头开始创建项目,插件目录和主程序目录在同一级:

$ cd ..
$ ls
ImageView
$ mkdir BlurPlugin
$ ls 
ImageView BlurPlugin
$ cd blurPlugin
$ touch blur_plugin.h blur_plugin.cpp
$ qmake -project

首先,将目录更改为ImageView项目的父目录,创建一个名为blurPlugin的新目录,然后进入该目录。创建两个空的源文件,blur_plugin.hblur_plugin.cpp。运行qmake -project,它将创建名为BlurPlugin.pro项目文件,修改项目文件设置:

TEMPLATE = lib
TARGET = ErodePlugin
CONFIG += plugin
INCLUDEPATH += . ../ImageEditor

在项目文件中使用lib而不是app作为其模板设置的值。添加了一行CONFIG+=plugin,告诉qmake这个项目是一个Qt插件项目。最后,将ImageView项目的根目录添加为该项目的include路径中的一项,这样编译器就可以找到接口头文件editor_plugin_interface.h。
在这个插件中,还需要OpenCV来实现编辑功能,因此需要在Qt插件项目的设置中添加OpenCV库的信息,库路径和包含库的路径(根据自己的OpenCV安装目录):

unix:!mac {
  INCLUDEPATH += /home/brainiac/Program/opencv4/include/opencv4
  LIBS += -L/home/brainiac/Program/opencv4/lib -lopencv_core -l opencv_imgproc
}
unix:mac {
  INCLUDEPATH += /path/to/opencv/include/opencv4
  LIBS += -L/path/to/opencv4/lib -lopencv_world
}
WIN32 {
  INCLUDEPATH += C:/path/to/opencv/include/opencv4
  LIBS += -lc:/path/to/opencv4/lib/opencv_world
}

在项目文件的结尾,将头文件和C++源文件添加到项目中:

HEADERS += blur_plugin.h
SOURCES += blur_plugin.cpp

插件的项目文件已经完成,开始编写插件。为一个新的功能编写一个插件只是为了提供EditorPluginInterface接口的实现。因此,在blur_plugin.h中声明该接口的一个子类:

#include <QObject>
#include <QtPlugin>
#include "editor_plugin_interface.h"
class BlurPlugin:public QObject,public EditorPluginInterface
{
    Q_OBJECT;
    Q_PLUGIN_METADATA(IID EDIT_PLUGIN_INTERFACE_IID);
    Q_INTERFACES(EditorPluginInterface);
public:
    QString name();
    void edit(const cv::Mat &input,cv::Mat &output);
};

在包含必要的头文件之后,声明了一个名为BlurPlugin的类,它继承了QObject和EditorPluginInterface。后者是在editor_plugin_interface.h中定义的接口。将插件实现作为QOBject的子类,是因为Qt元对象系统和插件机制的要求。在类的主体中,使用Qt库定义的一些宏添加更多信息:

    Q_OBJECT;
    Q_PLUGIN_METADATA(IID EDIT_PLUGIN_INTERFACE_IID);
    Q_INTERFACES(EditorPluginInterface);

Q_OBJECT宏与Qt元对象系统有关。
Q_PLUGIN_METADATA(IID EDIT_PLUGIN_INTERFACE_IID);声明此插件的元数据,在这里声明为在editor_plugin_interface.h中定义的插件接口的唯一标识符作为其IID元数据。
使用Q_INTERFACES(EditorPluginInterface);告诉Qt这个类正试图实现的是EditorPluginInterface接口。由于前面的信息的清楚声明,Qt插件系统对这个项目了如指掌:
这是一个Qt插件项目,所以项目的目标是生成库文件。
这个插件是EditorPluginInterface的一个实例,它的IID是EDIT_PLUGIN_INTERFACE_IID,因此Qt应用程序可以加载这个插件。
在接口中声明两个重要成员函数:

public:
    QString name();
    void edit(const cv::Mat &input,cv::Mat &output);

然后,在blur_plugin.cpp文件中实现这两个函数。对于name函数,只返回一个QString Blur,作为插件的名称(也是操作的名称):

#include "blur_plugin.h"
QString BlurPlugin::name()
{
    return "Blur";
}

对于操作函数:

void BlurPlugin::edit(const cv::Mat &input, cv::Mat &output)
{
    blur(input,output,cv::Mat());
}

只需调用由OpenCV库提供的模糊函数。
编译插件项目。与编译普通Qt应用程序的方法相同。

$ qmake -makefile
$ make
$ ls -l *.so*
-rwxr-xr-x 1 brainiac brainiac 74480 Jul 21 10:10 libBlurPlugin.so

编译过程完成后,使用ls -l .so检查输出文件,输出共享对象文件。这些是将加载到应用程序中的插件文件。
在检查输出文件时,可能会发现有许多扩展名类似1.0.0的文件。这些类型的字符串是库文件的版本号。这些文件大多是一个真正的库文件的别名。
如果使用平台并非Linux,那么输出文件可能也会有所不同:在Windows上,这些文件的名称类似于BlurPlugin.dll,而在macOS上,这些文件被命名为libBlurPlugin.dylib.

将插件加载到应用程序中

将Blur插件加载到应用程序中,就可以使用模糊操作。之后,将看到一个名为模糊的新操作,可以在编辑菜单和编辑工具栏上找到。
加载插件首先,修改ImageView项目的项目文件ImageView.pro,并将包含插件接口的头文件添加到HEADERS列表中,同时声明OpenCV库位置:

HEADERS += mainwindow.h editor_plugin_interface.h

unix:!mac {
  INCLUDEPATH += /home/brainiac/Program/opencv4/include/opencv4
  LIBS += -L/home/brainiac/Program/opencv4/lib -lopencv_core -l opencv_imgproc
}
unix:mac {
  INCLUDEPATH += /path/to/opencv/include/opencv4
  LIBS += -L/path/to/opencv4/lib -lopencv_world
}
WIN32 {
  INCLUDEPATH += C:/path/to/opencv/include/opencv4
  LIBS += -lc:/path/to/opencv4/lib/opencv_world
}

然后,在mainwindow.h源文件中引入插件接口头文件。还将使用一个名为QMap的数据结构来保存加载的所有插件的列表,因此还包括QMap的头文件:

#include <QMap>
#include "editor_plugin_interface.h"

然后,在MainWindow类的声明体中,声明了两个成员函数:
void loadPlugins():用于加载某个目录中出现的所有插件。
void pluginPerform():这是一个公共槽,将连接到加载插件创建的所有操作。在这个slot中,应该区分哪个动作被触发了,哪个动作导致了这个slot被调用,然后找到与该动作相关的插件并执行它的编辑操作。
添加这两个成员函数后,添加一个QMap类型的成员字段来注册所有加载的插件:

    QMap<QString, EditorPluginInterface*> editPlugins;

这个映射的键是插件的名称,值将是指向已加载插件实例的指针。
实现loadPlugins函数来加载插件。首先,在mainwindow.cpp:

#include <QPluginLoader>

然后,实现loadPlugins成员函数:

void MainWindow::loadPlugins()
{
    QDir pluginsDir(QApplication::instance()->applicationDirPath()+"/plugins");
    QStringList nameFilters;
    nameFilters<<"*.so"<<"*.dylib"<<"*.dll";
    QFileInfoList plugins = pluginsDir.entryInfoList(
                nameFilters,QDir::NoDotAndDotDot|QDir::Files,QDir::Name);
    foreach(QFileInfo plugin,plugins){
        QPluginLoader pluginLoader(plugin.absoluteFilePath(),this);
        EditorPluginInterface *plugin_ptr=dynamic_cast<EditorPluginInterface*>(pluginLoader.instance());
        if (plugin_ptr){
            QAction *action = new QAction(plugin_ptr->name());
            editMenu->addAction(action);
            editToolBar->addAction(action);
            editPlugins[plugin_ptr->name()]=plugin_ptr;
            connect(action,SIGNAL(triggered(bool)),this,SLOT(pluginPerform()));
        }else{
            qDebug()<<"bad plugin:"<<plugin.absoluteFilePath();
        }
    }
}

假设在可执行文件所在的目录中有一个名为plugins的子目录。调用QApplication::instance()->applicationDirPath()即可获取包含可执行文件的目录,然后在其末尾附加/plugins字符串以生成plugins目录。插件是库文件,根据使用的操作系统,它们的名称以.so、.dylib或.dll结尾。然后,在plugins目录中列出具有这些扩展名的所有文件。
在将所有潜在的插件文件列为QFileInfoList之后,遍历该列表,尝试用foreach加载每个插件。foreach是由Qt定义的宏,实现for循环。在循环中,每个文件都是QFileInfo的一个实例。通过调用它的abstractFilePath方法获得它的绝对路径,然后在该路径上构造一个QPluginLoader的实例。
然后,首先调用QPluginLoader实例上的instance方法。如果目标插件已经加载,则返回指向QObject的指针,否则返回0。然后,将返回指针转换为指向插件接口类型的指针,即EditorPluginInterface*。如果这个指针不是零,那就是一个插件的实例!然后,创建一个QAction,它的名称是加载的插件的名称,即plugin_ptr->name()的结果。
创建侵蚀操作后,通过使用该操作调用它们的addAction方法来将其添加到编辑菜单和编辑工具栏中。然后,在editPlugins映射中注册加载的插件:
editPlugins[plugin_ptr->name()]=plutin_ptr;
稍后,将使用此映射在插件创建的所有操作的公共槽中按其名称查找插件。
最后,将连接一个槽和动作:
connect(action,SIGNAL(triggered(bool)),this,SLOT(pluginPerform()));
这一行代码在循环中,将所有动作的触发信号连接到同一个槽;这可以吗?是的,有一种方法来区分在槽中触发了哪个操作,然后可以根据这个方法执行操作。在pluginPerform slot的实现中,检查是否有一个图片打开:

void MainWindow::pluginPerform()
{
    if (currentImage==nullptr) {
        QMessageBox::information(this,"Information","No Image!")
    }
}

然后,找到它刚刚触发的动作,以便它发送信号并通过调用Qt库提供的sender()函数来调用槽。sender()函数的作用是:返回指向QObject实例的指针。在这里,知道只将QAction的实例连接到此槽,因此可以使用qobject_cast将返回的指针安全地强制转换为QAction的指针。现在,知道触发了哪个动作。然后,得到动作的文本。在应用程序中,动作的文本是创建该操作的插件的名称。通过使用此文本,可以从注册映射中找到某个插件:

    QAction *active_action=qobject_cast<QAction*>(sender());
    EditorPluginInterface *plugin_ptr = editPlugins[active_action->text()];
    if(!plugin_ptr){
        QMessageBox::information(this,"Information","No plugin is fount.");
        return;
    }

在得到插件指针之后,检查它是否存在。如果没有,向用户显示一个消息框,然后从slot函数返回。
现在有一个插件,用户已经触发了它的动作,所以现在可以编写编辑操作。首先,将打开的图像作为QPixmap,然后依次将其转换为QImage和Mat。一旦它成为Mat的一个实例,就可以应用插件的编辑功能,即plugin_ptr->edit(mat mat);。在编辑操作之后,将编辑的Mat分别转换回QImage和QPixmap,然后在图形场景中显示QPixmap:

    QPixmap pixmap = currentImage->pixmap();
    QImage image = pixmap.toImage();
    image = image.convertToFormat(QImage::Format_RGB888);
    cv::Mat mat=cv::Mat(
                image.height(),
                image.width(),
                CV_8UC3,
                image.bits(),
                image.bytesPerLine());
    plugin_ptr->edit(mat,mat);
    QImage image_edited(
                mat.data,
                mat.cols,
                mat.rows,
                mat.step,
                QImage::Format_RGB888);
    pixmap = QPixmap::fromImage(image_edited);
    imageScene->clear();
    imageView->resetTransform();
    currentImage=imageScene->addPixmap(pixmap);
    imageScene->update();
    imageView->setSceneRect(pixmap.rect());

最后在MainWindow类的构造函数MainWindow::MainWindow(QWidget *parent)的末尾添加以下行来调用loadPlugins函数:

    loadPlugins();

将模糊插件文件复制到插件目录中,就从可执行文件所在的目录中的plugins子目录加载并设置了插件。
如果是macOS,在编译项目之后,会发现一个名为ImageEditor.app的可执行文件。因为,在macOS上,每个应用程序都是一个扩展名为.app的目录。真正的可执行文件位于ImageEditor.app/Contents/MacOS/imageditor,所以,在macOS上,插件目录是ImageEditor.app/Contents/MacOS/plugins. 应该创建该目录并将插件文件复制到那里。
编译应用程序并对其进行测试。
插件测试

The end

Enjoy coding.

posted @ 2020-07-21 11:07  盼小辉丶  阅读(518)  评论(0编辑  收藏  举报