亂談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()

來進行事件派發。前面的問題也都是由它派發的事件引發的。

相關文章
相關標籤/搜索