一、Qt 事件概述:
事件(event)是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户做出相应时发出,如鼠标、键盘事件等;还有一些事件是由系统自动发出,如计时器事件。
我们知道,Qt 程序需要在 main() 函数中创建一个 QApplication 对象,然后调用他的 exec() 函数,这个函数的功能就是开始 Qt 的事件循环。在执行 exec() 函数之后,程序将进入事件循环来监听应用程序的事件。
当事件发生时,Qt 将创建一个事件对象(QEvent 对象)。Qt 中所有事件类都继承于 QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给 QObject 的 event() 函数。event() 函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler)。
在所有组件的父类 QWidget 中,定义了很多事件处理的回调函数,比如常见的如:
-
keyPressEvent() 按键按下的事件
-
keyReleaseEvent() 按键释放的事件
-
mouseDoubleClickEvent() 鼠标双击的事件
-
mouseMoveEvent() 鼠标移动的事件
-
mousePressEvent() 鼠标按下的事件
-
mouseReleaseEvent() 鼠标释放的事件
这些事件函数都是 protected virtual 的,也就是说,我们可以在子类中重新实现这些函数。
二、Qt 事件案例:
在项目中新建一个类 MyLabel,继承于 QLabel 类:
添加新文件:
选择添加新文件的模板类型:
选择基类:
在创建好的代码中将基类 QWidget 改成 QLabel:
查看帮助文档可以看到 QLabel 所有可以重写的事件方法,如下所示:
注意:还有很多是父对象的事件方法,如果需要的事件功能在 QLabel 中没找到,可以去其父对象中找。
此处我们拿鼠标的三个事件方法进行测试:
mylabel.h:
#include <QLabel>
class MyLabel : public QLabel
{
Q_OBJECT
public:
explicit MyLabel(QWidget *parent = 0);
signals:
public slots:
// 重写事件方法,需要用 protected 修饰
protected:
void mouseMoveEvent(QMouseEvent *ev); // 鼠标移动事件
void mousePressEvent(QMouseEvent *ev); // 鼠标按下事件
void mouseReleaseEvent(QMouseEvent *ev); // 鼠标弹起事件
};
mylabel.cpp:
#include "mylabel.h"
#include <QDebug>
#include <QMouseEvent>
MyLabel::MyLabel(QWidget *parent) : QLabel(parent)
{
}
// 鼠标移动事件
// 注意:程序启动以后,直接移动鼠标并不会触发该事件,而是需要在控件上点击
// 一下鼠标之后才会触发该事件;这是因为 QWidget 中有一个 mouseTracking 属性,
// 该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent 事件才会触发。
// 如果 mouseTracking 是 false(默认),控件至少要被点击一次之后,才能够被追踪,
// 也就是能够触发 mouseMoveEvent 事件。
// 如果 mouseTracking 为 true,则 mouseMoveEvent 事件可以直接被触发;
void MyLabel::mouseMoveEvent(QMouseEvent *ev)
{
// 设置 label 上显示的值;
// label 控件上显示的文本信息支持 html 代码;
// QString 的 arg() 函数可以自动替换掉 QString 中出现的占位符;
// 占位符以 % 开始,后面是占位符的位置,从 1 开始,比如 %1、%2 等;
this->setText(QString("<center><h1>Move:(%1, %2)</h1></center>")
.arg(QString::number(ev->x()))
.arg(QString::number(ev->y())));
}
// 鼠标按下事件
void MyLabel::mousePressEvent(QMouseEvent *ev)
{
if (ev->button() == Qt::LeftButton)
{
qDebug() << "左键按下";
}
else if (ev->button() == Qt::RightButton)
{
qDebug() << "右键按下";
}
else if (ev->button() == Qt::MidButton)
{
qDebug() << "中间滚轮按下";
}
this->setText(QString("<center><h1>Press:(%1, %2)</h1></center>")
.arg(QString::number(ev->x()))
.arg(QString::number(ev->y())));
}
// 鼠标弹起事件
void MyLabel::mouseReleaseEvent(QMouseEvent *ev)
{
// 使用 QString 的 sprintf 方法格式化字符串
QString msg;
msg.sprintf("<center><h1>Release:(%d, %d)</h1></center>", ev->x(), ev->y());
this->setText(msg);
}
在主窗口中使用新创建的 MyLabel 类:
#include "widget.h"
#include "ui_widget.h"
#include "mylabel.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
// 在主窗口中添加自定义的 Label 控件
MyLabel *myLabel = new MyLabel(this);
// 为自定义 Label 控件设置一个边框
myLabel->setFrameShape(QFrame::Box);
// 设置自定义 Label 控件显示位置和大小
myLabel->setGeometry(10, 10, this->width() - 20, this->height() - 20);
// 启用鼠标追踪,即一开始就可以触发 mouseMoveEvent 事件
myLabel->setMouseTracking(true);
}
还有一类事件不需要用户人为触发,而是由 Qt 内部自动触发,比如定时器事件:
我们在主窗口上拖两个 QLabel 控件,如下:
widget.h:
#include <QWidget>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
private:
Ui::Widget *ui;
// 定义两个定时器 id
int timerId;
int timerId2;
protected:
void timerEvent(QTimerEvent *); // 重写定时器事件
};
widget.cpp:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
// 开启定时器:单位是毫秒。返回一个定时器 id,用于标识当前定时器
// 每隔 1 秒钟执行一次定时器
this->timerId = this->startTimer(1000);
// 每隔 0.5 秒执行一次定时器
this->timerId2 = this->startTimer(500);
}
Widget::~Widget()
{
delete ui;
}
// 定时器事件
void Widget::timerEvent(QTimerEvent *ev)
{
// 判断当前运行的定时器是哪一个
if (ev->timerId() == timerId)
{
static int count = 0;
ui->timerId->setText(QString("<center><h1>timer:%1</h1></center>").arg(count++));
if (count == 5)
{
// 停止定时器:需要传入一个定时器 id 作为参数
this->killTimer(timerId);
}
}
else if (ev->timerId() == timerId2)
{
static int count = 0;
ui->timerId2->setText(QString("<center><h1>timer2:%1</h1></center>").arg(count++));
}
}
三、事件的接收和忽略:
我们新建一个 MyButton 类,继承于 QPushButton,在 MyButton 中重写父类的鼠标按下事件:
class MyButton : public QPushButton
{
Q_OBJECT
public:
explicit MyButton(QWidget *parent = 0);
protected:
void mousePressEvent(QMouseEvent *e); // 重写父类的鼠标按下事件
};
#include "mybutton.h"
#include <QMouseEvent>
#include <QDebug>
MyButton::MyButton(QWidget *parent) : QPushButton(parent)
{
}
// 因为鼠标按下事件是重写父类的,所以鼠标点击父类控件的时候,先执行这个事件
void MyButton::mousePressEvent(QMouseEvent *e)
{
if (e->button() == Qt::LeftButton)
{
// 事件接收:此处我们自己处理了鼠标左键按下事件,就不会再将鼠标左键点击事件传递给父类了;
// 也就是说,我们在子类中拦截了鼠标左键点击事件。
qDebug() << "鼠标左键被按下";
}
else
{
// 事件忽略:对于不关心的事件,调用父类的同名函数,将事件传给父类。
QPushButton::mousePressEvent(e);
}
}
然后在主窗口中拖一个 QPushButton 控件,提升为我们自定义的 MyButton,并为其添加 clicked 信号处理函数:
为按钮添加一个槽函数,输出一句话:
// 按钮被点击时触发的事件
void Widget::on_pushButton_clicked()
{
qDebug() << "按钮被点击";
}
测试发现:点击按钮时,只输出了 "鼠标左键被按下",没有输出 "按钮被点击",这是因为我们在子类(MyButton)中重写了父类(QPushButton)的鼠标按下事件,并自己处理了鼠标左键按下事件,所以父类不会再接收到鼠标点击(clicked)事件。也就是说,我们在子类中拦截了鼠标点击事件。
关于事件的接收和忽略,Qt 的事件对象有两个专门的函数:accept() 和 ignore():
- accept() 用来告诉 Qt,这个类的事件处理函数想要处理这个事件;如果一个类的事件处理函数调用了事件对象的 accept() 函数,这个事件就不会被继续传播给父组件(注意:不是父对象)。
-
ignore() 则告诉 Qt,这个类的事件处理函数不想要处理这个事件;如果调用了事件的 ignore() 函数,Qt 会将事件继续向下传递给其父组件。
-
在事件处理函数中,可以使用 isAccepted() 来查询这个事件是不是已经被接收了。
事实上,我们很少会使用 accept() 和 ignore(),而是像上面的示例一样,如果希望忽略事件,只要调用父类的响应函数即可。
accept() 和 ignore() 最长使用的地方是在窗口关闭事件中:
// 重写父类的窗口关闭事件
void Widget::closeEvent(QCloseEvent *e)
{
int ret = QMessageBox::question(this, "question", "是否关闭窗口?");
if (ret == QMessageBox::Yes)
{
// 关闭窗口
// 处理窗口关闭事件,接收事件,事件就不会再往下传递了
e->accept();
}
else
{
// 不关闭窗口
// 忽略事件,事件继续向父组件传递,则程序不会退出
e->ignore();
}
}
四、event() 事件分发函数:
事件对象(QEvent 对象)创建完毕后,Qt 将这个事件对象传递给 QObject 的 event() 函数。event() 函数并不直接处理事件,而是将这些事件对象按照他们不同的类型,分发给不同的事件处理器(event handler)。
event() 函数主要用于事件的分发。所以,如果我们希望在事件分发之前做一些操作,就可以重写这个 event() 函数。
例如,如果我们希望在上面创建的 MyLabel 控件中监听键盘按键的按下,那么我们可以修改程序如下:
mylabel.h:
#include <QLabel>
class MyLabel : public QLabel
{
Q_OBJECT
public:
explicit MyLabel(QWidget *parent = 0);
signals:
public slots:
protected:
bool event(QEvent *e); // 重写事件分发函数
};
mylabel.cpp:
#include "mylabel.h"
#include <QDebug>
#include <QEvent>
#include <QKeyEvent>
MyLabel::MyLabel(QWidget *parent) : QLabel(parent)
{
}
// 事件分发函数:参数为事件对象
bool MyLabel::event(QEvent *e)
{
// 判断事件类型:按键按下事件
if (e->type() == QEvent::KeyPress)
{
// 将事件转换成键盘按下事件
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
// 输出按下的键
qDebug() << (char)ev->key();
// 如果按下的是 Tab 键
if (keyEvent->key() == Qt::Key_Tab)
{
qDebug() << "Tab 键被按下了";
// 返回 true 表示事件被处理完毕,不用再将该事件分发给其他对象了
return true;
}
}
// 对于不关心的事件,需要调用父类的 event() 函数继续分发
return QWidget::event(e);
}
在主窗口中使用:
#include "widget.h"
#include "ui_widget.h"
#include "mylabel.h"
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
// 在主窗口中添加自定义的 Label 控件
MyLabel *myLabel = new MyLabel(this);
// 为自定义 Label 控件设置一个边框
myLabel->setFrameShape(QFrame::Box);
// 设置自定义 Label 控件显示位置和大小
myLabel->setGeometry(10, 10, this->width() - 20, this->height() - 20);
// 设置控件获取焦点(控件没有焦点时,无法响应 键盘按下事件)
myLabel->setFocus();
}
event() 函数实际中实际是通过事件处理器来响应一个具体的事件。这相当于 event() 函数将具体事件的处理 “委托” 给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此我们重写了某个事件处理器,即可让 Qt 调用我们自己实现的版本。
五、事件过滤器:
有时候,对象需要查看,甚至拦截发送到另外对象的事件。
前面我们已经知道,Qt 创建了 QEvent 事件对象以后,会调用 QObject 的 event() 函数处理事件的分发。显然,我们也可以在 event() 函数中实现拦截的操作。由于 event() 函数是 protected 的,因此,需要继承已有的父类。而且如果控件很多,每个控件中都要重写一个 event() 函数,会很麻烦。好在,Qt 提供了另一种机制来达到这一目的:事件过滤器。
QObject 有一个 eventFilter() 函数,用于建立事件过滤器,函数原型如下:
这个函数正如其名字显示的那样,是一个 “事件过滤器”。所谓事件过滤器,可以理解为一种过滤代码。事件过滤器会检查接收到的事件。如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让他继续转发,就返回 true,否则返回 false。事件过滤器的调用时间是 目标对象接收到事件对象之前。也就是说,如果在事件过滤器中停止了某个事件,那么目标对象以及以后所有的事件过滤器都不会知道这么一个事件。
Qt 帮助文档中有一段代码如下:
// MainWindow 是我们自己定义的类,继承于 QMainWindow
class MainWindow : public QMainWindow
{
public:
MainWindow();
protected:
bool eventFilter(QObject *obj, QEvent *ev); // 重写事件过滤器函数
private:
QTextEdit *textEdit; // 创建一个 textEdit 控件对象
};
MainWindow::MainWindow()
{
// 实例化 textEdit 控件
textEdit = new QTextEdit;
// 将 textEdit 设置为核心部件
setCentralWidget(textEdit);
// 为 textEdit 控件安装事件过滤器,安装之后才能过滤指定控件上的事件
textEdit->installEventFilter(this);
// 移除事件过滤器
//textEdit->removeEventFilter(this);
}
// eventFilter() 函数相当于创建了事件过滤器,如果想使用这个过滤器,需要安装
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
// 判断安装事件过滤器的对象:textEdit 对象
if (obj == textEdit)
{
// 判断事件的类型:键盘按下事件
if (event->type() == QEvent::KeyPress)
{
// 将事件转换成 键盘按下事件
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
qDebug() << "Ate key press" << keyEvent->key();
// 如果我们不想让 textEdit 控件处理键盘按下的事件(我们自己来处理),返回 true,
// 即过滤掉了这个事件。
return true;
}
else
{
// 因为其他事件还需要 textEdit 继续处理,所以此处返回 false。
return false;
}
}
else
{
// pass the event on to the parent class
// 对于其他的控件,调用父类的 eventFilter() 函数。
return QMainWindow::eventFilter(obj, event);
}
}
事件过滤器的强大之处在于,我们可以为整个应用程序添加一个事件过滤器。记得,installEventFilter()函数是QObject的函数,QApplication或者QCoreApplication对象都是QObject的子类,因此,我们可以向QApplication或者QCoreApplication添加事件过滤器。这种全局的事件过滤器将会在所有其它特性对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况,否则的话我们不应该这么做。
注意:
事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。