打通 C/C++与JavaScript通信 快速开发Electron桌面应用
小拉实际工作中,有时需要开发串口、网络等工具类上位机桌面应用软件,业务逻辑通常是C、C++开发的,当然使用QT、MFC等技术来开发,怎奈羡慕前端做界面又美观、又速度,有大量的UI框架,图表库使用。笔者找到了一条解决方案业务逻辑由C/C++实现,界面由纯html5实现,应用QWebchannel打通C++与JS的通信。..................
小拉在实际工作中,接触C、C++多些,有时也需要开发串口、网络等工具类上位机桌面应用软件,当然可以直接使用QT、MFC等技术来开发也很方便;
怎奈接触到前端技术后,羡慕前面做界面又美观、又快速,还有大量的UI框架,图表库使用。如果能够实现业务逻辑由C/C++开发,界面由纯html5开发,该有多好;
我试着找到了还算好用的解决方案: 应用QWebchannel打通C++与JS的通信,Electron开发桌面应用
技术栈:
1、业务处理:Qt Console Application + WebSocketServer + QWebchannel
2、界面显示:Electron(Tauri) + Vue + vite +TS + WebSocket(html) + QWebchannel.js
解释:
1、为啥不支持用Qt + WebEngine 开发呢?
因为前端的热更新实在太爽,开发效率高! 边写代码,界面实时更新。
2、是不可以采用Duilib + cef的方式来开发?
Duilib + cef / MFC + WebView2 / WPF + WebView2 方式类似 QT + WebEngine的方案
实现步骤:
Electron 工程建立
使用electron 任意脚手架创建都可以, 传送门 使用vue 框架,构建工具使用 vite
npm create electron-vite
// 安装依赖
npm install
// 安装qwebchannel.js
npm install qwebchannel
// 启动前面项目
npm run dev
由于工程是使用TS开发的,实现类型的声明文件,官方库没有,为了有智能提示,需要自已添加,js的项目工程不需要,在node_modules/qwebchannel
下添加 index.d.ts
文件,复制如下内容
declare module 'qwebchannel' {
export const enum QWebChannelMessageTypes {
signal = 1,
propertyUpdate = 2,
init = 3,
idle = 4,
debug = 5,
invokeMethod = 6,
connectToSignal = 7,
disconnectFromSignal = 8,
setProperty = 9,
response = 10,
}
interface WebChannelTransport {
send(data: any, cb?: (err?: Error) => void): void;
onmessage: (payload: { [key: string]: any }) => void
}
export type QWebChannelTransport = {
webChannelTransport: any;
}
export class QWebChannel {
constructor (transport: EventEmitter, initCallback: (channel: QWebChannel) => void);
objects: any;
send(data: any): void;
exec(data: any, callback: (data: any) => void): void;
handleSignal(message: MessageEvent): void;
handleResponse(message: MessageEvent): void;
handlePropertyUpdate(message: MessageEvent): void;
}
}
Qt工程建立
为了程序打包之后体积小一些,QT开发的程序作为子进程嵌入electron中,这里使用Qt console Application。 先在qmake的配置工程文件中加入webchannel、websockets两个模块,可以参考官方示例standalone
的实现, 将websockettransport.h
websockettransport.cpp
websockettransport.h
websockettransport.cpp
这些官方提供的类加入工程
QT -= gui
QT += webchannel websockets
新建一个通信处理类, Core.h Core.cpp, 为实现模块C++的主动发消息动作,我这里使用的定时器,如果有中文乱码问题,注单加入宏
#pragma execution_character_set("utf-8")
指定编译编码方式为utf8
core.h
#ifndef CORE_H
#define CORE_H
#include <QObject>
#include <QJsonObject>
#include <QDebug>
#include <QTimer>
// 解决中文乱码问题
#pragma execution_character_set("utf-8")
class Core : public QObject
{
Q_OBJECT
public:
explicit Core(QObject *parent = nullptr);
signals:
// c++ 使用signal 主动发送给js的数据
void sendText(const QString &text);
void sendNum(int x);
void sendPerson(QJsonObject o);
public slots:
// 注册slots提供js的直接调用的接口,可以有返回值
// 由于是异步通信,js端使用promise方式调用
// 接收JS传递过来的字符串
void receiveText(const QString &text)
{
qDebug() << "receiveText" << text;
}
// 接收一个整数,返回一个整数
int get_num(int x);
// 无参数,返回参数为一个对象
QJsonObject get_result();
private:
QTimer *timer;
int count = 0;
private slots:
void update();
};
#endif // CORE_H
core.cpp
#include "core.h"
Core::Core(QObject *parent)
: QObject{parent}
{
timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(update()));
timer->start(3000);//start之后,每隔一秒触发一次槽函数
}
// 接收JS传的字符串参数
void Core:: receiveText(const QString &text)
{
qDebug() << "receiveText" << text;
}
// 返回参数为Number
int Core:: get_num(int x)
{
qDebug() << "get_num" << "";
return x + 1;
}
// 返回一个json对象
QJsonObject Core:: get_result()
{
qDebug() << "get_result" << "";
return QJsonObject
{
{"name","张三"},
{"age", 123}
};
}
void Core::update()
{
count++;
int id = count % 3;
if(id == 0)
{
emit sendText("C++发来的文本");
}
if(id == 1)
{
emit sendNum(count);
}
if(id == 2)
{
QJsonObject obj;
obj.insert("name", "张三333");
obj.insert("version", count);
obj.insert("windows", true);
emit sendPerson(obj);
}
}
main.cpp
建立一个websocket服务器,注册Core对象到WebChannel
#include <QCoreApplication>
#include <QWebChannel>
#include <QWebSocketServer>
#include "./shared/websocketclientwrapper.h"
#include "./shared/websockettransport.h"
#include "./shared/core.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 建立一个websocket服务器
QWebSocketServer server(QStringLiteral("QWebChannel Standalone Example Server"), QWebSocketServer::NonSecureMode);
if (!server.listen(QHostAddress::LocalHost, 12345)) {
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);
// 将通信类注册到channel
Core core = Core();
channel.registerObject(QStringLiteral("core"), &core);
return a.exec();
}
打通C++ 与 JS通信
前端作为WebSocket
客户端,可以直接使用html支持的WebSocket
也可以使用js实现的ws模块(npm install ws来安装),由于使用的是Vue,采用Vue插件方式,让Vue自动调用来建立client. 使用mitt
来作JS端的数据消息总线,将C++发来的消息发送出去,VUE接收。
也可以使用pinia
实现一个响应式属性。vue界面直接使用。 当websocket没有连接成功或中断时,通过ws.onclose事件重新建立连接可以实现自动重连。
新建一个 qwebchannel
文件夹 添加 一个index.ts
文件,复制以下内容:
import { App } from "vue";
import { QWebChannel } from "qwebchannel";
import { coreStore } from "../store/core";
import mitt, { Emitter } from "mitt";
let core: any = null;
type Person = {
name:string;
age:number;
}
type Events = {
send_text: string;
send_num: number;
send_person: Person
};
let mitter: Emitter<Events> = mitt<Events>();
let setupCore = (core: any) => {
const _coreStore = coreStore();
core.sendText.connect(function (message: string) {
//alert(message);
_coreStore.msg = message;
mitter.emit("send_text", message);
});
core.sendNum.connect(function (x: number) {
// alert(x);
_coreStore.num = x;
mitter.emit("send_num", x);
});
core.sendPerson.connect(function (x: any) {
// alert(x);
_coreStore.num = x;
mitter.emit("send_person", x);
});
//app.config.globalProperties.$core = core;
};
let setup_in_ws = () => {
const wsUrl: string = "ws://localhost:12345";
const ws = new WebSocket(wsUrl);
ws.onopen = (e) => {
// is_connected = true;
new QWebChannel(ws, function (channel: QWebChannel) {
core = channel.objects.core;
setupCore(core);
});
};
ws.onerror = (e) => {
console.error("onerror,正在重连")
// setup_in_ws()
}
ws.onclose = () => {
console.error("close,正在重连")
setup_in_ws()
}
};
export default {
install(app: App) {
setup_in_ws();
},
};
let receiveText = (msg: string) => {
core.receiveText(msg);
};
let get_num = (n: number) => {
//alert("get_num " + n);
return core.get_num(n);
}
let get_result = () => {
//alert("get_result");
let result = core.get_result();
return result;
}
export { receiveText, mitter,get_result , get_num};
下面是的 App.vue
的代码
<script setup lang="ts" allowjs="true">
import { ref, onMounted } from "vue";
import { receiveText, mitter, get_result, get_num } from "./qwebchannel";
import { coreStore } from "./store/core";
import { layer } from "@layui/layer-vue";
const _coreStore = coreStore();
const message = ref<string>("");
const click_me = () => {
receiveText("ssss");
};
type person = {
name: string;
age: number;
};
const on_click_get_result = () => {
let x = get_result().then((result: person) => {
// alert("on_click_get_result 1-- " + result.name + "age: " + result.age);
let content =
"on_click_get_result 1-- " + result.name + "age: " + result.age;
layer.notifiy({
title: "收到结果",
content: content,
});
});
};
const on_click_get_number = () => {
let x = get_num(3).then((result: number) => {
console.log("get_num -- " + result);
layer.notifiy({
title: "收到结果",
content: result,
});
});
};
mitter.on("send_num", (n: number) => {
message.value = n + "";
console.log("send_num -- " + n);
layer.notifiy({
title: "收到C++发来的数字",
content: n,
});
});
mitter.on("send_text", (str: string) => {
message.value = str;
console.log("send_text -- " + str);
layer.notifiy({
title: "收到C++发来的文本",
content: str,
});
});
mitter.on("send_person", (str: any) => {
message.value = str;
console.log("name: " + str.name);
console.log("age: " + str.age);
console.log("address: " + str.address);
console.log("sex: " + str.name);
layer.notifiy({
title: "收到C++发来的对象",
content: str,
});
});
onMounted(() => {
//setup_in_ws();
});
</script>
<template>
<div>
<br>
<p> C++ 发送的消息: </p>
<br>
{{ message }}
<br />
<lay-button type="normal" @click="click_me">向c++发送消息</lay-button>
<br />
<br />
<lay-button type="normal" @click="on_click_get_result">调用C++ get_result() 接收返回JsonObject</lay-button>
<br />
<br />
<lay-button type="normal" @click="on_click_get_number">调用C++ get_number() 接收返回Number</lay-button>
</div>
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.logo-box {
display: flex;
width: 100%;
justify-content: center;
}
.logo-box span {
width: 74px;
}
.static-public {
display: flex;
align-items: center;
justify-content: center;
}
.static-public code {
background-color: #eee;
padding: 2px 4px;
margin: 0 4px;
border-radius: 4px;
color: #304455;
}
</style>
Electron启动Qt应用作为子进程
使用child_process
,swap()
方法可以实现打开electron程序时, 自动将qt子进程启动,electron进程关闭时自动关闭子进程,用户感觉不到qt的子进程的存在。
在electron工程中新建一个server
文件夹,将release
版本的Qt console Application复制到该文件夹下, 使用
windeployqt`工具将这个程序依赖的dll添加进来,这样这个应用才能运行,效果图下
在electron的main.ts
中将这个子进程启动, 核心代码如下
import { spawn } from 'child_process';
export const ROOT_PATH = {
server: join(__dirname, app.isPackaged ? '../../../../server': '../../../server'),
}
// 子进程的路径
const serverPath = join(ROOT_PATH.server, 'channel_server.exe')
async function createWindow() {
// 可以electron创建主窗口之间创建qt开发的子进程
spawn(serverPath);
// electron主窗口
win = new BrowserWindow({...})
}
将Qt 应用一起打包,制作 Electron应用安装包
Electron
项目使用的是electron-builder
来生成应用安装包的,如果想把qt的应用一起打包,需要修改打包配置文件 ,在配置文件electron-builder.json5
中添加 extraResources: ["server"],
这样就可以上边添加的server
文件夹一起打包了。
全部配置:
{
appId: "YourAppID",
asar: true,
directories: {
output: "release/${version}",
},
files: ["dist"],
mac: {
artifactName: "${productName}_${version}.${ext}",
target: ["dmg"],
},
// 打包的文件夹
extraResources: ["server"],
win: {
target: [
{
target: "nsis",
arch: ["x64"],
},
],
artifactName: "${productName}_${version}.${ext}",
},
nsis: {
oneClick: false,
perMachine: false,
allowToChangeInstallationDirectory: true,
deleteAppDataOnUninstall: false,
},
}
制作安装包
npm run build
效果界面
收到了C++主动发类的消息,文件和JSON对象
点击【get_number】按钮,收到了C++返回的数字结果 4
代码资源分享
1、electron工程
2、qt工程
附一张实际产品截图
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)