QT开发(六十三)——QT事件机制分析
一、事件机制
事件是由系统或者QT平台本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
事件的出现,使得程序代码不会按照原始的线性顺序执行。线性顺序的程序设计风格不适合处理复杂的用户交互,如用户交互过程中,用户点击“打开文件”将开始执行打开文件的操作,用户点击“保存文件”将开始执行保存文件的操作。用户交互过程中进行什么样的操作是由用户决定的,程序设计时无法事先预测,同时用户的操作都会发出相应的事件,因此用户交互的程序设计中程序的执行顺序不再是线性的,而是由一个个事件驱动着程序继续执行,如果没有事件,程序将阻塞,不执行任何代码。
QT中,使用QT组件时,通常不会把主要精力放在事件上。更多是关心事件关联的信号。比如,对于QPushButton的鼠标点击,不需要关心这个鼠标点击事件,而是关心clicked()信号的发出。但QT中的事件和信号槽却并不是可以相互替代的。信号由具体的对象发出,信号一旦发出,然后会马上交给由connect()函数连接的槽进行处理;而对于事件,QT使用一个事件队列对所有发出的事件进行维护,当新的事件产生时,会被追加到事件队列的尾部,前一个事件完成后,取出后面的事件进行处理,并且QT的事件也可以不进入事件队列,而是直接处理。事件则可以使用“事件过滤器”进行过滤,对于有些事件进行额外的处理,其它的事件则不关心。
通常,如果使用组件,关心的是信号槽;如果自定义组件,关心的是事件。因为可以通过事件来改变组件的默认操作。比如,如果要自定义一个能够响应鼠标事件的EventLabel,就需要重写QLabel的鼠标事件,做出期望的操作,有可能还得在恰当的时候发出一个类似按钮的clicked()信号(如果期望让EventLabel能够被其它组件使用)或者其它的信号。
QT程序需要在main()函数创建一个QCoreApplication对象,然后调用exec()函数。exec()函数就是开始QT的事件循环。执行exec()函数后,程序将进入事件循环来监听应用程序的事件。当事件发生时,QT将创建一个事件对象。QT中所有事件类都继承于QEvent。在事件对象创建完毕后,QT将创建的事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler)。
在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如keyPressEvent()、keyReleaseEvent()、mouseDoubleClickEvent()、mouseMoveEvent()、mousePressEvent()、mouseReleaseEvent()等。如果要在自定义组件中对事件进行处理,需要在子类中重新实现事件处理函数。
QWidget组件有一个mouseTracking属性,用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。如果mouseTracking是 false(默认),QWidget组件在至少一次鼠标点击后才能够被追踪,即在鼠标点击后才能够发出mouseMoveEvent()事件。如果设置mouseTracking为true,则mouseMoveEvent()直接可以被发出。
二、事件处理
1、自定义类的事件处理函数
事件处理包括事件接受和忽略。
实例代码:
CustomButton.h文件:
#ifndef CUSTOMBUTTON_H #define CUSTOMBUTTON_H #include <QPushButton> #include <QDebug> class CustomButton : public QPushButton { public: CustomButton(QWidget* parent = 0); protected: void mousePressEvent(QMouseEvent *event); private slots: void onButton(); }; #endif // CUSTOMBUTTON_H
CustomButton.cpp文件:
#include "CustomButton.h" #include <QMouseEvent> CustomButton::CustomButton(QWidget* parent):QPushButton(parent) { connect(this, &CustomButton::clicked, this, &CustomButton::onButton); } void CustomButton::onButton() { qDebug() << "child clicked"; } void CustomButton::mousePressEvent(QMouseEvent *event) { if(event->button() == Qt::LeftButton) { qDebug() << "Left press"; } else { QPushButton::mousePressEvent(event); } }
Mainc.cpp文件:
#include "CustomButton.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); CustomButton button; button.setText("button"); button.show(); return a.exec(); }
自定义类CustomButton继承自QPushButton,重写了mousePressEvent()函数,即鼠标按下事件处理函数。mousePressEvent()函数中,如果鼠标按下的是左键,则打印“Left press”字符串,否则,调用父类的同名函数。槽函数onButton()打印“child clicked”。
编译运行程序,鼠标点击按钮后打印出“Left press”字符串,没有打印出“child clicked”字符串。
原因在于自定义类CustomButton的事件处理函数mousePressEvent()覆盖了父类QPushButton相应的事件处理函数。父类QPushButton的mousePressEvent()事件处理函数会发出clicked()信号,自定义类CustomButton实现则不发出clicked()信号。因此,自定义类CustomButton对象在鼠标按下时没有发出clicked()信号,连接的槽函数onButton()不会执行。
自定义类中重写父类的事件处理函数时会覆盖父类相应的事件处理函数,父类的事件处理函数中包含的操作(如发出某些信号)也将会被覆盖。因此当重写事件回调函数时,必须注意是否需要通过调用父类的同名函数来确保原有实现仍能进行。
通过调用父类的同名函数,可以把QT的事件传递看成链状:如果子类没有处理这个事件,就会继续向其父类传递。Qt 的事件对象有两个函数:accept()和ignore()。accept()通知QT平台,事件处理函数要处理这个事件;ignore()则通知QT平台,事件处理函数不处理这个事件。在事件处理函数中,可以使用isAccepted()来查询某个事件是不是已经被接受。
如果一个事件处理函数调用了一个事件对象的accept()函数,这个事件就不会被继续传播给其父组件;如果事件对象调用了事件的ignore()函数,QT平台会从其父组件中寻找另外的接受者。
通常很少会使用accept()和ignore()函数,如果希望忽略事件,只要调用父类的相应事件处理函数即可。由于无法确认父类中相应的事件处理函数有没有额外的操作,如果在子类中直接使用ignore()函数忽略事件,QT会去寻找其他的接受者,父类的操作会被忽略(因为没有调用父类的同名函数),这可能会有潜在的危险。为了避免子类去调用accept()和ignore()函数,而是尽量调用父类实现,QT做了特殊的设计:事件对象默认是accept的,而作为所有组件的父类QWidget的默认实现则是调用ignore()。因此,如果子类重新实现事件处理函数,不调用QWidget的默认实现,就等于接受事件;如果要忽略事件,只需调用QWidget的默认事件处理函数实现。
QT5中QWidget的mousePressEvent()函数的源码实现如下:
void QWidget::mousePressEvent(QMouseEvent *event) { event->ignore(); if ((windowType() == Qt::Popup)) { event->accept(); QWidget* w; while ((w = QApplication::activePopupWidget()) && w != this) { w->close(); if (QApplication::activePopupWidget() == w) w->hide(); // hide at least } if (!rect().contains(event->pos())) { close(); } } }
如果子类没有重写mousePressEvent()函数,QT会默认忽略这个事件,继续寻找下一个事件接收者。如果在子类的mousePressEvent()函数中直接调用了accept()或者ignore(),而没有调用父类相应的事件处理函数,QWidget::mousePressEvent()函数中关于Popup判断的代码就不会被执行,因此可能会出现莫名其妙的怪异现象。
2、事件的传播
CustomButton.h文件:
#ifndef CUSTOMBUTTON_H #define CUSTOMBUTTON_H #include <QPushButton> class CustomButton : public QPushButton { Q_OBJECT public: CustomButton(QWidget* parent = 0); protected: void mousePressEvent(QMouseEvent *event); }; #endif // CUSTOMBUTTON_H
CustomButton.cpp文件:
#include "CustomButton.h" #include <QMouseEvent> #include <QDebug> CustomButton::CustomButton(QWidget* parent):QPushButton(parent) { } void CustomButton::mousePressEvent(QMouseEvent *event) { event->ignore(); qDebug() << "CustomButton"; }
CustomButtonEx.h文件:
#ifndef CUSTOMBUTTONEX_H #define CUSTOMBUTTONEX_H #include "CustomButton.h" class CustomButtonEx : public CustomButton { Q_OBJECT public: CustomButtonEx(QWidget* parent = 0); protected: void mousePressEvent(QMouseEvent *event); }; #endif // CUSTOMBUTTONEX_H
CustomButtonEx.cpp文件:
#include "CustomButtonEx.h" #include "CustomWidget.h" #include <QMouseEvent> #include <QDebug> CustomButtonEx::CustomButtonEx(QWidget* parent):CustomButton(parent) { } void CustomButtonEx::mousePressEvent(QMouseEvent *event) { event->ignore(); qDebug() << "CustomButtonEx"; }
CustomWidget.h文件:
#ifndef CUSTOMWIDGET_H #define CUSTOMWIDGET_H #include <QWidget> class CustomWidget : public QWidget { Q_OBJECT public: explicit CustomWidget(QWidget *parent = 0); protected: void mousePressEvent(QMouseEvent *event); }; #endif // CUSTOMWIDGET_H
CustomWidget.cpp文件:
#include "CustomWidget.h" #include <QMouseEvent> #include <QDebug> CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) { } void CustomWidget::mousePressEvent(QMouseEvent *event) { qDebug() << "CustomWidget"; }
Main.cpp文件:
#include "CustomButton.h" #include "CustomButtonEx.h" #include "CustomWidget.h" #include <QApplication> #include <QHBoxLayout> int main(int argc, char *argv[]) { QApplication a(argc, argv); CustomWidget* customWidget = new CustomWidget(); CustomButton* custombutton = new CustomButton(customWidget); custombutton->setText("CustomBuuton"); CustomButtonEx* custombuttonex = new CustomButtonEx(customWidget); custombuttonex->setText("CustomButtonEx"); QHBoxLayout* layout = new QHBoxLayout(customWidget); layout->addWidget(custombutton); layout->addWidget(custombuttonex); customWidget->setLayout(layout); customWidget->show(); return a.exec(); }
在自定义组件CustomWidget内放置两个按钮对象:CustomButton和CustomButtonEx,每一个类都重写了mousePressEvent()函数。
如果在CustomButtonEx的mousePressEvent()中调用event->accept(),鼠标按下事件将不会传播到父组件CustomWidget,只会打印出“CustomButtonEx”;如果CustomButtonEx的mousePressEvent()中调用event->ignore(),鼠标按下事件会继续传播到父组件CustomWidget,会打印出“CustomButtonEx”、“CustomWidget”。由于CustomWidget的mousePressEvent()函数中并未调用event->accept(),因此鼠标按下事件传播到CustomWidget组件为止。
CustomButtonEx的事件传播给了父组件CustomWidget,而不是父类CustomButton。事件的传播是在组件层次上面的,而不是依靠类继承机制。
在窗口关闭事件处理函数closeEvent必须使用accept()和ignore()函数。对于窗口关闭QCloseEvent事件,调用accept()意味着QT会停止事件的传播,窗口关闭;调用ignore()则意味着事件继续传播,即阻止窗口关闭。
void CustomWidget::closeEvent(QCloseEvent *event) { bool exit = QMessageBox::question(this, tr("Quit"), tr("Are you sure to quit this application?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes; if (exit) { event->accept(); } else { event->ignore(); } }
3、事件分发
事件对象创建完毕后,QT将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型分发给不同的事件处理器(event handler)。
event()函数主要用于事件的分发。如果期望在事件分发前做一些操作,可以重写event()函数。
在QWidget组件中监听某个按键按下事件实现如下:
bool CustomWidget::event(QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); if (keyEvent->key() == Qt::Key_K) { qDebug() << "You press K."; return true; } } return QWidget::event(event); }
CustomWidget继承自QWidget,重写了event()函数,event()函数有一个QEvent对象作为参数,是需要转发的事件对象。函数返回值是 bool类型。如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,并且事件对象设置了accept(),那么QT会认为这个事件已经处理完毕,不会再将这个事件发送给其它组件,而是会继续处理事件队列中的下一事件。注意,在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播。
通过使用QEvent::type()函数可以检查事件的实际类型,其返回值是QEvent::Type类型的枚举。处理完自己感兴趣的事件后,可以直接返回 true,表示已经对此事件进行了处理;对于其它不关心的事件,则需要调用父类的event()函数继续转发,否则自定义组件就只能处理我们感兴趣的事件,其它事件将被丢弃。
QT5 中QObject::event()函数的源代码如下:
bool QObject::event(QEvent *e) { switch (e->type()) { case QEvent::Timer: timerEvent((QTimerEvent*)e); break; case QEvent::ChildAdded: case QEvent::ChildPolished: case QEvent::ChildRemoved: childEvent((QChildEvent*)e); break; // ... default: if (e->type() >= QEvent::User) { customEvent(e); break; } return false; } return true; }
QT使用QEvent::type()判断事件类型,调用特定的事件处理器实现事件的分发。比如,如果event->type()返回值是QEvent::Timer,则调用timerEvent()函数。
4、事件过滤器
QT创建了QEvent事件对象后,会调用QObject的event()函数处理事件的分发。虽然可以在event()函数中实现拦截的操作,但event()函数有两大缺点。
A、event()函数是 protected的,需要继承已有类。如果组件很多,就需要重写很多个event()函数。
B、event()函数虽然可以拦截事件,但其实组件是接收到了事件的。
为了解决event()函数拦截事件的缺陷,QT提供了另外一种方式来实现对事件的拦截:事件过滤器。
QObject有一个eventFilter()函数,用于建立事件过滤器。
virtual bool QObject::eventFilter(QObject * watched, QEvent * event);
事件过滤器会检查接收到的事件。如果这个事件是感兴趣的类型,就进行处理;如果不是,就继续转发。eventFilter函数返回一个bool类型,如果想将参数event过滤出来,比如不想让它继续转发,就返回true,否则返回false。事件过滤器的调用时间是目标对象(watched对象)接收到事件对象前。如果在事件过滤器中停止了某个事件,那么watched对象以及以后所有的事件过滤器都不会知道这个事件。
bool CustomWidget::eventFilter(QObject *watched, QEvent *event) { if (watched == this && event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); if (keyEvent->key() == Qt::Key_K) { qDebug() << "eventFilter: You press K."; return true; } else { return false; } } return false; }
自定义类CustomWidget重写了eventFilter()函数。为了过滤特定组件上的事件,首先需要判断这个组件对象是不是感兴趣的组件,然后判断要过滤事件的类型。如果是要过滤的事件则直接返回true,即过滤掉这个事件,其他事件还是要继续处理,所以返回false。对于其它的组件,由于不保证是不是还有过滤器,最保险的办法是调用父类的eventFilter()函数,保证父对象上面设置的事件过滤器可以被调用。
eventFilter()函数相当于创建了过滤器,要使过滤器生效需要安装过滤器。安装过滤器需要调用QObject::installEventFilter()函数。
void QObject::installEventFilter (QObject * filterObj);
filterObj是过滤器对象,即事件过滤器所属的类对象。
CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) { this->installEventFilter(this); }
创建事件过滤器并安装事件过滤器后,在CustomWidget按下鼠标时,鼠标事件将会被过滤,因此会打印出“eventFilter: You press K.”字符串,mousePressEvent()函数将不会被调用,不会打印出“CustomWidget”字符串。
eventFilter()函数是QObject的一个成员函数,因此,任意QObject都可以作为事件过滤器(如果没有重写eventFilter()函数,事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过QObject::removeEventFilter()函数移除。
可以向一个对象上面安装多个事件处理器,只要调用多次installEventFilter()函数。如果一个对象存在多个事件过滤器,那么,最后一个安装的会第一个执行,也就是后进先执行的顺序。
事件过滤器的强大之处在于可以为整个应用程序添加一个事件过滤器。installEventFilter()函数是QObject的函数,QApplication或者QCoreApplication对象都是QObject的子类,因此,可以向QApplication或者QCoreApplication添加事件过滤器。
如果使用installEventFilter()函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的eventFilter()函数进行过滤,其它对象不受影响。如果给QApplication对象安装事件过滤器,那么该过滤器对程序中的每一个对象都有效,任何对象的事件都是先传给eventFilter()函数。这种全局的事件过滤器将会在所有其它特性对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况,否则的话不应该这么做。
注意,如果在事件过滤器中delete了某个接收组件,务必将函数返回值设为 true。否则,QT还是会将事件分发给这个接收组件,从而导致程序崩溃。
事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。如果在安装过滤器之后,两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。QT中,对象创建之后可以使用moveToThread()函数将一个对象移动到另外的线程。
5、事件处理总结
QT事件处理的层次:
A、重写paintEvent()、mousePressEvent()等事件处理函数,是最普通、最简单的形式,同时功能也最简单。
B、重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。
C、在特定对象上面安装事件过滤器,事件过滤器仅过滤该对象接收到的事件。
D、在QCoreApplication::instance()上安装事件过滤器。事件过滤器将过滤所有对象的所有事件。全局的事件过滤器可以看到disabled组件上面发出的鼠标事件。全局过滤器只能用在主线程。
E、重写QCoreApplication::notify()函数。与全局事件过滤器一样,提供完全控制,并且不受线程的限制。但全局范围内只能有一个被使用(因为QCoreApplication是单例的)。
CustomWidget.h文件:
#ifndef CUSTOMWIDGET_H #define CUSTOMWIDGET_H #include <QWidget> class CustomWidget : public QWidget { Q_OBJECT public: explicit CustomWidget(QWidget *parent = 0); protected: void mousePressEvent(QMouseEvent *event); void closeEvent(QCloseEvent *event); bool event(QEvent *event); bool eventFilter(QObject *watched, QEvent *event); }; #endif // CUSTOMWIDGET_H
CustomWidget.cpp文件:
#include "CustomWidget.h" #include <QMouseEvent> #include <QDebug> CustomWidget::CustomWidget(QWidget *parent) : QWidget(parent) { this->installEventFilter(this); } void CustomWidget::mousePressEvent(QMouseEvent *event) { qDebug() << "CustomWidget::mousePressEvent"; } bool CustomWidget::event(QEvent *event) { if (event->type() == QEvent::MouseButtonPress) { qDebug() << "CustomWidget::event"; } return QWidget::event(event); } bool CustomWidget::eventFilter(QObject *watched, QEvent *event) { if (event->type() == QEvent::MouseButtonPress) { qDebug() << "CustomWidget::eventFilter"; } return false; }
Main.cpp文件:
#include "CustomWidget.h" #include <QApplication> #include <QDebug> class EventFilter : public QObject { public: EventFilter(QObject *watched, QObject *parent = 0) : QObject(parent), m_watched(watched) { } bool eventFilter(QObject *watched, QEvent *event) { if (watched == m_watched) { if (event->type() == QEvent::MouseButtonPress) { qDebug() << "QApplication::eventFilter"; } } return false; } private: QObject *m_watched; }; int main(int argc, char *argv[]) { QApplication a(argc, argv); CustomWidget customWidget; a.installEventFilter(new EventFilter(&customWidget, &customWidget)); customWidget.show(); return a.exec(); }
按下鼠标键后,打印出字符串:
QApplication::eventFilter
CustomWidget::eventFilter
CustomWidget::event
CustomWidget::mousePressEvent
全局事件过滤器被第一个调用,组件对象上的事件过滤器第二个被调用,event()函数第三个调用,特定的事件处理函数第四个调用。
三、自定义事件
事件的分发既可以是同步的,又可以是异步的,而信号槽的回调总是同步的。并且事件可以使用过滤器。
1、自定义事件的类型
QT自定义事件需要继承QEvent。QEvent提供一个QEvent::Type类型的参数,作为自定义事件的类型值。
QEvent::Type是QEvent定义的一个枚举。需要注意的是自定义事件类型不能和已经存在的type值重复,否则会有不可预料的错误发生,因为系统会将新增加的自定义事件当做系统事件进行派发和调用。QT中,系统保留0 – 999的值,自定义事件的type要大于 999。QT定义了两个边界值:QEvent::User和QEvent::MaxUser,自定义事件的type应该在两个值的范围之间。其中,QEvent::User的值是1000,QEvent::MaxUser的值是65535。通过这两个枚举值,可以保证自定义的事件类型不会覆盖系统定义的事件类型。但并不能保证自定义事件相互之间不会被覆盖。为了避免自定义事件间的相互覆盖,QT提供了一个函数:registerEventType(),用于自定义事件的注册。
static int QEvent::registerEventType ( int hint = -1 );
registerEventType函数是static的,可以使用QEvent类直接调用。函数返回值是向系统注册的新的Type类型的值。如果hint是合法的,即hint不会发生任何覆盖(系统的以及其它自定义事件的),则会直接返回这个值;否则,系统会自动分配一个合法值并返回。使用registerEventType函数即可完成type 值的指定。registerEventType函数是线程安全的,不必另外添加同步。
2、事件的发送方式
可以在自定义事件中添加所需要的数据,然后进行事件的发送。
QT提供了两种事件发送方式:
A、非阻塞式发送
static bool QCoreApplication::sendEvent(QObject *receiver,QEvent *event);
直接将event事件发送给receiver接收者,使用的是QCoreApplication::notify()函数。函数返回值就是事件处理函数的返回值。在事件被发送的时候,event对象并不会被销毁。通常会在栈上创建event对象,例如:
QMouseEvent event(QEvent::MouseButtonPress, pos, 0, 0, 0);
QApplication::sendEvent(receiver, &event);
B、阻塞式发送
static void QCoreApplication::postEvent(QObject *receiver,QEvent *event);
将event事件及其接收者receiver一同追加到事件队列中,函数立即返回。
因为post事件队列会持有事件对象,并且在其post的时候将其delete掉,因此,必须在堆上创建event对象。当对象被发送之后,再试图访问event对象就会出现问题(因为post后,event对象就会被delete)。
当控制权返回到主线程循环时,保存在事件队列中的所有事件都通过notify()函数发送出去。
事件会根据post的顺序进行处理。如果想要改变事件的处理顺序,可以考虑为其指定一个优先级。默认的优先级是Qt::NormalEventPriority。
postEvent函数是线程安全的。
static void QCoreApplication::sendPostedEvents(QObject *receiver,int event_type);
sendPostedEvents函数的作用是将事件队列中的接收者为receiver,事件类似为event_type的所有事件立即发送给receiver进行处理。需要注意的是,来自窗口系统的事件并不由sendPostedEvents函数进行处理,而是processEvent()。
3、自定义事件处理函数
自定义事件的处理既可以定义一个自定义事件处理函数,也可以在event()函数中直接处理。
void CustomWidget::customEvent(QEvent *event) { CustomEvent *customEvent = static_cast<CustomEvent *>(event); // ... } bool CustomWidget::event(QEvent *event) { if (event->type() == CustomEventType) { CustomEvent *myEvent = static_cast<CustomEvent *>(event); // processing... return true; } return QWidget::event(event); }