2

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

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
johnco3
  • 2,401
  • 4
  • 35
  • 67
  • @eyllanesc done! – johnco3 Oct 27 '18 at 04:59
  • @eyllanesc ty for the great answer, I't not quite working for me yet. I'm a bit confused as to how the FlightRoute object magically notifies a slot in the javascript - my code is calling the emit routesChanged(array) with a Json array - but my function is not being called anymore in JavaScript - note I removed the qRegisterMetaType<..> and the Q_DECLARE_METATYPE(...) for my FlightRoute. My javascript should theoretically make the connection using the dataSource.routesChanged.connect(updateMap) (it used to) – johnco3 Oct 28 '18 at 02:48
  • My test code is in the following link: https://github.com/eyllanesc/stackoverflow/tree/master/53018492 execute it and open the link: http://127.0.0.1:9000 and you will see the impression. If you have problems I could help you if you share your code with me through github so I can correct it. – eyllanesc Oct 28 '18 at 14:22
  • @eyllanesc thanks - gosh you're really organized! amazing workflow. I saw a ton of snippet projects there. BTW I finally got my code to work, in your example the lifetime of the routeIPC lives on as it lives in main, in my case I needed to have an instance of it around in mainwindow, I only have a WebViewEngine member. So in order to get access to the IPC from (in order to publish to it) I needed to retrieve the ipc object via: auto pFlightRouteIPC = mpWebView->page()->webChannel()->registeredObjects().find("routeIPC").value() and then publish it via pFlightRouteIPC->setRoutes(routes). – johnco3 Oct 28 '18 at 17:54
  • @eyllanesc Incidentally where does it say that QWebChannel only supports Json objects? Its really not written anywhere, does the engine somehow require QVariants or QStrings that have native json conversions in order for it to work? - just curious. BTW next thing I need to do (now that I have the lat/lon values at the javascript level is to construct a map with lines joining these points together. Its probably a bit of a stretch but do you happen to have such an example :). I'll tweak my html to call something like this https://developers.google.com/maps/documentation/javascript/shapes – johnco3 Oct 28 '18 at 17:54
  • As I point out in my answer, it is not written in the docs but only accept types that can be converted to json, but it is written in the BUG that you indicate, and that can be concluded so well after understanding the examples provided. For example, in the link that you indicate: https://github.com/euler0/demo-qt-webchannel uses a QVariantList of int, and the int can be easily mapped in a json, the same for QString and basic types as bool , but more complicated elements do not. – eyllanesc Oct 28 '18 at 17:59
  • I did not think you were going to have problems with the scope of the variables, but I see that you have solved it. – eyllanesc Oct 28 '18 at 17:59
  • Why do not you use google-maps, osm or similar libraries that are in js ?, I've written something similar but for an old version of PyQt that I have not had the time to update up to now: https://github.com/eyllanesc/qMap/blob/master/qgmap/qgmap.js, Finally, if my answer helps you, do not forget to mark it as correct. – eyllanesc Oct 28 '18 at 18:02
  • @eyllanesc thanks marked as correct, yes I am using google.maps - not familiar with osm - it that is C++ library? If so, I would prefer it, as it is painful going between C++ & JavaScript (plus I am a JavaScript newbie which does not help) - again thanks for your your answer it was really helpful. – johnco3 Oct 29 '18 at 13:24

1 Answers1

3

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();
}
eyllanesc
  • 235,170
  • 19
  • 170
  • 241