简介《C#编写的Gerber查看器》是一款面向电子工程师与PCB设计师的轻量级CAM查看工具基于C#语言实现Gerber文件的读取、解析与可视化适用于PCB制造前的图形预览与层信息核对可辅助用户快速检查设计数据减少制造阶段因图档错误导致的返工。Gerber作为PCB制造通用格式包含各层导电路径、孔位、丝印等关键信息对正确解析要求较高。资源包共35个文件压缩后仅76KB以cs源码为核心同时包含界面资源、应用配置与可直接运行的执行程序结构紧凑完整。工程标记为GMDO_V4.0采用Visual Studio解决方案组织涵盖主窗体、解析器与图形显示模块从文件逐行读入、指令解析到画布渲染的完整链路均有清晰呈现学习者可快速掌握文本指令的解析思路构建对象模型并借助图形框架完成展示。目前已有3640人学习下载对于希望深入Gerber文件格式或从事PCB/CAM工具开发的读者这套带完整源码和工程配置的项目实例具有直接的参考价值与二次开发基础。1. 这个C# Gerber查看器一个能直接拉起来看PCB图的小工具某个周五下午你拿到一块刚投板的样片想确认顶层铜和钻孔是否对得上结果打开工作站上的专业CAM工具弹出授权到期提示运维又休假了。这时候一个两兆不到、用C#写的Gerber查看器反而成了全组最后一根救命稻草。它做的事情非常朴素把Gerber 文件读进来按铜箔、阻焊、丝印、钻孔分成图层显示成能缩放、能测距的二维图形并允许你点任意一段走线查看所属D码和光圈尺寸。项目本身不是一个玩具Demo而是把Gerber读入源码、图层组织、矢量渲染、鼠标交互完整跑通的工程代码。适合三类人PCB工程师当应急看图工具CAM软件二次开发者做解析参考还有想弄懂Gerber格式的C#新手。2. 整体架构与模块划分解析、模型、渲染三层分离2.1 技术选型为什么是C#和WinFormsGerber查看器在本质上不是图形编辑器它是一个“解析 几何计算 2D绘制”的流水线。C#在这条链路里相当顺手ListT管理百万级图元不痛苦System.Drawing提供了成熟的画刷、路径和矩阵变换垃圾回收把内存释放的黑匣子也包掉了。项目采用的是WinForms而不是WPF原因很实际PCB业界大量工具都跑在Windows上GDI的绘制模型比WPF的硬件加速更容易预测性能并且WinForms对低配办公电脑兼容更好打开几百兆的大板子时不容易掉帧。如果你拿到这份源码准备迁移到Linux要注意System.Drawing.Common在非Windows平台需要安装libgdiplus否则画板会白屏。这个项目目前的实现绑定了Windows GDI跨平台是可选改造方向但不是一个开箱即用的特性。2.2 目录结构与工程组织拿到源码后第一件事是看解决方案结构。项目采用标准的三层拆分解析库不依赖UIUI只做绘制测试工程独立。GerberViewer.sln ├─ src/ │ ├─ Gerber.Core/ # 解析与数据模型 │ │ ├─ GerberParser.cs # Gerber文本解析器 │ │ ├─ ApertureTable.cs # 光圈表 │ │ ├─ GerberLayer.cs # 图层模型 │ │ ├─ Geometry/ │ │ │ ├─ GerberPrimitive.cs # 图元基类 │ │ │ ├─ GerberArc.cs # 圆弧图元 │ │ │ └─ GerberLine.cs # 直线图元 │ │ └─ Utils/CoordinateParser.cs# 坐标精度换算 │ ├─ Gerber.Viewer/ # WinForms界面 │ │ ├─ MainForm.cs # 主窗口 │ │ ├─ Renderers/GerberRenderer.cs │ │ ├─ Controls/GerberCanvas.cs # 自定义画板 │ │ └─ App.config │ └─ Gerber.Tests/ # 单元测试 │ ├─ ParserTests.cs │ └─ GeometryTests.cs ├─ samples/ # 示例Gerber文件 └─ README.mdGerber.Core是整个项目的核心不引用任何UI类这样二次开发时可以把解析器直接嵌进Web API或命令行工具。Gerber.Viewer里的GerberCanvas继承自Control负责重绘、鼠标事件和缓存。看到这个结构你应该就知道改哪个文件了想改解析逻辑去Gerber.Core想调颜色找GerberRenderer.cs想改交互事件就看GerberCanvas.cs。2.3 核心数据模型GerberLayer、Aperture和PrimitiveGerber格式的图元只有三大类直线、圆弧、闪光Flash即直接放置一个光圈形状。项目的数据模型就围绕它们设计。public class GerberLayer { public string Name { get; set; } public LayerType Type { get; set; } public ListGerberPrimitive Primitives { get; } new ListGerberPrimitive(); public double MinX { get; set; } public double MinY { get; set; } public double MaxX { get; set; } public double MaxY { get; set; } } public enum LayerType { Copper, SolderMask, Silkscreen, Drill }GerberLayer的包围盒是渲染模块用来计算初始缩放比例的解析器每加入一个图元就会刷新MinX/MinY/MaxX/MaxY。值得注意的是这里没有把层分成“顶层”“底层”这样的物理概念而是让文件加载器根据文件名后缀匹配例如.GTL映射为顶层铜箔、.GTS映射为顶层阻焊。如果你需要支持自定义层名可以在MainForm里维护一张映射表。光圈Aperture是Gerber世界的“笔”D码对应不同形状。数据模型如下public class Aperture { public int Code { get; set; } public ApertureKind Kind { get; set; } public double SizeX { get; set; } public double SizeY { get; set; } public double HoleDiameter { get; set; } } public enum ApertureKind { Circle, Rectangle, Oval, Custom }ApertureTable内部是一个SortedDictionaryint, Aperture解析器遇到D码时就到这里查光圈。后面第3章会详细讲这个表是怎么从%AD指令构建的。3. 核心模块拆解Gerber读入源码的关键状态机3.1 RS-274X指令解析从文本行到图元对象Gerber文件本质上是一串行指令每一行或每几行构成一个“数据块”。解析器的难点不在正则表达式而在状态保持当前插补模式直线还是圆弧、当前D码、当前坐标、当前单位精度这些状态下一次绘图会用到。项目的GerberParser.cs采用了一个递归下降的思路但外层是逐行扫描。核心循环如下public ListGerberPrimitive Parse(TextReader reader) { var primitives new ListGerberPrimitive(); var apertures new ApertureTable(); double currentX 0, currentY 0; int currentDCode 0; InterpolationMode interpolation InterpolationMode.Linear; string line; while ((line reader.ReadLine()) ! null) { var clean StripComment(line); // 去掉G04注释 foreach (var token in SplitTokens(clean)) { if (token.StartsWith(%AD)) // 光圈定义 { var aperture ApertureParser.Parse(token); apertures.Set(aperture); } else if (token.StartsWith(D)) { currentDCode ApertureParser.ParseDCode(token); interpolation UpdateInterpolation(token); } else if (token.StartsWith(X) || token.StartsWith(Y)) { // 这里是坐标指令逻辑稍后在 3.3 单独拆解 } } } return primitives; }这个循环有两点设计值得记住。第一SplitTokens不是简单按空格切分而是把Gerber特有的星号分隔符*当作断句边界。第二UpdateInterpolation函数会根据G01/G02/G03切换直线/圆弧模式但它必须只影响后续的D01动作不能立刻生成图元。如果你在阅读源码时找不到“生成图元的调用”原因就在这Gerber的绘图指令是“先给坐标再给D码”所以生成动作发生在遇到D01/D02/D03那一分支里。3.2 光圈表是如何建立和匹配的光圈定义指令长这样%AD D10 C,0.254*%含义是D10、圆形、直径0.254毫米。也有的文件用矩形%AD D12 R,0.5X0.2*%。ApertureParser.Parse会把这段文本拆成类型和参数。public static Aperture Parse(string token) { var body token.Replace(%AD, ).Replace(*%, ); var codePart body.Substring(0, body.IndexOf( )); // 可能直接是D10 int code int.Parse(codePart.TrimStart(D)); var shapePart body.Substring(body.IndexOf( ) 1); var parts shapePart.Split(,); var kind parts[0] switch { C ApertureKind.Circle, R ApertureKind.Rectangle, O ApertureKind.Oval, _ ApertureKind.Custom }; var aperture new Aperture { Code code, Kind kind }; if (parts.Length 1 kind ApertureKind.Circle) aperture.SizeX double.Parse(parts[1]); if (parts.Length 2) aperture.SizeY double.Parse(parts[2]); return aperture; }这段代码没有处理HMS热转印散热孔那种复杂修饰但对绝大多数PCB生产文件已经够用。你可能会遇到一个坑同一个D10在文件中被定义了两次比如前一层定义为圆形后一层需要改成矩形。Gerber规范允许这种重定义所以我在ApertureTable.Set里不做查重直接覆盖但会在调试输出打一条警告。这样做的原因是很多CAD软件确实会复用D码号但图形含义不同覆盖比报错更符合实际生产。3.3 直线、闪光与圆弧坐标换算的完整逻辑Gerber坐标是整数字符串比如X1000Y2000它到底表示1mm还是10mm取决于文件头的s%FSLAX24Y24*%。这里的“X24”表示整数2位、小数4位所以1000就是0.1mm。CoordinateParser就干这个活public static double ParseCoordinate(string value, int intDigits, int decimalDigits) { long raw long.Parse(value); double divisor Math.Pow(10, decimalDigits); return raw / divisor; }注意这里一定要用long而不是int。Gerber坐标的绝对值可以到五位数乘以10的4次方后可能超过int上限用int解析会让你看到诡异的图形反转这点在第5章的避坑清单里还会专门讲。生成图元时项目把D01曝光、D02移动、D03闪光三种情况分开var point new PointD(currentX, currentY); // 经过换算的坐标 switch (dCode) { case 2: lastX currentX; lastY currentY; break; case 3: primitives.Add(new GerberFlash { Center point, ApertureCode currentDCode }); break; case 1: if (interpolation InterpolationMode.Linear) { primitives.Add(new GerberLine { Start new PointD(lastX, lastY), End point }); } else if (interpolation InterpolationMode.CwArc || interpolation InterpolationMode.CcwArc) { primitives.Add(BuildArc(lastX, lastY, currentX, currentY, iOffset, jOffset, interpolation)); } lastX currentX; lastY currentY; break; }D02只移动笔不产生图形D03是闪光直接放一个光圈D01线性模式画直线圆弧模式需要额外读取I和J偏移。I和J在指令里通常和坐标写在一起比如X20Y10I5J0*它表示圆弧中心相对起点偏了5和0毫米。圆弧构建是最容易写错的函数。我摘一段项目中正常的实现public static GerberArc BuildArc( double startX, double startY, double endX, double endY, double i, double j, InterpolationMode mode) { double centerX startX i; double centerY startY j; double radius Math.Sqrt(i * i j * j); double startAngle Math.Atan2(startY - centerY, startX - centerX); double endAngle Math.Atan2(endY - centerY, endX - centerX); bool clockwise mode InterpolationMode.CwArc; double sweep; if (clockwise) { sweep startAngle - endAngle; if (sweep 0) sweep 2 * Math.PI; } else { sweep endAngle - startAngle; if (sweep 0) sweep 2 * Math.PI; } return new GerberArc { Center new PointD(centerX, centerY), Radius radius, StartAngle startAngle, SweepAngle sweep }; }参数的关键在startAngle和endAngle都是弧度制而GDI的DrawArc用的是角度制渲染之前还要做一次RadiansToDegrees。很多人在这一步翻车直接把Math.PI当成180传入图形就会变成一个诡异的螺旋。4. 渲染与交互GDI双缓冲与缩放的实现细节4.1 图层渲染顺序与颜色配置PCB查看器要按真实生产顺序叠图层底铜、负片层、阻焊、丝印。项目在GerberRenderer.cs里维护了一个LayerRenderOrder数组不同层对应预设颜色private static readonly DictionaryLayerType, Color LayerColors new() { [LayerType.Copper] Color.FromArgb(200, 255, 140, 0), [LayerType.SolderMask] Color.FromArgb(80, 0, 255, 0), [LayerType.Silkscreen] Color.FromArgb(255, 255, 255, 255), [LayerType.Drill] Color.FromArgb(180, 0, 0, 0) };铜箔用半透明橙色阻焊用半透明绿色这样多层叠加时你能看出铜箔是否被阻焊覆盖。这个半透明叠加本身对显卡没有压力因为GDI渲染时是按多边形填充不会出现像素级混合爆炸。真正影响性能的是抗锯齿我们会在第5章细说。颜色配置建议放在一个ColorProfile配置类里而不是硬编码。你可以在MainForm上加一个ColorDialog让PCB工程师自定义铜箔颜色很多人习惯绿油背景也有人喜欢蓝色阻焊硬编码会被人吐槽。4.2 坐标变换与缩放平移渲染的核心是把世界坐标毫米映射到屏幕坐标像素。GerberCanvas里定义一个Matrix transform每次重绘前根据缩放比例和平移量重建。private Matrix BuildTransform() { var matrix new Matrix(); matrix.Scale(_scale, _scale, MatrixOrder.Append); matrix.Translate( Width / 2 - _viewCenter.X * _scale, Height / 2 - _viewCenter.Y * _scale, MatrixOrder.Append); return matrix; }这段代码的关键是先缩放再平移。平移量是viewCenter乘以scale后的结果意思是世界坐标系下的视图中心点经过缩放后要落在屏幕中心。如果你把顺序写反缩放的时候图形会沿左上角边界飞出这是一个很常见的坐标变换陷阱。GerberCanvas重写OnPaint时把所有图元转换成GraphicsPath然后用transform整体变换protected override void OnPaint(PaintEventArgs e) { e.Graphics.Clear(BackgroundColor); e.Graphics.SmoothingMode _isPanning ? SmoothingMode.HighSpeed : SmoothingMode.AntiAlias; using (var transform BuildTransform()) { foreach (var layer in _layers) { using (var path new GraphicsPath()) { foreach (var primitive in layer.Primitives) { primitive.AddToPath(path); } path.Transform(transform); e.Graphics.FillPath(new SolidBrush(LayerColors[layer.Type]), path); } } } }这已经是经过性能权衡的写法。GraphicsPath创建和销毁有成本所以项目在图层没变化时会把上一次生成的Path缓存起来只有缩放水平或图层切换时才重建。如果你直接把每个图元DrawLine十万条走线会卡到无法移动窗口。4.3 鼠标交互、测量与信息显示鼠标交互分三件事左键测量、中键或右键平移、滚轮缩放。缩放最值得注意的实现细节是“以鼠标位置为中心”。protected override void OnMouseWheel(MouseEventArgs e) { double oldScale _scale; _scale * (e.Delta 0) ? 1.1 : 0.9; // 让鼠标指向的世界坐标点缩放后仍然在鼠标位置 var worldBefore ScreenToWorld(e.Location); _viewCenter worldBefore; _scale oldScale; var worldAfter ScreenToWorld(e.Location); _viewCenter worldBefore - (worldAfter - _viewCenter); }这里ScreenToWorld要用当前的transform反算。如果缩放时没有校正_viewCenter你的画面会以左上角为中心缩放当图形有几平方厘米大时稍微放大一下目标就走出了视野。PCB工程师看板子几乎全靠这个细节不然用起来非常劝退。测量模式通过状态栏显示结果。记录鼠标左键按下的世界坐标拖动时实时计算与当前点的距离double distance Math.Sqrt( Math.Pow((worldPoint.X - _measureStart.X), 2) Math.Pow((worldPoint.Y - _measureStart.Y), 2)); toolStripStatusLabel.Text $距离: {distance:F3} mm;如果你需要测量两焊盘中心距这个功能已经够用。而如果后续想加网测量、角度测量可以直接在现有MeasureTool类里扩展。5. 避坑指南Gerber解析与渲染中最常见的5个问题5.1 整数坐标溢出导致图形镜像翻转现象打开一块尺寸较大的板子局部图形对称性完全错乱位置离谱甚至有些线段跑到负坐标区域。原因Gerber坐标用整数存储比如FSLAX24Y24表示小数4位一个200mm的板子坐标值可能是2000000超过了int上限的两百万实际上int能存21亿不会溢出。真正的溢出点是乘精度的时候如果直接int.Parse再除以10000没问题但如果你在解析时用int保存累计偏移多次累加后可能溢出。项目曾经把坐标解析写成int coord int.Parse(...)对超大工件确实会出现符号翻转。解决所有坐标变量统一使用long最后除以10的N次方时再转double。另外PointD类中的X和Y必须用double而不是float否则精度到小数后3位就丢了贴片厂会出现焊盘错位。5.2 圆弧方向与IJ中心点理解错误现象圆弧画成了半圆之外的形状或者弧向和实际走线相反。原因Gerber的G02表示顺时针G03表示逆时针但这个“顺时针”是站在Gerber坐标系的视角。有些EDA工具在导出时使用负坐标系统导致顺时针弧看起来是逆时针。另外IJ中心偏移是相对起点的不是相对原点的很多人直接当绝对坐标算弧就完全偏离。解决给BuildArc写单元测试用已知文件对照一个从(0,0)到(10,0)的圆弧I5,J0顺时针时应该画出上凸还是下凸先跑测试确认方向再决定startAngle和sweepAngle的正负。我在项目中加入了一组来自Samples的标准圆弧样例每次解析结果都会和参考图比对。5.3 光圈重复定义与D码错乱现象某些层显示出来非常粗或者图元全部变成了圆形闪光而实际应该是矩形走线。原因Gerber允许在不同层次阶段重新定义同一个D码。如果你的解析器在发现重复D10时报错退出会导致后续所有图元匹配到旧定义如果直接后定义覆盖但前一层还没绘制完就会让前一层变样。解决对每个GerberLayer创建独立的ApertureTable实例不要在整个文件共享一个表。我一开始为了省内存把光圈表放在Parser级结果多层文件乱成一锅粥。后来改成每层一个表问题立刻消失。文件内切换到新层时要重置光圈表状态。5.4 抗锯齿导致大数据量卡顿现象10万条走线时鼠标缩放反应慢到无法操作CPU占用100%。原因SmoothingMode.AntiAlias在GDI里是CPU运算每次Paint都会对多边形边缘做多次采样图元总数一多就直接卡死。解决采用“平移时降级缩放结束后重绘”策略。_isPanning标志位在MouseDown设置为trueMouseUp设为falseOnPaint里根据标志位选择HighSpeed或AntiAlias。还有一个优化是把静态图层预渲染成一张大Bitmap缩放时直接DrawImage只有缩放比例变化超过阈值才重新生成Bitmap。5.5 英制/公制单位混用现象同一个文件一部分图形尺寸正确另一部分放大了25.4倍或缩小了1/25.4。原因Gerber头部的FSLAX24Y24中没有单位标志单位写在MOIN或MOMM两条指令里。如果读数时忽略单位把英制文件当公制解析所有孔位都差25.4倍。解决解析器必须维护一个UnitType状态在遇到MOIN和MOMM时切换。遇到没有单位声明的旧式Gerber项目会在界面右下角弹一个提示让用户手动选择英寸或毫米而不要静默假设。这个提示虽然打扰人但避免了板子报废的风险。6. 二次开发把查看器升级成CAM校验小工具6.1 增加DRC间距检查看Gerber只能“看”不能自动发现问题。你可以利用Gerber.Core里的图元数据循环所有铜层计算相邻图元之间的最小间距。常见做法是把图元包围盒插入四叉树或统一网格然后只检查邻近格子里的图元对避免O(n^2)的笛卡尔积。实现时可以复用GerberRenderer里的坐标变换把世界坐标转成像素坐标做粗测再用真实距离精测。这个功能一旦加上这个工具就从一个“查看器”变成了“CAM前校验器”实用性立刻不一样。6.2 导出高清图片和SVG给PCB工程师截图汇报时屏幕分辨率经常不够清晰。你可以在GerberRenderer.cs里加一个RenderToBitmap方法传入目标DPI和毫米宽度创建一个高分辨率Bitmap然后逐个层绘制。SVG导出更简单Gerber图元本身就是矢量的把每个图元写成path标签即可。关键点是圆弧要离散化成多段贝塞尔曲线否则某些SVG渲染器不认单弧命令。6.3 命令行参数与自动化测试集成把UI剥离后Gerber.Core已经是个纯库。你可以定义一个Program.cs入口支持命令行参数-f input.GTL -o output.png -scale 2。这样就能在CI流水线里自动把每一版Gerber渲染成图片和上一版做像素对比检测意外改动。我后来在项目里加了一个Gerber.Cli工程把Parser的日志级别接入Console.WriteLine调试时直接跑命令行就能看到每一条光圈重定义警告。从那以后我每次拿到一份新Gerber文件都会先用命令行工具转成PNG再和EDA里的原图比对一遍确认单位和精度都没问题然后才敢把文件发给板厂。这个方法避免了至少两回“单位错位但肉眼没发现”的重大翻车。希望这个项目的源码结构也能帮你少走这些夜路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?