小拉在实际工作中,接触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_processswap() 方法可以实现打开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工程

附一张实际产品截图

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐