C#实现Gerber查看器:解析、渲染与二次开发实战

发布时间:2026/10/25 7:59:27
C#实现Gerber查看器:解析、渲染与二次开发实战

简介:面向电子工程师、PCB设计师及C#图形编程学习者的Gerber查看器源码项目,聚焦CAM/Genesis类Gerber文件的读入与可视化展示,帮助读者理解文本指令解析、坐标与单位转换、图形渲染等关键环节。压缩包共35个文件,约76KB,以11个cs源码文件为核心,配合resx与resources资源、config配置、csproj与sln工程文件及可执行程序,构成可直接编译运行的完整解决方案,目录中GMDO_V4.0体现版本迭代痕迹。已有3646人学习下载,说明其在Gerber解析与WinForms/WPF绘图实践上具备参考价值。读者可从中获取文件逐行读取、指令解析器构建、图元对象建模到界面缩放平移的完整实现思路,并借鉴层叠加、颜色编码、测量等扩展功能的组织方式,适合作为EDA工具开发入门与二次开发的对照样本。

1. 拿到一块 PCB 的 Gerber 文件,我为什么先打开这个 C# 查看器

上周帮同事排查一块四层板的开短路问题,对方甩过来一个压缩包,里面是十几个.gbr和.drl文件。用在线工具打开要上传、要等、还担心图纸外流;装个大型 CAM 软件又得几百兆起步。这种时候我一般会先翻出自己常备的 Gerber 查看器——一个用 C# 写的桌面小工具,双击就能跑,把 Gerber 读入、渲染、分层显示一条龙做完。它解决的就是「手头有一堆 Gerber 文件,想快速看一眼每层长什么样、焊盘和走线对不对」这个高频场景。适合做 PCB 设计、CAM 工程、硬件测试的开发者,也适合想研究 Gerber 格式解析和 GDI+ 图形渲染的同学。下面我按「能干什么 → 怎么跑 → 核心怎么实现 → 怎么改」拆一遍。

2. 技术栈与目录结构:一个 WinForms 桌面程序的最小骨架

2.1 技术选型:为什么是 C# + WinForms + GDI+

这个项目定位是「本地、轻量、即开即用」的 Gerber 查看器,所以技术栈非常克制。语言用 C#,界面用 WinForms,绘图用 GDI+(System.Drawing),文件解析全部手写,不依赖任何第三方 PCB 库。这套组合的好处是:.NET Framework 或 .NET 桌面运行时自带,编译出来一个 exe 加几个 dll 就能跑,不需要装 Python 环境、不需要 Node,也不涉及浏览器上传。

Gerber 文件本质是纯文本,一行一条命令,比如%FSLAX46Y46*%定义坐标格式、G01*切到线性插值、X1000000Y2000000D02*移动画笔、D03*打一个焊盘。解析器要做的就是把这些命令翻译成线段、圆弧、矩形、多边形,再交给 GDI+ 画到画布上。选 GDI+ 而不是 Direct2D,是因为 Gerber 单层图元数量通常在几万到几十万级别,GDI+ 配合双缓冲足够流畅,代码也简单得多。

常见做法是把「解析」和「渲染」彻底分开:解析层输出一个与界面无关的图元列表,渲染层只负责把图元列表画出来。这样后面加导出 PNG、加测量工具、换渲染后端都不用动解析代码。我一般会按这个思路组织工程。

2.2 目录结构:按职责切成四块

一个合格的 Gerber 查看器工程,目录大致长这样(不同版本命名会有出入,但职责划分基本一致):

GerberViewer/ ├── GerberViewer.sln ├── GerberViewer/ │ ├── Program.cs // 入口,启动主窗体 │ ├── Forms/ │ │ ├── MainForm.cs // 主界面:菜单、工具栏、画布 │ │ └── LayerPanel.cs // 图层列表与显隐控制 │ ├── Gerber/ │ │ ├── GerberParser.cs // 逐行解析 Gerber 命令 │ │ ├── GerberCommand.cs // 命令枚举与参数结构 │ │ ├── Aperture.cs // 光圈(D 码)定义 │ │ └── GerberDocument.cs // 解析结果:图层 + 图元集合 │ ├── Render/ │ │ ├── GerberRenderer.cs // 图元 -> GDI+ 绘制 │ │ └── ViewTransform.cs // 缩放、平移、坐标映射 │ ├── Drill/ │ │ └── ExcellonParser.cs // 钻孔文件(.drl)解析 │ └── Utils/ │ └── UnitConverter.cs // 英寸/毫米、坐标格式换算 └── README.md

Gerber/只做文本到图元的转换,不碰任何 UI 类型;Render/只认图元列表,不认文件;Forms/负责把两者串起来。这种切法在二次开发时特别省事——想加一个新图元类型,改GerberCommand和GerberRenderer两处即可,主窗体几乎不用动。

2.3 环境准备与第一次运行

运行环境要求不高:Windows 10/11,装 .NET Framework 4.7.2 以上或 .NET 6/8 桌面运行时,Visual Studio 2022(或 Rider)能打开 sln 即可。步骤:

# 1. 克隆或解压源码后,进入解决方案目录 cd GerberViewer # 2. 用 dotnet 命令行还原并编译(.NET SDK 项目) dotnet restore dotnet build -c Release # 3. 运行(如果项目是 .NET Framework,用 VS 直接 F5 更稳) dotnet run --project GerberViewer/GerberViewer.csproj

启动后主界面一般分三块:左侧图层列表(每层一个勾选框)、中间绘图画布、顶部工具栏(打开、缩放、适应窗口、单位切换)。点「打开」,选中一个 Gerber 文件或整个文件夹,程序会逐文件解析并加入图层列表。第一次跑建议拿一块简单板子的顶层铜(.gtl)和阻焊(.gts)试,图元少,出问题好定位。

提示:如果打开后画布一片空白,先确认文件是不是 RS-274X 格式(文件头有%FS、%MO这类扩展命令)。老式 RS-274D 需要单独的光圈表文件,这个查看器通常不处理。

3. 核心解析链路:一行 Gerber 命令怎么变成屏幕上的线段

3.1 坐标格式与单位:先把「数」翻译对

Gerber 里所有坐标都是整数,真实尺寸靠文件头的格式声明还原。%FSLAX46Y46*%的意思是:前导零省略(L)、绝对坐标(A)、X 和 Y 都是 4 位整数 + 6 位小数。所以X1000000实际是 1.000000 英寸(如果单位是英寸)。这一步错了,整张图要么缩成一点,要么放大到看不见,是新手最容易翻车的地方。

// UnitConverter.cs public class UnitConverter { public int IntegerDigits { get; private set; } = 4; public int DecimalDigits { get; private set; } = 6; public bool IsInch { get; private set; } = true; // 解析 %FSLAX46Y46*% 这类格式声明 public void ParseFormatSpec(string line) { // 形如 FSLAX46Y46,取 X 后两位数字 int xIdx = line.IndexOf('X'); if (xIdx >= 0 && line.Length > xIdx + 2) { IntegerDigits = line[xIdx + 1] - '0'; DecimalDigits = line[xIdx + 2] - '0'; } } // 把 Gerber 整数坐标转成毫米(内部统一用毫米) public double ToMillimeter(long raw) { double scale = Math.Pow(10, DecimalDigits); double value = raw / scale; // 先还原成文件单位 return IsInch ? value * 25.4 : value; // 英寸转毫米 } }

IntegerDigits和DecimalDigits直接决定缩放系数,ToMillimeter把内部坐标统一到毫米,后面渲染、测量都基于毫米,避免单位混用。注意%MOIN*%/%MOMM*%要单独解析并设置IsInch,别只认格式声明忘了单位声明。

3.2 命令分发:一个 switch 吃掉大部分 Gerber 指令

Gerber 的命令分两类:扩展命令(%...*%)和普通命令(...*)。解析器按行读,先判断是不是扩展命令,再按首字符分发。核心就是一个状态机:当前坐标、当前光圈、当前插值模式(G01 直线 / G02 顺时针弧 / G03 逆时针弧)、是否处于区域填充(G36/G37)。

// GerberParser.cs(节选) public GerberDocument Parse(string filePath) { var doc = new GerberDocument(); var state = new ParserState(); // 当前坐标、光圈、插值模式 foreach (var rawLine in File.ReadLines(filePath)) { var line = rawLine.Trim(); if (line.Length == 0) continue; if (line.StartsWith("%")) // 扩展命令 { HandleExtendedCommand(line, doc, state); continue; } // 普通命令可能一行多条,按 '*' 切开 foreach (var cmd in line.Split('*', StringSplitOptions.RemoveEmptyEntries)) { HandleCommand(cmd, doc, state); } } return doc; } private void HandleCommand(string cmd, GerberDocument doc, ParserState state) { if (cmd.StartsWith("G04")) return; // 注释,直接跳过 if (cmd.StartsWith("G01")) { state.Interpolation = InterpMode.Linear; return; } if (cmd.StartsWith("G02")) { state.Interpolation = InterpMode.CwArc; return; } if (cmd.StartsWith("G03")) { state.Interpolation = InterpMode.CcwArc; return; } if (cmd.StartsWith("G36")) { state.InRegion = true; return; } if (cmd.StartsWith("G37")) { state.InRegion = false; doc.FlushRegion(); return; } // D01 画线 / D02 移动 / D03 打焊盘 var (x, y, dCode) = ParseCoordinate(cmd, state); switch (dCode) { case 1: doc.AddLine(state.Current, (x, y), state.CurrentAperture); break; case 2: state.Current = (x, y); break; case 3: doc.AddFlash((x, y), state.CurrentAperture); break; } if (dCode == 1 || dCode == 2) state.Current = (x, y); }

ParserState保存跨行的状态,这是关键——Gerber 是流式命令,D02移动后下一条D01才知道从哪画到哪。G36/G37之间的线段要收集成闭合多边形再填充,不能逐段画线,否则区域填充会变成一堆描边。ParseCoordinate里要处理省略坐标(只给 X 不给 Y 表示 Y 不变),这是另一个高频坑。

3.3 光圈与焊盘:D 码表怎么建、怎么用

D10以上的 D 码是光圈编号,具体形状由%ADD10C,0.5*%这类定义给出:C圆形、R矩形、O椭圆、P多边形。解析时先建一张Dictionary<int, Aperture>,遇到D03就按当前光圈形状在坐标处画一个焊盘。

// Aperture.cs public class Aperture { public int Code { get; set; } // D 码,如 10 public char Shape { get; set; } // C/R/O/P public double[] Params { get; set; } // 尺寸参数,单位毫米 public void Draw(Graphics g, PointF center, float scale) { float w = (float)(Params[0] * scale); switch (Shape) { case 'C': g.FillEllipse(Brushes.Gold, center.X - w / 2, center.Y - w / 2, w, w); break; case 'R': float h = (float)(Params[1] * scale); g.FillRectangle(Brushes.Gold, center.X - w / 2, center.Y - h / 2, w, h); break; // O、P 类似,按参数画椭圆或多边形 } } }

Params里存的是文件单位换算后的毫米值,scale是当前视图缩放系数。焊盘用填充而不是描边,视觉上更接近 CAM 软件的显示习惯。注意圆形光圈画D01线时,线宽就是光圈直径,GDI+ 里用Pen的Width设置即可,别用默认 1 像素。

4. 渲染与交互:缩放平移不飘、图层叠加不乱

4.1 视图变换:世界坐标到屏幕坐标的一层映射

画布上的所有绘制都要经过一次变换:世界坐标(毫米)→ 屏幕坐标(像素)。把缩放和平移收进一个ViewTransform类,鼠标滚轮改Scale、拖拽改Offset,绘制时统一调用WorldToScreen。这样无论图元多少,交互逻辑只有一处。

// ViewTransform.cs public class ViewTransform { public float Scale { get; set; } = 1f; // 像素/毫米 public PointF Offset { get; set; } // 屏幕平移量 public PointF WorldToScreen(PointF world) => new PointF(world.X * Scale + Offset.X, world.Y * Scale + Offset.Y); // 以鼠标位置为中心缩放,避免缩放时图形跑偏 public void ZoomAt(PointF screenPoint, float factor) { var before = ScreenToWorld(screenPoint); Scale *= factor; var after = ScreenToWorld(screenPoint); Offset = new PointF( Offset.X + (after.X - before.X) * Scale, Offset.Y + (after.Y - before.Y) * Scale); } public PointF ScreenToWorld(PointF screen) => new PointF((screen.X - Offset.X) / Scale, (screen.Y - Offset.Y) / Scale); }

ZoomAt里先算缩放前鼠标对应的世界坐标,缩放后再算一次,用差值修正Offset,这样滚轮缩放时鼠标下的点始终不动。这是所有图形查看器的通用技巧,少了它缩放体验会很别扭。Scale的初始值建议在「适应窗口」时按图形包围盒算出来,别硬编码。

4.2 双缓冲与分层绘制:几十万图元也不闪

WinForms 的Panel默认会闪,解决办法是开双缓冲,并在OnPaint里按图层顺序绘制。每个图层有自己的颜色和显隐开关,绘制顺序按图层列表从上到下,顶层铜最后画,视觉上压在最上面。

// MainForm.cs 中的画布绘制 protected override void OnPaint(PaintEventArgs e) { var g = e.Graphics; g.SmoothingMode = SmoothingMode.AntiAlias; g.Clear(Color.Black); foreach (var layer in _layers.Where(l => l.Visible)) { using var pen = new Pen(layer.Color, 1f); foreach (var seg in layer.Segments) { var p1 = _transform.WorldToScreen(seg.Start); var p2 = _transform.WorldToScreen(seg.End); pen.Width = Math.Max(1f, (float)(seg.Width * _transform.Scale)); g.DrawLine(pen, p1, p2); } foreach (var flash in layer.Flashes) flash.Aperture.Draw(g, _transform.WorldToScreen(flash.Position), _transform.Scale); } }

SmoothingMode.AntiAlias让斜线不锯齿;pen.Width随缩放变化,放大后线宽跟着变粗,符合真实比例。图元特别多时,可以按包围盒做视口裁剪,只画屏幕内的线段,这一条能把大板子的重绘时间从几百毫秒压到几十毫秒。图层颜色建议给一套默认配色(顶层红、底层蓝、阻焊绿、丝印白),和主流 CAM 软件的习惯对齐,用户上手零成本。

4.3 钻孔文件:Excellon 和 Gerber 不是一回事

很多人以为.drl也是 Gerber,其实它是 Excellon 格式,语法完全不同:M48开头、T1C0.3定义刀具、X1000Y2000是钻孔坐标、M30结束。解析器要单独写一个ExcellonParser,输出钻孔点列表,渲染时画成小圆。单位声明在METRIC/INCH行,别漏。

// ExcellonParser.cs(节选) public List<DrillHit> Parse(string path) { var hits = new List<DrillHit>(); var tools = new Dictionary<int, double>(); // 刀具号 -> 直径(mm) int currentTool = 0; foreach (var line in File.ReadLines(path)) { if (line.StartsWith("T") && line.Contains("C")) { // T1C0.3 -> 刀具 1,直径 0.3mm var parts = line.Substring(1).Split('C'); tools[int.Parse(parts[0])] = double.Parse(parts[1]); } else if (line.StartsWith("T")) { currentTool = int.Parse(line.Substring(1)); } else if (line.StartsWith("X")) { var (x, y) = ParseXY(line); hits.Add(new DrillHit(x, y, tools.GetValueOrDefault(currentTool, 0.3))); } } return hits; }

tools字典把刀具号和直径对上,currentTool跟踪当前用的是哪把刀。钻孔渲染成实心圆,直径按刀具值缩放,和焊盘叠在一起就能看出孔位对不对。

5. 避坑与排查:这几个坑我踩过不止一次

5.1 打开后图形缩成一个小点或撑满屏幕

现象:文件能解析,但画布上要么一个像素点,要么全是线。原因几乎都是坐标格式没解析对——%FSLAX46Y46*%里的 4 和 6 被当成固定值,或者单位声明%MOIN*%没读到,导致缩放系数差了几个数量级。解决:在ParseFormatSpec里打印出IntegerDigits、DecimalDigits、IsInch三个值,和文件头对照;「适应窗口」时用图形包围盒反算Scale,别用固定初值。

5.2 区域填充(G36/G37)画出来是一堆描边

现象:铺铜区域显示成网格线而不是实心块。原因是把G36到G37之间的线段逐条DrawLine了,没有收集成闭合路径再FillPolygon。解决:在ParserState里维护一个List<PointF> regionPoints,G36开始收集,G37时FlushRegion一次性填充。注意区域可能不闭合,填充前要判断首尾点距离,必要时补一条闭合边。

5.3 省略坐标导致线段乱飞

现象:某些板子的走线突然连到原点或上一个很远的位置。原因是 Gerber 允许省略坐标:X1000*表示只改 X,Y 保持上次的值;Y2000*同理。如果解析时把缺失的坐标当 0 处理,就会画出错误的线。解决:ParseCoordinate里用state.Current兜底,只更新命令里出现的那个轴。

5.4 大文件打开卡死或内存暴涨

现象:几十 MB 的 Gerber 打开要十几秒,任务管理器内存飙到几个 G。原因是把每个图元都存成独立对象,且绘制时全量遍历。解决:图元用结构体数组或List<T>紧凑存储;绘制前按视口包围盒裁剪;解析用File.ReadLines流式读,别ReadAllLines一次性载入。我一般还会加一个进度条,解析超过 1 秒就显示,避免用户以为程序挂了。

5.5 图层颜色和显隐状态在重开后丢失

现象:调好的配色和图层开关,关掉程序再打开又回到默认。原因是这些状态只存在内存里。解决:用Properties.Settings或一个简单的 JSON 配置文件持久化图层名、颜色、可见性,启动时按文件名匹配恢复。注意别把绝对路径写死,换台机器就失效。

6. 二次开发:加一个测量工具,顺便把解析层解耦

拿到这份源码后,最值得做的二次开发是加「测量」功能——点两个点算距离,这是看 Gerber 时仅次于「看一眼」的高频需求。实现思路很直接:在MainForm里加一个测量模式开关,鼠标点击时用ScreenToWorld把屏幕坐标转回毫米坐标,存两个点,OnPaint里画一条高亮线和距离文字。核心就三行:

// 测量模式下的鼠标点击 var world = _transform.ScreenToWorld(e.Location); _measurePoints.Add(world); if (_measurePoints.Count == 2) { double dist = Math.Sqrt( Math.Pow(_measurePoints[1].X - _measurePoints[0].X, 2) + Math.Pow(_measurePoints[1].Y - _measurePoints[0].Y, 2)); _measureText = $"{dist:F3} mm"; }

ScreenToWorld是ViewTransform里现成的,dist单位是毫米,因为解析层已经统一换算过。这里能看出前面「解析与渲染分离」的价值:测量只碰视图变换,完全不关心 Gerber 语法。

再往深改,可以把GerberDocument的图元列表暴露成只读接口,外面套一层导出模块,把线段和焊盘转成 SVG 或 PNG。验证方法很简单:拿一块已知尺寸的板子(比如 100mm × 80mm 的矩形边框),用测量工具量边框,误差应在 0.01mm 以内;再对比 CAM 软件里同一位置的坐标,确认解析没有偏移。

改造方向改动文件验证方式
测量距离MainForm.cs、ViewTransform.cs量已知尺寸边框,误差 < 0.01mm
导出 PNGGerberRenderer.cs 新增方法导出图与屏幕显示逐像素对比
支持 RS-274DGerberParser.cs、Aperture.cs用带光圈表的老文件解析不报错
图层配色持久化MainForm.cs、配置文件重启后颜色和显隐状态一致

血泪经验是:改解析层之前,先把ParserState的所有字段列出来,确认新命令不会破坏跨行状态;改渲染层之前,先用小文件跑一遍,别拿几十 MB 的大板子当试验田。从那以后我每次动 Gerber 解析代码,都强制拿三块不同来源的板子各跑一遍——顶层铜、阻焊、钻孔全开,确认没有回归再提交。希望帮到你。

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