目录
前言:
一、事件介绍
二、事件的处理
(一)鼠标事件
1. 进入和离开事件
2. 鼠标点击事件
3. 释放事件
4. 双击事件
5. 移动事件
6. 滚轮事件
(二)键盘按键事件
1. 单个按键
2. 组合按键
(三)定时器
1. QTimerEvent
2. QTimer
(四)窗口事件
三、事件分发器
(一)事件分发器工作原理
四、事件过滤器
前言:
在信号和槽章节中,我们了解到用户进行的操作可能会产生某种信号,给一个信号连接上槽函数,当信号触发的时候,就可以执行对应的槽函数,进而完成各种功能。除了信号,用户进行的操作也会产生事件,我们也可以给事件关联上处理函数,当事件触发的时候,就可以执行对应的代码。
这么一看,事件和信号还是差不多的,事件本身是操作系统提供的机制,Qt 把操作系统的事件机制进行封装,但是事件的代码编写起来不是很方便,所以 Qt 对于事件进一步封装,这就是信号和槽,事件就是它的底层机制。
实际 Qt 开发过程中,多数的交互功能都是通过信号和槽来完成的,但是也有特殊的情况,信号和槽无法实现,就比如 Qt 中没有这个信号,这就需要重写事件处理函数,来手动处理事件的逻辑。
一、事件介绍
事件是应用程序或者外部的事情或者动作的统称。在Qt平台中使用一个对象来表示一个事件。所有的Qt事件均继承于抽象类QEvent。事件是由系统或者Qt平台本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件是在用户操作时发出,如键盘事件、鼠标事件等,另一些事件则是由系统本身自动发出,如定时器事件。常见的Qt事件如下:
常见的 Qt 事件如下:
事件名称 | 描述 |
---|---|
⿏标事件 | ⿏标左键、⿏标右键、⿏标滚轮,⿏标的移动,⿏标按键的按下和松开 |
键盘事件 | 按键类型、按键按下、按键松开 |
定时器事件 | 定时时间到达 |
进⼊离开事件 | ⿏标的进⼊和离开 |
滚轮事件 | ⿏标滚轮滚动 |
绘屏事件 | 重绘屏幕的某些部分 |
显示隐藏事件 | 窗⼝的显⽰和隐藏 |
移动事件 | 窗⼝位置的变化 |
窗⼝事件 | 是否为当前窗⼝ |
⼤⼩改变事件 | 窗⼝⼤⼩改变 |
焦点事件 | 键盘焦点移动 |
拖拽事件 | ⽤⿏标进⾏拖拽 |
二、事件的处理
事件的处理一般常用做法是:重写相关的Event函数。
在 Qt 中,几乎所有的 Event 函数都是虚函数,所以可以重新实现。比如:在实现鼠标的进入和离开事件时,直接重新实现 enterEvent() 和 leaveEvent() 即可。enterEvent() 和 leaveEvent() 函数原型如下:
(一)鼠标事件
1. 进入和离开事件
代码示例:使用enterEvent事件来实现,鼠标进入或者离开一个Label文本框,就在控制台输出对应的打印。
这里基类选择 QWidget,同时设置文本边界框,方便观察:
还需在项目中新设计一个类,然后让这个类继承于Label控件:
类创建好之后,给构造函数中添加一个父元素参数,方便Qt将我们创建的mylabel对象挂载到对象树上去。这个类已经继承了QLabel,重写两个事件函数就可以了:
#include "mylabel.h"
#include <QDebug>
mylabel::mylabel(QWidget *parent) : QLabel(parent) {}
void mylabel::enterEvent(QEvent *event)
{
(void) event; // 这个参数暂时用不到
qDebug() << "鼠标进入文本框";
}
void mylabel::leaveEvent(QEvent *event)
{
(void) event; // 这个参数暂时用不到
qDebug() << "鼠标离开文本框";
}
在ui界面选中文本框,右击点击 “提升为”,只有自己创建的mylabel才可以触发我们自己写的这个事件处理函数,一定要注意不要拼错类名:
之后这里的类名就变成了我们定义的mylabel了:
运行程序,鼠标每次进入或者离开文本框就会触发对应的处理函数,打印出对应的内容:
2. 鼠标点击事件
在Qt中,鼠标事件是用QMouseEvent类来实现的。当在窗口中按下鼠标或者移动鼠标时,都会产生鼠标事件。利用QMouseEvent类可以获取鼠标的哪个键被按下了以及鼠标的当前位置等信息。
在Qt帮助文档中查找QMouseEvent类,如下图示:
代码示例:当鼠标在Label文本框中点击时,则获取到鼠标坐标。
在Qt中,鼠标按下是通过虚函数mousePressEvent()来捕获的mousePressEvent()函数原型如下:
- Qt::LeftButton:鼠标左键
- Qt::RightButton:鼠标右键
- Qt::MidButton:鼠标滚轮
还有一点要注意的就是,不管是使用鼠标左键、右键还是滚轮,甚至按下侧键也可以触发这个事件:
#include <QMouseEvent>
void mylabel::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
qDebug() << "按下左键";
else if(event->button() == Qt::RightButton)
qDebug() << "按下右键";
else
qDebug() << "其他键";
// 以Label左上角为原点
qDebug() << "以Label左上角为原点: " << event->x() << "," << event->y();
// 以整个屏幕左上角为原点
qDebug() << "以整个屏幕左上角为原点: " << event->globalX() << "," << event->globalY();
}
在文本框和内点击,就可以查看点击的坐标:
3. 释放事件
鼠标释放事件是通过mouseReleaseEvent()来捕获的:
void mylabel::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
qDebug() << "按下左键";
else if(event->button() == Qt::RightButton)
qDebug() << "按下右键";
else
qDebug() << "其他键";
}
void mylabel::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
qDebug() << "左键释放";
else if (event->button() == Qt::RightButton)
qDebug() << "右键释放";
else
qDebug() << "其他键释放";
}
4. 双击事件
双击事件通过虚函数mouseDoubleClickEvent()来实现:
void mylabel::mouseDoubleClickEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
qDebug() << "双击左键";
else if(event->button() == Qt::RightButton)
qDebug() << "双击右键";
else
qDebug() << "双击其他键";
}
5. 移动事件
鼠标移动事件通过mouseMoveEvent()来实现,为了实时捕捉鼠标位置的信息,需要通过setMouseTracking()方法追踪鼠标的位置:
鼠标移动不同于以上操作。随便移动鼠标就会产生大量事件,当捕获这个事件时,再进行一些复杂的逻辑,那么程序负担就很重,很容易产生卡顿等问题。
所以 Qt 为了保证程序的流畅性,默认情况下不会对鼠标移动进行追踪,也就不会调用mouseMoveEvent,只有在构造函数中指明当前窗口需要捕捉鼠标移动事件,使用setMouseTracking()方法,参数设置为true:
mylabel::mylabel(QWidget *parent) : QLabel(parent)
{
this->setMouseTracking(true);
}
void mylabel::mouseMoveEvent(QMouseEvent *event)
{
qDebug() << event->x() << event->y();
}
6. 滚轮事件
Qt 中滚轮事件是通过QWheelEvent类实现的,而滚轮滑动的距离可以通过delta()方法获取:
void mylabel::wheelEvent(QWheelEvent *event)
{
qDebug() << event->delta();
}
打印的值为正负120,滚轮向上滚动为 +,向下滚动为 - :
现在我们可以写一个通过滚轮调节字体大小的:
void mylabel::wheelEvent(QWheelEvent *event)
{
QFont font = this->font();
qDebug() << font;
if (event->delta() > 0)
font.setPointSize(font.pointSize() + 1);
else if (event->delta() < 0)
font.setPointSize(font.pointSize() - 1);
this->setFont(font);
}
(二)键盘按键事件
Qt 中的按键事件是通过 QKeyEvent 类来实现的。当键盘上的按键被按下或者被释放时,键盘事件便会触发。
在帮助文档中查找 QKeyEvent 类如下:
1. 单个按键
之前我们也使用过QShortCut,这个是信号和槽封装的获取键盘的方式,站在更底层的角度课可以通过事件获取当前用户键盘按下的情况,使用的是keyPressEvent:
#include <QKeyEvent>
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() << "按钮被按下"; // 打印ASCII值
qDebug() << event->text() << "按钮被按下";
if(event->key() == Qt::Key_A)
qDebug() << "按下了a键";
}
运行效果如下:
还有一点要注意的是,我们这里直接在QWidget中重写了这个事件函数,也可以在QMainWindow中重写这个事件函数,这里就要注意了,想要触发这个事件,一定要让该控件获取焦点,也就是说焦点不在,是触发不了事件的,什么是焦点,那就是要选中这个控件。
2. 组合按键
Qt::KeyboardModifier 中定义了在处理键盘事件时对应的修改键。在 Qt 中,键盘事件可以与修改键⼀起使⽤,以实现⼀些复杂的交互操作。KeyboardModifier 中修改键的具体描述如下:
Qt::NoModifier | ⽆修改键 |
Qt::ShiftModifier | Shift 键 |
Qt::ControlModifier | Ctrl 键 |
Qt::AltModifier | Alt 键 |
Qt::MetaModifier | Meta键(在Windows上指Windows键,在macOS上指Command键) |
Qt::KeypadModifier | 使⽤键盘上的数字键盘进⾏输⼊时,Num Lock键处于打开状态 |
Qt::GroupSwitchModifier | ⽤于在输⼊法 组之间 切换 |
void Widget::keyPressEvent(QKeyEvent *event)
{
// 判断 a+ctrl 键是否被按下
if(event->key() == Qt::Key_A && event->modifiers() == Qt::ControlModifier)
qDebug() << "ctrl+A 键被按下";
}
(三)定时器
Qt 在进行窗口程序处理的过程中,经常要周期性的执行某些操作,或者制作一些动画效果,使用定时器就可以实现,定时器就是间隔一段时间后执行某些任务。定时器在很多场景下都会使⽤到,如弹窗⾃动关闭之类的功能等。
Qt 中的定时器分为QTimerEvent和QTimer两个类:
- QTimerEvent类:用来描述一个定时器事件,使用startTimer()函数来开启定时器,需要输入一个以毫秒为单位的整数作为参数来表明设定的时间,它返回的整型值代表一个定时器。当定时器溢出时(定时时间到达)就可以在timeEvent()函数中获取该定时器的编号来进行相关操作。
- QTimer类:用来实现定时器,它提供了更高一层的编程接口,比如:可以连接信号和槽,还可以设置只运行一次的定时器。QTimer 的背后是QTimerEvent 定时器事件进行支撑的。
1. QTimerEvent
代码示例:在 UI 界面上放置一个 LCD Number 控件,让其 10 秒数字不断递减到 0,相当于倒计时。
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 开启定时器事件
// 返回一个定时器id
timerId = this->startTimer(1000);
}
Widget::~Widget()
{
delete ui;
}
void Widget::timerEvent(QTimerEvent *event)
{
// 如果一个程序中存在多个定时器(startTimer创建的定时器),此时每个定时器都会触发这个函数
// 先判断
if(event->timerId() != this->timerId) // 如果不是就忽略
return;
int value = ui->lcdNumber->intValue();
if(value <= 0)
{
// 停止定时器
this->killTimer(this->timerId);
return;
}
value -= 1;
ui->lcdNumber->display(value);
}
运行效果如下,倒计时:
使用timerEvnet比QTimer还要复杂,需要手动管理timerId,区分这次的timerId是否正确,所以后续还是使用QTimer。
2. QTimer
代码示例:在UI界⾯放置⼀个 LCD 标签,两个按钮,分别是 “开始” 和 “停⽌” ,当点击 “开始” 按钮时,开始每隔1秒计数⼀次,点击 “停⽌” 按钮时,暂停计数。
#include <QTimer>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QTimer *time = new QTimer(this);
connect(ui->pushButton, &QPushButton::clicked, [=](){
time->start(1000);
});
connect(time, &QTimer::timeout, [=](){
static int num = 1;
ui->label->setText(QString::number(num++));
});
connect(ui->pushButton_2, &QPushButton::clicked, [=](){
time->stop();
});
}
运行效果如下,按下开始按钮开始计时,停止按钮则停止:
(四)窗口事件
moveEvent 是窗口移动时触发的事件,resizeEvent 是窗口大小改变时触发的事件。
拖动窗口和调整窗口大小就会打印相应的内容。
#include <QMoveEvent>
void Widget::moveEvent(QMoveEvent *event)
{
qDebug() << event->pos();
}
void Widget::resizeEvent(QResizeEvent *event)
{
qDebug() << event->size();
}
三、事件分发器
在 Qt 中,事件分发器(Event Dispatcher) 是⼀个核⼼概念,⽤于处理 GUI 应⽤程序中的事件。事件分发器负责将事件从⼀个对象传递到另⼀个对象,直到事件被处理或被取消。每个继承⾃ QObject类 或QObject类 本⾝都可以在本类中重写 bool event(QEvent *e) 函数,来实现相关事件的捕获和拦截。
(一)事件分发器工作原理
在 Qt 中,我们发送的事件都是传给了 QObject 对象,更具体点是传给了 QObject 对象的 event() 函数。所有的事件都会进⼊到这个函数⾥⾯,那么我们处理事件就要重写这个 event() 函数。event() 函数本⾝不会去处理事件,⽽是根据 事件类型(type值)调⽤不同的事件处理函数。事件分发器就是⼯作在应⽤程序向下分发事件的过程中,如下图:
如上图,事件分发器⽤于分发事件。在此过程中,事件分发器也可以做拦截操作。事件分发器主要是通过 bool event(QEvent *e) 函数来实现。其返回值为布尔类型,若为 ture,代表拦截,不向下分发。
上面是比较官方的理解,下面我来说一说自己对于Qt事件分发器的理解:
举个具体的例子:假设一个ui界面上有一个Label控件,现在我在这个Label控件上鼠标左键点击了一下,讲道理来说这是会触发这个Label控件的鼠标点击事件的,如果我们重写了mousePressEvent()事件,那么接下来Qt程序就会去调用这个事件处理函数,来处理用户做出点击操作,但是现在有了Qt事件分发器这一层,因此用户产生的这个点击事件,会先到事件分发器这一层,在这一层中用户可以来决定是否继续向下分发这个事件,如果用户在这一层拦截了这个鼠标点击事件,那么mousePressEvent()事件就不会被执行,转而去执行用户在事件分发层设计的一些拦截逻辑;相反,如果用户下放了这个事件,那么这个鼠标点击事件最终会被它的默认处理动作,也就是mousePressEvent()事件来进行处理。
在这其中,每个控件的 bool event(QEvent* ev);接口被当作每个控件自己的事件分发器,对于一个控件来说,如果想要享受事件分发器的功能,那么就请重写event()接口,同时如果确实想要拦截的话,那么返回值请return true,否则return false。
代码示例:拦截一下鼠标左键点击事件
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QMouseEvent>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
Widget::~Widget()
{
delete ui;
}
void Widget::mousePressEvent(QMouseEvent *event)
{
if(event->button() == Qt::LeftButton)
qDebug() << "鼠标左键被按下";
}
bool Widget::event(QEvent *event)
{
if(event->type() == QEvent::MouseButtonPress)
{
qDebug() << "Event中鼠标被按下";
return true; // 返回true代表不向下分发
}
// 其他事件交给父类处理(默认处理)
return QWidget::event(event);
}
运行效果如下,当在窗口中点击鼠标左键时,就会执行event函数,而不会执行mousePressEvent函数:
四、事件过滤器
在 Qt 中,一个对象可能经常要查看或拦截另外一个对象的事件,如对话框想要拦截按键事件,不让别的组件接收到,或者修改按键的默认值等。通过上面的学习,我们已经知道,Qt 创建了 QEvent 事件对象之后,会调用 QObject 的 event() 函数 处理事件的分发。显然,我们可以在 event() 函数中实现拦截的操作。由于 event() 函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个 event() 函数。这当然相当⿇烦,更不用说重写 event() 函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。
事件过滤器是在应用程序分发到 event 事件分发器 之前,再做一次更高级的拦截。
上面是对于事件过滤器比较官方的解释,我来说一说我自己对于事件过滤器的理解:
诚然,上面我们了解到的事件分发器似乎也能做到过滤的作用,但是事件分发器的过滤只能针对于一个控件本身所发出的事件进行过滤,如果有多个不同的控件,都有事件需要进行过滤操作,那么每个控件就都得重写自己的事件分发器(bool event(QEvent*ev);) ,这显然是费时费力的方式,因此为了高效的完成事件过滤工作,Qt提出了事件过滤器的概念。
事件过滤器的一般使用步骤:
- 创建事件过滤器
要实现事件过滤器,需要创建一个继承于QObject的类,并重写里面的eventFilter()函数;该函数会在事件到达控件对象时被调用,开发者可以在其中处理事件并返回布尔值来指示是否拦截该事件。如果返回true,表示事件已被拦截,如果返回false,则表示事件尚未被处理,继续向下传递;
- 安装事件过滤器
使用QObject类中的installEventFilter()函数将事件过滤器安装到目标对象上。安装事件过滤器的对象可以是任何继承自QObject的类,包括窗口、控件等。安装完成后,当目标对象接收到事件时,事件过滤器就会被调用。
- 事件处理与分发
在eventFilter()函数内部,你可以对事件进行预处理,然后根据需要调用QEvent::accept()来接受事件,或QEvent::ignore()来忽略事件。如果事件不被过滤器处理,它应该返回false以允许事件继续传递给其原始的接收者。
代码示例:演示事件过滤器的使用
在ui界面上设计一个Label,并带有边框,并提升:
重写一个Label类,让其继承自QLabel,并将其命名为mylabel,并在mylabel.cpp" 文件中实现鼠标点击事件和事件分发器:
#include "mylabel.h"
#include <QMouseEvent>
#include <QDebug>
mylabel::mylabel(QWidget *parent) : QLabel(parent)
{
}
void mylabel::mousePressEvent(QMouseEvent *event)
{
QString str = QString("鼠标按下: x = %1, y = %2").arg(event->x()).arg(event->y());
qDebug() << str.toUtf8().data();
}
bool mylabel::event(QEvent *event)
{
// 如果是鼠标按下,在event事件分发时做拦截操作
if(event->type() == QEvent::MouseButtonPress)
{
QMouseEvent *event = static_cast<QMouseEvent *>(event);
QString str = QString("Event函数中鼠标按下: x = %1, y = %2").arg(event->x()).arg(event->y());
qDebug() << str.toUtf8().data();
return true;
}
// 其他事件交给父类处理
return QLabel::event(event);
}
在 "widget.cpp" 文件中实现事件过滤器:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QMouseEvent>
#include <QEvent>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// 1. label安装事件过滤器 this:当前窗口安装事件过滤器
ui->label->installEventFilter(this);
}
Widget::~Widget()
{
delete ui;
}
// 2. 重写
bool Widget::eventFilter(QObject *obj, QEvent *event)
{
if(obj == ui->label) // 判断控件
{
if(event->type() == QEvent::MouseButtonPress)
{
QMouseEvent *event = static_cast<QMouseEvent *>(event);
QString str = QString("事件过滤器中鼠标按下: x = %1, y = %2").arg(event->x()).arg(event->y());
qDebug() << str.toUtf8().data();
return true;
}
}
// 其他的交给父类处理
return QWidget::eventFilter(obj, event);
}
运行效果如下,在标签中点击鼠标不会执行event函数,而是执行eventFilter函数: