您的位置:首页 > 编程语言 > Qt开发

qt 事件机制

2015-10-08 20:56 260 查看
什么是自发事件?哪些类型的事件可以被propagated 或compressed? posting and sending 事件之间有何不同?什么时候应该调用 accept() 或是ignore() ? 如果这些问题你还不是很了解,那么继续看下去。

事件起源:

基于事件如何被产生与分发,可以把事件分为三类:

* Spontaneous 事件,由窗口系统产生,它们被放到系统队列中,通过事件循环逐个处理。

* Posted 事件,由Qt或是应用程序产生,它们被Qt组成队列,再通过事件循环处理。

* Sent  事件,由Qt或是应用程序产生,但它们被直接发送到目标对象。

当我们在main()函数的末尾调用QApplication::exec()时,程序进入了Qt的事件循环,大概来讲,事件循环如下面所示:

while (!exit_was_called)

{

  while(!posted_event_queue_is_empty)

       {

         process_next_posted_event();

       }

  while(!spontaneous_event_queue_is_empty)

      {

         process_next_spontaneous_event();

      }

  while(!posted_event_queue_is_empty)

      {

        process_next_posted_event();

      }

}

首先,事件循环处理所有的posted事件,直到队列空。然后再处理所有的spontaneous事件,最后它处理所有的因为处理spontaneous事件而产生的posted事件。send 事件并不在事件循环内处理,它们都直接被发送到了目标对象。现在看一下实践中的paint 事件是如何工作的。当一个widget第一次可见,或是被遮挡后再次变为可见,

窗口系统产生一个(spontaneous) paint事件,要求程序重画widget,事件循环最终从事件队列中捡选这个事件并把它分发到那个需要重画的widget。

并不是所有的paint事件都是由窗口系统产生的。当你调用QWidget::update()去强行重画widget,这个widget会post 一个paint 事件给自己。这个paint事件被放入队列,最终被事件循环分发之。

假如你很不耐烦,等不及事件循环去重画一个widget, 理论上,你应该直接调用paintEvent()强制进行立即的重画。但实际上这不总是可行的,因为paintEvent()函数是protected的(很可能访问不了)。它也绕开了任何存在的事件过滤器。因为这些原因,Qt提供了一个机制,直接sending事件而不是posting 。

QWidget::repaint()就使用了这个机制来强制进行立即重画。

posting 相对于sending的一个优势是,它给了Qt一个压缩(compress)事件的机会。假如你在一个widget上连续地调用update() 十次,因update()而产生的这十个事件,将会自动地被合并为一个单独的事件,但是QPaintEvents事件附带的区域信息也合并了。可压缩的事件类型包括:paint,move,resize,layout hint,language change。

最后要注意,你可以在任何时候调用QApplication::sendPostedEvent(),强制Qt产生一个对象的posted事件。

人工合成的事件

QT应用程序可以产生他们自己的事件,或是预定义类型,或是自定义类型。 这可以通过创建QEvent类或它的

子类的实例,并且调用QApplication:postEvent()或QApplication::sendEvent()来实现。

这两个函数需要一个 QObject* 与一个QEvent * 作为参数,假如你调用postEvent(),你必须用 new 操作符来创建事件对象,Qt会它被处理后帮你删除它。假如你用sendEvent(), 你应该在栈上来创建事件。下面举两个例子:

一是posting 事件:

QApplication::postEvent(mainWin, new QKeyEvent(QEvent::KeyPress,Key_X,'X',0));

二是sending 事件:

    QKeyEvent event(QEvent::KeyPress, Key_X, 'X', 0);

    QApplication::sendEvent(mainWin, &event);

Qt应用程序很少直接调用postEvent()或是sendEvnet(),因为大多数事件会在必要时被Qt或是窗口系统自动产生

。在大多数的情况下,当你想发送一个事件时,Qt已经为了准备好了一个更高级的函数来为你服务。(例如

update()与repaint())。

定制事件类型

qt允许你创建自己的事件类型,这在多线程的程序中尤其有用。在单线程的程序也相当有用,它可以作为

对象间的一种通讯机制。为什么你应该用事件而不是其他的标准函数调用,或信号、槽的主要原因是:事件既可用于同步也可用于异步(依赖于你是调用sendEvent()或是postEvents()),函数调用或是槽调用总是同步的。事件的另外一个好处是它可以被过滤。

演示如何post一个定制事件的代码片段:

const QEvent::Type MyEvent = (QEvent::Type)1234;

  ...

QApplication::postEvent(obj, new QCustomEvent(MyEvent));

事件必须是QCustomEvent类型(或子类)的。构造函数的参数是事件的类型,1024以下被Qt保留。其他可被程序使用。为处理定制事件类型,要重新实现customEvent()函数:

void MyLineEdit::customEvent(QCustomEvent *event)

    {

        if (event->type() == MyEvent) {

            myEvent();

        } else {

            QLineEdit::customEvent(event);

        }

    }

QcustomEvent类有一个void *的成员,可用于特定的目的。你也可以子类化QCustomEvent,加上别的成员,但是你也需要在customEvent()中转换QCustomeEvent到你特有的类型。

事件处理与过滤

Qt中的事件可以在五个不同的层次上被处理

1,重新实现一个特定的事件handler

 QObject与QWidget提供了许多特定的事件handlers,分别对应于不同的事件类型。(如paintEvent()对应paint事件)

2,重新实现QObject::event()

 event()函数是所有对象事件的入口,QObject和QWidget中缺省的实现是简单地把事件推入特定的事件handlers。

3,在QObject安装上事件过滤器

  事件过滤器是一个对象,它接收别的对象的事件,在这些事件到达指定目标之间。

4,在aApp上安装一个事件过滤器,它会监视程序中发送到所有对象的所有事件

5,重新实现QApplication:notify(),Qt的事件循环与sendEvent()调用这个函数来分发事件,通过重写它,你可以在别人之前看到事件。

一些事件类型可以被传递。这意味着假如目标对象不处理一个事件,Qt会试着寻找另外的事件接收者。用新的目标来调用QApplication::notify()。举例来讲,key事件是传递的,假如拥有焦点的Widget不处理特定键,Qt会分发相同的事件给父widget,然后是父亲的父亲,直到最顶层widget。

接受或是忽略?

可被传递的事件有一个accept()函数和一个ignore()函数,你可以用它们来告诉Qt,你“接收”或是

“忽略”这个事件。假如事件handler调用accept(),这个事件将不会再被传递。假如事件handler调用

ignore(),Qt会试着查找另外的事件接收者。

像大多数的开发者一样,你可能不会被调用accept()或是ignore()所烦恼。缺省情况下是“接收”,在

QWidget中的缺省实现是调用ignore(),假如你希望接收事件,你需要做的是重新实现事件handler,避免

调用QWidget的实现。假如你想“忽略”事件,只需简单地传递它到QWidget的实现。下面的代码演示了这一点:

void MyFancyWidget::keyPressEvent(QKeyEvent *event)

    {

        if (event->key() == Key_Escape) {

            doEscape();

        } else {

            QWidget::keyPressEvent(event);

        }

    }

在上面的例子里,假如用户按了"ESC"键,我们会调用doEscape()并且事件被“接收”了(这是缺省的情况),

事件不会被传递到父widget,假如用户按了别的键,则调用QWidget的缺省实现。

void QWidget::keyPressEvent(QKeyEvent *event)

    {

        event->ignore();

    }

应该感谢ignore(),事件会被传递到父widget中去。

讨论到目前为至,我们都假设基类是QWidget,然而,同样的规则也可以应用到别的层次中,只要用QWidget

代替基类即可。举例来说:

 void MyFancyLineEdit::keyPressEvent(QKeyEvent *event)

    {

        if (event->key() == Key_SysReq) {

            doSystemRequest();

        } else {

            QLineEdit::keyPressEvent(event);

        }

    }

由于某些原因,你会在event()中处理事件,而不是在特定的handler中,如keyPressEvent(),这个过程会有些不同。event() 会返回一个布尔值,来告诉调用者是否事件被accept或ignore,(true表示accept),从event()中调用accept()或是 ignore()是没有意义的。“Accept”标记是event()与特定事件handler之间的一种通讯机制。而从event()返回的布尔值却是用来与QApplication:notify()通讯的。在QWidgetk中缺省的event()实现是转换“Accept”标记为一个布尔值,如下所示:

bool QWidget::event(QEvent *event)

    {

        switch (e->type()) {

        case QEvent::KeyPress:

            keyPressEvent((QKeyEvent *)event);

            if (!((QKeyEvent *)event)->isAccepted())

                return false;

            break;

        case QEvent::KeyRelease:

            keyReleaseEvent((QKeyEvent *)event);

            if (!((QKeyEvent *)event)->isAccepted())

                return false;

            break;

            ...

        }

        return true;

    }

到现在为至,我们所说的内容不仅仅适用于key事件,也适用于mouse,wheel,tablet,context menu等事件

Close事件有点不同,调用QCloseEvent:ignore()取消了关闭操作,而accept()告诉Qt继续执行正常的关闭操作。为了避免混乱,最好是在closeEvent()的新实现中明确地进行accept()与ignore()的调用:

 void MainWindow::closeEvent(QCloseEvent *event)

    {

        if (userReallyWantsToQuit()) {

            event->accept();

        } else {

            event->ignore();

        }

    }vv

 

Qt程序是事件驱动的, 程序的每个动作都是由幕后某个事件所触发. Qt事件的类型很多, 常见的qt的事件如下:

键盘事件: 按键按下和松开.

鼠标事件: 鼠标移动,鼠标按键的按下和松开.

拖放事件: 用鼠标进行拖放.

滚轮事件: 鼠标滚轮滚动.

绘屏事件: 重绘屏幕的某些部分.

定时事件: 定时器到时.

焦点事件: 键盘焦点移动.

进入和离开事件: 鼠标移入widget之内,或是移出.

移动事件: widget的位置改变.

大小改变事件: widget的大小改变.

显示和隐藏事件: widget显示和隐藏.

窗口事件: 窗口是否为当前窗口.

还有一些非常见的qt事件,比如socket事件,剪贴板事件,字体改变,布局改变等等.

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

2. 事件产生和处理流程

2.1 事件的产生

事件的两种来源:

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

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

// 自定义事件的时候讲述: 需要注意的时, 这两个函数的使用方法不大一样, 一个是new, 一个是....

2.2 事件的调度

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

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.3 事件的派发和处理

首先说明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.4 事件的转发

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

3. 实际运用

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

3.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);

    }

}

3.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);

}

3.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;

}

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

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

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

Qt 是用QApplication::notify()函数来分发事件的.想要在任何事件过滤器查看任何事件之前先得到这些事件,重载这个函数是唯一的办法. 通常来说事件过滤器更好用一些, 因为不需要去继承QApplication类. 而且可以给QApplication对象安装任意个数的事
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  qt 事件机制 qt