Qt Plugin插件开发指南(1)- 一般开发流程

Qt Plugin插件开发指南(1)- 一般开发流程

DateAuthorVersionNote
2020.02.17Dog TaoV1.0整理后发表。
2020.12.10Dog TaoV1.11. 增加了插件中信号的实现。 2. 修改了部分源码。
2021.04.06Dog TaoV1.21. 增加了插件绑定信号与槽的说明。
2022.04.08Dog TaoV1.31. 增加了插件信号发射自定类型变量的说明。

概述

Qt插件简介

插件是一种遵循一定规范的应用程序接口编写出来的程序,定位于开发实现应用软件平台不具备的功能的程序。插件与宿主程序之间通过接口联系,就像硬件插卡一样,可以被随时删除,插入和修改,所以结构很灵活,容易修改,方便软件的升级和维护。Qt提供了两种API用于创建插件:一种是高阶API,用于扩展Qt本身的功能,如自定义数据库驱动,图像格式,文本编码,自定义样式等;一种是低阶API,用于扩展Qt应用程序。本文主要是通过低阶API来创建Qt插件,并通过静态、动态两种方式来调用插件。

Qt插件开发的流程

  1. 定义一个接口集(只有纯虚函数的类)。
  2. 用宏Q_DECLARE_INTERFACE()将该接口告诉Qt元对象系统
  3. 声明插件类,插件类继承自QObject和插件实现的接口。
  4. 用宏Q_INTERFACES()将插件接口告诉Qt元对象系统(在头文件中)。
  5. 用宏Q_EXPORT_PLUGIN2()导出插件类。
  6. 用适当的.pro文件构建插件。

Qt插件调用的流程

  1. 包含接口头文件(只有纯虚函数的类)。
  2. 应用程序中用QPluginLoader来加载插件。
  3. 用宏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 += widgets

QMAKE_CXXFLAGS += -std=c++11
TARGET = EWhalesSerialPort
 
# TEMPLATE = lib 生成插件
# TEMPLATE = app 生成应用程序
TEMPLATE = lib
CONFIG  += plugin

SOURCES += main.cpp\
    serialport.core.cpp \
    serialport.pannel.cpp \
    serialport.framework.cpp \
    serialport.thread.cpp

HEADERS  += \
    serialport.core.h \
    serialport.interface.h \
    serialport.pannel.h \
    serialport.framework.h \
    serialport.thread.h

FORMS    += widget.ui

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

#target.path += /root/
#INSTALLS += target

RESOURCES += \
    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 >= 0x050000
    Q_PLUGIN_METADATA(IID QTPLUGIN_ISERIALPORT_IID)
#endif

public:
    /*
    此处省略了与插件开发无关的代码
    */
    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 < 0x050000
  Q_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:
		// 定义新的枚举变量:MBState
    typedef 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 changed
    virtual 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_OBJECT

public:

		/* 省略无关内容 */

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();
}
  • 4
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全能骑士涛锅锅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值