【QT】事件系统入门——QEvent 基础与示例

发布于:2025-03-14 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、事件介绍

事件是 应用程序内部或者外部产生的事情或者动作的统称

  • 在 Qt 中使用一个对象来表示一个事件。所有的 Qt 事件均继承于抽象类 QEvent。
  • 事件是由系统或者 Qt 平台本身在不同的时刻发出的。
  • 当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。
  • 一些事件是在用户操作时发出,如键盘事件、鼠标事件等,另一些事件则是由系统本身自动发出,如定时器事件。

事件本身是 操作系统提供的 机制,Qt 也同样把操作系统事件机制进行了封装拿到了 Qt 中,但由于 事件 对应的代码编写起来不方便,因此 Qt 对于事件机制 又进行了进一步的 封装,就得到了 信号槽

常见的 QT 事件如下:

image-20250128131332619

不同场景下,要关注的点不一样。这些事件的子类中就会包含一些对应的不同属性。

常见事件描述:

从图片中提取的文字信息如下表所示:

事件名称 描述
鼠标事件 鼠标左键、鼠标右键、鼠标滚轮,鼠标的移动,鼠标按键的按下和松开
键盘事件 按键类型、按键按下、按键松开
定时器事件 定时时间到达
进入离开事件 鼠标的进入和离开
滚轮事件 鼠标滚轮滚动
绘屏事件 重绘屏幕的某些部分
显示隐藏事件 窗口的显示和隐藏
移动事件 窗口位置的变化
窗口事件 是否为当前窗口
大小改变事件 窗口大小改变
焦点事件 键盘焦点移动
拖拽事件 用鼠标进行拖拽

二、事件的处理

事件处理一般常用的方法为:重写相关的 Event 函数。

在 Qt 中,几乎所有的 Event 函数都是虚函数,所以可以重新实现。

比如:在实现鼠标的进入和离开事件时,直接重新实现 enterEvent()leaveEvent() 即可。enterEvent()leaveEvent() 函数原型如下:
img

🐇标签提升 & 演示

1)新建 Qt 项目

基类选择 QWidget,同时勾选 UI 界面文件,并且设计 UI 文件,如下:

image-20250129160200592

  • 有了边框,方便观察当前鼠标是否进入和离开

这里我们还需要创建 QLabel 子类,来重写 enterEventleaveEvent

2)在项目中新添加一个类

先选中项目名称 QEvent,点击鼠标右键,选择 add new … ,弹出如下对话框:

img

3)定义类名并选择基类

image-20250129160800887

4)此时项目中会新添加以下两个文件

img

5)修改基类,重写 enterEvent()leaveEvent

如果我们想了解这两个函数,则可以在 帮助文档 中查找对应的内容

  • 要想重写父类的函数就需要确保写的函数名字和函数的参数列表完全一致(形参名无所谓)。
  • 然后对这两个函数进行重写

label.hlabel.cpp 代码如下:

image-20250129161706755

但是当前代码还是有问题的,如下:

image-20250129163141755

  • 在 UI 文件中我们可以看到当前在界面上创建的这个 Label 并不是自己写的 Label,而是 QLabel
  • 但是我们需要确保界面上的这个 Label 是自己定义的 Label 类实例才会被执行,因此需要 提升

6)在 UI 文件中选中 Label,右键 ——> 提升为…,点击之后弹出如下:

image-20250129161405787

  • 这里需要确保这里填写的类名以及头文件 和 上述自定义的 类名头文件 匹配

💡 通过 “提升为…” 这样的方式就可以把 Qt Designer 中托上去的控件的类型转换成自定义的控件类型,如下:

img

7)执行效果

当鼠标进入设计好的标签之后,就会在应用程序输出栏中打印:enterEvent;鼠标移出设计好的标签之后,就会在应用程序输出栏中打印:leaveEvent

image-20250129163712359

  • 这个时候就说明当前的 enterEventleaveEvent 这两个事件就被我们给捕获到了。

🐇 示例 – 当鼠标点击时,获取对应的坐标值

该示例主要基于上面代码,实现:当鼠标点击时,获取对应的坐标值

void Label::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        qDebug() << "按下左键";
    } else if (event->button() == Qt::RightButton) {
        qDebug() << "按下右键";
    }
    // 当前 event 对象就包含了鼠标点击位置的坐标.
     qDebug() << event->x() << ", " << event->y();
    // globalX 和 globalY 是以屏幕左上角为原点, 获取的坐标.
     qDebug() << event->globalX() << ", " << event->globalY();
}

实现效果如下:

img

  • 我们这里就通过事件 获取到 鼠标点击 的位置
  • mouseEvent 这个函数 其实按左键、右键、滚轮都可以触发的,甚至还有 前进 后退侧键

三、键盘按键事件

  • Qt 中 QShortCut 是信号槽机制封装过 获取 键盘按键的 方式
  • 当然我们也可以从更底层角度,通过事件获取到当前用户键盘按下情况

Qt 中的按键事件是通过 QKeyEvent 类来实现的。当键盘上的按键被按下或者被释放时,键盘事件便会触发。

在帮助文档中查找 QKeyEvent 类,然后查找按键事件中所有的按键类型:在帮助文档中输入:Qt::Key,如下图:

img

1. 单个按键

示例:当某个按键被按下时,输出:某个按键被按下了;

  1. 新建项目,在头文件 “widget.h” 中声明虚函数 keyPressEvent();
  2. 然后重写 keyPressEvent() 虚函数,如下图

image-20250129202354185

2. 组合按键

在 Qt 助手中搜索:Qt::KeyboardModifier,如下图示:

img

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 用于在输入法组之间切换

这些修饰键常用于编程中处理键盘事件,特别是在使用Qt框架开发跨平台应用程序时。

代码如下:

void Widget::keyPressEvent(QKeyEvent *event)
{
    if(event->key() == Qt::Key_A){
        qDebug() << "A 按键被按下";
    }
    if(event->modifiers() == Qt::ControlModifier && event->key() == Qt::Key_A){
        qDebug() << "Ctrl + A 按键被按下";
    }
}

四、鼠标事件

在 Qt 中,鼠标事件是用 QMouseEvent 类来实现的。当在窗口中按下鼠标或者移动鼠标时,都会产生鼠标事件。

  • 利用 QMouseEvent 类可以获取鼠标的哪个键被按下了以及鼠标的当前位置等信息。

在 Qt 帮助文档中查找 QMouseEvent 类如下图示:

img

鼠标单击 | 释放 | 双击 | 移动 事件

① 在 Qt 中,鼠标按下是通过虚函数 mousePressEvent() 来捕获的。mousePressEvent() 函数原型如下:

  • [ virtual protected] void QWidget:: mousePressEvent (QMouseEvent * event )

鼠标左右键及滚的表示如下:

  1. Qt::LeftButton 鼠标左键

  2. Qt::RightButton 鼠标右键

  3. Qt::MidButton 鼠标滚轮

void Label::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        qDebug() << "按下左键";
    } else if (event->button() == Qt::RightButton) {
        qDebug() << "按下右键";
    }
}

② 鼠标释放事件是通过虚函数 mouseReleaseEvent() 来捕获的。mouseReleaseEvent() 函数原型如下:

  • [ virtual protected] void QWidget:: mouseReleaseEvent (QMouseEvent * event )
void Label::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        qDebug() << "释放左键";
    } else if (event->button() == Qt::RightButton) {
        qDebug() << "释放右键";
    }
}

③ 鼠标双击事件是通过虚函数:mouseDoubleClickEvent() 来实现的。mouseDoubleClickEvent() 函数原型如下:

  • [ virtual protected] void QWidget:: mouseDoubleClickEvent (QMouseEvent * event )
void Label::mouseDoubleClickEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        qDebug() << "双击左键";
    } else if (event->button() == Qt::RightButton) {
        qDebug() << "双击右键";
    }
}

④ 鼠标移动事件是通过虚函数:mouseMoveEvent() 来实现的。同时为了实时捕获鼠标位置信息,需要通过函数 setMouseTracking() 来追踪鼠标的位置。mouseMoveEvent()函数原型如下:

  • [ virtual protected] void QWidget:: mouseMoveEvent (QMouseEvent * event )

setMouseTracking() 函数原型如下:

  • void setMouseTracking( bool enable )

说明:setMouseTracking() 函数默认是 false,需要设置为 true,才能实时捕获鼠标位置信息。

否则只有当鼠标按下时才能捕获其位置信息。
image-20250129172741229

  • 上面的操作其实和 二中说的标签提升 那里一样,可以仔细看看

最后演示结果如下:

image-20250129173003843

  • 这里演示的话,还是把实时捕捉关了,敏感太高不适合演示现象,我们这里坐标可以长按拖拽鼠标也可以显示 鼠标位置信息
  • 我们这里重写鼠标事件的操作都是放在 自定义的 Label 中完成的,此时只有鼠标在 Label 范围内进行动作才能捕捉到
  • 当前也可以把其放到 Widget(Qwidget 子类) 来完成,此时鼠标在整个窗口都可以捕捉到

五、定时器

Qt 中在进行窗口程序的处理过程中,经常要周期性的执行某些操作,或者制作一些动画效果,使用定时器就可以实现。

所谓 定时器就是在间隔一定时间后,去执行某一个任务

定时器在很多场景下都会使用到,如弹窗自动关闭之类的功能等。

Qt 中的定时器分为 QTimerEventQTimer 这 2 个类。

  • QTimerEvent 类用来描述一个定时器事件。
    • 在使用时需要通过 startTimer() 函数来开启⼀个定时器,这个函数需要输入一个以毫秒为单位的整数作为参数来表明设定的时间,它返回的整型值代表这个定时器。
    • 当定时器溢出时(即定时时间到达)就可以在 timerEvent() 函数中获取该定时器的编号来进行相关操作。
  • QTimer 类来实现一个定时器,它提供了更高层次的编程接口,如:可以使用信号和槽,还可以设置只运行一次的定时器。

1. QTimerEvent 类

联系前文:【QT】 控件 – 显示类

在 UI 界面上放置一个 LCD Number 控件,让其 10 秒数字不断递减到 0,相当于倒计时。

  1. 新建项目,在 UI 界面文件放置一个 LCD Number 控件,并且给定初始值为 10
  2. 在 “widget.h” 头文件中声明 timerEvent() 函数,并定义一个整型变量
  3. 在 “widget.cpp” 文件中重写 timerEvent() 函数
#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    // 开启定时器事件
    // 此处 timerId 是一个定时器的身份标识(类似于 Linux 中的文件描述符)
    timerId = this->startTimer(1000); // 这里 timerId 设为成员变量
}

void Widget::timerEvent(QTimerEvent *event)
{
    // 如果一个程序中存在多个定时器 (startTimer 创建的定时器), 此时每个定时器都会触发 timerEvent 函数.
    // 先判定一下这次触发是否是想要的定时器触发的.
    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);
}
  • 此时运行程序就可以获得我们想要的倒计时结果了

但是相比于 QTimer ,使用 timerEvent 还是要更加复杂一点,因为需要手动管理 timerId,需要区分整个函数调用是由哪个 timer 引起

2. QTimer 类

纯代码实现,无调用 ui

  • 实现基本的计时功能
  • 还实现了**【获取系统日期及实时时间】**,如下:
#include "widget.h"
#include "ui_widget.h"

#include <QVBoxLayout>
#include <QLabel>
#include <QTimer>
#include <QPushButton>
#include <QDateTime>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    QVBoxLayout *layout = new QVBoxLayout(this);
    // 创建控件
    QLabel *label = new QLabel("0");
    label->setStyleSheet("font-size: 50px; color: red;");
    QLabel *label_date = new QLabel("0");
    label_date->setStyleSheet("font-size: 50px; color: blue;");

    QPushButton *startBtn = new QPushButton("开始");
    QPushButton *stopBtn = new QPushButton("停止");

    // 按钮布局
    QHBoxLayout *btnLayout = new QHBoxLayout;
    btnLayout->addWidget(startBtn);
    btnLayout->addWidget(stopBtn);

    // 主布局
    layout->addWidget(label, 0, Qt::AlignCenter);
    layout->addWidget(label_date, 0, Qt::AlignCenter);
    layout->addLayout(btnLayout);

    // 创建定时器和计数器
    QTimer *timer = new QTimer(this);
    
    // 连接信号与槽
    connect(timer, &QTimer::timeout, [=](){
        static int num=1;
        label->setText(QString::number(num++));
    });
    
    connect(timer, &QTimer::timeout, [=](){
        QString str=QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
        label_date->setText(str);
    });
    
    
    connect(startBtn, &QPushButton::clicked, [=](){
        timer->start(1000);
    });
    connect(stopBtn, &QPushButton::clicked, [=]{
        timer->stop();
    });
}

结果如下:

image-20250129220136849

六、事件分发器

1. 概述

在 Qt 中,事件分发器(Event Dispatcher)是一个核心概念,用于处理 GUI 应用程序中的事件。

事件分发器负责 将事件从一个对象传递到另一个对象,直到事件被处理或被取消。

每个继承自 QObject 类QObject 类 本身都可以在本类中 重写 bool event(QEvent *e) 函数,来实现相关事件的捕获和拦截。

2. 事件分发器工作原理

在 Qt 中,我们发送的事件是传给了 QObject 对象的 event() 函数。

  • 所有的事件都会进入到这个函数里面,那么我们处理事件就要重写这个 event() 函数。
  • event() 函数本身不会去处理事件,而是根据 事件类型(type值)调用不同的事件处理函数。事件分发器就是工作在应用程序向下分发事件的过程中,如下图:

如上图,事件分发器用于分发事件。在此过程中,事件分发器也可以做 拦截操作

  • 事件分发器主要是通过 bool event(QEvent *e) 函数来实现。其返回值为布尔类型,若为 true,代表拦截,不向下分发。
  • Qt 中的事件是封装在 QEvent类 中,在 Qt 助手中输入 QEvent 可以查看其所包括的事件类型,如下图示:

image-20250128133052036

演示代码如下:

void Widget::mousePressEvent(QMouseEvent *event)
{
    if(event->button() == Qt::LeftButton)
    {
        qDebug() << "鼠标左键按下";
    }
}

bool Widget::event(QEvent *ev)
{
    if(ev->type() == QEvent::MouseButtonPress)
    {
        qDebug() << "Event 中鼠标被按下";
        return true; // 表示不向下分发
    }
    // 其他事件交给父类处理(默认)
    return QWidget::event(ev);
}

结果如下:

image-20250129220805361

当我们 return false 时,就会出现一下结果

image-20250129221037788

  • 单机鼠标右键,鼠标被按下
  • 单击鼠标左键, event 函数和 mousePressEvent 函数交替触发

七、事件过滤器

在 Qt 中,一个对象可能经常要查看或拦截另外一个对象的事件,如对话框想要拦截按键事件,不让别的组件接收到,或者修改按键的默认值等。

  • 通过上面的学习,我们已经知道,Qt 创建了 QEvent 事件对象之后,会调用 QObjectevent() 函数 处理事件的分发。
  • 显然,我们可以在 event() 函数中实现拦截的操作。由于 event() 函数是 protected 的,因此,需要继承已有类。
  • 如果组件很多,就需要重写很多个 event() 函数。这当然相当⿇烦,更不用说重写 event() 函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器

事件过滤器是在 应用程序 分发到 event 事件分发器 之前,再做一次更高级的拦截

img

事件过滤器的⼀般使用步骤:

  1. 安装事件过滤器;
  2. 重写事件过滤器函数:eventfilter()

【示例】:基于文章上面演示的 标签提升 那的操作,在 "Label.cpp" 中代码如下:

#include "label.h"
#include <QDebug>
#include <QMouseEvent>

Label::Label(QWidget* parent): QLabel(parent)
{

}

void Label::mousePressEvent(QMouseEvent *event)
{
    QString str = QString("鼠标按下: x = %1, y = %2").arg(event->x()).arg(event->y());
    qDebug() << str.toUtf8().data();
}

bool Label::event(QEvent *e)
{
    //如果是鼠标按下,在event事件分发时做拦截操作
    if (e->type() == QEvent::MouseButtonPress)
    {
        QMouseEvent *event = static_cast<QMouseEvent *>(e);
        QString str = QString("Event函数中鼠标按下: x = %1, y = %2").arg(event->x()).arg(event->y());
        qDebug() << str.toUtf8().data();

        return true; //返回true,代表用户自己处理,不向下分发
    }

    //其他事件交给父类处理
    return QLabel::event(e);
}

Widget.cpp 代码如下:

#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QEvent>
#include <QMouseEvent>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    // 给 label 装上事件过滤器 this;当前窗口安装事件过滤器
    ui->label->installEventFilter(this);
}


bool Widget::eventFilter(QObject *obj, QEvent *e)
{
    if(obj == ui->label) // 判断控件
    {
        if(e->type() == QEvent::MouseButtonPress){
            QMouseEvent *event = static_cast<QMouseEvent *>(e);
            QString str = QString("事件过滤器中鼠标按下: x = %1, y = %2").arg(event->x()).arg(event->y());
            qDebug() << str.toUtf8().data();

            return true; // 返回true,代表用户自己处理,不向下分发
        }
    }
    return QWidget::eventFilter(obj, e);
}
  • 注意书写函数实现时,记得先声明函数名

结果如下:

img

八、其他

  • moveEvent:窗口移动时触发的事件
  • resizeEvent:窗口大小改变时触发的事件

image-20250130163923426


网站公告

今日签到

点亮在社区的每一天
去签到