Qter 发表于 2023-3-25 15:13:24

QCefView + QWebChannel 实现Qt程序中嵌入Web页面

目录

1. 为什么要用QCefView + QWebChannel开发?

2. 自定义WebChannel

3. 示例完整代码

3.1 自定义Transport类

3.2 自定义channel

3.3 交互窗体、注册对象及通讯

3.4 Html页面如下:

4. 运行结果

5. 调试

6. QWebEngine vs QCefView + QWebChannel vs QCefView 对比

1. 为什么要用QCefView + QWebChannel开发?
基于Qt自带的QWebEngineView + QWebChannel开发,常规电脑可以满足需求。但实际项目部署时在个别环境会有兼容性问题,导致有些电脑莫名崩溃,或者出现web加载失败,卡死的情况。

如果系统自带的.netframe版本过低,QWebengineView 编译的程序在windows7无法运行;

网上查相关资料,有部分介绍如下:

(1)QWebEngineView在运行之前需要检查本地硬件环境,硬件必须要支持OpenGL2.0以上的版本,否则无法运行。

(2)机器的显卡和系统所带的显卡驱动不匹配,导致QtWebEngine在渲染时出现了崩溃。用户需要手动更新显卡驱动来解决这个问题。

而QCefView 基于CEF的封装,对硬件要求低,性能好(XP、windowNT和其他Unix、MacOS都可以支持),在显示上规避了QWebEngineView的问题。



对于QCefView的通讯机制,网上有人说当QCefView整合Vue项目时,会有QCefClient找不到的问题,我也本地Demo了一下,这里没有出现问题,即QCefView直接整合Vue项目也是通过的。

但QCefView和WebChannel的通讯调用方式不太一样,还是可以尝试下不同的组合,再根据喜好做个选择。

而且本人感觉,WebChannel方式,调试起来会更方便。



以下是基于QCefView + QWebChannel 的开发方法。



2. 自定义WebChannel
基本原理是通过channel将C++对象暴露给HTML,在HTML中调用qwebchannel.js。 前提是建立transport,QT只提供了一个抽象基类QWebChannelAbstractTransport,需要自己进行实现,官方建议用QWebSocket实现,并给出了实例。

1、实现Transport类,内置一个WebSocket套接字;

2、实现新的channel类,内置一个WebSocketServer;

3、利用新的channel注册C++对象,从而HTML可以使用该对象;

之后使用跟QWebChannel相同.



3. 示例完整代码
3.1 自定义Transport类
websockettransport.h

#ifndef WEBSOCKETTRANSPORT_H
#define WEBSOCKETTRANSPORT_H

#include <QWebChannelAbstractTransport>

QT_BEGIN_NAMESPACE
class QWebSocket;
QT_END_NAMESPACE

class WebSocketTransport : public QWebChannelAbstractTransport
{
    Q_OBJECT
public:
    explicit WebSocketTransport(QWebSocket * socket);
    virtual ~WebSocketTransport();

    void sendMessage(const QJsonObject & message) override;

public slots:
    void textMessageReceived(const QString & message);

private:
    QWebSocket * m_socket;
};

#endif // WEBSOCKETTRANSPORT_H
websockettransport.cpp

#include "websockettransport.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QWebSocket>

/*!
   Construct the transport object and wrap the given socket.
   The socket is also set as the parent of the transport object.
*/
WebSocketTransport::WebSocketTransport(QWebSocket * socket)
      : QWebChannelAbstractTransport(socket)
      , m_socket(socket)
{
    connect(socket, &QWebSocket::textMessageReceived,
            this, &WebSocketTransport::textMessageReceived);
    connect(socket, &QWebSocket::disconnected,
            this, &WebSocketTransport::deleteLater);
}

/*!
   Destroys the WebSocketTransport.
*/
WebSocketTransport::~WebSocketTransport()
{
    m_socket->deleteLater();
}

/*!
   Serialize the JSON message and send it as a text message via the WebSocket to the client.
*/
void WebSocketTransport::sendMessage(const QJsonObject & message)
{
    QJsonDocument doc(message);
    m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact)));
}

/*!
    Deserialize the stringified JSON messageData and emit messageReceived.
*/
void WebSocketTransport::textMessageReceived(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);
}



3.2 自定义channel
websocketchannel.h

#ifndef WEBSOCKETCHANNEL_H
#define WEBSOCKETCHANNEL_H

#include <QWebChannel>

class QWebSocketServer;
class WebSocketTransport;

//继承QWebchannel类,在内部建立socket server与transport中socket的连接

class WebSocketChannel : public QWebChannel
{
    Q_OBJECT
public:
    WebSocketChannel(QWebSocketServer* server);

signals:
    void clientConnected(WebSocketTransport* client);

private slots:
    void handleNewConnection();

private:
    QWebSocketServer* _server;
};

#endif // WEBSOCKETCHANNEL_H
websocketchannel.cpp

#include "websocketchannel.h"

#include <QWebSocketServer>
#include "websockettransport.h"

WebSocketChannel::WebSocketChannel(QWebSocketServer* server)
    :_server(server)
{
    connect(server, &QWebSocketServer::newConnection,
      this, &WebSocketChannel::handleNewConnection);

    connect(this, &WebSocketChannel::clientConnected,
      this, &WebSocketChannel::connectTo);//connectTo槽继承自QWebchannel
}

void WebSocketChannel::handleNewConnection()
{
    emit clientConnected(new WebSocketTransport(_server->nextPendingConnection()));
}

3.3 交互窗体、注册对象及通讯
testCEF02.h

#pragma once

#include <QtWidgets/QWidget>
#include <QTextEdit>
#include <QLineEdit>
#include <QPushButton>
#include <QWebSocketServer>
#include "websocketchannel.h"
#include "QCefView.h"

class testCEF02 : public QWidget
{
    Q_OBJECT

public:
    testCEF02(QWidget *parent = Q_NULLPTR);
    ~testCEF02();

signals:
    void sendMessage(const QString& msg);   // 用信号向Web发送消息

public slots:
    void btnGoClicked();
    void btnSendClicked();
    void receiveMessage(const QString& msg);

private:
    void initView();
    void initConnect();
    void initWebSocket();
    void initWebChannel();

    void displayMsg(const QString& msg);

private:
    QLineEdit* m_edtWeb;
    QPushButton* m_btnGo;
    QCefView* m_cefView;
    QTextEdit* m_edtText;
    QLineEdit* m_edtLine;
    QPushButton* m_btnSend;

    QWebSocketServer* m_server;
    WebSocketChannel* m_channel;
};

testCEF02.cpp

#include "testCEF02.h"
#include <QHBoxLayout>
#include <QVBoxLayout>

testCEF02::testCEF02(QWidget *parent)
    : QWidget(parent)
{
    initView();
    initConnect();
    initWebSocket();
    initWebChannel();
}

testCEF02::~testCEF02()
{
    m_channel->deregisterObject(this);
    delete m_channel;
    m_server->close();
    m_server->deleteLater();
}

void testCEF02::receiveMessage(const QString& msg)
{
    displayMsg("Receive Msg: " + msg);
}

void testCEF02::btnGoClicked()
{
    QString strUrl = m_edtWeb->text();
    QUrl urlCheck(strUrl);
    if (urlCheck.isValid())
    {
      m_cefView->navigateToUrl(strUrl);
    }
}

void testCEF02::btnSendClicked()
{
    displayMsg("Send Msg: " + m_edtLine->text());
    emit sendMessage(m_edtLine->text());
    m_edtLine->clear();
}

void testCEF02::initView()
{
    m_edtWeb = new QLineEdit;
    m_btnGo = new QPushButton("Go");
    m_cefView = new QCefView();
    m_edtWeb->setText("file:///E:/test_code/qt/testCEF02/testCEF02/x64/index.html");

    m_edtText = new QTextEdit;
    m_edtLine = new QLineEdit;
    m_btnSend = new QPushButton("Send");

    QHBoxLayout* webBarLayout = new QHBoxLayout;
    webBarLayout->addWidget(m_edtWeb);
    webBarLayout->addWidget(m_btnGo);

    QVBoxLayout* webLayout = new QVBoxLayout;
    webLayout->addLayout(webBarLayout);
    webLayout->addWidget(m_cefView);

    QHBoxLayout* sendBarLayout = new QHBoxLayout;
    sendBarLayout->addWidget(m_edtLine);
    sendBarLayout->addWidget(m_btnSend);

    QVBoxLayout* qtLayout = new QVBoxLayout;
    qtLayout->addWidget(m_edtText);
    qtLayout->addLayout(sendBarLayout);

    QVBoxLayout* layout = new QVBoxLayout;
    layout->addLayout(webBarLayout, 1);
    layout->addLayout(webLayout, 2);
    layout->addLayout(qtLayout, 2);
    setLayout(layout);
}

void testCEF02::initConnect()
{
    connect(m_btnGo, SIGNAL(clicked()), this, SLOT(btnGoClicked()));
    connect(m_btnSend, SIGNAL(clicked()), this, SLOT(btnSendClicked()));
}

void testCEF02::initWebSocket()
{
    //建立QWebSocketServer,url是ws://localhost:12345
    m_server = new QWebSocketServer(QStringLiteral("QWebChannel Server"), QWebSocketServer::NonSecureMode);
    bool isListened = m_server->listen(QHostAddress::LocalHost, 12345);
    if (!isListened)
    {
      qDebug() << "Failed to open web socket server.";
    }
}

void testCEF02::initWebChannel()
{
    m_channel = new WebSocketChannel(m_server);
    m_channel->registerObject(QStringLiteral("qtClient"), this);
}

void testCEF02::displayMsg(const QString& msg)
{
    m_edtText->setText(m_edtText->toPlainText() + msg + '\n');
}

main函数默认不变,如下:

#include "testCEF02.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    testCEF02 w;
    w.show();
    return a.exec();
}
3.4 Html页面如下:
<!DOCTYPE html>
<html>
    <head>
      <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
      <script type="text/javascript" src="./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() {
                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 qtClient object accessible globally
                        window.qtClient = channel.objects.qtClient;


                        document.getElementById("send").onclick = function() {
                            var input = document.getElementById("input");
                            var text = input.value;
                            if (!text) {
                              return;
                            }

                            output("Sent message: " + text);
                            input.value = "";

                            //调用C++公有槽函数
                            qtClient.receiveMessage(text);
                        }

                        //连接C++信号与javascript函数
                        qtClient.sendMessage.connect(function(message) {
                            output("Received message: " + message);
                        });

                        qtClient.receiveMessage("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;
            }
                        body {
                                background-color: #FAEBD7;
                        }
      </style>
    </head>
    <body>
                <h4>这是Web界面</h4>
      <textarea id="output"></textarea><br />
      <input id="input" /><input type="submit" id="send" value="Send" />
    </body>
</html>



4. 运行结果


5. 调试
这种方式调试方式更加灵活。

直接打开浏览器输入对应地址,也能够跟QT客户端进行通讯,

相比QWebEngineView方式方便很多。

截图如下:



6. QWebEngine vs QCefView + QWebChannel vs QCefView 对比
对比项

显示

通讯

兼容性

调试

与前端集成

资料

QWebEngine

支持

支持

少数电脑较差

一般(除控制台输出外,支持浏览器调试,但需要降级chrome版本)

支持



QCefView + QWebChannel

支持

支持



好(可直接用浏览器调试)

支持



QCefView

支持

支持



差(只有控制台输出,浏览器调试不了)

支持

(网上有资料说不支持,但本地尝试没有问题)



综上,这几种方案中,QCefView + QWebChannel最稳定合适

PS. 单比QCefView与QWebEngine,QCefView在显示,加载和通讯上,支持的操作都比QWebEngine要丰富一些,灵活度更高,但是一般情况下,客户端集成很使用,所以整体对比,从客户端集成角度看,QCefView显示+QWebChannel通讯更适合项目开发。
————————————————
版权声明:本文为CSDN博主「码拉小农虾」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/d_chunyu/article/details/117118058

页: [1]
查看完整版本: QCefView + QWebChannel 实现Qt程序中嵌入Web页面