恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于Qt与OpenGL的CNC路径显示工具:G代码预处理与三维可视化实现
首页
资讯中心
/
基于Qt与OpenGL的CNC路径显示工具:G代码预处理与三维可视化实现
基于Qt与OpenGL的CNC路径显示工具:G代码预处理与三维可视化实现
发布时间:2026/9/17 10:39:34
简介基于Qt开发的CNC加工CAM辅助显示工具面向数控编程人员、CNC操作员及Qt桌面应用开发者。工具支持加载并显示CNC文件路径直观呈现控制面板与对应功能同时允许用户按需配置显示参数便于在加工前检查刀路与程序逻辑。压缩包共302个文件约41.7MB。以C源码cpp/h、Qt界面文件ui、资源文件png/svg/ico、工程配置pro/vcxproj为主另含可直接运行的exe程序与glsl着色器文件。源码结构完整涵盖主窗口、设置面板、G代码预处理、OpenGL图形绘制等模块。目前已有491人学习下载。对于想学习Qt图形界面编程、OpenGL绘制刀路或CNC/G代码解析的读者这份资源提供了从界面设计到渲染实现的完整工程参考可编译调试并二次开发省去从零搭建环境的时间。1. 用 Qt 重建 CNC 路径显示工具先把坐标这件事想清楚做 CAM 后处理和机床上机之前总得有一个工具能先把 nc 文件快速可视化。直接看 G 代码文本1000 行以后基本失去空间感连上实际机床空跑又太慢撞刀风险不可控。这套由 Qt Widgets 和 OpenGL 组合实现的 nc 显示工具源码里包含 frmmain、frmsettings、glwidget、gcodedrawer、gcodepreprocessorutils 等模块覆盖了文件读取、G 代码预处理、三维路径绘制和显示参数配置的完整链路。文中的代码片段和参数设置可以直接复制到自己的 Qt 工程里验证适合正在做 CAM 嵌入式工具、数控系统上位机或想把二维显示改造为三维预览的工程师。下面按模块逐个拆开讲。2. Qt 工程骨架与 OpenGL 画布frmmain 和 glwidget 的职责划分工程目录里出现moc_predefs.h.cbt、qrc_images.cpp、moc_frmmain.cpp这类文件说明这不是一个随手写的脚本项目而是标准 Qt Widgets qmake 构建出来的桌面程序。moc_predefs.h.cbt是 moc 阶段的预编译头缓存qrc_images.cpp由 rcc 资源编译器生成负责把资源文件里的图标和样式表内嵌到可执行文件里。自己用 CMake 写构建时这些生成文件不会出现在源码树里但接手 qmake 工程时要清楚它们的来源不要去手改。文件职责frmmain.cpp主窗口负责文件打开、界面调度与信号转发moc_frmmain.cppmoc 元对象编译器生成的信号槽元数据不允许手改glwidget.cppOpenGL 画布处理视角旋转、缩放与重绘gcodedrawer.cpp把预处理后的指令流转换成 OpenGL 顶点缓冲gcodepreprocessorutils.cppG 代码文本解析、坐标规范化、圆弧变换frmsettings.cpp显示参数面板与参数持久化2.1 moc 系统在自定义控件中的实际作用凡是在类声明里写了 Q_OBJECT 的 QWidget 派生类构建时都会额外编译一个 moc_xxx.cpp。frmmain 和 glwidget 都使用了信号槽所以目录里能看到对应的 moc 文件。排查链接错误时出现undefined reference to vtable for ...就说明 moc 文件没有被正确生成qmake 下检查 .pro 文件是否写了QT widgets openglCMake 下则要确认set(CMAKE_AUTOMOC ON)否则自定义控件链接不过。我一般遇到按钮点击没反应时会先翻开moc_frmmain.cpp里的qt_static_metacall函数确认槽函数索引与信号发射是否匹配。这里比打断点更快能看到信号参数类型是否和槽函数签名一致很多“连了槽但没执行”的问题其实都是参数类型不匹配信号发不出去。2.2 用 QOpenGLWidget 搭建可旋转的绘制画布GLWidget 类的骨架可以这样写#include QOpenGLWidget #include QOpenGLFunctions #include QMatrix4x4 #include QWheelEvent #include QMouseEvent class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions { Q_OBJECT public: explicit GLWidget(QWidget *parent nullptr); protected: void initializeGL() override; void paintGL() override; void resizeGL(int w, int h) override; void mousePressEvent(QMouseEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; void wheelEvent(QWheelEvent *event) override; private: QMatrix4x4 m_projectionMatrix; QQuaternion m_rotation; float m_zoom 1.0f; };initializeGL 里第一步要调用基类的 initializeOpenGLFunctions把当前平台的 OpenGL 函数指针加载进来。然后设置背景色并开启深度测试void GLWidget::initializeGL() { initializeOpenGLFunctions(); glClearColor(0.10f, 0.10f, 0.12f, 1.0f); glEnable(GL_DEPTH_TEST); }背景色不要用纯黑CNC 路径线用高亮色绘制时深灰底比纯黑底更不刺眼。paintGL 里先清屏再绑定绘制器传来的顶点缓冲最后画线。投影矩阵的 near 平面不要设太小CNC 路径动辄几百毫米甚至几米near0.1 时深度缓冲精度不足远端线条会出现肉眼可见闪烁。常见做法是让 near 取路径包围盒对角线的 1/1000far 取对角线的 10 倍把深度缓冲利用率撑满。2.3 主窗口的信号槽调度与线程边界frmmain 的职责是接收文件路径、把参数变化转发给 GLWidget并维护“打开文件→预处理→重建缓冲→重绘”这条链路。打开文件用 QFileDialog 选 .nc/.cnc/.tap选中后不要在槽函数里直接解析几十 MB 的 nc 文件在主线程解析会让界面卡死。常见做法是引入一个 QThread 工作线程主窗口发信号过去connect(this, FrmMain::filePathChanged, m_parseWorker, ParseWorker::loadFile, Qt::QueuedConnection);解析完成后的结果信号同样用队列连接发回 GUI 线程。需要注意跨线程传长字符串时用 QString 值传参不要在线程里持有界面控件的引用。解析过程中如果用户切换了文件要在 worker 里检查一个 QAtomicInteger 取消标志否则两个请求先后到达展示结果顺序可能颠倒。这个在设计阶段不处理后期复现 bug 非常头疼。3. gcodepreprocessorutils 解析链路模态指令、坐标补全与圆弧换算预处理器是整个工具的地基。CAM 后处理器输出的 nc 文件中运动指令通常只写变化的坐标例如上一行是G01 X100. Y0.下一行只有X102.含义是 Y 保持 0 不动。直接按字面量绘制会把缺失轴当 0 处理路径完全变形。除此之外G02/G03 圆弧还有 I/J/K 偏移和 R 半径两种写法G90/G91 决定坐标是绝对还是增量不统一到内部数据结构绘制层没法做连续轨迹。3.1 指令行拆分与字段识别先定义内部指令结构体struct GCodeLine { int lineNumber 0; QString rawText; std::mapchar, double addresses; // G/M/X/Y/Z/I/J/K/F/S/T bool isMotion false; bool isRapid false; bool isArc false; };解析时按行读取先去注释分号后缀内容忽略然后逐个字符扫描字母和数字。这里有个常见坑X0.5Y0.8这种没有空格的写法很常见所以不能按空格 split。扫描规则是遇到字母就开启新字段遇到数字就拼进当前字段的值。GCodeLine GCodePreprocessorUtils::parseLine(const QString line, int number) { GCodeLine result; result.lineNumber number; result.rawText line; QString cleaned line.section(/*;*/;, 0, 0).trimmed(); char currentAddr 0; QString value; for (QChar ch : cleaned) { if (ch.isLetter()) { if (currentAddr ! 0) { result.addresses[currentAddr] value.toDouble(); value.clear(); } currentAddr ch.toLatin1(); } else if (ch.isDigit() || ch . || ch - || ch ) { value.append(ch); } } if (currentAddr ! 0) { result.addresses[currentAddr] value.toDouble(); } return result; }上面的代码在遇到字母时把上一个字段写入 map再切换到新地址。value清空时机要放在写入之后否则下一个字段会把上一个字段的内容混进来。addresses用 map 而不是 vector 的另一个原因是同一行里某些地址不会重复出现用 key 查找坐标时更直接。3.2 模态指令状态机与坐标补全G 代码的特点是状态保持。G0/G1 是模态指令下一行如果不写 G 就直接沿用上一行运动模式。预处理器需要维护一份 currentState保存当前运动模式、绝对/增量模式G90/G91、当前平面G17/G18/G19、进给率和主轴转速。struct GCodeState { double x 0.0, y 0.0, z 0.0; int motionMode 0; // 0: 无运动, 1: G0, 2: G1 int plane 17; // G17 XY 平面 bool absoluteMode true; // G90 }; void GCodePreprocessorUtils::normalizeLine(GCodeLine raw, GCodeState state) { auto itG raw.addresses.find(G); if (itG ! raw.addresses.end()) { int gCodeVal static_castint(itG-second); if (gCodeVal 0 || gCodeVal 1) state.motionMode gCodeVal 1; else if (gCodeVal 90) state.absoluteMode true; else if (gCodeVal 91) state.absoluteMode false; } double targetX raw.addresses.count(X) ? raw.addresses[X] : state.x; double targetY raw.addresses.count(Y) ? raw.addresses[Y] : state.y; double targetZ raw.addresses.count(Z) ? raw.addresses[Z] : state.z; if (!state.absoluteMode) { targetX state.x targetX; targetY state.y targetY; targetZ state.z targetZ; } state.x targetX; state.y targetY; state.z targetZ; }很多新手不理解为什么要补全坐标绘制器渲染的是折线或圆弧集合如果某条记录里只有 X 变化没有补全 Y 和 Z绘制时要么丢掉这条线段要么从上一个零坐标画出一条错误对角线。预处理器里补全坐标后绘制层拿到的每一条运动指令都是完整的三轴坐标轨迹才是连续的。3.3 圆弧插补I/J/K 与 R 的统一换算圆弧路径在 CAM 输出中有两种常见形式I/J/K 表示相对起点的圆心偏移R 表示半径。绘制器需要圆心坐标R 形式必须几何反推。写法字段含义圆心推算方式IJK 模式I/J/K 是圆心相对起点的增量圆心 起点 I/J/KR 模式R 是圆弧半径根据起点、终点和方向反推圆心注意左右侧判定两种形式混在一个文件里时最好统一成圆心描述。注意三个边界条件起点到终点的距离大于 2 倍半径时是非法圆弧继续换算会出现 NaNG02 是顺时针G03 是逆时针圆心位置在弦的哪一侧与方向相关I/J/K 在绝对坐标模式下仍按增量语义解释不要直接叠加到当前坐标。#### 3.3.1 R 模式圆心反推的实用写法 把 R 换算成圆心时我一般先计算弦长 L再算出弦的中点和法向偏移量 H cpp double L QLineF(start, end).length(); double H qSqrt(qMax(0.0, r * r - (L * L / 4.0))); // 圆心在中垂线上方向由 G02/G03 决定这里要留意qMax(0.0, ...)半径不足时不能抛异常而是直接把圆弧退化成直线段。预处理层的职责是尽量保证输出数据结构合法绘制层才不至于因为负开方崩溃。4. gcodedrawer 绘制管线顶点缓冲、分层着色与视角交互gcodedrawer.cpp 的职责是接收预处理后的指令流把每条运动指令转换成线段或圆弧离散点再组装进 OpenGL 缓冲。这个模块最影响观感。把颜色写进同一顶点缓冲也就是常见的 interleaved vertex format缓冲区里坐标与颜色交替存放一次 glDrawArrays 画完所有路径。4.1 顶点缓冲的构建策略void GCodeDrawer::buildVertices(const QVectorGCodeLine lines, QOpenGLBuffer vbo) { QVectorfloat vertices; for (const auto line : lines) { if (!line.isMotion) continue; QColor color line.isRapid ? QColor(255, 160, 0) : QColor(80, 200, 120); vertices start.x() start.y() start.z() color.redF() color.greenF() color.blueF(); // 如果是圆弧先离散成若干线段再逐点压入 } vbo.bind(); vbo.allocate(vertices.constData(), vertices.size() * sizeof(float)); }顶点布局里坐标占 3 个 float颜色占 3 个 float所以一个顶点是 6 个 float。shader 端用 location 0 接收坐标location 1 接收颜色。实际项目里我会再把快移路径和切削路径分到不同的 VBO因为快移路径通常只有少量比例可以单独设置粗一点的 glLineWidth避免切换线宽时整批顶点重传。4.2 快移与切削分层着色为什么要分层因为 CNC 操作员最关心的是危险段的辨识。刀具快速移动G0通常发生在安全高度切削G1才接触材料。如果两者颜色一样大零件里找一次异常抬刀非常费眼。把 G0 画成橙色、G1 画成绿色同时快移路径的线宽设为 1切削路径线宽设为 2视觉层次一下子就出来了。注意 shader 里不要做坐标系镜像。很多机床的机械坐标系 Y 轴正方向朝向操作者如果显示时顺手乘了一个镜像矩阵视角就会变成从机床后方看左右颠倒会让操作者误判切削方向。最好约定显示坐标系的 Y 轴与机床实际方向一致旋转视图只是调整摄像机不要动模型坐标。4.3 视角交互的缩放参数滚轮缩放不要直接把缩放范围放太宽。幅度太小零件小的文件怎么滚都看不到细节幅度太大略动一格就穿模到工件内部。我一般用固定系数 1.1void GLWidget::wheelEvent(QWheelEvent *event) { float factor std::pow(1.1f, event-angleDelta().y() / 120.0f); m_zoom std::clamp(m_zoom * factor, 0.02f, 50.0f); update(); }angleDelta().y() / 120把滚轮刻度归一成 ±1每一格 10% 缩放。有些触摸板和鼠标驱动支持像素级滚动但兼容性差pixelDelta 在某些驱动下返回 0所以滚轮事件用 angleDelta 最稳妥。旋转中心方面我采用以包围盒中心为原点的做法旋转时先把模型平移到原点旋转后再平移回视野中心。不要用鼠标点击坐标做旋转中心否则连续操作两次后视角会越转越偏。5. frmsettings 参数体系持久化、刷新策略与线程同步frmsettings.cpp 负责把显示参数做成可视化界面。文件路径、网格开关、是否显示快移路径、背景颜色、圆弧细分精度这些选项都应当能在界面上调整并保存。参数配置是这个工具区别于一次性 demo 的关键不同 CAM 后处理器输出的 nc 文件差异很大用户会频繁切换不保存参数意味着每次都要重新设置。5.1 QSettings 持久化方案void Frmsettings::loadSettings() { QSettings settings(QSettings::IniFormat, QSettings::UserScope, QStringLiteral(CNCViewer), QStringLiteral(CNCViewer)); settings.beginGroup(Display); m_gridEnabled settings.value(gridEnabled, true).toBool(); m_bgColor settings.value(bgColor, QColor(30, 30, 36)).valueQColor(); m_showRapid settings.value(showRapid, true).toBool(); m_arcTessellation settings.value(arcTessellation, 0.5).toDouble(); settings.endGroup(); }QSettings 以 INI 格式写入用户目录Linux 下是~/.config/CNCViewer/CNCViewer.conf路径不存在时 QSettings 会自己创建目录不需要手动 mkdir。arcTessellation表示圆弧离散化时允许的最大弦高误差单位是毫米。这个值建议暴露给高级用户小半径圆弧多的零件需要把它调小到 0.1否则刀具轨迹边缘会有明显的多边形感。5.2 参数更新的两级刷新策略参数调整后要立即生效但不必所有参数都触发完整重建。我的做法是把刷新分成两级轻量级刷新只影响绘制状态比如网格开关、背景颜色只调用update()触发重绘重量级刷新影响顶点缓冲内容比如圆弧细分精度、快移路径显隐需要重建 VBO。connect(ui-chkGrid, QCheckBox::clicked, this, [this](bool checked) { m_glWidget-setGridVisible(checked); m_glWidget-update(); }); connect(ui-spinArcPrecision, QOverloadint::of(QSpinBox::valueChanged), this, [this](int value) { m_gcodeDrawer-setArcPrecision(value); m_glWidget-rebuildBuffers(); });这里有一个性能调优原则顶点缓冲重建不要放在 paintGL 里。paintGL 里分配缓冲会导致窗口 resize 或移动时都重新算一遍全部几何数据卡顿很明显。把重建放到设置变更事件里执行paintGL 只负责绑定缓冲并绘制职责清晰重绘性能也稳。5.3 参数跨线程传递的边界如果预处理在工作线程里跑主线程改设置项时不能直接改全局变量。显示参数量不大常见做法是整个配置对象用 const 值拷贝传给工作线程设置变更后触发重新解析。注意 QColor 这类类型跨线程拷贝是安全的只要两边不共享同一个可变引用就行。颜色参数尤其不要用全局 QColor 指针传递界面线程和工作线程同时改颜色极小概率会读到中间态排查起来非常隐蔽。6. 实机验证用真实 NC 文件核对路径并接入 CNC/CAM 工作流的技巧把查看器交给车间之前先用一个真实后处理器输出的 nc 文件做三次核对。第一次加载文件后检查包围盒尺寸是否与 CAM 软件一致偏差超过 1% 就检查单位看看是不是把英制当公制了。第二次让预处理器打印前 30 条规范化指令确认 G90/G91 状态切换和模态补全正确。第三次随机抽取一条圆弧把起点、终点、圆心坐标代入 CAD 或 Python 脚本验证半径误差小于 0.01mm。grep -nE ^[N0-9]*G0*[23] fixture.nc | head -30上面这行命令用于快速抽取圆弧指令配合预处理器日志核对 I/J/K 或 R 字段是否被正确换算。它会匹配G02和G03不管行号前缀是N10还是N 10都能命中。G02 X10. Y20. I-30. J0.这类指令如果 I/J 偏移方向反了显示效果不会立刻看出来因为单段圆弧的形状可能仍然相似。我的验证方法是用一个已知的 CAM 后处理器输出同一零件把两套路径叠在一起对比轮廓差异。要和 TwinCAT3 这类软 PLC 环境联动时可以把解析结果导出成 CSV 坐标序列与 NC 轴实际规划轨迹对照。注意无论控制器内部单位是脉冲还是用户自定义导出时都要换算回毫米坐标反馈和原始代码保持一致才能判断显示工具和实走路径是否存在左右镜像或 Z 轴方向反了。车间里最实用的一招让 CAM 输出一条带大负 Z 下刀的路径查看器里把安全高度以上的部分标成灰色切屑接触区域保持绿色。如果显示里出现灰色段直接扎进工件内部说明要么是 G0 和 G1 误判要么是后处理器的退刀高度设置本身有问题。这套标记逻辑稳定之后就可以把它接到后处理器回归测试里作为每次改 CAM 参数后验证路径安全性的可视化底座。本文还有配套的精品资源点击获取