简介这份资源面向从事PCB制造与CAM开发的工程师及Qt C学习者提供一套完整的Gerber文件解析与转换工具源码解决将PCB设计数据转化为生产指令的实际问题。包内共381个文件以147个svg图形资源、90个h头文件与86个cpp源文件为核心辅以16个ui界面文件、8个pri工程配置及png、qm、pro等资源压缩包约1.3MB结构清晰便于按模块研读。已有364人学习下载说明其在Gerber解析领域具备一定参考价值。源码围绕Gerber二进制结构的读取与解码展开涵盖光圈定义、钻孔格式、裁剪与Voronoi计算等模块并借助Qt实现图形界面预览同时提供转换为CNC雕刻机G代码与PDF输出的算法实现。读者可从中掌握从Gerber解析到加工指令生成、再到可视化输出的完整链路为PCB自动化流程开发或向3D打印、机械自动编程等方向扩展打下基础。1. 从一份 GERBER.rar 说起PCB 光绘文件到底怎么在 Qt 里打开上周有个做硬件工具链的读者丢过来一个压缩包名字就叫 GERBER.rar里面是一套 Gerber 光绘文件加钻孔文件问我能不能用 Qt 写个桌面端把它们渲染出来。这个需求在 PCB 上下游其实很常见CAM 工程师要快速预览、SMT 产线要核对层别、测试治具开发要提取坐标。Gerber 是 PCB 行业事实上的光绘标准格式RS-274X 是其中最主流的扩展版本它用一套 D 码加坐标指令描述每一层的铜箔、阻焊、丝印图形。很多人第一次拿到 Gerber 文件会懵——它既不是图片也不是 CAD 图纸而是一堆形如X12345Y67890D01*的文本指令。这份资源要解决的就是把这类文件解析成可绘制图元再在 Qt 的 QGraphicsView 体系里还原出来。适合有 C/Qt 基础、想自己造 PCB 预览工具的开发者也适合需要把 Gerber 解析嵌进现有 CAM 系统的团队。2. Gerber RS-274X 格式拆解从 D 码到坐标的解析逻辑2.1 先搞清楚文件里到底有什么一份典型的 RS-274X 文件开头是参数区用%包裹比如%FSLAX46Y46*%定义坐标格式%MOMM*%声明单位是毫米%ADD10C,0.5*%定义 D 码 10 是一个直径 0.5mm 的圆。参数区之后是绘图区每行以*结尾D01是画线、D02是移动、D03是闪绘打点。坐标值本身不带小数点要靠 FS 指令里的整数位和小数位来还原。比如X12345Y67890在FSLAX46Y46下表示 X12.345、Y67.890单位由 MO 决定。这个「坐标隐含小数」的设计是新手第一个翻车点直接按整数读会得到放大一万倍的图形。解析器要维护的状态包括当前坐标格式、单位、当前 D 码、当前插补模式G01 线性、G02 顺时针弧、G03 逆时针弧、极性LPD 正片、LPC 负片。负片在阻焊层里很常见处理不好整层就反了。常见做法是先把文件扫一遍收集所有 AD 定义的 D 码再第二遍解析绘图指令这样遇到 D 码时不用回头查。2.2 用 Qt 的 QRegularExpression 做词法切分Gerber 不是严格的行式语言一条指令可能跨行也可能一行多条。稳妥的做法是按*切分而不是按换行切分。下面这段代码演示了参数区和绘图区的分离以及用正则提取坐标和 D 码。// gerberparser.cpp #include QRegularExpression #include QStringList #include QVector struct GerberAperture { int code; QString shape; // C圆, R矩形, O椭圆 double params[4]; }; struct GerberCommand { enum Type { Move, Draw, Flash, ArcCW, ArcCCW } type; double x, y; // 已还原为毫米 int aperture; }; class GerberParser { public: bool parse(const QString content) { // 按 * 切分过滤空段 QStringList blocks content.split(*, Qt::SkipEmptyParts); bool inParam false; for (const QString raw : blocks) { QString block raw.trimmed(); if (block.isEmpty()) continue; // 参数区以 % 开头结尾 if (block.startsWith(%)) { inParam true; block.remove(0, 1); } if (block.endsWith(%)) { block.chop(1); inParam false; } if (inParam || block.startsWith(FS) || block.startsWith(MO) || block.startsWith(AD)) { parseParameter(block); } else { parseCommand(block); } } return true; } private: // 坐标格式FSLAX46Y46 - 整数4位 小数6位 int m_intDigits 4; int m_decDigits 6; double m_scale 1e-6; // 10^-6 bool m_unitMM true; QHashint, GerberAperture m_apertures; double m_curX 0, m_curY 0; int m_curAperture 0; void parseParameter(const QString block) { if (block.startsWith(FSLA)) { // 形如 FSLAX46Y46 QRegularExpression re(X(\\d)(\\d)Y(\\d)(\\d)); auto m re.match(block); if (m.hasMatch()) { m_intDigits m.captured(1).toInt(); m_decDigits m.captured(2).toInt(); m_scale qPow(10.0, -m_decDigits); } } else if (block.startsWith(MOMM)) { m_unitMM true; } else if (block.startsWith(MOIN)) { m_unitMM false; m_scale * 25.4; // 英寸转毫米 } else if (block.startsWith(ADD)) { // ADD10C,0.5 QRegularExpression re(ADD(\\d)([A-Z]),([\\d.X])); auto m re.match(block); if (m.hasMatch()) { GerberAperture ap; ap.code m.captured(1).toInt(); ap.shape m.captured(2); ap.params[0] m.captured(3).toDouble(); m_apertures.insert(ap.code, ap); } } } void parseCommand(const QString block) { // 处理 G 码切换插补模式 if (block.startsWith(G01)) { /* 线性后续 D01 按直线处理 */ } if (block.startsWith(G02)) { /* 顺时针弧 */ } if (block.startsWith(G03)) { /* 逆时针弧 */ } // 提取坐标和 D 操作码 QRegularExpression re((?:X(-?\\d))?(?:Y(-?\\d))?D0([123])); auto m re.match(block); if (!m.hasMatch()) return; double x m.captured(1).isEmpty() ? m_curX : m.captured(1).toDouble() * m_scale; double y m.captured(2).isEmpty() ? m_curY : m.captured(2).toDouble() * m_scale; int op m.captured(3).toInt(); GerberCommand cmd; cmd.x x; cmd.y y; cmd.aperture m_curAperture; switch (op) { case 1: cmd.type GerberCommand::Draw; break; case 2: cmd.type GerberCommand::Move; break; case 3: cmd.type GerberCommand::Flash; break; } m_commands.append(cmd); m_curX x; m_curY y; } public: QVectorGerberCommand m_commands; };这段代码的关键点有三个。第一split(*)而不是split(\n)因为 Gerber 允许一条指令跨多行按行切会漏掉后半截。第二m_scale由 FS 指令动态决定默认 1e-6 只是兜底遇到FSLAX25Y25就得变成 1e-5。第三坐标缺省时沿用上一次的值这是 Gerber 的省略规则X100单独出现表示只改 X、Y 不变。参数说明m_intDigits和m_decDigits直接决定还原精度读错会导致图形整体缩放m_unitMM影响英寸文件的换算MOIN 时要在 scale 上乘 25.4。2.3 把图元映射到 QGraphicsScene解析出命令序列后渲染层要做的是把 Draw 转成 QGraphicsLineItem 或 QPainterPathFlash 按 D 码形状转成对应的图元。圆弧要用QPainterPath::arcTo注意 Gerber 的弧是「起点、终点、圆心偏移」三要素I、J 参数表示圆心相对起点的偏移不是绝对坐标。常见做法是给每一层建一个 QGraphicsItemGroup用不同颜色区分顶层、底层、阻焊、丝印再套一个 QGraphicsView 做缩放平移。坐标原点在 Gerber 里通常是左下角而 Qt 的 Y 轴向下渲染前要对 Y 取反否则整块板子是镜像的。3. 在 Qt 里搭一个能用的 Gerber 预览器工程结构与渲染管线3.1 工程分层与关键类设计一个能落地的预览器不建议把所有逻辑塞进 MainWindow。我一般分成四层解析层GerberParser、数据层GerberLayer 持有命令列表和图元、渲染层GerberScene 继承 QGraphicsScene、界面层MainWindow 加工具栏和层列表。解析层只依赖 QtCore不碰 GUI这样以后要做命令行批量转换也能复用。数据层负责把命令转成 QPainterPath一个层一个 path比几千个 QGraphicsLineItem 性能好得多。渲染层管视图变换和鼠标交互。界面层管文件打开、层显隐、颜色配置。层别识别是个实际问题。Gerber 文件本身不带层名靠扩展名区分.GTL顶层、.GBL底层、.GTS顶层阻焊、.GTO顶层丝印、.TXT或.DRL是钻孔。压缩包里如果文件名规范直接按扩展名映射不规范就得让用户手动指定。我一般会在打开时弹一个层别映射对话框把识别结果列出来让用户确认避免自动猜错导致整板颜色混乱。3.2 渲染管线与坐标变换渲染的核心是把 Gerber 坐标转成场景坐标。假设板子范围是 X[0, 200]、Y[0, 150] 毫米场景坐标可以直接用毫米然后让 QGraphicsView 的 transform 去缩放。这样好处是测量工具算出来的距离就是真实毫米不用来回换算。Y 轴翻转在场景层做scene-setSceneRect(0, -maxY, maxX, maxY)或者给每个图元加一个QTransform().scale(1, -1)。我倾向后者因为场景矩形保持正数更直观。// gerberscene.cpp void GerberScene::buildFromLayer(const GerberLayer layer) { QPainterPath path; double curX 0, curY 0; for (const auto cmd : layer.commands()) { switch (cmd.type) { case GerberCommand::Move: curX cmd.x; curY cmd.y; break; case GerberCommand::Draw: path.moveTo(curX, -curY); // Y 取反 path.lineTo(cmd.x, -cmd.y); curX cmd.x; curY cmd.y; break; case GerberCommand::Flash: { // 按 D 码形状生成图元这里以圆为例 double dia layer.apertureDiameter(cmd.aperture); path.addEllipse(QPointF(cmd.x, -cmd.y), dia / 2, dia / 2); break; } case GerberCommand::ArcCW: case GerberCommand::ArcCCW: { // I、J 是圆心相对起点的偏移 QPointF center(curX cmd.i, -(curY cmd.j)); QRectF rect(center.x() - cmd.radius, center.y() - cmd.radius, cmd.radius * 2, cmd.radius * 2); double startAngle QLineF(center, QPointF(curX, -curY)).angle(); double endAngle QLineF(center, QPointF(cmd.x, -cmd.y)).angle(); path.arcMoveTo(rect, startAngle); path.arcTo(rect, startAngle, endAngle - startAngle); curX cmd.x; curY cmd.y; break; } } } auto *item new QGraphicsPathItem(path); item-setPen(QPen(layer.color(), layer.lineWidth())); addItem(item); }逻辑说明Move 只更新当前点不产生图元Draw 从当前点到目标点画线Flash 在目标点按孔径形状打点圆弧用 QPainterPath 的 arcTo角度用 QLineF::angle 算注意 Qt 的角度是逆时针为正、0 度在三点钟方向和 Gerber 的数学角度一致但 Y 翻转后要留意方向。参数说明layer.lineWidth()一般取孔径直径Gerber 的线是有宽度的不是零宽线忽略这点画出来的走线会细一圈。apertureDiameter从解析层存的 D 码表里查C 形取 params[0]R 形取长宽。3.3 性能与交互大板子不卡的几个手段一块 20 层的服务器板Gerber 命令动辄几十万条全转成 QGraphicsItem 会卡到没法用。我的做法是每层合并成一个 QPainterPath一个 QGraphicsPathItem 承载整层Qt 内部对 path 有优化几万个线段也能流畅缩放。如果还卡就开 QGraphicsView 的setViewportUpdateMode(QGraphicsView::SmartViewportUpdate)减少重绘区域。测量工具用 QGraphicsLineItem 临时画鼠标移动时更新不要每帧重建场景。层显隐直接item-setVisible()比删了重建快得多。钻孔文件单独一层用不同颜色的小圆表示孔径用 D 码区分这样能一眼看出过孔和插件孔的区别。4. 避坑与排查Gerber 解析里最容易翻车的五件事4.1 图形整体放大或缩小了一万倍现象打开文件后图形要么小得看不见要么撑满屏幕还溢出。原因FS 指令的整数位和小数位没解析对或者默认 scale 用了 1e-6 但文件实际是FSLAX25Y25scale 应为 1e-5。解决在解析参数区时打印出 FS 指令原文确认 intDigits 和 decDigitsscale 用qPow(10, -decDigits)动态算不要写死。英寸文件还要在 scale 上乘 25.4。4.2 整层图形镜像或上下颠倒现象顶层和底层叠在一起对不上或者文字丝印是反的。原因Gerber 原点在左下、Y 向上Qt 场景 Y 向下没做翻转。解决在生成图元时对 Y 取反或者给场景加QTransform().scale(1, -1)。注意翻转后圆弧的顺逆时针方向会变G02 和 G03 要互换处理否则弧会画到另一侧。4.3 负片层显示成全黑或全空现象阻焊层打开后要么一片实心要么什么都没有。原因LPC负片极性没处理或者把负片当正片画了。解决解析时记录 LPD/LPC 状态负片层在渲染时用 QPainter 的 CompositionMode_Clear 或者先画整层再挖孔。简单做法是负片层单独用 QPainterPath 的 subtract 做布尔减把负片图形从整板矩形里减掉。4.4 圆弧画成了直线或方向反了现象本该是弧的地方出现直线或者弧凸向错误的一侧。原因G02/G03 的 I、J 偏移没读或者圆心算错。I、J 是圆心相对起点的偏移不是绝对坐标也不是半径。解决圆心 起点 (I, J)半径 起点到圆心的距离。算完圆心后用 QLineF 算起止角度注意 Y 翻转后角度要取反。如果文件用 G75 多象限模式还要处理跨象限的弧。4.5 钻孔文件和 Gerber 对不上位现象钻孔层和铜箔层错位或者钻孔整体偏移。原因钻孔文件Excellon 格式的坐标格式和 Gerber 不同它用单独的 METRIC 或 INCH 指令加坐标位数声明且原点可能和 Gerber 不一致。解决钻孔解析单独写一个 ExcellonParser单位换算独立处理加载后如果发现偏移提供一个手动偏移量输入框让用户微调。常见做法是先加载顶层铜箔作为基准再把钻孔叠上去目视对齐。5. 进阶把解析器做成可复用的库顺带聊聊验证方法写到这儿解析和渲染的主干就通了。但如果你想让这份代码不止于一个 Demo我建议把 GerberParser 和 ExcellonParser 抽成一个独立的静态库只依赖 QtCoreGUI 部分单独一个可执行文件链接它。这样以后要做批量 Gerber 转 PNG、命令行提取坐标、或者嵌到别的 CAM 系统里直接复用解析层就行。抽库的时候注意两点一是解析器不要持有任何 QWidget 相关的东西二是错误处理用返回值或异常不要弹 QMessageBox让调用方决定怎么提示。验证解析对不对我一般用三个手段。第一拿同一份 Gerber 在成熟 CAM 软件里导出 PNG和自己的渲染结果做像素级对比差异超过几个像素就说明坐标或线宽有问题。第二用 Gerber 文件里的%IPPOS*%或%IPNEG*%图像极性指令构造测试用例确认正负片切换正确。第三故意改坏 FS 指令的位数看解析器是报错还是静默出错好的解析器应该在遇到不认识的参数时记录警告而不是直接崩。下面这个片段演示了怎么在解析时收集警告方便排查。// 在 parseParameter 里加警告收集 QStringList m_warnings; void GerberParser::warn(const QString msg) { m_warnings msg; } // 遇到未知参数时 if (!handled) { warn(QString(未识别的参数指令: %1).arg(block)); }参数说明m_warnings在解析结束后由调用方读取可以显示在日志面板里。不要用 qDebug 直接打库不应该往控制台输出交给上层决定。我自己的习惯是每次加载新文件后先扫一遍 warnings如果超过十条就说明这个文件用了不少扩展指令得回头补解析逻辑。还有个容易被忽略的点是单位混用。有些老文件参数区写 MOMM 但坐标实际是英寸或者反过来。这种文件在成熟软件里能打开是因为它们有容错自己写解析器时可以在加载后做一个范围检查如果板子尺寸超过 1000mm 或者小于 1mm大概率是单位搞错了弹个提示让用户手动切换。这个后悔药我吃过好几次后来每次加载都强制走一遍范围校验省得对着一个放大一万倍的板子发呆。最后说下圆弧的另一种处理思路。如果不想自己算圆心和角度可以把圆弧离散成多段直线段数按弧长和精度要求定一般每度一段就够肉眼看了。缺点是文件变大、测量不准优点是绝对不会画错方向。我一般只在快速预览时用离散正式测量还是走精确弧。希望帮到你。本文还有配套的精品资源点击获取