QMessageBox::warning on secondary thread
-
Some code that's executing in a future created by std::async (so not on the main UI thread) is checking for the existence of a file it's about to read or for an error reading the file it it does exist.
In either case it is calling QMessageBox::warning() on that thread and the application hangs at that point with the message box displayed without text or buttons.
I thought that QMessageBox ran its own message pump, and therefore this wouldn't be a problem, but clearly I was wrong.
How to handle this?
-
@Perdrix AFAIK yes,
maybe you also need
https://doc.qt.io/qt-6/qmetatype.html#Q_DECLARE_METATYPEI'm never 100% sure with this. The Meta System is always a lot of magic imho :D
Final code apart from the registration on QMessageBox::Icon in main().
if (fs::status(filePath).type() != fs::file_type::regular) { QString errorMessage{ QCoreApplication::translate( "DSS::StackingDlg", "%1 does not exist or is not a file").arg(QString::fromStdWString(fileName)) }; #if defined(_CONSOLE) std::cerr << errorMessage.toUtf8().constData(); #else QMainWindow* mainWindow{ getMainApplicationWindow() }; DeepSkyStacker* dss = dynamic_cast<DeepSkyStacker*>(mainWindow); if (nullptr != dss) { bool result = QMetaObject::invokeMethod(dss, "displayMessageBox", Qt::QueuedConnection, Q_ARG(const QString&, errorMessage), Q_ARG(QMessageBox::Icon, QMessageBox::Warning)); } else // This is here for DeepSkyStackerLive which is not yet Qt { AfxMessageBox(errorMessage.toStdWString().c_str(), MB_OK | MB_ICONWARNING); } #endif return false; } -
Some code that's executing in a future created by std::async (so not on the main UI thread) is checking for the existence of a file it's about to read or for an error reading the file it it does exist.
In either case it is calling QMessageBox::warning() on that thread and the application hangs at that point with the message box displayed without text or buttons.
I thought that QMessageBox ran its own message pump, and therefore this wouldn't be a problem, but clearly I was wrong.
How to handle this?
-
@Perdrix By not calling GUI elements, that includes QMessageBox, from any thread but the one where QApplication lives in!
Simply emit a signal from your thread, and do the Gui stuff in a connected Slot
@J-Hilk Ok, I've defined a slot in my QMainWindow class:
void DeepSkyStacker::displayMessageBox(const QString& message, QMessageBox::Icon icon)
{
QMessageBox msgBox{ icon, "DeepSkyStacker", message, QMessageBox::Ok , this};
msgBox.exec();
}The code I want to invoke it from isn't in a Q_OBJECT - in fact it's a free-standing function that's not part of a class. I believe that means I can't emit a signal to display the message.
I suspect some incantation involving QMetaObject::invokeMethod() is needed?
If you could help with that, it would be much appreciated.
Thanks
David -
@J-Hilk Ok, I've defined a slot in my QMainWindow class:
void DeepSkyStacker::displayMessageBox(const QString& message, QMessageBox::Icon icon)
{
QMessageBox msgBox{ icon, "DeepSkyStacker", message, QMessageBox::Ok , this};
msgBox.exec();
}The code I want to invoke it from isn't in a Q_OBJECT - in fact it's a free-standing function that's not part of a class. I believe that means I can't emit a signal to display the message.
I suspect some incantation involving QMetaObject::invokeMethod() is needed?
If you could help with that, it would be much appreciated.
Thanks
David@Perdrix said in QMessageBox::warning on secondary thread:
If you could help with that, it would be much appreciated.
depends, how exactly are you invoke that free-standing function in a different thread ?
can you show that code part ?
-
@J-Hilk Ok, I've defined a slot in my QMainWindow class:
void DeepSkyStacker::displayMessageBox(const QString& message, QMessageBox::Icon icon)
{
QMessageBox msgBox{ icon, "DeepSkyStacker", message, QMessageBox::Ok , this};
msgBox.exec();
}The code I want to invoke it from isn't in a Q_OBJECT - in fact it's a free-standing function that's not part of a class. I believe that means I can't emit a signal to display the message.
I suspect some incantation involving QMetaObject::invokeMethod() is needed?
If you could help with that, it would be much appreciated.
Thanks
David -
@Perdrix said in QMessageBox::warning on secondary thread:
If you could help with that, it would be much appreciated.
depends, how exactly are you invoke that free-standing function in a different thread ?
can you show that code part ?
@J-Hilk
const auto readTask = [this, pStackingInfo, firstBitmap = m_vBitmaps.cbegin()](const size_t lightTaskNdx, ProgressBase* pProgress) -> std::pair<std::shared_ptr<CMemoryBitmap>, int> { if (lightTaskNdx >= pStackingInfo->m_pLightTask->m_vBitmaps.size()) return { {}, -1 }; const int bitmapNdx = FindBitmapIndex(pStackingInfo->m_pLightTask->m_vBitmaps[lightTaskNdx].filePath.c_str()); if (bitmapNdx < 0) return { {}, -1 }; const auto& lightframeInfo = m_vBitmaps[bitmapNdx]; if (lightframeInfo.m_bDisabled) return { {}, -1 }; ZTRACE_RUNTIME("Stack %s", lightframeInfo.filePath.generic_string().c_str()); std::shared_ptr<CMemoryBitmap> rpBitmap; if (::LoadFrame(lightframeInfo.filePath, PICTURETYPE_LIGHTFRAME, pProgress, rpBitmap)) return { rpBitmap, bitmapNdx }; else return { {}, -1 }; }; auto futureForRead = std::async(std::launch::deferred, readTask, 0, m_pProgress); // Load first lightframe synchronously. const auto firstBitmap = m_vBitmaps.cbegin(); for (size_t i = 0; i < pStackingInfo->m_pLightTask->m_vBitmaps.size() && !bStop; ++i) { auto [pBitmap, bitmapNdx] = futureForRead.get(); futureForRead = std::async(std::launch::async, readTask, i + 1, nullptr); // Immediately load next lightframe asynchronously (need to set progress pointer to null).loadFrame() in turn invokes FetchPicture() after doing some stuff.
In FetchPicture() I wrote this:
if (fs::status(filePath).type() != fs::file_type::regular) { QString errorMessage{ QCoreApplication::translate( "DSS::StackingDlg", "%1 does not exist or is not a file").arg(QString::fromStdWString(fileName)) }; #if defined(_CONSOLE) std::cerr << errorMessage.toUtf8().constData(); #else QMainWindow* mainWindow{ getMainApplicationWindow() }; ZASSERT(nullptr != mainWindow); QMetaObject::invokeMethod(mainWindow, SLOT(displayMessageBox(const QString&, QMessageBox::Icon)), Q_ARG(QString, errorMessage), Q_ARG(QMessageBox::Icon, QMessageBox::Warning)); #endif return false; }Unfortunately the displayMessageBox() mf of the DeepSkyStacker class never gets driven?
-
@J-Hilk
const auto readTask = [this, pStackingInfo, firstBitmap = m_vBitmaps.cbegin()](const size_t lightTaskNdx, ProgressBase* pProgress) -> std::pair<std::shared_ptr<CMemoryBitmap>, int> { if (lightTaskNdx >= pStackingInfo->m_pLightTask->m_vBitmaps.size()) return { {}, -1 }; const int bitmapNdx = FindBitmapIndex(pStackingInfo->m_pLightTask->m_vBitmaps[lightTaskNdx].filePath.c_str()); if (bitmapNdx < 0) return { {}, -1 }; const auto& lightframeInfo = m_vBitmaps[bitmapNdx]; if (lightframeInfo.m_bDisabled) return { {}, -1 }; ZTRACE_RUNTIME("Stack %s", lightframeInfo.filePath.generic_string().c_str()); std::shared_ptr<CMemoryBitmap> rpBitmap; if (::LoadFrame(lightframeInfo.filePath, PICTURETYPE_LIGHTFRAME, pProgress, rpBitmap)) return { rpBitmap, bitmapNdx }; else return { {}, -1 }; }; auto futureForRead = std::async(std::launch::deferred, readTask, 0, m_pProgress); // Load first lightframe synchronously. const auto firstBitmap = m_vBitmaps.cbegin(); for (size_t i = 0; i < pStackingInfo->m_pLightTask->m_vBitmaps.size() && !bStop; ++i) { auto [pBitmap, bitmapNdx] = futureForRead.get(); futureForRead = std::async(std::launch::async, readTask, i + 1, nullptr); // Immediately load next lightframe asynchronously (need to set progress pointer to null).loadFrame() in turn invokes FetchPicture() after doing some stuff.
In FetchPicture() I wrote this:
if (fs::status(filePath).type() != fs::file_type::regular) { QString errorMessage{ QCoreApplication::translate( "DSS::StackingDlg", "%1 does not exist or is not a file").arg(QString::fromStdWString(fileName)) }; #if defined(_CONSOLE) std::cerr << errorMessage.toUtf8().constData(); #else QMainWindow* mainWindow{ getMainApplicationWindow() }; ZASSERT(nullptr != mainWindow); QMetaObject::invokeMethod(mainWindow, SLOT(displayMessageBox(const QString&, QMessageBox::Icon)), Q_ARG(QString, errorMessage), Q_ARG(QMessageBox::Icon, QMessageBox::Warning)); #endif return false; }Unfortunately the displayMessageBox() mf of the DeepSkyStacker class never gets driven?
@Perdrix said in QMessageBox::warning on secondary thread:
displayMessageBox
QMainWindow does not have such a slot, it is your own, right?
I think you need to cast QMainWindow* to your main window class pointer. -
@J-Hilk
const auto readTask = [this, pStackingInfo, firstBitmap = m_vBitmaps.cbegin()](const size_t lightTaskNdx, ProgressBase* pProgress) -> std::pair<std::shared_ptr<CMemoryBitmap>, int> { if (lightTaskNdx >= pStackingInfo->m_pLightTask->m_vBitmaps.size()) return { {}, -1 }; const int bitmapNdx = FindBitmapIndex(pStackingInfo->m_pLightTask->m_vBitmaps[lightTaskNdx].filePath.c_str()); if (bitmapNdx < 0) return { {}, -1 }; const auto& lightframeInfo = m_vBitmaps[bitmapNdx]; if (lightframeInfo.m_bDisabled) return { {}, -1 }; ZTRACE_RUNTIME("Stack %s", lightframeInfo.filePath.generic_string().c_str()); std::shared_ptr<CMemoryBitmap> rpBitmap; if (::LoadFrame(lightframeInfo.filePath, PICTURETYPE_LIGHTFRAME, pProgress, rpBitmap)) return { rpBitmap, bitmapNdx }; else return { {}, -1 }; }; auto futureForRead = std::async(std::launch::deferred, readTask, 0, m_pProgress); // Load first lightframe synchronously. const auto firstBitmap = m_vBitmaps.cbegin(); for (size_t i = 0; i < pStackingInfo->m_pLightTask->m_vBitmaps.size() && !bStop; ++i) { auto [pBitmap, bitmapNdx] = futureForRead.get(); futureForRead = std::async(std::launch::async, readTask, i + 1, nullptr); // Immediately load next lightframe asynchronously (need to set progress pointer to null).loadFrame() in turn invokes FetchPicture() after doing some stuff.
In FetchPicture() I wrote this:
if (fs::status(filePath).type() != fs::file_type::regular) { QString errorMessage{ QCoreApplication::translate( "DSS::StackingDlg", "%1 does not exist or is not a file").arg(QString::fromStdWString(fileName)) }; #if defined(_CONSOLE) std::cerr << errorMessage.toUtf8().constData(); #else QMainWindow* mainWindow{ getMainApplicationWindow() }; ZASSERT(nullptr != mainWindow); QMetaObject::invokeMethod(mainWindow, SLOT(displayMessageBox(const QString&, QMessageBox::Icon)), Q_ARG(QString, errorMessage), Q_ARG(QMessageBox::Icon, QMessageBox::Warning)); #endif return false; }Unfortunately the displayMessageBox() mf of the DeepSkyStacker class never gets driven?
@Perdrix ah ok, thats a way to do it :D
since this connect is a cross threads, the arguments need to be copied over. For QString this is trivial and done out of the box for you.
But I think you need to register enum QMessageBox::Icon with the meta system so it knows what it is and can copy it over
https://doc.qt.io/qt-6/qmetatype.html#qRegisterMetaType
Edit:
the Point of @jsulm is also more than valid! You'll need to cast it to your actual class, that has the slot :D -
@Perdrix ah ok, thats a way to do it :D
since this connect is a cross threads, the arguments need to be copied over. For QString this is trivial and done out of the box for you.
But I think you need to register enum QMessageBox::Icon with the meta system so it knows what it is and can copy it over
https://doc.qt.io/qt-6/qmetatype.html#qRegisterMetaType
Edit:
the Point of @jsulm is also more than valid! You'll need to cast it to your actual class, that has the slot :DAnother way would be to use a custom QEvent and simply post it to the global event queue. This way you don't need to know who is the receiver. Good for e.g. status messages from anywhere inside your app to show it in a central widget/status bar.
-
@Perdrix ah ok, thats a way to do it :D
since this connect is a cross threads, the arguments need to be copied over. For QString this is trivial and done out of the box for you.
But I think you need to register enum QMessageBox::Icon with the meta system so it knows what it is and can copy it over
https://doc.qt.io/qt-6/qmetatype.html#qRegisterMetaType
Edit:
the Point of @jsulm is also more than valid! You'll need to cast it to your actual class, that has the slot :D -
Another way would be to use a custom QEvent and simply post it to the global event queue. This way you don't need to know who is the receiver. Good for e.g. status messages from anywhere inside your app to show it in a central widget/status bar.
@Christian-Ehrlicher
That is a useful technique, compared to having to create/maintain aQObject. Shame it's not documented somewhere as a useful approach. -
@J-Hilk So to register QMessageBox::Icon
Is that as simple as adding this to main()
// // Register the QMessageBox::Icon enum as a meta type // qRegisterMetaType<QMessageBox::Icon>();@Perdrix
May I mention that you can avoid registration of types if you provided aMainWindow::displayWarningBox()method (which can calldisplayMessageBox()itself) which did not need aQMessageBox::Iconsent to it. This would allow the caller more of an "abstraction" layer which does not need to know so many Qt/message box details. -
@J-Hilk So to register QMessageBox::Icon
Is that as simple as adding this to main()
// // Register the QMessageBox::Icon enum as a meta type // qRegisterMetaType<QMessageBox::Icon>();@Perdrix AFAIK yes,
maybe you also need
https://doc.qt.io/qt-6/qmetatype.html#Q_DECLARE_METATYPEI'm never 100% sure with this. The Meta System is always a lot of magic imho :D
-
@Perdrix AFAIK yes,
maybe you also need
https://doc.qt.io/qt-6/qmetatype.html#Q_DECLARE_METATYPEI'm never 100% sure with this. The Meta System is always a lot of magic imho :D
Final code apart from the registration on QMessageBox::Icon in main().
if (fs::status(filePath).type() != fs::file_type::regular) { QString errorMessage{ QCoreApplication::translate( "DSS::StackingDlg", "%1 does not exist or is not a file").arg(QString::fromStdWString(fileName)) }; #if defined(_CONSOLE) std::cerr << errorMessage.toUtf8().constData(); #else QMainWindow* mainWindow{ getMainApplicationWindow() }; DeepSkyStacker* dss = dynamic_cast<DeepSkyStacker*>(mainWindow); if (nullptr != dss) { bool result = QMetaObject::invokeMethod(dss, "displayMessageBox", Qt::QueuedConnection, Q_ARG(const QString&, errorMessage), Q_ARG(QMessageBox::Icon, QMessageBox::Warning)); } else // This is here for DeepSkyStackerLive which is not yet Qt { AfxMessageBox(errorMessage.toStdWString().c_str(), MB_OK | MB_ICONWARNING); } #endif return false; } -
P Perdrix has marked this topic as solved on
-
Your approach is good (and working). You can still avoid some hassle by using lambdas instead. Here is what we basically do in our software:
QMetaObject::invokeMethod(qApp, [&errorMessage](){ QMessageBox{icon, "DeepSkyStacker", errorMessage}.exec(); });Please note that your thread will just continue running before the message box might even be shown. Sometimes you might want to wait for a response by the user. For this we need extra synchronization. I've put several use case into a nice little header-only library: https://github.com/SimonSchroeder/QtThreadHelper
You can have a look how you might solve some of the problems.