Qt事件机制概览

Qt事件机制基于消息循环,包括QEventLoop的创建与执行,涉及线程间的信号槽异步调用。QEventLoop在GUI线程中用于处理窗口事件,非GUI线程中用于跨线程事件处理。窗口过程和事件派发器是关键,其中QWindowsGuiEventDispatcher在GUI线程创建时伴随message-only窗口一起生成,而在非GUI线程中延迟创建。模态窗口与异步槽调用机制保证了线程安全。通过QApplication的notify方法实现事件分发,经过过滤器和对象的event处理函数进行处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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,程序陷入断点,得到如下调用图:

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值