Update UI from a C++ library callback
-
Hi all,
I'm writing a QT desktop application that includes a C++ library.
The library inludes a time consuming procedure that I'd like to see the progression by a progress bar.
So I wrote a (pure) C++ function that receive a function pointer:TerminalProtocol(LOG_DOWNLOAD_CB callback);
where
typedef void(*LOG_DOWNLOAD_CB)(DownloadLogPhase phase, int nParam);
At the QT side I defined the callback that updates the UI, both listbox and progressbar.
My current version is:void logCallback( DownloadLogPhase phase, int value) { QString msg = "?"; switch(phase) { case DL_PHASE_OK: qDebug() << "[logCallback] OK "; msg = "OK"; break; case DL_PHASE_OPENING: qDebug() << "[logCallback] OPENING "; msg = "Opening .."; break; case DL_PHASE_INIT: msg = "Init done!"; break; case DL_PHASE_DOWNLOADING: qDebug() << "[logCallback] DOWNLOADING " << value; msg = "Download " + QString::number(value); break; case DL_PHASE_ERROR: qDebug() << "[logCallback] ERROR " << value; // Codice UpdaterRet_* msg = "ERROR " + QString::number(value) + " - "; msg.append(Updater::GetUpdaterRetDescription((UpdaterRet)value)); break; default: qDebug() << "[logCallback] " << phase; msg = "NotHandled phase: " + QString::number(phase); } // QWidgetList wl = QApplication::topLevelWidgets(); foreach(QWidget * widget, wl) { if (MainWindow* mw = qobject_cast<MainWindow*>(widget)) { mw->ui->listDownloadLog->addItem(msg); if (phase == DL_PHASE_DOWNLOADING) mw->ui->progressBar->setValue(value); break; } } }
The strange thing is that the 'addItem' call has no problem; the progressbar update generates error:
ASSERT failure in QCoreApplication::sendEvent: "Cannot send events to objects owned by a different thread. Current thread 0x0x1e537a4e4a0. Receiver 'MainWindow' (of type 'MainWindow') was created in thread 0x0x1e52e723c30", file ...
How can I solve this situation?
Is there other solutions for this architecture (QT app + C++ lib) ??Thanks!
-
@SamiV123 said in Update UI from a C++ library callback:
I'd make your callback implementation post a new Event to the Qt event queue.
This is the most reasonable approach. Connections of signals/slots between threads just work.
Another way is to just put everything into a lambda an send that to the GUI thread:
void logCallback( DownloadLogPhase phase, int value) { QString msg = "?"; switch(phase) { case DL_PHASE_OK: qDebug() << "[logCallback] OK "; msg = "OK"; break; case DL_PHASE_OPENING: qDebug() << "[logCallback] OPENING "; msg = "Opening .."; break; case DL_PHASE_INIT: msg = "Init done!"; break; case DL_PHASE_DOWNLOADING: qDebug() << "[logCallback] DOWNLOADING " << value; msg = "Download " + QString::number(value); break; case DL_PHASE_ERROR: qDebug() << "[logCallback] ERROR " << value; // Codice UpdaterRet_* msg = "ERROR " + QString::number(value) + " - "; msg.append(Updater::GetUpdaterRetDescription((UpdaterRet)value)); break; default: qDebug() << "[logCallback] " << phase; msg = "NotHandled phase: " + QString::number(phase); } QMetaObject::invokeMethod(qGuiApp, [msg=msg]() { QWidgetList wl = QApplication::topLevelWidgets(); foreach(QWidget * widget, wl) { if (MainWindow* mw = qobject_cast<MainWindow*>(widget)) { mw->ui->listDownloadLog->addItem(msg); if (phase == DL_PHASE_DOWNLOADING) mw->ui->progressBar->setValue(value); break; } } }); }
There is a non-zero chance that the order of these calls to the GUI thread is not kept. Have a look at the implementation of
guiThread()
in my library https://github.com/SimonSchroeder/QtThreadHelper for different ways how to synchronize with the GUI thread. If your progress calls get out of sync using a queue (scheduleWorkerThread::ASYNC
in my lib) is the best approach. The other two implementations will fully block the calling thread which is most likely not what you want. -
Apparently your callback is called from another thread than UI thread. Only UI thread is allowed to manupulate UI. You can use QMetaObject::invokeMethod to call setValue in UI thread.
-
@SteMMo said in Update UI from a C++ library callback:
what I should use as 'context' parameter?
From your code, it is the address of MainWindow.
You need to pass a context parameter here:
TerminalProtocol(LOG_DOWNLOAD_CB callback, void* context);
then in the callback, get back the MainWindow object with a cast:
void logCallback( DownloadLogPhase phase, int value, void* context) { MainWindow* mw = qobject_cast<MainWindow*>(context); ... }
-
I'd make your callback implementation post a new Event to the Qt event queue.
you can for example use your dialog/widget/window as the receiver object in your call to QCoreApplication::postEvent(...) call and then you can receive / handle the event in your MyWidget:event(QEvent* event) {} method and there you'd down cast the QEvent to your event type and update the UI appropriately.
-
@SamiV123 said in Update UI from a C++ library callback:
I'd make your callback implementation post a new Event to the Qt event queue.
This is the most reasonable approach. Connections of signals/slots between threads just work.
Another way is to just put everything into a lambda an send that to the GUI thread:
void logCallback( DownloadLogPhase phase, int value) { QString msg = "?"; switch(phase) { case DL_PHASE_OK: qDebug() << "[logCallback] OK "; msg = "OK"; break; case DL_PHASE_OPENING: qDebug() << "[logCallback] OPENING "; msg = "Opening .."; break; case DL_PHASE_INIT: msg = "Init done!"; break; case DL_PHASE_DOWNLOADING: qDebug() << "[logCallback] DOWNLOADING " << value; msg = "Download " + QString::number(value); break; case DL_PHASE_ERROR: qDebug() << "[logCallback] ERROR " << value; // Codice UpdaterRet_* msg = "ERROR " + QString::number(value) + " - "; msg.append(Updater::GetUpdaterRetDescription((UpdaterRet)value)); break; default: qDebug() << "[logCallback] " << phase; msg = "NotHandled phase: " + QString::number(phase); } QMetaObject::invokeMethod(qGuiApp, [msg=msg]() { QWidgetList wl = QApplication::topLevelWidgets(); foreach(QWidget * widget, wl) { if (MainWindow* mw = qobject_cast<MainWindow*>(widget)) { mw->ui->listDownloadLog->addItem(msg); if (phase == DL_PHASE_DOWNLOADING) mw->ui->progressBar->setValue(value); break; } } }); }
There is a non-zero chance that the order of these calls to the GUI thread is not kept. Have a look at the implementation of
guiThread()
in my library https://github.com/SimonSchroeder/QtThreadHelper for different ways how to synchronize with the GUI thread. If your progress calls get out of sync using a queue (scheduleWorkerThread::ASYNC
in my lib) is the best approach. The other two implementations will fully block the calling thread which is most likely not what you want. -
S SteMMo has marked this topic as solved on