技术分享。
在做项目中,遇到一个需求,应用程序正常情况下只保留使用功能,用户只能正常使用,无法修改什么数据; 但对于运维人员,那么就需要有相应的权限去对程序的数据进行设置;
所以,得给程序留个后门,即程序鉴权成功后,即可获得设置的能力。
我的想法是,鼠标单击/双击某个组件,后台将开始监控键盘输入,当键盘输入的密码正确后,将开放设置权限;再次单击/双击就恢复。
基于此,将代码实现。 如下是测试运行截图:
这里测试的是,鼠标点击按钮后,后台将开始监听键盘输入,当输入正确的密码(123456)后,将显示设置等按钮,再次点击按钮后,将隐藏。当在监听中,没有键盘输入时,10秒后将超时返回;当输入过程中,停顿了3秒钟,也将超时返回。
主要实现过程已经独立封装,如下:
eavesdropauthentication.h
cpp
#ifndef EAVESDROPAUTHENTICATION_H
#define EAVESDROPAUTHENTICATION_H
#include <QObject>
#include <QString>
class QTimer;
class QEvent;
/**
* @brief 键盘窃听式鉴权
*
* 职责:从 ServiceManagement 中抽离出来的"密码监控"模块,只做四件事------
* 1. 自行在 qApp 上安装事件过滤器,全局窃听按键(宿主无需关心事件过滤器生命周期);
* 2. 把可打印 ASCII 字符累积成候选口令,与期望口令实时比对;
* 3. 命中立即停止监控并发出 signalAuthenticated(),由宿主决定"进入管理员模式"后的业务动作;
* 4. 启动后 m_startTimeoutMs 内、或每次按键后 m_inputTimeoutMs 内无输入,则停止监控并发出 signalTimeout()。
*
* 该类不持有任何 UI、不引用任何业务控件,只输出"鉴权结果",因此可被任意窗口复用。
* 日志中不会输出期望口令与已输入内容,避免口令泄露到运行日志。
*/
class EavesdropAuthentication : public QObject
{
Q_OBJECT
public:
/**
* @brief 构造:创建超时定时器并安装全局键盘过滤器
* @param secret 期望口令(由调用方注入,例如 config/appinfo.h 的 SUPER_ROOT_PWD)
* @param parent 父对象;传入宿主窗口即可随宿主一同销毁
*/
explicit EavesdropAuthentication(const QString &secret, QObject *parent = nullptr);
~EavesdropAuthentication() override;
/// 是否处于监控中(等价于原 ServiceManagement::m_isMonitoringPassword)
bool isMonitoring() const;
/// 期望口令长度(仅用于调试/自检,不暴露口令内容)
int secretLength() const;
/**
* @brief 设置各阶段超时时间(建议在 start() 之前调用)
* @param startTimeoutMs 启动后、首次按键前的等待上限
* @param inputTimeoutMs 每次按键后的间隔上限
*/
void setTimeout(int startTimeoutMs, int inputTimeoutMs);
public slots:
/// 开始窃听;重复调用等价于"重置后重新开始"
void start();
/// 停止窃听并清空已累积的输入
void stop();
signals:
/// 口令匹配成功(发出前已自动 stop())
void signalAuthenticated();
/// 超时未完成输入(发出前已自动 stop())
void signalTimeout();
/// 监控状态变化,便于宿主更新界面提示
void signalMonitoringChanged(bool monitoring);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private slots:
void onTimerTimeout();
private:
/// 停止监控、清空输入缓冲、停表(不发送任何信号)
void reset();
private:
QString m_secret; ///< 期望口令
QString m_inputSequence; ///< 已窃听到的字符缓冲
QTimer *m_timer; ///< 单次触发的超时定时器
bool m_monitoring; ///< 是否监控中
bool m_filterInstalled; ///< 事件过滤器是否已安装到 qApp
int m_startTimeoutMs; ///< 启动后的首次超时
int m_inputTimeoutMs; ///< 按键后的间隔超时
};
#endif // EAVESDROPAUTHENTICATION_H
eavesdropauthentication.cpp
cpp
#include "eavesdropauthentication.h"
#include <QTimer>
#include <QEvent>
#include <QKeyEvent>
#include <QCoreApplication>
#include <QDebug>
namespace {
const int kDefaultStartTimeoutMs = 10000; ///< 启动后、首次按键前的等待上限
const int kDefaultInputTimeoutMs = 3000; ///< 每次按键后的间隔上限
}
EavesdropAuthentication::EavesdropAuthentication(const QString &secret, QObject *parent) :
QObject(parent),
m_secret(secret),
m_timer(nullptr),
m_monitoring(false),
m_filterInstalled(false),
m_startTimeoutMs(kDefaultStartTimeoutMs),
m_inputTimeoutMs(kDefaultInputTimeoutMs)
{
m_timer = new QTimer(this);
m_timer->setSingleShot(true);
connect(m_timer, &QTimer::timeout, this, &EavesdropAuthentication::onTimerTimeout);
// 自行安装全局键盘监听:宿主不必再关心事件过滤器的安装与摘除
if (qApp) {
qApp->installEventFilter(this);
m_filterInstalled = true;
}
}
EavesdropAuthentication::~EavesdropAuthentication()
{
// Qt 在过滤器对象析构时会自动摘除过滤器,此处显式摘除仅为表达意图并保证确定性
if (m_filterInstalled && qApp) {
qApp->removeEventFilter(this);
m_filterInstalled = false;
}
}
bool EavesdropAuthentication::isMonitoring() const
{
return m_monitoring;
}
int EavesdropAuthentication::secretLength() const
{
return m_secret.length();
}
void EavesdropAuthentication::setTimeout(int startTimeoutMs, int inputTimeoutMs)
{
if (startTimeoutMs > 0) {
m_startTimeoutMs = startTimeoutMs;
}
if (inputTimeoutMs > 0) {
m_inputTimeoutMs = inputTimeoutMs;
}
}
void EavesdropAuthentication::start()
{
reset();
m_monitoring = true;
qDebug() << "[窃听鉴权] 密码监控已启动,请输入密码...";
m_timer->start(m_startTimeoutMs);
emit signalMonitoringChanged(true);
}
void EavesdropAuthentication::stop()
{
const bool wasMonitoring = m_monitoring;
reset();
if (wasMonitoring) {
emit signalMonitoringChanged(false);
}
}
void EavesdropAuthentication::reset()
{
m_monitoring = false;
m_inputSequence.clear();
m_timer->stop();
}
void EavesdropAuthentication::onTimerTimeout()
{
if (!m_monitoring) {
return;
}
stop();
qDebug() << "[窃听鉴权] 输入超时,密码监控已关闭";
emit signalTimeout();
}
bool EavesdropAuthentication::eventFilter(QObject *watched, QEvent *event)
{
Q_UNUSED(watched);
// 仅在监控中处理按键事件
if (!m_monitoring || event->type() != QEvent::KeyPress) {
return false;
}
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
// 只累积可打印 ASCII(Key_Space ~ Key_AsciiTilde),功能键/导航键原样放行
if (keyEvent->key() < Qt::Key_Space || keyEvent->key() > Qt::Key_AsciiTilde) {
return false;
}
m_inputSequence.append(keyEvent->text());
m_timer->start(m_inputTimeoutMs); // 有输入后,间隔内无输入则超时
// 实时比对;匹配成功则停止监控并由宿主决定后续业务动作
if (m_inputSequence == m_secret) {
stop();
qDebug() << "[窃听鉴权] 密码正确";
emit signalAuthenticated();
}
// 吞掉该按键,避免同一个按键同时落到当前焦点控件
return true;
}
运用
定义两个槽函数,接收结果:
cpp
public slots:
void onAuthenticationPassed(); // 窃听鉴权通过:进入管理员模式
void onAuthenticationTimeout(); // 窃听鉴权超时:仅记录日志
定义变量:
cpp
bool m_adminMode; ///< 当前是否是管理员模式(留守)
EavesdropAuthentication *m_eavesdropAuth; ///< 键盘窃听式鉴权
构造函数中做初始化:
cpp
m_eavesdropAuth = new EavesdropAuthentication("123456", this);
connect(m_eavesdropAuth, &EavesdropAuthentication::signalAuthenticated,
this, &Widget::onAuthenticationPassed);
connect(m_eavesdropAuth, &EavesdropAuthentication::signalTimeout,
this, &Widget::onAuthenticationTimeout);
m_adminMode = false;
ui->widget->setVisible(false); // 模拟的控件,先隐藏
实现:
cpp
void Widget::onAuthenticationPassed()
{
m_adminMode = true;
qDebug() << "密码正确,显示设置按钮";
ui->widget->setVisible(true);
}
void Widget::onAuthenticationTimeout()
{
qDebug() << "输入超时,密码监控已关闭";
}
void Widget::on_pushButton_clicked()
{
if (m_adminMode) {
ui->widget->setVisible(false);
m_adminMode = false;
} else {
m_eavesdropAuth->start();
}
}
这样,一个简单的实现就完成了!
在实际公司项目中,我的做法是,鼠标双击程序的头像,即双击QLabel,在槽函数中触发监听,再次双击恢复。
如下是AI文章
Qt 全局事件过滤器实战:手写一个「键盘窃听式鉴权」模块
选题说明 :本文主题为 Qt 事件过滤器机制(
installEventFilter/eventFilter) ,以一个小而完整的自研类EavesdropAuthentication(键盘窃听式鉴权)作为载体来讲解。如果你手上有别的 Qt 类,把"用钩子拿到按键 + 缓冲比对 + 定时超时"这套骨架换掉即可,原理部分是通用的。文中的 Qt 代码在 Qt 5.14.1(MinGW 64)实测通过,Qt 5.x / Qt 6.x 通用,差异点我在文中单独标出。
最近在做一个 Qt 桌面工具时碰到这么个需求:界面上不能出现任何密码框,管理员要在任意界面直接敲一串口令就进入高权限模式 。做出来之后发现这套东西挺有意思------它的核心其实就三块:qApp->installEventFilter() 全局钩子、QTimer 超时控制、信号槽解耦宿主。单独拆出来只有 130 行,却能把 Qt 事件系统的几个关键点都串起来。
这篇文章就把这个模块的来龙去脉完整写一遍:从应用场景,到 Qt 事件分发链路的底层机制,再到最小可运行示例、五个自动化验证用例的真实输出、踩坑清单和方案选型。
1 它能解决什么问题
1.1 场景:隐形入口的管理员模式
先说清楚它是什么:一个不需要输入框、不需要焦点、不限当前界面,只要在程序任意位置按对一串字符就能触发的口令鉴权模块。
典型的应用场景:
| 场景 | 说明 |
|---|---|
| 隐藏后台入口 | 工业上位机、展厅大屏、自助终端的管理界面入口,不想在 UI 上暴露"管理员登录"按钮 |
| 现场运维快捷通道 | 设备调试时运维人员直接敲口令进入调试模式,省去切窗口、找输入框、来回点 |
| 演示/看板解锁 | 大屏展示态下锁定操作,输入口令临时解锁 30 秒 |
| 调试开关 | 开发阶段用口令打开日志面板、性能悬浮窗,发布版保留但对用户不可见 |
它的价值在于解耦 :这个类不持有任何 UI、不引用任何业务控件,只向外发三个信号(signalAuthenticated / signalTimeout / signalMonitoringChanged)。宿主想拿它干什么------解锁按钮、显示隐藏面板、切 stack 页面------随宿主自己决定,换一个窗口复用,一行 UI 代码都不用改。
1.2 环境前提与版本说明
| 项目 | 要求 |
|---|---|
| Qt 版本 | Qt 5.x / Qt 6.x 均可(本文实测 Qt 5.14.1 MinGW 64-bit) |
| 依赖模块 | QT += core(qApp、QTimer);示例用到 widgets 仅为演示宿主界面 |
| 编译器 | 支持 C++11 即可(MSVC / GCC / Clang 均可) |
| 线程要求 | 必须在 GUI 主线程创建和使用,事件过滤器不接受跨线程对象 |
| 平台差异 | 依赖 QKeyEvent::text(),中文输入法 / 虚拟键盘场景下行为有差异,见 [4.2](#项目 要求 Qt 版本 Qt 5.x / Qt 6.x 均可(本文实测 Qt 5.14.1 MinGW 64-bit) 依赖模块 QT += core(qApp、QTimer);示例用到 widgets 仅为演示宿主界面 编译器 支持 C++11 即可(MSVC / GCC / Clang 均可) 线程要求 必须在 GUI 主线程创建和使用,事件过滤器不接受跨线程对象 平台差异 依赖 QKeyEvent::text(),中文输入法 / 虚拟键盘场景下行为有差异,见 4.2 "#42-%E4%BA%94%E4%B8%AA%E5%B8%B8%E8%A7%81%E5%9D%91%E4%B8%8E%E6%8E%92%E6%9F%A5%E6%80%9D%E8%B7%AF") |
工程配置(.pro):
qmake
QT += widgets
CONFIG += console c++11
TARGET = demo
TEMPLATE = app
SOURCES += demo_main.cpp \
eavesdropauthentication.cpp
HEADERS += eavesdropauthentication.h
CMake 写法(Qt 6):
cmake
find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_standard_project_setup() # Qt 6.3+,或手动 set(CMAKE_AUTOMOC ON)
target_link_libraries(demo PRIVATE Qt6::Widgets)
Q_OBJECT不能忘 :这个类是QObject子类且用了信号槽,.h里写了Q_OBJECT,qmake/CMake 的 AUTOMOC 才会生成moc_*.cpp。手写 Makefile 编译的话必须自己跑 moc,否则链接报undefined reference to vtable。
1.3 为什么不用那几种常规做法
明确一下为什么不选这些"看起来更简单"的方案,理解这一点比记代码重要:
| 做法 | 为什么不行 |
|---|---|
QLineEdit + echoMode=Password |
需要 UI 上有个框、还得点进去获得焦点。我们要的是"无痕输入",屏幕上不能出现任何痕迹 |
重写 keyPressEvent() |
只对当前有焦点的那个控件生效,而且每个窗口都要重写一遍。焦点在按钮上时你一个键都收不到 |
QShortcut / QAction |
它是"组合键 触发一个动作",不是"连续一串字符"。而且组合键会被引擎吃掉、无法做逐字符累积比对 |
grabKeyboard() |
会真的把键盘焦点独占,导致其它控件完全收不到输入------比我们要的"临时监听"重太多,还得配 releaseKeyboard() 兜底 |
原生钩子(SetWindowsHookEx 等) |
能监听其它应用 的按键,属于系统级操作,权限高、代码跨平台性差。我们只监听自己进程内的事件,完全用不到它 |
需要的能力画像很清晰:进程内、跨窗口、无需焦点、能静默吃掉按键、可随时启停 ------ 这正好是 installEventFilter() 的能力边界。
2 核心实现原理
2.1 Qt 事件分发链路
要理解这个模块,必须先搞懂 Qt 的事件是怎么走到你的代码里的。一条 QKeyEvent 的完整旅程:
vbnet
用户输入硬件产生按键
│
▼
操作系统消息队列
│
▼
Qt 平台插件把它包装成 QKeyEvent
│
▼
QCoreApplication::notify(receiver, event) ← 所有事件的总入口
│
│ ① 先过 qApp 上安装的【全局事件过滤器】(本文用的就是这一层)
│ qApp->eventFilter(watched, event)
│ ├── 返回 true → 事件被"吞掉",链路到此结束
│ └── 返回 false → 继续往下
▼
② 再过 receiver 自身安装的【对象级事件过滤器】
│ (多个过滤器时:后安装的先被调用)
│ ├── 返回 true → 吞掉
│ └── 返回 false → 继续
▼
③ receiver->event(event) ← QObject::event(),按类型分发
│
▼
④ receiver->keyPressEvent(event) ← 具体的处理函数
│
▼
事件可能被 accept() / ignore(),继续向父对象冒泡(仅限 key/mouse 等)
三个结论,直接决定我们怎么写代码:
结论一 :把过滤器装在
qApp上就是"全进程监听"------因为所有事件都必须经过QCoreApplication::notify(),无一例外。装在某个具体 widget 上就只监听那个 widget 及其子对象链上的事件。结论二 :
eventFilter()返回true= 吞掉事件 (后面的 target 一律收不到);返回false= 放行。这是全部蝴蝶效应的源头,见 2.4。结论三 :后安装的过滤器先被调用 (LIFO)。这个调试时很关键------如果你发现按键"莫名失效",很可能是别处也装了全局过滤器并且抢先
return true了。
2.2 关键数据结构与成员职责
整个类只有 6 个成员,每个都有明确分工:
| 成员 | 类型 | 职责 |
|---|---|---|
m_secret |
QString |
期望口令,由外部注入(不要类内硬编码) |
m_inputSequence |
QString |
已窃听到的字符缓冲,命中或超时即清空 |
m_timer |
QTimer* |
单次触发定时器;每次按键都重新 start,实现"滑动超时窗口" |
m_monitoring |
bool |
是否监控中,是 eventFilter 的第一个快速出口 |
m_filterInstalled |
bool |
过滤器是否已安装到 qApp,用于析构时确定性摘除 |
m_startTimeoutMs / m_inputTimeoutMs |
int |
启动超时 / 按键间隔超时,默认 10000 / 3000 ms |
注意两个设计取向:
- 缓冲用
QString而不是std::string:直接append(keyEvent->text()),省掉编码转换;口令长度一般 < 32,性能完全不用考虑。 - 定时器是
singleShot:构造时setSingleShot(true),每次按键start()一次就等于"重置倒计时",比自己维护时间戳简洁得多。
2.3 执行流程
分两条支路。启动与成功:
scss
start()
│
├─ reset():清空缓冲、停表、m_monitoring=false
├─ m_monitoring = true
├─ m_timer->start(m_startTimeoutMs) ← 启动超时倒计时开始
└─ emit signalMonitoringChanged(true) ← 宿主更新提示
│
▼
用户按下 '9'
│
├─ eventFilter:m_monitoring? ✓ 是 KeyPress? ✓
├─ key 在 0x20~0x7E 之间? ✓
├─ m_inputSequence.append("9")
├─ m_timer->start(m_inputTimeoutMs) ← 倒计时被重置为"间隔超时"
├─ m_inputSequence == m_secret ? 否 → 继续等下一个键
└─ return true ← 吞掉这个按键,不落给任何控件
│
▼
依次按下 '5' '2' '7' 时 m_inputSequence == "9527" == m_secret
│
├─ stop() → reset() + emit signalMonitoringChanged(false)
└─ emit signalAuthenticated() ← 宿主决定后续动作
超时支路:
scss
start() 后 m_startTimeoutMs 内没有任何按键
│ 或每次按键后 m_inputTimeoutMs 内没有下一个键
▼
QTimer::timeout → onTimerTimeout()
│
├─ if (!m_monitoring) return; ← 已经停了就不重复处理
├─ stop() → reset() + signalMonitoringChanged(false)
└─ emit signalTimeout()
2.4 两个容易忽略的细节
细节一:return true 的副作用比你想象的大。
原实现最后一行是 return true,注释写的是"吞掉该按键,避免同一个按键同时落到当前焦点控件"。这是刻意设计:口令不该出现在任何输入框里,也不该被 QLineEdit 塞进撤销栈。
但代价是------监听期间,用户敲的所有可打印字符都不会进入当前焦点控件 。我用自动化程序实测过这一点(输出见 [4.2 坑三](#4.2 坑三 "#%E5%9D%91%E4%B8%89%E8%A2%AB%E5%90%9E%E6%8E%89%E7%9A%84%E6%8C%89%E9%94%AE%E8%AE%A9%E5%85%B6%E5%AE%83%E8%BE%93%E5%85%A5%E9%9B%86%E4%BD%93%E5%A4%B1%E7%81%B5")):同一个 'a' 键,未监听时焦点控件正常收到;监听中焦点控件完全收不到。所以监听时间要短、要有明确超时,别让用户在"以为自己在输密码"的时候被静默吞掉输入。
细节二:为什么构造函数里自己装过滤器。
cpp
if (qApp) {
qApp->installEventFilter(this); // 自己装
m_filterInstalled = true;
}
把这件事放在类内部,好处是调用方只需要 new 一个对象,不用记得去 hook 生命周期 。Qt 在过滤器对象析构时会自动摘除过滤器,析构函数里那段 removeEventFilter() 是"显式表达意图"------因为对象析构期间仍有极小概率有事件进来,显式摘除能消除这个窗口期。
3 完整实现与使用方式
3.1 头文件
cpp
#ifndef EAVESDROPAUTHENTICATION_H
#define EAVESDROPAUTHENTICATION_H
#include <QObject>
#include <QString>
class QTimer;
class QEvent;
/**
* @brief 键盘窃听式鉴权
*
* 职责:从 ServiceManagement 中抽离出来的"密码监控"模块,只做四件事------
* 1. 自行在 qApp 上安装事件过滤器,全局窃听按键(宿主无需关心事件过滤器生命周期);
* 2. 把可打印 ASCII 字符累积成候选口令,与期望口令实时比对;
* 3. 命中立即停止监控并发出 signalAuthenticated(),由宿主决定"进入管理员模式"后的业务动作;
* 4. 启动后 m_startTimeoutMs 内、或每次按键后 m_inputTimeoutMs 内无输入,则停止监控并发出 signalTimeout()。
*
* 该类不持有任何 UI、不引用任何业务控件,只输出"鉴权结果",因此可被任意窗口复用。
* 日志中不会输出期望口令与已输入内容,避免口令泄露到运行日志。
*/
class EavesdropAuthentication : public QObject
{
Q_OBJECT
public:
/**
* @brief 构造:创建超时定时器并安装全局键盘过滤器
* @param secret 期望口令(由调用方注入,例如 config/appinfo.h 的 SUPER_ROOT_PWD)
* @param parent 父对象;传入宿主窗口即可随宿主一同销毁
*/
explicit EavesdropAuthentication(const QString &secret, QObject *parent = nullptr);
~EavesdropAuthentication() override;
/// 是否处于监控中(等价于原 ServiceManagement::m_isMonitoringPassword)
bool isMonitoring() const;
/// 期望口令长度(仅用于调试/自检,不暴露口令内容)
int secretLength() const;
/**
* @brief 设置各阶段超时时间(建议在 start() 之前调用)
* @param startTimeoutMs 启动后、首次按键前的等待上限
* @param inputTimeoutMs 每次按键后的间隔上限
*/
void setTimeout(int startTimeoutMs, int inputTimeoutMs);
public slots:
/// 开始窃听;重复调用等价于"重置后重新开始"
void start();
/// 停止窃听并清空已累积的输入
void stop();
signals:
/// 口令匹配成功(发出前已自动 stop())
void signalAuthenticated();
/// 超时未完成输入(发出前已自动 stop())
void signalTimeout();
/// 监控状态变化,便于宿主更新界面提示
void signalMonitoringChanged(bool monitoring);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private slots:
void onTimerTimeout();
private:
/// 停止监控、清空输入缓冲、停表(不发送任何信号)
void reset();
private:
QString m_secret; ///< 期望口令
QString m_inputSequence; ///< 已窃听到的字符缓冲
QTimer *m_timer; ///< 单次触发的超时定时器
bool m_monitoring; ///< 是否监控中
bool m_filterInstalled; ///< 事件过滤器是否已安装到 qApp
int m_startTimeoutMs; ///< 启动后的首次超时
int m_inputTimeoutMs; ///< 按键后的间隔超时
};
#endif // EAVESDROPAUTHENTICATION_H
3.2 实现文件
cpp
#include "eavesdropauthentication.h"
#include <QTimer>
#include <QEvent>
#include <QKeyEvent>
#include <QCoreApplication>
#include <QDebug>
namespace {
const int kDefaultStartTimeoutMs = 10000; ///< 启动后、首次按键前的等待上限
const int kDefaultInputTimeoutMs = 3000; ///< 每次按键后的间隔上限
}
EavesdropAuthentication::EavesdropAuthentication(const QString &secret, QObject *parent) :
QObject(parent),
m_secret(secret),
m_timer(nullptr),
m_monitoring(false),
m_filterInstalled(false),
m_startTimeoutMs(kDefaultStartTimeoutMs),
m_inputTimeoutMs(kDefaultInputTimeoutMs)
{
m_timer = new QTimer(this);
m_timer->setSingleShot(true);
connect(m_timer, &QTimer::timeout, this, &EavesdropAuthentication::onTimerTimeout);
// 自行安装全局键盘监听:宿主不必再关心事件过滤器的安装与摘除
if (qApp) {
qApp->installEventFilter(this);
m_filterInstalled = true;
}
}
EavesdropAuthentication::~EavesdropAuthentication()
{
// Qt 在过滤器对象析构时会自动摘除过滤器,此处显式摘除仅为表达意图并保证确定性
if (m_filterInstalled && qApp) {
qApp->removeEventFilter(this);
m_filterInstalled = false;
}
}
bool EavesdropAuthentication::isMonitoring() const
{
return m_monitoring;
}
int EavesdropAuthentication::secretLength() const
{
return m_secret.length();
}
void EavesdropAuthentication::setTimeout(int startTimeoutMs, int inputTimeoutMs)
{
if (startTimeoutMs > 0) {
m_startTimeoutMs = startTimeoutMs;
}
if (inputTimeoutMs > 0) {
m_inputTimeoutMs = inputTimeoutMs;
}
}
void EavesdropAuthentication::start()
{
reset();
m_monitoring = true;
qDebug() << "[窃听鉴权] 密码监控已启动,请输入密码...";
m_timer->start(m_startTimeoutMs);
emit signalMonitoringChanged(true);
}
void EavesdropAuthentication::stop()
{
const bool wasMonitoring = m_monitoring;
reset();
if (wasMonitoring) {
emit signalMonitoringChanged(false);
}
}
void EavesdropAuthentication::reset()
{
m_monitoring = false;
m_inputSequence.clear();
m_timer->stop();
}
void EavesdropAuthentication::onTimerTimeout()
{
if (!m_monitoring) {
return;
}
stop();
qDebug() << "[窃听鉴权] 输入超时,密码监控已关闭";
emit signalTimeout();
}
bool EavesdropAuthentication::eventFilter(QObject *watched, QEvent *event)
{
Q_UNUSED(watched);
// 仅在监控中处理按键事件
if (!m_monitoring || event->type() != QEvent::KeyPress) {
return false;
}
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
// 只累积可打印 ASCII(Key_Space ~ Key_AsciiTilde),功能键/导航键原样放行
if (keyEvent->key() < Qt::Key_Space || keyEvent->key() > Qt::Key_AsciiTilde) {
return false;
}
m_inputSequence.append(keyEvent->text());
m_timer->start(m_inputTimeoutMs); // 有输入后,间隔内无输入则超时
// 实时比对;匹配成功则停止监控并由宿主决定后续业务动作
if (m_inputSequence == m_secret) {
stop();
qDebug() << "[窃听鉴权] 密码正确";
emit signalAuthenticated();
}
// 吞掉该按键,避免同一个按键同时落到当前焦点控件
return true;
}
关于那行范围判断,值得单独解释:
cpp
if (keyEvent->key() < Qt::Key_Space || keyEvent->key() > Qt::Key_AsciiTilde) return false;
Qt::Key_Space=0x20,Qt::Key_AsciiTilde=0x7E,正好是 ASCII 可打印区间的两端;- Qt 里所有功能键的值都 ≥ 0x01000000 (如
Qt::Key_F1 = 0x01000030),天然被这个判断排除------所以Esc、F1、Tab、方向键一律原样放行,不会污染口令缓冲; - 判断用
key()(物理键),累积用text()(实际字符)。这意味着 Shift+a 的 text 是 "A" ,所以口令是大小写敏感 的,9527和Ab12这类混合口令也能支持------这点要么写进文档,要么在比对时统一toLower()。
3.3 最小可运行示例
一个完整的宿主窗口,演示"点击按钮 → 直接敲口令 → 进入管理员模式":
cpp
// demo_main.cpp ------ 最小可运行示例
#include "eavesdropauthentication.h"
#include <QApplication>
#include <QDebug>
#include <QFont>
#include <QLabel>
#include <QMainWindow>
#include <QPushButton>
#include <QStatusBar>
#include <QVBoxLayout>
#include <QWidget>
// 演示用口令。真实项目应从配置/凭据服务注入,不要硬编码,更不要提交明文。
static const char *kSuperRootPwd = "9527";
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr) : QMainWindow(parent)
{
QWidget *central = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout(central);
m_tipLabel = new QLabel("当前模式:普通用户", central);
m_tipLabel->setAlignment(Qt::AlignCenter);
QFont f = m_tipLabel->font();
f.setPointSize(14);
m_tipLabel->setFont(f);
m_startButton = new QPushButton("启用管理员模式(点击后直接输入口令)", central);
layout->addStretch(1);
layout->addWidget(m_tipLabel);
layout->addWidget(m_startButton);
layout->addStretch(1);
setCentralWidget(central);
resize(520, 240);
setWindowTitle("Qt 全局事件过滤器 Demo");
// 鉴权模块:父对象传 this,宿主销毁时自动销毁;口令由外部注入
m_auth = new EavesdropAuthentication(kSuperRootPwd, this);
m_auth->setTimeout(10000, 3000); // 首次等待 10s,按键间隔 3s
connect(m_startButton, &QPushButton::clicked, this, [this]() {
m_auth->start(); // 开始窃听,之后直接敲键盘即可
m_tipLabel->setText("监听中:请直接输入口令(无需焦点在任何输入框)");
setFocus(); // 把焦点从按钮挪走,避免空格键触发按钮
});
connect(m_auth, &EavesdropAuthentication::signalAuthenticated,
this, &MainWindow::onAuthenticated);
connect(m_auth, &EavesdropAuthentication::signalTimeout,
this, &MainWindow::onTimeout);
connect(m_auth, &EavesdropAuthentication::signalMonitoringChanged,
this, [this](bool monitoring) {
m_startButton->setEnabled(!monitoring);
qDebug().noquote() << "[宿主] 监听状态变化 ->" << monitoring;
});
}
private slots:
void onAuthenticated()
{
m_tipLabel->setText("已进入管理员模式 ✓");
// 由宿主决定"进入管理员模式"之后的业务动作:解锁按钮、显示隐藏面板......
statusBar()->showMessage("鉴权成功", 3000);
}
void onTimeout()
{
m_tipLabel->setText("当前模式:普通用户(输入超时,已退出监听)");
statusBar()->showMessage("输入超时", 3000);
}
private:
EavesdropAuthentication *m_auth;
QLabel *m_tipLabel;
QPushButton *m_startButton;
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow w;
w.show();
return app.exec();
}
#include "demo_main.moc"
注意两处细节:
new EavesdropAuthentication(kSuperRootPwd, this)传了this作为父对象 ------ 宿主窗口销毁时自动析构,无需手动delete,也不会泄漏那个全局过滤器;setFocus()把焦点从按钮上挪走 ------ 否则按钮保持焦点时用户按空格 会触发按钮的clicked()(空格/回车是按钮的默认触发键),而此时空格已经被过滤器吞掉了,会造成"看起来按键没反应"的诡异体验。
构建与运行:
bash
qmake demo.pro && mingw32-make
./demo.exe
3.4 典型业务接入
真实项目里接入只需要三步。以文中注释提到的 ServiceManagement(宿主页)为例:
cpp
// ── 第一步:在宿主头文件里声明成员(父对象传 this 即可随宿主销毁)──
EavesdropAuthentication *m_passwordAuth = nullptr;
// ── 第二步:构造时注入口令并接线 ──
m_passwordAuth = new EavesdropAuthentication(SUPER_ROOT_PWD, this); // 口令来自配置
m_passwordAuth->setTimeout(10000, 3000);
connect(m_passwordAuth, &EavesdropAuthentication::signalAuthenticated,
this, &ServiceManagement::slotEnterAdminMode); // 成功:解锁 UI
connect(m_passwordAuth, &EavesdropAuthentication::signalTimeout,
this, &ServiceManagement::slotAuthTimeout); // 超时:恢复提示
connect(m_passwordAuth, &EavesdropAuthentication::signalMonitoringChanged,
this, &ServiceManagement::slotUpdateAuthTip); // 状态:更新界面
// ── 第三步:在合适时机启动(比如某个隐蔽按钮长按、或某个快捷键)──
void ServiceManagement::onTriggerAuth()
{
if (!m_passwordAuth->isMonitoring()) { // 避免重复 start 造成状态错乱
m_passwordAuth->start();
}
}
宿主的三个槽函数决定"进了管理员模式干嘛",鉴权类完全不掺和:
cpp
void ServiceManagement::slotEnterAdminMode()
{
ui->btnDebugPanel->setVisible(true); // 解锁隐藏功能
ui->stackedWidget->setCurrentWidget(ui->pageAdmin);
m_adminTimer->start(30 * 1000); // 30 秒后自动退出管理员模式
}
这个 30 秒自动过期很重要 ------ 隐形鉴权一旦成功就永久有效,风险远大于密码框。建议成功之后立刻起一个"有效时长"定时器,超时重新锁回去。
3.5 配置项与关键参数
| 参数/接口 | 默认值 | 含义 | 调整建议 |
|---|---|---|---|
secret(构造参数) |
无,必填 | 期望口令,大小写敏感 | 长度 6~12 位、混合字符;从配置文件注入,别硬编码进源码 |
setTimeout(start, input) |
10000 / 3000 ms |
启动超时 / 按键间隔超时 | 见下方组合建议 |
start() |
- | 开始监听;重复调用 = 重置后重新开始 | 调用前用 isMonitoring() 判断一下更安全 |
stop() |
- | 停止并清空缓冲 | 宿主窗口隐藏、切换页面时务必调用 |
isMonitoring() |
- | 查询状态 | 用于防重复触发、更新 UI 提示 |
secretLength() |
- | 口令长度,用于自检 | 可在启动时 Q_ASSERT(auth->secretLength() >= 6) |
关于 setTimeout() 的两个参数,这里有个容易忽略的行为 :它内部做了 > 0 的保护,传 0 或负数会被静默忽略,沿用旧值:
cpp
void EavesdropAuthentication::setTimeout(int startTimeoutMs, int inputTimeoutMs)
{
if (startTimeoutMs > 0) { m_startTimeoutMs = startTimeoutMs; }
if (inputTimeoutMs > 0) { m_inputTimeoutMs = inputTimeoutMs; }
}
好处是防御性,代价是写错参数时没有任何反馈 。调试时如果"超时时间不对",第一件事就是检查这里是不是传了 0(比如用 #define AUTH_TIMEOUT 0 的笔误)。我个人更倾向改成断言或打日志,让错误暴露在开发期。
3.6 常见组合
| 场景 | 推荐配置 | 理由 |
|---|---|---|
| 展厅大屏/自助终端 | setTimeout(8000, 2500),口令 8 位 |
用户多为临时操作,间隔给足一点,避免手忙脚乱 |
| 开发调试面板 | setTimeout(15000, 4000),口令短到 4 位 |
自己用,方便优先,口令短也没关系 |
| 高安全后台 | setTimeout(5000, 1500) + 成功后 30s 有效期 |
监听窗口越短越好,减少被观察和录屏的机会 |
| 触屏/虚拟键盘设备 | 不推荐用本方案 | 虚拟键盘未必产生可靠的 QKeyEvent,见 [4.3](#场景 推荐配置 理由 展厅大屏/自助终端 setTimeout(8000, 2500),口令 8 位 用户多为临时操作,间隔给足一点,避免手忙脚乱 开发调试面板 setTimeout(15000, 4000),口令短到 4 位 自己用,方便优先,口令短也没关系 高安全后台 setTimeout(5000, 1500) + 成功后 30s 有效期 监听窗口越短越好,减少被观察和录屏的机会 触屏/虚拟键盘设备 不推荐用本方案 虚拟键盘未必产生可靠的 QKeyEvent,见 4.3 多个窗口都要能用 做成单例,或统一挂到主窗口 避免多处安装多个全局过滤器互相干扰,见 坑四 "#43-%E9%80%82%E7%94%A8%E8%BE%B9%E7%95%8C") |
| 多个窗口都要能用 | 做成单例,或统一挂到主窗口 | 避免多处安装多个全局过滤器互相干扰,见 [坑四](#场景 推荐配置 理由 展厅大屏/自助终端 setTimeout(8000, 2500),口令 8 位 用户多为临时操作,间隔给足一点,避免手忙脚乱 开发调试面板 setTimeout(15000, 4000),口令短到 4 位 自己用,方便优先,口令短也没关系 高安全后台 setTimeout(5000, 1500) + 成功后 30s 有效期 监听窗口越短越好,减少被观察和录屏的机会 触屏/虚拟键盘设备 不推荐用本方案 虚拟键盘未必产生可靠的 QKeyEvent,见 4.3 多个窗口都要能用 做成单例,或统一挂到主窗口 避免多处安装多个全局过滤器互相干扰,见 坑四 "#%E5%9D%91%E5%9B%9B%E5%A4%9A%E4%B8%AA%E5%AE%9E%E4%BE%8B%E9%87%8D%E5%A4%8D%E5%AE%89%E8%A3%85%E5%85%A8%E5%B1%80%E8%BF%87%E6%BB%A4%E5%99%A8") |
4 实践建议
4.1 性能优化点
第一,eventFilter() 是全局热路径,必须"早返回"。
这一点原实现做得很好:
cpp
if (!m_monitoring || event->type() != QEvent::KeyPress) {
return false; // 绝大多数事件在这一行就返回了
}
顺序不能调换 ------ 先把成本最低、命中率最高的条件放前面。一个 Qt 程序每秒可能产生成百上千个事件(Paint、Timer、HoverMove......),如果这里先做 static_cast<QKeyEvent*> 再判断类型,等于给每个事件都增加一次无谓开销。
原则 :全局事件过滤器里只做布尔判断和轻量赋值 ,绝不在这里做文件 IO、加锁、复杂字符串处理、跨线程
invokeMethod。真有重活,投递一个异步任务出去。
第二,控制全局过滤器的数量。
每在 qApp 上装一个过滤器,每个事件就多一次虚函数 crossings 。如果项目里 5 个模块各装一个全局过滤器,那就是每秒几千次多余调用。建议像节流 plane 一样:全项目只保留一个全局过滤器,内部再按 event 类型分发给各模块。
第三,singleShot 定时器比"周期性 tick + 自己算时间"划算得多。
QTimer::setSingleShot(true) 只在倒计时结束产生一次事件;改成 100ms 周期定时器自己去比较时间戳,等于每秒多 10 次无意义的定时器事件穿过全局过滤器。
第四,日志里永远不打口令。 原实现这一点做得很好------成功日志只有 "窃听鉴权 密码正确",失败和超时都不打印缓冲内容。生产环境里泄密最常见的途径就是日志 ,别用 qDebug() << m_inputSequence 去调试,实在要看就临时加,完事删掉。
4.2 五个常见坑与排查思路
以下五条都用自动化程序实测过,输出是真实运行结果。
坑一:输错一个字符就再也匹配不上(无自愈)
这是最影响手感的一个。缓冲不会自动清空,一旦从某个字符起失配,后面不管怎么敲都对不上。实测:
ini
=========================================================
用例 4:输入 9 9 5 2 7(手抖多按了一个 9)
=========================================================
[窃听鉴权] 密码监控已启动,请输入密码...
按下 '9'(Qt::Key = 0x39)
按下 '9'(Qt::Key = 0x39)
按下 '5'(Qt::Key = 0x35)
按下 '2'(Qt::Key = 0x32)
按下 '7'(Qt::Key = 0x37)
缓冲内容已从第 1 个字符起就失配,但不会被自动清空
按下 '9'(Qt::Key = 0x39)
按下 '5'(Qt::Key = 0x35)
按下 '2'(Qt::Key = 0x32)
按下 '7'(Qt::Key = 0x37)
紧接着再补一次正确的 9 5 2 7,期望:仍然匹配不上
-------------------------------------------------------
结果:无任何信号发出(仍在监听中)
用户视角就是"我明明重输了一遍对的,怎么没反应"。
坑二:中文输入法下口令永远打不出来
eventFilter 里用 key() 做范围判断、用 text() 累积。但中文输入法激活时,按键事件的 text() 是候选/组字过程中的内容,key() 可能是 Qt::Key_Process(0x01100081)或者干脆是一个远超 0x7E 的值,直接被范围判断过滤掉。
表现:中文输入法状态下怎么敲都没反应,切成英文输入法立刻正常。
坑三:被吞掉的按键让其它输入"集体失灵"
这是 return true 的必然副作用,实测如下:
rust
=========================================================
用例 5:吞键与放行
=========================================================
未监听时向探针发 'a' -> 探针收到按键 = true
[窃听鉴权] 密码监控已启动,请输入密码...
监听中向探针发 'a' -> 探针收到按键 = false(事件被过滤器吞掉)
发送功能键 F1(Qt::Key_F1 = 0x1000030,超出可打印 ASCII 区间)-> 探针收到按键 = true
三条结论很清楚:
- 未监听 → 按键正常抵达目标;
- 监听中 → 可打印字符被完全吞掉,焦点控件什么都收不到;
- 功能键一律放行 (
F1 = 0x01000030远超 0x7E) ------ 这意味着Esc、F5、Ctrl+C这类组合仍然正常工作,算是设计上的兜底。
坑四:多个实例重复安装全局过滤器
如果在 A 窗口里 new 一个、B 窗口里又 new 一个,两个实例都会 在 qApp 上安装过滤器。后果:
- 每个事件都要多过一次虚函数(性能);
- 后安装的先被调用 ,如果它先返回
true,前一个实例根本收不到按键(行为诡异且难查); - 一个
start()了另一个没start(),未start()的那个因为m_monitoring == false会放行,看起来"时灵时不灵"。
坑五:把它当成"真正的身份认证"
这是定位层面的坑,比代码 bug 更危险。这个模块的本质是"本地 UI 解锁开关",不具备任何安全能力:
| 缺失的安全属性 | 后果 |
|---|---|
| 明文比对 | m_secret 以明文存在于内存,可被内存 dump、可被 strings 从二进制里直接翻出来 |
| 无防爆破 | 可以无限次重试,配合短口令(4 位)几秒就能试出来 |
| 无服务端校验 | 权限提升完全在本地,绕过它不需要任何网络权限 |
| 无审计 | 谁在什么时候进了管理员模式,没有任何记录 |
最低限度的加固建议:
cpp
// 1. 存哈希,不存明文(Qt 5/6 的 QtCore 均有 QCryptographicHash)
#include <QCryptographicHash>
static QString hashSecret(const QString &raw, const QByteArray &salt)
{
QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(salt);
hash.addData(raw.toUtf8());
return QString::fromLatin1(hash.result().toHex());
}
// 使用时比对的是 hashSecret(用户输入, salt) == m_secretHash
// 2. 防爆破:连续失败 N 次后冷却 M 秒
if (++m_failCount >= 5) {
m_lockUntil = QDateTime::currentSecsSinceEpoch() + 60;
// 冷却期内 start() 直接 emit signalTimeout(),让调用方看起来"照常失败"
}
底线 :如果这个功能保护的是真金白银或者有合规要求的权限,请走后端认证(票据、OAuth、OTP),本地只负责展示。这个模块适合的场景是"防误操作"和"隐藏出调试入口",不是"防攻击者"。
4.3 适用边界
明确一下它的能力边界,避免用错地方:
| 维度 | 能力 |
|---|---|
| 监听范围 | ✅ 仅限本进程。它基于 Qt 事件循环,其它程序的按键一概监听不到 |
| 跨进程全局热键 | ❌ 做不到,需要 QAbstractNativeEventFilter + 平台原生 API(RegisterHotKey / XGrabKey)各平台写一份 |
| 触屏 / 虚拟键盘 | ⚠️ 不可靠。Qt Virtual Keyboard 主要产生 QInputMethodEvent,不一定产生带 text() 的 QKeyEvent |
| 远程桌面 / VNC | ⚠️ 通常可用(远端只转发键码),但个别实现会合并/丢弃按键,建议实测 |
| 无障碍 / 输入法 / 宏键盘 | ⚠️ 见 [坑二](#维度 能力 监听范围 ✅ 仅限本进程。它基于 Qt 事件循环,其它程序的按键一概监听不到 跨进程全局热键 ❌ 做不到,需要 QAbstractNativeEventFilter + 平台原生 API(RegisterHotKey / XGrabKey)各平台写一份 触屏 / 虚拟键盘 ⚠️ 不可靠。Qt Virtual Keyboard 主要产生 QInputMethodEvent,不一定产生带 text() 的 QKeyEvent 远程桌面 / VNC ⚠️ 通常可用(远端只转发键码),但个别实现会合并/丢弃按键,建议实测 无障碍 / 输入法 / 宏键盘 ⚠️ 见 坑二,中文输入法下基本失效 无头 / 无 GUI 环境 ❌ 必须有 QGuiApplication 实例和事件循环;后台服务里跑不了 后台/失焦窗口 ❌ 窗口失焦时 Qt 收不到键盘事件(这是 OS 行为,与本模块无关) "#%E5%9D%91%E4%BA%8C%E4%B8%AD%E6%96%87%E8%BE%93%E5%85%A5%E6%B3%95%E4%B8%8B%E5%8F%A3%E4%BB%A4%E6%B0%B8%E8%BF%9C%E6%89%93%E4%B8%8D%E5%87%BA%E6%9D%A5"),中文输入法下基本失效 |
| 无头 / 无 GUI 环境 | ❌ 必须有 QGuiApplication 实例和事件循环;后台服务里跑不了 |
| 后台/失焦窗口 | ❌ 窗口失焦时 Qt 收不到键盘事件(这是 OS 行为,与本模块无关) |
4.4 与同类方案的对比选型
| 方案 | 作用范围 | 能否吞掉事件 | 跨平台 | 典型用途 |
|---|---|---|---|---|
重写 keyPressEvent() |
单个控件,必须有焦点 | ✅ | ✅ | 常规键盘交互 |
QShortcut / QAction |
窗口级 / 应用级 | ❌(触发而非拦截) | ✅ | 快捷键、菜单加速键 |
installEventFilter(本文) |
进程级(装在 qApp) | ✅ | ✅ | 隐形口令、全局按键拦截、统一埋点 |
grabKeyboard() |
进程级,独占焦点 | ✅(独占) | ✅ | 模态输入、游戏式输入 |
QAbstractNativeEventFilter |
原生事件层 | ✅(平台相关) | ❌ 各平台要单独实现 | 跨应用全局热键、拦截系统消息 |
QLineEdit + echoMode=Password |
单个输入框 | ❌ | ✅ | 常规登录框 |
一句话选型:
- 要跨窗口、无焦点、隐形输入 的口令 →
installEventFilter(本文方案);- 要监听别人家程序 的按键 →
QAbstractNativeEventFilter+ 原生 API,做好各平台各写一份的准备;- 只是想加个快捷键 →
QShortcut,别自己拦事件;- 做正经的登录认证 →
QLineEdit+ 后端校验,别省这个输入框。
顺带说一个容易被忽略的替代思路:如果只是"隐藏入口",很多时候 Alt/Tool 菜单项 + QShortcut 就够了,用不着拦截键盘。先问自己"是不是真的不能用快捷键",再决定要不要上全局过滤器------毕竟后者会让整个应用的事件流都多一道检查。
5 总结与延伸阅读
5.1 要点回顾
原理层:
- Qt 事件的必经之路是
QCoreApplication::notify(),装在qApp上的过滤器就是进程级钩子; eventFilter()返回true= 吞掉 ,false= 放行;多个过滤器时后安装的先执行;- 用
Qt::Key_Space(0x20) ~ Qt::Key_AsciiTilde(0x7E)做范围判断,能天然排除所有功能键(它们的键值都 ≥ 0x01000000); - 判断用
key()、累积用text(),所以口令是大小写敏感的。
实现层:
- 过滤器在构造函数里自己安装、析构时显式摘除,调用方零感知;
QTimer设成singleShot,每次按键start()一次就是最简洁的"滑动超时窗口";- 只对外发
signalAuthenticated/signalTimeout/signalMonitoringChanged三个信号,不持有任何 UI,换窗口可零成本复用。
实践层(四个坑,务必记住):
- 失配无自愈 ------ 输错一个字符后续全废,必须加"前缀失配即清空";
- 中文输入法失效 ------
key()会变成Key_Process,要么提示切英文,要么改用输入法事件; - 监听期间按键被吞 ------ 务必给
Esc逃生通道,且监听窗口要短; - 别当真认证用 ------ 明文、无防爆破、无审计,至少做哈希 + 失败冷却,真要安全就走后端。
性能底线 :全局过滤器是热路径,只放布尔判断和轻量赋值,判断顺序把"成本最低、命中最高"的放最前面。
5.2 延伸阅读
如果你打算在项目里进一步用这套机制,下面几个方向值得继续深入:
QAbstractNativeEventFilter:想跨出进程边界做真正的全局热键,下一步就是它。Windows 上配RegisterHotKey、Linux X11 上配XGrabKey,各平台实现一次就明白为什么 Qt 要抽象这一层。- Qt 输入法框架 :被中文输入法坑过之后看看
QInputMethod/QInputMethodEvent的组字(preedit)与提交(commit)时序,很多"输入类"功能的坑根都在这里。 - 事件与信号的取舍 :什么时候该用自定义
QEvent子类做QCoreApplication::sendEvent(),什么时候该用信号槽?简单规则 ------ 需要"能否被拦截/能否继续传递"语义的用事件,纯通知用信号。 Qt::KeyboardModifiers与组合键 :想把Ctrl+Alt+Shift+K这类当触发条件,key()之外的modifiers()才是主角,记得配合Qt::KeypadModifier处理小键盘。
官方文档入口:
- The Event System | Qt 文档 ------ 事件分发与五种处理方式的官方总览
- QObject::installEventFilter ------ 过滤器的安装顺序与返回值语义
- QKeyEvent 文档 ------
key()/text()/modifiers()三者的区别讲得很清楚