Qt WebView and WebChannel over WebSockets in QML

2019-06-04 06:16发布

问题:

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.

回答1:

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