zoukankan      html  css  js  c++  java
  • 乱谈Qt事件循环嵌套

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

    引子

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

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

    但是在同一个线程内,我们可以开启多个事件循环,比如通过:

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

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

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

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

    如何演示?

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

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

    #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();
    }

    然后我们可以期待看到:

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

    但是,很让人失望,这并不会工作。因为Qt对Timer事件派发时进行了处理:

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

    演示

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

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

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

    #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()

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

  • 相关阅读:
    出现,视图必须派生自 WebViewPage 或 WebViewPage错误解决方法
    未能加载文件或程序集“Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad
    快速调试的VS设置
    WebAPI GET和POST请求的几种方(转发)
    Windows无法启动SQL server 代理服务(服务器)错误1067:进程意外终止
    LC.exe exited with code -1 报错
    Linq 合并数据并相加
    C#事务
    vs2013发布时: sgen.exe 已退出 代码为 1
    使用Jenkins部署.Net应用程序
  • 原文地址:https://www.cnblogs.com/lvdongjie/p/3758184.html
Copyright © 2011-2022 走看看