Search code examples
javascriptc++qtqt5qtwebchannel

QWebChannel sends null QVariant POD structs to JavaScript


I am writing a C++ QWidget application that communicates with JavaScript running in a resource hosted web page. I need to find a way to send an array of the following POD structs to a JavaScript function hosted in the web page but unfortunately the data always ends up as arrays of nulls. I think the problem similar to this question - but this does not have an answer either.

The custom POD struct (I need to send a list (QVariantList) of these) is:

using FPlanWPT = struct FPlanWPT {
    //std::string name;
    double longitude;
    double latitude;
    double bearing;
};

// register custom type with the QT type system
Q_DECLARE_METATYPE(FPlanWPT);

The IPC class that is used as a notifier is as follows:

class FlightRoute : public QObject {
    Q_OBJECT
    Q_PROPERTY(QVariantList data READ data NOTIFY dataChanged)
public:
    explicit FlightRoute(QObject* parent = nullptr)
        : QObject(parent)
    {}

    //! Flight route data getter.
    QVariantList data() const {
        return valueList;
    }
public slots:
    void updateRouteData(const QVariantList& data);

signals:
    void dataChanged(const QVariantList& data);

private:
    QVariantList valueList;
};

The idea above is that when the

The closest example I found to what I am trying to achieve is a QT Widget based JavaScript Chart.js application that generates randomized chart columns and updates a chart running in a web page.

The key to getting IPC communication between the QT C++ Widget application and JavaScript is to initialize a QWebChannel on both ends.

On the C++ side this is essentially:

class mainwindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit mainwindow(QWidget *parent = Q_NULLPTR);
    ~mainwindow();
    ...
private:
    ...
    std::unique_ptr<QWebEngineView> mpWebView;
}

the constructor is as follows:

//! Constructor.
mainwindow::mainwindow(QWidget *parent)
    : QMainWindow(parent)
    , mUI(new Ui::mainwindow())
    . . .
    , mpWebView(std::make_unique<QWebEngineView>())
    , mRecordBuffer{}
    , mpWorkerThread{nullptr}
    , mpWorkerObject{nullptr}
{
    static auto& gLogger = gpLogger->getLoggerRef(
        gUseSysLog ? Logger::LogDest::SysLog :
        Logger::LogDest::EventLog);

    // JavaScript integration - allow remote debugging with mpWebView
    qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "1234");

    // register custom types for serialization with signals/slots
    qRegisterMetaType<FPlanWPT>("FPlanWPT");

    // initialize the form
    mUI->setupUi(this);    

    // load the web page containing the google map javascript
    mpWebView->page()->load(QUrl("qrc:///html/test.html"));

    // initialize the link to the HTML web page content
    auto webChannel = new QWebChannel(this);
    const auto routeIPC = new FlightRoute(this);
    // register IPC object with the QWebChannel
    connect(mpWorkerObject, &Worker::fooSignal, routeIPC, &FlightRoute::updateRouteData, Qt::DirectConnection);
    webChannel->registerObject(QStringLiteral("routeIPC"), routeIPC);
    mpWebView->page()->setWebChannel(webChannel);

    // Insert the html page at the top of the grid layout
    mUI->flightTrackGridLayout->addWidget(mpWebView.get(), 0, 0, 1, 3);
    . . .
}

On the JavaScript side (this is my entire HTML file)

<html>
<head>
    <script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
</head>
<body>
    <div style="margin:auto; width:100%;">
        <canvas id="canvas"></canvas>
    </div>
    <script>
        // function to update the google
        var updateMap = function (waypointData) {
            // waypoint data is always a list of nulls
            console.log(waypointData);
        }

        // script called once web page loaded
        window.onload = function () {
            new QWebChannel(qt.webChannelTransport,
                function (channel) {
                    // whenever the route data changes, invoke updateMap slot
                    var dataSource = channel.objects.routeIPC;
                    dataSource.dataChanged.connect(updateMap);
                }
            );
        }
    </script>
</body>
</html>

Additionally, on the C++ side we set a page from the QWebEngineView to a Gui layout that displays the web content. These need to be initialized ahead of time the signal to slot flows asynchronously through the established QWebChannel. In my case, I only care about sending custom POD structs via QVariant objects from the C++ side to the JavaScript side.

In order to use custom data structures with the QVariant - or in my case QVariantList's - we need to register the custom type with QT - as a sort of meta rtti registration. Without this when the javascript slot function var updateMap will not be able to decode the type information from the fields of the POD.

The


Solution

  • There is a very common error, many think that to transport data through QWebChannel is using QMetatype, and the documentation is not very clear and does not indicate what kind of data can be transported, but a report clearly states that it can be transported: json or items that can be packaged in a json . And that can be checked if the implementation is reviewed, for example in a answer to implement the QWebChannel for WebView, my main task was to make the conversion from QString to QJsonObject and vice versa.

    So in any case that requires sending data through QWebChannel you have to convert it to a QJsonValue, QJsonObject or QJsonArray. As you wish to send an array of data, I will use QJsonArray:

    flightroute.h

    #ifndef FLIGHTROUTE_H
    #define FLIGHTROUTE_H
    
    #include <QJsonArray>
    #include <QJsonObject>
    #include <QObject>
    
    struct FPlanWPT {
        QString name;
        double longitude;
        double latitude;
        double bearing;
        QJsonObject toObject() const{
            QJsonObject obj;
            obj["name"] = name;
            obj["longitude"] = longitude;
            obj["latitude"] = latitude;
            obj["bearing"] = bearing;
            return obj;
        }
    };
    
    class FlightRoute : public QObject
    {
        Q_OBJECT
    public:
        using QObject::QObject;
        void setRoutes(const QList<FPlanWPT> &routes){
            QJsonArray array;
            for(const FPlanWPT & route: routes){
                array.append(route.toObject());
            }
            emit routesChanged(array);
        }
    signals:
        void routesChanged(const QJsonArray &);
    };
    
    #endif // FLIGHTROUTE_H
    

    main.cpp

    #include "flightroute.h"
    
    #include <QApplication>
    #include <QTimer>
    #include <QWebChannel>
    #include <QWebEngineView>
    #include <random>
    
    int main(int argc, char *argv[])
    {
        qputenv("QTWEBENGINE_REMOTE_DEBUGGING", "9000");
        QApplication a(argc, argv);
        QWebEngineView view;
    
        FlightRoute routeIPC;
        QWebChannel webChannel;
        webChannel.registerObject(QStringLiteral("routeIPC"), &routeIPC);
        view.page()->setWebChannel(&webChannel);
    
        std::random_device rd;
        std::mt19937 gen(rd());
        std::uniform_real_distribution<> dist(0, 100);
    
        QTimer timer;
        QObject::connect(&timer, &QTimer::timeout, [&](){
            QList<FPlanWPT> routes;
            for(int i=0; i<10; i++){
                routes << FPlanWPT{"name1", dist(gen), dist(gen), dist(gen)};
            }
            routeIPC.setRoutes(routes);
        });
        timer.start(1000);
    
        view.load(QUrl(QStringLiteral("qrc:/index.html")));
        view.resize(640, 480);
        view.show();
        return a.exec();
    }