【Qt】事件处理、事件分发器、事件过滤器
事件处理
- 一. 事件
- 事件处理
- 鼠标事件处理
- 按键事件处理
- 定时器事件处理
- 窗口事件处理
- 二. 事件分发器
- 三. 事件过滤器
虽然 Qt 是跨平台的 C++ 开发框架,Qt 的很多能力其实是操作系统提供的,只不过 Qt 封装了系统 API,程序是运行在操作系统上的,需要系统给我们提供支撑。如:事件、文件操作、多线程编程、网络编程、多媒体。
一. 事件
- 信号槽:用户进行的各种操作,就可能会产生出信号,可以给某个信号指定槽函数,当信号触发的时候,就能够自动的执行到对应的槽函数。
- 事件:用户进行的各种操作,也会产生事件,程序员同样可以给事件关联上处理函数 (处理的逻辑),当事件触发的时候,就能够执行到对应的代码。
- 事件本身是操作系统提供的机制,Qt 也同样把操作系统事件机制进行了封装,拿到了 Qt 中,但是由于事件对应的代码编写起来不是很方便。
- Qt 对于事件机制又进一步的封装,就得到了信号槽,信号槽就是对于事件的进一步封装,事件是信号槽的低层机制。
- 实际 Qt 开发程序的过程中,绝大部分和用户之间进行的交互都是通过 “信号槽” 来完成的,有些特殊情况下,信号槽不一定能搞定 (某个用户的动作行为,Qt 没有提供对应的信号),此时就需要通过重写事件处理函数,来手动处理事件的响应逻辑。
- 开发事件机制给我们程序员,我们就可以根据实际的需要进行更深度的定制化 DIY (自己动手做) 操作了。
- 用户进行了很多操作,就会产生很多的事件,当然也会产生出很多的信号。
Qt 中使用个对象来表示一个事件,所有的 Qt 事件均继承抽象类 QEvent。
- 事件是由系统或者 Qt 平台本身在不同的时刻发出的,当用户按鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。
- 一些事件是在用户操作时发出,如键盘事件、鼠标事件等,另一些事件则是由系统本身自动发出,如定时器事件。
常见的 Qt 事件如下:
常见事件描述:
事件处理
事件的处理:让一段代码和某个事件关联起来,当事件触发的时候,就能够执行这段代码。
- 之前信号槽这里通过 connect 来完成上述关联的。
- 对于事件来说不一样,让当前的类,重写某个事件处理函数,这里用到的是 “多态” 机制,创建子类,继承 Qt 已有的类,在子类中重写父类的事件处理函数,后续事件触发的过程中,就会通过多态这样的机制,执行到我们自己写的子类的函数。
- 事件处理一般常用的方法为:重写相关的 Event 函数。在 Qt 中,几乎所有的 Event 函数都是虚函数,所以可以重新实现。如:在实现鼠标的进入和离开事件时,直接重新实现 enterEvent() 和 leaveEvent() 即可。
enterEvent() 和 leaveEvent() 函数原型如下:
代码:鼠标进入和离开控件
给 ui 文件设置 QLabel,并添加上边框,为了方便观察当前鼠标是否进入和离开,如下:
这里需要创建 QLabel 的子类,重写 enterEvent 和 leaveEvent,如下:
要想重写父类的函数,就需要确保你这边写的函数名字和函数的参数列表都完全一致 (形参名无所谓),谨防单词拼写错,正常来说 Qt Create 应该要能够提示出来的,但实际上没有提示。
运行代码后,发现鼠标进入或者离开 QLabel 时,应用程序输出中并没有打印出 enterEvent 和 leaveEvent,如下:
原因分析:我们在 ui 文件中使用的控件是 QLabel,而 QLabel 默认不处理 enterEvent 和 leaveEvent,而且要启用鼠标跟踪功能也需要额外的操作。
我们需要在 ui 文件中,使用自己定义的 Label,如下:
通过 “提升为” 这样的方式,就可以把 Qt Designed 中拖上去的控件的类型转换为自定义的控件类型,如下:
运行程序后,此时就说明当前的 enterEvent 和 leaveEvent 这两个事件就被我们给捕捉到了,如下:
鼠标事件处理
在 Qt 中,鼠标事件是用 QMouseEvent 类来实现的。
- 当在窗口中 “按下鼠标” 或者 “移动鼠标” 时,都会产生鼠标事件。
- 利用 QMouseEvent 类可以获取鼠标的哪个键被按下了以及鼠标的当前位置等信息。
- 在 Qt 帮助文档中,可以查找 QMouseEvent 类,如下:
代码:鼠标点击事件
在 Qt 中,鼠标按下时,通过执行虚函数 QWidget::mousePressEvent(QMouseEvent *event) 来捕获的。而其中鼠标可以通过 “左键”、“右键”、“滚轮” 等等按下,如何区分?通过 event->button() 函数的返回值区分。
- 鼠标左键:Qt::LeftButton
- 鼠标左键:Qt::RightButton
- 鼠标滚轮:Qt::MidButton
同理将 QLabel 提升为我们自定义的 Label,如下:
修改 label.h 和 label.cpp 文件,如下:
代码:鼠标释放事件
鼠标释放事件是通过虚函数 QWidget::mouseReleaseEvent(QMouseEvent *event) 来捕获的。
代码:鼠标双击事件
鼠标双击事件是通过虚函数 QWidget::mouseDoubleClickEvent(QMouseEvent *event) 来实现的。
注意:这里第二次按下的时候,才能够识别时 “双击”,比如:程序可能有一些单击逻辑、有另一些双击逻辑,如果我们没注意到,可能双击操作就会触发单击的逻辑,可能就有 bug
代码:鼠标移动事件
- 鼠标移动事件是通过虚函数 QWidget::mouseMoveEvent(QMouseEvent *event) 来实现的。
- 同时为了实时捕获鼠标位置信息,需要通过函数 setMouseTracking(bool enable) 来追踪鼠标的位置。
- 说明:setMouseTracking() 函数默认是 false,需要设置为 true,才能实时捕获鼠标位置信息。否则只有当鼠标按下时才能捕获其位置信息。
刚才重写鼠标事件的操作,都是在自定义的 Label 中完成的,此时鼠标只有在 Label 范围内进行动作的时候,才能捕获到,也可以把这些操作直接放到 Widget (QWidget 的子类) 来完成,这样的话,鼠标在整个窗口中进行的各种动作都能获取到了。如下:
注意:
- 鼠标移动不同于鼠标按下,随便移动一下鼠标,就会产生大量的鼠标移动事件,当你进行捕获事件的时候,久其是在这里再进行一些复杂逻辑的时候,程序负担就很重,很容易产生卡顿之类的情况。
- Qt 为了保证程序的流畅性,默认情况下不会对鼠标移动进行追踪,鼠标移动的时候不会调用 mouseMoveEvent(),除非显示告诉 Qt 就要追踪鼠标位置。
如下:
代码:鼠标滚轮事件
在 Qt 中,鼠标滚轮事件是通过 QWindow::wheelEvent(QWheelEvent *event) 函数来实现的。滚轮滑动的距离可以通过 event->delta() 函数获取。
- 返回值:滚轮滑动的距离,正数表示滚轮相对于用户向前滑动,负数表示滚轮相对于用户向后滑动。
可以通过滚轮可以实现缩放字体的大小!
按键事件处理
- 要想获取到用户的键盘按键,可以通过 QShortCut,这是信号槽机制封装过,获取键盘按键的方式,站在更底层的角度,也可以通过事件获取到当前用户键盘按键按下的情况。
- Qt 中的按键事件是通过 QKeyEvent 类来实现的。当键盘上的按键被按下或者被释放时,键盘事件便会触发。在帮助文档中查找 QKeyEvent 类如下:
代码:按下单个按键
按下单个按键是通过虚函数 QWidget::keyPressEvent(QKeyEvent *event) 来实现的,搭配 event->key() 函数,来获取哪个按键被按下,如下:
代码:按下组合按键
在 Qt 助手中搜索:Qt::KeyboardModifier,如下图示:
Qt::KeyboardModifier 中定义了在处理键盘事件时对应的修改键。在 Qt 中,键盘事件可以与修改键一起使用,以实现一些复杂的交互操作。KeyboardModifier 中修改键的具体描述如下:
通过 event->modifiers() 函数,判断按下了哪一个修改键,如下:
定时器事件处理
Qt 中在进行窗口程序的处理过程中,经常要周期性的执行某些操作,或者制作一些动画效果,使用定时器就可以实现。所谓定时器就是在间隔一定时间后,去执行某一个任务。定时器在很多场景下都会使用到,如弹窗自动关闭之类的功能等。
Qt 中的定时器分为 QTimerEvent 和 QTimer 这两个类:
- QTimerEvent 类,用来描述一个定时器事件。在使用时需要通过 QObject::startTimer() 函数来开启一个定时器,这个函数需要输入一个以毫秒为单位的整数作为参数来表明设定的时间,它返回的整型值代表这个定时器。当定时器溢出时 (即定时时间到达) 就可以在 QObject::timerEvent() 函数中获取该定时器的编号来进行相关操作。
- QTimer 类,用来实现一个定时器,背后是 QTimerEvent 定时器事件进行支撑的,它提供了更高层次的编程接口,例如:可以使用信号和槽,还可以设置只运行一次的定时器。
代码:基于 QTimerEvent 定时器,实现倒计时
通过 QObject::startTimer(int msec) 这个虚函数,开启定时器,返回一个身份标识符,用于标识这个定时器,功能就是每隔一段时间,执行 QObject::timerEvent(QTimerEvent *event) 函数,通过 event->timerId() 函数来获取定时器标识符,通过 QObject::killTimer(int id) 函数来取消某个定时器,如下:
代码:基于 QTimer 定时器,实现倒计时
通过创建 QTimer 对象,调用其中的 start(int msec) 函数来启动定时器,每隔一段时间定时器对象就会发出 QTimer::timeout 信号,此时需要将该信号和处理定时器的槽函数建立连接,通过 stop() 函数来停止定时器,如下:
总结:使用 timerEvent 比 QTimer 还是要更复杂一些,需要手动管理 timerId,还需要区分这次函数调用是哪个 timerId 引起的,后续实际开发中,使用 QTimer 即可。
代码:获取系统日期及时间
- 在 Qt 中,获取系统的日期及实时时间可以通过 QTimer 类 和 QDateTime 类,QDateTime 类提供了字符串格式的时间。
- 字符串形式的时间输出格式由 toString() 方法中的 format 参数列表决定,可用的参数列表如下:
在 ui 文件中,放置一个 QLabel 控件,用来显示日期及时间,同时放置两个按钮 “开始” 和 “停止”,如下:
窗口事件处理
- 通过 QWidget::moveEvent(QMoveEvent *event) 虚函数,是窗口移动时,触发的事件。
- 通过 QWidget::resizeEvent(QResizeEvent *event) 虚函数,是窗口大小改变时,触发的事件。
代码:窗口移动、窗口大小改变,事件的处理
二. 事件分发器
事件分发器概述:
- 在 Qt 中,事件分发器 (Event Dispatcher) 是一个核心概念,用于处理 GUI 应用程序中的事件。
- 事件分发器负责将事件从一个对象传递到另一个对象,直到事件被处理或被取消。
- 每个继承 QObject 类 或 QObject 类本身,都可以在本类中重写 bool event(QEvent *event) 函数,来实现相关事件的捕获和拦截。
事件分发器工作原理:
- 在 Qt 中,我们发送的事件都是传给了 QObject 对象,更具体点是传给了 QObject 对象的 event() 函数。
- 所有的事件都会进入到 event() 函数里面,那么我们处理事件就要重写这个 event() 函数。
- event() 函数本身不会去处理事件,而是根据 事件类型 (type值) 调用不同的事件处理函数。
- 事件分发器就是工作在应用程序向下分发事件的过程中,如下图:
如上图,事件分发器用于分发事件。在此过程中,事件分发器也可以做拦截操作。事件分发器主要是通过 bool event(QEvent *event) 函数来实现。其返回值为布尔类型,若为 ture 代表拦截,不向下分
发。
Qt 中的事件是封装在 QEvent 类中,在 Qt 助手中输入 QEvent 可以查看其所包括的事件类型,如下图示:
代码:鼠标点击事件 和 事件分发器
事件处理顺序:
- 当鼠标按下事件发生时,系统首先调用 event() 方法,判断事件类型是否为 QEvent::MouseButtonPress:
- 如果是:则返回 true 表示事件已被处理,不会继续分发给 mousePressEvent()
- 如果不是: event() 返回 QWidget::event(event),表示交给父类处理,结果就是,事件会继续分发到 mousePressEvent(),处理鼠标按下事件。
三. 事件过滤器
在 Qt 中,一个对象可能经常要查看或拦截另外⼀个对象的事件,如对话框想要拦截按键事件,不让别的组件接收到,或者修改按键的默认值等。通过上面,已经知道,Qt 创建了 QEvent 事件对象之后,会调用 QObject::event() 函数来处理事件的分发。
显然,我们可以在 event() 函数中实现拦截的操作,由于 event() 函数是 protected 的,因此需要继承已有类。如果组件很多,就需要重写很多个 event() 函数。这当然相当麻烦,更不用说重写 event() 函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。
事件过滤器是在应用程序分发到 event 事件分发器之前,再做一次更高级的拦截,如下图示:
事件过滤器的一般使用步骤:安装事件过滤器、重写事件过滤器函数 eventfilter()
void QObject::installEventFilter(QObject *filterObj)