QT事件系统

事件发生时,Qt创建一个事件对象,传递给QObject的event()函数,event()函数并不直接处理事件,而是对这些事件按照它们不同的类型分发给不同的事件处理函数。

event()函数主要用于事件的分发,如果要在事件分发之前做一些操作,就需要重写event()函数。

事件作为一个对象,继承自QEvent类,常见的有键盘事件QKeyEvent、鼠标事件QMouseEvent和定时器事件QTimerEvent等。

QEvent类关系如下:

QEvent:

    QDropEvent:

        QDragMoveEvent:

            QDragEnterEvent

    QCloseEvent

    QPaintEvent

    QInputEvent:

        QContextMenuEvent、

        QKeyEvent、

        QMouseEvent、

        QWheelEvent

    QResizeEvent

    QTimerEvent

事件是对各种应用程序需要知道的由应用程序内部或者外部产生的事情或者动作的通称。在Qt中,任何QObject子类实例都可以接收和处理事件。

 

事件的处理

一个事件由一个特定的QEvent子类来表示,但是有时一个事件又包含多个事件类型,比如鼠标事件又可以分为鼠标按下、双击和移动等多种操作。这些事件类型都由QEvent类的枚举类型QEvent::Type来表示,其中包含了一百多种事件类型,可以在QEvent类的帮助文档中进行查看。

QCoreApplication类的notify()函数的帮助文档给出了5种处理事件的方法:

方法一:重新实现部件的paintEvent()、mousePressEvent()等事件处理函数,这是最常用的一种方法,不过只能用来处理特定部件的特定事件。

方法二:重新实现notify()函数。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。这个方法,下文没提及。

方法三:向Application对象上安装事件过滤器。因为一个程序只有一个QApplication对象,所以这样实现的功能与使用notify()函数是相同的,优点是可以同时处理多个事件。

方法四:重新实现event()函数。QObjcet类的event()函数可以在事件到达默认的事件处理函数之前获得该事件。

方法五:在对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。

在实际编程中,最常用的是方法一,其次是方法五。因为方法二需要继承自QApplication类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递,所以,虽然这两种方法功能很强大,但是却很少被用到。

 

事件的传递

每个程序main()函数的最后都会调用QApplication类的exec()函数,它会使Qt应用程序进入事件循环,这样就可以使应用程序在运行时接收发送的各种事件。一旦有事件发生,Qt便会构建一个相应的QEvent子类的对象来表示它,然后将它传递给相应的QObject对象或其子对象。

#include<QKeyEvent>
protected:
    void keyPressEvent(QKeyEvent *event);

void MyLineEdit::KeyPressEvent(QKeyEvent *event)//键盘按下事件
{
    qDebug()<<tr("MyLineEdit键盘按下事件");
    QLineEdit::keyPressEvent(event);//执行QLineEdit类的默认事件处理
    event->ignore();//ignore()函数要在最后调用,忽略该事件,这样就能让父部件也接收到事件了
}

void Widget::KeyPressEvent(QKeyEvent *event)//键盘按下事件
{
    qDebug()<<tr("MyLineEdit键盘按下事件");
}

//学一下代码写组件的技巧
widget.h
MyLineEdit *lineEdit;


widget.cpp

lineEdit=new MyLineEdit(this);//继承关联

lineEdit->move(100,100);//这里控位置

事件是先传递给指定窗口部件的,确切地说应该是先传递给获得焦点的窗口部件,但是如果该部件忽略掉该事件,那么这个事件就会传递给这个部件的父部件。重新实现事件处理函数时,一般要调用父类的相应事件处理函数来实现默认操作。(事件传递是先子类再父类)

 

下面试着加一个过滤器:

myLineEdit.h
bool event(QEvent *event);

myLineEdit.cpp
bool MyLineEdit::event(QEvent *event)//事件,这里用了方法四,重写部件的event
{
    if(event->type()==QEvent::KeyPress)
        qDebug()<<tr("MyLineEdit的event()函数");
    return QLineEdit::event(event);//执行QLineEdit类event()函数的默认操作
}
//因为event()函数具有bool型的返回值,所以该函数的最后要使用return语句,这里一般时返回父类的event()函数的操作结果。

widget.h
bool eventFilter(QObject *obj,QEvent *event);

widget.cpp
//在构造函数里
lineEdit->installEventFilter(this);//在Widget上为lineEdit安装事件过滤器

bool Widget::eventFilter(QObject *obj,QEvent *event)//事件过滤器,方法五
{
    if(obj==lineEdit)//如果是lineEdit部件上的事件
    {
        if(event->type()==QEvent::KeyPress)
            qDebug()<<tr("Widget的事件过滤器");
    }
    return QWidget::eventFilter(obj,event);
}
//在事件过滤器中,先判断该事件的对象是不是lineEdit,如果是,再判断事件类型。最后返回了QWidget类默认的事件过滤器的执行结果。

 

事件的传递顺序是这样的,先是事件过滤器,然后是焦点部件的event()函数,最后是焦点部件的事件处理函数;如果焦点部件忽略了该事件,那么会执行父部件的事件处理函数。注意,event()函数和事件处理函数是在焦点部件内重新定义的,而事件过滤器却是在焦点部件的父部件中定义的

键盘按下事件->eventFilter()->MyLineEdit(event()->keyPressEvent())->keyPressEvent()

 

鼠标事件和滚轮事件

通常是重定义部件的鼠标事件处理函数来进行一些自定义的操作。

widget.h
#include<QMouseEvent>
protected:
    void mousePressEvent(QMouseEvent *event);

private:
    QPoint offset;//用来储存鼠标指针位置与窗口位置的差值

widget.cpp
//在构造函数中
QCursor cursor;//创建光标对象
cursor.setShape(Qt::OpenHandCursor);//设置光标形状,小手掌形状,可以在帮助中通过Qt::CursorShape关键字查看
setCursor(cursor);//使用光标
void Widget::mousePressEvent(QMouseEvent *event)//鼠标按下事件
{
    if(event->button()==Qt::LeftButton)//如果是鼠标左键按下
    {
        QCursor cursor;
        cursor.setShape(Qt::CloseHandCursor);
        QApplication::setOverrideCursor(cursor);//使鼠标指针暂时改变形状,restoreOverrideCursor()可解
        offset=event->globalPos()-pos();//获取指针位置和窗口位置的差值,这里使用了globalPos()函数来获取鼠标指针的位置,这个位置是指针在桌面上的位置,因为窗口的位置就是指它在桌面上的位置。另外,还可以使用QMouseEvent类的pos()函数获取鼠标指针在窗口中的位置。
    }
    else if(event->button()==Qt::RightButton)//如果使鼠标右键按下
    {
        QCursor cursor(QPixmap("../mymouseevent/logo.pbg"));
        QApplication::setOverrideCursor(cursor);//使用自定义的图片作为鼠标指针
    }
}

void Widget::mouseMoveEvent(QMouseEvent *event)//鼠标移动事件
{
    if(event->buttons()&Qt::LeftButton)//这里必须使用buttons,因为鼠标移动会检测所有按下的键,而这时使用QMouseEvent的button()函数无法获取哪个按键被按下,只能使用buttons()函数,所以这里使用buttons()和Qt::LeftButton进行按位与的方法来判断是否是鼠标左键按下。
    {
        QPoint temp;
        temp=event->globalPos()-offset;
        move(temp);//使用鼠标指针当前的位置减去差值,就得到了窗口应该移动的位置
    }
}

void Widget::mouseReleaseEvent(QMouseEvent *event)//鼠标释放事件
{
    Q_UNUSED(event);
    QApplication::restoreOverrideCursor();//恢复鼠标指针形状
}

void mouseDoubleClickEvent(QMouseEvent *event)//鼠标双击事件
{
    if(event->button()==Qt::LeftButton)//如果是鼠标左键按下
        if(windowState()!=Qt::WindowFullScreen)//如果现在不是全屏
            setWindowState(Qt::WindowFullScreen);//将窗口设置为全屏
        else setWindowState(Qt::WindowNoState);//否则恢复以前的大小
}

void wheelEvent(QWheelEvent *event)//滚轮事件
{
    if(event->delta()>0)//当滚轮远离使用者时
        ui->textEdit->zoomIn();//进行放大
    else//当滚轮向使用者方向旋转时
        ui->textEdit->zoomOut();//进行缩小
}

注意button()和buttons()的区别。

Qt::MouseButton QMouseEvent::button() const    返回产生事件的按钮

Qt::MouseButton QMouseEvent::buttons() const   返回产生事件的按钮状态,函数返回当前按下的所有按钮,按钮状态可以是Qt::LeftButton,Qt::RightButton,Qt::MidButton或运算组合

假设鼠标左键已经按下,

如果移动鼠标,会发生的move事件,button返回Qt::NoButton,buttons返回LeftButton。

再按下了右键,会发生press事件,button返回RightButton,buttons返回LeftButton | RightButton

再移动鼠标,会发生move事件,button返回Qt::NoButton,buttons返回LeftButton | RightButton

再松开左键,会发生Release事件,button返回LeftButton,buttons返回RightButton

也就是说,button返回“那个按钮发生了此事件”,buttons返回"发生事件时哪些按钮还处于按下状态"

 

在滚轮事件处理函数中,使用QWheelEvent类的delta()函数获取了滚轮移动的距离,每当滚轮转一下,默认是15°,这时delta()函数就会返回15*8即整数120。当滚轮向远离使用者的方向旋转时,返回正值;当向靠近使用者的方向旋转时,返回负值。

默认时当按下鼠标按键时移动鼠标,鼠标移动事件才会产生;如果想不按鼠标按键,也可以获得鼠标移动事件,那么就要在构造函数中添加下面一行代码:

setMouseTracking(true);//设置鼠标跟踪

这样便会开启窗口部件的鼠标跟踪功能。

 

键盘事件

QKeyEvent类用来描述一个键盘事件。当键盘按钮被按下或者被释放时,键盘事件便会被发送给拥有键盘输入焦点的部件。

具体按键可以在帮助中通过Qt::Key关键字查看。

需要特别说明的是,回车键在这里是Qt::Key_Return键盘上的一些修饰键,比如Ctrl和Shift等,这里需要使用QKeyEvent的modifiers()函数来获取,可以在帮助中使用Qt::Keyboard-Modifier关键字来查看所有的修饰键。

widget.h
#include<QKeyEvent>
protected:
    void keyPressEvent(QKeyEvent *event);
    void keyReleaseEvent(QKeyEvent *event);

widget.cpp
void Widget::keyPressEvent(QKeyEvent *event)//键盘按下事件
{
    if(event->modifiers()==Qt::ControlModifier)//是否按下Ctrl键
        if(event->key()==Qt::key_M)//是否按下M键
            setWindowState(Qt::WindowMaximized);//窗口最大化
    else QWidget::keyPressEvent(event);
}

void Widget::keyReleaseEvent(QKeyEvent *event)//按键释放事件
{
    //其他操作
}

从网上查资料发现,Qt的按键消息响应keyPressEvent()表现为按住一个键时,会先响应一次,停顿一会,然后开始不断响应。

而且Qt的键盘函数有一个问题,它不是“按下按键才触发keyPressEvent(), 弹起按键才触发keyReleaseEvent()",而是”输出按键消息前触发keyPressEvent(),输出后触发keyReleaseEvent()“,表现为按住一个键时,不断地press、release、press、release、press、release......

Qt的QKeyEvent类中提供isAutoRepeat()判断,即按住按键时触发的那些键盘事件isAutoRepeat类型,可以据此排除中间触发的那些press和release

综上所述,Qt的键盘事件整体表现为,按住一个键时:

1、第一次触发keyPressEvent(), isAutoRepeat()返回false

2、没有触发keyReleaseEvent(),停顿一会

3、再一次触发keyPressEvent(),isAutoRepeat()返回true

4、触发keyReleaseEvent()

5、若没松开按键,isAutoRepeat()返回true,返回第3步;若松开按键,isAutoRepeat()返回false

 

验证上面的例子如下:

widget.cpp
//构造函数中
setFocus();//使主界面获得焦点
void Widget::keyPressEvent(QKeyEvent *event)//键盘按下事件
{
    if(event->key()==Qt::Key_Up)//如果是向上方向键
        qDebug()<<"press:"<<event->isAutoRepeat();//是否自动重复
}

void Widget::keyReleaseEvent(QKeyEvent *event)//键盘释放事件
{
    if(event->key()==Qt::Key_Up)//如果是向上方向键
    {
        qDebug()<<"press:"<<event->isAutoRepeat();//是否自动重复
        qDebug()<<"up";
    }
}

如果要想实现两个普通按键同时按下,就要避免按键的自动重复。比如,同时按下向上和向左方向键,按钮会向左上方移动。

widget.h
private:
    bool keyUp;//向上方向键按下的标志
    bool keyLeft;//向左方向键按下的标志
    bool move;//是否完成了一次移动

widget.cpp
//在构造函数里
    keyUp=false;//初始化变量
    keyLeft=false;
    move=false;

void Widget::keyPressEvent(QKeyEvent *event)//键盘按下事件
{
    if(event->key()==Qt::Key_Up)
    {
        if(event->isAutoRepeat()) return;//按键重复时不做处理
        keyUp=true;//标记向上方向键已经按下
    }
    else if(event->key()==Qt::Key_Left)
    {
        if(event->isAutoRepeat()) return;//按键重复时不做处理
        keyLeft=true;//标记向左方向键已经按下
    }
}

void Widget::keyReleaseEvent(QKeyEvent *event)//按键释放事件
{
    if(event->key()==Qt::Key_Up)
    {
        if(event->isAutoRepeat()) return;
        keyUp=false;//释放按键后将标志设置为false
        if(move)//如果已经完成了移动
        {
            move=false;//设置标志为false
            return;//直接返回
        }
        if(keyLeft)//如果向左方向键已经按下且没有释放
        {
            ui->pushButton->move(30,80);//斜移
            move=true;//标记已经移动
        }
        else//否则上移
            ui->pushButton->move(120,80);
    }
    else if(event->key()==Qt::Key_Left)
    {
      if(event->isAutoRepeat()) return;
      keyLeft=false;//释放按键后将标志设置为false
      if(move)//如果已经完成了移动
      {
          move=false;//设置标志为false
          return;//直接返回
      }
      if(keyUp)//如果向上方向键已经按下且没有释放
      {
          ui->pushButton->move(30,80);//斜移
          move=true;//标记已经移动
      }
      else//否则上移
          ui->pushButton->move(30,120);
  }
  else if(event->key()==Qt::Key_Down)
    ui->pushButton->move(120,120);//使用向下方向键来还原按钮的位置
}

关键点在于,当move为真时,这里将不再进行操作,然后将move标记为假。这样就完成了整个斜移操作,而且所有的标志又恢复到了操作前的状态。

 

定时器事件与随机数

QTimerEvent类用来描述一个定时器事件。对于一个QObject的子类,只需要使用int QObject::startTimer(int interval)函数就可以开启一个定时器,这个函数需要输入一个以毫秒为单位的整数作为参数来表明设定的时间,函数返回一个整型编号来代表这个定时器。当定时器溢出时可以在timerEvent()函数中进行需要的操作。

其实编程中更多的是使用QTimer类来实现一个定时器,它提供了更高层次的编程接口,比如可以使用信号和槽,还可以设置只运行一次的定时器。关于定时器的介绍,可以在帮助中通过Timers关键字查看。

关于随机数,Qt中是使用qrand()和qsrand()两个函数实现的。

widget.h
protected:
void timerEvent(QTimerEvent *event);
private:
int id1,id2,id3;

widget.cpp
//构造函数中
id1=startTimer(1000);
id2=startTimer(1500);
id3=startTimer(2000);

void Widget::timeEvent(QTimerEvent *event)
{
    if(event->timerId()==id1)//判断是哪个定时器
        qDebug()<<"timer1";
    else if(event->timerId()==id2)//判断是哪个定时器
        qDebug()<<"timer2";
    else
        qDebug()<<"timer3";
}

 

下面使用QTimer类实现一个简单的电子表。

在界面上添加一个LCD Number部件。

widget.h
private slots:
    void timerUpdate();

widget.cpp
#include<QTimer>
#include<QTime>
//构造函数中
QTimer *timer=new QTimer(this);//创建一个新的定时器
//关联定时器的溢出信息到槽上
connect(timer,&QTimer::timeout,this,&Widget::timerUpdate);
timer->start(1000);//设置溢出时间为1s,并启动定时器,想停止可以调用它的stop()函数


void Widget::timerUpdate()//定时器溢出处理
{
    QTime time=QTime::currentTime();//获取当前时间
    QString text=time.toString("hh:mm");//转换为字符串
    if((time.second()%2)==0) text[2]=' ';//每隔一秒就将":"显示为空格
    ui->lcdNumber->display(text);
}

 

下面是随机数的使用:

widget.cpp
//构造函数
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
//在timerUpdate()函数里添加
int rand=qrand()%300;//产生300以内的正整数
ui->lcdNumber->move(rand,rand);

在使用qrand()函数产生随机数之前,一般要使用qsrand()函数为其设置初值,如果不设置初值,那么每次运行程序qrand()都会产生相同的一组随机数。为了每次运行程序时都可以产生不同的随机数,那么就要使用qsrand()设置一个不同的初值,这里使用了QTime类的secsTo()函数,它表示两个时间点之间所包含的秒数,比如代码中就是指从零点整到当前时间所经过的秒数。

QTimer类中还有一个singleShot()函数来开启一个只运行一次的定时器,下面使用这个函数让程序运行10s后自动关闭。

QTimer::singleShot(10000,this,&Widget::close);

 

事件过滤器与事件的发送

Qt中提供了事件过滤器来实现在一个部件中监控多个部件的事件。事件过滤器与其他部件不同,它不是一个类,只是两个函数组成的一种操作,用来完成一个部件对其他部件的事件的监视。这两个函数分别是installEventFilter()和eventFilter(),都是QObject类中的函数。

示例如下,向界面上拖入一个Text Edit和一个Spin Box。

widget.h
public:
    bool eventFilter(QObject *obj,QEvent *event);

widget.cpp
#include<QKeyEvent>
#include<QWheelEvent>
//在构造函数中
ui->textEdit->installEventFilter(this);//为编辑器部件在本窗口上安装事件过滤器
ui->spinBox->installEventFilter(this);//其参数this表明要在本部件(即Widget)中监视textEdit和spinBox的事件。

bool eventFilter(QObject *obj,QEvent *event)//事件过滤器
{
    if(obj==ui->textEdit)//判断部件
    {
        if(event->type()==QEvent::Wheel)//判断事件
        {//将event强制转换为发生的事件的类型
            QWheelEvent *wheelEvent=static_cast<QWheelEvent*>(event);
            if(wheelEvent->delta()>0) ui->textEdit->zoomIn();
            else ui->textEdit->zoomOut();
            return true;//该事件已被处理
        }
        else return false;//如果是其他事件,可以进行进一步的处理
    }
    else if(obj==ui->spinBox)
    {
        if(event->type()==QEvent::KeyEvent)
        {
            QKeyEvent *keyEvent=static_cast<QKeyEvent *>(event);
            if(keyEvent->key()==Qt::Key_Space)
            {
                ui->spinBox->setValue(0);
                return true;
            }
            else return false;
        }
    }
    else return QWidget::eventFilter(obj,event);
}

这里需要说明,如果要对一个特定的事件进行处理,而且不希望它在后面的传递过程中再被处理,那么就返回true,否则返回false。

可以看到,使用事件过滤器可以很容易地处理多个部件的多个事件,如果不使用它,那么就得分别子类化各个部件,然后重新实现它们对应的各个事件处理函数,那样就会很麻烦。

 

Qt中也提供了发送一个事件的功能,它由QCoreApplication类的

bool QCoreApplication::sendEvent(QObject *receiver,QEvent *event)

或者

void QCoreApplication::postEvent(QObject *receiver,QEvent *event,int priority=Qt::NormalEventPriority)

函数来实现。

这两个函数的主要区别是:sendEvent()会立即处理给定的事件,而postEvent()则会将事件放到等待调度队列中,当下一次Qt的主事件循环运行时才会处理它。这两个函数还有其他一些区别,比如sendEvent()中的QEvent对象参数在事件发送完成后无法自动删除,所以需要在栈上创建QEvent对象;而postEvent()中的QEvent对象参数必须在堆上进行创建(例如使用new),当事件被发送后事件队列会自动删除它。这两个函数更多的介绍可以参考它们的帮助文档。

下面添加代码来向spinBox部件发送一个向上方向键被按下的事件:

QKeyEvent myEvent(QEvent::KeyPress,Qt::Key_Up,Qt::NoModifier);

qApp->sendEvent(ui->spinBox,&myEvent);//发送键盘事件到1spinBox部件

这里使用了sendEvent()函数,其中,QKeyEvent对象是在栈上创建的。这里的qApp是QApplication对象的全局指针,每一个应用程序只能使用一个QApplication对象,等价于使用QApplication::sendEvent()。

Qt中还可以使用自定义的事件,这个需要继承QEvent类,可以通过帮助中The Event System关键字中的相关内容查看。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值