一、认识事件
1、事件(event) 是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
2、事件也就是我们通常说的“事件驱动(event drive) ”程序设计的基础概念。事件的出现,使得程序代码不会按照原始的线性顺序执行。想想看,从最初的 C 语言开始,我们的程序就是以一种线性的顺序执行代码:这一条语句执行之后,开始执行下一条语句;这一个函数执行过后,开始执行下一个函数。这种类似“批处理”的程序设计风格显然不适合于处理复杂的用户交互。我们来想象一下用户交互的情景:我们设计了一堆功能放在界面上,用户点击了“打开文件”,于是开始执行打开文件的操作;用户点击了“保存文件”,于是开始执行保存文件的操作。我们不知道用户究竟想进行什么操作,因此也就不能预测接下来将会调用哪一个函数。如果我们设计了一个“文件另存为”的操作,如果用户不点击,这个操作将永远不会被调用。这就是所谓的“事件驱动”,我们的程序的执行顺序不再是线性的,而是由一个个事件驱动着程序继续执行。没有事件,程序将阻塞在那里,不执行任何代码。3、在 Qt 中,事件的概念似乎同信号槽类似。的确如此,一般来说,使用 Qt 组件时,我们并不会把主要精力放在事件上。因为在 Qt 中,我们关心的更多的是事件关联的一个信号。比如,对于 QPushButton 的鼠标点击,我们不需要关心这个鼠标点击事件,而是关心它的clicked()信号的发出。这与其他的一些 GUI 框架不同:在 Swing 中,你所要关心的是JButton 的 ActionListener 这个点击事件。由此看出,相比于其他 GUI 框架, Qt 给了我们额外的选择:信号槽。
4、但是, Qt 中的事件和信号槽却并不是可以相互替代的。信号由具体的对象发出,然后会马上交给由 connect()函数连接的槽进行处理;而对于事件, Qt 使用一个事件队列对所有发出的事件进行维护,当新的事件产生时,会被追加到事件队列的尾部。前一个事件完成后,取出后面的事件进行处理。但是,必要的时候, Qt 的事件也可以不进入事件队列,而是直接处理。信号一旦发出,对应的槽函数一定会被执行。但是,事件则可以使用“事件过滤器”进行过滤,对于有些事件进行额外的处理,另外的事件则不关心。总的来说,如果我们使用组件,我们关心的是信号槽;如果我们自定义组件,我们关心的是事件。因为我们可以通过事件来改变组件的默认操作。比如,如果我们要自定义一个能够响应鼠标事件的 EventLabel,我们就需要重写 QLabel 的鼠标事件,做出我们希望的操作,有可能还得在恰当的时候发出一个类似按钮的 clicked()信号(如果我们希望让这个 EventLabel 能够被其它组件使用)或者其它的信号。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QMouseEvent>
#include <QLabel>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
};
/*
* EventLabel 继承了 QLabel,覆盖了 mousePressEvent()、 mouseMoveEvent()
* 和MouseReleaseEvent() 三个函数。
*/
class EventLabel:public QLabel{
protected:
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include <QString>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
}
MainWindow::~MainWindow()
{
}
/*
*QLabel支持HTML代码,<center>设置文本为水平居中,<h1>设置字体为黑体,</h1></center>是结束符。
* QString 的 arg() 函数可以自动替换掉 QString 中出现的占位符。其占位符以 % 开始,后面是占位
* 符的位置,例如 %1, %2 这种。
*/
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){
QString msg;
/*
* 使用另外一种 QString 的构造方法。类似C风格的格式化函数sprintf()来构造 QString。
*/
msg.sprintf("<center><h1>Release:(%d,%d)</h1></center>",event->x(),event->y());
this->setText(msg);
}
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
EventLabel *la=new EventLabel;
la->setWindowTitle("鼠标事件");
la->resize(300,200);//设置窗口大小
/*
* QWidget 中有一个 mouseTracking 属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,
* mouseMoveEvent() 才会发出。如果 mouseTracking 是 false(默认即是),组件在至少一次
* 鼠标点击之后, 才能够被追踪, 也就是能够发出 mouseMoveEvent() 事件。如果 mouseTracking
* 为 true,则 mouseMoveEvent() 直接可以被发出。
*/
la->setMouseTracking(true);
la->show();
return a.exec();
}
二、重写event()
event() 函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个 event() 函数了。我们可以通过使用QEvent::type()函数可以检查事件的实际类型,其返回值是 QEvent::Type类型的枚举。我们处理过自己感兴趣的事件之后,可以直接返回 true,表示我们已经对此事件进行了处理;对于其它我们不关心的事件,则需要调用父类的 event() 函数继续转发,否则这个组件就只能处理我们定义的事件了。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QWidget>
#include <QEvent>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
};
class CustomWidget:public QWidget{
protected:
bool event(QEvent *e);
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include <QKeyEvent>
#include <QDebug>
#include <Qt>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
}
MainWindow::~MainWindow()
{
}
/*
* CustomWidget 是一个普通的 QWidget 子类。我们重写了它的 event()函数,这个函数有
* 一个 QEvent 对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型。如果
* 传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,并
* 且,该事件对象设置了 accept(),那么 Qt 会认为这个事件已经处理完毕,不会再将这个事
* 件发送给其它对象,而是会继续处理事件队列中的下一事件。注意,在 event()函数中,调
* 用事件对象的 accept()和 ignore()函数是没有作用的,不会影响到事件的传播。
*/
bool CustomWidget::event(QEvent *e){
if(e->type()==QEvent::KeyPress){
QKeyEvent *keyEvent=static_cast<QKeyEvent *>(e);
if(keyEvent->key()==Qt::Key_Tab){
qDebug()<<"tab键被按下";
return true;
}
}
return QWidget::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 对象以及以后所有的事件过滤器根本不会知道这么一个事件。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTextEdit>
#include <QObject>
#include <QEvent>
class MainWindow : public QMainWindow
{
Q_OBJECT
private:
QTextEdit *textedit;
protected:
bool eventFilter(QObject *obj, QEvent *event);
public:
MainWindow();
~MainWindow();
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include <QEvent>
#include <QDebug>
#include <QKeyEvent>
MainWindow::MainWindow(){
textedit=new QTextEdit;
setCentralWidget(textedit);
/*
* eventFilter() 函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用
* QObject::installEventFilter() 函数。我们可以向一个对象上面安装多个事件处理器,只要调
* 用多次 installEventFilter() 函数。如果一个对象存在多个事件过滤器,那么,最后一个安装
* 的会第一个执行,也就是后进先执行的顺序。
*/
textedit->installEventFilter(this);
}
/*
* MainWindow 是我们定义的一个类。我们重写了它的 eventFilter() 函数。为了过滤特定组件上的事件,
* 首先需要判断这个对象是不是我们感兴趣的组件,然后判断这个事件的类型。在上面的代码中,我们不想
* 让 textEdit 组件处理键盘按下的事件。所以,首先我们找到这个组件,如果这个事件是键盘事件,则直
* 接返回 true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以返回 false。对于其它的组件,
* 我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。
*/
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{
return QMainWindow::eventFilter(obj,event);
}
}
/*
* 注意,如果你在事件过滤器中 delete 了某个接收组件,务必将函数返回值设为 true。否则,
* Qt 还是会将事件分发给这个接收组件,从而导致程序崩溃。事件过滤器和被安装过滤器的组件
* 必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了
* 不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
*/
MainWindow::~MainWindow()
{
}
四、定时器
Qt中有两种方法来使用定时器,一种是定时器事件,另一种是使用信号和槽。一般使用了多个定时器时最好使用定时器事件来处理。
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QTimerEvent>
#include <QLabel>
#include <QLineEdit>
#include <QTimer>
#include <QDateTime>
class Widget : public QWidget
{
Q_OBJECT
private slots:
void timerUpdate();
private:
int id1,id2,id3;
QLabel *label1;
QLabel *label2;
QLineEdit *lineEdit;
protected:
void timerEvent(QTimerEvent *event);
public:
Widget(QWidget *parent = 0);
~Widget();
};
#endif // WIDGET_H
#include "widget.h"
#include <QString>
Widget::Widget(QWidget *parent)
: QWidget(parent)
{
resize(600,600);
setWindowTitle(tr("定时器和随机数"));
label1=new QLabel(this);
label1->move(100,100);
label2=new QLabel(this);
label2->move(200,100);
label2->setFixedSize(40,30);
/*
* 这里开启了三个定时器,分别返回了它们的id,这个id用来区分不同的定时器。
* 定时器的时间单位是毫秒。每当一个定时器溢出时,都会调用定时器事件处理函数,
* 我们可以在该函数中进行相应的处理。
*/
id1=startTimer(1000);
id2=startTimer(2000);
id3=startTimer(10000);
/*
* 如果只是想开启少量的定时器,也可以使用信号和槽来实现。
* 这里创建了一个定时器,并将其溢出信号和更新槽关联起来,最后使用start()函数来开启定时器。
*/
QTimer *timer=new QTimer(this);
connect(timer,&QTimer::timeout,this,&Widget::timerUpdate);
timer->start(1000);
lineEdit=new QLineEdit(this);
lineEdit->move(100,300);
lineEdit->setFixedSize(200,50);
/*
*关于随机数,在Qt中是使用qrand()和qsrand()两个函数实现的。在前面的程序中已经看到了qrand()
* 函数的使用,其可以产生随机数,qrand()%10可以产生0-9之间的随机数。要想产生100以内的随机数
* 就是%100,以此类推。在使用qrand()函数产生随机数之前,一般要使用qsrand()函数为其设置初值,
* 如果不设置初值,那么每次运行程序,qrand()都会产生相同的一组随机数。为了每次运行程序时,
* 都可以产生不同的随机数,我们要使用qsrand()设置一个不同的初值。这里使用了QTime类的secsTo()
* 函数,它表示两个时间点之间所包含的秒数,比如代码中就是指从零点整到当前时间所经过的秒数。
*/
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
}
Widget::~Widget()
{
}
/*
* 这里先使用timerId()函数返回了溢出的定时器的id,然后根据该id来判断是哪个定时器溢出了,
* 并进行相应的处理。每当第一个定时器溢出时都产生一个小于10的随机数;当第二个定时器溢出时,
* 就更改标签的文本。
*/
void Widget::timerEvent(QTimerEvent *event){
if(event->timerId()==id1){
label1->setText(tr("%1").arg(qrand()%10));
}
else if(event->timerId()==id2){
label2->setText(tr("呵呵"));
}
else{
return QWidget::timerEvent(event);
}
}
void Widget::timerUpdate(){
QDateTime time=QDateTime::currentDateTime();//获取系统现在的时间
QString str=time.toString("yyyy-MM-dd hh:mm:ss ddd");//设置系统时间显示格式
lineEdit->setText(str);//在标签上显示时间
int rand=qrand()%300;//坐标随机数
int rand2=qrand()%200;
lineEdit->move(rand,rand2);
}
五、事件总结
Qt 中有很多种事件:鼠标事件、键盘事件、大小改变的事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:
1. 所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择。
2. 每一种事件对应一个事件处理函数。
Qt 具有这么多种事件处理函数,肯定有一个地方对其进行分发,否则, Qt 怎么知道哪一种事件调用哪一个事件处理函数呢?这个分发的函数,就是 event()。显然,当 QMouseEvent产生之后, event()函数将其分发mouseEvent()事件处理器进行处理。
event()函数会有两个问题:
1. event()函数是一个 protected 的函数,这意味着我们要想重写 event(),必须继承一个已有的类。试想,我的程序根本不想要鼠标事件,程序中所有组件都不允许处理鼠标事件,是不是我得继承所有组件,一一重写其 event()函数? protected 函数带来的另外一个问题是,如果我基于第三方库进行开发,而对方没有提供源代码,只有一个链接库,其它都是封装好的。我怎么去继承这种库中的组件呢?
2. event()函数的确有一定的控制,不过有时候我的需求更严格一些:我希望那些组件根本看不到这种事件。event()函数虽然可以拦截,但其实也是接收到了 QMouseEvent对象。我连让它收都收不到。这样做的好处是,模拟一种系统根本没有那个事件的效果,所以其它组件根本不会收到这个事件,也就无需修改自己的事件处理函数。这种
需求怎么办呢?这两个问题是 event()函数无法处理的。于是, Qt 提供了另外一种解决方案:事件过滤器。
事件过滤器给我们一种能力,让我们能够完全移除某种事件。事件过滤器可以安装到任意QObject 类型上面,并且可以安装多个。如果要实现全局的事件过滤器,则可以安装到QApplication 或者 QCoreApplication 上面。这里需要注意的是,如果使用installEventFilter()函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,
只有这个对象的事件需要先传递给事件过滤器的 eventFilter()函数进行过滤,其它对象不受影响。如果QApplication 对象安装事件过滤器,那么该过滤器对程序中的每一个对象都有效,任何对象的事件都是先传给 eventFilter()函数。
事件过滤器可以解决刚刚我们提出的 event()函数的两点不足:首先,事件过滤器不是protected 的,因此我们可以向任何 QObject 子类安装事件过滤器;其次,事件过滤器在目标对象接收到事件之前进行处理,如果我们将事件过滤掉,目标对象根本不会见到这个事件。
Qt 的事件处理,实际上是有五个层次:
1. 重写 paintEvent()、 mousePressEvent() 等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。
2. 重写 event() 函数。 event() 函数是所有对象的事件入口, QObject 和 QWidget中的实现,默认是把事件传递给特定的事件处理函数。
3. 在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。
4. 在 QCoreApplication::instance() 上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和 notify() 函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。全局过滤器有一个问题:只能用在主线程。
5. 重写 QCoreApplication::notify() 函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication 是单例的)。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QWidget>
#include <QMouseEvent>
#include <QEvent>
#include <QObject>
/*
* 事件过滤器可以解决 event() 函数的两点不足:首先,事件过滤器不是
* protected 的,因此我们可以向任何 QObject 子类安装事件过滤器;其次,事件过滤器在目标
* 对象接收到事件之前进行处理,如果我们将时间过滤掉,目标对象根本不会见到这个事件。
*/
class Label:public QWidget{
public:
Label(){
installEventFilter(this);
}
bool eventFilter(QObject *watched, QEvent *event);
~Label(){}
protected:
void mousePressEvent(QMouseEvent *);
bool event(QEvent *e);
};
class EventFilter:public QObject{
private:
QObject *m_watched;
public:
EventFilter(QObject *watched,QObject *parent=0):QObject(parent),m_watched(watched){}
bool eventFilter(QObject *watched, QEvent *event);
~EventFilter(){}
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include <QDebug>
/*
* 第二是全局对象上面的事件过滤器
*/
bool Label::eventFilter(QObject *watched, QEvent *event){
if(watched==this){
if(event->type()==QEvent::MouseButtonPress){
qDebug()<<"eventFilter";
}
}
return false;
}
/*
* 最后才是特定的事件处理函数
*/
void Label::mousePressEvent(QMouseEvent *){
qDebug()<<"mousePressEvent";
}
/*
* 第三是重写的event函数
*/
bool Label::event(QEvent *e){
if(e->type()==QEvent::MouseButtonPress){
qDebug()<<"event";
}
return QWidget::event(e);
}
/*
* 全局事件过滤器最先被调用
*/
bool EventFilter::eventFilter(QObject *watched, QEvent *event){
if(watched==m_watched){
if(event->type()==QEvent::MouseButtonPress){
qDebug()<<"Application::eventFilter";
}
}
return false;
}
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Label label;
a.installEventFilter(new EventFilter(&label,&label));//安装过滤器
label.show();
return a.exec();
}