qt源码解析1--事件循环原理(重写事件函数,事件过滤器等)

QT事件通过队列逐个处理,分为操作系统事件队列和QT事件队列。事件包含内容、线程号、接收者等信息,处理时由目标对象开始,未被接受则传递给父对象。事件默认接受状态为true,可通过setAccept()或ignore()改变。事件过滤器能拦截事件,优先处理。重载event()函数返回true并accept()可阻止事件继续传递。QEvent构造函数中事件默认为accept状态。了解事件循环有助于优化程序性能。

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

首先看我上篇博客准备好环境:

qt源码解析0--源码获取与环境准备


现在进入主题。

先说答案:

  • 事件包含事件内容,线程号,接收者,等各种信息,从我们经常写的event->pos就可以看出,还包括鼠标坐标等各种属性的。可以打开QEvent类的头文件看看就知道了。

    在这里插入图片描述
    QT事件循环原理_caicai_xiaobai的博客-CSDN博客
  • 事件在qt中存放在一个队列里(准确来说是2个,操作系统产生的事件在一个队列,qt自己产生出的事件在另一个队列 postEventList),事件是逐个处理的。而一个事件的处理会派发给目标对象,如果没有被目标对象的事件处理函数返回true,且accept,那么还会继续派发给目标对象的父对象,爷爷对象,太爷爷对象,一直下去……

    因为qt源码的代码是这也写的:

    eventAccepted = (w == receiver ? mouse : &me)->isAccepted();
    if (res && eventAccepted)
        break;
    w = w->parentWidget();  //不断寻找父窗口对象继续传递事件

    源码详情,见我下面的图,就清楚了。

  • 事件处理是异步的,也就是说你产生的事件(QWidget::update()函数,new出来一个paintEvent事件对象)会被QApplication::postEvent()放入Qt的消息队列队尾中,等待依次被处理。所以只有事件循环到这个事件的时候,期望的动作才会得到对应的执行。如果调用sendEvent()函数. 这时候事件不会放入队列, 而是直接被派发实现立即处理(qt直接调用QApplication::notify()了),此时就是同步的了。QWidget::repaint()函数用的就是这种方式。

  • 事件event默认是否已接收状态是不确定的,我们可以在处理函数里调用setAccept()或者ignore()函数来改变这个状态
  • 重写事件函数event()时,函数返回true,而且事件是接收状态,则事件不会继续往父对象传递了,进行队列里的下一个事件的处理,否则会传递给父对象。
    注:有些特定事件是否传递给父对象不依赖于返回值,而是依赖与这个接收者类型,以及属性。比如鼠标移动事件 QEvent::MouseMove
    if (w->isWindow() || w->testAttribute(Qt::WA_NoMousePropagation))
            break
  • 当我们重载event()函数时, 需要调用父类的event()函数来处理我们不需要处理或是不清楚如何处理的事件,实际开发中往往都是这样干的,而且不要随便去accept(),或者ignore()它,因为父类的这个函数自己知道应该怎么办,除非我们自己非常清楚这个事件的行为(比如我们自定义的事件,或者自己post给qt队列的事件)。

    下面这个例子演示了如何重载event()函数, 改变Tab键的默认动作: (默认的是键盘焦点移动到
    下一个控件上. )(注意返回值)
    //下面这个例子演示了如何重载event()函数, 改变Tab键的默认动作: (默认的是键盘焦点移动到下一个控件上. )
    bool CodeEditor::event(QEvent * event)
    {
        if (event->type() == QEvent::KeyPress)
        {
            QKeyEvent *keyEvent = (QKeyEvent *) event;
    
            if (keyEvent->key() == Key_Tab)
            {
                insertAtCurrentPosition('\t');
                return true;
            }
        }
        return QWidget::event(event);
    }
    我们来看看 QWidget 的event()函数是如何定义的:(所以可以注意一下事件的accept状态的对返回值的影响)因此,上面的代码其实应该在第2个 if 里再加一句,event->accept();,当然好像事件有默认值的,虽传入event()函数前事件状态没有指定,但是打开QEvent构造函数,可知,m_accept变量是初始化为true的,也就是默认状态是accept的,所以只要都没有调用accept()函数和ignore()函数,那么事件状态默认就一直是accept状态的。而作为所有组件的父类QWidget的默认实现则是调用了ignore(),即鼓励继续向父对象(父控件)传递,所以QPushButton等控件,都是自己重新实现了事件处理函数,且调用的是accept()函数,就是不想给按钮的父控件传递了,这个确实应该这样,因为比如我们点击了按钮,当然不希望mainwindow等父控件也去响应一下,而是仅仅按钮自己响应这个事件即可。QAbstractButton部分源码:
    void QAbstractButton::mouseReleaseEvent(QMouseEvent *e)
    {
        Q_D(QAbstractButton);
        
        xxxxxxxxxx....;
    
        if (e->button() != Qt::LeftButton) {
            e->ignore();
            return;
        }
    
        //检测mouseReleaseEvent事件发生时,坐标是否在按键区域内
        if (hitButton(e->pos())) {
            d->repeatTimer.stop();
            d->click();
            e->accept();//accept该事件,停止对事件往父对象(也就是父控件)转发
        } else {
            setDown(false);
            e->ignore();
        }
    


    所以:重写event()等函数是时,如果要忽略事件,需要调用QWidget的默认实现函数一下,否则就等于接受了事件。
     
    /*!
        Contructs an event object of type \a type.
    */
    QEvent::QEvent(Type type)
        : d(0), t(type), posted(false), spont(false), m_accept(true)
    {}
    bool QWidget::event(QEvent *event) 
    {
            switch (e->type()) {
            case QEvent::KeyPress:
                     keyPressEvent((QKeyEvent *)event);
                    if (!((QKeyEvent *)event)->isAccepted())
                            return false;
                    break;
            case QEvent::KeyRelease:
                    keyReleaseEvent((QKeyEvent *)event);
                    if (!((QKeyEvent *)event)->isAccepted())
                            return false;
                    break;
                    // more...
            }
            return true;
    }
  • 还能解释为什么一个最简单的qt窗口程序,也会有2个线程存在。c++ - Qt's default threads - Stack Overflow
  • qt是事件驱动型,我们可以看到,如果没有消息的输入(也就是qt就没有事件了),此时qt的事件队列就是空的,就不会有那个事件处理大循环了,因此qt就不运行了,正是这样的特性,所以qt的效率高,因为不吃cpu的性能。


     
  • 安装事件过滤器时,eventFilter(receiver, event))函数返回true,则目标对象的event()函数(这个就是目标对象的事件处理函数)得不到执行了。
  • 事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
  • 我们可以给qApp(每个程序中唯一的QApplication对象)装上过滤器,那么所有的事件在发往任何其他的过滤器时,都要先经过当前这个eventFilter()。这种行为会严重降低整个应用程序的事件分发效率,要看具体情况使用(比如debug时候,就有人这么干)。
  • 拿到一个对象的事件,有两种方案,继承这个类,重写这个类的事件函数event(),当然我们如果没有奇怪的需求,那么重写event()函数里面的已有的特定虚函数比如mousePressEvent()等即可。但是这个方案太重了。另一个方案是,另一个对象安装为这个对象的事件过滤器,这样那个对象就能优先拿到这个对象的事件,我们在那个对象的事件过滤器虚函数里进行操作事件即可,这个方案比较轻量化。

    比如 B->installEventFilter(A),那么此时A就能优先拿到即将派发给B的事件
    C->installEventFilter(A),一个对象可以给多个对象安装事件过滤器的。此时A就能优先拿到B和C的事件。
    同样, 一个对象能同时被安装多个过滤器, 在事件到达之后, 这些过滤器以安装次序的反序被调用。

    然后, A要重载QObject::eventFilter()函数, 在eventFilter() 中书写对事件进行处理的代码。
    用这种方法改写上面的例子: (假设我们将CodeEditor 放在MainWidget中)(注意返回值)
    MainWidget::MainWidget()
    {
        CodeEditor * ce = new CodeEditor( this, “code editor”);
        ce->installEventFilter( this );
    }
    
    bool MainWidget::eventFilter( QOject * target , QEvent * event )
    {
       if( target == ce )
       {
           if( event->type() == QEvent::KeyPress )
           {
                 QKeyEvent *ke = (QKeyEvent *) event;
                 if( ke->key() == Key_Tab )
                 {
                    ce->insertAtCurrentPosition('\t');
                    return true;
                 }
          }
       }
       return false;
    }

    值得参考博客:Qt Event-暗夜linux-ChinaUnix博客

 


qt事件循环和处理过程(我跟踪源码得到的)如下图所示:只需要好好看这个图,你就能明白一切了

(目前,我仍然还有个疑问,关于windows的消息编程的那几个windows的api函数,我还是不清楚消息是怎么传递的???莫非一个是界面消息(比如鼠标点击),一个是系统异步事件消息(比如socket)???不知道我画的这个箭头传递方向对不对(我调试发现过程好像是这样的,但是原理是啥我不知道),知道的记得评论区告诉我喔)

 

熟悉了这个过程后,我们再来看几个例子练习一下:QT-qevent 事件的accept()和ignore()_luckyone906的博客-CSDN博客_event->accept()


threadData:记录自己线程上的事件派发器(eventDispatcher)、事件队列、对象列表等信息。是独一份的,每个派发器,派发对象它们都有它的指针

QApplication::exec()

QGuiApplication::exec();

QCoreApplication::exec();

QEventLoop eventLoop;

eventLoop.exec();

eventLoop.processEvents(flags | WaitForMoreEvents | EventLoopExec);

threadData->eventDispatcher.load()->processEvents(flags);
//eventDispatcher是基类指针,子类化的有QEventDispatcherWin32、QEventDispatcherBlackberry、QEventDispatcherUNIX等
//这个是在函数QCoreApplicationPrivate::createEventDispatcher()里面根据平台宏定义来创建的

QEventDispatcherWin32::createInternalHwnd()
//创建一个windows系统的隐形窗口,用于接收windows系统所有派发事件

static HWND qt_create_internal_window(const QEventDispatcherWin32 *eventDispatcher)
//里面为它注册了一个叫做qt_internal_proc的WNDPROC函数

QEventDispatcherWin32::installMessageHook()
//注册系统钩子qt_GetMessageHook函数,截获操作系统系统所有事件(注意:这个钩子函数是在操作系统自己的线程执行的)

while(canWait)

反复查询PeekMessage(&msg, 0, 0, 0, PM_REMOVE)的消息(这个就是上面钩子函数截获的消息)

相应放入用户输入事件的队列 queuedUserInputEvents

相应放入用户输入事件的队列 queuedSocketEvents

canWait = (!retVal   && !d->interrupt  && (flags & QEventLoop::WaitForMoreEvents));
//如果消息处理完了,则阻塞自己,等到操作系统有消息发送过来,才会继续往下执行
MsgWaitForMultipleObjectsEx(nCount, pHandles, INFINITE, QS_ALLINPUT, MWMO_ALERTABLE | MWMO_INPUTAVAILABLE);

LRESULT QT_WIN_CALLBACK qt_GetMessageHook(int code, WPARAM wp, LPARAM lp)
//由于这个函数是在操作系统线程里执行的???所以要尽量的快

消息是PM_REMOVE,则
    PostMessage(d->internalHwnd, WM_QT_SENDPOSTEDEVENTS, 0, 0);
    //通过PostMessage()函数将事件发送到那个隐形窗口对来处理,对象窗口专门处理

LRESULT QT_WIN_CALLBACK qt_internal_proc(HWND hwnd, UINT message, WPARAM wp, LPARAM lp)
//处理上面说的隐形窗口的消息

QWindowsGuiEventDispatcher::sendPostedEvents()

QEventDispatcherWin32::sendPostedEvents();

QWindowSystemInterface::sendWindowSystemEvents(m_flags);

QGuiApplicationPrivate::processWindowSystemEvent(event);

QGuiApplicationPrivate::processWheelEvent(static_cast<QWindowSystemInterfacePrivate::WheelEvent *>(e));

QGuiApplicationPrivate::processMouseEvent(static_cast<QWindowSystemInterfacePrivate::MouseEvent *>(e));

QGuiApplication::sendSpontaneousEvent(window, &ev);

notifyInternal2(receiver, event)

QCoreApplication::notify(QObject *receiver, QEvent *event)
//这个是虚函数,会对应到子类的具体函数,一般是QApplication::notify
bool eventAccepted = mouse->isAccepted(); //也就是事件默认是接收状态
QPointer<QWidget> pw = w;

while (w) 

res = d->notify_helper(w, w == receiver ? mouse : &me);

 // 这里让事件过滤器先执行了
 if (sendThroughObjectEventFilters(receiver, e))
             return true;

 // 遍历安装到receiver的事件过滤器对象们,先让它们的eventFilter()函数执行
 for (int i = 0; i < receiver->d_func()->extraData->eventFilters.size(); ++i)            
    if (obj->eventFilter(receiver, event))   //这儿就是我们熟悉的事件过滤器函数了
        return true;

// deliver the event
bool consumed = receiver->event(e);    //这儿就是我们熟悉的可以重写的事件处理函数了

//这里以继承Qobject的Qwidget为例:
bool QWidget::event(QEvent *event)
    switch (event->type()) {
    case QEvent::MouseMove:
        mouseMoveEvent((QMouseEvent*)event);
        break;

    case QEvent::MouseButtonPress:
        mousePressEvent((QMouseEvent*)event);
        break;

。。。

QCoreApplicationPrivate::setEventSpontaneous(e, false); 
return consumed;

eventAccepted = (w == receiver ? mouse : &me)->isAccepted();
if (res && eventAccepted)
    break;
w = w->parentWidget();  //不断寻找父窗口对象

参考博客:

Qt源码学习笔记系列之事件循环(一) - 知乎

【QT】深入了解QT消息循环及线程相关性_伐尘的博客-CSDN博客_qt 消息循环

另外一些参考博客:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值