版本记录表
文章目录
- 概述
- Qt插件简介
- Qt插件开发的流程
- Qt插件调用的流程
- 插件的开发
- 设计接口文件
- 配置工程文件
- 实现接口的类
- 插件的调用
- 特别说明
- 绑定信号与槽
- 发射自定类型变量
- 注册回调函数
概述
Qt插件简介
插件是一种遵循一定规范的应用程序接口编写出来的程序,定位于开发实现应用软件平台不具备的功能的程序。插件与宿主程序之间通过接口联系,就像硬件插卡一样,可以被随时删除,插入和修改,所以结构很灵活,容易修改,方便软件的升级和维护。Qt提供了两种API用于创建插件:一种是高阶API,用于扩展Qt本身的功能,如自定义数据库驱动,图像格式,文本编码,自定义样式等;一种是低阶API,用于扩展Qt应用程序。本文主要是通过低阶API来创建Qt插件,并通过静态、动态两种方式来调用插件。
Qt插件开发的流程
- 定义一个接口集(只有纯虚函数的类)。
- 用宏
Q_DECLARE_INTERFACE()
将该接口告诉Qt元对象系统 - 声明插件类,插件类继承自
QObject
和插件实现的接口。 - 用宏
Q_INTERFACES()
将插件接口告诉Qt元对象系统(在头文件中)。 - 用宏
Q_EXPORT_PLUGIN2()
导出插件类。 - 用适当的.pro文件构建插件。
Qt插件调用的流程
- 包含接口头文件(只有纯虚函数的类)。
- 应用程序中用
QPluginLoader
来加载插件。 - 用宏
qobject_cast()
来判断一个插件是否实现了接口。
参考文章1 参考文章2
插件的开发
Qt插件开发主要包括 声明接口文件、建立工程文件、声明和定义实现接口的类等步骤。
设计接口文件
接口文件在插件开发、插件调用中都需要引用。接口的方法需要定义成纯虚函数。值得注意的是,Qt插件也是支持信号与槽的机制,在接口文件中信号也同样被声明为纯虚函数。注意源码中使用了Q_DECL_OVERRIDE
宏确保重写了虚函数。
#ifndef ISERIALPORT_H
#define ISERIALPORT_H
#include <QString>
#include <QtPlugin>
#include <QObject>
#include <QList>
#include <functional>
/*
宏定义 接口IID,用来唯一标记该接口类。实际开发中,IID的名称为了避免重复,推荐采用本例所示的方式命名
*/
#define QTPLUGIN_ISERIALPORT_IID "ewhales.plugin.interface.serialport"using namespace std;
using namespace placeholders;/*
该处省略与插件无关的业务代码
*//*
std::function对象用于实现函数回调,下面会详细说明。
*/
typedef std::function<void(const unsigned char *,int count)>FUNdataReceive;
typedef std::function<void(const QList<QString> &)>FUNportChange;/*
接口需要定义成纯虚函数
*/
class ISerialPort
{
public:virtual void GetPortList(QStringList &portList)=0;virtual void GetSerialPortConfig(SerialPort_Typedef &serialPort_Typedef)=0;virtual void SetSerialPort(SerialPort_Typedef *serialPort_Typedef)=0;virtual int OpenSerialPort()=0;virtual int CloseSerialPort()=0;virtual void StartListening()=0;virtual void StopListening()=0;virtual int SendData(QByteArray data)=0;virtual int SendData(QString string)=0;virtual void SetPortChangedHandler(FUNportChange fPortChange)=0;virtual void SetDataReceivedHandler(FUNdataReceive fDataRecv)=0;virtual QWidget *GetPanel()=0;//Override as signals.virtual void Signal_1(QString data1)=0;virtual void Signal_2(QString data2)=0;
};/*
为了能够在运行时查询插件是否实现给定的接口,我们必须使用宏Q_DECLARE_INTERFACE(),该宏的第一参数为接口类的名称,第二个参数是一个字符串,用于唯一标记该接口类。
*/
Q_DECLARE_INTERFACE (ISerialPort, QTPLUGIN_ISERIALPORT_IID)#endif // EW_SERIALPORT_INTERFACE
配置工程文件
#-------------------------------------------------
#
# Project created by QtCreator 2018-05-31T15:19:56
#TEMPLATE = lib
#-------------------------------------------------QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgetsQMAKE_CXXFLAGS += -std=c++11
TARGET = EWhalesSerialPort# TEMPLATE = lib 生成插件
# TEMPLATE = app 生成应用程序
TEMPLATE = lib
CONFIG += pluginSOURCES += main.cpp\serialport.core.cpp \serialport.pannel.cpp \serialport.framework.cpp \serialport.thread.cppHEADERS += \serialport.core.h \serialport.interface.h \serialport.pannel.h \serialport.framework.h \serialport.thread.hFORMS += widget.uiunix {#target.path += /root/target.path =/usr/libINSTALLS += target
}#target.path += /root/
#INSTALLS += targetRESOURCES += \resource.qrc
实现接口的类
头文件
#ifndef SERIALPORTINTERACTIVE_H
#define SERIALPORTINTERACTIVE_H#include <QObject>
#include <functional>
#include <QFileSystemWatcher>
#include <QMetaEnum>
#include "serialport.pannel.h"
#include "serialport.interface.h"
#include "serialport.core.h"
#include "serialport.thread.h"
#include "serialport.pannel.h"/*
实现插件的类必须继承自插件接口类
*/
class SerialPortInteractive : public QObject, public ISerialPort
{Q_OBJECT/*使用Q_INTERFACES声明:类支持ISerialPort*/Q_INTERFACES(ISerialPort)/*
Qt4与Qt5的插件开发方式略有差异,此处采用条件编译可以实现版本兼容
*/
#if QT_VERSION >= 0x050000Q_PLUGIN_METADATA(IID QTPLUGIN_ISERIALPORT_IID)
#endifpublic:/*此处省略了与插件开发无关的代码*/SerialPortInteractive();~SerialPortInteractive();void GetPortList(QStringList &portList) Q_DECL_OVERRIDE;void GetSerialPortConfig(SerialPort_Typedef &serialPort_Typedef) Q_DECL_OVERRIDE;void SetSerialPort(SerialPort_Typedef *serialPort_Typedef) Q_DECL_OVERRIDE;int OpenSerialPort() Q_DECL_OVERRIDE;int CloseSerialPort() Q_DECL_OVERRIDE;void StartListening() Q_DECL_OVERRIDE;void StopListening() Q_DECL_OVERRIDE; int SendData(QByteArray data) Q_DECL_OVERRIDE;int SendData(QString string) Q_DECL_OVERRIDE;void SetPortChangedHandler(FUNportChange fPortChange) Q_DECL_OVERRIDE;void SetDataReceivedHandler(FUNdataReceive fDataRecv) Q_DECL_OVERRIDE;QWidget* GetPanel() Q_DECL_OVERRIDE;void ShowPanel(QWidget *parent) Q_DECL_OVERRIDE;
private:QList<QString> spList;QFileSystemWatcher watcher;SerialPort_Typedef *sp_Typedef;serialportCore *port;Pannel *panel;BackgroundThread *thread;FUNportChange _fportchange;FUNdataReceive _fdatarecv;
private slots:void _detectPortChange();void _receiveData(const unsigned char *data, int count);signals:void Signal_1(QString data1) Q_DECL_OVERRIDE;void Signal_2(QString data2) Q_DECL_OVERRIDE;
};#endif // SERIALPORT_FRAMEWORK_H
源文件
#include "serialport.framework.h"
#include <QList>
#include <QDebug>SerialPortInteractive::SerialPortInteractive()
{/*类构造函数此处省略了与插件开发无关的代码*/
}SerialPortInteractive::~SerialPortInteractive()
{/*类析构函数此处省略了与插件开发无关的代码*/
}
void SerialPortInteractive::ShowPanel(QWidget *parent)
{panel->setParent(parent);panel->show();
}QWidget *SerialPortInteractive::GetPanel()
{return panel;
}void SerialPortInteractive::GetPortList(QStringList &portList)
{QString str=port->GetPortList().trimmed();portList=str.split("\n");
}void SerialPortInteractive::GetSerialPortConfig(SerialPort_Typedef &serialPort_Typedef)
{serialPort_Typedef=*sp_Typedef;
}void SerialPortInteractive::SetSerialPort(SerialPort_Typedef *serialPort_Typedef)
{sp_Typedef=serialPort_Typedef;
}int SerialPortInteractive::OpenSerialPort()
{int i =port->OpenPort(sp_Typedef);return i;
}int SerialPortInteractive::CloseSerialPort()
{int i=port->ClosePort();return i;
}void SerialPortInteractive::StartListening()
{thread->start();
}void SerialPortInteractive::StopListening()
{thread->stop();
}int SerialPortInteractive::SendData(QByteArray data)
{int i=port->SendData(data);return i;
}int SerialPortInteractive::SendData(QString string)
{int i=port->SendData(string);return i;
}void SerialPortInteractive::SetPortChangedHandler(FUNportChange fPortChange)
{_fportchange=fPortChange;
}void SerialPortInteractive::SetDataReceivedHandler(FUNdataReceive fDataRecv)
{_fdatarecv=fDataRecv;
}void SerialPortInteractive::_detectPortChange()
{QStringList portList;GetPortList(portList);if(_fportchange!=NULL)_fportchange(portList);emit Signal_1("data1");
}void SerialPortInteractive::_receiveData(const unsigned char *data,int count)
{if(_fdatarecv!=NULL)_fdatarecv(data,count);emit Signal_2("data2");
}/*
Qt4与Qt5的插件开发方式略有差异,此处采用条件编译可以实现版本兼容。
导出Qt插件,第一参数为插件的IID,第二个参数为实现接口的类。
*/
#if QT_VERSION < 0x050000Q_EXPORT_PLUGIN2(QTPLUGIN_ISERIALPORT_IID,SerialPortInteractive)
#endif
插件的调用
示例为动态调用插件的方法。对于静态调用方法不推荐使用。
bool Widget::loadSerialPortPlugin()
{QObject *obj=NULL;QString serialPortPluginPath("/usr/lib/libEWhalesSerialPort.so");QPluginLoader pluginLoader(serialPortPluginPath);obj=pluginLoader.instance();if(obj!=NULL){serialPort=qobject_cast<ISerialPort *>(obj);if(serialPort){qDebug()<<serialPortPluginPath<<"is loaded...";return true;}}else{qDebug()<<serialPortPluginPath<<"is loaded failed: "<<pluginLoader.errorString();return false;}
}
特别说明
绑定信号与槽
插件中如果有信号,可以在加载插件的时候绑定对应的槽函数。但是由于插件的Class类型并不被Qt的Connect直接支持,因此需要采用以下的写法才能避免报错(使用<QObject *>
类型):
QObject *obj = NULL;QString serialPortPluginPath("/usr/lib/libEWhalesSerialPort.so");QPluginLoader pluginLoader(modbusRTUMasterPluginPath);obj = pluginLoader.instance();/* 省略无关代码 */if(obj != NULL){modbusMaster = qobject_cast<IModbusRTUMaster *>(obj);if(modbusMaster){connect(obj, SIGNAL(Get_IRData_Failed(QString)), this, SLOT(userslots_modbusMaster_GetIRDataFailed(QString)));connect(obj, SIGNAL(Get_DRData_Failed(QString)), this, SLOT(userslots_modbusMaster_GetDRDataFailed(QString)));/* 省略无关代码 */return true;}}/* 省略无关代码 */
发射自定类型变量
在插件的信号中发射自定类型的变量与在支持Qt特性的class中定义、发射自定义类型变量本质上是一样的。但是由于插件的接口文件定义的是一个普通的class类型(无构造/析构函数、无Q_OBJECT
宏),因此需要在实现插件接口纯虚函数定义的class中(一般在构造函数中)完成对变量类型的注册。
一个完整的示例如下:
- 声明类型。完全定义变量类型后,在”global scope”中调用
Q_DECLARE_METATYPE
完成Qt元类型的声明。
/*** IModbusRTUMaster的接口文件,接口需要定义成纯虚函数*/
class IModbusRTUMaster: public IPluginBase
{
public:// 定义新的枚举变量:MBStatetypedef enum{state_disconnected = 0,state_connected,state_running,state_stop,state_notExist,state_error,} MBState;// 插件的接口virtual PluginInfo GetPluginInfo() = 0;virtual QWidget *GetPanel(QWidget *parent = nullptr) = 0;// Override as signals.// State of MB master changedvirtual void MBStateChanged(const MBState &state) = 0;/* 省略无关内容 */
}// 声明Qt的元类型
Q_DECLARE_METATYPE(IModbusRTUMaster::MBState);/*** 为了能够在运行时查询插件是否实现给定的接口,* 我们必须使用宏Q_DECLARE_INTERFACE(),* 该宏的第一参数为接口类的名称,第二个参数是一个字符串,用于唯一标记该接口类。*/
Q_DECLARE_INTERFACE (IModbusRTUMaster, QTPLUGIN_IMODBUSRTUMASTER_IID)
- 注册类型。在实现接口类的构造函数中调用
qRegisterMetaType
完成类型注册。
// 实现接口类的构造函数(源文件)
ModbusRTUMaster::ModbusRTUMaster()
{// 注册Qt元类型qRegisterMetaType<MBState>("MBState");/* 省略无关内容 */
}// 实现接口类的声明(头文件)
class ModbusRTUMaster : public QThread, public IModbusRTUMaster
{Q_OBJECTpublic:/* 省略无关内容 */signals:// 接口实现为信号void MBStateChanged(const MBState &state) override;/* 省略无关内容 */
}
- 发射与连接信号。
// 在合适的地方发射信号
void ModbusRTUMaster::StartMaster()
{/* 省略无关内容 */isModbusRun = state_running;emit MBStateChanged(isModbusRun);/* 省略无关内容 */
}// 在合适的地方定义槽函数
void Panel::on_MBStateChanged(const IModbusRTUMaster::MBState &state)
{switch (state){case IModbusRTUMaster::state_disconnected:case IModbusRTUMaster::state_stop:case IModbusRTUMaster::state_notExist:case IModbusRTUMaster::state_error:on_MBStateChanged_disconnected();break;case IModbusRTUMaster::state_running:case IModbusRTUMaster::state_connected:on_MBStateChanged_Connected();break;default:break;}
}// 在合适的地方连接信号与槽函数
Panel::Panel(ModbusRTUMaster *modbusMaster, QWidget *parent) :QWidget(parent),ui(new Ui::Panel),_modbusMaster(modbusMaster)
{/* 省略无关内容 */connect(_modbusMaster,SIGNAL(SendRawData(const QByteArray&)), this, SLOT(on_MBSendRawData(const QByteArray&)));connect(_modbusMaster,SIGNAL(RecvRawData(const QByteArray&)), this, SLOT(on_MBRecvRawData(const QByteArray&)));// 绑定信号与槽connect(_modbusMaster,SIGNAL(MBStateChanged(const IModbusRTUMaster::MBState &)), this, SLOT(on_MBStateChanged(const IModbusRTUMaster::MBState &)));/* 省略无关内容 */
}
注册回调函数
普通的C++成员函数都隐含了一个“this”指针参数,当在类的非静态成员函数中访问类的非静态成员时,C++编译器通过传递一个指向对象本身的指针给其成员函数,从而能够访问类的数据成员。也就是说,即使你没有写上this指针,编译器在编译的时候自动加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。正是由于this指针的作用,使得将一个CALLBACK型的成员函数作为回调函数时就会因为隐含的this指针使得函数参数个数不匹配,从而导致回调函数匹配失败。所以为了实现回调,类中的成员函数必须舍弃掉隐藏的this指针参数。参考文章
下面示例展示了一种如何正确设置回调函数的方法:
#include "serialport.framework.h"
#include "serialport.core.h"
#include "serialport.interface.h"
#include "serialport.pannel.h"
#include <QApplication>
#include <QDebug>
#include <functional>
#include <stdio.h>using namespace std;
using namespace std::placeholders;SerialPortInteractive *frame1;void Widget::test_DataReceivedHandler(const unsigned char *data,int count)
{QString qString=QByteArray((const char*)data,count);qDebug()<<"received data:"<<qString;
}void Widget::test_PortChangedHandler(QList<QString> list)
{qDebug()<<"test_PortChangedHandler is called, port list is:";QListIterator<QString> hashIterator(list);while (hashIterator.hasNext()){qDebug()<<hashIterator.next();}
}void Widget::serialPortInit()
{/*串口参数设置*/SerialPort_Typedef serialPortSetting;serialPortSetting.baudRate=SerialPort_BR_19200;serialPortSetting.dataBit=SerialPort_DB_8;serialPortSetting.parity=SerialPort_CB_None;serialPortSetting.stopBit=SerialPort_SB_1;serialPortSetting.name = "/dev/ttymxc1";/*serialPort是插件加载后的实例*/serialPort->SetSerialPort(&serialPortSetting);/*std::bind函数将可调用对象和可调用对象的参数进行绑定,返回新的可调用对象(std::function类型,参数列表可能改变),返回的新的std::function可调用对象的参数列表根据bind函数实参中std::placeholders::_x从小到大对应的参数确定。*/fDataReceive=std::bind(&Widget::test_DataReceivedHandler,this,_1,_2);fPortChange=std::bind(&Widget::test_PortChangedHandler,this,_1);/*设置回调函数,用来处理串口插拔与串口数据接收。*/serialPort->SetDataReceivedHandler(fDataReceive);serialPort->SetPortChangedHandler(fPortChange);serialPort->OpenSerialPort();serialPort->StartListening();
}