How can I change GUI from std::thread?

Viewed 378

Firstly, I tried to use setVisible() from thread

There is an event:

void MainWindow::OnShow(){
    // Start OnShow actions
    ui->LoadingBox->setVisible(true);
    std::thread dThread(OnShow_threaded, ui, &(this->settingsMap));
    dThread.join();
}

There is a function OnShow_threaded:

void OnShow_threaded(Ui::MainWindow *ui, std::unordered_map<QString,QString> *settingsMap){

    // Connect to server
    bool hasInternet = false;
   
    // If app doesn't have Internet access -> show offline mode
    if (!hasInternet) {
        ui->SettingsLabel->setVisible(true);
    }
}

The program crashes when compiling a static assembly with an error:

ASSERT failure in QCoreApplication::sendEvent: "Cannot send events to objects owned by a different thread. Current thread 0x0x36c56540. Receiver 'WarningMsg' (of type 'QGroupBox') was created in thread 0x0x341c2fa0", file kernel\qcoreapplication.cpp, line 558

On the line: ui->SettingsLabel->setVisible(true);

At the same time, there is no such error when linking dynamically.

You can find full project on GitHub


Secondly, I tried to use events.

There is a function OnShow_threaded:

void OnShow_threaded(MainWindow* mw, Ui::MainWindow *ui, std::unordered_map<QString,QString> *settingsMap){
    // Connect to server
    bool hasInternet = false;

    // If app doesn't have Internet access -> show offline mode
    if (!hasInternet) {
        MyEvent* event = new MyEvent(EventTypes::InternetConnectionError);
        QCoreApplication::postEvent(mw, event);
        //delete event;
        //delete receiver;
    }
}

There is an event class:

#ifndef EVENTS_HPP
#define EVENTS_HPP

#include <QEvent>
#include <QString>

enum EventTypes {
    InternetConnectionError,
    Unknown
};

class MyEvent : public QEvent
{
public:
  MyEvent(const EventTypes _type) : QEvent(QEvent::User) {_localType = _type;}
 ~MyEvent() {}

  auto localType() const {return _localType;}


private:
  int _localType;
};

#endif // EVENTS_HPP

There is an event handler:

void MainWindow::events(QEvent *event)
{
    if (event->type() == QEvent::User)
      {
        MyEvent* postedEvent = static_cast<MyEvent*>(event);

        if (postedEvent->localType() == EventTypes::InternetConnectionError){
            ui->WarningMsg->setVisible(true);
            ui->SettingsLabel->setVisible(true);
        }
    }
}

Passing parameters:

void MainWindow::OnShow(){
    // Start OnShow actions
    ui->LoadingBox->setVisible(true);
    std::thread dThread(OnShow_threaded, this, ui, &(this->settingsMap));
    dThread.detach();
}

There is a mainwindows hpp file:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QDebug>
#include <QMovie>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QObject>
#include <QMessageBox>
#include <QStandardPaths>
#include <QDir>
#include <QFile>
#include <QCoreApplication>
#include <QSaveFile>
#include <QProcess>

#include <thread>
#include <chrono>
#include <unordered_map>
#include <iostream>
#include <fstream>
#include <cstdlib>

#include "settings.hpp"
#include "events.hpp"

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

    void OnShow();

private slots:
    void SettingsLabelPressed();

    void on_CloseMsgButton_clicked();

    void on_Settings_SaveButton_clicked();

    void on_Settings_UseTranslation_stateChanged(int arg1);

protected:

    void events(QEvent* event);

private:
    Ui::MainWindow *ui;
    std::unordered_map<QString,QString> settingsMap;
};

void OnShow_threaded(MainWindow* mw, Ui::MainWindow *ui, std::unordered_map<QString,QString> *settingsMap);

#endif // MAINWINDOW_H

But event doesn't execute.
What did I do wrong?
And how to properly change the GUI from another thread?


З.Ы. Sorry for my English, I'm from Russia....


3 Answers

As you asked for the demo with QThread, in the comments, then here it is.

As GUI, I have a mainwindow with two simple buttons and I want to show, hide the big buttons with a QThread (instead of just the slot clicked) I emit an intermediate signal from the clicked to hide/show the button.

The role of the QThreadis only to emit the signal to sigShowHide with argument trueor false.

The main UI thread treats this signal by showing or hiding the button by calling the slot onShowHideButtonThreaded which reacts to the signal sigShowHide

enter image description here

Here are the code files:

mainwindows.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QThread>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

signals:
        
       void sigShowHide(bool);

public slots:
       void onShowHideButtonThreaded(bool);
       void onButton1Click();


private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

mainwindows.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    QObject::connect(ui->pushButton, &QPushButton::clicked, this, &MainWindow::onButton1Click);
    QObject::connect(this,&MainWindow::sigShowHide, this, &MainWindow::onShowHideButtonThreaded);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::onShowHideButtonThreaded(bool a)
{
    qDebug() << " the main thread id = " << QThread::currentThread() << "set the visibility ";
    ui->pushButton_2->setVisible(a);
}

void MainWindow::onButton1Click()
{
    qDebug()<< "clicked";
    qDebug() << " the main thread id = " << QThread::currentThread();
    QThread* l_thread = QThread::create([&]()
    {
        qDebug() << "Running Thread " << QThread::currentThreadId() << " to emit signal only ";
        emit sigShowHide( !this->ui->pushButton_2->isVisible());
    });
    l_thread->start();
  }

`

The main.cpp

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

An example of execution is :

the main thread id = QThread(0x116ee18)
Running Thread 0x1ed8 to emit signal only 
the main thread id = QThread(0x116ee18) set the visibility 

As stated in @rustyx's answer: In Qt, like in many other GUI frameworks, the GUI may be updated from the main thread only.

I was also stuck on this problem, and here are my two solutions:

  1. Use QMetaObject::invokeMethod.

    QMetaObject::invokeMethod(ui->SettingsLabel, "setVisible", Q_ARG(bool, true));
    

    QMetaObject::invokeMethod is a thread-safe API, it has a Qt::ConnectionType type parameter, which has a default value Qt::AutoConnection.

    Descriptions of some Qt::ConnectionType values:

    Qt::AutoConnection:

    (Default) If the receiver lives in the thread that emits the signal, Qt::DirectConnection is used. Otherwise, Qt::QueuedConnection is used. The connection type is determined when the signal is emitted.

    Qt::DirectConnection:

    The slot is invoked immediately when the signal is emitted. The slot is executed in the signalling thread.

    Qt::QueuedConnection:

    The slot is invoked when control returns to the event loop of the receiver's thread. The slot is executed in the receiver's thread.

    Qt::BlockingQueuedConnection:

    Same as Qt::QueuedConnection, except that the signalling thread blocks until the slot returns. This connection must not be used if the receiver lives in the signalling thread, or else the application will deadlock.

    It is clear from the description that the API will put your call request into a queue, and then the main thread will take it out from the queue to finally process your call request.

    And if you need to get the return value of the target method, you need to explicitly pass Qt::BlockingQueuedConnection to the Qt::ConnectionType type parameter. Because the indirect default value Qt::QueuedConnection will not wait for the call to complete, you will likely get the wrong return value.

    But this solution has a trouble that is it only supports signal and slot functions, NOT non-signal and non-slot functions.

  2. Use connect.

    // In "MainWindow" class declaration
    //
    class MainWindow : public QMainWindow
    {
        // ...
    Q_SIGNALS:
        void setSettingsLabelVisibleSafety(bool value);
        // ...
    };
    
    // In "MainWindow" constructor
    //
    connect(this, &MainWindow::setSettingsLabelVisibleSafety, this,
        [this](bool value) {
            ui->SettingsLabel->setVisible(value);
        }
    );
    
    // In other thread
    //
    setSettingsLabelVisibleSafety(value);
    

    connect also has a Qt::ConnectionType type parameter, which has a default value Qt::AutoConnection if you pass the receiver parameter explicitly, otherwise the default value is Qt::DirectConnection. So you need to either explicitly pass the receiver parameter or explicitly pass Qt::AutoConnection value to the Qt::ConnectionType type parameter.

    If the target method has a return value, Qt::BlockingQueuedConnection needs to be passed explicitly as well.

    This solution supports signal and slot functions, as well as non-signal and non-slot functions.

Both of the above solutions require you to call qRegisterMetaType to register the user-defined types (if any).

qRegisterMetaType<YourType>("YourType");

Obviously, I prefer the second solution.

Related