{"id":"58954546","type":"page","status":"current","title":"9 Qt消息机制和事件","version":{"by":{"type":"known","username":"ethanliu","userKey":"2c90931b6cfb654f016d5c69becc0001","profilePicture":{"path":"/images/icons/profilepics/default.svg","width":48,"height":48,"isDefault":true},"displayName":"刘浩","_links":{"self":"https://wiki.suncaper.net/rest/api/user?key=2c90931b6cfb654f016d5c69becc0001"},"_expandable":{"status":""}},"when":"2022-07-09T01:51:26.000Z","message":"Migration of unmigrated content due to installation of a new plugin","number":2,"minorEdit":false,"hidden":false,"_links":{"self":"https://wiki.suncaper.net/rest/experimental/content/58954546/version/2"},"_expandable":{"content":"/rest/api/content/58954546"}},"body":{"storage":{"value":"

_Toc466556796_Toc4718420299.1 事件

事件(event)是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
在前面我们也曾经简单提到,Qt 程序需要在main()函数创建一个QApplication对象,然后调用它的exec()函数。这个函数就是开始 Qt 的事件循环。在执行exec()函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler),关于这一点,会在后边详细说明。
在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如

这些函数都是 protected virtual 的,也就是说,我们可以在子类中重新实现这些函数。下面来看一个例子:


cppsetText(QString(\"

Move: (%1, %2)\n

\").arg(QString::number(event->x()),\n QString::number(event->y())));\n}\n \nvoid EventLabel::mousePressEvent(QMouseEvent *event)\n{\n this->setText(QString(\"

Press:(%1, %2)\n

\").arg(QString::number(event->x()),\n QString::number(event->y())));\n}\n \nvoid EventLabel::mouseReleaseEvent(QMouseEvent *event)\n{\n QString msg;\n msg.sprintf(\"

Release: (%d, %d)

\",\n event->x(), event->y());\n this->setText(msg);\n}\n \nint main(int argc, char *argv[])\n{\n QApplication a(argc, argv);\n \n EventLabel *label = new EventLabel;\n label->setWindowTitle(\"MouseEvent Demo\");\n label->resize(300, 200);\n label->show();\n \n return a.exec();\n}\n\n]]>


运行上面的代码,当我们点击了一下鼠标之后,label 上将显示鼠标当前坐标值。

为什么要点击鼠标之后才能在mouseMoveEvent()函数中显示鼠标坐标值?
这是因为QWidget中有一个mouseTracking属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。如果mouseTracking是 false(默认即是),组件在至少一次鼠标点击之后,才能够被追踪,也就是能够发出mouseMoveEvent()事件。如果mouseTracking为 true,则mouseMoveEvent()直接可以被发出。
知道了这一点,我们就可以在main()函数中添加如下代码:

cppsetMouseTracking(true);]]>


在运行程序就没有这个问题了。

_Toc466556797_Toc4718420309.2 event()

事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。
如上所述,event()函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个event()函数了。例如,我们希望在一个QWidget组件中监听 tab 键的按下,那么就可以继承QWidget,并重写它的event()函数,来达到这个目的:

cpptype() == QEvent::KeyPress) {\nQKeyEvent *keyEvent = static_cast(e);\nif (keyEvent->key() == Qt::Key_Tab) {\nqDebug() << \"You press tab.\";\nreturn true;\n}\n}\nreturn QWidget::event(e);\n}]]>


CustomWidget是一个普通的QWidget子类。我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型。

我们可以通过使用QEvent::type()函数可以检查事件的实际类型,其返回值是QEvent::Type类型的枚举。我们处理过自己感兴趣的事件之后,可以直接返回 true,表示我们已经对此事件进行了处理;对于其它我们不关心的事件,则需要调用父类的event()函数继续转发,否则这个组件就只能处理我们定义的事件了。为了测试这一种情况,我们可以尝试下面的代码:

cpptype() == QEvent::KeyPress)\n{\nQKeyEvent *keyEvent = static_cast(e);\nif (keyEvent->key() == Qt::Key_Tab)\n{\nqDebug() << \"You press tab.\";\nreturn true;\n}\n}\nreturn false;\n}]]>


CustomTextEdit是QTextEdit的一个子类。我们重写了其event()函数,却没有调用父类的同名函数。这样,我们的组件就只能处理 Tab 键,再也无法输入任何文本,也不能响应其它事件,比如鼠标点击之后也不会有光标出现。这是因为我们只处理的KeyPress类型的事件,并且如果不是KeyPress事件,则直接返回 false,鼠标事件根本不会被转发,也就没有了鼠标事件。
通过查看QObject::event()的实现,我们可以理解,event()函数同前面的章节中我们所说的事件处理器有什么联系:

这是 Qt 5 中QObject::event()函数的源代码(Qt 4 的版本也是类似的)。我们可以看到,同前面我们所说的一样,Qt 也是使用QEvent::type()判断事件类型,然后调用了特定的事件处理器。比如,如果event->type()返回值是QEvent::Timer,则调用timerEvent()函数。可以想象,QWidget::event()中一定会有如下的代码:

cpptype()) {\ncase QEvent::MouseMove:\nmouseMoveEvent((QMouseEvent*)event);\nbreak;\n// ...\n}]]>


事实也的确如此。timerEvent()和mouseMoveEvent()这样的函数,就是我们前面章节所说的事件处理器 event handler。也就是说,event()函数中实际是通过事件处理器来响应一个具体的事件。这相当于event()函数将具体事件的处理"委托"给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此,我们重写了某一个事件处理器,即可让 Qt 调用我们自己实现的版本。
由此可以见,event()是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个event()函数,通过QEvent::type()判断不同的事件。鉴于重写event()函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了event()函数的另外一个作用:屏蔽掉某些不需要的事件处

cpptype()) {\ncase QEvent::Timer:\ntimerEvent((QTimerEvent*)e);\nbreak;\ncase QEvent::ChildAdded:\ncase QEvent::ChildPolished:\ncase QEvent::ChildRemoved:\nchildEvent((QChildEvent*)e);\nbreak;\n// ...\ndefault:\nif (e->type() >= QEvent::User) {\ncustomEvent(e);\nbreak;\n}\nreturn false;\n}\nreturn true;\n}]]>

理器。正如我们前面的CustomTextEdit例子看到的那样,我们创建了一个只能响应 tab 键的组件。这种作用是重写事件处理器所不能实现的。

_Toc466556798_Toc4718420319.3 事件过滤器

有时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,对话框可能想要拦截按键事件,不让别的组件接收到;或者要修改回车键的默认处理。
通过前面的章节,我们已经知道,Qt 创建了QEvent事件对象之后,会调用QObject的event()函数处理事件的分发。显然,我们可以在event()函数中实现拦截的操作。由于event()函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个event()函数。这当然相当麻烦,更不用说重写event()函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。
QObject有一个eventFilter()函数,用于建立事件过滤器。函数原型如下:
virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );
这个函数正如其名字显示的那样,是一个"事件过滤器"。所谓事件过滤器,可以理解成一种过滤代码。事件过滤器会检查接收到的事件。如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让它继续转发,就返回 true,否则返回 false。事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched对象以及以后所有的事件过滤器根本不会知道这么一个事件。
我们来看一段简单的代码:

cppinstallEventFilter(this);\n}\nbool MainWindow::eventFilter(QObject *obj, QEvent *event)\n{\nif (obj == textEdit) {\nif (event->type() == QEvent::KeyPress) {\nQKeyEvent *keyEvent = static_cast(event);\nqDebug() << \"Ate key press\" << keyEvent->key();\nreturn true;\n} else {\nreturn false;\n}\n} else {\n// pass the event on to the parent class\nreturn QMainWindow::eventFilter(obj, event);\n}\n}]]>


void QObject::installEventFilter ( QObject * filterObj )
这个函数接受一个QObject *类型的参数。记得刚刚我们说的,eventFilter()函数是QObject的一个成员函数,因此,任意QObject都可以作为事件过滤器(问题在于,如果你没有重写eventFilter()函数,这个事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过QObject::removeEventFilter()函数移除。


还记得我们前面的那个例子吗?我们使用event()函数处理了 Tab 键:

cpptype() == QEvent::KeyPress) {\nQKeyEvent *keyEvent = static_cast(e);\nif (keyEvent->key() == Qt::Key_Tab) {\nqDebug() << \"You press tab.\";\nreturn true;\n}\n}\nreturn QWidget::event(e);\n}\n现在,我们可以给出一个使用事件过滤器的版本:\nbool FilterObject::eventFilter(QObject *object, QEvent *event)\n{\nif (object == target && event->type() == QEvent::KeyPress)\n{\nQKeyEvent *keyEvent = static_cast(event);\nif (keyEvent->key() == Qt::Key_Tab) {\nqDebug() << \"You press tab.\";\nreturn true;\n} else {\nreturn false;\n}\n}\nreturn false;\n}]]>


事件过滤器的强大之处在于,我们可以为整个应用程序添加一个事件过滤器。记得,installEventFilter()函数是QObject的函数,QApplication或者QCoreApplication对象都是QObject的子类,因此,我们可以向QApplication或者QCoreApplication添加事件过滤器。这种全局的事件过滤器将会在所有其它特性对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况,否则的话我们不应该这么做。
注意,
事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。

_Toc466556799_Toc4718420329.4 总结

Qt 的事件是整个 Qt 框架的核心机制之一,也比较复杂。说它复杂,更多是因为它涉及到的函数众多,而处理方法也很多,有时候让人难以选择。现在我们简单总结一下 Qt 中的事件机制。
Qt 中有很多种事件:鼠标事件、键盘事件、大小改变的事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:

cpp


在这个函数中,我们需要使用switch语句,选择message参数的类型进行处理,典型代码是:

cpp


Qt 具有这么多种事件处理函数,肯定有一个地方对其进行分发,否则,Qt 怎么知道哪一种事件调用哪一个事件处理函数呢?这个分发的函数,就是event()。显然,当QMouseEvent产生之后,event()函数将其分发给mouseEvent()事件处理器进行处理。
event()函数会有两个问题:

这两个问题是event()函数无法处理的。于是,Qt 提供了另外一种解决方案:事件过滤器。事件过滤器给我们一种能力,让我们能够完全移除某种事件。事件过滤器可以安装到任意QObject类型上面,并且可以安装多个。如果要实现全局的事件过滤器,则可以安装到QApplication或者QCoreApplication上面。这里需要注意的是,如果使用installEventFilter()函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的eventFilter()函数进行过滤,其它对象不受影响。如果给QApplication对象安装事件过滤器,那么该过滤器对程序中的每一个对象都有效,任何对象的事件都是先传给eventFilter()函数。
事件过滤器可以解决刚刚我们提出的event()函数的两点不足:

事实上,还有一种方法,我们没有介绍。Qt 事件的调用最终都会追溯到QCoreApplication::notify()函数,因此,最大的控制权实际上是重写QCoreApplication::notify()。这个函数的声明是:

cpp


该函数会将event发送给receiver,也就是调用receiver->event(event),其返回值就是来自receiver的事件处理器。注意,这个函数为任意线程的任意对象的任意事件调用,因此,它不存在事件过滤器的线程的问题。不过我们并不推荐这么做,因为notify()函数只有一个,而事件过滤器要灵活得多。
现在我们可以总结一下 Qt 的事件处理,实际上是有五个层次:


","representation":"storage","_expandable":{"content":"/rest/api/content/58954546"}},"_expandable":{"editor":"","view":"","export_view":"","styled_view":"","anonymous_export_view":""}},"extensions":{"position":57},"_links":{"webui":"/pages/viewpage.action?pageId=58954546","edit":"/pages/resumedraft.action?draftId=58954546","tinyui":"/x/MpODAw","collection":"/rest/api/content","base":"https://wiki.suncaper.net","context":"","self":"https://wiki.suncaper.net/rest/api/content/58954546"},"_expandable":{"container":"/rest/api/space/2CT","metadata":"","operations":"","children":"/rest/api/content/58954546/child","restrictions":"/rest/api/content/58954546/restriction/byOperation","history":"/rest/api/content/58954546/history","ancestors":"","descendants":"/rest/api/content/58954546/descendant","space":"/rest/api/space/2CT"}}