Qt事件机制概览
本文内容基于window平台进行Qt事件机制的简要梳理
消息循环
windows窗口所发生的一切都是通过消息传给窗口过程,然后窗口过程以某种形式对消息做出反应,或是把消息传递给DefWindowProc进行默认处理。windows的每个窗口线程都有各自的消息队列,线程可以循环的获取队列中的消息:
while(GetMessage(&msg,NULL,0,0))
{
TranlateMessage(&msg);
DispatchMessage(&msg); //--->调用窗口过程
}
从单独一个线程的角度看,他们各自的消息循环都是序列化的:
获取消息—》分发消息到窗口过程—》窗口过程处理消息并返回—》获取消息
窗口过程应尽可能快的处理消息并返回消息循环,否则,在窗口过程卡在一个十分耗时的消息的处理上时,应用程序的这个窗口所在的线程的消息循环就会卡在这里,这就导致这个窗口看上去卡死在桌面上,关也关不了,移也移不动。事实是,在消息循环阻塞时,操作系统能感知用户的操作,并改变窗口句柄在内核中的对象的数据,也会将消息发送到窗口线程的消息队列中。例如,当用户改变窗口大小时,窗口的内核对象会记录窗口改变后的大小、无效区域等信息,然后发送WM_PAINT消息给窗口线程的消息队列,当阻塞的窗口过程从耗时的消息返回后,回到事件循环,继续从消息队列中获取消息,就能获得阻塞时操作系统发送到线程队列中的大量的延时消息,并处理这些消息。
Qt事件循环
简介
int main(int argc, char *argv[])
{
QApplication a(argc,argv);
...
return a.exec();
}
事件循环一般以exec调用开始,例如QApplication::exec()、QDialog::exec()、QMenu::exec()…,其实他们最后都依赖于QEventLoop来创建一个事件循环:
The QEventLoop class provides a means of entering and leaving an event loop.
At any time, you can create a QEventLoop object and call exec() on it to start a local event loop. From within the event loop, calling exit() will force exec() to return.
QEventLoop是对事件循环的抽象,一个线程可以有多个嵌套的事件循环:
int QEventLoop::exec()
…
void OnXXXSlot()
{ QEventLoop loop; ... loop.exec(); }
上面的loop就是一个嵌套的事件循环,loop的嵌套使得OnXXXSlot函数在loop结束之前不会返回,函数栈也就一直存在,所以在函数体中创建的栈对象得以长时间的存在。
局部的QEventLoop和QApplication创建的QEventLoop的功能是没差别的,局部的事件循环嵌套在上一层的事件循环中,可以替代外层的循环处理事件:
class MyClass : public QWidget
{
Q_OBJECT //如果不需要信号和槽功能,则可以将Q_OBJECT宏去掉
public:
MyClass(QWidget *parent = 0):QWidget(parent){}
protected:
void mousePressEvent(QMouseEvent * event)
{
static int level = 0;
m_label.setText(QString("Enter : %1").arg(++level));
}
private:
QLabel m_label;
};
class Widget : public QWidget
{
public:
Widget(QWidget *_p = nullptr) :QWidget(_p){ }
protected:
void mousePressEvent(QMouseEvent *e)
{
static int level = 0;
m_label.setText(QString("Enter : %1").arg(++level));
//创建并启动一个局部的事件循环作为线程当前的事件循环
QEventLoop loop;
loop.exec();
}
QLabel m_label;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MyClass w;
Widget cw(&w);
w.show();
return a.exec();
}
上面的程序创建了两个窗口,在点击窗口cw
之前,只有一个最外层的循环,是QApplication创建的。这时点击w产生的QMouseEvent是通过这个循环传递给w的。第一次点击cw时产生的QMouseEvent也是这个循环传递的。之后,点击w获得的QMouseEvent则是来自于cw的mousePressEvent创建的局部事件循环。
QEventLoop
当这样做时:
{
...
QEventLoop loop
loop.exec();
...
}
就创建了一个事件循环,那么QEventLoop::exec干了什么?
先看构造函数
QEventLoop::QEventLoop(QObject *parent)
: QObject(*new QEventLoopPrivate, parent)
{
Q_D(QEventLoop);
//QApplication是所有线程共享的对象,全局且唯一
if (!QCoreApplication::instance()) {
qWarning("QEventLoop: Cannot be used without QApplication");
} else if (!d->threadData->eventDispatcher.load()) {
//如果当前线程还没有事件派发器,那就创建一个
QThreadPrivate::createEventDispatcher(d->threadData);
}
}
两点,一,QApplication是所有线程共享的对象,全局且唯一 ;二,一个线程有且只有一个eventDispatcher,如果不存在,则创建一个。而且,由于QEventLoop不能在QApplication之前创建,所以,如果QEventLoop是在GUI线程中构造,那么eventDispatcher早在QApplication构造时就被创建了,所以免了自己创建eventDispatcher的步骤。如果QEventLoop是在非GUI线程中构造呢?这种情况肯定是存在的,因为非GUI线程可能也需要处理事件,这些事件不是来自可见窗口,而是来自自己或其他线程。例如,使用跨线程的信号和槽。下面看看在非GUI下的QEventLoop:
class MyThread :public QThread
{
void run() Q_DECL_OVERRIDE{
//start a event loop
this->exec();
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MyThread t;
t.start();
return a.exec();
}
现在,程序有两个线程,一个是GUI线程,一个是线程t。GUI线程有一个事件循环,在a.exec中创建并启动,线程t也有一个事件循环,在t.exec中创建并启动。
就像在GUI线程中的事件循环需要使用一个事件派发器一样,任何一个线程中的事件循环都需要一个派发器。GUI线程中的事件派发器是在构造QApplication时创建的,是一个QWindowsGuiEventDispatcher类的派发器,在这个派发器的构造函数中同时还创建了一个message-only窗口。
QWindowsGuiEventDispatcher::QWindowsGuiEventDispatcher(QObject *parent) :
QEventDispatcherWin32(parent), m_flags(0)
{
setObjectName(QStringLiteral("QWindowsGuiEventDispatcher"));
//创建 message-only 窗口
createInternalHwnd();
}
对于需要事件循环的非GUI线程,message-only窗口是不可或缺的,因为没有他,线程就没有消息队列,何谈消息循环,除非Qt使用另外的机制而非消息循环机制来支持非GUI线程的事件循环,不过这完全没必要。我们来看看这些步骤在非GUI线程中是怎么完成的:
第一步,创建一个事件派发器
事件派发器在t.start
中被创建:
unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg)
{
QThread *thr = reinterpret_cast<QThread *>(arg);
QThreadData *data = QThreadData::get2(thr);
qt_create_tls();
TlsSetValue(qt_current_thread_data_tls_index, data);
data->threadId = reinterpret_cast<Qt::HANDLE>(GetCurrentThreadId());
...
if (data->eventDispatcher.load()) // custom event dispatcher set?
data->eventDispatcher.load()->startingUp();
else
createEventDispatcher(data); //创建事件派发器
...
}
创建的是一个QEventDispatcherWin32类的事件派发器,它并不像QWindowsGuiEventDispatcher一样在构造的同时还创建 message-only 窗口。
第二步,创建一个 message-only 窗口
如果在启动事件循环的过程中发现当前的事件派发器还没有创建 message-only 窗口的话,那就会为其创建一个这样的窗口。
bool QEventDispatcherWin32::processEvents(QEventLoop::ProcessEventsFlags flags)
{
Q_D(QEventDispatcherWin32);
if (!d->internalHwnd) {
createInternalHwnd();
wakeUp(); // trigger a call to sendPostedEvents()
}
...
}
对比GUI线程创建事件派发器和message-only窗口的一步到位,非GUI线程采用延迟的方式来处理。为什么要这样做呢?像GUI线程一步到位不行吗?当然可以,但是没必要,因为创建一个 message-only 窗口是要占用内核资源的,GUI线程一定需要一个消息循环来实现事件循环,所以一步到位的创建没毛病,但是非GUI线程可能根本就不需要一个事件循环,所以,白白浪费资源干嘛呢?
跨线程的信号和槽与事件循环
TestOb.h
#ifndef TESTOB
#define TESTOB
#include <QObject>
class TestOb : public QObject
{
Q_OBJECT
public:
TestOb(QObject *parent = 0) :QObject(parent){}
public slots :
void updateNumber(int num){
m_num = num;
sendChangedeSignal(m_num);
}
signals:
void sendChangedeSignal(int num);
private:
int m_num = 0;
};
#endif
testUI.h
#ifndef TESTUI
#define TESTUI
#include <QtWidgets/QApplication>
#include <QLabel>
#include <QGridLayout>
class TestUI : public QWidget
{
Q_OBJECT
public:
TestUI(QWidget *parent = 0) :QWidget(parent), m_pLabel(nullptr), m_layout(this){
setGeometry(600, 200, 50, 200);
m_pLabel = new QLabel(this);
m_pLabel->setText("0");
m_layout.addWidget(m_pLabel);
setLayout(&m_layout);
}
public slots :
void updateNumber(int num){
if (m_pLabel)
m_pLabel->setText(QString("%1").arg(num));
emit sendChangedeSignal(m_pLabel->text().toInt() + 1);
}
signals:
void sendChangedeSignal(int num);
protected:
void mousePressEvent(QMouseEvent * event){
emit sendChangedeSignal(m_pLabel->text().toInt() + 1);
}
private:
QGridLayout m_layout;
QLabel *m_pLabel;
};
#endif
Mythread.h
#ifndef MYTHREAD
#define MYTHREAD
#include <QThread>
#include <QEventLoop>
#include "testOb.h"
class MyThread :public QThread
{
Q_OBJECT
void run() Q_DECL_OVERRIDE{
TestOb Ob;
QObject::connect(this, SIGNAL(sendChangedeSignal_1(int)),
&Ob, SLOT(updateNumber(int)));
QObject::connect(&Ob, SIGNAL(sendChangedeSignal(int)),
this, SLOT(updateNumber_1(int)));
//start a event loop in this thread
QEventLoop loop;
loop.exec();
}
public slots:
void updateNumber(int num){
emit sendChangedeSignal_1(num);
}
void updateNumber_1(int num){
emit sendChangedeSignal(num);
}
signals:
void sendChangedeSignal(int num);
void sendChangedeSignal_1(int num);
};
#endif
#include "Mythread.h"
#include "testUI.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MyThread t;//线程t,在gui线程中创建
TestUI ui; //ui界面,在gui线程中创建
QObject::connect(&ui, SIGNAL(sendChangedeSignal(int)),
&t, SLOT(updateNumber(int)));
QObject::connect(&t, SIGNAL(sendChangedeSignal(int)),
&ui, SLOT(updateNumber(int)));
ui.show();
//启动线程
t.start();
return a.exec();
}
解释下上面的实验代码:
GUI线程创建了ui,创建了一个线程t,ui和t同属于GUI线程。然后建立了ui–>t以及t–>ui的两个同线程的信号和槽链接。然后开始运行t线程。在线程t的 run方法中,创建了ob对象,然后建立了t–>ob以及ob–>t的跨线程的信号和槽链接,接着启动一个事件循环。程序主要做的就是:点击ui,发出信号,将信号转给同属于GUI线程的对象t的槽,t的槽又将信号发给属于非GUI线程的ob,ob记下信号值,然后将信号发给t,t又发给ui进行回显。这里从t–>ob以及从ob–>t的链接是跨线程的。
在TestOb的updateNumber槽上打上断点,运行程序,点击一下ui,程序陷入断点,得到如下调用图: