/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
我是AI时代的无业游民,我游荡在现实与意念之间
Qt 事件机制详解:从 QEvent 派生类到事件过滤器的全景指南

什么时候用这份手册
你在写 Qt 作业或小项目时,遇到下面任意一条,就可以直接往下翻:
- 按钮点了没反应,clicked 信号不触发;
- 自定义控件收不到键盘/鼠标事件;
- 想在不动别人代码的前提下,给某个控件"加一层"行为;
- 面试被问"信号槽和事件有什么区别"、“事件过滤器返回 true 会怎样”。
前置知识只有两条:会写 QWidget 子类、知道 connect 怎么用。读完你能交付一个"可写进作品集"的小能力:给任意第三方控件安全地插入自定义交互逻辑,而不修改它的源码。
① 重构前的症状
一个典型的新手写法:在父窗口里重写 keyPressEvent,想捕获子控件里的回车键。
// 症状代码:父窗口想接管子控件的按键
class MainWindow : public QMainWindow {
protected:
void keyPressEvent(QKeyEvent *e) override {
if (e->key() == Qt::Key_Return) {
qDebug() << "submit"; // 永远不会打印
}
QMainWindow::keyPressEvent(e);
}
};
界面上放一个 QLineEdit,运行后按回车——没有任何输出。同样的问题还会出现在:给 QTableWidget 里的单元格加右键菜单、给 QComboBox 的下拉列表加滚轮拦截。
痛点很清楚:事件先给谁、被谁吃掉,代码里完全不可见。
② 问题诊断
根因是 Qt 的事件分发顺序,记住这一条链就够了:
QApplication::notify
→ 目标对象的 event()
→ 具体处理函数(keyPressEvent / mousePressEvent …)
→ 未处理则交给父对象(事件冒泡)
关键点有三个:
| 事件先发给焦点控件,不是父窗口 | 父窗口的 keyPressEvent 收不到子控件的按键 |
| QLineEdit 在 keyPressEvent 里处理了回车并接受事件 | 事件不再向上冒泡 |
| event() 返回 true 表示"已处理",返回 false 才继续传递 | 返回值的含义容易记反 |
所以症状代码不是"写错了",而是位置错了:拦截点应该放在事件到达目标控件之前,也就是 eventFilter。
面试常问:信号槽和事件什么关系?一句话——事件是底层输入,信号是对象在事件处理中主动发出的通知。QPushButton 在 mouseReleaseEvent 里判断按下与释放都在按钮内,然后 emit clicked()。
③ 重构后的写法
写法 A:事件过滤器(推荐,零侵入)
// 用法:在任意 QObject 子类里安装,不需要继承 QLineEdit
class SubmitFilter : public QObject {
public:
explicit SubmitFilter(QObject *parent = nullptr) : QObject(parent) {}
protected:
bool eventFilter(QObject *watched, QEvent *e) override {
if (e->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(e);
if (ke->key() == Qt::Key_Return) {
qDebug() << "submit from" << watched->objectName();
return true; // 吃掉事件,QLineEdit 不再处理
}
}
return QObject::eventFilter(watched, e); // 其余放行
}
};
// 安装
auto *filter = new SubmitFilter(this);
ui->lineEdit->installEventFilter(filter);
注意点:return true 是拦截,return false 是放行;忘记调用基类会静默吞掉所有事件。
写法 B:自定义控件重写 event()
class EnterLineEdit : public QLineEdit {
protected:
void keyPressEvent(QKeyEvent *e) override {
if (e->key() == Qt::Key_Return) {
emit submitted(text());
e->accept(); // 明确表示已处理
return; // 不再调用基类
}
QLineEdit::keyPressEvent(e); // 其余交给基类,别自己造轮子
}
signals:
void submitted(const QString &text);
};
写法 C:QEvent 派生类做自定义事件(跨线程/异步通知)
class DataReadyEvent : public QEvent {
public:
static const QEvent::Type kType = QEvent::Type(QEvent::User + 1);
explicit DataReadyEvent(int n) : QEvent(kType), count(n) {}
int count;
};
// 发送
QCoreApplication::postEvent(target, new DataReadyEvent(42));
// 接收:重写 event() 或 customEvent()
bool MyWidget::event(QEvent *e) {
if (e->type() == DataReadyEvent::kType) {
auto *d = static_cast<DataReadyEvent *>(e);
qDebug() << d->count;
return true;
}
return QWidget::event(e);
}
注意点:postEvent 的事件由 Qt 负责 delete,不要自己删;sendEvent 则是同步直发、栈上对象即可。
三种方式怎么选
| 改不了对方源码(第三方/生成代码) | 事件过滤器 |
| 一个过滤器要管多个控件 | 事件过滤器(watched 参数区分) |
| 控件是自己的、行为是它的核心职责 | 重写 event() / xxxEvent() |
| 跨线程、延迟、批量通知 | postEvent + 自定义 QEvent |
④ 收益与代价
以"给 5 个输入框加回车提交"为例:
| 新增类数量 | 5 个 | 1 个 |
| 改动第三方控件源码 | 需要 | 不需要 |
| 新增代码行数 | ~60 行 | ~20 行 |
| 运行开销 | 无额外 | 每次事件一次虚函数调用,纳秒级 |
可读性上,过滤器把"拦截逻辑"集中到一处,代价是控制流变隐式:后来的人看 QLineEdit 代码找不到回车处理,得知道有个 filter 存在。所以务必在安装处写一行注释,并用 objectName 区分来源。
复杂度代价主要在事件过滤器:过滤器装在 qApp 上会拦截所有对象的事件,性能与调试成本都会上升,能装到具体控件就别装全局。
⑤ 适用边界与检查清单
不该这么改的时候:
- 只是想响应按钮点击 → 用信号槽,别碰事件;
- 业务逻辑依赖大量控件内部状态 → 过滤器拿不到私有成员,重写子类更合适;
- 需要修改控件绘制 → 那是 paintEvent 和样式表的事,与事件分发无关。
提交前检查清单:
- eventFilter 的默认分支返回了基类结果,而不是 false;
- 拦截分支确认返回 true,并想清楚"被吃掉后原控件会失去什么行为";
- 过滤器对象的生命周期长于被监视控件(通常 new 到父对象上);
- postEvent 创建的事件没有手动 delete;
- 自定义事件类型用 QEvent::User + n,避免与 Qt 内置类型冲突;
- 在安装过滤器处留一行注释,说明"这里有一层隐式拦截"。
把这份清单对着自己的代码过一遍,事件相关的"点了没反应"问题基本能当场定位。




