首页 Qt 学习之路 2 Qt 学习之路 2(20):event()

Qt 学习之路 2(20):event()

52 5K

前面的章节中我们曾经提到event()函数。事件对象创建完毕后,Qt 将这个事件对象传递给QObjectevent()函数。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 类型。如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,并且,该事件对象设置了accept(),那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。注意,在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;
}

CustomTextEditQTextEdit的一个子类。我们重写了其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 键的组件。这种作用是重写事件处理器所不能实现的。

52 评论

Anonymous 2012年10月12日 - 20:49

又问一个无关的问题……最近想在自己的手机上移植android cyanogenmod,因为手机是中国电信合约机,根本没有外国hacker组织帮忙做包,国内的包也很少(电信合约机的缘故)而且比较坑……豆哥熟悉android移植么,编译环境啊什么源码获取啊都已经全部搞定了,感觉原材料和工具都已经备齐,但是实在是不知道怎么着手,难道我应该去一字一行看cyanogenmod和htc官方的两套源代码吗,我觉得这工作量实在是太巨了……下载就下了我三天三夜……,网上那些蜻蜓点水的教程根本就是水贴,只会教怎么链接到代码库等最基本的工作(其实大陆还有g`f`w障碍那些教程也基本不说可见应该不是认真做的),还教怎么在虚拟机里装一个ubuntu...(我现在是单硬盘主引导的opensuse反正环境是没有什么障碍),而从头为一台机子做抑制更是中外都没有,连cmwiki都没有,唉……想来豆哥既然对qt这样的框架感兴趣,似乎也应该会了解android的样子(这是武断随便想的,因为总觉得有点共鸣,像是我平时比较在意的就有android qt 和 html5等等)。觉得如果能有linux c方面的资深咨询一下,或许可以不至于想现在这样手足无措,不过只是问一问而已算不上是什么请求,谢先。

回复
DevBean 2012年10月13日 - 09:03

Android 移植的确没有做过,google 上面一下 android port 没准儿会有一些信息,比如这里 http://www.freeyourandroid.com/guide/porting-android。因为对移植一直没有研究,所以不便多做解释。抱歉的哦。

回复
Anonymous 2012年10月13日 - 12:58

啊……多谢回复,在xda之类的地方问了很久,回答基本就是google it,说实话我已经google好久了才发问了(否则好像会被骂),可是终究还是遇到很大阻塞,只能慢慢来了吧。

回复
andmoe 2013年1月15日 - 15:52

先去把linux嵌入式驱动开发学会,一切就都不是问题了~

回复
scutlpf 2014年2月19日 - 13:03

event()函数返回值true或false有什么用

回复
豆子 2014年2月19日 - 15:48

event() 返回值供系统使用,一般自己不会在意这个返回值的。如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。

回复
scutlpf 2014年2月19日 - 16:23

如果事件处理了却返回false会产生错误码?会影响事件向父组件的传递吗?

回复
豆子 2014年2月19日 - 16:26

这个我的确没有测试过,你可以试一下看看 。像这种回调函数,系统只能假设开发人员会按照文档说明实现并且不会犯错,因为系统本身没办法判断究竟需要怎样的逻辑,只能相信开发人员。

回复
scutlpf 2014年2月19日 - 19:08

谢谢!

sxy 2015年5月19日 - 15:43

true就不会传递event
false会继续向父组建传递event

回复
豆纸 2016年3月11日 - 22:52

我看没明白这个文章
是不是我点 tab键不会有反应???
求解

回复
羊习习 2016年7月12日 - 09:38

点tab键会被这个自定义的custom组件接收处理,处理完成后变返回了,
如果不是点tab的其他所有事件依然交付给上一级的父对象处理。

回复
manfred 2014年5月21日 - 11:47

一个事件发生之后,Qt怎么知道该发给哪个QObject的event()函数呢?我看了一下文档里关于QApplication::sendEvent()函数的说明,一个sendEvent()函数只能连接一对event-receiver。所以Qt是会自动把事件发送给所有的QObject(包括自定义的)吗?

回复
豆子 2014年5月23日 - 15:09

个人认为会进行广播,也就是发送给所有 QObject,只不过有的对象会进行处理,有的对象直接忽略。至于 sendEvent() 函数,你当然可以通过两次调用来发送给两个对象的

回复
manfred 2014年6月4日 - 23:32

thx,感谢豆子君的回复~btw,说得更细点可能是QCoreApplication类先发送给每个Object树的顶层?(因为子组件ignore后会自动发送给父组件)

回复
豆子 2014年6月5日 - 10:26

感觉应该是可以这样理解的

回复
sxy 2015年5月19日 - 15:52

keypress event应该是发给当前焦点的object,如果不处理为false,再由object向父object传递,处理就为true 停止传递

Geek 2014年9月16日 - 16:49

豆子豆子,你的第一段代码和第二段代码是不是贴反了?

回复
Geek 2014年9月16日 - 16:56

代码没贴反,是我看错了。

回复
martin 2015年5月30日 - 23:10

第一段代码, 如果处理了 tab 键后, 直接返回 true, 那是不是意味着 tab 键消息不会再被分发?

回复
martin 2015年5月30日 - 23:12

同样的, 第二段代码, 如果只是想拦截所有的消息, 那返回 false 和 返回 true 不都是一样的吗?

回复
martin 2015年5月30日 - 23:17

如果返回值是 true,并且,该事件对象设置了accept(),那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。
这里, 返回 false 又会怎么样? 我感觉不管是返回 true 还是 false, 因为都没有进行分发, 所以消息都算是被拦截了, 返回 true QT 会继续处理下一个事件, 返回 false 难道是不处理了吗?

回复
菜瓜 2015年11月9日 - 16:07

编译提示CustomWidget不是域名或命名空间,怎么办?

回复
豆子 2015年11月9日 - 21:00

有可能是没有 include 头文件之类,或者哪里写错了

回复
菜瓜 2015年11月10日 - 19:48

那需要include什么头文件?是QWidget吗?我加了但是没用...

回复
豆子 2015年11月13日 - 15:55

如果是 CustomWidget 找不到,应该 include CustomWidget 头文件

回复
Joey 2016年1月21日 - 17:45

Qt::Popup属性在windows平台与linux平台下表现得不一致是怎么回事?
需求:
A,B同属于一个主窗口
第一步:窗口A,通过点AA按键 后popup出来,点此窗口以外的主窗口区域,这个窗口A会自动隐藏起来。
第二步:窗口B,通过点BB按键 后popup出来,
当点AA按键时效果是把B窗口隐藏再把A窗口显示出来。

windows平台的表现是这样的,只要设置了Qt::Popup属性

linux平台的表现是:在第二步按AA键时只是把B窗口隐藏了,A窗口并没有显示出来

回复
guoyinbo2012 2016年3月20日 - 10:06

看了这两节,想问下组件事件和信号处理的区别是什么呢或者说两者适用的场合,他们处理的信息有什么区别呢?比如说,同样的按钮点击,信号和事件应该都能进行处理吧?上节说的是使用组件关心的是槽,自定义组件,关心的是事件,这里自定义组件应该也是从组件类继承来的吧,还是说自己完全制作的(就是说自己封装的)?到底应该怎样理解他们之间的关系呢?谢谢

回复
豆子 2016年3月20日 - 16:31

这个没有绝对的答案,不过个人认为,如果信号槽和事件都可以处理的,优先选择信号,因为信号的解耦更好,使用也更简单,不容易出错。当没有类似信号时,只能选择事件。当你继承了某个组件,就可以选择使用事件,因为事件处理函数都是 protected 的,很明显只能继承使用;并且,由于信号槽比一般的函数调用慢大约一个数量级,因此在已经继承了情况下,选择事件可能更合理一些。

回复
guoyinbo2012 2016年3月20日 - 16:48

好的,多谢豆子哥的耐心解答,对于您说的 没有类似信号时,只能选择事件。是在什么情况下呢?信号和事件都是可以进行自定义的,没有类似信号可以通过自定义信号来处理?不知道这种理解对不对?对于继承,感觉绝大多数自定义组件都是继承来的吧,不知道理解对不对?

回复
豆子 2016年3月20日 - 16:54

没有类似信号,比如你需要分开处理鼠标左键、右键等点击时,就只能使用事件,信号是没有类似区分的。如果你是自定义组件供外部使用(不仅仅是给别的开发者,也可能是被系统其它组件调用),就可以在事件中发出自己的信号,方便调用。自定义组件大部分会通过继承,也有可能是组合,比如几个按钮一组,类似 QDialogButtonBox(当时都会是继承了 QWidget)。

回复
guoyinbo2012 2016年3月20日 - 17:02

需要分开处理鼠标左键、右键等点击时,可以通过自定义鼠标左键、右键等点击信号以及相应的槽来实现吧?

豆子 2016年3月20日 - 18:45

是的,但是系统没有类似的信号,所以就是不得不使用事件的情况

ccppaa 2016年8月16日 - 11:00

请问大神,按钮用鼠标点击了一下之后,再按下空格键会自动调用该按钮,是怎么回事,

回复
豆子 2016年8月17日 - 08:50

鼠标点击之后,按钮会获得焦点。在操作系统的实现上,按下空格会触发窗口中具有焦点的组件的动作

回复
bin 2016年9月7日 - 16:31

重写事件处理器是不是要在原来event()函数中重新调用这个事件处理器,那不是相当于要改写event()函数?

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

不需要,因为event()函数是虚函数,会在运行时调用我们实现的版本,也就是动态绑定。

回复
Alien You 2017年5月1日 - 10:31

那是不是通过子类进行重写事件处理器,运行期间会调用子类的实现版本?

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

是的,因为这个函数是 virtual 的,会在运行时决定执行哪个实现

回复
Alien You 2017年5月1日 - 10:52

豆哥,返回false是指继续传播事件给父类吗?如果是那么上面说,鼠标事件根本不会被转发又是什么意思呢?

回复
newcoderlife 2017年7月31日 - 22:10

您好。我重写CustomWidget中重写event()函数后编译运行,按下Tab键,不会触发CustomWidget::event(),而是焦点在上一章的两个按钮(CustonButton和CustomButtonEx)之间来回切换。请问这说明我没有重载成功吗?谢谢!

回复
豆子 2017年8月1日 - 21:37

默认行为应该是切换,所以注意下函数签名是不是重写?

回复
gruLewis 2017年9月18日 - 16:38

你好。
对于前面的一个有关sendpost函数的问题,你的猜测是广播形式,我这边跟踪源码后,发现并不是这样的,而是在发送信号的时候,先使用reciever和事件形成QPostEvent存入list,然后在事件循环执行到sendPost的时候,取出这个list的元素,往reciever分发,这是我对目前5.9源码的跟踪,请作者有时间再次跟踪确认。
我之前也看过作者有关事件的文章,但是前几次看完都是晕晕呼呼的,可能是我基础不好,看了就忘记了。
最近,我自己开始跟踪qt源码,收获很大,对于qt的事件机制有一些了解,但也模糊。
作者的qtcreator源码分析,我也看过一些,还是非常不错的,有时间继续读,这些作者的贡献。
码字略多,请耐心读完,谢谢。

回复
zgbzsu2008 2018年9月4日 - 10:59

既然用c++分格的类型转换 dynamic_cast 转换不是比用static_cast更加安全,对于基类到派生类的转换

回复
豆子 2018年9月8日 - 17:21

是的,这里用 dynamic_cast 要比 static_cast 好

回复
WHC 2020年4月21日 - 23:25

豆子 怎么调用父类同名函数呀 那个只处理tab不处理其他事件这个问题,最后是怎么解决了的呢?没看太明白这里

回复
豆子 2020年4月24日 - 11:30

子类如果实现了父类的同名函数,由于多态机制,使用父类指针调用时会直接调用子类的实现,这样父类函数的实现逻辑就没有了。如果仍然需要父类函数的实现逻辑,就需要使用 QTextEdit::event() 这样的调用。

回复
Misgin 2021年4月6日 - 17:44

处理Tab 按键事件,使用`KeyPressEvent()`好呢,还是`event()` 好呢?

回复
豆子 2021年4月10日 - 14:45

两种都可以实现,看怎么方便怎么来

回复
Misgin 2021年4月6日 - 17:59

如果自己需要处理多种类型的事件时,使用event,反则使用 对应的事件处理器

回复
Nephren 2021年6月23日 - 19:24

这里例子中关于Tab键盘事件非常值得注意的是,QWidget::event帮助里说了,如果一个组件有子组件,则Tab和Shift+Tab事件不会被event(也就不会被keyPressEvent)所处理,而是直接在子组件之间切换焦点。通过测试的确是这样的,这里例子成立的条件是没有可切换焦点的子组件,那么Tab和Shift+Tab则会被event接受到。
感谢作者的教程,受益匪浅。

回复
Nephren 2021年6月23日 - 19:49

上面说的问题在QWidget::event里找到了代码
```
case QEvent::KeyPress: {
QKeyEvent *k = (QKeyEvent *)event;
bool res = false;
if (!(k->modifiers() & (Qt::ControlModifier | Qt::AltModifier))) { //### Add MetaModifier?
if (k->key() == Qt::Key_Backtab
|| (k->key() == Qt::Key_Tab && (k->modifiers() & Qt::ShiftModifier)))
res = focusNextPrevChild(false);
else if (k->key() == Qt::Key_Tab)
res = focusNextPrevChild(true);
if (res)
break;
}
keyPressEvent(k);
```
但是如果我在一个外层MainWindow里面放CustomWidget,里面再放Button的话,最后按下Tab的时候外层Mainwindow、里层CustomWidget还是Button都收不到Tab,所以我很好奇这个切换焦点操作是在什么位置执行的。看来还需要进一步了解。

回复

回复 Alien You 取消回复

关于我

devbean

devbean

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

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