事件
介绍
事件(event)是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
事件也就是我们通常说的“事件驱动(event drive)”程序设计的基础概念。事件的出现,使得程序代码不会按照原始的线性顺序执行。
Qt 中的事件和信号槽却并不是可以相互替代的。信号由具体的对象发出,然后会马上交给由 connect()函数连接的槽进行处理;而对于事件,Qt 使用一个事件队列对所有发出的事件进行维护,当新的事件产生时,会被追加到事件队列的尾部。前一个事件完成后,取出后面的事件进行处理。但是,必要的时候,Qt 的事件也可以不进入事件队列,而是直接处理。信号一旦发出,对应的槽函数一定会被执行。但是,事件则可以使用“事件过滤器”进行过滤,对于有些事件进行额外的处理,另外的事件则不关心。总的来说,如果我们使用组件,我们关心的是信号槽;如果我们自定义组件,我们关心的是事件。因为我们可以通过事件来改变组件的默认操作。比如,如果我们要自定义一个能够响应鼠标事件的 EventLabel,我们就需要重写 QLabel 的鼠标事件,做出我们希望的操作,有可能还得在恰当的时候发出
一个类似按钮的 clicked()信号(如果我们希望让这个 EventLabel 能够被其它组件使用)或者其它的信号。
Qt 程序需要在 main()函数创建一个 QCoreApplication对象,然后调用它的 exec()函数。这个函数就是开始 Qt 的事件循环。在执行 exec()函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于 QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给 QObject的 event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler)。
在所有组件的父类 QWidget 中,定义了很多事件处理的回调函数,如 keyPressEvent()、keyReleaseEvent()、mouseDoubleClickEvent()、mouseMoveEvent()、mousePressEvent()、mouseReleaseEvent()等。这些函数都是 protected virtual 的,也就是说,我们可以在子类中重新实现这些函数。
示例
QLabel 支持 HTML 代码
要点击鼠标之后才能在 mouseMoveEvent()函数中显示鼠标坐标值
// main.cpp
#include "eventlabel.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// MainWindow w;
// w.show();
EventLabel *label = new EventLabel();
label->setWindowTitle("location");
label->setFixedSize(600, 400);
label->show();
return a.exec();
}
// eventlabel.cpp
#include "eventlabel.h"
#include <QMouseEvent>
EventLabel::EventLabel()
{
}
void EventLabel::mouseMoveEvent(QMouseEvent *event) {
this->setText(QString("<center><h1>Move: (%1, %2)<h1></center>").arg(QString::number(event->x()), QString::number(event->y())));
}
void EventLabel::mousePressEvent(QMouseEvent *event) {
this->setText(QString("<center><h1>Press: (%1, %2)</h1></center>").arg(QString::number(event->x()), QString::number(event->y())));
}
void EventLabel::mouseReleaseEvent(QMouseEvent *event) {
this->setText(QString("<center><h1>Release: (%1, %2)</h1></center>").arg(QString::number(event->x()), QString::number(event->y())));
}
QWidget 中有一个 mouseTracking 属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。如果 mouseTracking 是 false(默认即是),组件在至少一次鼠标点击之后,才能够被追踪,也就是能够发出 mouseMoveEvent()事件。
在main()中设置,label->setMouseTracking(true);
,不用点击鼠标即可显示。
事件的接受与忽略
// custombutton.cpp
#include "custombutton.h"
CustomButton::CustomButton(QWidget *parent) : QPushButton(parent)
{
this->setMouseTracking(true);
connect(this, &CustomButton::clicked, this, &CustomButton::onButtonClicked);
}
void CustomButton::onButtonClicked() {
setText(tr("clicked me"));
}
void CustomButton::mousePressEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) {
this->setText(tr("left"));
// } else if (event->button() == Qt::RightButton) {
// this->setText(tr("right"));
} else if (event->button() == Qt::MidButton) {
this->setText(tr("middle"));
} else {
QPushButton::mousePressEvent(event);
}
}
// main.cpp
#include "custombutton.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
CustomButton btn;
btn.setText("Button");
btn.show();
return a.exec();
}
我们重写了 CustomButton 的 mousePressEvent()函数,也就是鼠标按下。在这个函数中,我们判断如果鼠标按下的是左键,则打印出来“left”字符串,否则,调用父类的同名函数。
编译运行这段代码,当我们点击按钮时,“You clicked this!”字符串不再出现,只有一个“left”。也就是说,我们把父类的实现覆盖掉了。由此可以看出,父类 QPushButton 的mousePressEvent()函数中肯定发出了 clicked()信号。当重写事件回调函数时,时刻注意是否需要通过调用父类的同名函数来确保原有实现仍能进行!比如我们的 CustomButton 了,如果像我们这么覆盖函数,clicked()信号永远不会发生,你连接到这个信号的槽函数也就永远不会被执行。这个错误非常隐蔽,很可能会浪费你很多时间才能找到。因为这个错误不会有任何提示。这一定程度上说,我们的组件“忽略”了父类的事件,但这更多的是一种违心之举,一种错误。
通过调用父类的同名函数,我们可以把 Qt 的事件传递看成链状:如果子类没有处理这个事件,就会继续向其父类传递。Qt 的事件对象有两个函数:accept()和 ignore()。正如它们的名字一样,前者用来告诉 Qt,这个类的事件处理函数想要处理这个事件;后者则告诉 Qt,这个类的事件处理函数不想要处理这个事件。在事件处理函数中,可以使用 isAccepted()来查询这个事件是不是已经被接收了。具体来说:如果一个事件处理函数调用了一个事件对象的 accept()函数,这个事件就不会被继续传播给其父组件;如果它调用了事件的 ignore()函数,Qt 会从其父组件中寻找另外的接受者。
事实上,我们很少会使用 accept()和 ignore()函数,而是像上面的示例一样,如果希望忽略事件(所谓忽略,是指自己不想要这个事件),只要调用父类的响应函数即可。
如果我们在子类中直接忽略事件,Qt 会去寻找其他的接收者,该子类的父类的操作会被忽略(因为没有调用父类的同名函数),这可能会有潜在的危险。为了避免自己去调用 accept()和 ignore()函数,而是尽量调用父类实现,Qt 做了特殊的设计:事件对象默认是 accept 的,而作为所有组件的父类 QWidget的默认实现则是调用 ignore()。
事件的传播是在组件层次上面的,而不是依靠类继承机制。当子组件忽略事件(调用ignore())时,事件将被父组件接收。
如果得到事件的对象,调用了accept(),则事件停止继续传播;如果调用了ignore(),事件向上一级继续传播。但closeEvent是个特殊情形,accept表示quit,ignore表示取消,所以最好在closeEvent显式调用accept和ignore。
event()函数
事件对象创建完毕后,Qt 将这个事件对象传递给QObject 的 event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。即event()函数主要用于事件的分发。
监听 tab 键:
bool CustomWidget::event(QEvent *e) {
if (e->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
if (keyEvent->key() == Qt::Key_Tab) {
qDebug() << "You press tab.";
return true;
}
}
return QWidget::event(e);
}
CustomWidget 是一个普通的 QWidget 子类。如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,并且,该事件对象设置了 accept(),那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。注意,在 event()函数中,调用事件对象的 accept()和 ignore()函数是没有作用的,不会影响到事件的传播。
event()函数是事件分发器,将不同的事件根据类型分发到相应的事件处理器中。
事件过滤器
Qt 创建了 QEvent 事件对象之后,会调用 QObject 的event()函数处理事件的分发。可以使用重写event()函数中实现拦截的操作,如果组件很多,就需要重写很多个 event()。Qt 提供了另外一种机制来达到这一目的:事件过滤器。
eventFilter()
QObject 有一个 eventFilter()函数,用于建立事件过滤器。
virtual bool QObject::eventFilter(QObject *watched, QEvent *event);
-
返回值: true,不转发;
false,转发;
-
watched: 目标对象
-
事件过滤器的调用时间是目标对象(也就是参数里面的 watched 对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched 对象以及以后所有的事件过滤器根本不会知道这么一个事件。
屏蔽所有键盘输入:
#include "mainwindow.h"
#include <QEvent>
#include <QKeyEvent>
#include <QMessageBox>
#include <QString>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
textEdit = new QTextEdit;
setCentralWidget(textEdit);
textEdit->installEventFilter(this);
}
MainWindow::~MainWindow()
{
}
bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
if (obj == textEdit) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
QMessageBox::information(this, "key press", keyEvent->text());
return true;
} else {
return false;
}
} else {
return QMainWindow::eventFilter(obj, event);
}
}
installEventFilter()
eventFilter()函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用 QObject::installEventFilter()函数。
void QObject::installEventFilter(QObject *filterObj);
已经存在的过滤器则可以通过 QObject::removeEventFilter()函数移除。
一个对象上面安装多个事件处理器,只要调用多次 installEventFilter()函数。如果一个对象存在多个事件过滤器,那么,最后一个安装的会第一个执行,也就是后进先执行的顺序。
事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
总结
Qt 中有很多种事件:鼠标事件、键盘事件、大小改变的事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:
-
所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择,其代表作就是 win32 API 的 WndProc()函数
-
每一种事件对应一个事件处理函数。Qt 就是使用的这么一种机制:
mouseEvent()
、keyPressEvent()
...event()
函数负责分发事件
Qt事件处理的五个层次:
- 重写 paintEvent()、mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单;
- 重写 event()函数。event()函数是所有对象的事件入口,QObject 和 QWidget 中的实现,默认是把事件传递给特定的事件处理函数;
- 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件;
- 在 QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和 notify()函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程;
- 重写 QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication 是单例的)
class Label : public QWidget
{
public:
Label()
{
installEventFilter(this);
}
// eventFilter
bool eventFilter(QObject *watched, QEvent *event)
{
if (watched == this) {
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "eventFilter";
}
}
return false;
}
protected:
// mousePressEvent
void mousePressEvent(QMouseEvent *)
{
qDebug() << "mousePressEvent";
}
// event
bool event(QEvent *e)
{
if (e->type() == QEvent::MouseButtonPress) {
qDebug() << "event";
}
return QWidget::event(e);
}
};
class EventFilter : public QObject
{
public:
EventFilter(QObject *watched, QObject *parent = 0) :
QObject(parent),m_watched(watched)
{
}
// eventFilter
bool eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_watched) {
if (event->type() == QEvent::MouseButtonPress) {
qDebug() << "QApplication::eventFilter";
}
}
return false;
}
private:
QObject *m_watched;
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
Label label;
app.installEventFilter(new EventFilter(&label, &label));
label.show();
return app.exec();
}
自定义事件
Qt 自定义事件很简单,同其它类库的使用很相似,都是要继承一个类进行扩展。在 Qt 中,你需要继承的类是 QEvent。继承 QEvent 类,最重要的是提供一个 QEvent::Type 类型的参数,作为自定义事件的类型值。QEvent::Type 是 QEvent 定义的一个枚举。因此,我们可以传递一个 int 值。但是需要注意的是,我们的自定义事件类型不能和已经存在的 type 值重复,否则会有不可预料的错误发生。
1. 事件注册
Qt 提供了一个函数:registerEventType(),用于自定义事件的注册。这个函数是线程安全的。
static int QEvent::registerEventType ( int hint = -1 );
2. 事件发送
Qt 中提供了两种事件发送方式:
-
直接将 event 事件发送给 receiver 接受者。通常我们会在栈上创建 event 对象
static bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event);
-
.将 event 事件及其接受者 receiver 一同追加到事件队列中,函数立即返回。
static void QCoreApplication::postEvent(QObject *receiver, QEvent *event);
post 事件队列会持有事件对象,并且在其 post 的时候将其 delete 掉,因此,我们必须在堆上创建 event 对象。当对象被发送之后,再试图访问 event 对象就会出现问题(因为 post 之后,event 对象就会被 delete)。
事件会根据 post 的顺序进行处理。如果你想要改变事件的处理顺序,可以考虑为其指定一个优先级。默认的优先级是 Qt::NormalEventPriority。这个函数是线程安全的。
static void QCoreApplication::sendPostedEvents(QObject *receiver, int event_type);
这个函数的作用是,将事件队列中的接受者为 receiver,事件类似为 event_type 的所有事件立即发送给 receiver 进行处理。
3. 事件处理
处理自定义事件,同前面那些处理方法没有什么区别。我们可以重写QObject::customEvent()函数,该函数接收一个 QEvent 对象作为参数:
void QObject::customEvent(QEvent *event);