首页 Qt 学习之路 2 Qt 学习之路 2(22):事件总结

Qt 学习之路 2(22):事件总结

58 5

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 就是使用的这么一种机制:

  • mouseEvent()
  • keyPressEvent()
  • ...

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

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

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

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

事件过滤器可以解决刚刚我们提出的event()函数的两点不足:首先,事件过滤器不是 protected 的,因此我们可以向任何QObject子类安装事件过滤器;其次,事件过滤器在目标对象接收到事件之前进行处理,如果我们将事件过滤掉,目标对象根本不会见到这个事件。

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

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

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

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

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

为了进一步了解这几个层次的事件处理方式的调用顺序,我们可以编写一个测试代码:

class Label : public QWidget
{
public:
    Label()
    {
        installEventFilter(this);
    }

    bool eventFilter(QObject *watched, QEvent *event)
    {
        if (watched == this) {
            if (event->type() == QEvent::MouseButtonPress) {
                qDebug() << "eventFilter";
            }
        }
        return false;
    }

protected:
    void mousePressEvent(QMouseEvent *)
    {
        qDebug() << "mousePressEvent";
    }

    bool event(QEvent *e)
    {
        if (e->type() == QEvent::MouseButtonPress) {
            qDebug() << "event";
        }
        return QWidget::event(e);
    }
};

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 app(argc, argv);
    Label label;
    app.installEventFilter(new EventFilter(&label, &label));
    label.show();
    return app.exec();
}

我们可以看到,鼠标点击之后的输出结果是:

QApplication::eventFilter 
eventFilter 
event 
mousePressEvent

因此可以知道,全局事件过滤器被第一个调用,之后是该对象上面的事件过滤器,其次是event()函数,最后是特定的事件处理函数。

58 评论

羊八井 2012年11月17日 - 18:46

类定义开始应该需要Q_OBJECT宏吧?

回复
DevBean 2012年11月17日 - 20:21

如果继承了 QObject,最好添加 Q_OBJECT 宏。对于信号槽、国际化支持,这是必须的。但是如果没有使用这些特性,不加也是可以的(所以这里没有添加),但是还会有潜在的问题。所以,一般建议是添加 Q_OBJECT 宏。

回复
qingxp9 2013年4月27日 - 10:54

这一节提到了主线程,我检索了一下,到这一节为止,都没有关于线程的介绍,对线程不怎么理解。虽然不影响对这一节内容的理解,还望能简介一下。

回复
豆子 2013年4月28日 - 08:58

后面会对线程作详细介绍的。线程是 OS 里面最基本的概念,如果不理解的话最好查找一下资料。简单来说,就是程序运行时有一个主线程,你可以把它当做程序执行本身。针对 GUI 程序,几乎所有 GUI 库都是把界面绘制放在一个 GUI 线程里面,同时在这个线程中执行事件监听循环等操作,为的是避免 GUI 复杂的交互带来的线程同步问题。

回复
Glory_D 2017年1月11日 - 22:05

豆哥,我对两次执行QApplication::eventFilter也不太理解,你所指的所有对象是指哪个?另外,EventFilter是一种怎样的存在?这个类似乎仅提供了过滤器函数,它本身似乎不具备事件处理能力。当我在它的过滤器中试图过滤掉该对象自身时,即
if (watched == this) {
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "EventFilter::eventFilter";
}
这段代码是不会执行的,求解?
}

回复
Jakes 2013年4月29日 - 17:30

发现两个将“事件”写成“时间”的错别字。 🙂

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

修改过了 ;-P

回复
sinn 2013年11月3日 - 21:14

请问我将那个全局时间过滤器中的return false;语句改为return
true;之后,单击鼠标没什么反应,我奇怪为什么连"QApplication::eventFilter"这一句也没输出?

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

我的测试(Qt 4.8.4,VS2012,Windows 8)会输出 QApplication::eventFilter 一行。只不过由于返回的是 true,关闭窗口等事件都不会被执行了。不知道你的环境是怎样的。

回复
sinn 2013年11月3日 - 22:44

我的环境是(Qt 5.1.1, Qt Creator, win7),确实无输出,关闭窗口时间也不能执行,最大化,最小化可以。

回复
豆子 2013年11月4日 - 21:09

Qt5 的行为的确和 Qt4 不一致。这可能是因为 Qt5 的事件处理流程发生了变化,导致无法进行控制台输出。关于这一点文档上也没有清晰地描述。不过,鉴于这种情况(return true)不大可能出现(因为这会拦截所有事件,相当于禁止了 Qt 的事件循环),因此我们不必对一个错误的使用过于深究。

回复
Puzzle 2014年4月14日 - 22:40

豆子哥,请问事件不是首先被过滤器接收,对象就不会接收到该事件了吗?那么最后一个例子中event为什么还会输出呢?

回复
豆子 2014年4月15日 - 09:30

因为事件过滤器返回的是 false。只有当过滤器返回 true 的时候,事件传播才会停止,否则会继续执行。

回复
ksn13 2014年5月8日 - 20:19

豆哥,我在你写“return QWidget::event(e);”的地方
替换为: return (this->patent())->event(e);
编译不会出错,但是运行的话会报错:The Program has unexpectedly finished

我不理解,我觉得应该是一样的才对。请豆哥帮忙解答一下

回复
豆子 2014年5月9日 - 21:27

这两种写法是不一样的:QWidget::event(e) 是调用父类的同名实现;(this->patent())->event(e) 是调用该对象的父对象的实现。Qt 里面的 parent 并不是面向对象意义上的父类,而是 Qt 实现的对象树的父节点。二者是不同的。

回复
rainc 2014年6月16日 - 10:59

豆子哥,if (watched == m_watched)这个条件语句是做什么的,有什么用,不过我去掉这个条件语句后,结果会输出两次的QApplication::eventFilter。这是什么原因

回复
豆子 2014年6月18日 - 11:05

这个语句用于判断事件过滤器接收到的对象是不是 m_watched。如果没有这个判断,所有的对象都会执行,而这个判断限制仅针对某一特定对象。

回复
Glory_D 2017年1月12日 - 20:10

豆哥,我对两次执行QApplication::eventFilter也不太理解,你所指的所有对象是指哪个?另外,EventFilter是一种怎样的存在?这个类似乎仅提供了过滤器函数,它本身似乎不具备事件处理能力。当我在它的过滤器中试图过滤掉该对象自身时,即
if (watched == this) {
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "EventFilter::eventFilter";
}
}
这段代码是不会执行的,求解?

回复
xzp21st 2014年7月7日 - 20:57

调用顺序这个例子是lable自己给自己安装了一个过滤器?自己观察自己?目前有点混乱,就是观察者和被观察者怎么区分,我的理解是被观察者给自己安装一个观察者,在观察者中实现事件过滤,是这样吗?

回复
豆子 2014年7月9日 - 09:24

是的,这个例子是这么实现的,相当于自己观察自己。在实际应用中,有时候可能没有办法通过继承一个类(比如这个类需要在某个闭源类库中使用,这样你就不能用一个子类进行替换)实现事件的效果,因而就需要另外的类进行“观察”。所以在实际应用中,观察的类与被观察的类通常是两个类。

回复
tengyft 2014年11月16日 - 20:27

请问编译时,老出现这种错误
/home/tengyft/projects/QtProjects/EventHandler/main.cpp:13: error: undefined reference to `vtable for Label'
将析构函数写成~Label() {}还是有这种错误,这是怎么回事啊

回复
豆子 2014年11月18日 - 10:47

这个不大清楚,看错误是由于虚表的问题,是不是继承的错误?

回复
tengyft 2014年11月23日 - 23:30

问题已经解决,就是在类中加入了Q_OBJECT,就出现了这种问题,删除以后就没有问题了

回复
Alien You 2017年5月2日 - 19:25

执行一下qmake,重新构建项目就可以了,我也是这样的

回复
cossu 2015年1月23日 - 00:07

豆子哥,请教个问题。最后的那个例子中,
app.installEventFilter(new EventFilter(&label, &label));
//在全局中安装了事件过滤器,这个事件过滤器是过滤传导到EventFilter组件的事件的过滤器。
//可是在之前的的代码中并没有看到有安装eventFilter(QObject *watched, QEvent *event);的代码啊。EventFilter的构造函数也是空的。请问是我的理解有问题么?那里理解错了。

回复
豆子 2015年1月23日 - 15:08

事件过滤器是一个类,app.installEventFilter() 语句就是将 EventFilter 所定义的过滤器安装到了 app 这个对象,也就是全局的对象。这样安装之后,Qt 会自动调用 EventFilter(也就是被安装的对象)的 eventFilter() 函数

回复
sxy 2015年5月20日 - 22:58

请问豆大神,上面的例子中main函数中是不是少了一句ilabel.nstallEventFilter(&label)

回复
豆子 2015年6月3日 - 11:33

这一句在 Label 的构造函数中已经添加了

回复
王先先 2015年9月23日 - 18:01

event()函数是所有对象的事件入口
==================================
豆子大神 ,建议把这句话 改成 对象的所有事件 入口 ,,,,

回复
XxZzf 2015年11月26日 - 10:37

在父类中重写的eventFilter/event/事件处理函数 子类会不会继承这些特性呢?

回复
2016年5月18日 - 17:44

请问豆豆大神:
我在对象上面的事件过滤器中将return false;改为return QWidget::eventFilter(watched,event);后为什么后面还会输出event和
mousePressEvent。不是应该把事件传递给父类QWidget的同名函数了吗?

回复
韩逸 2016年5月28日 - 21:32

EventFilter(QObject *watched, QObject *parent = 0) :
QObject(parent),
m_watched(watched)
请问这个初始化列表中的QObject(parent)是做什么用的

回复
豆子 2016年6月3日 - 12:39

调用父类的构造函数,这是标准 C++ 的做法

回复
bin 2016年9月7日 - 20:06

"事件过滤器可以安装到任意QObject类型上面,并且可以安装多个",怎么安装多个?是在一种QObject类型上安装多个还是在各种QObject类型上安装,但每种QObject只能安装一个?

回复
豆子 2016年9月8日 - 09:20

多次调用installEventFilter()就可以安装多个,类似于一个信号可以连接多个槽

回复
libreflood 2017年1月24日 - 14:22

"如果我基于第三方库进行开发,而对方没有提供源代码,只有一个链接库,其它都是封装好的。我怎么去继承这种库中的组件呢?"
对于链接库中的类,我们不能继承它、重写event函数吗?
这一点我不太明白,能不能详述一下?

回复
豆子 2017年1月27日 - 15:45

这里只是举了一个例子,比如第三方库只给了头文件和编译好的二进制链接库,如果我们只是想替换掉某个类的事件响应的实现,此时没有办法通过继承的方式修改事件响应代码,因为我们没有这个库的源代码,也就无法重新编译这个库,这样通过继承的方式就不合适,只能使用事件过滤。

回复
libreflood 2017年2月12日 - 09:50

哦哦哦,我明白了,谢谢?

回复
sat 2017年2月5日 - 21:49

豆子哥,我测试了eventfilter的安装顺序,发现事件过滤是和事件过滤的安装顺序有关,先安装的事件过滤后执行,感觉像栈,下面是修改后的代码:
class Label:
...
Label()
{
//installEventFilter(this);
}
void installmy()
{
installEventFilter(this);
}
...

main():
...
Label l;

EventFilter e(&l);

l.installEventFilter(&e);

l.installmy();
...

对了 qt版本是5.7

回复
toming 2020年5月3日 - 18:55

If multiple event filters are installed on a single object, the filter that was installed last is activated first.

回复
Alien You 2017年5月2日 - 10:47

豆哥,“但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)”这句话是什么意思

回复
Alien You 2017年5月2日 - 10:51

文档中说明的是只有一个子类能被激活一次

回复
豆子 2017年5月6日 - 10:32

如果你选择重写 QCoreApplication::notify(),那么只能重写一次,就是说不同的类不能提供不同的重写,后面的实现会覆盖前面的,因为只有一个 QCoreApplication 实例。

回复
Alien You 2017年5月2日 - 20:15

豆哥,这句new EventFilter(&label, &label)第二个参数把label传递给QObject有什么作用吗

回复
Alien You 2017年5月2日 - 20:16

原有的构造函数已经有parent = 0

回复
豆子 2017年5月6日 - 10:39

第二个参数是 parent,目的是当 label 析构的时候,以 label 为 parent 的 EventFilter 实例会被析构掉,不需要手动调用 delete,防止内存泄露。这是 Qt 中大部分类都有的 parent 指针的作用。

回复
Cyshall 2018年1月2日 - 23:20

豆哥还在么?
您文章中说event()函数是protected,而eventFilter()不是protected的。但是我在Qt文档中看到的这两个函数都是public的虚函数阿。

回复
豆子 2018年1月3日 - 22:11

QObject::event() 是 public 的,但是 QWidget::event() 却是 protected 的。我会写一篇文章来详细解释这一点

回复
Jhon Wong 2018年7月3日 - 15:08

为什么向下翻页留言区会抖动?

回复
ken 2018年12月13日 - 11:57

豆哥你好,小弟有个问题卡了很久,实在受不了上来求救了
我需要让我的视窗在鼠标按下,放开的时候会产生涟漪的效果,但是不能影响到按钮或文字输入的动作,请问什么方法比较好呢?

附:以下是我尝试过的方法和问题:

1.在视窗最上层建立一个透明视窗,接收鼠标动作产生效果后,用的QApplication ::事件后传给下层的物件。
问题:下层的物件无法收到hover,鼠标移到按钮上方会没有变化,滚轮的操作也会失效。

2.继承QWidget,QPushButton等类,并用用于覆盖mousePressEvent的方式产生效果。
问题:QPushButton和QTextEdit中的mousePressEvent被覆盖后,都会有功能失效,想重写原本mousePressEvent里的功能,又太过复杂。

3.在最底层的窗口小部件用事件过滤器,把所有物件加入,让物件接收完鼠标动作后会继续传下去,由底层的widget产生效果。
问题:主Widget里面有多个子Widget,写在不同cpp档案中,我试着将子Widget中的物件加入事件过滤器,但是都会报错。

跪求指点,拜托了!

回复
lsaejn 2019年5月10日 - 00:38

买过一本QT的书,基本是讲api怎么用。像阁下这么高屋建瓴地写东西,真心不容易。。

回复
豆子 2019年5月13日 - 19:10

客气了,应该多看看文档,文档里面写得更清楚的

回复
小学生Kane 2020年8月18日 - 14:06

最好这个例子很有意思,试了下双击鼠标左键,结果是:
QApplication::eventFilter
eventFilter
event
mousePressEvent
mousePressEvent
又了

回复
小学生Kane 2020年8月18日 - 14:06

又懵了

回复
小学生Kane 2020年8月19日 - 09:58

给一个对象安装事件过滤器,是指把这个“事件”作为目标对象,还是作为过滤对象(filobj,即installeventfilter里的参数)

回复
enak 2020年8月19日 - 22:53

例子里存在两个eventFilter,按上一节所说,为何不是return各自父类的eventFilter,而是return false ?

回复
小学生Kane 2020年8月20日 - 08:37

最后一个例子里有两个事件过滤器,一个全局的,一个是label类里安装的监听自己的过滤器,疑问有两点。label在构造函数里installEventFilter(this),自己监听自己这种写法在实际使用中是否有意义;二是两个过滤器都是return false,而上一小节讲的是,当对同一个监控对象,存在其他过滤器时,要return父类的eventfilter,感觉二者有些矛盾。希望豆哥耐心解答一下。

回复
豆子 2020年8月21日 - 19:18

我的理解是,自己将自己注册为事件过滤器,只不过是因为某些原因而把 eventFilter() 放在了自己的类(可能是为了访问到自己的私有数据等),所以是有意义的。否则 eventFilter() 完全可以作为一个 global 的存在。第二个问题,这里仅是为了说明才这么写的,但无论是返回 false 还是返回父类的实现,都不是固定的,要依照具体情形具体选择。不存在一个普遍的规律(否则 Qt 也就不会让你有这种选择了)。

回复

发表评论

关于我

devbean

devbean

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

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