本节对应的视频讲解:B_站_链_接
【QT开发笔记-基础篇】 第4章 事件 4.10 总结事件传递流程(1) 事件处理函数接受还是忽略
本章要实现的整体效果如下:
事件传递总流程图,如下:
这张图是不是还不太明白??
别担心,本节课彻底搞懂事件的传递流程!!!
程序的入口 main()
函数的最后都会调用 QApplication
类的 exec()
函数。它会使 Qt 应用程序进入事件循环。这样就可以使应用程序在运行时接收发生的各种事件。一旦有事件发生,Qt 便会构建一个相应的QEvent
子类的对象来表示它,然后将它传递给相应的 QObject
对象或其子对象。下面通过例子来看一下Qt中的事件传递过程。
Qt 中的事件由特定的 QEvent
子类来表示,比如鼠标事件对应的子类为 QMouseEvent
,键盘事件对应的子类为 QKeyEvent
等。
通常,一个事件包含多个事件类型,比如鼠标事件包含鼠标按下、鼠标移动和鼠标释放三个事件类型。键盘事件包含键盘按下和键盘释放两个事件类型。这些事件类型都由 QEvent
类的枚举型 QEvent::Type
来表示,其中包含了 一百多种事件类型,可以在 QEvent
类的帮助文档中查看。
虽然 QEvent
的子类用来表示一个事件,那么应该怎样来处理一个事件呢?在 QCoreApplication
类的 notify()
函数的帮助文档处给出了 5
种处理事件的方法:
- 方法一:重新实现部件的
paintEvent()
、mousePressEvent()
等事件处理函数。这是最常用的一种方法,不过它只能用来处理特定部件的特定事件。 - 方法二:重新实现
notify()
函数。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。 - 方法三:向
QApplication
对象上安装事件过滤器。因为一个程序只有一个QApplication
对象,所以这样实现的功能与使用notify()
函数是相同的,优点是可以同时处理多个事件。 - 方法四:重新实现
event()
函数。QObject
类的event()
函数可以在事件到达默认的事件处理函数之前获得该事件。 - 方法五:在对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。
**在实际编程中,最常用的是方法一,其次是方法五。**因为方法二需要继承自 QApplication
类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递,所以,虽然这两种方法功能很强大,但是却很少被用到。
接下来通过一个案例,来演示事件的传递流程。
1. 事件处理函数
自定义一个标签控件 PropagateLabel
,让它继承自 QLabel
,然后重写父类的 mousePressEvent()
。
1.1 添加自定义控件类 PropagateLabel
首先,在左侧项目文件名上右键,然后选择 “添加新文件”,选择 “C++ Class”,如下:
新建类文件信息如下:
然后,把父类修改为 QLabel
来到 propagatelabel.h
将父类由 QWidget
修改为 QLabel
,如下:
#include <QLabel>
class PropagateLabel : public QLabel
{
// ...
};
来到 propagatelabel.cpp
将父类由 QWidget
修改为 QLabel
,如下:
#include "propagatelabel.h"
PropagateLabel::PropagateLabel(QWidget* parent) : QLabel{parent}
{
}
1.2 重写 mousePressEvent()
首先,来到 propagatelabel.h
,声明它:
class PropagateLabel : public QLabel
{
private:
void mousePressEvent(QMouseEvent* event);
};
然后,来到 propagatelabel.cpp
实现它:
#include <QDebug>
void PropagateLabel::mousePressEvent(QMouseEvent* event)
{
qDebug() << "PropagateLabel::mousePressEvent";
}
1.3 将 PropagateLabel 显示到界面
首先,在 propagate_widget.h
中,声明 PropagateLabel
类型成员变量:
#include "propagatelabel.h"
class PropagateWidget : public QWidget
{
private:
PropagateLabel* lbl;
};
然后,来到 propagate_widget.cpp
,在构造函数中添加 PropagateLabel
控件,如下:
PropagateWidget::PropagateWidget(QWidget* parent) : QWidget{parent}
{
QVBoxLayout* verticalLayout = new QVBoxLayout(this);
verticalLayout->setSpacing(0);
verticalLayout->setContentsMargins(0, 0, 0, 0);
// 1. 添加一个自定义的标签 LabelX
lbl = new PropagateLabel(this);
lbl->setText("");
lbl->setFrameShape(QFrame::Box);
lbl->setFixedHeight(50);
lbl->setAlignment(Qt::AlignCenter);
lbl->setStyleSheet("background-color: red;color: white;font-size: 25px");
verticalLayout->addWidget(lbl);
}
此时运行程序,效果如下:
此时,点击标签,就会执行自定义的 PropagateLabel
控件的 mousePressEvent()
函数
到目前为止,都是前面讲解过的内容。
1.4 接受/忽略事件
PropagateLabel
控件的 mousePressEvent()
函数,处理事件后,可以决定是 “接受”( accept
) 还是 “忽略”( ignore
) 这个事件。
Qt 框架传递过来的这个 event
, 它有一个标志位 m_accept
:
-
把该事件的
m_accept
标志设置为true
:该事件的传递,到此为止。则该事件不会传递给其父控件,也就是
PropagateWidget
-
把该事件的
m_accept
标志设置为false
:该事件会再传递给其父控件,也就是
PropagateWidget
那么,如何设置 m_accept
标志位呢?
- 忽略事件(事件继续传递给父控件)
方法一:
event->ignore();
跳转到 ignore() 的实现,可见它就是直接设置 m_accept 为 false
方法二:
调用父类的实现
// QLineEdit 是直接继承自父类 QWidget 中的 keyReleaseEvent
QLineEdit::keyReleaseEvent(event);
而父类 QWidget 中的 keyReleaseEvent 实现,就是直接调用 event->ignore() 如下:
void QWidget::keyReleaseEvent(QKeyEvent *event)
{
event->ignore();
}
可见,不管是方法一,还是方法二,最终都是设置 event 中的 m_accept 标志位。
- 接受事件(事件到此为止,不再传递给其父控件)
event->accept();
跳转到 accept() 的实现,可见它就是直接设置 m_accept 为 true
然而,由于该标志位刚传递过来的值为 true(可以加打印查看),因此不需要显示地调用 accept()
接下来,看PropagateLabel
控件的鼠标按下事件能否传递到其父控件 PropagateWidget
首先,来到 propagate_widget.h
中,声明 mousePressEvent()
函数,如下:
class PropagateWidget : public QWidget
{
private:
void mousePressEvent(QMouseEvent* event);
};
然后,来到 propagate_widget.cpp
中,实现 mousePressEvent()
函数,如下:
void PropagateWidget::mousePressEvent(QMouseEvent* event)
{
qDebug() << "PropagateWidget::mousePressEvent";
}
此时可以修改 propagatelabel.cpp
中的 mousePressEvent()
函数,来决定事件是否继续向上传递:
void PropagateLabel::mousePressEvent(QMouseEvent* event)
{
qDebug() << "PropagateLabel::mousePressEvent";
// 接受事件(事件到此为止,不再传递给其父控件)
// 方法一:
// event->accept();
// 方法二:什么都不用写。因为传递过来的 event 其 accept 标志位默认为 true,可打印验证
// qDebug() << event->isAccepted();
// 忽略事件(事件继续传递给父控件)
event->ignore();
}
以上忽略事件之后,事件会传递到propagate_widget.cpp
的 mousePressEvent()
函数,打印如下:
2. 事件分发函数 event()
通过查看Qt源码 qwidget.cpp
,可以得知:
在 event()
中,通过区分不同的事件类型来调用特定的事件处理函数
也就是说,可以在事件到达默认的事件处理函数之前,在 event()
中截获
bool QWidget::event(QEvent *event)
{
switch (event->type()) {
case QEvent::MouseMove:
mouseMoveEvent((QMouseEvent*)event);
break;
case QEvent::MouseButtonPress:
mousePressEvent((QMouseEvent*)event);
break;
case QEvent::MouseButtonRelease:
mouseReleaseEvent((QMouseEvent*)event);
break;
case QEvent::MouseButtonDblClick:
mouseDoubleClickEvent((QMouseEvent*)event);
break;
case QEvent::Drop:
dropEvent((QDropEvent*) event);
break;
case QEvent::DragEnter:
dragEnterEvent((QDragEnterEvent*) event);
break;
case QEvent::DragMove:
dragMoveEvent((QDragMoveEvent*) event);
break;
case QEvent::DragLeave:
dragLeaveEvent((QDragLeaveEvent*) event);
break;
default:
return QObject::event(event);
}
return true;
}
2.1 重写 event() 函数
接下来演示 event()
函数如何使用
首先,在 propagate_label.h
中,声明 event()
函数:
class PropagateLabel : public QLabel
{
private:
bool event(QEvent* e);
};
然后,在 propagate_label.cpp
中,实现 event()
函数:
bool PropagateLabel::event(QEvent* e)
{
if ( e->type() == QEvent::MouseButtonPress ) {
qDebug() << "PropagateLabel::event";
}
return QLabel::event(e); // 调用父类的 event() 函数
}
这里,仅仅是根据事件类型,做了一个打印。
最后一般调用父类的 event()
函数,这样可以接着把事件分发到特定的事件处理函数。如果直接返回 true
或 false
,就无法将事件传递给特定的事件处理函数!
此时运行,控制台打印如下:
事件传递流程:
- 事件首先到达
PropagateLabel
中的event()
函数 - 由于上一步的
event()
函数,调用了父类的event()
函数,因此事件被分发到PropagateLabel
中的mouseMoveEvent()
函数 - 由于
PropagateLabel
中的mouseMoveEvent()
函数,忽略了该事件,因此事件又被传递到PropagateWidget
中的mouseMoveevent()
函数
2.2 event() 返回 true
event()
函数返回一个 bool 值
通常直接 return QLabel::event(e);
,这样可以接着把事件分发到特定的事件处理函数
如果不调用父类的 event() 函数,而是直接返回 true 或者 false,会有什么效果呢?
bool PropagateLabel::event(QEvent* e)
{
if ( e->type() == QEvent::MouseButtonPress ) {
qDebug() << "PropagateLabel::event";
return true;
}
return QLabel::event(e);
}
以上对于 QEvent::MouseButtonPress
事件,直接返回 true
,表示事件被识别,传递到此为止,不会接着传递。
此时运行,控制台打印如下:
2.3 event() 返回 false
event()
函数返回一个 bool 值
通常直接 return QLabel::event(e);
,这样可以接着把事件分发到特定的事件处理函数
如果不调用父类的 event() 函数,而是直接返回 true 或者 false,会有什么效果呢?
bool PropagateLabel::event(QEvent* e)
{
if ( e->type() == QEvent::MouseButtonPress ) {
qDebug() << "PropagateLabel::event";
return false;
}
return QLabel::event(e);
}
以上对于 QEvent::MouseButtonPress
事件,直接返回 false,表示事件没有被识别,会接着传递。由于没有调用调用父类的 event()
函数,因此不会将事件分发到 PropagateLabel
的 mousePressEvent()
,而是直接传递给父控件的 mousePressEvent()
此时运行,控制台打印如下:
3. 事件过滤函数
前面讲解的事件处理函数比如 mouseMoveEvent()
函数,以及事件分发函数 event()
,都是事件已近到达了控件,那么在事件到达控件之前,能不能提前拦截,或者说过滤呢?
答:可以,给控件安装事件过滤器。
首先,来到 propagate_widget.cpp
构造,为 PropagateLabel
安装事件过滤器:
PropagateWidget::PropagateWidget(QWidget* parent) : QWidget{parent}
{
// ...
lbl->installEventFilter(this);
}
然后,在 propagate_widget.h
中声明 eventFilter()
函数:
class PropagateWidget : public QWidget
{
private:
bool eventFilter(QObject *watched, QEvent *event);
};
并在 propagate_widget.cpp
中实现:
bool PropagateWidget::eventFilter(QObject* watched, QEvent* event)
{
if ( watched == lbl && event->type() == QEvent::MouseButtonPress ) {
qDebug() << "PropagateWidget::eventFilter";
}
return QWidget::eventFilter(watched, event);
}
这里,仅仅是做了一个打印。
eventFilter()
返回一个 bool
值:
- 返回 true:则事件到此为止,不再向下传递,也就是不再传递到对应的控件
- 返回 false:则事件继续传递,也就是接着会传递到对应的控件
父类中 eventFilter()
函数,在 QObject 类中实现,源码对应 qobject.cpp
(在 qwidget.cpp
中没有实现):
bool QObject::eventFilter(QObject * /* watched */, QEvent * /* event */)
{
return false;
}
可见,只是简单地返回了一个 false,表示让事件接着传递。
因此,上面的
return QWidget::eventFilter(watched, event);
等价于
return false;
此时运行,控制台打印如下:
事件传递流程:
- 事件首先到达
PropagateWidget
中的eventFilter()
函数 - 由于上一步的
eventFilter()
函数,不管是调用父类的eventFilter()
函数,还是直接返回false
,都表示要继续事件的传递。因此事件被分发到PropagateLabel
中的event()
函数 - 由于
PropagateLabel
中的event()
函数,直接调用父类的event()
函数,因此事件被分发到PropagateLabel
中的mouseMoveevent()
函数 - 由于
PropagateLabel
中的mouseMoveevent()
函数,忽略了该事件,因此事件又被传递到PropagateWidget
中的mouseMoveevent()
函数
4. 事件传递-画图演示
从Qt程序的入口开始:
#include "mywindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
// 1. QApplication 是 Qt 框架提供的应用程序类
// 作用:负责 Qt 中事件的处理,比如鼠标的单击事件,键盘的输入事件等
QApplication a(argc, argv);
// 2. 创建自己的窗口对象,并调用其 show 方法,将窗口显示出来
MyWindow w;
w.show();
// 3. 调用 QApplication 类的 exec 方法,应用程序就阻塞在这里,并不会退出,而是进入到事件循环的处理, 直到退出程序(比如点击了窗体右上角的关闭按钮)
return a.exec();
}
可见程序启动后,就是进入一个事件的循环,事件的传递流程如下:
图示中除了 notify() 函数,其他都讲到了。
notify()
是 QCoreApplication
类中的方法,QApplication
是其子类。main()
函数最后调用 return a.exec()
,就进入了事件循环。
如果要重写 notify()
函数,需要自定义一个 QApplication
类。实际工作中,一般不会重写 notify()
函数,这里就略过!