9. 事件机制

在Qt工程中,main函数中一般会有这么一段程序,最后会执行QApplication类的exec函数,Qt应用程序进入事件循环:

1
2
3
4
5
6
7
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

在exec()中,Qt接受并处理用户和系统的事件并把它们传递给适当的窗口部件。

其中,事件就是从抽象QEvent类中派生的对象,表示发生在应用程序内部或由于应用程序需要了解的外部活动而发生的事情。 例如当用户按下鼠标、敲下键盘就会产生QMouseEvent和QKeyEvent;系统自动发出的QTimerEvent事件等。

Qt事件系统是依托于元对象系统的,所有的QObject类都可以接收/处理 QEvent事件。

9.1. 事件产生和传递(派发)

9.1.1. 事件的产生

在Qt中,事件就是对象,派生自QEvent抽象类,事件的产生,就是创建一个对象。 例如在一个窗口界面,当我们鼠标点击、双击、移动等,就会产生一个QMouseEvent事件对象。

事件的来源一般分为三个,来自窗口系统,例如鼠标键盘产生的QMouseEvent和QKeyEvent;来自其他程序,比如QTimerEvent;来自Qt程序本身,一些自定义的事件等。

9.1.2. 事件类和事件类型

Qt中所有事件类都是继承于QEvent,下面是我们常见的用户输入类事件继承关系(部分):

enent

事件类都有个唯一身份值:type值,也就是事件类型。需要注意有些事件类可能对应于多个事件类型,例如QMouseEvent类对应于鼠标按压事件,鼠标双击事件,鼠标移动事件等。 下面列出了一些Qt自带的 事件类型

事件类型

描述

QEvent::None

0

无事件产生

QEvent::Close

19

窗口小部件已关闭(QCloseEvent)

QEvent::KeyPress

6

按键(QKeyEvent)

QEvent::MouseButtonPress

2

按下鼠标(QMouseEvent)

QEvent::Paint

12

屏幕更新是必需的(QPaintEvent)

QEvent::Timer

1

常规计时器事件(QTimerEvent)

QEvent::User

1000

用户定义的事件

QEvent::MaxUser

65535

上次用户事件ID

Qt自带的事件类的type值都已经在 QEvent::type中有了,数值范围在0 - 999之间,我们自定义的事件值不能和这些冲突, Qt规定了两个边界值:QEvent::User 和 QEvent::MaxUser,即 1000 - 65535,自定义的事件值在这中间选择。 为了避免用户自定义的事件值也产生冲突,Qt提供一个函数用来注册自定义事件:QEvent::registerEventType()。

Qt还提供了QEvent::registerEventType()函数,专门用于自定义事件的注册,这样做可以避免意外自定义事件类型的冲突。 下面程序自定义一个事件类:

lubancat_qt_tutorial_code/Event/myevent.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#ifndef MYEVENT_H
#define MYEVENT_H

#include <QEvent>

class myevent : public QEvent
{
    Q_OBJECT
public:
    myevent();

    static Type myEventype();

private:
    static Type myType;   //自定义的事件类型
};

#endif // MYEVENT_H
lubancat_qt_tutorial_code/Event/myevent.cpp
1
2
3
4
5
6
7
QEvent::Type myevent::myEventype()
{
    if (myType == QEvent::None)
        myType = (QEvent::Type)QEvent::registerEventType();  //使用registerEventType函数注册自定义事件类型

    return myType;
}

9.1.3. 事件派发

当事件发生时,Qt通过构造适当的QEvent子类的实例来表示一个事件,并通过调用其event()函数将其传递到QObject的特定实例(或其子类之一)。 event()函数不处理事件本身,而是根据所传递事件的类型,调用对应的事件处理程序,并根据该事件被接受还是忽略,发送响应。

许多应用程序都希望创建并发送自定义的事件,Qt提供了一些事件发送函数:

static bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event);

static bool QCoreApplication::postEvent(QObject *receiver, QEvent *event);
  • sendEvent() 直接将event事件发送给receiver接受者,立即处理事件。返回值是事件过滤器和/或对象本身已经处理了该事件。对于许多事件类,有一个称为isAccepted()的函数,该函数告诉该事件是被最后一个调用的处理程序接受还是拒绝。

  • postEvent() 将接受者receiver和事件event发布到队列中,以供以后分派。下次Qt的主事件循环运行时,它将分派所有已发布的事件,并进行一些优化。例如,如果有多个调整大小事件,它们将被视为为一个。这同样适用于绘画事件中,如QWidget :: update()调用postEvent(),通过避免多次重新绘画来消除闪烁并提高速度。

另外,还有一个函数是:

static void QCoreApplication::sendPostedEvents(QObject *receiver, int event_type);

该函数将事件队列中的接受者为receiver,事件类似为event_type的所有事件立即发送给receiver进行处理。 需要注意的是,来自窗口系统的事件并不由这个函数进行处理,而是processEvent()。

下面是自定义事件类的发送例程:

lubancat_qt_tutorial_code/Event/widget.cpp
1
2
3
4
5
6
void MainWindow::handletimeout()
{
    myevent *mypostEvent = new myevent();
    //使用postevent方式发送,事件添加到事件队列,此事件分配给this来处理,,系统会自动释放的。
    QCoreApplication::postEvent(this, mypostEvent);
}

9.2. 事件处理

一个类接收到应用程序派发来的事件后,调用其event()函数处理,该函数是QObject类中定义的一个虚函数。任何从QObject派生的类都可以重新实现该函数。 在event()函数中我们可以设置是否接收事件,QEvent类中有accept()函数,表示接收事件并对事件进行处理,ignore()函数,表示不接收该事件。 不接受的事件将会传播到接受者的父容器组件中,由父容器的event()函数去处理。

QWidget类是所有窗口类的基类,它重新实现了event()函数,并对一些典型的事件定义了专门处理函数,event()函数会根据事件的类型自动去运行相应的事件处理函数, 这些处理函数是 protected virtual, 不能被外部调用,但可以被派生类重新实现。

QWidget中常用的事件处理函数:

1
2
3
4
5
6
7
8
9
void mousePressEvent(QMouseEvent *event);   // 鼠标按下,对应事件类型QEvent::MouseButtonPress
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void mouseDoubleClickEvent(QMouseEvent *event);
void wheelEvent(QWheelEvent *event);
void keyPressEvent(QKeyEvent *event);
void closeEvent(QCloseEvent *event);
void resizeEvent(QResizeEvent *event);
void paintEvent(QPaintEvent *event);

每个事件都有一个关联的类型,在QEvent::Type中定义了相关类型,并且可以用作运行时类型信息的便捷来源,以快速确定构造给定事件对象的子类。

例程中重写鼠标相关的事件:

lubancat_qt_tutorial_code/Event/wedget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 重写鼠标按下事件处理函数
void Widget::mousePressEvent(QMouseEvent *event)
{
    QString mesg;

    if(event->button() == Qt::LeftButton)
        mesg=QString("鼠标左键:(%1,%2)").arg(event->x()).arg(event->y());
    else if(event->button() == Qt::RightButton)
        mesg=QString("鼠标右键:(%1,%2)").arg(event->x()).arg(event->y());
    else if(event->button() == Qt::MiddleButton)
        mesg=QString("鼠标滚轮:(%1,%2)").arg(event->x()).arg(event->y());

    ui->textEdit->append(QString("[%1] %2").arg(QDateTime::currentDateTime().toString("hh:mm:ss")).arg(mesg));
}

通过event->button()获取键值判断,更多键值参考下:https://doc.qt.io/qt-5/qt.html#Key-enum

9.3. 事件过滤器

事件会被派发给接受者,接受者通过event函数处理。我们可以重写event函数和对应的处理函数,例如:

lubancat_qt_tutorial_code/Event/Widget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
bool Widget::event(QEvent *e)
{
    if (e->type() == QEvent::KeyPress) {                       //通过事件值判断事件类型
        QKeyEvent *ke = static_cast<QKeyEvent *>(event);
        if (ke->key() == Qt::Key_Tab) {
            QString mesg=QString("tab按键:(eventype,%1)").arg(event->type());
            ui->textEdit->append(QString("[%1] %2").arg(QDateTime::currentDateTime().toString("hh:mm:ss")).arg(mesg));
            return true;
        }
    }

    if (e->type() == myevent::myEventype())
    {
        QString mesg=QString("自定义事件");
        ui->textEdit->append(QString("[%1] %2").arg(QDateTime::currentDateTime().toString("hh:mm:ss")).arg(mesg));
        return true;
    }

    return QWidget::event(e); //其他类型事件,执行父类的event()
}

而在Qt还设计了eventFilter()函数,可以将一个对象的事件委托给另一个对象监视和处理,实现更灵活的事件过滤。

QObject :: installEventFilter()函数将自己注册给监视对象,监视对象会重新实现QObject :: eventFilter()函数,对监视的事件进行处理。

函数原型定义
1
2
void QObject::installEventFilter(QObject *filterObj)
bool QObject::eventFilter(QObject *watched, QEvent *event)

事件过滤器可以在目标对象之前处理事件,从而使它可以根据需要检查和丢弃事件,可以使用QObject :: removeEventFilter()函数删除现有的事件过滤器。

调用过滤器对象的eventFilter()实现时,它可以接受或拒绝事件,并允许或拒绝事件的进一步处理,如果所有事件过滤器都允许进一步处理事件(每次返回false), 则事件将发送到目标对象本身。如果其中一个停止处理(返回true),则目标过滤器和任何以后的事件过滤器将根本看不到该事件。一般我们

通过在QApplication或QCoreApplication对象上安装事件过滤器,还可以过滤整个应用程序的所有事件。此类全局事件过滤器在特定于对象的过滤器之前被调用。 这是非常强大的功能,但是它也会减慢整个应用程序中每个事件的事件传递速度。

例程中过滤按键事件并处理:

lubancat_qt_tutorial_code/Event/Widget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
bool Widget::eventFilter(QObject *object, QEvent *event)
{
    if (object == this && event->type() == QEvent::KeyPress) // 判断是该事件的对象是不是Widget,并且要是QEvent::KeyPress
    {
        QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
        if (keyEvent->key() == Qt::Key_Space) {
            QString mesg=QString("空格按键");
            ui->textEdit->append(QString("[%1] %2").arg(QDateTime::currentDateTime().toString("hh:mm:ss")).arg(mesg));
            return true;
        } else
            return false;
    }
    return QWidget::eventFilter(object,event);     //返回父类的eventFilter()函数执行结果
}

过滤按键事件,如果是按下空格键,就往界面输出信息,自行处理事件。 其他事件通过返回QWidget::eventFilter(object,event),交给父类的eventFilter()函数处理。

9.4. 测试例程

例程演示了Qt中常用的事件以及事件处理(重写事件处理函数和事件的过滤等),自定义事件并处理。

  • 新建一个工程,基于widget

  • 新建一个自定义事件类

  • 重写窗口的常见事件

  • 对窗口安装事件过滤器,捕捉控件按键按下事件

详细参考下配套例程。

9.4.1. 编译构建

  • Ubuntu 上测试选择 Desktop Qt 5.15.2 GCC 64bit 套件,编译运行;

  • LubanCat板卡上运行选择 LubanCat_RK,只编译生成可执行文件。

build001

9.4.2. 运行结果

9.4.2.1. PC实验

直接点击编译并运行程序,效果如下:

result002

9.4.2.2. LubanCat实验

拷贝可执行程序到板卡,可以通过SCP、NFS、sftp、u盘等。 NFS使用 参考这里

SCP命令如下:

# scp传输文件,ip地址根据具体板子和PC,板子和PC在一个局域网下
# scp 文件名 服务器上的某个用户@服务器ip:/文件保存路径
scp filename server_user_name@192.168.103.102:server_file_path
# 从服务器拉取文件
# scp 服务器上的某个用户@服务器ip:/服务器文件存放路径 拉取文件保存路径
scp server_user_name@192.168.103.102:server_file_path local_path

编译好的程序在 lubancat_qt_tutorial_code/app_bin 目录中,通过scp命令将编译好的程序拉到LubanCat。

# 可执行程序文件的目录,根据自己实际项目设置的目录
scp root@192.168.103.102:~/lubancat_qt_tutorial_code/Event ~/home/cat/

在LubanCat板卡上运行程序,执行Event。

# 设置环境变量,然后执行程序
sudo  ~/home/cat/Event
result002