ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C#手搓Gerber查看器:从RS-274X解析到GDI+渲染的完整实现

C#手搓Gerber查看器:从RS-274X解析到GDI+渲染的完整实现

简介:这份资源是一款基于C#实现的Gerber查看器源码,面向电子工程师、PCB设计师以及希望学习图形解析与桌面开发的C#开发者,用于解决PCB制造中Gerber图形文件的导入、解析与可视化查看问题。压缩包共35个文件,约76KB,以cs源码文件为主,辅以resx与resources资源文件、exe可执行程序、config配置、pdb调试符号及sln解决方案等,结构完整,便于直接编译运行与二次开发。目前已有3641人学习下载,说明其在CAM与Gerber Viewer方向具有一定参考价值。源码涵盖Gerber指令逐行读取、坐标与功能码解析、内部图形对象建模,以及基于WinForms的界面加载、缩放与平移显示等核心环节,并涉及层叠加、颜色编码、测量工具等CAM Viewer常见功能的实现思路,适合作为EDA工具开发与文本解析渲染的实战参考。

1. 用 C# 手搓一个 Gerber 查看器:从文件解析到图形渲染的完整实现

如果你在 PCB 行业待过,一定对 Genesis、CAM350 这类 CAM 工具不陌生。它们能打开 Gerber 文件、叠层预览、测量线宽线距,但要么是重量级商业软件,要么授权费用不低。某天我拿到一份 C# 写的 Gerber 查看器源码,第一反应是:这东西能跑起来吗?打开一看,从 RS-274X 格式解析到 GDI+ 图形渲染,整条链路都是自己实现的,没有依赖任何商业库。这意味着你可以直接拿它做二次开发——加个 DRC 检查、接个自动拼板逻辑、甚至嵌到自己公司的 MES 系统里做在线预览。它适合谁?想理解 Gerber 文件底层结构的人、需要给内部工具加 PCB 预览功能的开发者、以及想用 C# 做 CAM 相关工具但不想从零造轮子的人。

2. 项目架构拆解:解析层、数据层、渲染层怎么分工

2.1 整体分层设计

这份源码的架构不复杂,但分层很清晰。最底层是 Gerber 解析器,负责把 RS-274X 格式的文本指令翻译成内存中的图形对象;中间是数据模型层,用统一的图元结构(Pad、Trace、Region、Outline)描述 PCB 上的几何元素;最上层是渲染层,基于 GDI+ 把图元画到 Panel 或 Bitmap 上。三层之间通过接口解耦,解析器不关心怎么画,渲染器不关心文件怎么读。

常见做法是解析器直接输出绘图指令,渲染器边解析边画。但这个项目没有这么做,而是先把整个文件解析成图元列表,再统一渲染。好处是支持缩放、平移、图层开关这些交互操作时不用重新解析文件,坏处是内存占用会高一些。对于中小尺寸的 PCB 文件,这个取舍是合理的。

目录结构大致是这样的:

GerberViewer/ ├── Parsers/ │ ├── GerberParser.cs # 主解析器,逐行读取 RS-274X 指令 │ ├── ApertureManager.cs # 光圈定义管理(C/D/R/O/P 形状) │ └── CoordinateParser.cs # 坐标格式解析(前导零/后导零/绝对/相对) ├── Models/ │ ├── GerberImage.cs # 一个 Gerber 文件的完整内存表示 │ ├── GraphicObject.cs # 图元基类 │ ├── Pad.cs # 焊盘图元 │ ├── Trace.cs # 走线图元 │ └── Region.cs # 铜皮区域图元 ├── Rendering/ │ ├── GerberRenderer.cs # GDI+ 渲染引擎 │ ├── LayerManager.cs # 多图层叠加与可见性控制 │ └── Viewport.cs # 视口变换(缩放/平移) └── UI/ ├── MainForm.cs # 主窗口 └── LayerPanel.cs # 图层列表控件

2.2 解析层:RS-274X 指令怎么读

Gerber RS-274X 是一种基于文本的矢量格式,每一行是一条指令,以%包裹的是扩展指令,其他是绘图指令。解析器的核心任务就是逐行读取、识别指令类型、更新状态机。

// GerberParser.cs 核心解析循环 public GerberImage Parse(string filePath) { var image = new GerberImage(); var apertureMgr = new ApertureManager(); var coordParser = new CoordinateParser(); // 当前绘图状态 int currentAperture = -1; PointF currentPoint = PointF.Empty; bool isRegionMode = false; foreach (string rawLine in File.ReadLines(filePath)) { string line = rawLine.Trim(); if (string.IsNullOrEmpty(line)) continue; // 扩展指令:光圈定义、坐标格式、单位等 if (line.StartsWith("%")) { ParseExtendedCommand(line, apertureMgr, coordParser, image); continue; } // 绘图指令:D01 画线、D02 移动、D03 闪绘 if (line.StartsWith("D") && line.Length >= 3) { int code = int.Parse(line.Substring(1, 2)); switch (code) { case 1: // D01:从当前点到目标点画线 var trace = new Trace(currentPoint, coordParser.Parse(line), currentAperture); image.Objects.Add(trace); currentPoint = trace.EndPoint; break; case 2: // D02:移动当前点,不画 currentPoint = coordParser.Parse(line); break; case 3: // D03:在目标点闪绘一个焊盘 var pad = new Pad(coordParser.Parse(line), currentAperture); image.Objects.Add(pad); break; } } // 光圈选择指令:D10 以上 else if (line.StartsWith("D") && line.Length > 3) { currentAperture = int.Parse(line.Substring(1)); } // G 代码:G36/G37 区域模式、G01/G02/G03 插补模式 else if (line.StartsWith("G")) { ParseGCode(line, ref isRegionMode, image); } } return image; }

这段代码有几个关键点。第一,%开头的扩展指令负责定义光圈(Aperture)、坐标格式(FS)、单位(MO)、极性(LP)等全局参数,必须在绘图指令之前解析。第二,D01/D02/D03 是 Gerber 最核心的三个操作码:D01 画线、D02 移动、D03 闪绘。第三,光圈选择用 D10 以上的代码,和操作码共用 D 前缀,靠长度区分——D01 是三位,D10 是三位,D100 是四位。这里有个容易翻车的地方:有些 Gerber 文件用 D100 以上表示光圈,有些用 D10-D99,解析时要兼容。

坐标解析是另一个坑。Gerber 的坐标是整数,需要根据 FS 指令定义的格式(比如%FSLAX34Y34*%表示 3 位整数 4 位小数)转换成实际尺寸。前导零省略和后导零省略两种模式处理方式不同,搞错了整个图形会偏移。

// CoordinateParser.cs 坐标格式解析 public class CoordinateParser { private int _intDigits = 3; private int _decDigits = 4; private bool _leadingZeroOmitted = true; private bool _absolute = true; private PointF _lastPoint = PointF.Empty; public void SetFormat(int intDigits, int decDigits, bool leadingOmitted, bool absolute) { _intDigits = intDigits; _decDigits = decDigits; _leadingZeroOmitted = leadingOmitted; _absolute = absolute; } public PointF Parse(string line) { // 提取 X 和 Y 坐标值 int xPos = line.IndexOf('X'); int yPos = line.IndexOf('Y'); long xRaw = 0, yRaw = 0; if (xPos >= 0) { int end = yPos > xPos ? yPos : line.Length; xRaw = long.Parse(line.Substring(xPos + 1, end - xPos - 1)); } if (yPos >= 0) { int end = line.IndexOf('D', yPos); if (end < 0) end = line.Length; yRaw = long.Parse(line.Substring(yPos + 1, end - yPos - 1)); } // 根据格式转换为实际坐标 float x = xRaw / (float)Math.Pow(10, _decDigits); float y = yRaw / (float)Math.Pow(10, _decDigits); if (!_absolute) { x += _lastPoint.X; y += _lastPoint.Y; } _lastPoint = new PointF(x, y); return _lastPoint; } }

参数说明:_intDigits和_decDigits来自 FS 指令,决定坐标的缩放比例;_leadingZeroOmitted影响前导零填充方式,但实际解析时直接按整数读再除以 10 的幂次更简单;_absolute区分绝对坐标和相对坐标,相对坐标要累加上一个点。

2.3 数据层:图元模型怎么设计

图元模型的设计直接决定了渲染和后续操作的便利性。这个项目用了一个基类GraphicObject,派生出Pad、Trace、Region三种基本类型。每个图元都持有对光圈定义的引用,渲染时根据光圈形状决定画圆、矩形还是多边形。

// GraphicObject.cs 图元基类 public abstract class GraphicObject { public int ApertureIndex { get; set; } public abstract void Render(Graphics g, Matrix transform); public abstract RectangleF GetBounds(); } // Pad.cs 焊盘图元 public class Pad : GraphicObject { public PointF Position { get; set; } public override void Render(Graphics g, Matrix transform) { // 根据光圈形状绘制 var aperture = ApertureManager.Get(ApertureIndex); var points = new PointF[] { Position }; transform.TransformPoints(points); switch (aperture.Shape) { case ApertureShape.Circle: g.FillEllipse(brush, points[0].X - r, points[0].Y - r, aperture.Diameter, aperture.Diameter); break; case ApertureShape.Rectangle: g.FillRectangle(brush, points[0].X - w/2, points[0].Y - h/2, w, h); break; // 其他形状... } } }

Trace图元稍微复杂一些,它需要根据光圈形状生成走线的轮廓。圆形光圈直接画粗线,矩形光圈需要计算旋转后的矩形条带。Region图元是一组闭合路径,用 G36/G37 指令定义,通常用来描述铜皮。

2.4 渲染层:GDI+ 怎么画才不闪

渲染层用 GDI+ 的Graphics对象,配合Matrix做视口变换。关键优化是双缓冲——把图形先画到内存 Bitmap 上,再一次性贴到 Panel,避免闪烁。

// GerberRenderer.cs 渲染核心 public void Render(Graphics g, Rectangle viewport, float zoom, PointF offset) { // 设置高质量渲染模式 g.SmoothingMode = SmoothingMode.AntiAlias; g.InterpolationMode = InterpolationMode.HighQualityBicubic; // 构建变换矩阵:先平移到视口中心,再缩放,再偏移 var matrix = new Matrix(); matrix.Translate(viewport.Width / 2f, viewport.Height / 2f); matrix.Scale(zoom, -zoom); // Y 轴翻转,Gerber 原点在左下 matrix.Translate(-offset.X, -offset.Y); // 遍历所有可见图元 foreach (var obj in _visibleObjects) { obj.Render(g, matrix); } }

参数说明:zoom控制缩放倍数,offset是当前视口中心在 Gerber 坐标系中的位置。Y 轴翻转是因为 Gerber 文件的原点在左下角,而屏幕坐标原点在左上角。这个变换矩阵的乘法顺序不能错,否则缩放和平移会互相干扰。

多图层叠加时,每个 Gerber 文件对应一个GerberImage,LayerManager负责管理可见性和绘制顺序。常见做法是用不同的颜色区分顶层、底层、阻焊层,透明度可调。

3. 跑起来:环境配置与第一个 Gerber 文件加载

3.1 开发环境与依赖

这个项目基于 .NET Framework 4.5 和 WinForms,用 Visual Studio 2015 以上版本就能打开。没有第三方 NuGet 包,所有依赖都在 BCL 里。如果你用 .NET Core 或 .NET 5+,需要把 WinForms 部分改成 WPF 或 Avalonia,解析层和渲染层可以原样移植。

编译步骤:

# 克隆或解压源码后,用 MSBuild 编译 msbuild GerberViewer.sln /p:Configuration=Release # 或者用 dotnet CLI(如果已迁移到 SDK 风格项目) dotnet build GerberViewer.sln -c Release

编译产物是一个 WinForms 可执行文件,直接双击就能运行。主界面左侧是图层列表,右侧是绘图区,顶部工具栏有打开文件、缩放、平移、测量等按钮。

3.2 加载第一个 Gerber 文件

点击“打开文件”,选择一个.gbr或.ger文件。解析器会逐行读取,遇到不认识的指令会跳过并在日志区输出警告。加载完成后,绘图区自动缩放到适合窗口的大小。

如果你手头没有 Gerber 文件,可以用 KiCad 或 Eagle 导出一个。KiCad 的导出路径是File -> Fabrication Outputs -> Gerber Files,选 RS-274X 格式。导出的文件可以直接用这个查看器打开。

加载过程中常见的现象是图形偏移或比例不对,大概率是坐标格式解析错了。检查日志里有没有FS指令的警告,确认_intDigits和_decDigits的值是否和文件头一致。

3.3 交互操作与性能调优

缩放用鼠标滚轮,平移按住中键拖动。测量工具点两个点,显示距离。这些交互都依赖Viewport类的变换矩阵,修改zoom和offset后触发重绘。

性能方面,如果文件很大(比如上万条走线),渲染会卡。优化手段有三种:一是只渲染视口内的图元,用GetBounds()做裁剪;二是把静态图元缓存到 Bitmap,缩放时只做位图缩放;三是用多线程解析,解析和渲染分离。这个项目实现了第一种,后两种留作扩展点。

4. 避坑记录:Gerber 解析与渲染的五个常见问题

4.1 图形整体偏移或镜像

现象:打开文件后图形位置不对,或者上下颠倒。

原因:Gerber 原点在左下角,屏幕原点在左上角,Y 轴需要翻转。另外有些文件用英寸,有些用毫米,单位没统一。

解决:在变换矩阵里加matrix.Scale(zoom, -zoom)翻转 Y 轴。单位问题检查%MOIN*%和%MOMM*%指令,统一转换成毫米再渲染。

4.2 光圈形状识别错误

现象:焊盘画成了圆形,但实际应该是矩形。

原因:光圈定义指令%ADD10R,1.5X1.5*%里的R表示矩形,C表示圆形,O表示椭圆,P表示多边形。解析时没正确识别形状代码。

解决:在ApertureManager里根据形状代码创建对应的光圈对象,渲染时按形状分派。注意多边形光圈还有旋转角度参数。

4.3 区域填充出现自交

现象:铜皮区域填充后出现奇怪的交叉线。

原因:G36/G37 定义的区域路径可能自交,GDI+ 的FillPolygon对自交多边形处理不好。

解决:用GraphicsPath的FillMode.Winding或FillMode.Alternate控制填充规则。如果还是不对,把区域拆成多个简单多边形再分别填充。

4.4 大文件加载卡死

现象:打开几 MB 的 Gerber 文件时界面无响应。

原因:解析和渲染都在 UI 线程,文件大了就卡。

解决:把解析放到Task.Run里,解析完成后用Invoke更新 UI。渲染时只画视口内的图元,减少绘制量。

4.5 坐标精度丢失

现象:细走线的宽度看起来不对,或者焊盘位置有微小偏移。

原因:用float存储坐标,大尺寸 PCB 加上高精度小数时精度不够。

解决:内部用double或decimal存储坐标,渲染时再转float。Gerber 坐标是整数,解析时先保持整数,只在最后渲染时缩放。

5. 二次开发:加一个 DRC 检查模块的完整思路

拿到这份源码后,我最想加的功能是 DRC(设计规则检查)。Gerber 查看器只负责看,如果能自动检查线宽、间距、焊盘环宽,就能在出板前发现一批低级错误。下面是我实际改造的步骤。

5.1 扩展数据模型:给图元加属性

DRC 需要知道每条走线的宽度、每个焊盘的尺寸、以及图元之间的间距。现有的Trace和Pad已经包含了这些信息,但缺少图层归属和网络标识。我加了一个LayerId属性和一个NetName字段,解析时从文件名或额外的网表文件里读取。

// 扩展后的 Trace 类 public class Trace : GraphicObject { public PointF StartPoint { get; set; } public PointF EndPoint { get; set; } public float Width { get; set; } public int LayerId { get; set; } // 新增:图层标识 public string NetName { get; set; } // 新增:网络名 // 计算走线到另一个图元的最短距离 public float DistanceTo(GraphicObject other) { if (other is Trace otherTrace) return SegmentToSegmentDistance(StartPoint, EndPoint, otherTrace.StartPoint, otherTrace.EndPoint); if (other is Pad pad) return PointToSegmentDistance(pad.Position, StartPoint, EndPoint); return float.MaxValue; } }

5.2 实现间距检查:线段到线段的距离

间距检查的核心是计算两条线段之间的最短距离。如果两条走线不在同一层,跳过;如果在同一层且距离小于规则值,报错。

// DrcChecker.cs 间距检查 public class DrcChecker { private float _minClearance = 0.15f; // 最小间距 0.15mm public List<DrcViolation> CheckClearance(GerberImage image) { var violations = new List<DrcViolation>(); var traces = image.Objects.OfType<Trace>().ToList(); for (int i = 0; i < traces.Count; i++) { for (int j = i + 1; j < traces.Count; j++) { var a = traces[i]; var b = traces[j]; // 不同层不检查 if (a.LayerId != b.LayerId) continue; float dist = a.DistanceTo(b) - a.Width / 2 - b.Width / 2; if (dist < _minClearance) { violations.Add(new DrcViolation { Type = "Clearance", Location = MidPoint(a, b), ActualValue = dist, RequiredValue = _minClearance, Description = $"走线间距 {dist:F3}mm 小于规则 {_minClearance}mm" }); } } } return violations; } }

参数说明:_minClearance是最小间距规则,单位毫米,可以从配置文件读取。DistanceTo返回的是中心线距离,减去两个走线的半宽才是实际边缘间距。MidPoint取两条走线的中间位置作为报错坐标。

5.3 在 UI 上标记违规位置

检查结果需要可视化。我在渲染层加了一个DrcOverlay,把违规点画成红色圆圈,鼠标悬停显示详情。

// 在 GerberRenderer 里叠加 DRC 标记 public void RenderDrcMarkers(Graphics g, List<DrcViolation> violations, Matrix transform) { using (var pen = new Pen(Color.Red, 2)) using (var brush = new SolidBrush(Color.FromArgb(80, 255, 0, 0))) { foreach (var v in violations) { var points = new PointF[] { v.Location }; transform.TransformPoints(points); // 画红色圆圈标记 g.DrawEllipse(pen, points[0].X - 10, points[0].Y - 10, 20, 20); g.FillEllipse(brush, points[0].X - 10, points[0].Y - 10, 20, 20); } } }

5.4 验证方法与改造建议

改造完成后,用 KiCad 导出的测试板验证。先故意把两条走线画近,确认 DRC 能报出来;再调整到合规间距,确认不报。检查结果和 KiCad 自带的 DRC 对比,误差在 0.01mm 以内算合格。

如果想继续扩展,可以加线宽检查(走线宽度小于规则值)、环宽检查(焊盘和过孔的内外径差)、以及开路短路检查(基于网络名的连通性分析)。这些都需要在解析阶段保留更多信息,比如过孔的钻孔尺寸、焊盘的层属性。

从那以后我每次拿到新的 Gerber 文件,都强制先跑一遍 DRC 再出板,省了好几次返工。希望这个思路帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进