Qt界面程序嵌入另一个Qt界面程序[Linux]
Qt的界面程序出现的问题
在Qt界面程序里,主线程是界面线程,并且有且只有主线程才能完成界面的渲染操作,这样就会带来问题。一旦主进程一直处于一个非常耗时的函数中(如构建一个庞大的界面、处理大量的数据),无法处理事件循环中积压的事件以及回调,那么整个界面都会处于未响应状态。
Qt处理问题的传统方法
- 方法1 常规的处理方式,也是Qt官方的处理方式是调用 processEvents() 函数,在长期占用主线程的函数的合适地方不断地调用该函数,这样就可以不断处理各种积压的界面刷新动作、用户操作响应等
- 方法2 另一种处理方式,可以将数据的处理放置于另一个线程中处理,这样就可以解放主线程,让主线程专心应对于界面相关的工作。
传统方法的缺陷
有一种情况,上述两种方式都无法处理。比如说,程序在收到某个消息时创建一个新的界面,而创建这个界面非常耗时,在这种情况下,首先方法2是无法使用的,因为这是界面控件的创建必须在主线程。
然后是方法1,方法1存在风险,设想这样一个情况,程序收到消息 M_A 将数据 dataA 存储在对象 * structA* 上,然后界面 WidgetA 根据 dataA 来创建控件,在创建 widgetA 的中途中调用 processEvents() ,这时又收到一次消息 M_B ,那么调用processEvents()就会处理该消息的对应的函数,那么就会把 dataB 存在对象 structA 上,就会将原本的 dataA 数据给覆盖,那么一旦 processEvents() 结束,回到WidgetA的创建函数,但此时的数据已经不是原来的数据了,这样就会带来未知的后果。所以说,需要确保事件队列的所有动作不影响本函数(调用 processEvents() 的函数),才能调用 processEvents() 函数。
那么至少,我们在加载又大又多的界面时,需要给用户一个等待条的提示,这样才能避免界面未响应的不良效果。那等待条在加载界面时是无法刷新的,界面的刷新需要不断处理事件队列里关于绘制的事件,处理事件队列需要调用 processEvents() 函数,但是该函数是一股脑的把所有积压的事件处理了(当然Qt的 processEvents() 可以过滤用户动作产生的事件),只要其中有一个事件对正在进行创建的界面产生了影响,那么就会出现问题,那么这种情况下有什么解决方案呢?当然有,多进程界面开发。
多进程界面开发
参考blog:
https://www.cnblogs.com/swarmbees/p/11100513.html
https://blog.youkuaiyun.com/u013394556/article/details/78534833
https://blog.youkuaiyun.com/r5014/article/details/79286444
既然主线程的随心的调用processEvents(),而我们又需要刷新等待界面,而刷新界面必须主线程,既然如此,那就再创建一个主线程不就好了么!但是一个进程只有一个主线程啊,所以说我们需要再创建一个界面进程,然后把这个子界面进程嵌入到主界面进程当中,在主界面进程忙于工作时,子界面进程仍可以不断刷新窗口,这样就可以避免给用户界面未响应的体验。
关键函数:
QWindow::fromWinId(winId),可以通过句柄返回一个window对象
QWidget::createWindowContainer(), 可以为QWindow创建一份widget对象
示例代码
子窗口进程:
main.cpp
#include <QApplication>
#include <QDebug>
#include <stdio.h>
#include <QLabel>
#include <QMovie>
#include <QWindow>
int main(int argc, char *argv[])
{
if(argc == 2)
{
QApplication a(argc, argv);
//这里是子窗口的顶层窗口
QLabel w;
QMovie movie(&w);
movie.setFileName("XXX.gif");
w.setMovie(&movie);
movie.start();
//end
WId wid = WId(QString(argv[1]).toInt());//通过参数列表获取父进程窗口的WinId
QWindow *window = QWindow::fromWinId(wid);//获取父进程窗口
w.setProperty("_q_embedded_native_parent_handle", QVariant(wid));//设置属性,这句是必须的
w.winId();//必须调用一次,生成winId
w.windowHandle()->setParent(window);//设置父窗口
w.show();//最后调用show,提前调用qt会为其生成窗口控件,这样就会和你原本想要嵌入进的父进程界面产生冲突
fprintf(stderr, "%lld", w.winId());//写入标准错误输出,stderr能立即输出,stdout则不行
return a.exec();
}
return 0;
}
主窗口进程
waitingbarwin.h
#ifndef WAITINGBARWIN_H
#define WAITINGBARWIN_H
#include <QWidget>
namespace Ui {
class WaitingBarWin;
}
class QProcess;
class WaitingBarWin : public QWidget
{
Q_OBJECT
public:
explicit WaitingBarWin(QWidget *parent = 0);
~WaitingBarWin();
private:
Ui::WaitingBarWin *ui;
QProcess *m_process;
private slots:
void slot_createWaitingBar();
};
#endif // WAITINGBARWIN_H
waitingbarwin.cpp
#include "waitingbarwin.h"
#include "ui_waitingbarwin.h"
#include <QProcess>
#include <QDebug>
#include <QWindow>
#include <QPixmap>
#include <QBitmap>
WaitingBarWin::WaitingBarWin(QWidget *parent) :
QWidget(parent),
ui(new Ui::WaitingBarWin),
m_process(nullptr)
{
ui->setupUi(this);
QPixmap pixmap;
pixmap.load("XXX.gif");
setFixedSize(pixmap.size());
setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);
setMask(pixmap.mask());
if(m_process == nullptr)
{
QString cmd = "WaitingBar";//子程序执行文件地址
QStringList argList;
argList << QString::number(this->winId());//把父窗口的id给子进程传递过去
m_process = new QProcess(this);//使用进程运行子进程窗口
connect(m_process, &QProcess::readyReadStandardError, this, &WaitingBarWin::slot_createWaitingBar);//等待子进程窗口把自身的winId传递过来
m_process->start(cmd, argList);
}
}
WaitingBarWin::~WaitingBarWin()
{
delete ui;
m_process->terminate();//在父窗口关闭时,主动终止子窗口进程
m_process->waitForFinished(50);
}
void WaitingBarWin::slot_createWaitingBar()
{
quint64 winId = m_process->readAllStandardError().toLongLong();
QWindow *childWin = QWindow::fromWinId(winId);
if(childWin)
{
QWidget *widget = QWidget::createWindowContainer(childWin);//获取一个子进程窗口的widget
ui->verticalLayout->addWidget(widget);//这里是可以使用布局器管理子进程窗口的,不管理的话就在坐标0,0处
}
}
main.cpp
#include "waitingbarwin.h"
#include <QApplication>
#include <QMainWindow>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton win;
win.show();
WaitingBarWin w(&win);
//测试,主进程陷入长时间工作时,子进程窗口仍然正常刷新
QObject::connect(&win, &QPushButton::clicked, [&w](){
w.show();
while (1) {
}
});
return a.exec();
}
结语
关于windows平台的使用我没有测试,网上也大多是windows平台的,感觉是获取winId的方式可以使用windows系统的函数实现,这样就可以去嵌入更多的应用。linux平台的获取WinId的方法我暂时没有实现,只是通过Qt自带的方式获取本进进程界面的WinId,如果想要嵌入其他程序,能找到WinId的话理论上是可行的。