Declaration of VAR

and some other stuff

Interacting with HTML from QML over WebChannel/WebSockets

2018-07-14 16:00:55 +0200

2018-07-14 16:00:55 +0200 | Comments

Qt allows to create applications with so-called hybrid GUI - where you can mix native parts with HTML-based content. Such a mix even supports interaction between those native parts and HTML-side - by exposing QObjects via WebChannel and WebSockets.

And there are several ways of implementing that.

How to display HTML content

First of all, here’s how you can show HTML-content:

  1. Using WebEngineView;
  2. Using WebView;
  3. And of course you can simply use a standalone web-browser, although in this case it won’t be integrated in your application, obviously.

All three options support communication between QML and HTML worlds, although each one does it differently. To be precise, WebEngineView does in one way, and WebView (just like a web-browser) does it another way.

As you might have already guessed, WebEngineView and WebView are two different things. To make it more clear:

WebEngineView WebView

WebEngineView is a web-view provided by Qt’s own web-browser engine which is based on Chromium (Qt WebEngine). It is a full-featured web-browser that is bundled and integrated with Qt, which is good, but at the same time it means that you need to drag it around together with your application, and this thing is quite a big one (what did you expect, it’s a frigging web-browser, innit).

WebView is a, well, web-view, but the difference is that it uses a native web-browser of the platform (if available), thus it does not require including a full web-browser stack as part of the application (which is the case with WebEngineView), so your application is more light-weight. Another point is that some platforms simply do not allow any non-system web-browsers, so WebView is the only option available there.

The crucial difference between WebEngineView and WebView in terms of this article is the way of how Qt can communicate with HTML-content inside those views. WebEngineView provides the easiest way - directly via WebChannel, thanks to Chromium IPC capabilities. And WebView (as well as external web-browser) requires you to establish some transport for WebChannel first.

Interacting with HTML from QML

Okay, we can display our HTML, but how to interact with it from QML? This way or another, everything goes through WebChannel, and at HTML side it is done via special JavaScript library - Qt WebChannel JavaScript API.

Now let’s see how to make it work with WebEngineView and WebView.

WebEngineView - direct WebChannel

As I said, WebEngineView can use WebChannel directly, so this example is pretty easy. But even being an easy one (comparing with the WebSockets examples), it made me spent quite a some time to make it work. Luckily, I found this repository, which I used as a base.

main.qml:

// an object with properties, signals and methods - just like any normal Qt object
QtObject {
    id: someObject

    // ID, under which this object will be known at WebEngineView side
    WebChannel.id: "backend"

    property string someProperty: "Break on through to the other side"

    signal someSignal(string message);

    function changeText(newText) {
        txt.text = newText;
        return "New text length: " + newText.length;
    }
}

Text {
    id: txt
    text: "Some text"
    onTextChanged: {
        // this signal will trigger a function at WebEngineView side (if connected)
        someObject.someSignal(text)
    }
}

WebEngineView {
    url: "qrc:/index.html"
    webChannel: channel
}

WebChannel {
    id: channel
    registeredObjects: [someObject]
}

So, here we create WebChannel and assign its ID to WebEngineView and register QtObject’s ID at the channel. Instead of QtObject defined at QML side you can of course have a C++/Qt object “injected” from C++ side.

index.html:

<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>

<script type="text/javascript">
    // here will be our QtObject from QML side
    var backend;

    window.onload = function()
    {
        new QWebChannel(qt.webChannelTransport, function(channel) {
            // all published objects are available in channel.objects under
            // the identifier set in their attached WebChannel.id property
            backend = channel.objects.backend;

            // connect to a signal
            backend.someSignal.connect(function(someText) {
                alert("Got signal: " + someText);
                document.getElementById("lbl").innerHTML = someText;
            });
        });
    }

    // just to demonstrate you async interaction
    var result = "ololo";
    function changeLabel()
    {
        var textInputValue = document.getElementById("input").value.trim();
        if (textInputValue.length === 0)
        {
            alert("You haven't entered anything!");
            return;
        }

        // invoke a method, and receive the return value asynchronously
        backend.changeText(textInputValue, function(callback) {
            result = callback;
            // since it's async, this alert will appear later and show the actual result
            alert(result);
            // reset variable back to default value
            result = "ololo";
        });
        // this alert will appear first and show default "ololo"
        alert(result);
    }

    // you can also read/write properties of QtObject from QML side
    function getPropertyValue()
    {
        var originalValue = backend.someProperty;

        alert(backend.someProperty);
        backend.someProperty = "some another value";
        alert(backend.someProperty);

        backend.someProperty = originalValue;
    }
</script>

Here you need to create a QWebChannel at windows.onload event and get the backend object. After that you can call its methods, connect to its signal and access its properties.

Here’s a simple demo of such communication between QML (everything that’s outside the blue rectangle) and HTML (the part inside the blue rectangle):

And here’s a schema for it:

By the way, interaction is done asynchronously - take a look at changeLabel() function and note the order of alerts.

WebView - WebChannel over WebSockets

WebView (and external web-browser) cannot use WebChannel directly. You need to create a WebSockets transport first, and then use WebChannel on top of it.

This is something that is not possible with QML only, so you’ll have to write some C++ code too. It’s a bit frustrating, but more frustrating is the fact that documentation does not mention it clearly.

So, when I discovered that, first I decided to rework one of the C++ examples, and when I almost finished I also got an answer at Stack Overflow showing how to do almost everything in QML, so I ended up with two solutions.

I’ll share both with you.

Mostly C++

This one has most of the work done in C++ and there is not much left for QML. It is based on the Standalone Example.

main.cpp:

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

    // don't forget about this
    QtWebView::initialize();

    QWebSocketServer server(
                QStringLiteral("WebSockets example"),
                QWebSocketServer::NonSecureMode
                );
    if (!server.listen(QHostAddress::LocalHost, 55222)) { return 1; }

    // wrap WebSocket clients in QWebChannelAbstractTransport objects
    WebSocketClientWrapper clientWrapper(&server);

    // setup the channel
    QWebChannel channel;
    QObject::connect(&clientWrapper, &WebSocketClientWrapper::clientConnected,
                     &channel, &QWebChannel::connectTo);

    // setup the core and publish it to the QWebChannel
    Backend *backend = new Backend();
    channel.registerObject(QStringLiteral("backend"), backend);

    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("someObject", backend);
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty()) { return -1; }

    return app.exec();
}

Most important thing here is WebSocketClientWrapper (which uses WebSocketTransport underneath). That is something you have to implement yourself, and documentation won’t help you much with that (thank god there is this example at least). Using WebSocketClientWrapper you can finally connect QWebChannel and register your object (Backend in my case, although I kept the same ID - someObject) so it would be available at HTML side. Note, that this time I need to register an already created C++ object (not a type), so I’m using setContextProperty.

index.html:

<script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>

<script type="text/javascript">
    // here will be our QtObject from QML side
    var backend;

    window.onload = function()
    {
        var socket = new WebSocket("ws://127.0.0.1:55222");

        socket.onopen = function()
        {
            new QWebChannel(socket, function(channel) {
                backend = channel.objects.backend;

                // connect to a signal
                backend.someSignal.connect(function(someText) {
                    alert("Got signal: " + someText);
                    document.getElementById("lbl").innerHTML = someText;
                });
            });
        };
    }
</script>

Unlike index.html from WebEngineView example, here first you need to establish WebSocket connection and then use it as a transport for QWebChannel. The rest is all the same.

main.qml:

Text {
    id: txt
    text: "Some text"
    onTextChanged: {
        someObject.someSignal(text)
    }
    Component.onCompleted: {
         someObject.textNeedsToBeChanged.connect(changeText)
    }
    function changeText(newText) {
        txt.text = newText;
    }
}

WebView {
    id: webView
    url: "qrc:/index.html"
}

QML code is a bit different too. First of all, someObject this time is a context property, so there is no need for importing and declaring it. Second of all, interaction between C++ object and QML components required adding one more signal (textNeedsToBeChanged).

Thus, interaction schema became a bit weird as well:

Fortunately, there is a better solution.

Mostly QML

I like this example more, because it’s done mostly in QML, except for a tiny bit on C++. I got this solution as an answer to my question at Stack Overflow.

First we need to implement a transport for WebChannel.

websockettransport.h:

class WebSocketTransport : public QWebChannelAbstractTransport
{
    Q_OBJECT

public:
    Q_INVOKABLE void sendMessage(const QJsonObject &message) override
    {
        QJsonDocument doc(message);
        emit messageChanged(QString::fromUtf8(doc.toJson(QJsonDocument::Compact)));
    }

    Q_INVOKABLE void textMessageReceive(const QString &messageData)
    {
        QJsonParseError error;
        QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error);
        if (error.error)
        {
            qWarning() << "Failed to parse text message as JSON object:" << messageData
                       << "Error is:" << error.errorString();
            return;
        } else if (!message.isObject())
        {
            qWarning() << "Received JSON message that is not an object: " << messageData;
            return;
        }
        emit messageReceived(message.object(), this);
    }

signals:
    void messageChanged(const QString & message);
};

Then register it to QML.

main.cpp:

#include "websockettransport.h"

int main(int argc, char *argv[])
{
    // ...
    
    qmlRegisterType("io.decovar.WebSocketTransport", 1, 0, "WebSocketTransport");

    // ...
}

And the rest is QML.

main.qml:

import io.decovar.WebSocketTransport 1.0

// ...

// an object with properties, signals and methods - just like any normal Qt object
QtObject {
    id: someObject

    // ID, under which this object will be known at WebEngineView side
    WebChannel.id: "backend"

    property string someProperty: "Break on through to the other side"

    signal someSignal(string message);

    function changeText(newText) {
        txt.text = newText;
        return "New text length: " + newText.length;
    }
}

WebSocketTransport {
    id: transport
}

WebSocketServer {
    id: server
    listen: true
    port: 55222
    onClientConnected: {
        if(webSocket.status === WebSocket.Open) {
            channel.connectTo(transport)
            webSocket.onTextMessageReceived.connect(transport.textMessageReceive)
            transport.onMessageChanged.connect(webSocket.sendTextMessage)
        }
    }
}

Text {
    id: txt
    text: "Some text"
    onTextChanged: {
        // this signal will trigger a function at WebView side (if connected)
        someObject.someSignal(text)
    }
}

WebView {
    url: "qrc:/index.html"
}

WebChannel {
    id: channel
    registeredObjects: [someObject]
}

index.html here is the same as in previous example. Create a WebSocket and use it as a transport for QWebChannel.

By the way, as I told you in the earlier, WebView and standalone/external browser are the same thing, so you can just open this index.html in your web-browser and it will work the same way - just don’t forget to remove qrc:/ from code and copy qwebchannel.js to the same folder.

The full source code for all three examples can be found in this repository.

So, what’s the point

Okay, we can interact between HTML (running inside a WebEngineView/WebView/web-browser) and QML (or rather proper QObjects from C++ side). But why? What is the point of this? What problem does it solve?

I have no fucking clue. In my opinion, that was a stupid idea in the first place.

You want to have an HTML-based GUI? Why use Qt at all then? Just take any of bazillion web-frameworks and run everything in browser. Or go totally mental and create applications with a size of 200 MB for running console utilities.

You need a client for your remote backend? Do it with Qt Quick, it even has XMLHttpRequest out of the box. And it will perform much better than HTML-based equivalent.

But why on earth would you need to have HTML-content integrated into your Qt application? I seriously don’t understand.

A few words about documentation

The whole thing is quite poorly documented. I spent around one week going back and forth, trying to make things work, and now I’m not even angry - I’m fascinated.

Qt WebChannel appeared somewhere in Qt 5.4, and here’s a video from Qt Developer Days 2014 about that. Some examples from this video are nowhere to find, and presenter used features that are not part of the Qt (could not be found in the documentation). Same stuff is happening in this article. Sure, those are old materials, and experimental features were changed by the release, but how can we do these things in the released version, where is documentation for that? Because now it really looks like quite a gap - from experimental to?.. To what?

Here’s a good resume for both the video and the article:

No, but seriously, why so many things are not explained in the documentation? Why, despite the fact that there are more than 5 examples for both WebChannel and WebSockets, it is so hard to understand how it works? And why there is no a single example of making it work with QML?

Now, regarding qwebchannel.js. Take a look at the very first paragraph of the documentation page:

To communicate with a QWebChannel or WebChannel, a client must use and set up the JavaScript API provided by qwebchannel.js. For clients run inside Qt WebEngine, you can load the file via qrc:///qtwebchannel/qwebchannel.js. For external clients, you need to copy the file to your web server.

So, for integrated web-views we can use a special resource qrc:///qtwebchannel/qwebchannel.js, but where can we find this file for external clients? Yeah, get fucked, because this file is nowhere to find on this or any other page. Fortunately, you can take it (hopefully, the same one) from some of the examples: qwebchannel.js.

QWebChannelAbstractTransport’s documentation page is also far from being called a detailed one as it does not have a single line of code, not to say about examples. Its necessity for WebChannel is briefly mentioned in a casual way like this:

Note that a QWebChannel is only fully operational once you connect it to a QWebChannelAbstractTransport.

Thanks a lot for the hint, but would you care to elaborate a bit? No? Oh well.

Basically, if it wasn’t for the repository I found and the help I got at Stack Overflow - I simply wouldn’t be able to make everything work.