事件
用户进行的各种操作,除了产生信号,也可能产生事件。我们同样可以给事件关联上处理函数,当事件出发的时候,就能够执行到对应的代码
事件本身是操作系统提供的机制。Qt也同样把操作系统事件机制进行了封装
Qt对于事件机制又进行了又一步封装,就得到了信号槽
可以说,事件是信号槽的底层机制
文章目录
事件处理
处理事件的常见方法,就是:
利用多态机制,创建子类,继承Qt已有的类,让子类,重写某个事件处理函数
鼠标事件
enterEvent && leaveEvent
鼠标进入、离开事件
[virtual protected] void QWidget::enterEvent(QEnterEvent *event)
[virtual protected] void QWidget::leaveEvent(QEvent *event)
例如,当鼠标光标进入Widget
窗口时,就会触发enterEnvent
事件;离开窗口时,就会触发leaveEvent
事件。我们就可以对这两个事件进行处理:
// label.h
#ifndef LABEL_H
#define LABEL_H
#include <QLabel>
class Label : public QLabel {
Q_OBJECT
public:
Label(QWidget* parent = nullptr);
void enterEvent(QEnterEvent* event);
void leaveEvent(QEvent* event);
};
#endif // LABEL_H
// label.cpp
#include "label.h"
#include <QDebug>
Label::Label(QWidget* parent) : QLabel(parent) {}
void Label::enterEvent(QEnterEvent* event) { qDebug() << "enter..."; }
void Label::leaveEvent(QEvent* event) { qDebug() << "leave..."; }
注意:如果我们要测试的控件,是在Qt Designer
中放置的控件,那么其还是Qt内置的类,如果我们需要让其能够执行我们自定义的事件处理函数,就需要对其进行提升:
同时,还可能需要在CMakeLists.txt
中加上:include_directories(${CMAKE_CURRENT_SOURCE_DIR})
。指定头文件搜索路径
效果:
mousePressEvent
鼠标点击事件
[virtual protected] void QWidget::mousePressEvent(QMouseEvent *event)
这里的鼠标点击,不仅仅是左键点击,只要是鼠标上的按键点击,都可以触发
例如,我们可以通过这个事件,来获取鼠标点击的位置
// label.h
#ifndef LABEL_H
#define LABEL_H
#include <QLabel>
#include <QMouseEvent>
class Label : public QLabel {
Q_OBJECT
public:
Label(QWidget* parent = nullptr);
void mousePressEvent(QMouseEvent* event);
};
#endif // LABEL_H
// label.cpp
#include "label.h"
#include <QDebug>
Label::Label(QWidget* parent) : QLabel(parent) {}
void Label::mousePressEvent(QMouseEvent* event) {
qDebug() << "x: " << event->x() << ", y: " << event->y();
}
效果:
mouseReleaseEvent
鼠标释放事件
[virtual protected] void QWidget::mouseReleaseEvent(QMouseEvent *event)
例如:
#ifndef LABEL_H
#define LABEL_H
#include <QLabel>
#include <QMouseEvent>
class Label : public QLabel {
Q_OBJECT
public:
Label(QWidget* parent = nullptr);
void mousePressEvent(QMouseEvent* event);
void mouseReleaseEvent(QMouseEvent* event);
};
#endif // LABEL_H
// label.cpp
#include "label.h"
#include <QDebug>
Label::Label(QWidget* parent) : QLabel(parent) {}
void Label::mousePressEvent(QMouseEvent* event) {
qDebug() << "鼠标点击 " << "x: " << event->x() << ", y: " << event->y();
}
void Label::mouseReleaseEvent(QMouseEvent* event) { qDebug() << "鼠标释放"; }
效果:
mouseDoubleClickEvent
鼠标双击事件
[virtual protected] void QWidget::mouseDoubleClickEvent(QMouseEvent *event)
例如:
#ifndef LABEL_H
#define LABEL_H
#include <QLabel>
#include <QMouseEvent>
class Label : public QLabel {
Q_OBJECT
public:
Label(QWidget* parent = nullptr);
void mousePressEvent(QMouseEvent* event);
void mouseReleaseEvent(QMouseEvent* event);
void mouseDoubleClickEvent(QMouseEvent* event);
};
#endif // LABEL_H
// label.cpp
#include "label.h"
#include <QDebug>
Label::Label(QWidget* parent) : QLabel(parent) {}
void Label::mousePressEvent(QMouseEvent* event) {
qDebug() << "鼠标点击 " << "x: " << event->x() << ", y: " << event->y();
}
void Label ::mouseDoubleClickEvent(QMouseEvent* event) { qDebug() << "鼠标双击"; }
void Label::mouseReleaseEvent(QMouseEvent* event) { qDebug() << "鼠标释放"; }
- 鼠标双击,同样可以触发鼠标点击事件
- 因此,如果一个控件既要处理鼠标单击事件,又要处理鼠标双击事件,此时就可能出现bug
效果:
mouseMoveEvent
鼠标移动事件
[virtual protected] void QWidget::mouseMoveEvent(QMouseEvent *event)
需要注意:
- 鼠标移动不同于鼠标按下
- 随便一动一下鼠标,就会产生大量的鼠标移动事件,Qt就需要花费大量的资源来处理事件处理函数
- 因此Qt为了保证效率,默认不会处理鼠标移动事件,除非我们显式指定**
setMouseTracking()
**
例如:
#ifndef LABEL_H
#define LABEL_H
#include <QLabel>
#include <QMouseEvent>
class Label : public QLabel {
Q_OBJECT
public:
Label(QWidget* parent = nullptr);
void mouseMoveEvent(QMouseEvent* event);
};
#endif // LABEL_H
// label.cpp
#include "label.h"
#include <QDebug>
Label::Label(QWidget* parent) : QLabel(parent) {
this->setMouseTracking(true); // 设置开启鼠标追踪
}
void Label::mouseMoveEvent(QMouseEvent* event) {
qDebug() << "移动 " << "x: " << event->x() << ", y: " << event->y();
}
效果:
wheelEvent
鼠标滚动事件
[virtual protected] void QWidget::wheelEvent(QWheelEvent *event)
例如:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWheelEvent>
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget {
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void wheelEvent(QWheelEvent *event);
private:
Ui::Widget *ui;
int total_ = 0;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include <QDebug>
#include "./ui_widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); }
Widget::~Widget() { delete ui; }
void Widget::wheelEvent(QWheelEvent *event) {
total_ += event->angleDelta().y();
qDebug() << total_;
}
-
方法
angleDelta
用于获取滚轮的角度增量 -
它返回一个
QPoint
对象,包含了水平和垂直两个方向的滚动信息:angleDelta().y()
对应原来的垂直方向滚动(与旧的delta()
功能相同)angleDelta().x()
可以获取水平滚轮(如果鼠标支持)的滚动信息
效果:
键盘事件
keyPressEvent
键盘按下事件
[virtual protected] void QWidget::keyPressEvent(QKeyEvent *event)
例如:
#ifndef WIDGET_H
#define WIDGET_H
#include <QKeyEvent>
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget {
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void keyPressEvent(QKeyEvent *event);
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include <QDebug>
#include "./ui_widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); }
Widget::~Widget() { delete ui; }
void Widget::keyPressEvent(QKeyEvent *event) {
qDebug() << "按下按键: " << event->key();
if (event->key() == 'A' && event->modifiers() == Qt::ShiftModifier) {
qDebug() << "按下了: Shift + A";
}
}
QKeyEvent
的方法key()
,用于返回用户按下的按键的Key
值,不区分大小写QKeyEvent
的方法modifiers()
,用于返回键盘修饰键(CTRL,SHIFT、ALT…)的状态(是否被按下)- 而这组合就可以判断组合键的按下
定时器事件 QTimer
之前我们将定时器和信号槽机制关联使用过,实际上,QTimer背后是QTimerEvent定时器事件进行支撑的
QObject
提供了一个timerEvent
这个函数,来处理定时器事件
同时可以使用方法startTimer
和killTimer
来启动和关闭定时器事件
int QObject::startTimer(int interval, Qt::TimerType timerType = Qt::CoarseTimer)
void QObject::killTimer(int id)
startTimer
会返回一个time_id
,即定时器的唯一标识killTimer
需要传入一个time_id
,即要关闭哪一个计时器
例如:
#ifndef WIDGET_H
#define WIDGET_H
#include <QTimerEvent>
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget {
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void timerEvent(QTimerEvent *event);
private:
Ui::Widget *ui;
int time_id_;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include <QDebug>
#include "./ui_widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) {
ui->setupUi(this);
time_id_ = startTimer(1000); // 设置一个1000ms触发一次的定时器,并记录time_id
ui->lcdNumber->display(6);
}
Widget::~Widget() { delete ui; }
void Widget::timerEvent(QTimerEvent *event) {
// 说明是自己这个类触发的定时器
if (event->timerId() == time_id_) {
int val = ui->lcdNumber->value();
if (val > 0) {
val -= 1;
ui->lcdNumber->display(val);
} else {
killTimer(time_id_);
}
}
}
窗口事件
moveEvent && resizeEvent
[virtual protected] void QWidget::moveEvent(QMoveEvent *event)
[virtual protected] void QWidget::resizeEvent(QResizeEvent *event)
moveEvent
:用来检测窗口的移动
resizeEvent
:用来检测窗口的大小
例如:
#ifndef WIDGET_H
#define WIDGET_H
#include <QMoveEvent>
#include <QResizeEvent>
#include <QWidget>
QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE
class Widget : public QWidget {
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void moveEvent(QMoveEvent *event);
void resizeEvent(QResizeEvent *event);
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
// widget.cpp
#include "widget.h"
#include <QDebug>
#include "./ui_widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); }
Widget::~Widget() { delete ui; }
void Widget::moveEvent(QMoveEvent *event) {
qDebug() << "窗口移动 " << "之前的位置: " << event->oldPos() << "现在的位置: " << event->pos();
}
void Widget::resizeEvent(QResizeEvent *event) {
qDebug() << "窗口大小变化 " << "之前的大小: " << event->oldSize()
<< "现在的大小: " << event->size();
}
效果:
事件过滤器
问题
如果要实现这样的功能:
点击按钮,弹出对话框,对话框中有多个
QLineEdit
,我们想要在对话框中,以按下空格的方式,切换QLineEdit
的焦点
利用上述学到的事件知识,可以利用事件keyPressEvent
,重写QLineEdit
的keyPressEvent
,不难写出下面的代码:
// LineEdit.cpp
#include "lineedit.h"
LineEdit::LineEdit(QWidget *parent) : QLineEdit(parent) {}
void LineEdit::keyPressEvent(QKeyEvent *event) {
// 如果按下空格,就将焦点移动到下一个widget
if (event->key() == Qt::Key_Space) {
this->focusNextChild();
} else {
QLineEdit::keyPressEvent(); // 如果不是空格,就使用默认的keyPressEvent事件
}
}
注意:方法focusNextChild
的功能为,将输入焦点从当前控件转移到父窗口中 “下一个” 符合焦点规则的子控件
但是,如果对话框中,不只有LineEdit
,还有其他的控件,仅仅重写QLineEdit
的keyPressEvent
事件,就无法解决问题了:
可以看到,因为我没有重写QPushButton
的keyPressEvent
事件,导致当焦点转移到QPushButton
上时,再按下空格,就会触发QPushButton
的默认逻辑,这不是我们想要的。
解决办法之一,就是继续重写QPushButton
的keyPressEvent
事件,但是如果不同的控件多起来,这种方法效率显然就不高了
事件过滤器
针对上述情况,最好的办法就是让对话框Dialog
监视其子控件的键盘事件
上面的方法,也就是Qt的事件过滤器。实现一个事件过滤包括两个步骤:
-
在监视对象的构造函数中,被监视对象调用installEventFilter(),注册监视对象。
-
在监视对象的eventFilter()函数中处理目标对象的事件。
[virtual] bool QObject::eventFilter(QObject *watched, QEvent *event)
- 作用:如果
watched
被注册到了Filter
中,那么所有发送到watched
的事件,都会先经过事件过滤器Filter
(即函数eventFilter
) - 它相当于一个 “事件拦截站”,可以在事件到达目标对象前对其进行处理或拦截
- 如果返回
TRUE
,表示事件过滤器对watched
的event
事件进行了处理并拦截,不会再将event
事件发送给watched
- 如果返回
FALSE
,表示过滤器不拦截event
,Qt 会继续将事件传递给 watched 对象,由其正常处理(如调用自身的事件处理函数
例如:
#include "dialog.h"
#include <QKeyEvent>
#include "ui_dialog.h"
Dialog::Dialog(QWidget *parent) : QDialog(parent), ui(new Ui::Dialog) {
ui->setupUi(this);
ui->lineEdit->installEventFilter(this);
ui->lineEdit_2->installEventFilter(this);
ui->lineEdit_3->installEventFilter(this);
ui->pushButton->installEventFilter(this);
ui->pushButton_2->installEventFilter(this);
ui->pushButton->setFocusPolicy(Qt::StrongFocus);
ui->pushButton_2->setFocusPolicy(Qt::StrongFocus);
}
Dialog::~Dialog() { delete ui; }
bool Dialog::eventFilter(QObject *watched, QEvent *event) {
// 如果watched被注册到了Filter中
if (watched == ui->lineEdit || watched == ui->lineEdit_2 || watched == ui->lineEdit_3 ||
watched == ui->pushButton || watched == ui->pushButton_2) {
// 如果是按钮按下事件
if (event->type() == QEvent::KeyPress) {
QKeyEvent *key_event = static_cast<QKeyEvent *>(event);
// 如果按下的是空格
if (key_event->key() == Qt::Key_Space) {
this->focusNextChild();
return true; // 拦截事件
}
}
}
return QDialog::eventFilter(watched, event);
}
效果:
事件过滤器的优先级
在 Qt 中,事件过滤器的优先级遵循 “安装顺序”和“作用范围” 的规则,具体可分为以下几种情况:
同一目标对象的多个过滤器
当一个目标对象(如 QLineEdit
)被多个过滤器对象监视时,后安装的过滤器会优先处理事件。
例如:
// 目标对象
QLineEdit *edit = new QLineEdit;
// 过滤器 A 先安装
edit->installEventFilter(filterA);
// 过滤器 B 后安装
edit->installEventFilter(filterB);
此时,事件到达顺序为:filterB->eventFilter()
→ filterA->eventFilter()
→ 目标对象自身的事件处理。
- 若
filterB
返回true
(拦截事件),则filterA
和目标对象都不会收到该事件。 - 若
filterB
返回false
(不拦截),事件才会传递给filterA
。
2. 不同作用范围的过滤器
当事件同时被局部过滤器(针对单个目标对象)和全局过滤器(如 QApplication
安装的过滤器)监视时,局部过滤器优先处理。
例如:
// 全局过滤器(监视所有对象)
qApp->installEventFilter(globalFilter);
// 局部过滤器(只监视 edit)
edit->installEventFilter(localFilter);
此时,事件到达顺序为:localFilter->eventFilter()
→ globalFilter->eventFilter()
→ 目标对象自身的事件处理。
- 局部过滤器(
localFilter
)先判断是否处理事件,若拦截(返回true
),全局过滤器和目标对象均无感知。 - 若局部过滤器不拦截,事件才会传递给全局过滤器。
3. 目标对象与父对象的过滤器
如果目标对象和它的父对象(如 QDialog
)都安装了过滤器,目标对象自身的过滤器(或针对它的过滤器)优先于父对象的过滤器。
例如:
// 父窗口 dialog 安装了过滤器(监视自身及子对象)
dialog->installEventFilter(dialogFilter);
// 子控件 edit 安装了自己的过滤器
edit->installEventFilter(editFilter);
此时,事件到达顺序为:editFilter->eventFilter()
→ dialogFilter->eventFilter()
→ edit
自身事件处理。
- 子控件的过滤器先处理,若拦截,父窗口的过滤器和子控件自身均不会收到事件。
总结
- 同一目标的多个过滤器:后安装的过滤器优先级更高(先执行)。
- 局部过滤器 vs 全局过滤器:局部过滤器(针对单个对象)优先级高于全局过滤器(针对所有对象)。
- 子对象 vs 父对象过滤器:子对象的过滤器(或针对子对象的过滤器)优先级高于父对象的过滤器。