qtqmlqtwebviewqtwebsocketsqtwebchannel

Qt WebView and WebChannel over WebSockets in QML


I want to access QtObject from HTML-page running in WebView - invoke methods, read/write properties, etc.

As far as I understood, I need to establish WebSockets connection between QML and HTML sides, and then use it as a transport for WebChannel.

Don't confuse WebView with WebEngineView - I know how to do it with WebEngineView, but I need to do it with WebView.

So, here's what I have.

QML side

QtObject {
    id: someObject
    WebChannel.id: "backend"
    property string someProperty: “property value"
}

WebSocketServer {
    listen: true
    port: 55222
    onClientConnected: {
        console.log(webSocket.status);
        //webSocket.onTextMessageReceived.connect(function(message) {
        //    console.log(qsTr("Server received message: %1").arg(message));
        //});
    }
}

WebView {
    url: "index.html"
    //webChannel: channel // invalid property name "webChannel"
    //experimental.webChannel.registeredObjects: [someObject] // invalid property name "experimental"
}

WebChannel {
    id: channel
    registeredObjects: [someObject]
}

HTML side

window.onload = function()
{
    // here will be QtObject from QML side
    var backend;

    var socket = new WebSocket("ws://localhost:55222");
    socket.onopen = function()
    {
        //socket.send("some message");
        new QWebChannel(socket, function(channel) {
            backend = channel.objects.backend;
        });
    };
}

function alertProperty()
{
    alert(backend.someProperty);
}

Simple message exchange works fine (socket.send()), so transport is okay, but how do I assign WebChannel to WebView? With WebEngineView it was simple, there is a webChannel property there (and there is even no need in using WebSockets), but there is nothing alike in WebView. I mean, something has to tell WebView that WebChannel contains my QtObject so it would be visible to JS?

And if WebView does not support WebChannel(?), how to do it with external browser then? This example shows that it is possible with C++, but I want to do it with QML.

I use Qt 5.11.1.


Solution

  • WebView does not support WebChannel by default. So the solution is to use WebSocketServer with QWebChannelAbstractTransport as I show below:

    main.cpp

    #include <QGuiApplication>
    #include <QJsonDocument>
    #include <QQmlApplicationEngine>
    #include <QWebChannelAbstractTransport>
    #include <QtWebView>
    
    class WebSocketTransport : public QWebChannelAbstractTransport{
        Q_OBJECT
    public:
        using QWebChannelAbstractTransport::QWebChannelAbstractTransport;
        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);
    };
    
    int main(int argc, char *argv[])
    {
        QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    
        QGuiApplication app(argc, argv);
        qmlRegisterType<WebSocketTransport>("com.eyllanesc.org", 1, 0, "WebSocketTransport");
        QtWebView::initialize();
    
        QQmlApplicationEngine engine;
        engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
        if (engine.rootObjects().isEmpty())
            return -1;
    
        return app.exec();
    }
    
    #include "main.moc"
    

    main.qml

    import QtQuick 2.9
    import QtQuick.Window 2.2
    import QtWebSockets 1.1
    import QtWebView 1.1
    import QtWebChannel 1.0
    import com.eyllanesc.org 1.0
    
    Window {
        visible: true
        width: 640
        height: 480
        title: qsTr("Hello World")
    
        WebView {
            url: "qrc:/index.html"
            anchors.fill: parent
        }
    
        QtObject {
            id: someObject
            property string someProperty: "prop"
            WebChannel.id: "core"
            function receiveText(text){
                console.log("receiveText: ", text)
            }
            signal sendText(string text)
        }
    
        WebSocketTransport{
            id: transport
        }
    
        WebSocketServer {
            listen: true
            port: 12345
            onClientConnected: {
                if(webSocket.status === WebSocket.Open){
                    channel.connectTo(transport)
                    webSocket.onTextMessageReceived.connect(transport.textMessageReceive)
                    transport.onMessageChanged.connect(webSocket.sendTextMessage)
                }
            }
        }
    
        WebChannel {
            id: channel
            registeredObjects: [someObject]
        }
    
        // testing
        Timer{
            interval: 500
            running: true
            repeat: true
            onTriggered: someObject.sendText(Qt.formatTime(new Date(), "hh:mm:ss") + " from QML")
        }
    }
    

    index.html

    <!DOCTYPE html>
    <html>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
            <script type="text/javascript" src="qrc:///qtwebchannel/qwebchannel.js"></script>
            <script type="text/javascript">
                //BEGIN SETUP
                function output(message) {
                    var output = document.getElementById("output");
                    output.innerHTML = output.innerHTML + message + "\n";
                }
                window.onload = function() {
                    if (location.search != "")
                        var baseUrl = (/[?&]webChannelBaseUrl=([A-Za-z0-9\-:/\.]+)/.exec(location.search)[1]);
                    else
                        var baseUrl = "ws://localhost:12345";
    
                    output("Connecting to WebSocket server at " + baseUrl + ".");
                    var socket = new WebSocket(baseUrl);
    
                    socket.onclose = function() {
                        console.error("web channel closed");
                    };
                    socket.onerror = function(error) {
                        console.error("web channel error: " + error);
                    };
                    socket.onopen = function() {
                        output("WebSocket connected, setting up QWebChannel.");
                        new QWebChannel(socket, function(channel) {
                            // make core object accessible globally
                            window.core = channel.objects.core;
                            input.innerHTML = core.someProperty;
                            document.getElementById("send").onclick = function() {
                                var input = document.getElementById("input");
    
                                var text = input.value;
                                if (!text) {
                                    return;
                                }
                                output("Sent message: " + text );
                                input.value = "";
                                core.receiveText(text + " From HTML");
                            }
    
                            core.sendText.connect(function(message) {
                                output("Received message-" + core.someProperty + " : " + message);
                            });
    
                            core.receiveText("Client connected, ready to send/receive messages!");
                            output("Connected to WebChannel, ready to send/receive messages!");
                        });
                    }
                }
                //END SETUP
            </script>
            <style type="text/css">
                html {
                    height: 100%;
                    width: 100%;
                }
                #input {
                    width: 400px;
                    margin: 0 10px 0 0;
                }
                #send {
                    width: 90px;
                    margin: 0;
                }
                #output {
                    width: 500px;
                    height: 300px;
                }
            </style>
        </head>
        <body>
            <textarea id="output"></textarea><br />
            <input id="input" /><input type="submit" id="send" value="Send" onclick="javascript:click();" />
        </body>
    </html>
    

    The complete example can be found in the following link