Qt事件用法详解(附带实例)
事件是对各种应用程序需要知道的由应用程序内部或者外部产生的事情或者动作的通称。
Qt 中使用一个对象来表示一个事件,继承自 QEvent 类,相关类继承关系如下图所示:

图 1 QEvent类关系图
注意,事件与信号并不相同,比如单击一下界面上的按钮,那么就会产生鼠标事件 QMouseEvent(不是按钮产生的),而因为按钮被按下了,所以它会发射 clicked() 单击信号(是按钮产生的)。这里一般只关心按钮的单击信号,而不用考虑鼠标事件,但是如果要设计一个按钮,或者通过鼠标拖拽按钮移动,就要关心鼠标事件了。
可以看到,事件与信号是两个不同层面的东西,发出者不同,作用也不同。在 Qt 中,任何 QObject 子类实例都可以接收和处理事件。
一个事件由一个特定的 QEvent 子类来表示,但是有时一个事件又包含多个事件类型,比如鼠标事件又可以分为鼠标按下、双击和移动等多种操作。这些事件类型都由 QEvent 类的枚举类型 QEvent::Type 来表示,其中包含了 100 多种事件类型,可以在 QEvent 类的帮助文档中进行查看。
虽然 QEvent 的子类可以表示一个事件,但是却不能用来处理事件,那么应该怎样来处理一个事件呢?在 QCoreApplication 类的 notify() 函数的帮助文档处给出了 5 种处理事件的方法。
1) 重新实现部件的 paintEvent()、mousePressEvent() 等事件处理函数。这是最常用的一种方法,不过它只能用来处理特定部件的特定事件。
2) 重新实现 notify() 函数。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。
3) 向 QApplication 对象上安装事件过滤器。因为一个程序只有一个 QApplication 对象,所以这样实现的功能与使用 notify() 函数是相同的,优点是可以同时处理多个事件。
4) 重新实现 event() 函数。QObject 类的 event() 函数可以在事件到达默认的事件处理函数之前获得该事件。
5) 在对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。
在实际编程中,最常用的是方法一,其次是方法五。因为方法二需要继承自 QApplication 类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递。所以,虽然这两种方法功能很强大,但是却很少被用到。
下面通过例子来看一下 Qt 中的事件传递过程。新建 Qt Widgets 应用,项目名称为 myevent,基类选择 QWidget,类名保持 Widget 不变。建立完成后向项目中添加新文件,模板选择 C++ Class,类名为 MyLineEdit,基类手动填写为 QLineEdit。
完成后将 mylineedit.h 文件内容修改如下:
下面我们转到 mylineedit.cpp 文件中,添加头文件:
下面我们进入 widget.h 文件,添加类前置声明:
然后进入 widget.cpp 文件中,添加头文件:
要注意,这里既实现了 MyLineEdit 类的键盘按下事件处理函数,也实现了 Widget 类的键盘按下事件处理函数。另外,因为 event 参数没有使用,这样直接编译程序会出现警告提示(不影响程序的编译运行)。如果在编译程序时不想出现警告信息,可以像这里一样使用 Q_UNUSED() 包含 event 参数。
现在运行程序,这时光标焦点在行编辑器中,随便在键盘上按一个按键,比如按下 A 键,则 Qt Creator 的应用程序输出栏中只会出现“MyLineEdit键盘按下事件”,说明这时只执行了 MyLineEdit 类中的 keyPressEvent() 函数。
下面到 mylineedit.cpp 文件中的 keyPressEvent() 函数最后添加如下一行代码,让它忽略掉这个事件。
但是现在出现了一个问题,就是行编辑器中无法输入任何字符,为了让它可以正常工作,用户还需要在 mylineedit.cpp 文件的 keyPressEvent() 函数中添加一行代码,现在整个函数定义如下:
从这个例子中可以看到,事件是先传递给指定窗口部件的,这里确切地说是先传递给获得焦点的窗口部件。但是如果该部件忽略掉该事件,那么这个事件就会传递给这个部件的父部件。重新实现事件处理函数时,一般要调用父类的相应事件处理函数来实现默认操作。
下面将这个例子再进行改进,看一下使用事件过滤器等其他方法获取事件的顺序。
在 mylineedit.h 文件中添加 public 函数声明:
下面进入 widget.h 文件中进行 public 函数的声明:
在事件过滤器中,先判断该事件的对象是不是 lineEdit,如果是,再判断事件类型。最后返回了 QWidget 类默认的事件过滤器的执行结果。现在可以运行一下程序,然后按下键盘上的任意键,比如这里按下 A 键,查看应用程序输出栏。
可以看到,事件的传递顺序是这样的:先是事件过滤器,然后是焦点部件的 event() 函数,最后是焦点部件的事件处理函数,如果焦点部件忽略了该事件,那么会执行父部件的事件处理函数,如下图所示。

图 2 事件传递顺序示意图
需要注意,event() 函数和事件处理函数是在焦点部件内进行重新定义的,而事件过滤器却是在焦点部件的父部件中进行定义的。
Qt 中使用一个对象来表示一个事件,继承自 QEvent 类,相关类继承关系如下图所示:

图 1 QEvent类关系图
注意,事件与信号并不相同,比如单击一下界面上的按钮,那么就会产生鼠标事件 QMouseEvent(不是按钮产生的),而因为按钮被按下了,所以它会发射 clicked() 单击信号(是按钮产生的)。这里一般只关心按钮的单击信号,而不用考虑鼠标事件,但是如果要设计一个按钮,或者通过鼠标拖拽按钮移动,就要关心鼠标事件了。
可以看到,事件与信号是两个不同层面的东西,发出者不同,作用也不同。在 Qt 中,任何 QObject 子类实例都可以接收和处理事件。
Qt事件的处理
Qt 的主事件循环从事件队列中获取本地窗口系统事件,将它们转换为 QEvent 对象,并将转换后的事件发送到 QObject 对象。一个事件由一个特定的 QEvent 子类来表示,但是有时一个事件又包含多个事件类型,比如鼠标事件又可以分为鼠标按下、双击和移动等多种操作。这些事件类型都由 QEvent 类的枚举类型 QEvent::Type 来表示,其中包含了 100 多种事件类型,可以在 QEvent 类的帮助文档中进行查看。
虽然 QEvent 的子类可以表示一个事件,但是却不能用来处理事件,那么应该怎样来处理一个事件呢?在 QCoreApplication 类的 notify() 函数的帮助文档处给出了 5 种处理事件的方法。
1) 重新实现部件的 paintEvent()、mousePressEvent() 等事件处理函数。这是最常用的一种方法,不过它只能用来处理特定部件的特定事件。
2) 重新实现 notify() 函数。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。
3) 向 QApplication 对象上安装事件过滤器。因为一个程序只有一个 QApplication 对象,所以这样实现的功能与使用 notify() 函数是相同的,优点是可以同时处理多个事件。
4) 重新实现 event() 函数。QObject 类的 event() 函数可以在事件到达默认的事件处理函数之前获得该事件。
5) 在对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。
在实际编程中,最常用的是方法一,其次是方法五。因为方法二需要继承自 QApplication 类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递。所以,虽然这两种方法功能很强大,但是却很少被用到。
Qt事件的传递
我们知道,每个程序 main() 函数的最后都会调用 QApplication 类的 exec() 函数,它会使 Qt 应用程序进入事件循环,这样就可以使应用程序在运行时接收发生的各种事件。一旦有事件发生,Qt 便会构建一个相应的 QEvent 子类的对象来表示它,然后将它传递给相应的 QObject 对象或其子对象。下面通过例子来看一下 Qt 中的事件传递过程。新建 Qt Widgets 应用,项目名称为 myevent,基类选择 QWidget,类名保持 Widget 不变。建立完成后向项目中添加新文件,模板选择 C++ Class,类名为 MyLineEdit,基类手动填写为 QLineEdit。
完成后将 mylineedit.h 文件内容修改如下:
#ifndef MYLINEEDIT_H #define MYLINEEDIT_H #include <QLineEdit> class MyLineEdit : public QLineEdit { Q_OBJECT public: explicit MyLineEdit(QWidget *parent = nullptr); protected: void keyPressEvent(QKeyEvent *event) override; }; #endif // MYLINEEDIT_H这里主要是添加了 keyPressEvent() 函数的声明,建议使用 override 关键字。
下面我们转到 mylineedit.cpp 文件中,添加头文件:
#include <QKeyEvent> #include <QDebug>修改构造函数如下:
MyLineEdit::MyLineEdit(QWidget *parent) : QLineEdit(parent) { }然后添加事件处理函数的定义:
void MyLineEdit::keyPressEvent(QKeyEvent *event) // 键盘按下事件 { qDebug() << tr("MyLineEdit键盘按下事件"); }
下面我们进入 widget.h 文件,添加类前置声明:
class MyLineEdit;然后添加函数声明:
protected: void keyPressEvent(QKeyEvent *event) override;再添加一个 private 对象指针:
MyLineEdit *lineEdit;
然后进入 widget.cpp 文件中,添加头文件:
#include "mylineedit.h" #include <QKeyEvent> #include <QDebug>在 Widget 类的构造函数中添加代码:
lineEdit = new MyLineEdit(this); lineEdit->move(100,100);下面添加事件处理函数的定义:
void Widget::keyPressEvent(QKeyEvent *event) { Q_UNUSED(event); qDebug() << tr("Widget键盘按下事件"); }这里自定义了一个 MyLineEdit 类,它继承自 QLineEdit 类,然后在 Widget 界面中添加了一个 MyLineEdit 部件。
要注意,这里既实现了 MyLineEdit 类的键盘按下事件处理函数,也实现了 Widget 类的键盘按下事件处理函数。另外,因为 event 参数没有使用,这样直接编译程序会出现警告提示(不影响程序的编译运行)。如果在编译程序时不想出现警告信息,可以像这里一样使用 Q_UNUSED() 包含 event 参数。
现在运行程序,这时光标焦点在行编辑器中,随便在键盘上按一个按键,比如按下 A 键,则 Qt Creator 的应用程序输出栏中只会出现“MyLineEdit键盘按下事件”,说明这时只执行了 MyLineEdit 类中的 keyPressEvent() 函数。
下面到 mylineedit.cpp 文件中的 keyPressEvent() 函数最后添加如下一行代码,让它忽略掉这个事件。
event->ignore();再运行程序,按下 A 键,那么在以前输出的基础上又输出了“Widget键盘按下事件”,说明这时也执行了 Widget 类中的 keyPressEvent() 函数。
但是现在出现了一个问题,就是行编辑器中无法输入任何字符,为了让它可以正常工作,用户还需要在 mylineedit.cpp 文件的 keyPressEvent() 函数中添加一行代码,现在整个函数定义如下:
void MyLineEdit::keyPressEvent(QKeyEvent *event) // 键盘按下事件 { qDebug() << tr("MyLineEdit键盘按下事件"); QLineEdit::keyPressEvent(event); // 执行QLineEdit类的默认事件处理 event->ignore(); // 忽略该事件 }这里调用了 MyLineEdit 父类 QLineEdit 的 keyPressEvent() 函数来实现行编辑器的默认操作。这里一定要注意代码的顺序,ignore() 函数要在最后调用。
从这个例子中可以看到,事件是先传递给指定窗口部件的,这里确切地说是先传递给获得焦点的窗口部件。但是如果该部件忽略掉该事件,那么这个事件就会传递给这个部件的父部件。重新实现事件处理函数时,一般要调用父类的相应事件处理函数来实现默认操作。
下面将这个例子再进行改进,看一下使用事件过滤器等其他方法获取事件的顺序。
在 mylineedit.h 文件中添加 public 函数声明:
bool event(QEvent *event) override;然后在 mylineedit.cpp 文件中对该函数进行定义:
bool MyLineEdit::event(QEvent *event) // 事件 { if(event->type() == QEvent::KeyPress) qDebug() << tr("MyLineEdit的event()函数"); return QLineEdit::event(event); // 执行QLineEdit类event()函数的默认操作 }MyLineEdit 的 event() 函数中使用了 QEvent 的 type() 函数来获取事件的类型,如果是键盘按下事件 QEvent::KeyPress,则输出信息。因为 event() 函数具有 bool 型的返回值,所以该函数的最后要使用 return 语句,这里一般是返回父类的 event() 函数的操作结果。
下面进入 widget.h 文件中进行 public 函数的声明:
bool eventFilter(QObject *obj, QEvent *event) override;然后到 widget.cpp 文件中,在构造函数的最后添上一行代码:
lineEdit->installEventFilter(this); // 在Widget上为lineEdit安装事件过滤器添加事件过滤器函数的定义:
bool Widget::eventFilter(QObject *obj, QEvent *event) // 事件过滤器 { if(obj == lineEdit){ // 如果是lineEdit部件上的事件 if(event->type() == QEvent::KeyPress) qDebug() << tr("Widget的事件过滤器"); } return QWidget::eventFilter(obj, event); }
在事件过滤器中,先判断该事件的对象是不是 lineEdit,如果是,再判断事件类型。最后返回了 QWidget 类默认的事件过滤器的执行结果。现在可以运行一下程序,然后按下键盘上的任意键,比如这里按下 A 键,查看应用程序输出栏。
可以看到,事件的传递顺序是这样的:先是事件过滤器,然后是焦点部件的 event() 函数,最后是焦点部件的事件处理函数,如果焦点部件忽略了该事件,那么会执行父部件的事件处理函数,如下图所示。

图 2 事件传递顺序示意图
需要注意,event() 函数和事件处理函数是在焦点部件内进行重新定义的,而事件过滤器却是在焦点部件的父部件中进行定义的。