Qt插件机制介绍及实现
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.h
和blur_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.