Qt 事件系统
Qt 事件系统
Qt中,事件作为一个对象,继承自
QEvent类,常见的有键盘事件QKeyEvent,鼠标事件QMouseEvent,定时器事件QTimerEvent等。
[TOC]
1 Qt中的事件
事件是对各种应用程序需要知道的由应用程序内部或者外部产生的事情或者动作的通称。
事件与信号并不相同。
eg.单一一下按钮,那么就会产生鼠标事件QMouseEvent(不是按钮产生的),而因为按钮被按下了,所以他会发射
clicked()单击信号(按钮产生的)。一般需要考虑单击信号,如果要设计这个按钮被单击时候产生别的效果,那么需要考虑鼠标事件。因此,事件与信号是两个不同层面的东西。
Qt中,任何QObject子类对象都可以接收和处理事件。
1.1 事件的处理
一个事件由某个QEvent或QEvent子类对象表示,每个事件对象都有一个事件类型,事件类型由 QEvent::Type 枚举值表示,其中有100多个枚举常量。
1.1.1 事件处理函数
Qt 的控件类里已经准备了很多虚函数,专门用来处理事件。
比如:
1 | void mousePressEvent(QMouseEvent *event); |
自己写类时,可以重写这些函数。
比如:
1 | class PetWidget : public QWidget |
实现:
1 | void PetWidget::mousePressEvent(QMouseEvent *event) |
表示:当 PetWidget 收到鼠标按下事件时,执行这段代码。
1.1.2 事件的处理流程
以鼠标点击桌宠为例:
1 | 用户点击桌宠 |
1.1.3 accept和ignore
很多事件对象都有:
1 | event->accept(); |
它们表示:
1 | accept:表示当前对象已经接受/处理这个事件。 |
比如:
1 | void MyWidget::mousePressEvent(QMouseEvent *event) |
1.2 事件的传递
1.2.1 事件的传递
对 QWidget 来说,很多输入事件如果子控件不处理,可能会传给父控件。
比如:
1 | PetWidget |
如果点到了 QLabel,事件先到 QLabel。
如果 QLabel 不处理,可能再传给父控件 PetWidget。
1.2.2 event()函数
除了专门的事件函数,Qt 还有一个总入口:
1 | bool event(QEvent *event) override; |
所有事件进入某个对象时,通常会先经过这个总函数,然后再分发给具体处理函数。
比如:
1 | event() |
可以重写 event():
1 | bool MyWidget::event(QEvent *event) |
这里:
1 | return true; |
表示事件已经处理。
2 鼠标事件和滚轮事件
QMouseEvent类用来表示一个鼠标事件,在窗口部件中按下鼠标或者移动鼠标指针,都会产生鼠标事件。一般通过重定义部件的鼠标事件处理函数来进行一些自定义的操作。
最常用的几个函数是:
1 | void mousePressEvent(QMouseEvent *event) override; // 鼠标按下 |
这些函数通常写在自己的 QWidget 子类里面,例如:
1 | class PetWidget : public QWidget |
2.1 鼠标按下事件
1 | void mousePressEvent(QMouseEvent *event) override; |
鼠标按下时触发。常用来判断用户按的是左键、右键还是中键。
1 | void PetWidget::mousePressEvent(QMouseEvent *event) |
常用函数:
1 | Qt::MouseButton button() const; // 触发本次事件的按钮 |
注意:
1 | event->button() |
表示“这一次事件是哪一个按钮触发的”。
1 | event->buttons() |
表示“当前有哪些鼠标按钮正处于按下状态”。
2.2 鼠标移动事件
1 | void mouseMoveEvent(QMouseEvent *event) override; |
鼠标移动时触发。
不过:默认情况下,只有鼠标按下后移动,控件才会收到 mouseMoveEvent。
如果希望鼠标不按下,只要经过控件就触发移动事件,需要开启:
1 | setMouseTracking(true); |
例如:
1 | PetWidget::PetWidget(QWidget *parent) : QWidget(parent) |
然后:
1 | void PetWidget::mouseMoveEvent(QMouseEvent *event) |
- 例题:用鼠标事件实现拖拽窗口
基本思路:
1 | 鼠标按下:记录鼠标相对窗口左上角的偏移 |
示例代码:
1 | class PetWidget : public QWidget |
这里最关键的是:
1 | dragOffset = event->globalPosition().toPoint() - frameGeometry().topLeft(); |
表示鼠标点下去的位置,距离窗口左上角有多远。
移动时:
1 | move(event->globalPosition().toPoint() - dragOffset); |
表示:
1 | 窗口新左上角 = 当前鼠标屏幕位置 - 当初记录的偏移 |
这样窗口就会跟着鼠标移动,而且不会突然跳一下。
2.3 鼠标释放事件
1 | void mouseReleaseEvent(QMouseEvent *event) override; |
鼠标松开时触发。常用于结束拖动、结束选择、结束绘制等。
1 | void PetWidget::mouseReleaseEvent(QMouseEvent *event) |
2.4 鼠标双击事件
1 | void mouseDoubleClickEvent(QMouseEvent *event) override; |
双击时触发,例如桌宠双击打开聊天窗口:
1 | void PetWidget::mouseDoubleClickEvent(QMouseEvent *event) |
2.5 右键菜单
右键菜单可以直接在 mousePressEvent() 里判断右键,也可以重写专门的:
1 | void contextMenuEvent(QContextMenuEvent *event) override; |
简单写法:
1 | void PetWidget::mousePressEvent(QMouseEvent *event) |
不过更标准的是:
1 | void PetWidget::contextMenuEvent(QContextMenuEvent *event) |
2.6 滚轮事件
滚轮事件函数是:
1 | void wheelEvent(QWheelEvent *event) override; |
常用来做缩放、调节透明度、调节音量等。
Qt 6 常用:
1 | QPoint angleDelta() const; |
这个函数返回一个 QPoint,里面有两个方向的滚动量:
1 | event->angleDelta().x(); // 水平方向滚动量 |
例如:
1 | void PetWidget::wheelEvent(QWheelEvent *event) |
通常:
1 | angleDelta().y() > 0 向上滚 |
Q:
button()和buttons()的区别?A:
1 event->button()表示本次事件由哪个按钮触发。
常用于:
1
2
3 mousePressEvent()
mouseReleaseEvent()
mouseDoubleClickEvent()比如:
1 if (event->button() == Qt::LeftButton)
1 event->buttons()表示当前哪些按钮正在被按住。
常用于:
1 mouseMoveEvent()比如拖动时:
1 if (event->buttons() & Qt::LeftButton)
2.7 常用枚举
1 | Qt::LeftButton; // 左键 |
判断单个按钮:
1 | if (event->button() == Qt::LeftButton) {} |
判断当前是否按着某个按钮:
1 | if (event->buttons() & Qt::LeftButton) {} |
3 键盘事件
QKeyEvent类用来描述一个键盘事件。当键盘按键被按下或者被释放时,键盘事件就会被发送给有键盘输入焦点的部件。
常用的两个函数是:
1 | void keyPressEvent(QKeyEvent *event) override; // 键盘按下 |
它们一般写在自己的 QWidget 子类里。
3.1 示例
1 |
|
这里:
1 | event->key() |
表示用户按下的是哪个键。
常见按键枚举:
1 | Qt::Key_Escape; // Esc |
3.2 key() 和 text()
QKeyEvent 里常用两个信息:
1 | int key() const; |
区别是:
1 | key() :表示物理/逻辑按键,比如 Qt::Key_A、Qt::Key_Escape |
例如按下 A:
1 | event->key() 可能是 Qt::Key_A |
如果同时按 Shift + A:
1 | event->key() 仍然是 Qt::Key_A |
如果按 Esc:
1 | event->key() 是 Qt::Key_Escape |
所以:
- 判断快捷键/功能键:用 key()
- 获取用户输入字符:用 text()
3.3 判断组合键
比如判断 Ctrl + S:
1 | void MyWidget::keyPressEvent(QKeyEvent *event) |
这里:
1 | event->modifiers() |
返回当前按下的修饰键集合。
常见修饰键:
1 | Qt::ControlModifier; // Ctrl |
3.4 让 QWidget 接收键盘事件
有些控件默认不主动接受键盘焦点。可以设置焦点策略:
1 | setFocusPolicy(Qt::StrongFocus); |
例如:
1 | MyWidget::MyWidget(QWidget *parent) : QWidget(parent) |
常见焦点策略:
1 | Qt::NoFocus; // 不接受键盘焦点 |
3.5 按键自动重复
长按一个键时,系统会不断生成键盘事件。这叫 auto repeat。
可以用:
1 | bool isAutoRepeat() const; |
例如:
1 | void MyWidget::keyPressEvent(QKeyEvent *event) |
如果只想处理第一次按下,可以这样:
1 | if (event->isAutoRepeat()) { |
因为:
1 | 第1次:真正按下 A isAutoRepeat() == false |
3.6 keyPressEvent() 和快捷键系统的区别
可以用键盘事件判断快捷键:
1 | if (event->key() == Qt::Key_S && |
但更标准的快捷键做法通常是用:
1 | QShortcut |
例如:
1 | QShortcut *shortcut = new QShortcut(QKeySequence("Ctrl+S"), this); |
区别:
1 | keyPressEvent: |
注:
QKeySequence表示一个按键组合。例如:
1 QKeySequence("Ctrl+S")
QShortcut是真正的快捷键对象。它负责把某个快捷键注册到某个控件或窗口上。
例如:
1 QShortcut *shortcut = new QShortcut(QKeySequence("Ctrl+S"), this);意思是:在 this 这个窗口/控件中注册 Ctrl+S 快捷键
但是此时只是创建了快捷键,还没说触发后要干什么。
所以还要:
1 connect(shortcut, &QShortcut::activated, this, &MyWidget::save);意思是:当 Ctrl+S 被按下时,调用 save()
4 定时器事件与随机数
QTimerEvent类用来描述一个定时器事件。
QRandomGenerator类用来实现随机数的生成。
4.1 timerEvent函数
- QObject的子类都可以启动定时器。
核心函数:
1 | int startTimer(int interval); |
其中:
1 | interval 是时间间隔,单位是毫秒 |
例子:
1 | class MyWidget : public QWidget |
含义是:
1 | 构造函数里启动一个 1000ms 的定时器; |
Q: 为什么有timerId?
A: 因为一个对象可以启动多个定时器。
startTimer() 返回 id
killTimer(id) 停止对应定时器
4.2 QTimer
实际项目里更多用QTimer类。它是一个 QObject 子类,可以发出 timeout() 信号。
例子:
1 | QTimer *timer = new QTimer(this); |
含义:
1 | 每隔 1000ms,timer 发出 timeout() 信号; |
这个写法比 timerEvent() 更直观。
4.3 单次定时器
如果只想延迟执行一次,可以用singleShot:
1 | QTimer::singleShot(3000, this, [=]() { |
例如:
1 | 弹出提示后 3 秒自动隐藏 |
4.4 随机数
Qt 里常用QRandomGenerator类。
包含头文件:
1 |
生成一个随机整数:
1 | int value = QRandomGenerator::global()->bounded(100); |
含义:
1 | 生成 [0, 100) 之间的整数 |
生成指定范围:
1 | int value = QRandomGenerator::global()->bounded(10, 21); |
含义:
1 | 生成 [10, 21) 之间的整数 |
5 拖放事件
Qt提供了强大的拖放机制
Drag and Drop。
拖放事件的核心是:
1 | 用户把一个东西拖到窗口上 |
比如你把一个图片文件拖到桌宠窗口上,可以实现:
1 | 拖入图片 → 导入宠物资源 |
最常用的事件函数是:
1 | void dragEnterEvent(QDragEnterEvent *event) override; // 拖拽进入窗口 |
不过在使用前,必须先开启接收拖放:
1 | setAcceptDrops(true); |
否则窗口不会收到拖放事件。
5.1 MIME类型
拖放数据通常通过MIME格式描述。Qt 使用
QMimeData保存拖放数据,例如文本、URL、图片等。
一个最简单的“拖入文件并输出路径”的例子:
1 |
|
这里最关键的是:
1 | event->mimeData() |
拖进来的数据都放在 QMimeData 里面。它可能包含:
1 | event->mimeData()->hasUrls(); // 文件路径/URL |
5.2 拖放事件的流程
1 | 拖着文件进入窗口 |
所以
dragEnterEvent()的作用不是处理文件,而是告诉 Qt:这个东西我能接收。真正处理数据一般在:dropEvent()里面。
例题:拖入文件导入资源。比如只接受图片:
1 | void PetWidget::dragEnterEvent(QDragEnterEvent *event) |
Q: 这个event被ignore了会怎么样?
A: 在
dragEnterEvent()里调用
1
2 event->ignore();
return;就是告诉 Qt:这个拖进来的东西我不接收。
于是通常表现为:
1
2
3 鼠标光标会变成禁止拖放的样子
用户松开鼠标时不会触发有效 drop
你的 dropEvent() 通常也不会被调用
然后在 dropEvent() 里真正导入:
1 | void PetWidget::dropEvent(QDropEvent *event) |
6 事件过滤器与事件的发送
Qt中提供了事件过滤器,来实现在一个部件中监控其它多个部件的事件。
- 事件过滤器是什么
正常情况下,一个事件会直接送到目标对象。
比如用户点击 QLineEdit:
1 | 鼠标点击 |
但如果安装了事件过滤器,流程会变成:
1 | 鼠标点击 |
6.1 事件过滤器的基本写法
事件过滤器函数是:
1 | bool eventFilter(QObject *watched, QEvent *event) override; |
通常写在父窗口类里。
例如:
1 | class MyWidget : public QWidget |
构造函数里安装过滤器:
1 | MyWidget::MyWidget(QWidget *parent) : QWidget(parent) |
这句:
1 | edit->installEventFilter(this); |
意思是:让 this 帮 edit 过滤事件。
之后,edit 收到事件前,this->eventFilter(...) 会先被调用。
例子:拦截 QLineEdit 的 Enter 键。
1 | bool MyWidget::eventFilter(QObject *watched, QEvent *event) |
这里有几个关键点。
1 | watched == edit |
表示:这个事件是不是来自 edit。
1 | event->type() == QEvent::KeyPress |
表示:这个事件是不是键盘按下事件。
1 | static_cast<QKeyEvent *>(event) |
表示:既然已经确认它是 KeyPress,就把通用的 QEvent * 转成更具体的 QKeyEvent *,这样才能调用 key()。
6.2 return true 和 return false 的区别
这是事件过滤器最重要的地方。
1 | return true; |
表示:事件被拦截了,不再交给原来的对象处理。
也就是说,edit 不会再收到这个 Enter 键事件。
1 | return false; |
或者:
1 | return QWidget::eventFilter(watched, event); |
表示:不拦截,继续交给原来的对象处理。
Q: 事件过滤器的典型用途?
A: 它适合这种场景:
1
2
3
4 不想继承某个控件,但想监听它的事件。
想统一监听多个控件的事件。
想在事件到达控件之前提前处理。
想让父窗口管理子控件的一些特殊行为。比如:
1
2
3
4 监听多个输入框的 Enter 键。
鼠标进入某个控件时改变样式。
拦截 Esc 键关闭弹窗。
在 QTextEdit 中实现 Enter 发送、Shift+Enter 换行。
6.3 事件的发送
事件发送指的是:程序主动创建一个事件,然后发给某个对象。
常见函数有两个:
1 | static bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event); |
它们区别很重要。
6.3.1 sendEvent
1 | QKeyEvent event(QEvent::KeyPress, Qt::Key_A, Qt::NoModifier, "a"); |
意思是:
1 | 立刻把这个键盘事件发给 edit。 |
可以理解为:现在就送,现在就处理。
6.3.2 postEvent
1 | QKeyEvent *event = new QKeyEvent(QEvent::KeyPress, Qt::Key_A, Qt::NoModifier, "a"); |
意思是:
1 | 把事件放进 Qt 事件队列。 |
可以理解为:先排队,稍后处理。
注意:postEvent() 传进去的事件一般要用 new 创建,因为 Qt 之后会接管并删除它。
