QT-第五节 Qt消息机制和事件
消息机制和事件
事件
- 事件(event)是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
- 在前面我们也曾经简单提到,Qt 程序需要在main()函数创建一个QApplication对象,然后调用它的exec()函数。这个函数就是开始 Qt 的事件循环。在执行exec()函数之后,程序将进入事件循环来监听应用程序的事件。
- 当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于QEvent
- 在事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。
- event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler),关于这一点,会在后边详细说明。
-
在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如
keyPressEvent() keyReleaseEvent() mouseDoubleClickEvent() mouseMoveEvent() mousePressEvent() mouseReleaseEvent() 等。
- 这些函数都是 protected virtual 的,也就是说,我们可以在子类中重新实现这些函数。下面来看一个例子:
- 监听QLabel的一些事件
- 分析:要想监听QLabel的事件那么就必须自定义QLabel了
- 右击项目->Add New…->选择C++->C++ class 输入类名: MYLabel ;基类名为:QWidget(因为没有QLabel这个类,因此先选择这个),点击下一步、完成
- 打开MYLabel.h,修改基类为QLabel,注意修改的地方,有些地方不能改,必须仍然是QWidget
- 重写事件,cpp中实现监听,代码如下
- 在主UI中使用
- 拖一个QLabel,提升为MYLabel类既可
- 给改label添加一个边框,便于显示:
- 属性QFrame->frameshape ->box
- 运行demo就可以监听了
-
代码:
//mylabel.h文件 #ifndef MYLABEL_H #define MYLABEL_H #include <QLabel> class MYLabel : public QLabel { Q_OBJECT public: //注意这个地方不能改QWidget explicit MYLabel(QWidget *parent = nullptr); //捕获事件 //鼠标进入 void enterEvent(QEvent*); //鼠标离开 void leaveEvent(QEvent *); //鼠标移动 void mouseMoveEvent(QMouseEvent *ev); //鼠标按下 void mousePressEvent(QMouseEvent *ev); //鼠标松开 void mouseReleaseEvent(QMouseEvent *ev); }; #endif // MYLABEL_H //mylabel.cpp #include "mylabel.h" #include<QDebug> #include<QMouseEvent> MYLabel::MYLabel(QWidget *parent) : QLabel(parent) { //设置鼠标的追踪 //这么一设置下面的移动监听方法,本来是鼠标必须按下移动才会监听得到 //现在不按下,移动也能监听到了 this->setMouseTracking(true); } void MYLabel:: enterEvent(QEvent *){ qDebug()<<"鼠标进入"; } void MYLabel:: leaveEvent(QEvent *){ qDebug()<<"鼠标离开"; } //鼠标移动 void MYLabel:: mouseMoveEvent(QMouseEvent *ev){ //注意在移动内是无法用ev->button()==Qt::LeftButton判断左键和右键的了 if(ev->buttons() & Qt::LeftButton){ qDebug()<<"鼠标左键按下滑动"; } QString str = QString("鼠标移动 x= %1,y = %2").arg(ev->x()).arg(ev->y()); qDebug()<<str; } //鼠标按下 void MYLabel :: mousePressEvent(QMouseEvent *ev){ //判断鼠标按得是左键还是右键 if(ev->button()==Qt::LeftButton){ qDebug()<<"点击了左键"; } //按下的位置 QString str = QString("鼠标按下 x= %1,y = %2").arg(ev->x()).arg(ev->y()); qDebug()<<str; } //鼠标松开 void MYLabel :: mouseReleaseEvent(QMouseEvent *ev){ QString str = QString("鼠标松开 x= %1,y = %2").arg(ev->x()).arg(ev->y()); qDebug()<<str; }
定时器事件
- 第一种使用方式,通过监听QtimerEvent事件
- 在主窗口中重写监听事件,在.cpp中实现事件
-
代码如下:
//Widget.h #ifndef WIDGET_H #define WIDGET_H #include <QWidget> namespace Ui { class Widget; } class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent = nullptr); ~Widget(); private: Ui::Widget *ui; //定时器事件 void timerEvent(QTimerEvent *); //定时器的标识号 int id1; int id2; }; #endif // WIDGET_H //Widget.cpp #include "widget.h" #include "ui_widget.h" Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); //启动定时器 //每1000ms调用一次timerEent事件 id1 = startTimer(1000); //每个2s... id2 = startTimer(2000); } Widget::~Widget() { delete ui; } void Widget:: timerEvent(QTimerEvent *e){ if(e->timerId() == id1){ static int num = 0; ui->label_2->setText(QString::number(num++)); }else if (e->timerId() == id2) { static int num2 = 0; ui->label_3->setText(QString::number(num2++)); } }
- 定时器的第二种方式:(常用)
- 使用QTimer对象
-
代码示例:
//定时器的第二种方式 QTimer *timer1 = new QTimer(this); //启动,每个0.5s发送一次信号 timer1->start(500); connect(timer1,&QTimer::timeout,[=](){ static int num = 0; ui->label_4->setText(QString::number(num++)); }); //暂停定时器 connect(ui->pushButton,&QPushButton::clicked,[=](){ timer1->stop(); });
event()
- 事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。
-
如上所述,event()函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个event()函数了。例如,我们希望在一个QWidget组件中监听 tab 键的按下,那么就可以继承QWidget,并重写它的event()函数,来达到这个目的:
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子类。我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型。
- 如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。
- 在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播。
-
我们可以通过使用QEvent::type()函数可以检查事件的实际类型,其返回值是QEvent::Type类型的枚举。我们处理过自己感兴趣的事件之后,可以直接返回 true,表示我们已经对此事件进行了处理;对于其它我们不关心的事件,则需要调用父类的event()函数继续转发,否则这个组件就只能处理我们定义的事件了。为了测试这一种情况,我们可以尝试下面的代码:
bool CustomTextEdit::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 false; }
- CustomTextEdit是QTextEdit的一个子类。我们重写了其event()函数,却没有调用父类的同名函数。这样,我们的组件就只能处理 Tab 键,再也无法输入任何文本,也不能响应其它事件,比如鼠标点击之后也不会有光标出现。这是因为我们只处理的KeyPress类型的事件,并且如果不是KeyPress事件,则直接返回 false,鼠标事件根本不会被转发,也就没有了鼠标事件。
-
通过查看QObject::event()的实现,我们可以理解,event()函数同前面的章节中我们所说的事件处理器有什么联系:
//!!! Qt5 bool QObject::event(QEvent *e) { switch (e->type()) { case QEvent::Timer: timerEvent((QTimerEvent*)e); break; case QEvent::ChildAdded: case QEvent::ChildPolished: case QEvent::ChildRemoved: childEvent((QChildEvent*)e); break; // ... default: if (e->type() >= QEvent::User) { customEvent(e); break; } return false; } return true; }
-
这是 Qt 5 中QObject::event()函数的源代码(Qt 4 的版本也是类似的)。我们可以看到,同前面我们所说的一样,Qt 也是使用QEvent::type()判断事件类型,然后调用了特定的事件处理器。比如,如果event->type()返回值是QEvent::Timer,则调用timerEvent()函数。可以想象,QWidget::event()中一定会有如下的代码:
switch (event->type()) { case QEvent::MouseMove: mouseMoveEvent((QMouseEvent*)event); break; // ... }
- 事实也的确如此。timerEvent()和mouseMoveEvent()这样的函数,就是我们前面章节所说的事件处理器 event handler。也就是说,event()函数中实际是通过事件处理器来响应一个具体的事件。这相当于event()函数将具体事件的处理“委托”给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此,我们重写了某一个事件处理器,即可让 Qt 调用我们自己实现的版本。
- 由此可以见,event()是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个event()函数,通过QEvent::type()判断不同的事件。鉴于重写event()函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了event()函数的另外一个作用:屏蔽掉某些不需要的事件处理器。正如我们前面的CustomTextEdit例子看到的那样,我们创建了一个只能响应 tab 键的组件。这种作用是重写事件处理器所不能实现的。
-
-
在MYLabel.h/cpp重写并实现event方法
bool MYLabel:: event(QEvent*e){ //拦截鼠标下按做处理 if(e->type()==QEvent::MouseButtonPress){ //类型转换:转换为子类型 QMouseEvent *ev = static_cast<QMouseEvent *>(e); QString str = QString("event:鼠标按下 x= %1,y = %2").arg(ev->x()).arg(ev->y()); qDebug()<<str; //返回true那么外部的监听事件就无效了mousePressEvent return true; } //其他的事件,让父类左处理 return QLabel:: event(e); }
时间过滤器
- 有时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,对话框可能想要拦截按键事件,不让别的组件接收到;或者要修改回车键的默认处理。
- 通过前面已经知道,Qt 创建了QEvent事件对象之后,会调用QObject的event()函数处理事件的分发。显然,我们可以在event()函数中实现拦截的操作。
- 由于event()函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个event()函数。这当然相当麻烦,更不用说重写event()函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。
-
QObject有一个eventFilter()函数,用于建立事件过滤器。函数原型如下:
virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );
- 这个函数正如其名字显示的那样,是一个“事件过滤器”。所谓事件过滤器,可以理解成一种过滤代码。事件过滤器会检查接收到的事件。
- 如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。
- 这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让它继续转发,就返回 true,否则返回 false。
- 事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched对象以及以后所有的事件过滤器根本不会知道这么一个事件。
-
我们来看一段简单的代码:
class MainWindow : public QMainWindow { public: MainWindow(); protected: bool eventFilter(QObject *obj, QEvent *event); private: QTextEdit *textEdit; }; MainWindow::MainWindow() { textEdit = new QTextEdit; setCentralWidget(textEdit); //给textEdit安装过滤器 textEdit->installEventFilter(this); } bool MainWindow::eventFilter(QObject *obj, QEvent *event) { if (obj == textEdit) {//判断哪个对象 if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); qDebug() << "Ate key press" << keyEvent->key(); return true; } else { return false; } } else { // pass the event on to the parent class return QMainWindow::eventFilter(obj, event); } }
- MainWindow是我们定义的一个类。我们重写了它的eventFilter()函数。为了过滤特定组件上的事件,首先需要判断这个对象是不是我们感兴趣的组件,然后判断这个事件的类型。在上面的代码中,我们不想让textEdit组件处理键盘按下的事件。所以,首先我们找到这个组件,如果这个事件是键盘事件,则直接返回 true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以返回 false。对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。
-
eventFilter()
函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用QObject::installEventFilter()
函数。函数的原型如下:void QObject::installEventFilter ( QObject * filterObj )
- 这个函数接受一个QObject *类型的参数。记得刚刚我们说的,eventFilter()函数是QObject的一个成员函数,因此,任意QObject都可以作为事件过滤器(问题在于,如果你没有重写eventFilter()函数,这个事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过
QObject::removeEventFilter()
函数移除。
- 这个函数接受一个QObject *类型的参数。记得刚刚我们说的,eventFilter()函数是QObject的一个成员函数,因此,任意QObject都可以作为事件过滤器(问题在于,如果你没有重写eventFilter()函数,这个事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过
- 设置过滤的步骤:
- 给需要拦截的空间安装过滤器
- 重写eventFilter事件
- 事件过滤器的强大之处在于,我们可以为整个应用程序添加一个事件过滤器。记得,
installEventFilter()
函数是QObject的函数,QApplication或者QCoreApplication对象都是QObject的子类,因此,我们可以向QApplication或者QCoreApplication添加事件过滤器。这种全局的事件过滤器将会在所有其它特性对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况,否则的话我们不应该这么做。 - 注意,事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
- 过滤器的作用:
- 哪个控件需要过滤就给哪个控件安装过滤器installEventFilter
- 实现eventFilter过滤器的监听,过滤相应控件的相应事件
-
测试代码:
//Widget.cpp文件 Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget) { ui->setupUi(this); //给label做过滤器拦截 //安装过滤器 ui->label->installEventFilter(this); } bool Widget:: eventFilter(QObject*obj,QEvent*e){ if(obj==ui->label){ if(e->type()==QEvent::MouseButtonPress){ //类型转换:转换为子类型 QMouseEvent *ev = static_cast<QMouseEvent *>(e); QString str = QString("fliter:鼠标按下 x= %1,y = %2").arg(ev->x()).arg(ev->y()); qDebug()<<str; //返回true那么外部的监听事件就无效了mousePressEvent return true; } } return QWidget:: eventFilter(obj,e); }
总结
- Qt 中有很多种事件:鼠标事件、键盘事件、大小改变的事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:
-
所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择:在这个函数中,我们需要使用switch语句,选择message参数的类型进行处理,典型代码是
switch(message) { case WM_PAINT: // ... break; case WM_DESTROY: // ... break; ... }
-
每一种事件对应一个事件处理函数。Qt 就是使用的这么一种机制:
mouseEvent() keyPressEvent() …
-
- Qt 具有这么多种事件处理函数,肯定有一个地方对其进行分发,否则,Qt 怎么知道哪一种事件调用哪一个事件处理函数呢?这个分发的函数,就是event()。显然,当QMouseEvent产生之后,event()函数将其分发给mouseEvent()事件处理器进行处理。
- event()函数会有两个问题:
- event()函数是一个 protected 的函数,这意味着我们要想重写event(),必须继承一个已有的类。试想,我的程序根本不想要鼠标事件,程序中所有组件都不允许处理鼠标事件,是不是我得继承所有组件,一一重写其event()函数?protected 函数带来的另外一个问题是,如果我基于第三方库进行开发,而对方没有提供源代码,只有一个链接库,其它都是封装好的。我怎么去继承这种库中的组件呢?
- event()函数的确有一定的控制,不过有时候我的需求更严格一些:我希望那些组件根本看不到这种事件。event()函数虽然可以拦截,但其实也是接收到了QMouseEvent对象。我连让它收都收不到。这样做的好处是,模拟一种系统根本没有那个事件的效果,所以其它组件根本不会收到这个事件,也就无需修改自己的事件处理函数。这种需求怎么办呢?
- 这两个问题是event()函数无法处理的。于是,Qt 提供了另外一种解决方案:事件过滤器。事件过滤器给我们一种能力,让我们能够完全移除某种事件。事件过滤器可以安装到任意QObject类型上面,并且可以安装多个。如果要实现全局的事件过滤器,则可以安装到QApplication或者QCoreApplication上面。这里需要注意的是,如果使用installEventFilter()函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的eventFilter()函数进行过滤,其它对象不受影响。如果给QApplication对象安装事件过滤器,那么该过滤器对程序中的每一个对象都有效,任何对象的事件都是先传给eventFilter()函数。
- 事件过滤器可以解决刚刚我们提出的event()函数的两点不足:
- 首先,事件过滤器不是 protected 的,因此我们可以向任何QObject子类安装事件过滤器;
- 其次,事件过滤器在目标对象接收到事件之前进行处理,如果我们将事件过滤掉,目标对象根本不会见到这个事件。
- 现在我们可以总结一下 Qt 的事件处理,实际上是有五个层次:
- 重写paintEvent()、mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。
- 重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。
- 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。
- 在QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和notify()函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。
- 重写QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)。