自定义代码生成脚本(大幅减少体力活)

适合大量重复协议字段、表单、配置项

Python / Bash 脚本读取协议文档(Excel/JSON)

自动生成:ViewModel 属性、UI 输入组件、序列化 / 反序列化代码、校验函数

举个场景:你有几十条 GNSS / 对抗协议字段,不用手动逐个写控件 + 绑定,脚本一键生成整套界面绑定代码。 


场景痛点(你当前业务完全匹配)

你的业务特征:

  1. 大量二进制协议结构体干扰控制字、拖引参数、假目标、惯导 / GNSS 定位字段);
  2. 每个字段需要四套重复代码:
    • C++ 结构体定义(序列化 / 反序列化)
    • ViewModel Q_PROPERTY 双向绑定属性
    • Qt Widget 表单控件(标签 + 输入框 / 数值框 / 下拉)
    • 参数校验逻辑(范围、IP、端口、正负、长度)
  3. 字段经常增删改(迭代协议、新增干扰模式),手动改多处极易漏写、改错、同步不一致;
  4. 几十上百个字段,复制粘贴效率极低,维护成本爆炸。

核心思路:一份协议元数据(Excel/JSON)作为唯一数据源,Python 脚本一次性生成全部配套 C++ 代码,后续协议变更只改表格,重新运行脚本即可同步更新所有界面、模型、序列化代码


协议元数据.xlsx (唯一真值,所有代码从这里导出)
        ↓
Python代码生成器(解析表格、模板渲染)
        ↓ 一次性输出4类文件
1. 协议结构体头文件:ProtoStruct.h 打包/解包二进制
2. ViewModel:DevParamViewModel.h 自动Q_PROPERTY绑定
3. 通用表单UI生成代码:ParamFormWidget.h 批量创建输入控件
4. 校验工具函数:ParamValidator.h 各字段范围校验

第一步:定义协议元数据标准(Excel 模板)

新建 proto_meta.xlsx,单张表 param_info,列固定如下,适配 GNSS / 对抗参数:

字段名 数据类型 二进制字节长度 最小值 最大值 控件类型 中文显示名 备注说明
jam_mode uint8_t 1 0 5 ComboBox 干扰模式 0 = 压制 1 = 距离拖引 2 = 速度拖引
drag_acc float 4 -20 20 DoubleSpin 拖引加速度 m/s²
target_num uint16_t 2 1 32 SpinBox 假目标数量
gnss_lon double 8 -180 180 DoubleSpin 经度 WGS84 坐标
dev_ip string 16 "" "" LineEdit 设备 IP IPv4 校验
enable_flag bool 1 0 1 CheckBox 使能开关

字段类型覆盖你业务全部场景: 整型 / 浮点 / 布尔 / 字符串;控件对应 Qt 标准输入控件;内置校验区间。


Python 生成器核心逻辑

1. 依赖库

pip install pandas openpyxl jinja2
  • pandas/openpyxl:读取 Excel 协议表
  • jinja2:C++ 代码模板渲染(核心,避免字符串拼接写代码)

2.目录结构

3.主脚本 gen.py 完整示例

import pandas as pd
from jinja2 import Environment, FileSystemLoader
import os

# 1. 读取协议元数据
df = pd.read_excel("proto_meta.xlsx", sheet_name="param_info")
field_list = df.to_dict("records")

# 2. 初始化jinja模板引擎
env = Environment(loader=FileSystemLoader("template/"), trim_blocks=True, lstrip_blocks=True)

# 3. 渲染并写入各类代码文件
def render_save(tpl_name, out_path, ctx):
    tpl = env.get_template(tpl_name)
    code = tpl.render(ctx)
    with open(out_path, "w", encoding="utf-8") as f:
        f.write(code)
    print(f"生成完成: {out_path}")

# 传入全部字段数据,批量生成
context = {"fields": field_list}
render_save("struct.h.j2", "../src/proto/ProtoStruct.h", context)
render_save("viewmodel.h.j2", "../src/ui/viewmodel/DevParamViewModel.h", context)
render_save("form_widget.h.j2", "../src/ui/common/ParamFormWidget.h", context)
render_save("validator.h.j2", "../src/utils/ParamValidator.h", context)

四大核心模板详解(直接适配你的 GNSS / 对抗业务)

模板 1:struct.h.j2 二进制序列化 / 反序列化模板

自动生成打包、解包函数,适配设备二进制通信

#pragma once
#include <cstdint>
#include <QString>

struct DeviceParam
{
{% for f in fields %}
    {{f.数据类型}} {{f.字段名}}; // {{f.中文显示名}}
{% endfor %}

    // 序列化为二进制字节流
    QByteArray toBytes() const
    {
        QByteArray buf;
{% for f in fields %}
        // {{f.字段名}} 占{{f.二进制字节长度}}字节
{% if f.数据类型 == "uint8_t" %}
        buf.append(static_cast<char>({{f.字段名}}));
{% elif f.数据类型 == "uint16_t" %}
        uint16_t v = qToBigEndian({{f.字段名}});
        buf.append(reinterpret_cast<const char*>(&v), 2);
{% elif f.数据类型 == "float" %}
        float v = {{f.字段名}};
        buf.append(reinterpret_cast<const char*>(&v), 4);
{% elif f.数据类型 == "QString" %}
        buf.append({{f.字段名}}.toUtf8(), {{f.二进制字节长度}});
{% endif %}
{% endfor %}
        return buf;
    }

    // 从二进制解析参数
    static DeviceParam fromBytes(const QByteArray& buf)
    {
        DeviceParam p;
        int offset = 0;
{% for f in fields %}
        // 解析{{f.中文显示名}}
{% if f.数据类型 == "uint8_t" %}
        p.{{f.字段名}} = static_cast<uint8_t>(buf[offset]);
        offset += 1;
{% elif f.数据类型 == "uint16_t" %}
        uint16_t tmp;
        memcpy(&tmp, buf.data()+offset, 2);
        p.{{f.字段名}} = qFromBigEndian(tmp);
        offset += 2;
{% elif f.数据类型 == "float" %}
        memcpy(&p.{{f.字段名}}, buf.data()+offset, 4);
        offset +=4;
{% elif f.数据类型 == "QString" %}
        p.{{f.字段名}} = QString::fromUtf8(buf.data()+offset, {{f.二进制字节长度}}).trimmed();
        offset += {{f.二进制字节长度}};
{% endif %}
{% endfor %}
        return p;
    }
};

运行脚本后,自动生成完整打包解包逻辑,不用手动写 memcpy、大小端转换。

模板 2:viewmodel.h.j2 Qt 双向绑定 ViewModel 模板

自动生成全部 Q_PROPERTY,界面修改自动同步模型,模型更新自动刷新界面

#pragma once
#include <QObject>
#include "proto/ProtoStruct.h"

class DevParamViewModel : public QObject
{
    Q_OBJECT
public:
    explicit DevParamViewModel(QObject* parent = nullptr):QObject(parent){}

    void loadFromStruct(const DeviceParam& src)
    {
{% for f in fields %}
        m_{{f.字段名}} = src.{{f.字段名}};
{% endfor %}
        emit allChanged();
    }

    DeviceParam toStruct() const
    {
        DeviceParam p;
{% for f in fields %}
        p.{{f.字段名}} = m_{{f.字段名}};
{% endfor %}
        return p;
    }

signals:
    void allChanged();
{% for f in fields %}
    void {{f.字段名}}Changed();
{% endfor %}

private:
{% for f in fields %}
    Q_PROPERTY({{f.数据类型}} {{f.字段名}} READ {{f.字段名}} WRITE set{{f.字段名|capitalize}} NOTIFY {{f.字段名}}Changed)
    {{f.数据类型}} m_{{f.字段名}};

    {{f.数据类型}} {{f.字段名}}() const { return m_{{f.字段名}}; }
    void set{{f.字段名|capitalize}}({{f.数据类型}} val)
    {
        if(m_{{f.字段名}} != val)
        {
            m_{{f.字段名}} = val;
            emit {{f.字段名}}Changed();
        }
    }
{% endfor %}
};

效果:上百个字段不用手动写 Q_PROPERTY、get/set、信号,一键生成。

模板 3:form_widget.h.j2 自动生成完整表单 UI 控件

自动创建标签、输入框、布局、信号槽绑定 ViewModel,不用手写几百行控件创建代码

#pragma once
#include <QWidget>
#include <QFormLayout>
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QLineEdit>
#include <QCheckBox>
#include <QComboBox>
#include "viewmodel/DevParamViewModel.h"

class ParamFormWidget : public QWidget
{
    Q_OBJECT
public:
    explicit ParamFormWidget(QWidget* parent = nullptr):QWidget(parent)
    {
        m_layout = new QFormLayout(this);
        m_layout->setContentsMargins(10,10,10,10);
        m_layout->setSpacing(8);
        createAllControls();
    }

    void bindViewModel(DevParamViewModel* vm)
    {
        m_vm = vm;
        bindAllSignals();
    }

private:
    void createAllControls()
    {
{% for f in fields %}
        // {{f.中文显示名}}
{% if f.控件类型 == "SpinBox" %}
        QSpinBox* spin_{{f.字段名}} = new QSpinBox;
        spin_{{f.字段名}}->setRange({{f.最小值}}, {{f.最大值}});
        m_layout->addRow("{{f.中文显示名}}", spin_{{f.字段名}});
        m_controls["{{f.字段名}}"] = spin_{{f.字段名}};
{% elif f.控件类型 == "DoubleSpin" %}
        QDoubleSpinBox* dspin_{{f.字段名}} = new QDoubleSpinBox;
        dspin_{{f.字段名}}->setRange({{f.最小值}}, {{f.最大值}});
        dspin_{{f.字段名}}->setDecimals(2);
        m_layout->addRow("{{f.中文显示名}}", dspin_{{f.字段名}});
        m_controls["{{f.字段名}}"] = dspin_{{f.字段名}};
{% elif f.控件类型 == "LineEdit" %}
        QLineEdit* edit_{{f.字段名}} = new QLineEdit;
        m_layout->addRow("{{f.中文显示名}}", edit_{{f.字段名}});
        m_controls["{{f.字段名}}"] = edit_{{f.字段名}};
{% elif f.控件类型 == "CheckBox" %}
        QCheckBox* ck_{{f.字段名}} = new QCheckBox("{{f.中文显示名}}");
        m_layout->addRow("", ck_{{f.字段名}});
        m_controls["{{f.字段名}}"] = ck_{{f.字段名}};
{% elif f.控件类型 == "ComboBox" %}
        QComboBox* cb_{{f.字段名}} = new QComboBox;
        // 可扩展:从备注解析下拉选项
        m_layout->addRow("{{f.中文显示名}}", cb_{{f.字段名}});
        m_controls["{{f.字段名}}"] = cb_{{f.字段名}};
{% endif %}
{% endfor %}
    }

    void bindAllSignals()
    {
        if(!m_vm) return;
{% for f in fields %}
{% if f.控件类型 == "SpinBox" %}
        auto spin = qobject_cast<QSpinBox*>(m_controls["{{f.字段名}}"]);
        connect(spin, &QSpinBox::valueChanged, m_vm, &DevParamViewModel::set{{f.字段名|capitalize}});
        connect(m_vm, &DevParamViewModel::{{f.字段名}}Changed, spin, &QSpinBox::setValue);
{% elif f.控件类型 == "DoubleSpin" %}
        auto dspin = qobject_cast<QDoubleSpinBox*>(m_controls["{{f.字段名}}"]);
        connect(dspin, &QDoubleSpinBox::valueChanged, m_vm, &DevParamViewModel::set{{f.字段名|capitalize}});
        connect(m_vm, &DevParamViewModel::{{f.字段名}}Changed, dspin, &QDoubleSpinBox::setValue);
{% elif f.控件类型 == "LineEdit" %}
        auto edit = qobject_cast<QLineEdit*>(m_controls["{{f.字段名}}"]);
        connect(edit, &QLineEdit::textEdited, m_vm, &DevParamViewModel::set{{f.字段名|capitalize}});
        connect(m_vm, &DevParamViewModel::{{f.字段名}}Changed, edit, &QLineEdit::setText);
{% endif %}
{% endfor %}
    }

private:
    QFormLayout* m_layout;
    DevParamViewModel* m_vm = nullptr;
    QMap<QString, QWidget*> m_controls;
};

使用时一行代码加载完整参数表单:

DevParamViewModel vm;
ParamFormWidget form;
form.bindViewModel(&vm);

几百个参数不用手动拖拽、手动 connect,脚本一次性生成完整布局与双向绑定。

模板 4:validator.h.j2 自动生成字段校验函数

#pragma once
#include <QString>
#include "proto/ProtoStruct.h"

namespace ParamValidator
{
    // 单字段校验
    bool checkParam(const DeviceParam& p, QString& errMsg)
    {
{% for f in fields %}
        // 校验 {{f.中文显示名}}
{% if f.数据类型 in ["uint8_t","uint16_t","float","double"] %}
        if(p.{{f.字段名}} < {{f.最小值}} || p.{{f.字段名}} > {{f.最大值}})
        {
            errMsg = "{{f.中文显示名}}超出范围,范围{{f.最小值}}~{{f.最大值}}";
            return false;
        }
{% elif f.字段名 == "dev_ip" %}
        // 内置IP校验逻辑
        QString ip = p.dev_ip;
        QStringList seg = ip.split(".");
        if(seg.size() !=4)
        {
            errMsg = "设备IP格式错误";
            return false;
        }
{% endif %}
{% endfor %}
        return true;
    }
}

下发参数前统一校验,不用每个页面重复写范围判断。

我使用上面的代码生成了相关的文件:

PS D:\Code\genCodeTest\code_gen> py gen.py
生成完成: ../src/proto/ProtoStruct.h
生成完成: ../src/ui/viewmodel/DevParamViewModel.h
生成完成: ../src/ui/common/ParamFormWidget.h
生成完成: ../src/utils/ParamValidator.h

详见实例1

现在我们写一个示例来运行程序,实验是否生成的代码可用?

ps: 感觉使用python来干这种代码生成的工作,好方便啊,代码生成也没有那么追求性能

性能完全无关紧要

你的任务: 读取 Excel → 组装字段数据 → Jinja2 渲染 C++ 模板 → 写入文件。 字段最多几百条,整个流程毫秒级完成。 哪怕用最慢的脚本语言,性能瓶颈永远不会出现。

代码生成属于「一次性构建期任务」,不是运行时业务,不需要考虑并发、实时性。

生态开箱即用

你用到的能力:

  • 读取 Excel:pandas / openpyxl
  • JSON/YAML 协议元数据解析:内置支持
  • 模板渲染:Jinja2(和前端模板语法接近,极易写 C++ 模板)
  • 文件遍历、路径处理、文本替换、MD5 对比文件变更 C++ 想要实现同等功能: 需要自己引入 Excel 库、写简陋字符串模板、处理编码、跨平台路径,几百行样板代码; Python 几行搞定。

文本处理、动态数据操作极其舒服

协议字段是结构化表格数据:数组、字典、循环、条件判断。

fields = df.to_dict("records")
for f in fields:
    if f["控件类型"] == "DoubleSpin":
        ...

这种动态分支逻辑,写起来极其顺畅。 而如果你试图用 C++ 写代码生成器,大量字符串拼接极易出错,可读性极差。

模板引擎 Jinja2 是杀手锏

你之前那套 .j2 模板:

{% for f in fields %}
Q_PROPERTY({{f.数据类型}} {{f.字段名}} ...)
{% endfor %}

讲讲模板引擎 Jinja2

Jinja2 是 Python 生态最流行的文本模板引擎,最初为 Web 网页开发设计,但极其适合代码生成。 核心思想: 模板 = 静态文本(C++ 代码) + 嵌入控制逻辑(循环、if、变量) 运行时把 Python 传入的数据填充进模板,输出完整 .h / .cpp 文件。

和单纯字符串拼接最大区别: 你直接在模板里写 C++,不用在 Python 疯狂 +"\n"+ 拼接字符串,可读性拉满。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐