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.
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:
QException
thrown in Worker::doWork
is caught convenientlyHere 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"