Qt事件系统学习笔记
文章目录
- 前言
- ✅ 1. 事件生成与投递(起点)
- ✅ 2. 事件过滤器(可选拦截)
- ✅ 3. QWidget::event() 统一入口
- ✅ 4. 具体事件处理函数(最终生效点)
- ✅ 5. 事件接受/忽略机制
- ✅ 6. 事件冒泡(若未被接受)
- ✅ 一句话总结
- 1. Qt中的事件实例
- 2. 事件过滤器
- 3. 事件循环
前言
在 Qt 中,事件作为一个对象,继承自 QEvent 类,常见的有键盘事件 QKeyEvent、鼠标事件 QMouseEvent 和定时器事件 QTimerEvent 等,与 QEvent 类的继承关系图如下所示。本章会详细讲解这 3 个常见的事件,还会涉及事件过滤器和自定义事件的知识。关于本章的相关内容,可以在 Qt 帮助中通过 The Event System 关键字查看。
在 Qt 中,一个事件(如鼠标点击、键盘输入)在部件(widget)中真正“生效”,必须经历一个分层传递+处理链的过程。这个链条由以下几级组成:
✅ 1. 事件生成与投递(起点)
事件由系统或 Qt 内部生成,最终通过 QCoreApplication::notify() 投递给目标对象(通常是某个 QWidget)。
✅ 2. 事件过滤器(可选拦截)
如果该对象或其父对象安装了事件过滤器(installEventFilter()),事件会优先传给过滤器的 eventFilter() 函数:
若返回 true,事件被拦截,不再向下传递;
若返回 false,事件继续传给目标对象。
✅ 3. QWidget::event() 统一入口
事件进入目标部件的 event(QEvent *e) 函数,这是事件分发的总调度中心:
根据事件类型(如 QEvent::MouseButtonPress),调用对应的处理函数;
若你在 event() 中返回 true,事件被视为已处理,不再向下传递。
✅ 4. 具体事件处理函数(最终生效点)
event() 内部会根据事件类型,调用如下函数
事件类型 | 处理函数 |
---|---|
鼠标按下 | mousePressEvent(QMouseEvent *) |
键盘按下 | keyPressEvent(QKeyEvent *) |
重绘 | paintEvent(QPaintEvent *) |
大小变化 | resizeEvent(QResizeEvent *) |
这些函数通常是你重写以让事件“生效”的地方。
✅ 5. 事件接受/忽略机制
每个事件对象都有 accept() 和 ignore() 方法:
accept():表示事件已处理,不再向上传递给父组件;
ignore():表示事件未处理,继续冒泡给父组件。
✅ 6. 事件冒泡(若未被接受)
如果当前部件未处理事件(或显式 ignore()),事件会向父组件逐级传递,直到被处理或到达顶层窗口。
✅ 示例流程图(以点击按钮为例)
用户点击按钮↓
QApplication::notify()↓
按钮的 eventFilter()(如果安装了)↓
按钮的 event()↓
按钮的 mousePressEvent()↓(若 ignore())
父组件的 mousePressEvent()
✅ 一句话总结
事件在部件生效 = 过滤器放行 + event() 分发 + 具体处理函数执行 + accept() 标记。
任何一个环节拦截或接受,事件就不再继续传递。
1. Qt中的事件实例
在每个程序的 main() 函数的最后都会调用 QApplication 类的 exec() 函数,它会使 Qt 应用程序进人事件循环,这样就可以使应用程序在运行时接收发生的各种事件。一旦有事件发生,Qt 便会构建一个相应的 QEvent 子类的对象来表示,然后将它传递给相应的 QObject 对象或其子对象。下面通过例子来看一下 Qt 中的事件传递过程。
新建 Qt Gui 应用,项目名称为 myEvent,基类选择 QWidget,然后类名保持 Widget 不变。建立完成后向项目中添加新文件,模板选择 C++ 类,类名为 MyLineEdit,基类手动填写为 QLineEdit,自定义了一个 MyLineEdit 类。
mylineEdit. h 文件:
#ifndef MYLINEEDIT_H
#define MYLINEEDIT_H#include <QLineEdit>class MyLineEdit : public QLineEdit
{Q_OBJECT
public:explicit MyLineEdit(QWidget *parent = nullptr);// event()函数获取事件的类型bool event(QEvent *event); protected:// MyLineEdit类的键盘按下事件void keyPressEvent(QKeyEvent *event);
};#endif // MYLINEEDIT_H
这里添加了 keyPressEvent() 函数和 event() 函数的声明。
mylineEdit. cpp 文件:
#include "mylineedit.h"
#include <QKeyEvent>
#include <QDebug>MyLineEdit::MyLineEdit(QWidget *parent) :QLineEdit(parent)
{}// MyLineEdit类的键盘按下事件
void MyLineEdit::keyPressEvent(QKeyEvent *event)
{qDebug() << tr("MyLineEdit键盘按下事件");// 让MyLineEdit输入栏能输入字符QLineEdit::keyPressEvent(event); // 执行QLineEdit类的默认事件处理event->ignore(); // 忽略该事件
}//event()函数获取事件的类型
bool MyLineEdit::event(QEvent *event)
{// 判断触发事件类型是否为键盘按下事件if(event->type() == QEvent::KeyPress)qDebug() << tr("MyLineEdit的event()函数");return QLineEdit::event(event); // 执行QLineEdit类event()函数的默认操作
}
这里自定义了一个 MyLineEdit 类,它继承自 QWidget,并且实现了 MyLineEdit 类的 keyPressEvent() 函数和 event() 函数。event() 函数中使用了 event->type() 来获取事件的类型。如果是键盘按下事件 QEvent::KeyPress,则输出信息,另外返回父类的 event() 函数的操作结果。
widget.h 文件:
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
class MyLineEdit;
namespace Ui {
class Widget;
}class Widget : public QWidget
{Q_OBJECTpublic:explicit Widget(QWidget *parent = 0);~Widget();// Widget类的事件过滤器bool eventFilter(QObject *obj, QEvent *event); private:Ui::Widget *ui;MyLineEdit *lineEdit;protected:// Widget类的键盘按下事件void keyPressEvent(QKeyEvent *event);
};#endif // WIDGET_H
这里也添加了keyPressEvent()函数的声明。
widget.cpp 文件:
#include "widget.h"
#include "ui_widget.h"
#include "mylineedit.h"
#include <QKeyEvent>
#include <QDebug>Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget)
{ui->setupUi(this);lineEdit = new MyLineEdit(this);lineEdit->move(100, 100);
}Widget::~Widget()
{delete ui;
}// Widget类的键盘按下事件
void Widget::keyPressEvent(QKeyEvent *event)
{qDebug() << tr("Widget键盘按下事件");
}// Widget类的事件过滤器
bool Widget::eventFilter(QObject *obj, QEvent *event) // 事件过滤器
{// 如果是lineEdit部件上的事件if(obj == lineEdit){ if(event->type() == QEvent::KeyPress)qDebug() << tr("Widget的事件过滤器");}return QWidget::eventFilter(obj, event);
}
这里也实现了 Widget 类的 keyPressEvent() 函数,并且会调用 MyLineEdit 类的 keyPressEvent() 函数。在事件过滤器中,先判断该事件的对象是不是 lineEdit,如果是,再判断事件类型,最后返回 QWidget 类默认的事件过滤器的执行结果。
运行程序,然后按下键盘的任意键,比如这里按下 a 键,执行结果如下图所示。
可以看到,事件的传递顺序是这样的:先是事件过滤器,然后是焦点部件的 event() 函数,最后是焦点部件的事件处理函数,例如这里的键盘按下事件函数;如果焦点部件忽略了该事件,那么会执行父部件的事件处理函数,如上图所示。注意,event() 函数和事件处理函数,是在该部件内进行重新定义的,而事件过滤器却是在该部件的父部件中进行定义的。
2. 事件过滤器
Qt中提供了事件过滤器来实现在一个部件中监控其他多个部件的事件。事件过滤器与其他部件不同,它不是一个类,只是由两个函数组成的一种操作,用来完成一个部件对其他部件的事件的监视。这两个函数分别是installEventFilter()和eventFilter(),都是QObject类中的函数。下面通过具体的例子来进行讲解。
在widget.h文件中添加public函数声明:
bool eventFilter(QObject* obj,QEvent *event):
然后在widget.cpp文件中添加头文件
#include < QKeyEvent >
#include< QWheelEvent >
在构造函数中添加代码:
ui->textEdit->installEventFilter(this);//为编辑器部件在本窗口上安装事件过滤器
ui->spinBox->installEventFilter(this);
要对一个部件使用事件过滤器,那么就要先使用其的 installEventFilter()函数为其安装事件过滤器,这个函数的参数表明了监视对象。
这里就为textEdit部件和 spin-Box部件安装了事件过滤器,其参数this表明要在本部件(即 Widget)中监视 textEdit和spinBox的事件。
这样,就需要重新实现 Widget类的eventFilter()函数,在其中截获并处理两个子部件的事件。
下面实现事件过滤器函数
bool Widget::eventFilter(QObject *obj, QEvent *event) // 事件过滤器
{if (obj == ui->textEdit) { // 判断部件if (event->type() == QEvent::Wheel) { // 判断事件// 将event强制转换为发生的事件的类型QWheelEvent *wheelEvent = static_cast<QWheelEvent*>(event);if (wheelEvent->delta() > 0) ui->textEdit->zoomIn();else ui->textEdit->zoomOut();return true; // 该事件已经被处理} else {return false; // 如果是其他事件,可以进行进一步的处理}}else if (obj == ui->spinBox) {if (event->type() == QEvent::KeyPress) {QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);if (keyEvent->key() == Qt::Key_Space) {ui->spinBox->setValue(0);return true;} else {return false;}} else {return false;}}else return QWidget::eventFilter(obj, event);
}
在这个事件过滤器中先判断部件的类型,然后再判断事件的类型,如果是需要的事件,那么就将其进行强制类型转换static_cast<*>
,然后进行相应的处理。
这里需要说明,如果要对一个特定的事件进行处理,而且不希望它在后面的传递过程中再被处理,那么就返回true,否则返回false。
return true; // 该事件已经被处理} else {return false; // 如果是其他事件,可以进行进一步的处理}
这个函数实现了在textEdit部件中使用滚轮进行内容的放大或缩小,在spinBox部件中使用空格来使数值设置为0。现在运行程序查看效果。
可以看到,使用事件过滤器可以很容易地处理多个部件的多个事件,如果不使用它,那么就得分别子类化各个部件,然后重新实现它们对应的各个事件处理函数﹐那样就会很麻烦了。
Qt中也提供了发送一个事件的功能,它由QCoreApplication
类的
bool QCoreApplication: : sendEvent(Q0bject * receiver,QEvent * event)void QCoreApplication; : postEvent(Q0bject * receiver,QEvent * event,int priority =Qt::NormalEventPriority)
函数来实现。
这两个函数的主要区别是: sendEvent()会立即处理给定的事件,而postEvent()则会将事件放到等待调度队列中,当下一次Qt的主事件循环运行时才会处理它。
这两个函数还有其他一些区别,比如 sendEvent()中的QEvent对象参数在事件发送完成后无法自动删除,所以需要在栈上创建QEvent对象;而postEvent()的QEvent对象参数必须在堆上进行创建(例如使用new),当事件被发送后事件队列会自动删除它。这两个函数更多的介绍可以参考它们的帮助文档。
下面再widget.cpp文件中的构造函数中添加代码来向spinBox部件发送一个向上方向键被按下的事件:
QKeyEvent myEvent(QEvent::KeyPress, Qt::Key_Up, Qt::NoModifier);qApp->sendEvent(ui->spinBox, &myEvent); // 发送键盘事件到spinBox部件
这里使用了sendEvent()函数,其中,QKeyEvent对象是在栈上创建的。
这里的qApp是QApplication对象的全局指针,每一个应用程序只能使用一个 QApplication对象,等价于使用QApplication::sendEvent()。
现在运行程序可以发现, spinBox部件中初始值变为了1,这说明已经在这个部件按下了向上方向键。
Qt中还可以使用自定义的事件,这个需要继承QEvent类,可以通过帮助中TheEvent System关键字中的相关内容查看。
3. 事件循环
一句话结论
整个进程只有一个事件循环(QCoreApplication::exec())在跑;
主窗口、子窗口、对话框、定时器、网络、线程……都只是把事件投递到同一个循环;
所谓“局部事件循环”只是递归地再开一个 QEventLoop,它仍然共享同一个事件分发器,而不是第二套循环。
真正的“事件循环”长什么样
int main(int argc, char *argv[])
{QApplication app(argc, argv);MainWindow w; w.show();return app.exec(); // ← 这里开始唯一的事件循环
}
app.exec() 内部简化伪代码:
while (!exit)if (!postedEvents.empty())processPostedEvents(); // 先处理 sendPostedEvents 队列if (!windowEvents.empty())processWindowSystemEvents(); // 处理 OS 事件if (canWait) // 没有事件就阻塞waitForMoreEvents(); // select/epoll/GetMessage
所有 QWidget、QObject 都只是事件的接收者,而不是再开一条循环。
- 子窗口 / 对话框的“局部循环”是什么
QDialog::exec()、QMenu::exec() 会临时创建一个嵌套的 QEventLoop:
int QDialog::exec()
{QEventLoop loop;connect(this, &QDialog::finished, &loop, &QEventLoop::quit);show();return loop.exec(); // 递归进入
}
这个 loop 仍然调用 同一个 QCoreApplication::notify() 来分发事件;
它只是让 当前函数卡在这里,外层循环暂时“挂起”;当 loop.quit() 时,栈回到原来的 app.exec(),继续跑。
示意图:
app.exec() ——→ 事件A → MainWindow↑│ 局部loop.exec() ← 事件B → Dialog│ ↑└------------┘
- 线程才拥有独立事件循环
每个 QThread 可以有自己的 QEventLoop(通过 QThread::exec() 启动)。
GUI 相关对象必须在主线程里,因此主窗口和子窗口永远共享主线程的唯一循环;
子线程的事件循环用来跑信号槽、定时器、网络 IO 等,与 GUI 循环互不干扰。
- 快速判断“几个循环?”
场景 | 事件循环个数 | 备注 |
---|---|---|
普通主窗口 + 若干子窗口 | 1 | 全部在主线程 |
弹出 QDialog::exec() | 1(嵌套) | 临时第二层 loop,返回后只剩顶层 |
新建 QThread 并 moveToObject | 2 | 主线程 + 子线程各一个 |
QApplication::processEvents() | 0 | 只是手动触发一次分发,不新建循环 |
一句话再总结
主窗口、子窗口、对话框共用同一个全局事件循环;所谓“对话框阻塞”只是递归嵌套地再跑一遍这个循环,事件仍由 QApplication 统一分发。