需求说明
最近有一个项目需要2个人一起开发,开发工具都是Qt,每个人负责一块内容,2块内容都有各自的UI文件和业务逻辑,我需要将其他人的界面子窗口嵌入到我的主窗口中,将2个软件整合成一个软件,而我们彼此的代码不共享,因此这2块内容必然有数据交互。这里最麻烦的事情就是我们2个人软件如何整合,如何交换数据,如何将他的界面嵌入到我的主窗口中,点击主窗口中某个按钮如何弹出他的子窗口。最开始我们想到的是利用Qt IPC进程间通信,将他的窗口嵌入到我的主窗口中(主要是这个函数QWindow *QWindow::fromWinId(WIdid)实现嵌套),再用共享内存实现数据的交互,这种方法可以实现我们的要求,但是这种方法每次需要先启动他的进程,记录他的窗口句柄,然后我再获取到他的窗口句柄,才能将他的窗口显示出来,首先这里存在启动先后的问题,我这边是主界面,软件启动理应先加载我的界面,其次,由于要记录他的窗口句柄,我的界面启动需要延时,造成软件启动不流畅,最后也是最大的问题是,2个进程间窗口的嵌套会造成软件的卡顿,流程性很差,而且用共享内存交换数据,两者需要不断收发数据,造成资源浪费,增加程序的复杂度,最终我们放弃了这种方法。
后续我探索了Qt插件的开发,如果将他的程序做成一个带UI的dll,我直接调用接口函数,得到他的窗口指针就可以将他的窗口嵌入到主窗口中,交换数据也通过接口函数实现,最终软件将是一个进程。最终证实这种方法确实可行,软件复杂度和运行效率提高不少。
方法实现
Qt插件,制作方和调用方需要一个公共的头文件,这个头文件也就是接口,里面包含一些接口类和接口函数,2者通过这些接口函数实现数据的交换。接口函数都是纯虚函数,由制作方继承接口类实现接口纯虚函数。我演示用的头文件interface.h如下:
#ifndef INTERFACE_H
#define INTERFACE_H
#include <QtPlugin>
#include <QDialog>
#include <QGraphicsView>
class PopWidgetInterface
{
public:
virtual QDialog *dialogHandle() = 0;
virtual QStringList paraList() = 0;
virtual QGraphicsView *graphicViewHandle() = 0;
};
#define PopWidgetInterface_iid "org.qt-project.Interface/1.0"
QT_BEGIN_NAMESPACE
Q_DECLARE_INTERFACE(PopWidgetInterface, PopWidgetInterface_iid)
QT_END_NAMESPACE
#endif // INTERFACE_H
头文件中非常重要的一行代码是Q_DECLARE_INTERFACE(PopWidgetInterface, PopWidgetInterface_iid),这个宏将给定的标识符PopWidgetInterface_iid关联到接口类PopWidgetInterface,后面通过这个唯一的标识符找到对应的接口类,这个宏通常在头文件中接口类定义之后使用。
接着插件制作方需要继承接口类PopWidgetInterface,并实现接口纯虚函数。我定义了一个WidgetPlugin类实现接口函数,WidgetPlugin.h代码如下:
#include <Interface.h>
#include "platesetting.h"
#include "mygraphic.h"
class WidgetPlugin : public QObject, PopWidgetInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID PopWidgetInterface_iid)
Q_INTERFACES(PopWidgetInterface)
public:
explicit WidgetPlugin(QObject *parent = nullptr);
QDialog* dialogHandle() override;
QStringList paraList() override;
QGraphicsView* graphicViewHandle() override;
private:
PlateSetting *m_dlg;
};
头文件WidgetPlugin.h中也有2个很重要的宏,Q_PLUGIN_METADATA用于声明元数据,这些元数据是实列化类WidgetPlugin对象的一部分,可以将插件导出,宏中需要声明一个IID,也就是我们上述接口头文件中定义的 PopWidgetInterface_iid一串字符。Q_PLUGIN_METADATA还有一个FILE可选项,指向一个json文件,该文件包含插件相关的一些信息,json文件必须在构建系统指定的包含目录中,当选择了FILE参数,如果没有找到FILE指定的文件,moc出错退出,无法编译通过,这里为了方便,我没有指定FILE选项。
PlateSetting *m_dlg是一个待弹出对话框的类的实例,当在我的主窗口中点击某个按钮可以弹出别人的窗口,如下图所示,我通过接口函数paraList获取他的界面中某些参数。QGraphicsView* graphicViewHandle() 函数是返回一个MyGraphic对象,用于显示一个图像,这个widget对象需要嵌入到主窗口中。
WidgetPlugin.cpp文件如下:
WidgetPlugin::WidgetPlugin(QObject *parent) : QObject(parent)
{
m_dlg = new PlateSetting;
}
QDialog* WidgetPlugin::dialogHandle()
{
if(m_dlg)
return m_dlg;
return nullptr;
}
QStringList WidgetPlugin::paraList()
{
return m_dlg->paraList();
}
QGraphicsView* WidgetPlugin::graphicViewHandle()
{
return new MyGraphic;
}
MyGraphic类代码如下:
class MyGraphic : public QGraphicsView
{
Q_OBJECT
public:
MyGraphic(QWidget *parent = nullptr);
private:
QGraphicsScene *m_pScene;
QGraphicsPixmapItem *m_pItem;
};
MyGraphic::MyGraphic(QWidget *parent):QGraphicsView(parent)
{
m_pScene = new QGraphicsScene;
this->setScene(m_pScene);
m_pItem = m_pScene->addPixmap(QPixmap(":/pic.bmp"));
}
最后我们需要在插件工程文件.pro中添加CONFIG += plugin和TEMPLATE= lib告诉编译器需要生成插件dll.
插件部分到此结束,编译之后会生成一个dll库,供调用方使用。
接下来需要建立一个工程调用此插件。首先需要将接口头文件包含到调用工程项目中,将上述生成的dll文件拷贝到我的调用工程生成的exe目录下。调用函数代码如下:
void MainWindow::loadPlugin()
{
QDir pluginsDir(qApp->applicationDirPath());
QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(QString("plugins.dll")));
QObject *plugin = pluginLoader.instance();
if(plugin) {
m_pInterface = qobject_cast<PopWidgetInterface*>(plugin);
if(!m_pInterface) {
qDebug() <<"qobject_cast error";
}
else {
ui->verticalLayout_2->addWidget( m_pInterface->graphicViewHandle());
}
}
else {
qDebug() << __FUNCTION__ << pluginLoader.errorString();
}
}
通过类QPluginLoader加载插件,加载成功后将返回的QObject对象转换成接口类PopWidgetInterface实例对象即m_pInterface,转换成功后就可以调用接口函数,获取我们想要的子窗口以及参数列表。如下图所示,MainWindow是我的主窗口,窗口中包含一个按钮和一张表格,单机按钮可以弹出外部对话框,表格下方的图像窗口也是其他人的窗口,通过调用loadPlugin函数可以调用外部程序,成功实现窗口嵌套和数据交换。按钮单击事件代码如下:
void MainWindow::on_pushButton_8_clicked()
{
if(m_pInterface)
{
m_pInterface->dialogHandle()->show();
connect(m_pInterface->dialogHandle(), &QDialog::accepted, [this] {
qDebug() << m_pInterface->paraList();
});
}
}