QT从入门到实战x篇_22_番外1_Qt事件系统

发布于:2024-04-25 ⋅ 阅读:(17) ⋅ 点赞:(0)


比本篇好很多更多涉及底层的博文推荐: 【Qt 应用开发 】探索Qt事件处理时机:深入理解事件驱动机制

介绍清晰结构简洁的博文:Qt - QObject事件

以下部分来自:【Qt 元对象系统】深入探索Qt事件过滤:从基础到高级应用-CSDN博客Qt事件系统:Qt中的事件处理与传递Qt 事件机制Qt之事件处理机制

1. Qt事件系统简介

在Qt中,事件(Event)是一个核心概念,它代表了应用程序的一个动作或发生的事情。事件可以是用户的输入,如鼠标点击或键盘按键,也可以是系统生成的,如窗口大小改变或定时器超时。

为了处理这些事件,Qt提供了一个事件循环(Event Loop)。这个循环不断地检查是否有新的事件发生,然后将这些事件发送给相应的对象进行处理。

事件(event)是由系统或者Qt本身在不同时刻发出的。当用户按下鼠标、敲下键盘,或者其它情况时候都会发出一个相应的事件。一些事件在对用户操作做出相应时发出,如键盘事件等;另外一些则是由系统自动发出,如计时事件等。

Qt程序需要在main()函数创建一个QApplication对象,然后调用它的exec()函数。这个函数就是开始Qt的事件循环。在执行exec()函数之后,程序将进入事件循环来监听应用程序的事件,当事件发生时,Qt将创建一个事件对象。Qt中所有事件类都继承自QEvent。在事件对象创建完毕之后,Qt将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给指定的事件处理函数(event handler)进行处理。

  • 事件和信号的区别

需要说明的是,事件与信号并不相同,比如单击一下界面上的按钮,那么就会产生鼠标事件 QMou­seEvent (不是按钮产生的 ),而因为按钮被按下了 ,所以它会发出 clicked() 单击信号(是按钮产生的)。这里一般只关心按钮的单击信号,而不用考虑鼠标事件,但是如果要设计一个按钮,或者当单击按钮时让它产生别的效果,那么就要关心鼠标事件了。可以看到,事件与信号是两个不同层面的东西,发出者不同,作用也不同。在 Qt 中,任何 QObject 子类实例都可以接收和处理事件。摘自:[Qt事件系统:Qt中的事件处理与传递_事件处理系统对于每个学习qt人来说非常重要,可以说,qt是以事件驱动的ui工具集。 大-CSDN博客](Qt事件系统:Qt中的事件处理与传递_件处理系统对于每个学习qt人来说非常重要,可以说,qt是以事件驱动的ui工具集。 大-CSDN博客)

Qt的事件很容易和信号槽混淆。signal由具体对象发出,然后会马上交给由connect函数连接的slot进行处理;

而对于事件,Qt使用一个事件队列对所有发出的事件进行维护,当新的事件产生时,会被追加到事件队列的尾部,前一个事件完成后,取出后面的事件接着再进行处理。

但是,必要的时候,Qt的事件也是可以不进入事件队列,而是直接处理的。并且,事件还可以使用“事件过滤器”进行过滤。

比如一个按钮对象, 我们使用这个按钮对象的时候, 我们只关心它被按下的信号, 至于这个按钮如何接收处理鼠标事件,再发射这个信号,我们是不用关心的。

但是如果我们要重载一个按钮的时候,我们就要面对event了。 比如我们可以改变它的行为,在鼠标按键按下的时候(mouse press event) 就触发clicked()的signal而不是通常在释放的( mouse release event)时候。

总结的说,Qt的事件和Qt中的signal不一样。 后者通常用来使用widget, 而前者用来实现widget。 如果我们使用系统预定义的控件,那我们关心的是信号,如果自定义控件我们关心的是事件。摘自:Qt 事件机制

  • 事件的处理
    一个事件由一个特定的 QEvent 子类来表示,但是有时一个事件又包含多个事件类型,比如鼠标事件又可以分为鼠标按下、双击和移动等多种操作。这些事件类型都由 QEvent 类的枚举型 QEvent::Type 来表示,其中包含了 一百多种事件类型,可以在 QEvent 类的帮助文档中查看。虽然 QEvent 的子类可以表示一个事件,但是却不能用来处理事件,那么应该怎样来处理一个事件呢?在 QCoreApplication 类的 notify() 函数的帮助文档处给出了 5 种处理事件的方法:

    • 方法一:重新实现部件的 paintEvent()、mousePressEvent() 等事件处理函数。这是最常用的一种方法,不过它只能用来处理特定部件的特定事件。
    • 方法二:重新实现 notify() 函数。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。
    • 方法三:向 QApplication 对象上安装事件过滤器。因为一个程序只有一个 QApplication 对象,所以这样实现的功能与使用 notify() 函数是相同的,优点是可以同时处理多个事件。
    • 方法四:重新实现 event() 函数。QObject 类的 event() 函数可以在事件到达默认的事件处理函数之前获得该事件。
    • 方法五:在对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。

在实际编程中,最常用的是方法一,其次是方法五。因为方法二需要继承自 QApplication 类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递,所以,虽然这两种方法功能很强大,但是却很少被用到。

1.1 事件的来源和传递

在Qt中,事件可以由多种来源生成,包括用户输入、系统事件或自定义事件。一旦事件被生成,它会被发送到一个事件队列(Event Queue)。事件循环会从队列中取出事件,并将其传递给适当的对象进行处理

事件的传递是一个层层递进的过程。首先,事件会被发送到最顶层的对象,如应用程序对象(QApplication)。如果这个对象没有处理该事件,它会继续传递给下一级的对象,如窗口或控件,直到找到一个可以处理该事件的对象为止。

这种事件传递的方式,很像我们在生活中面对决策时的思考过程。当遇到一个问题时,我们首先会尝试自己解决,如果不能解决,我们可能会寻求他人的帮助,直到问题得到解决。

在每个程序的 main() 函数的最后都会调用 QApplication 类的 exec() 函数,它会使 Qt 应用程序进人事件循环,这样就可以使应用程序在运行时接收发生的各种事件。一旦有事件发生,Qt 便会构建一个相应的 QEvent 子类的对象来表示,然后将它传递给相应的 QObject 对象或其子对象。下面通过例子来看一下 Qt 中的事件传递过程。

新建 Qt Gui 应用,项目名称为 myEvent,基类选择 QWidget,然后类名保持 Widget 不变。建立完成后向项目中添加新文件,模板选择 C++ 类,类名为 MyLineEdit,基类手动填写为 QLineEdit,自定义了一个 MyLineEdit 类。

mylineEdit. h 文件:

#ifndef MYLINEEDIT_H
#define MYLINEEDIT_H
 
#include <QLineEdit>
 
class MyLineEdit : public QLineEdit
{
    Q_OBJECT
public:
    explicit MyLineEdit(QWidget *parent = nullptr);
    
    // event()函数获取事件的类型
	bool event(QEvent *event);    
 
protected:
    // MyLineEdit类的键盘按下事件
    void keyPressEvent(QKeyEvent *event);
};
 
#endif // MYLINEEDIT_H

这里添加了 keyPressEvent() 函数和 event() 函数的声明。

mylineEdit. cpp 文件:

#include "mylineedit.h"
#include <QKeyEvent>
#include <QDebug>
 
MyLineEdit::MyLineEdit(QWidget *parent) :
    QLineEdit(parent)
{
 
}
 
// MyLineEdit类的键盘按下事件
void MyLineEdit::keyPressEvent(QKeyEvent *event)
{
    qDebug() << tr("MyLineEdit键盘按下事件");
    // 让MyLineEdit输入栏能输入字符
    QLineEdit::keyPressEvent(event);          // 执行QLineEdit类的默认事件处理
    event->ignore();                          // 忽略该事件
}
 
//event()函数获取事件的类型
bool MyLineEdit::event(QEvent *event)  
{
    // 判断触发事件类型是否为键盘按下事件
    if(event->type() == QEvent::KeyPress)
        qDebug() << tr("MyLineEdit的event()函数");
    return QLineEdit::event(event);   // 执行QLineEdit类event()函数的默认操作
}

这里自定义了一个 MyLineEdit 类,它继承自 QWidget,并且实现了 MyLineEdit 类的 keyPressEvent() 函数和 event() 函数。event() 函数中使用了 event->type() 来获取事件的类型。如果是键盘按下事件 QEvent::KeyPress,则输出信息,另外返回父类的 event() 函数的操作结果。

widget.h 文件:

#ifndef WIDGET_H
#define WIDGET_H
 
#include <QWidget>
class MyLineEdit;
namespace Ui {
class Widget;
}
 
class Widget : public QWidget
{
    Q_OBJECT
 
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
 
	// Widget类的事件过滤器
    bool eventFilter(QObject *obj, QEvent *event);    
 
private:
    Ui::Widget *ui;
    MyLineEdit *lineEdit;
 
protected:
    // Widget类的键盘按下事件
    void keyPressEvent(QKeyEvent *event);
};
 
#endif // WIDGET_H

这里也添加了keyPressEvent()函数的声明。

widget.cpp 文件:

#include "widget.h"
#include "ui_widget.h"
#include "mylineedit.h"
#include <QKeyEvent>
#include <QDebug>
 
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
        
    lineEdit = new MyLineEdit(this);
    lineEdit->move(100, 100);
}
 
Widget::~Widget()
{
    delete ui;
}
 
// Widget类的键盘按下事件
void Widget::keyPressEvent(QKeyEvent *event)
{
    qDebug() << tr("Widget键盘按下事件");
}
 
// Widget类的事件过滤器
bool Widget::eventFilter(QObject *obj, QEvent *event) // 事件过滤器
{
    // 如果是lineEdit部件上的事件
    if(obj == lineEdit)
    {              
        if(event->type() == QEvent::KeyPress)
            qDebug() << tr("Widget的事件过滤器");
    }
    return QWidget::eventFilter(obj, event);
}

这里也实现了 Widget 类的 keyPressEvent() 函数,并且会调用 MyLineEdit 类的 keyPressEvent() 函数。在事件过滤器中,先判断该事件的对象是不是 lineEdit,如果是,再判断事件类型,最后返回 QWidget 类默认的事件过滤器的执行结果。

运行程序,然后按下键盘的任意键,比如这里按下 a 键,执行结果如下图所示。
在这里插入图片描述

可以看到,事件的传递顺序是这样的:先是事件过滤器,然后是焦点部件的 event() 函数,最后是焦点部件的事件处理函数,例如这里的键盘按下事件函数;如果焦点部件忽略了该事件,那么会执行父部件的事件处理函数,如上图所示。注意,event() 函数和事件处理函数,是在该部件内进行重新定义的,而事件过滤器却是在该部件的父部件中进行定义的。
在这里插入图片描述

1.2 事件循环和事件分发

1.2.1 QT消息/事件循环机制

事件循环是Qt事件处理的核心。它不断地检查事件队列,看是否有新的事件需要处理。一旦有事件,事件循环会将其取出,并通过事件分发(Event Dispatching)将其发送给适当的对象。

Qt作为一个可视化GUI界面操作系统,是基于事件驱动的,我们程序执行的顺序不再是线性的,而是由一个个应用程序内部或外部的事件进行驱动的,无事件时便阻塞。这个有点类似于while循环,函数体内不断处理用户的输入,类比到事件循环中,用户点击了鼠标,按下了键盘,便称为事件。
  一般对于带UI窗口的程序来说,“事件”是由操作系统或程序框架在不同的时刻发出的。当用户按下鼠标,敲下键盘,或是窗口需要重新绘制的时候,或是计时器触发的时候,都会发出一个相应的事件。下面是一个抽象的“循环事件”的代码:

 1 function eventloop()
 2 {
 3     initialize();
 4     bool shouldQuit = false;
 5     whlie (false == shouldQuit)
 6     {
 7         var message = get_next_message();
 8         process_message(message);
 9         if (message == QUIT)
10         {
11             shouldQuit = true;
12         }
13     }
14 }
1.2.1.1 机制解释

这样的程序运行流程,叫做“事件驱动”式的程序。一般的Qt程序,main函数中都会有一个QCoreApplication/QGuiApplication/QApplication,并在末尾调用exec。Application中的这个EventLoop,叫做“事件循环”,所有的事件分发、事件处理都从这里开始。

Application还提供了sendEvent和poseEvent两个函数,分别用来发送事件。sendEvent发出的事件会立即被处理,即“同步”执行。poseEvent发送的事件会被加入事件队列,在下一轮事件循环时才处理,即“异步”执行。

1.2.1.2 两个问题

(1) Qt是事件驱动的,怎么理解这句话
  Qt将系统产生的信号(软件中断)转换成Qt事件,并且将事件封装成类,所有的事件类都是由QEvent派生的,事件的产生和处理就是Qt程序的主轴,且伴随整个程序的运行周期。因此说Qt是事件驱动的。

QT将系统产生的消息转化为QT事件,QT事件被封装为对象,所有的QT事件均继承抽象类QEvent,用于描述程序内部或外部发生的动作,任意的QObject对象都具备处理QT事件的能力。

在这里插入图片描述

(2) Qt事件由谁产生的?
  事件有两个来源:程序内部和程序外部,多数情况下来自操作系统并且通过spontaneous()函数返回true来获知事件来自程序外部,当spontaneous()函数返回false时说明事件来自程序内部。

####1.2.1.2 Qt事件处理流程

事件循环:事件是一个类对象,具有特定的类型,多数情况下是被分发到一个队列中(事件队列),当队列中有事件时就不停地将队列中的事件发送给QObject对象,当队列为空时,就阻塞地等待事件。

在这里插入图片描述

QCoreApplication::exec()开启了这种循环,一直到QCoreApplication::exit()被调用才终止,所以说事件循环是伴随着Qt程序的整个运行周期。
另外一种同步处理情形是通过sendEvent()将事件发送出去,直接进入事件的传送和处理流程。

在这里插入图片描述

1.2.2 事件分发

事件分发是事件循环的一个重要环节。当事件被取出后,它需要被分发到正确的对象进行处理。Qt提供了一个事件分发器(QEventDispatcher)来完成这个任务。事件分发器会根据事件的类型和目标对象,将事件发送到正确的处理函数中。

要理解事件分发的底层原理,我们需要深入Qt的源码。在Qt的源码中,QEventDispatcher类负责事件的分发。当一个事件被取出时,QEventDispatcher会首先检查该事件的类型,然后根据事件的类型,调用相应的处理函数。

例如,当一个鼠标点击事件被取出时,QEventDispatcher会调用mousePressEvent()函数进行处理。这确保了每种事件都能被正确处理。

事件类型 处理函数
鼠标点击 mousePressEvent()
键盘输入 keyPressEvent()
窗口大小改变 resizeEvent()

2. 事件过滤基础

在深入探讨Qt的事件过滤机制之前,我们首先要理解为什么我们会选择某种编程方式。人们在面对问题时,往往会寻找最直观、最简单的方法来解决。这与我们的大脑对于复杂性的处理方式有关。我们的大脑会自动寻找模式,并尝试将新的信息与已知的模式相匹配,以减少认知负担。

2.1 什么是事件过滤器(Event Filter)?

事件过滤器(Event Filter)是Qt事件处理系统中的一个核心组件。它允许开发者在事件被传递给目标对象之前,对其进行拦截和处理。这种机制为我们提供了一个强大的工具,使我们能够在不修改目标对象的代码的情况下,对其行为进行定制。

从底层源码的角度看,当一个事件被发送或者发布时,它首先会被传递给安装在目标对象上的事件过滤器。如果事件过滤器选择处理这个事件,那么这个事件就不会再被传递给目标对象。反之,如果事件过滤器选择忽略这个事件,那么这个事件会继续被传递给目标对象。

这种机制的存在,使我们能够更加灵活地处理事件,而不需要修改已有的代码。

2.2 如何安装事件过滤器

在Qt中,安装事件过滤器是一个简单的过程。首先,你需要创建一个继承自QObject的类,并重写其eventFilter(QObject *watched, QEvent *event)方法。然后,你可以使用QObject::installEventFilter(QObject *filterObj)方法,将你的事件过滤器安装到任何QObject派生的对象上。

从底层原理的角度看,当你安装一个事件过滤器时,Qt会将这个过滤器添加到目标对象的内部事件过滤器列表中。当一个事件被发送到这个对象时,Qt会按照事件过滤器列表中的顺序,依次调用每一个事件过滤器的eventFilter方法。

为了帮助读者更好地理解事件过滤器的工作原理,我们可以从以下几个角度进行总结和对比:

角度 事件过滤器 常规事件处理
灵活性
代码侵入性
性能开销 低至中

3. 事件过滤实战

3.1 创建自定义事件过滤器

当我们想要对特定的事件进行处理或拦截时,自定义事件过滤器(Event Filter)成为了一个非常有力的工具。例如,当用户点击一个按钮(Button)时,我们可能想要在底层捕获这个点击事件并进行一些特殊的处理。

首先,我们需要创建一个继承自QObject的类,并重写其eventFilter(QObject *watched, QEvent *event)方法。在这个方法中,我们可以根据event的类型进行相应的处理。

bool CustomEventFilter::eventFilter(QObject *watched, QEvent *event) {
    if (event->type() == QEvent::MouseButtonPress) {
        // 处理鼠标点击事件
    }
    return QObject::eventFilter(watched, event);
}

在这里,我们可以看到事件过滤的真正力量。我们可以在事件到达目标对象之前捕获并处理它。这种能力使我们能够在底层进行细致的控制,而不仅仅是在高级API上进行操作。

3.2 过滤不同类型的事件

Qt提供了多种事件类型,例如QEvent::KeyPressQEvent::MouseMove等。每种事件类型都与特定的用户交互行为相关联。为了更好地理解这些事件类型,我们可以从以下几个角度进行对比:

事件类型(Event Type) 触发条件(Trigger Condition) 常见应用(Common Use Case)
QEvent::KeyPress 键盘按键被按下 快捷键处理
QEvent::MouseMove 鼠标移动 拖放操作
QEvent::MouseButtonPress 鼠标按钮被按下 自定义点击行为

3.3 在多个对象之间共享事件过滤器

有时,我们可能希望在多个对象之间共享同一个事件过滤器。这可以通过将事件过滤器安装到多个对象上来实现。这种方法的优势在于,我们可以在一个中心位置处理所有相关的事件,而不是分散在多个地方。

但是,这也带来了一个挑战:如何区分事件的来源?答案是使用watched参数,它指示产生事件的对象。

bool CustomEventFilter::eventFilter(QObject *watched, QEvent *event) {
    if (watched == button1 && event->type() == QEvent::MouseButtonPress) {
        // 处理button1的点击事件
    } else if (watched == button2 && event->type() == QEvent::MouseButtonPress) {
        // 处理button2的点击事件
    }
    return QObject::eventFilter(watched, event);
}

在这里,我们使用了watched参数来确定事件的来源,并据此进行相应的处理。

当我们调用installEventFilter方法时,Qt内部会将事件过滤器添加到一个列表中。每当一个事件被分发到对象时,Qt都会首先检查这个对象是否有安装的事件过滤器。如果有,它会按照安装的顺序调用这些事件过滤器。

这种机制确保了事件过滤器总是在事件到达目标对象之前被调用,从而使我们能够在事件到达目标之前进行拦截或处理。

4 高级应用

4.1 使用事件过滤器实现拖放功能

拖放功能(Drag and Drop)是许多应用程序中常见的交互方式。在Qt中,实现这一功能需要对QDrag和QDropEvent有深入的了解。但为什么我们会选择拖放作为交互方式呢?这与人们对直观和简单的追求有关。拖放为用户提供了一种直观的方式来操作对象,而不需要通过复杂的命令或菜单。

从底层源码的角度看,当我们开始拖动一个对象时,QDrag对象会被创建,并开始监听鼠标的移动事件。当对象被放下时,QDropEvent会被触发,我们可以在事件过滤器中捕获这个事件,从而实现自定义的拖放逻辑。

4.2 利用事件过滤器优化性能

在Qt中,事件过滤器可以帮助我们优化性能。例如,我们可以通过事件过滤器拦截并忽略那些不必要的事件,从而减少事件处理的开销。

从底层原理的角度看,每当一个事件被发送到一个对象时,它都会经过事件过滤器。如果事件过滤器决定忽略这个事件,那么这个事件就不会被传递到目标对象,从而节省了处理事件的时间。

优化方法 优点 缺点
事件过滤 减少不必要的事件处理 可能会误拦截重要事件
代码优化 提高代码执行效率 需要深入了解算法和数据结构

事件过滤的工作原理
当我们安装了一个事件过滤器后,每当一个事件被发送到目标对象之前,它都会首先被发送到事件过滤器。事件过滤器可以决定是否继续传递这个事件,或者直接处理并结束这个事件。

这种机制为我们提供了一个在事件到达目标对象之前预处理事件的机会,从而实现更加灵活和高效的事件处理。

5 事件过滤的注意事项

5.1 避免过度过滤

在Qt中,事件过滤器(Event Filter)允许我们拦截并处理特定的事件。但是,过度使用事件过滤器可能会导致代码的复杂性增加,从而降低程序的性能。正如人们在面对信息过载时可能会感到困惑和疲惫,程序也可能因为处理大量不必要的事件而变得缓慢。

事件类型 (Event Type) 处理方式 (Handling Method) 是否推荐 (Recommendation)
鼠标事件 (Mouse Event) 事件过滤器 (Event Filter) 是 (Yes)
键盘事件 (Keyboard Event) 事件过滤器 (Event Filter) 否 (No)
自定义事件 (Custom Event) 事件过滤器 (Event Filter) 是 (Yes)

5.2 确保事件的正确传递

在Qt中,事件从发送者传递到接收者。如果事件没有被处理,它会继续传递给接收者的父对象。这种传递机制确保了事件能够被正确处理。但是,如果我们在事件过滤器中阻止了事件的传递,可能会导致某些功能无法正常工作。这就好像一个人在面对困难时选择逃避,而不是寻求帮助,最终可能会错过解决问题的机会。

在深入Qt的源码时,我们可以发现事件传递的核心逻辑是在QObject::event函数中实现的。这个函数确保了事件能够按照预定的顺序传递给各个对象。

事件传递的原理
当一个事件被发送时,它首先会被传递给目标对象的事件过滤器。如果事件过滤器没有处理这个事件,它会继续传递给目标对象的event函数。如果event函数也没有处理这个事件,它会继续传递给目标对象的父对象,直到事件被处理或传递到顶层对象。

这种事件传递的机制确保了事件能够被正确处理,同时也提供了灵活性,允许我们在不同的层级处理事件。