【Qt编程之Widgets模块】-008:QEvent类事件处理器

  • Post author:
  • Post category:其他




1 官方资料



1.1 概述

  • 在Qt中,事件都是从抽象类

    QEvent

    派生出来的对象。它们表示发生在应用程序内部或由于应用程序需要了解的外部活动而发生的事情。 事件可以由QObject子类的任何实例接收和处理,但它们与小部件尤其相关。 本文档介绍了在典型应用程序中如何传递和处理事件。



1.2 事件如何传递

  • 当事件发生时,Qt通过

    构造

    适当的

    QEvent子类的实例

    来创建一个

    事件对象

    来表示它,并通过调用

    事件函数

    将其传递到

    QObject的特定实例(或其子类之一)

  • 该函数不处理事件本身; 根据所传递事件的类型,它针对该特定事件类型调用事件处理程序,并根据事件被接受还是忽略发送响应。

  • 一些事件,例如

    QMouseEvent和QKeyEvent ,来自窗口系统

    。 一些,例如QTimerEvent ,来自定时器;有些来自应用程序本身,如自定义事件等。



1.3 事件类型

  • 大多数事件类型都有特殊的类,尤其是

    QResizeEvent



    QPaintEvent



    QMouseEvent



    QKeyEvent



    QCloseEvent

    。 每个类都将

    QEvent

    子类化,并添加特定于事件的函数。 例如,

    QResizeEvent

    添加了size()和oldSize()来使小部件能够发现其尺寸如何更改。



1.4 事件处理程序

  • 传递事件的通常方法是调用

    虚拟函数

    。 例如,通过调用

    QWidget :: paintEvent ()

    来传递

    QPaintEvent

    。 这个虚函数负责适当地做出反应,通常通过重新绘制窗口小部件。 如果您没有在虚拟函数的实现中执行所有必要的工作,则可能需要调用基类的实现。



1.4.1 closeEvent():


  • 窗口关闭

    时触发的事件,通常在此事件做窗口关闭时的一些处理,例如显示一个对话框询问是否关闭窗口。



1.4.2 showEvent():


  • 窗口显示

    时触发的事件。



1.4.3 paintEvent():


  • 窗口绘制

    事件。



1.4.4 mouseMoveEvent():


  • 鼠标移动

    事件。



1.4.5 mousePressEvent():


  • 鼠标键按

    下事件。



1.4.6 mouseReleaseEvent():


  • 鼠标键释放

    事件。



1.4.7 keyPressEvent():


  • 键盘按键按下

    事件。



1.4.8 keyReleaseEvent():


  • 键盘按键释放

    事件。



1.5 下面给出一个滚动字母例子

Ticker .h

#ifndef MY_TEST_H
#define MY_TEST_H
 
#include <QWidget>
 
class MainWindow: public QWidget
{
    Q_OBJECT
    Q_PROPERTY(QString text READ text WRITE setText)
public:
    MyTest(QWidget *parent = 0);
 
    void setText(const QString &newText);
    QString text() const { return myText; }
    QSize sizeHint() const;
 
    // 从新实现事件处理器:
protected:
   virtual void paintEvent(QPaintEvent *event);
   virtual void timerEvent(QTimerEvent *event);
   virtual void showEvent(QShowEvent *event);
   virtual void hideEvent(QHideEvent *event);
 
private:
    QString myText;
    int offset;
    int myTimerId;
};
 
#endif

实现代码

#include <QtGui> 
#include "MyTest.h"
 //构造函数吧offset变量初始化为0,。用来绘制文本x坐标就去自与这个offset数值。定时器的ID通常是非0的,所以可以使用0表示定时器还没有启动。
MainWindow::MainWindow(QWidget *parent)
    : QWidget(parent)
{
    offset = 0;
    myTimerId = 0;
}
 
void MainWindow::setText(const QString &newText)
{
    myText = newText;
    // 强制执行一个重绘操作。
    update();
    // 用于告知包含该widget的layout:该widget的size hint已发生变化,layout会自动进行调整
    updateGeometry();
}
 
QSize MainWindow::sizeHint() const
{
    return fontMetrics().size(0, text());
}
 
// update后会执行paintEvent事件
void MainWindow::paintEvent(QPaintEvent * /* event */)
{
    QPainter painter(this);
 
    int textWidth = fontMetrics().width(text());
    if (textWidth < 1)
        return;
    int x = -offset;
    while (x < width()) {
        painter.drawText(x, 0, textWidth, height(),
                         Qt::AlignLeft | Qt::AlignVCenter, text());
        x += textWidth;
    }
}
 
 // 
void MainWindow::showEvent(QShowEvent * /* event */)
{
	//函数用来提供一个定时器,QObject::startTimer()调用会返回一个ID数字,可以在以后用这个数字识别该定时器。调用之后,大约没30毫秒产生一个定时器事件,切记精度并不是实际上的30毫秒,与操作系统有关的嘞。
    myTimerId = startTimer(30);
}
 
 //系统每隔一定时间都会调用一次该函数,通过在offset上加1来模拟移动,从而形成文本宽度的连续滚动。然后,使用scroll()执行滚动一个像素的操作。
void MainWindow::timerEvent(QTimerEvent *event)
{
    if (event->timerId() == myTimerId) {
        ++offset;
        if (offset >= fontMetrics().width(text()))
            offset = 0;
        scroll(-1, 0);
    } else {
        QWidget::timerEvent(event);
    }
}
 
void MainWindow::hideEvent(QHideEvent * /* event */)
{
	// 可以用来停止定时器。
    killTimer(myTimerId);
    myTimerId = 0;
}



2 事件来源两类:

  • (一) 系统产生的;通常是window system把从系统得到的消息,比如鼠标按键,键盘按键等, 放入系统的消息队列中,Qt事件循环的时候读取这些事件,转化为QEvent,再依次处理.

  • (二)是由Qt应用程序程序自身产生的.程序产生事件有两种方式, 一种是调用QApplication::postEvent(). 例如QWidget::update()函数,当需要重新绘制屏幕时,程序调用update()函数,new出来一个paintEvent,调用 QApplication::postEvent(),将其放入Qt的消息队列中,等待依次被处理. 另一种方式是调用sendEvent()函数. 这时候事件不会放入队列, 而是直接被派发和处理, QWidget::repaint()函数用的就是这种方式。



2.1 事件是怎样被处理的?



2.1.1 两种调度方式,一种是同步的, 一种是异步.

Qt的事件循环是异步的,当调用QApplication::exec()时,就进入了事件循环. 该循环可以简化的描述为如下的代码:

while ( !app_exit_loop )

{

   while( !postedEvents ) { processPostedEvents() }

   while( !qwsEvnts ){ qwsProcessEvents();   }

   while( !postedEvents ) { processPostedEvents() }

}

先处理Qt事件队列中的事件, 直至为空. 再处理系统消息队列中的消息, 直至为空, 在处理系统消息的时候会产生新的Qt事件, 需要对其再次进行处理.

调用QApplication::sendEvent的时候, 消息会立即被处理,是同步的. 实际上QApplication::sendEvent()是通过调用QApplication::notify(), 直接进入了事件的派发和处理环节.



2.1.2 事件的派发和处理

首先说明Qt中事件过滤器的概念. 事件过滤器是Qt中一个独特的事件处理机制, 功能强大而且使用起来灵活方便. 通过它, 可以让一个对象侦听拦截另外一个对象的事件. 事件过滤器是这样实现的: 在所有Qt对象的基类: QObject中有一个类型为QObjectList的成员变量,名字为eventFilters,当某个QObjec (qobjA)给另一个QObject (qobjB)安装了事件过滤器之后, qobjB会把qobjA的指针保存在eventFilters中. 在qobjB处理事件之前,会先去检查eventFilters列表, 如果非空, 就先调用列表中对象的eventFilter()函数. 一个对象可以给多个对象安装过滤器. 同样, 一个对象能同时被安装多个过滤器, 在事件到达之后, 这些过滤器以安装次序的反序被调用. 事件过滤器函数( eventFilter() ) 返回值是bool型, 如果返回true, 则表示该事件已经被处理完毕, Qt将直接返回, 进行下一事件的处理; 如果返回false, 事件将接着被送往剩下的事件过滤器或是目标对象进行处理.

事件过滤器的代码实现:

这个代码实现了点击图片进行放大的功能(属于事件过滤器的操作实例)

Qt中,事件的派发是从 QApplication::notify() 开始的, 因为QAppliction也是继承自QObject, 所以先检查QAppliation对象, 如果有事件过滤器安装在qApp上, 先调用这些事件过滤器. 接下来QApplication::notify() 会过滤或合并一些事件(比如失效widget的鼠标事件会被过滤掉, 而同一区域重复的绘图事件会被合并). 之后,事件被送到reciver::event() 处理.

同样, 在reciver::event()中, 先检查有无事件过滤器安装在reciever上. 若有, 则调用之. 接下来,根据QEvent的类型, 调用相应的特定事件处理函数. 一些常见的事件都有特定事件处理函数, 比如:mousePressEvent(), focusOutEvent(), resizeEvent(), paintEvent(), resizeEvent()等等. 在实际应用中, 经常需要重载这些特定事件处理函数在处理事件. 但对于那些不常见的事件, 是没有相对应的特定事件处理函数的. 如果要处理这些事件, 就需要使用别的办法, 比如重载event() 函数, 或是安装事件过滤器.

事件派发和处理的流程图如下:



2.1.3 事件的转发

对于某些类别的事件, 如果在整个事件的派发过程结束后还没有被处理, 那么这个事件将会向上转发给它的父widget, 直到最顶层窗口. 如图所示, 事件最先发送给QCheckBox, 如果QCheckBox没有处理, 那么由QGroupBox接着处理, 如果QGroupBox没有处理, 再送到QDialog, 因为QDialog已经是最顶层widget, 所以如果QDialog不处理, QEvent将停止转发. 如何判断一个事件是否被处理了呢? Qt中和事件相关的函数通过两种方式相互通信. QApplication::notify(), QObject::eventFilter(), QObject::event() 通过返回bool值来表示是否已处理. “真”表示已经处理, “假”表示事件需要继续传递. 另一种是调用QEvent::ignore() 或 QEvent::accept() 对事件进行标识. 这种方式只用于event() 函数和特定事件处理函数之间的沟通. 而且只有用在某些类别事件上是有意义的, 这些事件就是上面提到的那些会被转发的事件, 包括: 鼠标, 滚轮, 按键等事件.



2.1.4 实际运用

根据对Qt事件机制的分析, 我们可以得到5种级别的事件过滤,处理办法. 以功能从弱到强, 排列如下:



2.1.4.1 重载特定事件处理函数.

最常见的事件处理办法就是重载象mousePressEvent(), keyPressEvent(), paintEvent() 这样的特定事件处理函数. 以按键事件为例, 一个典型的处理函数如下:

void imageView::keyPressEvent(QKeyEvent * event)

{

    switch (event->key()) {

    case Key_Plus:

        zoomIn();

        break;

    case Key_Minus:

        zoomOut();

        break;

    case Key_Left:

        // …

    default:

        QWidget::keyPressEvent(event);

    }

}



2.1.4.2 重载event()函数.

通过重载event()函数,我们可以在事件被特定的事件处理函数处理之前(象keyPressEvent())处理它. 比如, 当我们想改变tab键的默认动作时,一般要重载这个函数. 在处理一些不常见的事件(比如:LayoutDirectionChange)时,evnet()也很有用,因为这些函数没有相应的特定事件处理函数. 当我们重载event()函数时, 需要调用父类的event()函数来处理我们不需要处理或是不清楚如何处理的事件.

下面这个例子演示了如何重载event()函数, 改变Tab键的默认动作: (默认的是键盘焦点移动到下一个控件上. )

bool CodeEditor::event(QEvent * event)

{

    if (event->type() == QEvent::KeyPress)

    {

        QKeyEvent *keyEvent = (QKeyEvent *) event;

        if (keyEvent->key() == Key_Tab)

        {

            insertAtCurrentPosition('\t');

            return true;

        }

    }

    return QWidget::event(event);

}



2.1.4.3 在Qt对象上安装事件过滤器.

安装事件过滤器有两个步骤: (假设要用A来监视过滤B的事件)

首先调用B的installEventFilter( const QOject *obj ), 以A的指针作为参数. 这样所有发往B的事件都将先由A的eventFilter()处理.

然后, A要重载QObject::eventFilter()函数, 在eventFilter() 中书写对事件进行处理的代码.

用这种方法改写上面的例子: (假设我们将CodeEditor 放在MainWidget中)

MainWidget::MainWidget()

{

    CodeEditor * ce = new CodeEditor( this, “code editor”);

    ce->installEventFilter( this );

}

bool MainWidget::eventFilter( QOject * target , QEvent * event )

{

   if( target == ce )

   {

       if( event->type() == QEvent::KeyPress )

       {

             QKeyEvent *ke = (QKeyEvent *) event;

             if( ke->key() == Key_Tab )

             {

                ce->insertAtCurrentPosition('\t');

                return true;

             }

      }

   }

   return false;

}



2.1.4.4 给QAppliction对象安装事件过滤器.

一旦我们给qApp(每个程序中唯一的QApplication对象)装上过滤器,那么所有的事件在发往任何其他的过滤器时,都要先经过当前这个 eventFilter(). 在debug的时候,这个办法就非常有用, 也常常被用来处理失效了的widget的鼠标事件,通常这些事件会被QApplication::notify()丢掉. ( 在QApplication::notify() 中, 是先调用qApp的过滤器, 再对事件进行分析, 以决定是否合并或丢弃)



2.1.5 继承QApplication类,并重载notify()函数.

Qt 是用QApplication::notify()函数来分发事件的.想要在任何事件过滤器查看任何事件之前先得到这些事件,重载这个函数是唯一的办法. 通常来说事件过滤器更好用一些, 因为不需要去继承QApplication类. 而且可以给QApplication对象安装任意个数的事



2.2 事件与信号的区别?

Qt 的事件和Qt中的signal不一样. 后者通常用来”使用”widget, 而前者用来”实现” widget. 比如一个按钮, 我们使用这个按钮的时候, 我们只关心他clicked()的signal, 至于这个按钮如何接收处理鼠标事件,再发射这个信号,我们是不用关心的。但是如果我们要重载一个按钮的时候,我们就要面对event了。 比如我们可以改变它的行为,在鼠标按键按下的时候(mouse press event) 就触发clicked()的signal而不是通常在释放的( mouse release event)时候.。

信号通过事件实现,事件可以过滤,事件更底层,事件是基础,信号是扩展。



3 问题排查



3.1 界面假死或不刷新的处理方式

  • 有时会遇到这样一种情况,比如界面最小化或者界面关闭后(进程未退出),当重新显示时界面确是一片白的,原因是paintEvent并被未调用,导致界面未刷新。如果手动改变下界面大小时又可以正常显示了,因为此操作调用了paintEvent,刷新了界面。


解决办法

  • 怎么办呢,处理也比较简单,重写下showEvent事件函数就行:
void MainWindow::showEvent(QShowEvent *event)
{
    setAttribute(Qt::WA_Mapped);
    QMainWindow::showEvent(event);
}


注意:需要在每次界面显示时调用下setAttribute(Qt::WA_Mapped)才行。

  • 后经大量测试发现,这样还是有一定几率出现假死现象,于是加两行代码:
void MainWindow::showEvent(QShowEvent *event)
{
    setAttribute(Qt::WA_Mapped);
    QMainWindow::showEvent(event);

    QSize oldSize = this->size();
    resize(oldSize + QSize(10, 10));
    resize(oldSize);
}
  • 这样做的目的就是确保paintEvent会被执行,双重保险。



版权声明:本文为Windgs_YF原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。