事件接收与忽略
来源 https://blog.51cto.com/devbean/225519
本章内容也是关于Qt事件。或许这一章不能有一个完整的例子,因为对于事件总是感觉很抽象,还是从底层上理解一下比较好的吧!
前面说到了事件的作用,下面来看看我们如何来接收事件。回忆一下前面的代码,我们在子类中重写了事件函数,以便让这些子类按照我们的需要完成某些功能,就像下面的代码:
{
if(event->button() == Qt::LeftButton) {
// do something
} else {
QLabel::mousePressEvent(event);
}
}
上面的代码和前面类似,在鼠标按下的事件中检测,如果按下的是左键,做我们的处理工作,如果不是左键,则调用父类的函数。这在某种程度上说,是把事件向上传递给父类去响应,也就是说,我们在子类中“忽略”了这个事件。
我们可以把Qt的事件传递看成链状:如果子类没有处理这个事件,就会继续向其他类传递。其实,Qt的事件对象都有一个accept()函数和ignore()函数。正如它们的名字,前者用来告诉Qt,事件处理函数“接收”了这个事件,不要再传递;后者则告诉Qt,事件处理函数“忽略”了这个事件,需要继续传递,寻找另外的接受者。在事件处理函数中,可以使用isAccepted()来查询这个事件是不是已经被接收了。
事实上,我们很少使用accept()和ignore()函数,而是想上面的示例一样,如果希望忽略事件,只要调用父类的响应函数即可。记得我们曾经说过,Qt中的事件大部分是protected的,因此,重写的函数必定存在着其父类中的响应函数,这个方法是可行的。为什么要这么做呢?因为我们无法确认父类中的这个处理函数没有操作,如果我们在子类中直接忽略事件,Qt不会再去寻找其他的接受者,那么父类的操作也就不能进行,这可能会有潜在的危险。另外我们查看一下QWidget的mousePressEvent()函数的实现:
{
event->ignore();
if ((windowType() == Qt::Popup)) {
event->accept();
QWidget* w;
while ((w = qApp->activePopupWidget()) && w != this){
w->close();
if (qApp->activePopupWidget() == w) // widget does not want to dissappear
w->hide(); // hide at least
}
if (!rect().contains(event->pos())){
close();
}
}
}
请注意第一条语句,如果所有子类都没有覆盖mousePressEvent函数,这个事件会在这里被忽略掉,这暗示着这个组件不关心这个事件,这个事件就可能被传递给其父组件。
不过,事情也不是绝对的。在一个情形下,我们必须使用accept()和ignore()函数,那就是在窗口关闭的时候。如果你在窗口关闭时需要有个询问对话框,那么就需要这么去写:
{
if(continueToClose()) {
event->accept();
} else {
event->ignore();
}
}
bool MainWindow::continueToClose()
{
if(QMessageBox::question(this,
tr("Quit"),
tr("Are you sure to quit this application?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No)
== QMessageBox::Yes) {
return true;
} else {
return false;
}
}
这样,我们经过询问之后才能正常退出程序。
----------------------------
Qt 中的事件系统
来源 https://zhuanlan.zhihu.com/p/50053079 编辑于 2018-11-16
本文结构如下:
概述
首先要明白的是:“在 Qt 里,一个事件就是一个对象,所有事件的祖先都来自于 QEvent”。意思就是说,只要有一个事件发生(如鼠标单击事件),此时就会有一个 QEvent 对象被创建出来,然后开始各种传送。由于 Qt 事件系统是依托于元对象系统的,所以所有的 QObject 类都可以接收/处理 QEvent 事件。
说起事件,其实无非就是围绕着“产生-发送-处理”这个基本流程来说的。
如何产生一个事件?
这是最简单的知识点了,当然是创建一个对象啦。这问题和“如何创建一个整数”一样简单。我创建一个整数是“int i”,那我创建一个事件就是“MyEvent event(1, 2, 3)”啦。其中 MyEvent 是我自定义的一个事件类,123是我往这个事件传入的参数。当你不知道怎么创建一个事件对象时,更多的是不知道一个事件对象长啥样。所以我们先看下事件是什么样的一个类。
事件类长什么样?
这是我自定义的一个事件类:
class MyEvent : public QEvent
{
public:
MyEvent();
MyEvent(int x, int y, int z);
static const Type type;
int x;
int y;
int z;
};
别忘了本文最开始的那句话“一个事件就是一个类对象”。你看我写的这个事件类,它里面可以存储三个 int 整数。假如我程序中触发了某某槽函数,产生了一个三维坐标事件,要把坐标发送出去,那我上面写的这个事件类就派上用场了。我创建一个事件对象,并把坐标参数传到这个事件对象里(MyEvent event(1, 2, 3)),这样这个 event 对象就产生了,它承载着一个三维坐标。
Qt 自带的事件类功能更多
Qt 自带的事件类有很多很多,有些事件类还附带了简单的函数,例如 QResizeEvent 这个事件类就有 size() 和 oldSize() 函数;有些事件类还支持多种实际操作,比如 QMouseEvent 支持鼠标单击、双击、移动等,也就是说当鼠标进行这些操作的时候,都会产生一个 QMouseEvent 事件对象。我上面写的那个例子是很简单的,只能存储 xyz 三个整数,我也可以定义多点变量或者函数。
每一个事件类都有个唯一身份值:type值
这个 type 值的作用就是我们在处理事件时用于识别事件类的代号。请看下面实际中的例子:
bool FilterObject::eventFilter(QObject *object, QEvent *event)
{
if (object == target && event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Tab) {
......
return true;
} else {
return false;
}
}
return false;
}
- 不能和系统值冲突
我们在官方文档可以看到,Qt 自带的事件类的 type 值都已经在 QEvent::type 中有了,数值范围在 0 - 999 之间。
- 自定义的不能冲突
而我自己定义的事件类也有个 type 值,如上文的代码“static const Type type”。为了保证我的这个值不和 Qt 的冲突,所以数值要大于 999。Qt 给我们规定了两个边界值:QEvent::User 和 QEvent::MaxUser,即 1000 - 65535。
但这也太难记了,怎么办?Qt 提供了一个函数 registerEventType() 专门用于自定义事件的注册。如下:
const QEvent::Type MyEvent::type = (QEvent::Type)QEvent::registerEventType()
好了,现在我们已经认识了 Qt 事件类长什么样了。接下来就是怎么把它发送出去了。
如何发送一个事件?
Qt 提供了三个 static 事件发送函数:sendEvent、postEvent、sendPostEvents。函数原型如下:
bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event)
void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority = Qt::NormalEventPriority)
void QCoreApplication::sendPostedEvents(QObject *receiver = Q_NULLPTR, int event_type = 0)
直接发送:sendEvent
这是最好理解的,两个参数中一个是要发给谁,另一个是发送什么事件。这种方式的发送我们要等待对方处理,所以返回值是一个 bool 量来判断。对于许多事件类,有一个名为 isAccepted() 来告诉你是否被处理。因为事件被发送的时候,event 对象并不会被销毁,因此我们要在栈上创建 event 对象。
举个例子比较好理解:
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
MainWindow w;
w.show();
MyEvent event(1, 2, 3);
QCoreApplication::sendEvent(&w, &event);
return a.exec();
};
发到队列:postEvent
我们创建一个 Qt 程序的时候,一般在 main 下面会看到 QCoreApplication a(argc, argv) 以及 return a.exec() 的字样。这其实就是开启了 Qt 事件循环来维护一个事件队列,exec 的本质就是不停的调用 processEvent() 函数从队列中获取事件来处理。而 postEvent() 的作用就是把事件发送到这个队列中去。
这种方式不需要等待处理结果,只要把事件发到队列中就可以了,所以返回值是 void。由于事件队列会持有发送的事件对象,在 post 的时候会将事件 delete 掉,所以我们必须在堆上创建 event 对象。
在队列中立即处理:sendPostedEvents
看参数我们就可以知道,这个函数的作用就是立刻、马上将队列中的 event_type 类型的事件立马交给 receiver 进行处理。需要注意的是,来自窗口系统的事件并不由这个函数进行处理,而是 processEvent()。
如何处理一个事件?
创建了事件,发送了事件,接下来就是怎么接收处理事件了。
发送的事件在哪里捕获?
我们发送的事件传给了 QObject 对象,更具体点是传给了 QObject 对象的 event() 函数。根据官方文档的说明,所有的事件都会进到这个函数里面,那么我们处理事件就要重写这个 event() 函数。
在 event() 中怎么处理事件?
event() 函数本身不会去处理事件,而是根据事件类型(type值)调用不同的事件处理器函数。大家都是这样的一个流程,所以我们也尽量保持统一。请看下图,其中的 timerEvent()、childEvent() 都是处理事件的函数。
再举个简单的例子:鼠标点击一下 -> 产生鼠标点击事件 -> 接收者的 event() 函数判断类型 -> 发给 mousePressEvent() 函数处理。写成代码就是这样的:
void MyCheckBox :: mousePressEvent( QMouseEvent *event )
{
if(event->button() == Qt::LeftButton)
{
....//点击左键后实现的功能
} else {
QCheckBox::mousePressEvent(event);
}
}
需要注意的是,重写事件处理器函数时,如果不实现任何功能,最好调用基类的实现。就像上面的那段代码,Qt 本身就已经写了一大堆的实现了,你要是不写上 QCheckBox::mousePressEvent(event) 这个代码,那你写的这个继承于 QCheckBox 类的 MyCheckBox 就不会对鼠标点击产生任何反应。正所谓“你要不会干这事,叫你爸爸来做吧”。
至此,一个完整的事件处理过程已经说完了。此时的你应该不仅了解了 Qt 自带的类是如何处理事件的,而且写个自定义事件也是应该是能下手了。接下来我们对事件处理再说说其他方便的功能:过滤、接收/忽略。
技巧一:接收/忽略事件
所有的事件的祖先都是 QEvent 类,而该类里有个 isAccepted() 函数来判断这个事件是否被接受了。看到这里我们就能推测出 QEvent 类身上有一个私有变量,或者称为标签吧,用来指示 QEvent 是不是被人处理过了。我一个小小的事件处理没处理有啥关系?很有关系!顺序是这样的:当收到一个事件对象时,我先拿到这个事件。如果处理它,那么事情也就到此为止了;如果不去处理,它会传到我爸爸(基类)那儿去;他如果也不处理,就继续传给我的爷爷。
事实上,我们很少会用到 accept()、ignore() 函数的,如果忽略事件(即自己不想去处理这个事件),只要调用父类的响应函数就可以了。
一个特殊的例子:QWidget
Qt 中大部分的事件处理函数都是 accept() 接收的事件对象,而 QWidget 却是 ignore() 的。这样的话如果某个 QWidget 子类想忽略某个事件,既可以调用 ignore() 函数,也可以调用基类 QWidget 的默认实现。另外还有一点需要知道,在图形编辑中,如果指定了父类(注意是父类,不是基类),事件的传播是在组件层面的,而不是靠类继承机制。
用一个小例子加深事件忽略/接收
看代码:
class A : public QPushButton
{
void mousePressEvent(QMouseEvent *e)
{ qDebug() << "A";}
};
class B : public A
{
void mousePressEvent(QMouseEvent *e)
{ qDebug() << "B";}
};
class MyWidget : public QWidget
{
void mousePressEvent(QMouseEvent *e)
{ qDebug() << "MyWidget"; }
};
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent)
{
MyWidget *myWidget = new MyWidget();
myWidget->setParent(this);
A *a = new A();
a->setParent(myWidget);
a->setText("A_Button");
B *b = new B();
b->setParent(myWidget);
b->setText("B_Button");
}
void MainWindow::mousePressEvent(QMouseEvent *e)
{
qDebug() << "MainWindow";
}
实验1:点击 B 按钮,运行结果是“B”。因为我们重写了鼠标按下事件处理函数,并没有调用基类函数;
实验2:将 B 的 mousePressEvent() 第一行添加“event->accept()”。运行结果不变,正如上文所述 QEvent 默认是 accept 的,调用这个函数并没有什么区别。
实验3:将 B 的 mousePressEvent() 改成 event->ignore()。运行结果是“B MyWidget”。ignore 是作用是让事件继续传播,因此父组件 MyWidget 也收到了这个事件。而 MyWidget 没有调用基类的事件处理函数,所以事件传播到此就结束了。
必须调用 accept()、ignore() 函数的场景
就是窗口关闭事件!对于 QCloseEvent 来说,接收意味着事件到此结束不再传播,窗口关闭;忽略意味着事件继续传播,窗口仍然开着。
技巧二:过滤事件
其实上面已经就可以对事件搞点事情了,在 event() 函数中,调用处理函数前可以做些事儿。Qt 还设计了 eventFilter() 函数,所以总的来说过滤事件有两种方法,后者更为灵活。
事件接收者是 QObject 类型的,而 QObject 类有个 eventFilter() 函数,有什么用呢?比如在主界面上有一个按钮叫“A”,有一个按钮叫“B”。当 B->installEventFilter(MainWindow) 的时候,B 产生的事件就不会进入接收者的 event() 函数中了,而是进入接收者的 eventFilter() 函数中。如下图。
注意:如果你在事件过滤器中 delete 了某个接收组件,务必将函数返回值设为 true。否则,Qt 还是会将事件分发给这个接收组件,从而导致程序崩溃。事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
ok,讲完啦~
============== End