近期接到的任务场景是需要在windows动态链接库(dll)中封装Qt的QWebEngine控件,调用该dll的主程序可能是win32或MFC程序,也可能是Qt程序本身。要求是在dll内部封装的QWebEngine显示在调用该dll的MFC或Qt程序界面中,类似于将dll中的Qt窗口嵌入到主程序的窗口中,并接收用户操作和响应。
查阅了网上的各种资料(网上关于这种应用场景的资料实在少的可怜),终于找到一个关于该方面的开源代码qt-solutions-master,并找到相关帖子写出的使用方法,地址为:
https://blog.csdn.net/libin88211/article/details/38183791。
然而,当我按照网上的方法使用qtwinmigrate中的qtdll示例,将Qt的界面封装进DLL中,再用MFC的应用程序调用,的确可以在MFC界面上嵌入了DLL中封装的Qt界面,但是拖动MFC界面时,DLL中的Qt界面并没有跟随MFC而动!MFC和DLL中的Qt界面是两个不同的界面。而且也无法做到MFC主程序中的数据和DLL中的Qt界面数据进行交互。
在尝试多次之后依然获取不到预期的结果,把使用qtwinmigrate这个方案彻底否决了。通过不停试验和写了无数个demo程序研究过后,终于找到了一个符合预期的解决方案。
目录
1、实现效果
2、MFC作为主程序调用dll
3、Qt作为主程序调用dll
4、代码实现
5、dll接口和注意事项
6、界面嵌入
系统:win7 64位
环境:Qt 5.8.0 + VisualStudio 2015
Qt版本: qt-opensource-windows-x86-msvc2015_64-5.8.0.exe
MFC程序:V140版本64位
如下图所示是在dll封装的一个Qt界面,这是一个QFrame,增加了一个QPushButton,下面增加了一个滑块QSlider,QSlinder下面是一个QLineEdit用来显示滑块数值。这三个控件仅仅用来测试看效果,QFrame上可以任意增加Qt控件和布局。
可以看到能够正确作出Qt事件响应。
在具体的实现中,可以看到MFC主程序调用该dll,Qt主程序调用该dll是作出区分的,下面会说明原因。
我们知道,一般性的Qt应用程序,是有自己的事件循环,在main()函数中,一般情况下会这样写代码:
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QWidget w;
w.show();
return a.exec();
}
当调用a.exec()后,主程序就会阻塞在这里,并且开启Qt自身的消息循环和处理。那么如果想在dll中嵌入Qt界面并实现Qt正常的工作,就必然少不了在dll中实现Qt事件循环,包括QApplication::exec()的调用。
所以在MFC调用dll之前,dll不仅仅是需要创建内部的Qt界面或者控件,而且也需要开启Qt的事件循环,最简单的方法就是开启单独的一个线程,用来作为Qt的事件循环。具体的实现就是初始化dll的接口中开启一个线程,在该线程中实现如下代码:
int argc = 1;
TCHAR targv[MAX_PATH] = { 0 };
GetModuleFileName(NULL, targv, MAX_PATH);
USES_CONVERSION;
std::string strPath = T2A(targv);
char argv[MAX_PATH];
memcpy(argv, strPath.c_str(), strPath.length());
char* pargv = argv;
QApplication* a= new QApplication(argc, &pargv);
// 这里创建dll内部的Qt界面例如QWidget
QWidget w;
w.show();
a.exec();
当调用a.exec()之后,该线程就被阻塞在这里,该线程就是实现QWidget的事件循环的线程动力。当需要结束这个事件循环,或者关闭这个dll中界面时,我们需要调用a->quit();来结束QApplication。
以上是MFC作为主程序来调用dll的实现。
那么为什么MFC作为主程序调用和Qt作为主程序调用要区分开呢?原因是当Qt作为主程序的时候,Qt本身自带了自己的Qt事件循环,这个时候我们再像MFC调用dll那样弄个单独的线程创建一个QApplication就会失败,提示进程空间中已经存在QApplication了,并且一个主线程Qt事件循环,一个我们自己的线程Qt事件循环,两者就会有冲突。解决这个方法如下:
在我的dll内部实现中,我继承一个QObject的类,在初始化中通过调用:
QCoreApplication* app = QCoreApplication::instance();
将QApplication或者称为QCoreApplication实例给取出来,然后调用:
this->moveToThread(app->thread());
将当前类(当前类继承自QObject)附着到Qt的主线程中,这个Qt的主线程就是Qt主程序,即调用该dll的Qt程序,这个时候我们就可以使用Qt的事件循环了,事件循环在一个线程中,并且在调用该dll的Qt主线程中。
我用QCoreApplication::postEvent()来发送创建自定义的消息,并通过继承实现Qt的
virtual void customEvent(QEvent *event);
接口来处理自定义的消息,在这个函数中实现内部Qt界面的创建、初始化和销毁。
我首先在dll中写出一个接口类:
// dialog接口类,根据主程序是Qt还是win32程序实例化不同的实现类
class libDialogBase
{
public:
libDialogBase() : qDialogPtr_(nullptr), hDialogHandle_(0) {}
virtual ~libDialogBase() {}
public:
virtual bool Initialize(const libGlobalParam* globalParam) = 0; // 初始化
virtual void UnInitialize() = 0; // 结束
virtual bool GetHandle(HWINDOW& handle) = 0; // 获取dll内部Qt窗口句柄
virtual void ShowDialog() = 0; // 显示dll内部的Qt窗口
virtual void HideDialog() = 0; // 隐藏dll内部的Qt窗口
virtual void Resize(int width, int height) = 0; // 改变dll内部的Qt窗口大小
protected:
std::shared_ptr qDialogPtr_;
HWINDOW hDialogHandle_;
};
其中DialogQt是真正在dll内部封装的Qt界面,继承QFrame,在DialogQt中布局了QPushbutton、QSlider和QLineEdit这些自己需要的控件,DialogQt声明如下:
#ifndef __DIALOG_QT_H__
#define __DIALOG_QT_H__
#include
#include
#include
#include
// 在本dll中封装的Qt界面,真正的Qt窗口。
class DialogQt : public QFrame
{
Q_OBJECT
public:
DialogQt(QWidget* parent = nullptr);
~DialogQt();
private:
void init();
void loadStyleSheet();
void connectSlots();
public:
void showDialog();
void hideDialog();
void resizeDialog(int width, int height);
signals:
void sglShowDialog();
void sglHideDialog();
void sglResizeDialog(int width, int height);
private slots:
void slotShowDialog();
void slotHideDialog();
void slotResizeDialog(int width, int height);
void slotBtnClick();
void slotSliderValue(int value);
private:
int width_;
int height_;
QPushButton* btnTest_;
QSlider* sliderTest_;
QLineEdit* editTest_;
};
#endif // !__DIALOG_QT_H__
其次我继承libDialogBase并实现了两个类:libDialogWin和libDialogQt。
libDialogWin是当MFC或Win32这样的主程序调用dll时实例化,来开启内部Qt的事件循环。而libDialogQt是当Qt主程序调用dll的时候实例化的。所以在dll接口传入参数时,还需要调用者传入当前主程序是否为Qt应用程序,这个区分需要调用者保证。
libDialogWin的声明和实现
libDialogWin.h:
#ifndef __LIB_DIALOG_WIN_H__
#define __LIB_DIALOG_WIN_H__
#include "libDialogBase.h"
#include "libTool.h"
#include
#include
#include
// libDialogBase的实例类,当调用该库的程序为win32程序时实例化该类
class libDialogWin : public libDialogBase, public std::enable_shared_from_this
{
public:
libDialogWin();
~libDialogWin();
protected:
virtual bool Initialize(const libGlobalParam* globalParam);
virtual void UnInitialize();
virtual bool GetHandle(HWINDOW& handle);
virtual void ShowDialog();
virtual void HideDialog();
virtual void Resize(int width, int height);
private:
void thread_proc();
void initWin();
void finishWin();
private:
std::atomic inited_;
libtool::Event initEvent_;
libtool::Event finishEvent_;
std::thread thread_;
libGlobalParam globalParam_;
QApplication* qtapp_;
};
#endif // !__LIB_DIALOG_WIN_H__
libDialogWin.cpp:
#include "libDialogWin.h"
#include
#include
#include
libDialogWin::libDialogWin()
: libDialogBase()
, inited_(false)
, initEvent_()
, finishEvent_()
, thread_()
, globalParam_()
, qtapp_(nullptr)
{
}
libDialogWin::~libDialogWin()
{
}
bool libDialogWin::Initialize(const libGlobalParam* globalParam)
{
bool expected = false;
if (!inited_.compare_exchange_strong(expected, true))
return true;
if (!globalParam || !globalParam->hWindow)
return false;
globalParam_ = *globalParam;
thread_ = std::thread(std::bind(&libDialogWin::thread_proc, shared_from_this()));
initEvent_.Wait();
return true;
}
void libDialogWin::UnInitialize()
{
bool expected = true;
if (!inited_.compare_exchange_strong(expected, false))
return;
if (qtapp_)
qtapp_->quit();
finishEvent_.Wait();
// finishWin();
inited_ = false;
}
bool libDialogWin::GetHandle(HWINDOW& handle)
{
if (!inited_)
{
handle = 0;
return false;
}
handle = hDialogHandle_;
return true;
}
void libDialogWin::ShowDialog()
{
if (inited_ && qDialogPtr_)
qDialogPtr_->showDialog();
}
void libDialogWin::HideDialog()
{
if (inited_ && qDialogPtr_)
qDialogPtr_->hideDialog();
}
void libDialogWin::Resize(int width, int height)
{
if (inited_ && qDialogPtr_)
qDialogPtr_->resizeDialog(width, height);
}
void libDialogWin::thread_proc()
{
initWin();
qtapp_->exec();
finishWin();
}
void libDialogWin::initWin()
{
if (qtapp_ == nullptr && qDialogPtr_ == nullptr)
{
int argc = 1;
TCHAR targv[MAX_PATH] = { 0 };
GetModuleFileName(NULL, targv, MAX_PATH);
USES_CONVERSION;
std::string strPath = T2A(targv);
char argv[MAX_PATH];
memcpy(argv, strPath.c_str(), strPath.length());
char* pargv = argv;
qtapp_ = new QApplication(argc, &pargv);
QWidget* parentWidget = QWidget::find(WId(globalParam_.hWindow));
if (parentWidget != nullptr)
{
int w = parentWidget->width();
int h = parentWidget->height();
}
qDialogPtr_ = std::make_shared();
hDialogHandle_ = (HWINDOW)qDialogPtr_->winId();
initEvent_.Set();
}
}
void libDialogWin::finishWin()
{
if (qDialogPtr_)
{
qDialogPtr_->close();
qDialogPtr_.reset();
}
SAFE_DELETE(qtapp_);
finishEvent_.Set();
}
libDialogQt的声明和实现
libDialogQt.h:
#ifndef __LIB_DIALOG_QT_H__
#define __LIB_DIALOG_QT_H__
#include "libDialogBase.h"
#include "libTool.h"
#include
#include
#include
#include
// libDialogBase的实例类,当调用该库的程序为Qt程序时实例化该类
class libDialogQt : public QObject, public libDialogBase
{
Q_OBJECT
public:
libDialogQt(QObject* parent = nullptr);
~libDialogQt();
protected:
virtual bool Initialize(const libGlobalParam* globalParam);
virtual void UnInitialize();
virtual bool GetHandle(HWINDOW& handle);
virtual void ShowDialog();
virtual void HideDialog();
virtual void Resize(int width, int height);
protected:
virtual void customEvent(QEvent *event);
private:
std::atomic inited_;
libtool::Event initEvent_;
libtool::Event finishEvent_;
std::thread thread_;
libGlobalParam globalParam_;
};
#include
class libDialogEvent : public QEvent
{
public:
enum EventCode
{
CREATE,
CLOSE,
};
public:
libDialogEvent(libDialogEvent::EventCode evtCode);
~libDialogEvent();
static QEvent::Type evtType_;
EventCode evtCode_;
};
#endif // !__LIB_DIALOG_QT_H__
libDialogQt.cpp:
#include "stdafx.h"
#include "libDialogQt.h"
#include
#include
libDialogQt::libDialogQt(QObject* parent)
: libDialogBase()
, inited_(false)
, initEvent_()
, finishEvent_()
, thread_()
, globalParam_()
{
}
libDialogQt::~libDialogQt()
{
}
bool libDialogQt::Initialize(const libGlobalParam* globalParam)
{
bool expected = false;
if (!inited_.compare_exchange_strong(expected, true))
return true;
if (!globalParam || !globalParam->hWindow)
return false;
QCoreApplication* app = QCoreApplication::instance();
if (globalParam->appType == APPTYPE::MAT_QT && app == nullptr)
return false;
globalParam_ = *globalParam;
this->moveToThread(app->thread());
QCoreApplication::postEvent(this, new libDialogEvent(libDialogEvent::EventCode::CREATE));
initEvent_.Wait();
inited_ = true;
return true;
}
void libDialogQt::UnInitialize()
{
bool expected = true;
if (!inited_.compare_exchange_strong(expected, false))
return;
QCoreApplication::postEvent(this, new libDialogEvent(libDialogEvent::EventCode::CLOSE));
inited_ = false;
}
bool libDialogQt::GetHandle(HWINDOW& handle)
{
if (!inited_)
{
handle = 0;
return false;
}
handle = hDialogHandle_;
return true;
}
void libDialogQt::ShowDialog()
{
if (inited_ && qDialogPtr_)
qDialogPtr_->showDialog();
}
void libDialogQt::HideDialog()
{
if (inited_ && qDialogPtr_)
qDialogPtr_->hideDialog();
}
void libDialogQt::Resize(int width, int height)
{
if (inited_ && qDialogPtr_)
qDialogPtr_->resizeDialog(width, height);
}
void libDialogQt::customEvent(QEvent *event)
{
if (event == nullptr)
return;
if (event->type() == libDialogEvent::evtType_)
{
libDialogEvent* libEvent = dynamic_cast(event);
if (libEvent->evtCode_ == libDialogEvent::CREATE)
{
QWidget* parent = QWidget::find(WId(globalParam_.hWindow));
qDialogPtr_ = std::make_shared(parent);
initEvent_.Set();
}
else if (libEvent->evtCode_ == libDialogEvent::CLOSE)
{
if (qDialogPtr_)
{
qDialogPtr_->close();
qDialogPtr_.reset();
}
finishEvent_.Set();
}
}
}
QEvent::Type libDialogEvent::evtType_ = (QEvent::Type)QEvent::registerEventType(QEvent::User + 100);
libDialogEvent::libDialogEvent(libDialogEvent::EventCode evtCode)
: QEvent(evtType_)
, evtCode_(evtCode)
{
}
libDialogEvent::~libDialogEvent()
{
}
可以看到增加了自定义的事件,并在事件处理中创建DialogQt的。
dll的接口声明和注释如下:
extern "C"
{
#ifndef __LIB_QT_IN_H__
#define __LIB_QT_IN_H__
#include "libCommon.h"
#ifdef LIBQTIN_EXPORTS
#define LIBQTIN_API __declspec(dllexport)
#else
#define LIBQTIN_API __declspec(dllimport)
#endif
/*
* 初始化接口。通过全局参数传入一些必要的信息,比如主窗口句柄,主程序类型
* 返回true表示接口调用成功,否则失败。通过回调告诉主程序初始化是否成功。
*/
LIBQTIN_API bool libQtIn_Initliaze(const libGlobalParam* globalParam);
/*
* 加载接口。初始化并加载dll中封装的Qt界面并显示到主程序窗口中。
* 返回true:接口调用成功,否则失败。
* 通过回调函数告诉主程序加载是否成功。
*/
LIBQTIN_API bool libQtIn_Load();
/*
* 获取库中封装的Qt主界面句柄
*/
LIBQTIN_API bool libQtIn_GetHandle(HWINDOW& handle);
/*
* 设置dll中界面大小。
* 主窗口在改变大小的时候可调用此接口让dll中的窗口随主窗口改变而改变
* true表示成功,否则失败。
*/
LIBQTIN_API bool libQtIn_Resize(int width, int height);
/*
* 卸载接口。卸载dll中的Qt窗口。
*/
LIBQTIN_API void libQtIn_UnLoad();
/*
* 结束dll的调用,释放一些资源。
*/
LIBQTIN_API void libQtIn_Finish();
#endif // !__LIB_QT_IN_H__
}
在dll中创建了Qt窗口(即类中的DialogQt界面),我们知道Qt界面程序需要使用Qt自带的moc来处理源文件。moc 全称是 Meta-Object Compiler,也就是“元对象编译器”。Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 源文件。如果它发现在一个头文件中包含了宏 Q_OBJECT,则会生成另外一个 C++ 源文件。这个源文件中包含了 Q_OBJECT 宏的实现代码。这个新的文件名字将会是原文件名前面加上 moc_ 构成。这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此我们可以知道,这个新的文件不是“替换”掉旧的文件,而是与原文件一起参与编译。另外,我们还可以看出一点,moc 的执行是在预处理器之前。因为预处理器执行之后,Q_OBJECT 宏就不存在了。
基于上述的原因,我们dll中封装的Qt界面同样需要moc来处理,否则无法使用。我的方法是在dll工程中增加一个moc.bat的批处理脚本,脚本内容为:
C:\Qt\Qt5.8.0\5.8\msvc2015_64\bin\moc "libDialogQt.h" -o "moc_libDialogQt.cpp"
C:\Qt\Qt5.8.0\5.8\msvc2015_64\bin\moc "DialogQt.h" -o "moc_DialogQt.cpp"
其中的路径为自己的Qt安装路径。
在第一次使用的时候,我们手动执行moc.bat,让其生成 moc_libDialogQt.cpp 和 moc_DialogQt.cpp两个源文件并加入工程中。然后在dll工程预处理事件中增加moc.bat命令,如下图所示:
这样每次在编译dll工程时,如果libDialogQt.h和DialogQt.h中有改变,执行moc命令后将改变写入到相应的源文件中编译了。
将dll界面嵌入主程序中是通过窗口句柄来完成。
我们定义dll初始化参数为:
typedef struct _lib_Global_Param_ libGlobalParam;
struct _lib_Global_Param_
{
HWINDOW hWindow; // 调用该dll窗口的父窗口句柄
APPTYPE appType; // 调用该dll的主程序类型
void* userData; // 用户数据,由调用者提供,可为nullptr
void* qtApplication; // 调用该dll的程序若为Qt,则该成员为调用者的主程序指针,
// 若为win32程序,则可为空
void(*OnEvent)(LIBEVENT eventType, bool bSuccess, libGlobalParam* globalParam); // 调用接口的回调函数
// 其中bSuccess为true,则表示该接口成功,否则为失败
libGlobalParam()
: hWindow(0)
, appType(APPTYPE::MAT_QT)
, userData(nullptr)
, qtApplication(nullptr)
, OnEvent(nullptr)
{}
};
HWINDOW是void*类型,作为窗口句柄的类型。
MFC在拿到dll中窗口句柄并嵌入到自己界面的方法如下:
void CWinTestDlg::OnSize(UINT nType, int cx, int cy)
{
CDialogEx::OnSize(nType, cx, cy);
// TODO: 在此处添加消息处理程序代码
HWINDOW libWinHandle = 0;
if (libQtIn_GetHandle(libWinHandle) && libWinHandle != 0)
{
CRect rect;
GetDlgItem(IDC_STATIC_QTDIALOG)->GetClientRect(&rect);
::SetParent((HWND)libWinHandle, GetDlgItem(IDC_STATIC_QTDIALOG)->GetSafeHwnd());
::MoveWindow((HWND)libWinHandle, rect.left, rect.top, rect.Width(),
rect.Height(), FALSE);
::ShowWindow((HWND)libWinHandle, SW_SHOW);
}
}
Qt主程序则将要嵌入的主界面句柄作为libGlobalParam的hWindow成员传进去即可。
该dll库程序是一个demo示例,仅仅用来作为DLL中封装Qt界面的一种解决方案。可以在dll的DialogQt上任意嵌入窗口、控件和布局,也可以让其作为父窗口创建子窗口。
dll中窗口与主程序直接的消息传送则靠dll导出的接口。
文中的代码及测试示例下载地址为:https://download.csdn.net/download/explorer114/12326450