Search code examples
qtenumsqmlsignals-slotsqthread

How can a QThread send a signal from its own thread with an enum as an argument for consumption in QML?


In the following code, if the signal errorHappened is emitted from the main thread it works without problem. However if it is emitted from the QThread thread it fails with the following error:

QObject::connect: Cannot queue arguments of type 'ErrorCode'
(Make sure 'ErrorCode' is registered using qRegisterMetaType().)

Is there a way that the signal can be successfully emitted from the QThread thread? If so, how?

Full code in this Gist

MyClass.h

#import <QThread>
#import <atomic>

class MyClass : public QThread
{
    Q_OBJECT

public:
    explicit MyClass(QObject *parent = Q_NULLPTR);
    virtual ~MyClass() override;

    enum ErrorCode {
        ErrorA,
        ErrorB,
        ErrorC
    };
    Q_ENUM(ErrorCode)

signals:
    void errorHappened(ErrorCode errorCode);

public slots:
    void mainThreadError();
    void otherThreadError();

private:
    std::atomic<bool> m_running;
    std::atomic<bool> m_signalStop;
    std::atomic<bool> m_signalError;

    void run() override;
    void stop();
};

MyClass.cpp

#include "MyClass.h"

MyClass::MyClass(QObject *parent)
    : QThread(parent)
{
    start();
}

MyClass::~MyClass()
{
    stop();
}

void MyClass::mainThreadError()
{
    emit errorHappened(ErrorCode::ErrorA);
}

void MyClass::otherThreadError()
{
    m_signalError = true;
}

void MyClass::run()
{
    m_running = true;

    while (!m_signalStop) {
        if (m_signalError) {
            emit errorHappened(ErrorCode::ErrorA);
            m_signalError = false;
        }
        msleep(1);
    }

    m_running = false;
    m_signalStop = false;
}

void MyClass::stop()
{
    if (m_running) {
        m_signalStop = true;
        wait();
    }
}

main.cpp

#include <QGuiApplication>
#include <QQuickView>
#include "MyClass.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView *view = new QQuickView();

    qmlRegisterType<MyClass>("MyClass", 1, 0, "MyClass");

    view->setSource((QUrl(QStringLiteral("qrc:/main.qml"))));
    view->create();
    view->show();

    return app.exec();
}

main.qml

import QtQuick 2.12
import QtQuick.Controls 2.5
import MyClass 1.0

Rectangle {
    id: root

    width: 800
    height: 600
    focus: true

    MyClass {
        id: tester
        onErrorHappened: {
            var s
            switch (errorCode) {
            case MyClass.ErrorA:
                s = "Error A happened"
                break
            }
            console.log(s)
        }
    }

    Row {
        spacing: 30

        Button {
            id: mainThreadButton

            enabled: !tester.testRunning
            text: "Test on main thread"
            onClicked: tester.mainThreadError()
        }

        Button {
            id: otherThreadButton

            enabled: !tester.testRunning
            text: "Test on other thread"
            onClicked: tester.otherThreadError()
        }
    }
}

Solution

  • qmlRegisterType makes the QObject (MyClass) class accessible in QML (Q_PROPERTY, Q_ENUM, Q_SIGNAL, Q_SLOT, Q_INVOKABLE, etc.) but does not allow the transmission of data between threads, for this case it must be registered using qRegisterMetaType<MyClass::ErrorCode>("ErrorCode"):

    main.cpp

    #include <QGuiApplication>
    #include <QQuickView>
    #include "MyClass.h"
    
    static void registerTypes(){
        qRegisterMetaType<MyClass::ErrorCode>("ErrorCode");
        qmlRegisterType<MyClass>("MyClass", 1, 0, "MyClass");
    }
    
    Q_COREAPP_STARTUP_FUNCTION(registerTypes)
    
    int main(int argc, char *argv[])
    {
        QGuiApplication app(argc, argv);
        QQuickView view;
        view.setSource((QUrl(QStringLiteral("qrc:/main.qml"))));
        view.show();
        return app.exec();
    }