Qt 5 在插件中如何使用信号槽机制

对于一个大型系统,如何保证可扩展性和可维护性是十分重要的。Qt为我们提供了一套插件系统,能够较好的解决扩展性的问题。但是在将插件系统与信号槽机制相结合的过程中,也遇到了一些问题。经过一番探索之后总算成功了,这里写一个小小的教程,供有需要的同学查阅。


一、概述

Qt的插件系统分为High-Level API接口和Low-Level API接口。

所谓High-Level API 是指通过继承Qt为我们提供的特定的插件基类,然后实现一些虚函数、添加需要的宏即可。该种插件开发方式主要是用来扩展Qt库本身的功能,比如自定义数据库驱动、图片格式、文本编码、自定义样式等。而我们为自己的应用程序编写插件来扩展其功能时主要使用第二种方式,即Low-Level API 的方式,该方式不仅能扩展我们自己的应用程序,同样也能像High-Level API 那样用来扩展Qt本身的功能。

在本文中,我们使用Low-Level API接口进行插件的编写。有关High-Level相关内容,参阅Qt官方说明文档

除此之外,插件还有静态和动态之分。静态插件顾名思义,就是编译出一个lib作为插件,在程序中静态编译。动态插件则是一个dll(windows下),方便扩展和维护。因此我们使用动态插件作为说明。

总的来说,插件扩展并使用信号槽机制需要以下步骤:

  1. 定义一个插件接口类,作为主程序与插件之间通信的桥梁,定义普通成员函数(纯虚函数)、槽函数(纯虚函数)、信号(不能为虚)。在接口类中使用 Q_DECLARE_INTERFACE() 通知 Qt 元对象系统这里有这么个接口。这个接口需要继承QObject;
  2. 继承接口类,实现其中的虚函数。并:a) 使用 Q_PLUGIN_METADATA()宏导出插件;b) 使用Q_INTERFACES()通知元对象系统此插件使用了哪些接口类;
  3. 在主程序中利用QPluginLoader加载插件,使用QPluginLoader::instance()方法实例化插件,使用qobject_cast强制转换为接口类指针,connect有关信号和槽;
  4. 开始愉快的使用;

注意:主程序、接口、插件应分别作为3个子工程。主程序和插件需要调用接口生成的lib文件,否则会出现未定义的外部符号错误。https://*.com/questions/50516359/declaring-signals-in-interface-class-using-qt-plugin-system-with-new-signal-slot


 

二、实现

2.1 工程(Qt creator)

2.1.1 新建项目

新建一个子项目目录,这个项目将包含主工程、接口和插件三个子工程。这里我们起名为PluginTest

Qt 5 在插件中如何使用信号槽机制
创建子目录项目

 

Qt 5 在插件中如何使用信号槽机制
汇总

2.1.2 新建主程序工程

主程序。起名叫App。右键PluginTest,选新建子项目,选Qt Widgets Application

 

Qt 5 在插件中如何使用信号槽机制
新建一个Qt Widgets Application

 

 

Qt 5 在插件中如何使用信号槽机制
选什么基类看自己喜好了,我这里选了QWidget

 2.1.3 新建接口工程

起名叫PluginInterface。右键PluginTest,选新建子项目, 在“项目”框中选library,右边只有一个项目可选

Qt 5 在插件中如何使用信号槽机制
选c++库

选共享库。

Qt 5 在插件中如何使用信号槽机制
类型是共享库

一路下一步,自动生成四个文件。

2.1.4 新建插件工程

起名叫myPlugin。右键PluginTest,选新建子项目,选Empty qmake Project

Qt 5 在插件中如何使用信号槽机制
选空qmake项目

会提示个这,不用管它,因为这个工程里没有任何文件。 

Qt 5 在插件中如何使用信号槽机制

到此为止,插件的框架就搭好了。总结一下,我们用了三个子工程:

  • 主程序:Qt Widgets Application,用来生成一个exe调用插件
  • 接口:c++共享库,用来生成一个lib向编译器提供接口信息
  • 插件:空qmake工程,后面手动对其进行填写,生成dll作为主程序的插件

另外,c++库中的qt plugin选项是生成High-Level API的,不用在这里。

Qt 5 在插件中如何使用信号槽机制
不选这个

2.2 接口类

工程是PluginInterface。plugininterface_global.h是自动生成的,这里我们无需修改它。

plugininterface.h:

//plugininterface.h
#ifndef PLUGININTERFACE_H
#define PLUGININTERFACE_H

#include "plugininterface_global.h"
#include <QObject>

//接口类
class PLUGININTERFACESHARED_EXPORT PluginInterface : public QObject
{
	Q_OBJECT
	
public:
	virtual ~PluginInterface() {} //虚析构函数,c++多态

public slots:
	virtual void SayHello(QWidget *parent) = 0;//虚槽函数,而且是纯的

signals:
	void doSomething();//信号不能为虚
};

#define InterfaceIID "interface"
Q_DECLARE_INTERFACE(PluginInterface, InterfaceIID)

#endif // PLUGININTERFACE_H

因为不需要对其进行实现,所以plugininterface.cpp就可以删掉了

注意到我们将槽函数声明成为一个纯虚函数,使得接口类成为了一个抽象类。增强了代码的健壮性。

又注意到信号是一个普通成员函数的声明,信号不能是虚的,否则连接不过。(moc相关,就不展开讲了)

我们在pro文件中指定生成的目录,方便其他工程对其进行引用:PluginInterface.pro:

#PluginInterface.pro
QT       -= gui

TARGET = PluginInterface
TEMPLATE = lib

DEFINES += PLUGININTERFACE_LIBRARY

# The following define makes your compiler emit warnings if you use
# any feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

SOURCES +=

HEADERS += \
        plugininterface.h \
        plugininterface_global.h 

unix {
    target.path = /usr/lib
    INSTALLS += target
}

###修改生成目录,到pro文件目录/lib中
DESTDIR = $$PWD/lib

2.3 插件类

首先向其添加一个类:右键myPlugin,添加新文件,c++class,起名为Plugin,继承PluginInterface

Qt 5 在插件中如何使用信号槽机制

然后修改pro文件:

#myPlugin.pro

HEADERS += \
    plugin.h

SOURCES += \
    plugin.cpp

QT += widgets

TARGET = Plugin#类型是plugin
TEMPLATE = lib#模板是lib
INCLUDEPATH += $$PWD/../PluginInterface#指定了包含目录
DEPENDPATH += $$PWD/../PluginInterface#指定了附加依赖项目录

LIBS += -L$$PWD/../PluginInterface/lib/ -lPluginInterface#要添加接口生成的库给编译器
DESTDIR = ../app/debug#生成的dll直接扔到app的目录下

plugin.h:

//plugin.h
#ifndef PLUGIN_H
#define PLUGIN_H

#include "plugininterface.h"
#include <QWidget>

class Plugin : public PluginInterface
{
	Q_OBJECT
	Q_PLUGIN_METADATA(IID "my.test.plugin.interface")//导出plugin
	Q_INTERFACES(PluginInterface)
public:
	void SayHello(QWidget *parent) Q_DECL_OVERRIDE;//声明是重写虚函数
};

#endif // PLUGIN_H

注意到需要一个Q_OBJECT宏。

又注意到Q_PLUGIN_METADATA用来导出插件,Q_INTERFACES声明使用的接口。

还注意到虚函数声明后边加了一个Q_DECL_OVERRIDE,用来向编译器说明这是个重写的虚函数。

plugin.cpp:

//plugin.cpp
#include "plugin.h"
#include "QMessageBox"

void Plugin::SayHello(QWidget *parent)
{
	emit doSomething();
	QMessageBox::information(parent, "123", "12345");
}

注意到,在plugin中直接emit了doSomething信号。

2.4 主程序

修改pro 文件:

#App.pro
QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = App
TEMPLATE = app

# The following define makes your compiler emit warnings if you use
# any feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0


SOURCES += \
        main.cpp \
        widget.cpp

HEADERS += \
        widget.h

FORMS += \
        widget.ui

LIBS += -L$$PWD/../PluginInterface/lib/ -lPluginInterface

INCLUDEPATH += $$PWD/../PluginInterface
DEPENDPATH += $$PWD/../PluginInterface

跟插件类类似,添加了接口类的lib、包含目录和附加依赖项。

在ui中添加两个按钮:

Qt 5 在插件中如何使用信号槽机制

 然后是widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QPluginLoader>
#include "plugininterface.h"

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
	Q_OBJECT

public:
	explicit Widget(QWidget *parent = 0);
	~Widget();

private slots:
	void on_pushButton_clicked();
	void on_pushButton_2_clicked();
	void onDoSomething();

signals:
	void saySomething(QWidget *);

private:
	Ui::Widget *ui;
	PluginInterface *interface = nullptr;
	QPluginLoader pluginLoader;
};

#endif // WIDGET_H

widget.cpp:

#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>

Widget::Widget(QWidget *parent) :
	QWidget(parent),
	ui(new Ui::Widget)
{
	ui->setupUi(this);
}

Widget::~Widget()
{
	delete ui;
}

void Widget::on_pushButton_clicked()
{
	if(!interface)
	{
		pluginLoader.setFileName("Plugin.dll");
		QObject *plugin = pluginLoader.instance();
		if(plugin)
		{
			interface = qobject_cast<PluginInterface*>(plugin);//使用多态,将基类指针强制转换成派生类指针,检查能否转换
			if(interface)
			{
				connect(this, &Widget::saySomething, interface, &PluginInterface::SayHello);
				connect(interface, &PluginInterface::doSomething, this, &Widget::onDoSomething);
			}
			else
			{
				QMessageBox::critical(this, "err", "this is not a proper plugin");
				return;
			}
		}
		else
		{
			QMessageBox::critical(this, "err", "could not find any plugin");
			return;
		}
	}
	emit saySomething(this);
}

void Widget::on_pushButton_2_clicked()
{
	if(interface)
	{
		if (pluginLoader.unload())
		{
			interface = nullptr;
			QMessageBox::information(this,"info","unloaded");
		}
		else
			QMessageBox::critical(this,"err", "unload failed");
	}
}

void Widget::onDoSomething()
{
	QMessageBox::information(this,"info","you want to do something");
}