【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
类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递,所以,虽然这两种方法功能很强大,但是却很少被用到。
接下来通过一个案例,来演示事件的传递流程。
自定义一个标签控件 PropagateLabel
,让它继承自 QLabel
,然后重写父类的 mousePressEvent()
。
首先,在左侧项目文件名上右键,然后选择 “添加新文件”,选择 “C++ Class”,如下:
新建类文件信息如下:
然后,把父类修改为 QLabel
来到 propagatelabel.h
将父类由 QWidget
修改为 QLabel
,如下:
#include
class PropagateLabel : public QLabel
{
// ...
};
来到 propagatelabel.cpp
将父类由 QWidget
修改为 QLabel
,如下:
#include "propagatelabel.h"
PropagateLabel::PropagateLabel(QWidget* parent) : QLabel{parent}
{
}
首先,来到 propagatelabel.h
,声明它:
class PropagateLabel : public QLabel
{
private:
void mousePressEvent(QMouseEvent* event);
};
然后,来到 propagatelabel.cpp
实现它:
#include
void PropagateLabel::mousePressEvent(QMouseEvent* event)
{
qDebug() << "PropagateLabel::mousePressEvent";
}
首先,在 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()
函数
到目前为止,都是前面讲解过的内容。
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()
函数,打印如下:
通过查看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;
}
接下来演示 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()
函数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
,表示事件被识别,传递到此为止,不会接着传递。
此时运行,控制台打印如下:
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()
此时运行,控制台打印如下:
前面讲解的事件处理函数比如 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
值:
父类中 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()
函数从Qt程序的入口开始:
#include "mywindow.h"
#include
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()
函数,这里就略过!