首页 Qt 学习之路 2 Qt 学习之路 2(21):事件过滤器

Qt 学习之路 2(21):事件过滤器

53 4.7K

有时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,对话框可能想要拦截按键事件,不让别的组件接收到;或者要修改回车键的默认处理。

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

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);
     }
 }

MainWindow是我们定义的一个类。我们重写了它的eventFilter()函数。为了过滤特定组件上的事件,首先需要判断这个对象是不是我们感兴趣的组件,然后判断这个事件的类型。在上面的代码中,我们不想让textEdit组件处理键盘按下的事件。所以,首先我们找到这个组件,如果这个事件是键盘事件,则直接返回 true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以返回 false。对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。

eventFilter()函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用QObject::installEventFilter()函数。这个函数的签名如下:

void QObject::installEventFilter ( QObject * filterObj )

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

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

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

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);
}

现在,我们可以给出一个使用事件过滤器的版本:

bool FilterObject::eventFilter(QObject *object, QEvent *event)
{
    if (object == target && event->type() == QEvent::KeyPress) {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        if (keyEvent->key() == Qt::Key_Tab) {
            qDebug() << "You press tab.";
            return true;
        } else {
            return false;
        }
    }
    return false;
}

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

注意,如果你在事件过滤器中 delete 了某个接收组件,务必将函数返回值设为 true。否则,Qt 还是会将事件分发给这个接收组件,从而导致程序崩溃。

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

53 评论

chsin 2013年1月1日 - 16:53

“事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。” 对此不甚理解, 希望豆子能举个例子

回复
DevBean 2013年1月4日 - 10:56

这句话的意思是,事件过滤器和安装过滤器的组件必须在同一线程。Qt 里面,对象创建之后,可以使用 moveToThread() 函数将一个对象移动到另外的线程。在这种情形下(当然,事件过滤器必须在同一线程时才能被正确安装,这是第一句话说明的),在它们分属在不同线程时,事件过滤器也是不起作用的,只用当它们重新回到同一线程(使用 moveToThread() 或者是线程自然结束)时,过滤器才能重新工作。

回复
longchisihai 2013年1月4日 - 16:42

第一个例子 类开头忘记加Q_OBJECT?

回复
DevBean 2013年1月4日 - 17:06

这个因为没有用到 Q_OBJECT 宏的相关内容,所以没有加上。不过在正式开发中,所有 QObject 类的子类都应该添加 Q_OBJECT 宏。

回复
Jakes 2013年4月29日 - 16:23

过滤器的代码中object == target target代表什么意思?

回复
豆子 2013年5月2日 - 10:54

object 是发生事件的对象,target 是你关注的对象。也就是说,当发出事件的是 target 的时候,才会进入判断分支。

回复
折一 2013年8月2日 - 00:53

请问上面的第一个完整的例子中,结尾为什么不用 return false;而是使用return QMainWindow::eventFilter(obj,event);啊?

回复
豆子 2013年8月2日 - 19:22

由于我们是覆盖了父类的实现,我们只关心 obj == textEdit 这个情况,对于其余情况,依然按照默认实现,也就是要调用父类的实现了

回复
张坤 2013年10月10日 - 16:12

豆子,您好,请问eventFilter()和event()谁先起作用啊?是不是可以这么理解:一般都使用eventFilter()函数,event()函数很少使用?

回复
豆子 2013年10月10日 - 22:42

event() 是 protected 的函数,所以只能以子类的形式使用;eventFilter() 就没有这个问题。eventFilter() 会在 event() 之前起作用,相当于“过滤”事件。如果你是继承一个类,覆盖 event() 函数会更适合一些。毕竟子类覆盖父类的做法更普遍一些。

回复
zz 2019年7月29日 - 10:25

event 声明不是public的吗

回复
豆子 2019年8月16日 - 21:25

QObject::event() 是 public 的,QWidget::event() 是 protected 的

回复
hysteria 2013年12月2日 - 23:04

我创建了三个控件,其中一个是主窗口,被其他两个控件遮盖了部分。他们都绑定了属于自己的过滤器,点击其中一个控件的时候,他似乎只调用了自己的过滤(看debug的数据输出),无论这个过滤器函数返回的是false、true还是调用父类的同名函数。这是不说这个过滤器并不是全局的,只是相对于控件而已,那这样eventFilter的QObject参数是否有点多余?这样,如果我们要使用过滤器,那也是要给每个控件绑定过滤器函数?

回复
hysteria 2013年12月2日 - 23:42

下一篇有写了。
事件过滤器可以安装到任意QObject类型上面,并且可以安装多个。如果要实现全局的事件过滤器,则可以安装到QApplication或者QCoreApplication上面。这里需要注意的是,如果使用installEventFilter()函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的eventFilter()函数进行过滤,其它对象不受影响。

回复
otto 2013年12月18日 - 15:43

豆子哥,对这句不是很明白:QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event)

回复
豆子 2014年1月14日 - 21:24

这是强制类型转换,类似于 QKeyEvent *keyEvent = (QKeyEvent *)event

回复
Linzhe 2015年10月26日 - 01:20

豆子哥你好,首先非常喜欢你的教程。
对于upper cast我也有个问题。
虽然从
bool QObject::event(QEvent *e) //第20篇教程
的实现来看,event和eventHandler是相匹配的。在这个前提下,static_cast一定会成功,因此不需要用dynamic_cast检查转换是否成功。如果转换真的失败了,让程序崩溃是更好的选择。
不知道我这样子理解是不是正确,再次感谢!

回复
hunanzai 2014年1月14日 - 09:36

博主 这个网页的titile错了

回复
豆子 2014年1月14日 - 21:19

的确,可能是缓存的问题 ;-P

回复
darkhandz 2014年5月11日 - 15:30

我现在看着还是“Qt学习之路 2(20):事件过滤器” ……

回复
末姬人生 2015年8月3日 - 16:52

我现在看还是20呢,看来不准备改了哈哈

回复
justin 2014年1月16日 - 15:49

豆子你好,请教一下啊,为什么tableview在installEventFilter之后,不响应鼠标事件?
在不通过继承tableview类的情况下,如何才能在tableview上区分是鼠标左键双击还是右键双击?

谢谢了:)

回复
豆子 2014年1月17日 - 14:39

是不是你的过滤器把鼠标事件过滤掉了?如果不继承 QTableView,只能使用事件过滤器来判断。(话说确实需要鼠标右键双击的事件么?)

回复
justin 2014年1月17日 - 22:55

我的事件过滤器就写了简单的几行,应该不存在误过滤的问题,键盘事件响应倒是很活跃,愁死我了。鼠标右键双击⋯⋯因为发现tableview的dblclick信号不区分左右键的,右键双击也能触发dblclick,想要屏蔽

回复
gameboy031 2014年8月19日 - 15:45

MainWindow::MainWindow()
{
textEdit = new QTextEdit;
setCentralWidget(textEdit);

textEdit->installEventFilter(this);
}这是指安装eventFilter到MainWindow对象?为什么要用textEdit来调用?而不是this->installEventFilter(this)?而eventFilter(QObject *obj, QEvent *event)的*obj是不是要传递子控件的对象?总的印象是在MainWindow上过滤了textEdit的事件(要从父窗口安装过滤器过滤子窗口的事件?) 不知是不是要这样理解,请豆子解惑,谢谢

回复
gameboy031 2014年8月19日 - 15:59

原来是要monitoredObj->installEventFilter(filterObj);查了帮助

回复
filterholl 2017年3月8日 - 15:42

这正是我想了解的。谢谢

回复
jjj 2015年6月25日 - 13:07

如何给QAppliction对象安装事件过滤器.需要继承QApplication类,并重载eventFilter()函数吗?

回复
豆子 2015年6月29日 - 10:41

事件过滤器用一个普通的 QObject 对象即可,只需要将其安装到 qApp 对象。

回复
蓝色 2015年11月10日 - 00:18

如果在QApplication调用installEventFilter,那是不是所有的widget都应用了事件过滤?

回复
豆子 2015年11月13日 - 16:09

是的

回复
蓝色 2015年11月10日 - 00:32

还有一个问题,你不是在texteditor上调用事件过滤器吗,为啥要考虑父对象?

回复
豆子 2015年11月13日 - 16:10

考虑父对象是为了保证父对象上面设置的事件过滤器可以被调用(如果全部代码都是你一个人写的,当然没有问题,但是如果是小组编写,你必须保证你的代码不会影响到其他人的代码运行,所以最好是做一些类似的工作)。

回复
孙江涛 2015年11月12日 - 23:58

最后面对于event()使用filter那个版本,代码中的target是怎么回事儿?

回复
豆子 2015年11月13日 - 16:17

target 是你关注的对象,因为大多数情况下,事件过滤器往往是针对一个对象的,通过检查是不是你关心的这个对象做出相应的操作

回复
daybyday 2016年1月2日 - 19:21

我只问一个问题,什么是事件过滤器,
你写道:evenfilter()这个函数正如其名字显示的那样,是一个“事件过滤器”。然而你还写道:
void QObject::installEventFilter ( QObject * filterObj )
任意QObject都可以作为事件过滤器

我不知道到底是filterObj还是eventfiler()是事件过滤器
textEdit->installEventFilter(this);
上面这句代码我根据qtassist 里面的解释:
void QObject::installEventFilter(QObject * filterObj)
Installs an event filter filterObj on this object. For example:
理解为:在textEdit上安装this过滤器 ,我有些许疑惑,望豆子帮我!

回复
罗伊马斯特 2016年3月18日 - 11:56

"注意,如果你在事件过滤器中 delete 了某个接收组件,务必将函数返回值设为 true。否则,Qt 还是会将事件分发给这个接收组件,从而导致程序崩溃。"

这个没明白啊, 是delete了 FilterObject* 还是 delete了QTextEdit*?
如果是 delete filter, 那应该先 removeFilter
如果是 delete textEdit, 那组件都没有了就不会有事件分发给 textEdit或它的 filter了吧?

回复
豆子 2016年3月18日 - 15:57

应该是 delete textEdit 这种。如果你 delete 了 textEdit 却还是返回 false,其它的事件并不知道这个组件已经被销毁,所以可能会出现程序错误

回复
罗伊马斯特 2016年3月18日 - 19:26

Thx, 明白了, 如果在eventFilter()里 delete textEdit, 但却返回 false, Qt会试图把事件交给已经被删除的 textEdit来处理event(), 这样就挂了

回复
小杨 2019年5月3日 - 11:36

豆子哥,我不明白“我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。”可以仔细讲解一下吗?

回复
豆子 2019年5月3日 - 11:48

因为所谓 MainWindow,它并不知道是否还有其它过滤器(过滤器的添加并不会通知别的类比如 MainWindow)。如果我们不调用父类函数,那么其它我们不关心的事件都会被过滤掉,也就是其它过滤器都不会起作用了。所以必须调用父类函数,给一个默认实现,保证其它添加的过滤器能够正常运行。

回复
yip 2019年5月12日 - 18:51

豆子老师,我遇见了一个令我焦头烂额的问题,就是我的事件过滤器可以过滤键盘事件,但但我将事件类型换成QEvent::MouseButtonPress或者其它鼠标事件时,就完全没有响应,就是事件过滤器是有执行的,但是它不能响应鼠标事件,请教一下您这是为什么啊?

回复
zz 2019年7月29日 - 10:28

豆子老师,请教个问题:
安装多个过滤器的时候,父对象例子都是this,那怎么区分多个过滤器的

回复
豆子 2019年8月16日 - 21:26

installEventFilter() 调用多次,每次需要传入不同的对象指针

回复
pan64271 2020年7月12日 - 01:00

第二段代码中,“对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。”那为什么最后一段代码处就不需要调用父类的eventFilter函数了呢?希望能得到解答。

回复
andy 2020年7月22日 - 09:49

你好,我想问一下我通过createwindowcontainer()捕获的外部程序窗口,我想获得他的鼠标事件,但是用事件过滤器并没有成功,想请教一下有什么办法吗?谢谢

回复
小学生Kane 2020年8月19日 - 13:55

对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。
没理解在担心什么,这样写和直接return false有何区别?(父类在这里并没有重新eventfilter吧,那么调用父类eventfilter和return false不是一样的吗)

回复
小学生Kane 2020年8月19日 - 14:23

下一节不是说,该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的eventFilter()函数进行过滤,其它对象不受影响。那么textedit之外的事件根本不会被传给这个过滤器,为什么“其它我们不关心的事件都会被过滤掉,也就是其它过滤器都不会起作用了。''

回复
小学生Kane 2020年8月19日 - 16:21

一个对象存在多个事件过滤器,是指一个目标对象安装多个事件过滤器吗,那要怎么安装。更常见的是一个事件过滤器对应多个待监听的目标对象吧?

回复
豆子 2020年8月19日 - 17:04

调用多次 installEventFilter() 就可以安装多个事件过滤器了。一般一个事件过滤器对应多个目标,但不同的类可能都给同一对象安装过滤器。比如 MainWindow 和 Dialog 都给 qApp 安装了各自的事件过滤器。

回复
小学生Kane 2020年8月19日 - 17:20

如果textEdit对象,注册了其他多个事件过滤器。如果只是return false的话,是直接绕过所有其他过滤器,把事件传递给textEdit的event函数吗?
如果是这样的话,那么在MainWindow这其中一个中的eventfilter中去return QMainWindow::eventFilter(obj, event) ,我的理解是,这句的作用是如果并没有重写
QMainWindow::eventFilter函数,那么就自动去寻找其他能过滤textEdit的事件过滤器(纯脑补的,不然的话没法保证假如并没有重写QMainWindow::eventFilter,或者有非QMainWindow子类的检测textEdit的过滤器能起作用)

回复
小学生Kane 2020年8月19日 - 17:28

感谢豆哥。后安装的先生效,那后面这种情况,MainWindow 和 Dialog 都给 qApp 安装了各自的事件过滤器,感觉有些情况可能不好确定谁先安装谁后安装,是看哪个事件构造器的类先创建出来对象吗。如果是在不同线程呢

回复
小学生Kane 2020年8月19日 - 17:29

感谢豆哥。后安装的先生效,那后面这种情况, 都给 qApp 安装了各自的事件过滤器,感觉有些情况可能不好确定谁先安装谁后安装,是看哪个事件构造器的类先创建出来对象吗。如果是在不同线程呢

回复

回复 罗伊马斯特 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2