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


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

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


      所以:重写event()等函数是时,如果要忽略事件,需要调用QWidget的默认实现函数一下,否则就等于接受了事件。
       
      1. /*!
      2. Contructs an event object of type \a type.
      3. */
      4. QEvent::QEvent(Type type)
      5. : d(0), t(type), posted(false), spont(false), m_accept(true)
      6. {}
      1. bool QWidget::event(QEvent *event)
      2. {
      3. switch (e->type()) {
      4. case QEvent::KeyPress:
      5. keyPressEvent((QKeyEvent *)event);
      6. if (!((QKeyEvent *)event)->isAccepted())
      7. return false;
      8. break;
      9. case QEvent::KeyRelease:
      10. keyReleaseEvent((QKeyEvent *)event);
      11. if (!((QKeyEvent *)event)->isAccepted())
      12. return false;
      13. break;
      14. // more...
      15. }
      16. return true;
      17. }
    • 还能解释为什么一个最简单的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中)(注意返回值)
      1. MainWidget::MainWidget()
      2. {
      3. CodeEditor * ce = new CodeEditor( this, “code editor”);
      4. ce->installEventFilter( this );
      5. }
      6. bool MainWidget::eventFilter( QOject * target , QEvent * event )
      7. {
      8. if( target == ce )
      9. {
      10. if( event->type() == QEvent::KeyPress )
      11. {
      12. QKeyEvent *ke = (QKeyEvent *) event;
      13. if( ke->key() == Key_Tab )
      14. {
      15. ce->insertAtCurrentPosition('\t');
      16. return true;
      17. }
      18. }
      19. }
      20. return false;
      21. }

      值得参考博客: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(e));

    QGuiApplicationPrivate::processMouseEvent(static_cast(e));

    QGuiApplication::sendSpontaneousEvent(window, &ev);

    notifyInternal2(receiver, event)

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

    另外一些参考博客:

  • 相关阅读:
    微服务 | Springboot整合Seata+Nacos实现分布式事务
    快手视频批量下载.py(10月可以用)
    一条通往服务器所有端口的隧道
    axios跨域请求设置并携带Cookies
    C/C++图书信息管理系统水电管理信息系统
    【云原生 | 27】Docker部署运行开源消息队列实现RabbitMQ
    多目标量子粒子群优化的经济排放调度问题(Matlab代码实现)
    Opengl ES之FBO
    【工具使用】怎么设置SSH隧道(Port Forwarding)
    如何将等保2.0的要求融入日常安全运维实践中?
  • 原文地址:https://blog.csdn.net/kangkanglhb88008/article/details/127801104