乱谈Qt事件循环嵌套

  • 本文旨在说明:QDialog::exec()、QMenu::exec()等开启的局部事件循环,易用的背后,还有不少的陷阱...

引子

Qt 是事件驱动的,基本上,每个Qt程序咱们都会经过QCoreApplication或其派生类的exec()函数来开启事件循环(QEventLoop):node

int main(int argc, char**argv)
{
    QApplication a(argc, argv);
    return a.exec(); 
}

可是在同一个线程内,咱们能够开启多个事件循环,好比经过:app

  • QDialog::exec()
  • QDrag::exec()
  • QMenu::exec()
  • ...

这些东西都很经常使用,不是么?它们每个里面都在执行这样的语句:异步

QEventLoop loop;     //事件循环
loop.exec();

既然是同一线程内,这些显然是没法并行运行的,那么只能是嵌套运行。ide

如何演示?

如何用最小的例子来直观说明这个问题呢?函数

利用定时器来演示应该是最方便的。因而,很容易写出来这样的代码:oop

#include <QtCore>

class Object : public QObject
{
public:
    Object() {startTimer(200); }

protected:
    void timerEvent(QTimerEvent *) {
        static int level = 0;
        qDebug()<<"Enter: <<"++level;
        QEventLoop loop;     //事件循环
        loop.exec();
        qDebug()<<"Leave: "<<level;
    }
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Object w;
    return a.exec();
}

而后咱们能够期待看到:post

Enter: 1
Enter: 2
Enter: 3
...

可是,很让人失望,这并不会工做。由于Qt对Timer事件派发时进行了处理:ui

  • 若是当前在处理Timer事件,新的Timer将不会被派发。

演示

咱们对这个例子进行一点改进:this

  • 收到Timer事件后,咱们当即post一个自定义事件(而后咱们在对自定义事件的相应中开启局部的事件循环)。这样Timer事件的相应能够当即返回,新的Timer事件能够持续被派发。

另外,为了友好一点,使用了 QPlainTextEdit 来显示结果:spa

#include <QtGui>
#include <QtCore>

class Widget : public  QPlainTextEdit
{
public:
    Widget() {startTimer(200); }

protected:
    bool event(QEvent * evt)
    {
        if (evt->type() == QEvent::Timer) {
            qApp->postEvent(this, new QEvent(QEvent::User));
        } else if (evt->type() == QEvent::User) {
            static int level = 0;
            level++;
            this->appendPlainText(QString("Enter : %1").arg(++level));
            QEventLoop loop;
            loop.exec();
            this->appendPlainText(QString("Leave: %1").arg(level));
        }
        return QPlainTextEdit::event(evt);
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

有什么用?

这个例子确实没有什么,由于彷佛没人会写这样的代码。

可是,当你调用

  • QDialog::exec()
  • QMenu::exec()
  • QDrag::exec()
  • ...

等函数时,实际上就启动了嵌套的事件循环,而若是不当心的话,还有遇到各类怪异的问题!

== QDialog::exec() vs QDialog::open()== 在 QDialog 模态对话框与事件循环 以及 漫谈QWidget及其派生类(四) 咱们解释过QDialog::exec()。它最终就是调用QEventLoop::exec()。

QDialog::exec()这个东西是这么经常使用,以致于咱们不多考虑这个东西的不利因素。QDialog::open()尽管被官方所推荐,可是彷佛不多有人用,不少人可能还不知道它的存在。

可是Qt官方blog中:

一文介绍了 exec() 可能形成的危害,并鼓励你们使用 QDialog::open()

在Qt官方的Qt Quarterly中: * QtQuarterly30 之 New Ways of Using Dialogs 对QDialog::open()有详细的介绍

QDialog::open()劣势与优点

看个例子:咱们经过颜色对话框选择一个颜色,

  • 使用静态函数,写法很简介(内部调用exec()启动事件循环)

 

void Widget::onXXXClicked()
{
    QColor c = QColorDialog::getColor();
}
  • 对话框的常见用法,使用exec()启动事件循环,很直观

 

void Widget::onXXXClicked()
{
    QColorDialog dlg(this);
    dlg.exec();
    QColor c = dlg.currentColor();
}
  • 使用open()函数,比较不直观(由于是异步,须要连接一个槽)

 

void Widget::onXXXClicked()
{
    QColorDialog *dialog = new QColorDialog;
    dialog->open(this, SLOT(dialogClosed(QColor)));
}
void Widget::dialogClosed(const QColor &color)
{
    QColor = color;
}

好处嘛(就摘录Andreas Aardal Hanssen的话吧):

  • By using open() instead of exec(), you need to write a few more lines of code (implementing the target slot). But what you gain is very significant: complete control over execution. Now, the event loop is no longer nested/reentered, you’re not blocking inside a magic exec() function

局部事件循环致使崩溃

Kde开发者官方blog中描述这个问题:

在某个槽函数中,咱们经过QDialog::exec() 弹出一个对话框。

void ParentWidget::slotDoSomething() 
{
    SomeDialog dlg( this ); //分配在栈上的对话框
    if (dlg.exec() == QDialog::Accepted ) {
        const QString str = dlg.someUserInput();
        //do something with with str
    }
}

若是这时ParentWidget或者经过其余方式(好比dbus)获得通知,须要被关闭。会怎么样?

程序将崩溃:ParentWidget析构时,将会delete这个对话框,而这个对话框却在栈上。

简单模拟一下(在上面代码中加一句便可):

void ParentWidget::slotDoSomething() 
{
    QTimer::singleShot(1000, this, SLOT(deleteLater()));
...

这篇blog最终给出的结论是:将对话框分配到堆上,并使用QPointer来保存对话框指针。

上面的代码,大概要写成这样:

void ParentWidget::slotDoSomething() 
{
    QWeakPointer<SomeDialog> dlg = new SomeDialog(this);
    if (dlg.data()->exec() == QDialog::Accepted ) {
        const QString str = dlg.data()->someUserInput();
        //do something with with str
    } else if(!dlg) {
        //....
    }
    if (!dlg) {
        delete dlg.data();
    }
}

感兴趣的能够去看看原文。比较起来 QDialog::open() 应该更值得考虑。

QCoreApplication::sendPostedEvents()

当程序作繁重的操做时,而又不肯意开启一个新线程时,咱们都会选择调用

 QCoreApplication::sendPostedEvents ()

来使得程序保持相应。这和前面提到的哪些exec()开启局部事件循环的效果实际上是彻底同样的。

不管是这个,仍是QEventLoop::exec()最终都是调用:

QAbstractEventDispatcher::processEvents()

来进行事件派发。前面的问题也都是由它派发的事件引发的。

相关文章
相关标签/搜索