c++qtqtconcurrent

Temporarily disable window during a function call


Consider:

class Worker
{
public:
  void DoWork();
};

class MainWindow : public QMainWindow
{
Q_OBJECT

private:
  Worker MyWorker;

public:
  void DoWork();
};

I want the DoWork function of my MainWindow to behave like:

void MainWindow::DoWork()
{
  setEnabled(false);

  std::future<void> Resu = std::async(std::launch::async, &Worker::DoWork, &MyWorker);

  while (Resu.wait_for(100ms) == std::future_status::timeout)
    qApp->processEvents();

  try
    { Resu.get(); }
  catch (Except const &MyExcept)
    { QMessageBox::critical(this, "Error", MyExcept.What()); }

   setEnabled(true);
 }

This works as I want: (i) the window is disabled while doing the work, (ii) the window stays responsive while doing the work (can be moved and resized), and (iii) any Except thrown in Worker::DoWork is caught conveniently.

However, this solution relies upon std::future and qApp->processEvents(), the latter not being recommended by this answer.

How can I write the above code properly in Qt?

So far, I have tried:

void MainWindow::DoWork()
{
  setEnabled(false);

  QFuture<void> Resu = QtConcurrent::run(std::bind(&Worker::DoWork, &MyWorker));

  try
    { while (Resu.isRunning()) qApp->processEvents(); }
  catch (Except const &MyExcept)
    { QMessageBox::critical(this, "Error", MyExcept.What()); }

   setEnabled(true);
 }

and I found the drawbacks of consuming a whole thread (because the too frequent calls to qApp->processEvents()), and furthermore the exceptions are not properly caught.


Solution

  • You can use a QFutureWatcher to receive notification about the completion of your QFuture without the need to be busy-waiting calling processEvents.

    Basically you subscribe to the signal QFutureWatcher<void>::finished to know when the operation is complete. In the associated slot you re-enable the widget. Particularly important is to call future.waitForFinished() even if the computation is already complete, to force the transfer of any QException that was thrown in the worker thread see here. It looks like a workaround but I don't know if there is a better solution for this.

    After the subscription you call setFuture() function to start your doWork operation. The computation will be carried out in its own thread.

    In this way you should be able to do everything as you want:

    1. the window is disabled while doing the work
    2. the window stays responsive while doing the work (can be moved and resized)
    3. any QException thrown in Worker::doWork is caught conveniently

    Here is a minimal working example, based on your question. It should show you what I mean...

    #include <QApplication>
    #include <QException>
    #include <QMessageBox>
    #include <QPushButton>
    #include <QThread>
    #include <QtConcurrent>
    
    
    class Exception : public QException
    {
    public:
        Exception(const QString& message = "") : m_message {message} { }
    
        void raise() const override { throw *this; }
    
        Exception *clone() const override { return new Exception(*this); }
    
        QString message() { return m_message; }
    
    private:
        QString m_message;
    };
    
    
    class Worker
    {
    public:
        void doWork() {
            QThread::sleep(5);
            // throw Exception {"something bad happened..."};
            QThread::sleep(5);
        }
    };
    
    
    class Button : public QPushButton
    {
        Q_OBJECT
    
    public:
        Button(QWidget* parent = nullptr) : QPushButton {parent} {
            resize(400, 300);
            setText("Click me to wait!");
    
            connect(this, &QPushButton::clicked,
                    this, &Button::doWork);
        }
    
    public slots:
        void doWork() {
            setEnabled(false);
    
            auto future {QtConcurrent::run(&m_worker, &Worker::doWork)};
    
            auto watcher {new QFutureWatcher<void> {}};
            connect(watcher, &QFutureWatcher<void>::finished,
                    [=]() mutable {
                        try {
                            // Call this to force the transfer of any QException that was thrown in the worker thread
                            // (https://doc.qt.io/qt-5/qexception.html)
                            future.waitForFinished();
                        } catch (Exception& e) {
                            QMessageBox::critical(this, "Error", e.message());
                        }
                        setEnabled(true);
                    });
    
            watcher->setFuture(future);
        }
    
    private:
        Worker                  m_worker;
    };
    
    
    int main(int argc, char* argv[])
    {
        QApplication app {argc, argv};
    
        auto button {new Button {}};
        button->show();
    
        return app.exec();
    }
    
    #include "main.moc"