看到标题,你大概能猜到这篇我想讲述的是什么了。对的,将要同大家分享的是一种目前PC软件常见的开发方案。
前言
1、桌面应用为什么要使用Vue等框架开发,而不再是Qt UI开发?
界面开发我们使用java语言,通过Vue框架快速开发,好处不言而喻,开发速度快,界面美观,同时兼具跨平台特性。其次,网页端与PC应用可同步开发和维护,同步更新,上线快,后期维护成本低。
有朋友会问,Qt的QML和QSS也完全可以做出极其精美和出彩的界面来呀,不可否认,但是这样做还有一个不可比拟的优势:可将项目打包发布到云服务器,后期界面更新、维护更加方便,不必因为界面布局的修改而被动升级更新桌面应用程序。就拿前4.6的全国哀悼日来说,我们的桌面应用也可以变灰,也可以即时做出样式风格的调整。
2、网页加载为什么用QCefView(或者CEF),而不是Qt自带的 QWebEngineView或者QWebKit组件?
不再使用QWebEngineVIew 和QWebKit,实在是因为笔者在实际的项目开发中遇到了太多的坑。
QWebKit : 如果是出学Qt,需要用到Qt加载和渲染网页的朋友,Qt5以下的版本,你可以尝试QWebKit套件,真心讲,如果不做Qt与JS之间的频繁通讯,webkit组件够用,而且系统和平台的兼容性也比较好,使用简单,开发速度也很快。前期我在使用Qt4.8.3的版本开发过的一个项目中,亦使用webkit加载百度地图,实现锚点、动态信息展示等,效果还可以,但是不得不否认他存在严重的内存泄露,这也是为什么Qt5以后,QWebKit组件被放弃的原因。但是细心的朋友可以看到WPS实际上也是使用了Qt4+webkit的方案,但是内存上泄漏并不严重,WPS做了某些处理就不得而知了。
QWebEngineView:不使用QWebEngineView的原因很简单,一个是只支持MSVC编译工具链,接口不丰富,无法和QFrame进行交互;其次就是系统兼容性真的很不友好。如果系统自带的.netframe版本过低,QWebengineView 编译的程序在windows7无法运行;
笔者曾在windows7下部署 运行调试环境,在QWebEngineView实例化对象时总是崩溃,后来发现直接和根本原因是跟计算机显卡驱动有关系。
(1)QWebEngineView在运行之前需要检查本地硬件环境,硬件必须要支持OpenGL2.0以上的版本,否则无法运行。
(2)机器的显卡和系统所带的显卡驱动不匹配,导致QtWebEngine在渲染时出现了崩溃。用户需要手动更新显卡驱动来解决这个问题。
QCefView : 基于CEF的封装,对硬件要求低,性能好(XP、windowNT和其他Unix、MacOS都可以支持)。CEF提供了很多复杂精细的功能,相对比较难理解,但是QCefView做了封装之后,难度减少很多 。爱打Dota1的朋友可以看看 09电竞平台使用的也是Qt+libcef的方案。
3、为什么使用QWebChannel来实现Qt与JS的通讯,而不是我们在上一篇(QCefView(2)—— 实现Qt与JS之间的通讯)中向大家介绍的方法?
首先说明,上一篇讲诉的方法还是很不错的,参照demo,如果是加载本地的HTML页面,值得推荐:但是在我们使用Vue框架时,会报出QCefClient未定义的问题,原因是Vue中并没有加载此模块,自然无法识得;不需要使用QCefClient的方法也有,如果你有足够的能力,可以在CEF的基础上自己进行封装,看到简书上有大神的教程过,可以去做参考下。所以QWebChannel才成为了备选方案,一个是因为Qt已经为QWebChannel做了足够的完善,在Vue中我们也可以很方便的通过
import { QWebChannel } from "@calvinscofield/qwebchannel"
进行导入,其次就是其通讯基于本地的websocket进行,实时性也很高,操作起来简单明了,故而成了明智之选。还是说到 09电竞平台,你可以看到其通讯部分实际上也是采用了QWebChannel组件。
项目开发
1、在Vue中进行通讯配置
伪代码如下:
import { QWebChannel } from "@calvinscofield/qwebchannel"
...
/// 譬如在某个按钮点击时调用add()发送消息给Qt
add() {
this.websocketsend()
},
// websocket连接
initWebpack() {
//初始化websocket
if ("WebSocket" in window) {
const baseUrl = "ws://localhost:32145";
this.websock = new WebSocket(baseUrl); //这里面的this都指向vue
this.websock.onopen = this.websocketopen;
this.websock.onmessage = this.websocketonmessage;
this.websock.onclose = this.websocketclose;
this.websock.onerror = this.websocketerror;
} else {
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
},
websocketopen() {
//打开
console.log("WebSocket连接成功");
new QWebChannel(this.websock, function(channel) {
window.bound = channel.objects.CPPJSInterface //连接qt信号
window.bound.CPPToJSFunc.connect(function(returnValue, val) {
//通过信号接收返回值
alert("returnValue=" + returnValue + val)
})
})
},
websocketsend() { // 数据发送
var str1 = 80
var str2 = "world"
window.bound.CPPJSInterfaceFunc(str1, str2)
},
websocketonmessage(e) {
//数据接收
},
websocketclose(e) {
//关闭
console.log("connection closed (" + e.code + ")");
console.log("WebSocket关闭");
},
websocketerror() {
//失败
console.log("WebSocket连接失败");
},
2、在Qt中进行页面加载和通讯模块编写
2.1 通过QCefView加载网页
QHBoxLayout* layout = new QHBoxLayout();
layout->setContentsMargins(2, 2, 2, 2);
layout->setSpacing(3);
#if defined(LOCAL_TEST) // test local html
QDir dir = QCoreApplication::applicationDirPath();
QString uri = QDir::toNativeSeparators(dir.filePath("QCefViewTestPage.html"));
cefview = new CustomCefView(uri, this);
#else // load webpage
cefview = new CustomCefView(CUR_IOT_URL, this); // www.unimatiot.com
#endif //LOCAL_TEST
layout->addWidget(cefview);
centralWidget()->setLayout(layout)
2.2 通过QWebChannel实现通讯
(1)在Qt上,创建一个本地websocket服务器端,网页端则是对应的websocket客户端,注意端口号一致。
(2)注意启动顺序,应先启动websocket服务器,然后再实例化窗体对象(窗体中包含了QCefView的子对象,会在窗体对象实例化过程中加载网页,即启动websocket客户端程序),否则会导致网页上websocket客户端无法连接到本地websocket服务器。
(3)注册对象:
channel.registerObject(QStringLiteral("CPPJSInterface"), &core);
那么,在JS中便可以通过”CPPJSInterface“ 对象来调用 Qt中 core class中的槽函数实现 JS发送消息给Qt了。
int main(){
QApplication a(argc, argv);
QFileInfo jsFileInfo(QDir::currentPath() + "/qwebchannel.js");
if (!jsFileInfo.exists()) QFile::copy(":/qtwebchannel/qwebchannel.js", jsFileInfo.absoluteFilePath());
// setup the QWebSocketServer
QWebSocketServer server(QStringLiteral("QWebChannel Standalone Example Server"), QWebSocketServer::NonSecureMode);
if (!server.listen(QHostAddress::LocalHost, 32145)) {
qFatal("Failed to open web socket server.");
return 1;
}
// wrap WebSocket clients in QWebChannelAbstractTransport objects
WebSocketClientWrapper clientWrapper(&server);
// setup the channel
QWebChannel channel;
QObject::connect(&clientWrapper, &WebSocketClientWrapper::clientConnected,
&channel, &QWebChannel::connectTo);
UManager w;
// setup the core and publish it to the QWebChannel
Core core(&w);
// 注册通讯对象==》QWebChannle进行通讯很重要的一点
channel.registerObject(QStringLiteral("CPPJSInterface"), &core);
w.show();
return a.exec();
}
在core.h中我们做消息的收发处理:
#ifndef CORE_H
#define CORE_H
#include "umanager.h"
#include <QObject>
/*
An instance of this class gets published over the WebChannel and is then accessible to HTML clients.
*/
class Core : public QObject
{
Q_OBJECT
public:
Core(UManager *dialog, QObject *parent = nullptr)
: QObject(parent), m_dialog(dialog)
{
connect(dialog, &UManager::sendText, this, &Core::sendText);// Qt send msg to js ,connect signal A with signal B
connect(dialog, &UManager::CPPToJSFunc, this, &Core::CPPToJSFunc);
connect(this, &Core::textRecived, dialog, &UManager::CPPJSInterfaceFunc); // 接收到的消息将转发到UManager进行分发处理
}
signals:
/*
This signal is emitted from the C++ side and the text displayed on the HTML client side.
*/
void sendText(const QString &text);
/****************************************************************************/
void CPPToJSFunc(int index, QString paramters); // send message from cpp to js.
/****************************************************************************/
void textRecived(int index, QString paramters); // core对象接收到JS端发送的消息会触发此信号
public slots:
/*
This slot is invoked from the HTML client side and the text displayed on the server side.
*/
void receiveText(const QString &text)
{
m_dialog->displayMessage(UManager::tr("Received message: %1").arg(text));
sendText(text.toUpper());
}
/*******************************************************/
void CPPJSInterfaceFunc(int index, QString paramters) {
emit textRecived(index, paramters);
}
/*******************************************************/
private:
UManager *m_dialog;
};
#endif // CORE_H
本篇先暂时到这儿,有疑问或者有好的建议的请留言不吝指教。谢谢。后面接着讲项目开发。