事件分发和事件过滤

图形界面应用程序的消息处理模型

Qt中的事件处理

(1)在Qt中,事件被封装成一个个对象,所有的事件均继承自抽象类QEvent.    

(2)Qt平台将系统产生的消息转变成Qt事件

(3)event()函数-事件分发

(4)GUI应用程序的事件处理流程

(5)事件(QEvent)和信号(SIGNAL)的不同

事件过滤

事件传递的过程

Qt中的事件过滤器

总结

Qt 的事件处理的五个层次



图形界面应用程序的消息处理模型

特点:

  • 基于操作系统才能运行
  • GUI应用程序提供的功能必须由用户触发
  • 用户操作界面时操作系统是第一个感知的 
  • 系统内核的消息通过事件处理转变成QT的信号

Qt中的事件处理

(1)在Qt中,事件被封装成一个个对象,所有的事件均继承自抽象类QEvent.    

事件处理的核心包括事件①产生、②分发、③接受和处理。

事件的产生

谁来产生事件? 最容易想到的是我们的输入设备,比如键盘、鼠标产生的keyPressEvent,keyReleaseEvent,mousePressEvent,mouseReleaseEvent事件(他们被封装成QMouseEvent和QKeyEvent)。

②Qt中事件的分发

对于non-GUI的Qt程序,是由QCoreApplication负责将QEvent分发给QObject的子类-Receiver. 
对于Qt GUI程序,由QApplication来负责

③事件的接受和处理

QObject类是整个Qt对象模型的心脏,事件处理机制是QObject三大职责(内存管理、内省(intropection)与事件处理制)之一。任何一个想要接受并处理事件的对象均须继承自QObject,可以选择重写QObject::event()函数或事件的处理权转给父类。

(2)Qt平台将系统产生的消息转变成Qt事件

  • Qt事件是一个QEvent(或子类)的对象。一个事件就是一个对象
  • 有时一个事件包含多个事件类型,比如鼠标事件又可.以分为鼠标按下双击、和移动多种操作
  • 事件类型由QEvent类的枚举型QEvent::Type来表示,可由帮助文档进行查询
  • Qt事件用于描述程序内部或外部发生的对应动作描述的是操作系统发生来的消息,一个系统消息对应着一个消息事件
  • 任意QObject对象都具备时间处理的能力

QEvent子类可以表示一个事件,但并不能处理这个事件

(3)event()函数-事件分发

Qt 程序需要在main()函数创建一个QApplication对象,然后调用它的exec()函数。这个函数就是开始 Qt 的事件循环。在执行exec()函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。如上所述,event()函数主要用于事件的分发。

所以,如果你希望在事件分发之前做一些操作,就可以重写这个event()函数了。例如,我们希望在一个QWidget组件中监听 tab 键的按下,那么就可以继承QWidget,并重写它的event()函数,来达到这个目的:

bool CustomWidget::event(QEvent *e)
{
    if (e->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
        if (keyEvent->key() == Qt::Key_Tab) {
            qDebug() << "You press tab.";
            return true;
        }
    }
    return QWidget::event(e);
}

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

  1. 如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件
  2. 在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播

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

bool CustomTextEdit::event(QEvent *e)
{
   if (e->type() == QEvent::KeyPress) 
{
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
       if (keyEvent->key() == Qt::Key_Tab) 
{
            qDebug() << "You press tab.";
            return true;
       }
    }
    return false;
}

 CustomTextEdit是QTextEdit的一个子类。我们重写了其event()函数,却没有调用父类的同名函数。这样,我们的组件就只能处理 Tab 键,再也无法输入任何文本,也不能响应其它事件,比如鼠标点击之后也不会有光标出现。这是因为我们只处理的KeyPress类型的事件,并且如果不是KeyPress事件,则直接返回 false,鼠标事件根本不会被转发,也就没有了鼠标事件。

通过查看QObject::event()的实现,我们可以理解,event()函数同前面的章节中我们所说的事件处理器有什么联系:

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

switch (event->type()) {
    case QEvent::MouseMove:
        mouseMoveEvent((QMouseEvent*)event);
        break;
    // ...
}

事实也的确如此。timerEvent()和mouseMoveEvent()这样的函数,就是我们前面章节所说的事件处理器 event handler。也就是说,event()函数中实际是通过事件处理器来响应一个具体的事件。这相当于event()函数将具体事件的处理“委托”给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此,我们重写了某一个事件处理器,即可让 Qt 调用我们自己实现的版本。

由此可以见,event()是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个event()函数,通过QEvent::type()判断不同的事件。鉴于重写event()函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了event()函数的另外一个作用:屏蔽掉某些不需要的事件处理器。正如我们前面的CustomTextEdit例子看到的那样,我们创建了一个只能响应 tab 键的组件。这种作用是重写事件处理器所不能实现的。

(4)GUI应用程序的事件处理流程

  1. Qt事件产生后会立即被分发到QWidget对象(QObject的子类,如按键QPushButton对象等)
  2. QWidget对象其内部会有一个event(QEVent*)函数被调用,进行事件处理
  3. event()根据事件类型调用不同的事件处理函数(默认的子函数)
  4. 在事件处理函数中发送Qt中预定义的信号
  5. 调用信号关联的槽函数

 QPushButton事件处理分析:

①接收到鼠标事件

②QApplication调用QObject::event(QEvent*)成员函数来处理,进行事件的分派。

③调用QPushButton的mouseReleaseEvent(QMouseEvent*)成员函数

④QPushButton调用click()成员函数

⑤触发信号SIGNAL(clicked())

(5)事件(QEvent)和信号(SIGNAL)的不同

先有事件,再有信号

事件(QEvent)

信号(SIGNAL)

与QObject的关系

由具体对象进行处理

由具体对象主动产生

对程序影响

改写事件处理函数可能导致程序行为发生改变

信号是否存在对应的槽函数不会改变程序行为

两者的联系

一般而言,信号在具体的事件处理函数中产生

(1)Qt中的事件和信号不同

(2)事件由QObject对象进行处理

(3)信号由QObject对象触发

(4)重写事件处理函数可能改变程序行为

(5)信号的触发不会对程序行为造成影响

(6)事件处理是在实际工程开发中应用非常普遍的

事件过滤

事件传递的过程

①源头:操作系统   操作系统检测到用户的动作时,就会产生一个系统消息,系统消息就会被发送到正在运行的Qt应用程序中,

②应用程序收到系统消息后, 他会将系统消息翻译成对应的QEvent事件对象,并调用QObject::event()将该对象分发下去,

③事件对象被分发到当前用户正在操作的窗口部件上去 ,该窗口部件是一个Qwidget的子类对象,该对象收到这个事件之后, 就会调用QWidget::event()函数来处理 ,该函数内部再调用其他的子函数(如KeyPressEvent或mouseReleaseEvent)进行具体的事件处理。

④可能会再被出传递到其父组件对象

(1)操作系统检测到用户动作时,会产生一条系统消息,该消息被发送到Qt应用程序

(2)Qt应用程序收到系统消息后,将其转化为一个对应的QEvent事件对象,并调用QObject::event()将其分发出去。

(3)事件对应被分发到当前正在操作的窗口部件上,该窗口部件会调用QWidget::event()函数来处理,然后,在这个函数内部又会调用其他的子函数(如KeyPressEvent或mouseReleaseEvent)来进行具体的处理。

(4)event函数处理完后,可能会将当前事件传递给父组件(parent)对象。但这个过程只是一种可能,也就是有一部分会被传递,有些并不需要被传递。

QEvent及其子类对象涉及的关键成员函数:实际上只是在操作或判断一个标志位

(1)void ignore():事件的接收者忽略当前事件,事件可能传递给父组件

(2)void accept();事件的接收者期望处理当前事件,表明我们自己写了事件处理函数,一般不需要被父组件再处理

(3)bool isAccepted();判断当前事件是否被处理

事件传递流程①事件先传递给指定窗口部件,确切的说传递给获得焦点窗口的部件,②如果该部件忽略该事件,那么这个事件就会传递给其父组件

还需要注意当重新实现事件处理函数时,一般需要调用父组件的相应事件处理函数来实现其默认的行为

我们已经知道,Qt 创建了QEvent事件对象之后,会调用QObject的event()函数处理事件的分发。显然,我们可以在event()函数中实现拦截的操作。由于event()函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个event()函数。这当然相当麻烦,更不用说重写event()函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。

Qt中的事件过滤器

(1)事件过滤器

  ①事件过滤器可以对其他组件接收到的事件进行监控

  ②任意的QObject对象都可以作为事件过滤器使用

  ③事件过滤器对象需要重写eventFilter()函数

(2)安装事件过滤器:installEventFilter()函数

  ①事件过滤器在组件之前接收到事件

  ②事件过滤器能够决定是否将事件转到到组件对象

QObject有一个eventFilter()函数,用于建立事件过滤器。函数原型如下:

virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );

这个函数正如其名字显示的那样,是一个“事件过滤器”。所谓事件过滤器,可以理解成一种过滤代码。事件过滤器会检查接收到的事件。如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让它继续转发,就返回 true,否则返回 false。事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched对象以及以后所有的事件过滤器根本不会知道这么一个事件。

class MainWindow : public QMainWindow
 {
 public:
     MainWindow();
 protected:
     bool eventFilter(QObject *obj, QEvent *event);
 private:
     QTextEdit *textEdit;
 };
 
 MainWindow::MainWindow()
 {
     textEdit = new QTextEdit;
     setCentralWidget(textEdit);
 
     textEdit->installEventFilter(this);
 }
 
 bool MainWindow::eventFilter(QObject *obj, QEvent *event)
 {
     if (obj == textEdit) {
         if (event->type() == QEvent::KeyPress) {
             QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
             qDebug() << "Ate key press" << keyEvent->key();
             return true;
         } else {
             return false;
         }
     } else {
         // pass the event on to the parent class
         return QMainWindow::eventFilter(obj, event);
     }
 }
  1. MainWindow是我们定义的一个类。我们重写了它的eventFilter()函数。为了过滤特定组件上的事件,首先需要判断这个对象是不是我们感兴趣的组件,然后判断这个事件的类型。在上面的代码中,我们不想让textEdit组件处理键盘按下的事件。所以,首先我们找到这个组件,如果这个事件是键盘事件,则直接返回 true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以返回 false。对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。
  2. eventFilter()函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用QObject::installEventFilter()函数。函数的原型如下
void QObject::installEventFilter ( QObject * filterObj )

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

我们可以向一个对象上面安装多个事件处理器,只要调用多次installEventFilter()函数。如果一个对象存在多个事件过滤器,那么,最后一个安装的会第一个执行,也就是后进先执行的顺序

事件过滤器的强大之处在于,我们可以为整个应用程序添加一个事件过滤器。记得,installEventFilter()函数是QObject的函数,QApplication或者QCoreApplication对象都是QObject的子类,因此,我们可以向QApplication或者QCoreApplication添加事件过滤器。这种全局的事件过滤器将会在所有其它特性对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况,否则的话我们不应该这么做。

注意,

事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。

总结

Qt 的事件是整个 Qt 框架的核心机制之一,也比较复杂。说它复杂,更多是因为它涉及到的函数众多,而处理方法也很多,有时候让人难以选择。现在我们简单总结一下 Qt 中的事件机制。

Qt 中有很多种事件:鼠标事件、键盘事件、大小改变的事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:

  • 所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择,其代表作就是 win32 API 的WndProc()函数:
LRESULT CALLBACK WndProc(HWND hWnd,

                         UINT message,

                         WPARAM wParam,

                         LPARAM lParam)

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

switch(message)

{

    case WM_PAINT:

        // ...

        break;

    case WM_DESTROY:

        // ...

        break;

    ...

}
  • 每一种事件对应一个事件处理函数。Qt 就是使用的这么一种机制:
    1. mouseEvent()
    2. keyPressEvent()

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

event()函数会有两个问题:

  1. event()函数是一个 protected 的函数,这意味着我们要想重写event(),必须继承一个已有的类。试想,我的程序根本不想要鼠标事件,程序中所有组件都不允许处理鼠标事件,是不是我得继承所有组件,一一重写其event()函数?protected 函数带来的另外一个问题是,如果我基于第三方库进行开发,而对方没有提供源代码,只有一个链接库,其它都是封装好的。我怎么去继承这种库中的组件呢?
  2. event()函数的确有一定的控制,不过有时候我的需求更严格一些:我希望那些组件根本看不到这种事件。event()函数虽然可以拦截,但其实也是接收到了QMouseEvent对象。我连让它收都收不到。这样做的好处是,模拟一种系统根本没有那个事件的效果,所以其它组件根本不会收到这个事件,也就无需修改自己的事件处理函数。这种需求怎么办呢?

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

事件过滤器可以解决刚刚我们提出的event()函数的两点不足:

  1. 首先,事件过滤器不是 protected 的,因此我们可以向任何QObject子类安装事件过滤器;
  2. 其次,事件过滤器在目标对象接收到事件之前进行处理,如果我们将事件过滤掉,目标对象根本不会见到这个事件。

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

virtual bool QCoreApplication::notify ( QObject * receiver,
                                        QEvent * event );

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

Qt 的事件处理的五个层次

现在我们可以总结一下 Qt 的事件处理,实际上是有五个层次:

  1. 重写paintEvent()、mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。
  2. 重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。
  3. 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。
  4. 在QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和notify()函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。
  5. 重写QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)。

源码演示:

重写一个MyBtn类,继承QPushButton

//"mybtn.h"
#ifndef MYBTN_H
#define MYBTN_H

#include <QPushButton>

class MyBtn : public QPushButton
{
    Q_OBJECT
public:
    MyBtn(QWidget *parent=0);
    void mousePressEvent(QMouseEvent *e);
    bool event(QEvent *e);

};

#endif // MYBTN_H
//"mybtn.cpp"
#include "mybtn.h"
#include<QDebug>
#include<QEvent>
#include<QMouseEvent>
MyBtn::MyBtn(QWidget *parent):QPushButton (parent)
{

}
void MyBtn::mousePressEvent(QMouseEvent *e)
{
    qDebug() << "3.按钮mousePressEvent事件处理,";
    //如果在此处自定义事件,而不将鼠标事件传递给父类,button将不能发送被点击信号
    QPushButton::mousePressEvent(e);
}

bool MyBtn::event(QEvent *e)
{
    //处理鼠标点击事件
    if (e->type() == QEvent::MouseButtonPress)
    {
        //强制转换失败?记得#include<QMouseEvent>
        QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(e);
        qDebug() << "2.MyBtn::event,event()事件分发";
        //return true;
    }
     return QPushButton::event(e);
}

 在Widget中添加自定义按钮

//widget.h
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include"mybtn.h"
class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();
    MyBtn *btn;
    bool event(QEvent *ev);
    bool eventFilter(QObject *watched, QEvent *event);
private slots:
    void on_pushButton_clicked();
private:

};
#endif // WIDGET_H
//"widget.cpp"
#include "widget.h"
#include<QDebug>
#include<QMouseEvent>

Widget::Widget(QWidget *parent)
    : QWidget(parent)   
{

    btn = new MyBtn(this);
    btn->setText ("按钮");
    btn->resize (200,150);
    connect (btn,&QPushButton::clicked,this,&Widget::on_pushButton_clicked );
    //给按钮安装过滤器
    //即让所有发往按钮的事件先由经由eventFilter过滤,这里委派Widget的EventFilter来过滤
    btn->installEventFilter (this);//在Widget上为pushButton安装过滤器
}

Widget::~Widget()
{

}

bool Widget::event(QEvent *e)
{
    //处理鼠标点击事件
    if (e->type() == QEvent::MouseButtonPress)
    {
        //强制转换失败?记得#include<QMouseEvent>
        QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(e);
        qDebug() << "Widget::event,MouseButtonPress";
        return true;
    }
    return QWidget::event(e);
}

bool Widget::eventFilter(QObject *watched, QEvent *event)
{
    //如果被监控的组件是pushButton
    if(watched == btn){
        //只过滤pushButton的点击事件
        if(event->type () == QEvent::MouseButtonPress){
            qDebug() << "1.在Widget中的安装在pushButton上的事件过滤器";
            //如果返回true,则按钮的event()将收不到点击事件
            //return true;
        }else{
            return false;
        }
    }
    //剩下的交给父类处理
    return QWidget::eventFilter (watched,event);
}
void Widget::on_pushButton_clicked()
{
    qDebug() << "4.槽函数on_pushButton_clicked接收到clicked信号";
}

运行项目,点击按钮:

输出结果:

1.在Widget中的安装在pushButton上的事件过滤器
2.MyBtn::event,event()事件分发
3.按钮mousePressEvent事件处理,
4.槽函数on_pushButton_clicked接收到clicked信号

参考链接:Qt 中的事件处理(一)Qt 中的事件处理(二)

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐