简介:这份资源是一套基于C#实现的Gerber查看器源码,面向电子工程师、PCB设计人员以及希望学习文本解析与图形渲染的C#开发者,用于解决PCB制造中Gerber图形导入与查看的问题。压缩包共35个文件,约76KB,以cs源码文件为主,配合resx与resources资源文件、exe可执行程序、config配置、pdb调试符号及sln解决方案等,构成一个可直接编译运行的完整工程。项目围绕Gerber文件结构展开,涵盖StreamReader逐行读取、正则与字符串解析指令、绝对与增量坐标及单位转换处理,并通过WPF或WinForms构建可缩放平移的图形界面,还涉及层叠加显示、颜色编码与测量工具等CAM Viewer常见功能。目前已有3641人学习下载,适合想深入理解Gerber解析流程、借鉴C#图形界面实现思路的读者参考与二次开发。
1. 从一块四层板的返工说起:这个 C# Gerber 查看器到底能干什么
前阵子帮朋友看一块四层板的返工问题,对方发来一堆 .gbr 和 .drl 文件,问我能不能快速确认阻焊开窗和线路层有没有对偏。装商业 CAM 软件太重,网页版又担心文件外流,最后翻出一个用 C# 写的 Gerber 查看器源码包,编译完直接拖文件进去就能看。这件事让我意识到,C# Gerber 读入源码这类东西对硬件和 PCB 工程师来说,价值不在于功能多全,而在于它把 Gerber 解析这件事从黑匣子里拽了出来——你能看到每一层是怎么被读进来、怎么被画到画布上的。
这个项目就是一个典型的PCB Gerber Viewer,用 C# 实现 Gerber 文件的解析与渲染,支持常见的 RS-274X 格式,能加载线路层、阻焊层、丝印层、钻孔文件,并在窗口里叠加显示。它适合两类人:一是想快速查看 Gerber 又不想装大型 CAM 工具的硬件工程师;二是想拿 Gerber 解析练手、或者要把它嵌进自己CAM Viewer工具链的 C# 开发者。源码结构不复杂,但把光圈、D 码、坐标格式这些 Gerber 核心概念都覆盖到了,拿来当Genesis类工具的轻量替代或者二次开发起点都合适。
2. 技术选型与整体架构:为什么用 C# 做 Gerber 解析
2.1 技术栈与运行环境
项目基于 .NET 平台,用 C# 编写,界面部分常见做法是 WinForms 或 WPF。从源码包的实际结构看,渲染层大概率走的是 GDI+ 的 Graphics 对象,因为 Gerber 的绘制本质就是大量线段、圆弧和填充区域的叠加,GDI+ 在这类二维矢量绘制上足够用,而且不引入额外依赖。如果你拿到的是 WPF 版本,那渲染会走 DrawingContext,思路一样,只是 API 不同。
运行环境上,建议用 Visual Studio 2019 及以上打开解决方案,目标框架一般是 .NET Framework 4.6+ 或 .NET 6/8。如果源码里用的是 .NET Framework,Windows 上直接 F5 就能跑;如果是 .NET 6+,需要确认安装了对应 SDK。这里有个血泪经验:Gerber 文件里坐标单位可能是英寸也可能是毫米,格式可能是 2.4、3.5 等不同精度,如果解析时没做单位归一化,画出来的板子要么大得离谱要么小得看不见。
2.2 模块划分:解析、数据模型、渲染三层
整个项目可以拆成三层,这也是我读这份源码时最先理清的东西:
| 层级 | 职责 | 关键类/文件 |
|---|---|---|
| 解析层 | 逐行读取 Gerber 指令,识别 D 码、G 码、坐标 | GerberParser |
| 数据层 | 存储光圈定义、绘制对象、层信息 | Aperture、DrawObject、GerberLayer |
| 渲染层 | 把绘制对象画到画布,处理缩放平移 | GerberRenderer、MainForm |
解析层负责把文本变成结构化数据,数据层是中间表示,渲染层只认数据层,不碰原始文本。这种分层的好处是,如果你想换成 OpenGL 或者 SkiaSharp 渲染,只需要改渲染层,解析逻辑不用动。
2.3 一个 Gerber 文件的解析流程
Gerber RS-274X 文件本质是一系列指令,核心包括:
%FSLAX24Y24*%:定义坐标格式,这里表示 2 位整数 4 位小数%MOMM*%或%MOIN*%:定义单位是毫米还是英寸%ADD10C,0.5*%:定义 D 码 10 是一个直径 0.5 的圆形光圈D10*:选择当前光圈为 D10X100000Y200000D01*:以当前光圈从当前位置画线到 (100000, 200000)D02*:移动不画线,D03*:闪绘(打点)
解析器就是按行读,遇到%开头的处理扩展指令,遇到D开头的处理绘图指令。下面这段代码展示了核心的解析循环逻辑:
// GerberParser.cs 核心解析循环 public GerberLayer Parse(string filePath) { var layer = new GerberLayer(); var lines = File.ReadAllLines(filePath); double currentX = 0, currentY = 0; Aperture currentAperture = null; foreach (var rawLine in lines) { var line = rawLine.Trim(); if (string.IsNullOrEmpty(line)) continue; // 扩展指令,以 % 开头 if (line.StartsWith("%")) { ParseExtendedCommand(line, layer); } // 坐标和 D 码指令 else if (line.StartsWith("X") || line.StartsWith("Y") || line.StartsWith("D")) { var cmd = ParseCoordinateCommand(line); switch (cmd.Code) { case 1: // D01 画线 layer.Objects.Add(new LineObject(currentX, currentY, cmd.X, cmd.Y, currentAperture)); currentX = cmd.X; currentY = cmd.Y; break; case 2: // D02 移动 currentX = cmd.X; currentY = cmd.Y; break; case 3: // D03 闪绘 layer.Objects.Add(new FlashObject(cmd.X, cmd.Y, currentAperture)); break; } } } return layer; }这段代码里,currentX/currentY维护当前绘图位置,currentAperture是当前选中的光圈。D01 生成线段对象,D02 只更新位置,D03 生成一个闪绘点。参数说明:cmd.X和cmd.Y是从字符串里按坐标格式解析出来的实际坐标值,解析时要根据%FSLAX..Y..%里定义的小数位数做除法。常见做法是先把格式存成intDecimals,解析时value / Math.Pow(10, intDecimals)。
2.4 光圈与 D 码的处理
光圈定义是 Gerber 里最容易翻车的部分。%ADD10C,0.5*%表示 D 码 10 是圆形,直径 0.5。但光圈类型不止 C,还有 R(矩形)、O(椭圆)、P(多边形)。如果解析时只认 C,遇到 R 就会画错。源码里通常用一个字典存光圈:
// 光圈定义解析 private void ParseApertureDefinition(string line, GerberLayer layer) { // 格式:%ADD10C,0.5*% var content = line.Trim('%', '*').Substring(3); // 去掉 ADD int dCode = int.Parse(content.Substring(0, 2)); char shape = content[2]; var parameters = content.Substring(4).Split('X'); Aperture ap; switch (shape) { case 'C': ap = new CircleAperture(double.Parse(parameters[0])); break; case 'R': ap = new RectAperture(double.Parse(parameters[0]), double.Parse(parameters[1])); break; default: ap = new CircleAperture(0.1); // 兜底 break; } layer.Apertures[dCode] = ap; }这里dCode是光圈编号,shape是形状字符,parameters是尺寸参数。注意%ADD10C,0.5*%里逗号后面才是参数,不同形状参数个数不同,R 需要两个(宽高),C 只需要一个(直径)。如果解析时没做长度校验,遇到参数不全的文件就会抛异常。
3. 从源码到运行:编译、加载与渲染的完整链路
3.1 编译与首次运行
拿到源码包后,第一步是确认解决方案文件。通常根目录下会有.sln文件,双击用 Visual Studio 打开。如果提示缺少某个 NuGet 包,在解决方案上右键“还原 NuGet 包”。常见依赖可能包括System.Drawing.Common(如果用的是 .NET Core/6+ 的 GDI+ 封装)。
编译时如果报“找不到 Windows Forms 引用”,检查项目文件里的<TargetFramework>和<UseWindowsForms>设置。.NET 6+ 的 WinForms 项目需要显式开启:
<PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net6.0-windows</TargetFramework> <UseWindowsForms>true</UseWindowsForms> </PropertyGroup>编译通过后直接 F5 运行,主窗口应该会出现一个空白画布和菜单栏。如果窗口没出来但进程在,多半是渲染初始化时抛了异常被吞掉了,在Main里加个 try-catch 把异常打到控制台。
3.2 加载 Gerber 文件与图层管理
加载逻辑一般在菜单的“打开文件”里。核心是调用解析器拿到GerberLayer,然后把它加到图层列表,最后触发重绘。一个典型的加载流程:
// MainForm.cs 加载文件 private void OpenGerberFile(string path) { try { var parser = new GerberParser(); var layer = parser.Parse(path); layer.Name = Path.GetFileNameWithoutExtension(path); layer.Color = GetNextLayerColor(); // 给每层分配不同颜色 _layers.Add(layer); RenderAllLayers(); } catch (Exception ex) { MessageBox.Show($"解析失败:{ex.Message}"); } }_layers是一个List<GerberLayer>,支持多层叠加。GetNextLayerColor常见做法是预定义一组颜色循环使用,比如线路层用红色、阻焊层用绿色、丝印层用白色。渲染时按顺序画,后加的层盖在前面。
3.3 渲染与坐标变换
渲染是这份源码里最值得细看的部分。Gerber 坐标是原始设计坐标,可能范围很大(比如 0 到 200000),直接画到画布上会超出窗口。所以需要一个变换:先计算所有图层的包围盒,然后根据窗口大小算缩放比例和平移量。
// GerberRenderer.cs 坐标变换 public void Render(Graphics g, List<GerberLayer> layers, Size canvasSize) { // 计算所有图层的包围盒 var bounds = GetOverallBounds(layers); double scaleX = canvasSize.Width / bounds.Width; double scaleY = canvasSize.Height / bounds.Height; double scale = Math.Min(scaleX, scaleY) * 0.9; // 留 10% 边距 // 设置变换矩阵:先平移到原点,再缩放,再平移到画布中心 g.TranslateTransform( (float)(canvasSize.Width / 2 - bounds.Center.X * scale), (float)(canvasSize.Height / 2 + bounds.Center.Y * scale) ); g.ScaleTransform((float)scale, (float)-scale); // Y 轴翻转,Gerber 原点在左下 foreach (var layer in layers) { foreach (var obj in layer.Objects) { obj.Draw(g, layer.Color); } } }这里有几个关键点:bounds是所有图层的整体范围,scale取 X/Y 缩放比例的较小值保证全部可见。ScaleTransform的 Y 轴传负数是因为 Gerber 坐标系原点在左下角,而屏幕坐标原点在左上角,不翻转的话板子会上下颠倒。obj.Draw是每个绘制对象自己实现的方法,线段用g.DrawLine,闪绘点用g.FillEllipse。
3.4 钻孔文件的处理
钻孔文件通常是 Excellon 格式,和 Gerber 不一样,但思路类似:解析刀具定义和钻孔坐标。常见做法是单独写一个ExcellonParser,解析出来的孔用FlashObject表示,光圈用刀具直径。加载时把钻孔层也加到_layers里,颜色设成灰色或黑色,这样叠在线路层上就能看出孔和焊盘的对位关系。
提示:钻孔文件里刀具单位可能是英寸,而 Gerber 是毫米,加载时要做单位换算,否则孔的位置会整体偏移。
4. 避坑与常见问题:解析 Gerber 时容易翻车的几个点
4.1 坐标格式没解析对,板子画出来是歪的
现象:加载文件后图形严重变形,或者所有线段挤成一团。原因:%FSLAX24Y24*%里的 2 和 4 分别表示整数位和小数位,解析坐标时要用10^4做除数。如果直接int.Parse当整数用,坐标会放大一万倍。解决:在解析扩展指令时把格式存下来,解析坐标时统一除以Math.Pow(10, decimals)。
4.2 光圈定义顺序问题导致 D 码找不到
现象:解析到D10*时抛 KeyNotFoundException。原因:Gerber 文件里光圈定义(%ADD...%)不一定都在使用之前,有些文件会先出现D10*再定义。解决:分两遍解析,第一遍只收集光圈定义,第二遍处理绘图指令。或者遇到未定义光圈时先跳过,等定义出现后再补画。
4.3 负片层和极性处理缺失
现象:阻焊层画出来是实心的,盖住了线路。原因:Gerber 有正片和负片之分,负片层(比如某些阻焊层)的绘制逻辑是“画的地方是开窗,不画的地方是覆盖”。如果统一按正片画,结果就反了。解决:在图层属性里加一个IsNegative标志,渲染负片层时用背景色填充整个区域,再用透明色“擦除”绘制对象。
4.4 大文件渲染卡顿
现象:加载几万个对象的 Gerber 文件时界面卡死。原因:GDI+ 逐条绘制线段,没有做批量优化。解决:常见做法是用GraphicsPath把同层同光圈的线段合并成一个路径,一次性绘制;或者用双缓冲(SetStyle(ControlStyles.OptimizedDoubleBuffer, true))减少闪烁。如果还卡,考虑把渲染放到后台线程,画好后用Invoke更新 UI。
4.5 单位混用导致尺寸不对
现象:板子看起来比例正常但尺寸差 25.4 倍。原因:Gerber 用英寸,钻孔用毫米,或者反过来。解决:解析时统一转成毫米存储,渲染时不再关心原始单位。转换系数:1 英寸 = 25.4 毫米。
5. 二次开发与改造:把这个查看器变成你自己的工具
5.1 加一个测量工具
查看器最实用的扩展就是测量。思路是在鼠标按下时记录起点,移动时画一条临时线并显示距离,松开后固定。核心是屏幕坐标转 Gerber 坐标:
// 屏幕坐标转 Gerber 坐标 private PointF ScreenToGerber(Point screenPoint) { var matrix = _graphics.Transform.Clone(); matrix.Invert(); var points = new[] { screenPoint }; matrix.TransformPoints(points); return points[0]; }拿到 Gerber 坐标后,两点距离就是Math.Sqrt(dx*dx + dy*dy),再根据单位换算成毫米或密耳显示。这个功能对确认线宽、间距特别有用。
5.2 导出为图片或 PDF
如果要把视图分享给别人,加一个导出功能。GDI+ 可以直接把Graphics的内容画到Bitmap上:
public void ExportToPng(string path, int width, int height) { using var bmp = new Bitmap(width, height); using var g = Graphics.FromImage(bmp); g.Clear(Color.Black); Render(g, _layers, new Size(width, height)); bmp.Save(path, ImageFormat.Png); }导出 PDF 的话,常见做法是引入PdfSharp或QuestPDF,把同样的渲染逻辑画到 PDF 页面上。注意 PDF 的坐标系和屏幕又不一样,需要再调整一次变换。
5.3 图层可见性与排序
多图层叠加时,用户需要单独开关某一层。给GerberLayer加一个Visible属性,渲染时跳过不可见的层。排序则用一个Order字段,渲染前按 Order 排序。UI 上可以用CheckedListBox列出所有层,勾选控制可见性,拖拽调整顺序。
5.4 性能优化的一个具体技巧
如果 Gerber 对象超过十万个,逐条绘制会明显卡顿。我一般会做一层缓存:把每个图层的绘制结果先画到一个离屏Bitmap上,缩放平移时只对这个 Bitmap 做变换,而不是重新遍历所有对象。只有当图层内容变化时才重建缓存。这样缩放操作能保持流畅,代价是内存占用增加。
// 离屏缓存 private Bitmap _layerCache; private bool _cacheDirty = true; private void RenderLayerCached(Graphics g, GerberLayer layer) { if (_cacheDirty || _layerCache == null) { _layerCache = new Bitmap((int)bounds.Width, (int)bounds.Height); using var cg = Graphics.FromImage(_layerCache); foreach (var obj in layer.Objects) obj.Draw(cg, layer.Color); _cacheDirty = false; } g.DrawImage(_layerCache, ...); }这个技巧在对象多的时候提升很明显,但要注意 Bitmap 尺寸不能超过系统限制,超大板子需要分块缓存。
5.5 改造时的一个教训
我最早改这个项目时,图省事直接在解析器里把坐标转成了屏幕坐标,结果后来加测量功能时发现原始坐标丢了,又得回头重写解析层。从那以后我每次做类似工具,都强制让解析层只输出原始设计坐标,所有变换只在渲染层做。这样上层功能怎么改,底层数据都是干净的。希望这个习惯能帮到你。
本文还有配套的精品资源,点击获取