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

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

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

现在进入主题。

先说答案:

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

qt源码解析1--事件循环原理(重写事件函数,事件过滤器等)
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源码解析1--事件循环原理(重写事件函数,事件过滤器等)

熟悉了这个过程后,我们再来看几个例子练习一下: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

QGuiApplicationPrivate::processMouseEvent(static_cast

QGuiApplication::sendSpontaneousEvent(window, &ev);

notifyInternal2(receiver, event)

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

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 消息循环

另外一些参考博客:

Original: https://blog.csdn.net/kangkanglhb88008/article/details/127801104
Author: 我是标同学
Title: qt源码解析1–事件循环原理(重写事件函数,事件过滤器等)

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/656274/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球