简介:这份资源面向使用 Visual Studio 与 MFC 进行桌面开发的 C++ 学习者,以及需要查看 PCB 设计文件的工程师,解决 Gerber 文件内容难以直观浏览的问题。项目基于 MFC 框架编写,通过 CFileDialog 选择 Gerber 文件,解析 RS-274X 格式中的 G 代码、M 代码、坐标与层定义等指令,再借助 CListCtrl 控件将解析结果逐行展示,便于检查与验证电路板各层图形。压缩包共 25 个文件,约 150KB,包含 9 个 h 头文件与 6 个 cpp 源文件承载核心解析与界面逻辑,另有 vcxproj、sln、filters 等工程配置,rc、rc2、ico、aps 等界面资源,以及 ReadMe.txt 说明文档,结构完整可直接编译运行。目前已有 801 人学习下载。读者可从中掌握 Gerber 文件格式解析思路、MFC 文件 I/O 与列表控件用法、对话框布局设计及异常处理方式,是提升 C++ 与 PCB 领域实践能力的参考案例。
1. 从一份 Gerber 文件说起:VS + MFC 里把光绘数据读成可读文本
手里拿到一块 PCB 的 Gerber 文件,用记事本打开是一堆G04、D10、X…Y…D01*之类的坐标指令,密密麻麻根本看不出哪条是走线、哪条是焊盘。这个项目的切入点就很实在:在 Visual Studio 里用 MFC 搭一个桌面程序,把 Gerber 文件读进来,按行拆开显示到界面上,让原本像黑匣子一样的光绘数据变成一行行能看、能查、能对照的文本。它适合两类人:一类是刚接触 VC++/MFC、想找一个完整读写文件 + 界面显示练手项目的开发者;另一类是做 PCB 相关工具、需要先把 Gerber 解析成中间文本再往下处理的人。技术栈就是 VS + MFC + VC,没有花哨的第三方库,核心是文件 IO、字符串切分和 CListCtrl 显示。
2. 项目定位与技术选型:为什么用 MFC 而不是控制台
2.1 这个项目到底解决什么问题
Gerber 是 PCB 行业通用的光绘格式,本质是一串带坐标和绘图指令的文本。直接看原始文件,指令和坐标混在一起,人眼很难快速定位某一段走线或某个焊盘。这个项目的目标不是做完整的 Gerber 渲染器,而是先做“可读化”这一步:把文件按行读出来,分行显示,让开发者能逐行检查数据、验证后续解析逻辑是否正确。
从工程角度看,这是一个典型的“数据预处理 + 界面展示”小工具。它的价值在于:当你后面要写真正的 Gerber 解析器(比如提取孔径、识别 D 码、还原坐标)时,得先有一个能稳定读文件、能按行展示、能定位到具体行的调试界面。很多 PCB 工具的开发卡壳,不是卡在算法,而是卡在“我连文件里到底写了什么、第几行有问题都看不到”。这个项目就是补上这一环。
技术选型上,用 VS + MFC 而不是控制台,原因也很直接:控制台读大文件翻页麻烦,而 MFC 的 CListCtrl 或 CEdit 能一次性把几千行铺开,还能加行号、加高亮、加搜索。对于需要反复对照原始文件行号的场景,图形界面比命令行舒服得多。VC 作为编译器,配合 VS 的调试器,断点打在读取循环里,能一行行看变量,这对排查编码、换行符、特殊字符问题非常关键。
2.2 技术栈拆解:VS、MFC、VC 各自承担什么
Visual Studio 在这里是 IDE 和调试器,负责工程管理、断点调试、资源编辑器。MFC 是界面框架,提供 CWinApp、CFrameWnd、CDialog、CListCtrl、CFile 这些类,省去手写 Win32 API 的窗口过程。VC 是 C++ 编译器,负责把 MFC 代码编译成可执行文件。
常见做法是建一个基于对话框的 MFC 工程,主界面放一个“打开文件”按钮、一个 CListCtrl 或 CEdit 用来显示行内容、一个状态栏显示当前文件路径和总行数。文件读取用 CFile 或 CStdioFile,后者更适合按行读文本,因为它有 ReadString 方法,能自动处理换行。
这里有个容易忽略的点:Gerber 文件可能是 UTF-8、GBK 或纯 ASCII,换行可能是\r\n也可能是\n。CStdioFile 在文本模式下读\r\n会正常,但如果文件是 Unix 换行,就得注意。我一般会先用二进制方式读一小段,判断换行符类型,再决定用哪种模式打开。这个判断逻辑不复杂,但能避免后面显示时所有行粘成一坨。
2.3 目录结构长什么样
一个典型的 MFC 对话框工程,目录结构大致如下:
GerberReader/ ├── GerberReader.sln ├── GerberReader/ │ ├── GerberReader.h / .cpp // 应用类 │ ├── GerberReaderDlg.h / .cpp // 主对话框,界面逻辑 │ ├── FileReader.h / .cpp // 文件读取封装 │ ├── LineParser.h / .cpp // 行解析与切分 │ ├── resource.h // 资源 ID │ ├── GerberReader.rc // 对话框、控件资源 │ └── res/ // 图标等把文件读取和行解析单独拆成类,是为了后面二次开发时能替换解析逻辑而不动界面。FileReader 只负责“把文件变成字符串数组”,LineParser 负责“把一行拆成有意义的字段”。界面层只调用这两个类,不直接碰文件 IO。这种分层在 MFC 项目里不算强制,但一旦文件格式变多、解析规则变复杂,分层的好处就出来了。
3. 核心模块拆解:从打开文件到分行显示
3.1 文件读取:用 CStdioFile 按行读
先看文件读取的核心代码。这里用 CStdioFile,因为它封装了按行读取,比 CFile 手动找换行符省事。
// FileReader.h #pragma once #include <afx.h> #include <vector> #include <string> class CFileReader { public: // 读取文件,返回按行拆分的字符串数组 // 参数:strPath 文件路径;bSkipEmpty 是否跳过空行 static bool ReadLines(const CString& strPath, std::vector<CString>& vecLines, bool bSkipEmpty = false); };// FileReader.cpp #include "pch.h" #include "FileReader.h" bool CFileReader::ReadLines(const CString& strPath, std::vector<CString>& vecLines, bool bSkipEmpty) { vecLines.clear(); CStdioFile file; // 以只读文本模式打开,typeText 让 ReadString 自动处理换行 if (!file.Open(strPath, CFile::modeRead | CFile::typeText)) { return false; // 打开失败,调用方弹提示 } CString strLine; while (file.ReadString(strLine)) { // 去掉行尾可能残留的 \r,避免显示时多一个不可见字符 strLine.TrimRight(_T('\r')); strLine.TrimRight(_T('\n')); if (bSkipEmpty && strLine.IsEmpty()) { continue; } vecLines.push_back(strLine); } file.Close(); return true; }逻辑说明:CStdioFile::Open的typeText标志让文件以文本模式打开,ReadString会读到换行符为止,并把换行符从结果里去掉。但不同编辑器保存的文件,行尾可能是\r\n,ReadString在文本模式下通常能处理,不过为了保险,我还是会手动 Trim 掉\r和\n。参数bSkipEmpty控制是否跳过空行,Gerber 文件里空行一般无意义,跳过能让显示更紧凑。
参数说明:strPath是完整路径,MFC 里用 CString 方便和界面控件交互。vecLines是输出参数,用引用避免拷贝。返回 bool 表示成功与否,调用方根据返回值决定是否弹 MessageBox。
这里有个细节:如果文件很大(比如几十 MB 的 Gerber),一次性读进 vector 会占内存。常见做法是分页读,或者只读前 N 行做预览。但作为调试工具,先全读进来更简单,后面要优化再改成分块。
3.2 行解析:把一行拆成指令和坐标
读进来只是第一步,真正要看懂还得把每行拆开。Gerber 的行大致分几类:G04开头是注释,D开头是孔径选择,X…Y…D01是绘图指令,M02是结束。下面这个解析函数把一行拆成“指令类型 + 参数列表”。
// LineParser.h #pragma once #include <afx.h> #include <vector> struct GerberLine { CString strRaw; // 原始行 CString strType; // 指令类型,如 G04、D10、X...Y... std::vector<CString> vecParams; // 拆分后的参数 }; class CLineParser { public: // 解析一行 Gerber 数据 static GerberLine Parse(const CString& strLine); };// LineParser.cpp #include "pch.h" #include "LineParser.h" GerberLine CLineParser::Parse(const CString& strLine) { GerberLine result; result.strRaw = strLine; CString strWork = strLine; strWork.Trim(); // 去掉首尾空白 if (strWork.IsEmpty()) { result.strType = _T("EMPTY"); return result; } // Gerber 指令常以 * 结尾,先去掉 if (strWork.Right(1) == _T("*")) { strWork = strWork.Left(strWork.GetLength() - 1); } // 按 G、D、X、Y、M 等关键字切分 // 这里用简单扫描:遇到字母就认为是一个新字段的开始 int nLen = strWork.GetLength(); CString strCur; for (int i = 0; i < nLen; ++i) { TCHAR ch = strWork[i]; if (_istalpha(ch) && !strCur.IsEmpty()) { // 上一个字段结束,存入 if (result.strType.IsEmpty()) result.strType = strCur; else result.vecParams.push_back(strCur); strCur.Empty(); } strCur += ch; } // 最后一个字段 if (!strCur.IsEmpty()) { if (result.strType.IsEmpty()) result.strType = strCur; else result.vecParams.push_back(strCur); } return result; }逻辑说明:这个解析器不追求完整覆盖 Gerber 规范,而是把一行按字母边界拆成若干段。第一段作为指令类型,后面的作为参数。比如X100Y200D01会被拆成X100、Y200、D01,类型取第一个X100。这种拆法对调试足够用,能快速看出每行有哪些字段。
参数说明:strLine是原始行,GerberLine结构体保存原始行、类型和参数列表。用_istalpha判断字母,是因为 Gerber 里字段以字母开头。Trim去掉首尾空白,*是 Gerber 的块结束符,先去掉避免干扰。
注意:这个解析逻辑对G04注释行会把注释内容也当参数拆,如果注释里有空格或特殊字符,拆出来可能不理想。实际项目里可以加一个判断:如果类型是G04,直接把整行剩余部分作为注释文本,不再拆参数。
3.3 界面显示:CListCtrl 分行展示
读进来、解析完,最后要显示到界面上。用 CListCtrl 报表模式,两列:行号 + 内容。这样既能看行号定位,又能看原始内容。
// 在对话框类里 void CGerberReaderDlg::OnBnClickedBtnOpen() { CFileDialog dlg(TRUE, _T("gbr"), NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, _T("Gerber Files (*.gbr;*.ger)|*.gbr;*.ger|All Files (*.*)|*.*||")); if (dlg.DoModal() != IDOK) return; CString strPath = dlg.GetPathName(); std::vector<CString> vecLines; if (!CFileReader::ReadLines(strPath, vecLines, true)) { AfxMessageBox(_T("文件打开失败,请检查路径和权限。")); return; } // 清空列表 m_listLines.DeleteAllItems(); m_listLines.SetRedraw(FALSE); // 批量插入时先冻结重绘,避免闪烁 for (size_t i = 0; i < vecLines.size(); ++i) { CString strNo; strNo.Format(_T("%zu"), i + 1); int nIndex = m_listLines.InsertItem((int)i, strNo); m_listLines.SetItemText(nIndex, 1, vecLines[i]); } m_listLines.SetRedraw(TRUE); m_listLines.Invalidate(); // 更新状态栏 CString strStatus; strStatus.Format(_T("文件:%s 共 %zu 行"), (LPCTSTR)strPath, vecLines.size()); SetDlgItemText(IDC_STATIC_STATUS, strStatus); }逻辑说明:CFileDialog弹出文件选择框,过滤 Gerber 常见扩展名。读文件成功后,先DeleteAllItems清空旧数据,再SetRedraw(FALSE)冻结重绘,批量插入完再恢复。这是 MFC 里插入大量列表项的常规优化,不加的话几千行会闪得厉害。每行插入时,第一列是行号,第二列是内容。
参数说明:m_listLines是绑定的 CListCtrl 控件变量,需要在DoDataExchange里用DDX_Control绑定。InsertItem返回新行的索引,SetItemText设置第二列。状态栏用静态文本控件显示文件路径和总行数。
这里有个坑:如果 Gerber 文件有几万行,CListCtrl 插入会慢。常见做法是设一个上限,比如只显示前 5000 行,或者用虚拟列表(LVS_OWNERDATA)。作为调试工具,先显示前几千行通常够用,后面要全量再改虚拟列表。
4. 避坑与常见问题:编码、换行、大文件
4.1 中文路径导致打开失败
现象:文件放在中文目录下,CStdioFile::Open返回失败,但文件确实存在。
原因:MFC 的CStdioFile在部分 VS 版本和字符集配置下,对 Unicode 路径支持不完整,尤其是项目用多字节字符集时。
解决:把项目字符集改成“使用 Unicode 字符集”,或者打开文件前用CFile::Open的宽字符版本。我一般直接在项目属性里把 Character Set 设为 Use Unicode,然后所有字符串用_T()或L"",路径问题基本消失。
4.2 换行符不一致导致所有行粘在一起
现象:文件在别的编辑器里看是正常多行,读进来后所有内容挤在一行。
原因:文件用的是 Unix 换行\n,而CStdioFile在某些模式下按\r\n识别,导致ReadString读不到换行。
解决:先用二进制方式读前 1024 字节,统计\r\n和\n的数量,判断换行类型。如果是纯\n,就用CFile二进制读,手动按\n切分。或者统一用二进制读整个文件,再自己按\n和\r\n切,这样最稳。
4.3 大文件一次性读入内存导致卡顿
现象:打开几十 MB 的 Gerber 文件,程序卡死几秒甚至内存暴涨。
原因:ReadLines把全部行存进 vector,CListCtrl 又全部插入,双重压力。
解决:分页读,或者设一个最大行数限制。常见做法是读文件时只保留前 N 行(比如 10000 行),界面显示这些,状态栏提示“仅显示前 10000 行”。如果确实要看全部,再改虚拟列表按需取数据。
4.4 特殊字符显示成乱码
现象:Gerber 文件里有非 ASCII 字符(比如注释里的中文),显示成问号或方块。
原因:文件编码和程序读取编码不一致。MFC 默认按当前代码页解释,如果文件是 UTF-8 而程序按 GBK 读,就会乱码。
解决:读文件时先检测 BOM。UTF-8 BOM 是EF BB BF,有 BOM 就按 UTF-8 转成 CString 再显示。没有 BOM 的 UTF-8 文件比较难自动判断,可以提供一个编码选择下拉框,让用户手动选 UTF-8 或 GBK。
4.5 列表插入时界面闪烁严重
现象:插入几千行时,列表不停闪,体验很差。
原因:每次InsertItem都触发重绘。
解决:插入前SetRedraw(FALSE),插入后SetRedraw(TRUE)再Invalidate。如果还闪,可以用LockWindowUpdate锁住窗口,但记得解锁。另外,把 CListCtrl 的LVS_EX_DOUBLEBUFFER扩展样式打开,也能减少闪烁。
5. 二次开发与改造:从分行显示到结构化解析
5.1 把显示结果导出成 CSV
分行显示只是第一步,很多时候需要把解析结果给别的工具用。最直接的做法是加一个“导出 CSV”按钮,把行号、类型、参数写到文件里。
void CGerberReaderDlg::OnBnClickedBtnExport() { CFileDialog dlg(FALSE, _T("csv"), _T("gerber_lines.csv"), OFN_OVERWRITEPROMPT, _T("CSV Files (*.csv)|*.csv||")); if (dlg.DoModal() != IDOK) return; CStdioFile out; if (!out.Open(dlg.GetPathName(), CFile::modeCreate | CFile::modeWrite | CFile::typeText)) { AfxMessageBox(_T("导出文件创建失败。")); return; } // 写表头 out.WriteString(_T("行号,类型,参数\n")); int nCount = m_listLines.GetItemCount(); for (int i = 0; i < nCount; ++i) { CString strNo = m_listLines.GetItemText(i, 0); CString strContent = m_listLines.GetItemText(i, 1); GerberLine line = CLineParser::Parse(strContent); CString strParams; for (size_t j = 0; j < line.vecParams.size(); ++j) { if (j > 0) strParams += _T("|"); strParams += line.vecParams[j]; } CString strOut; strOut.Format(_T("%s,%s,%s\n"), (LPCTSTR)strNo, (LPCTSTR)line.strType, (LPCTSTR)strParams); out.WriteString(strOut); } out.Close(); AfxMessageBox(_T("导出完成。")); }逻辑说明:遍历列表每一项,重新解析内容,把类型和参数用竖线拼起来,写成 CSV。参数里的逗号可能和 CSV 分隔符冲突,所以用竖线代替。如果参数里本身有竖线,可以再换成其他不常见符号,或者做转义。
参数说明:CFileDialog的FALSE表示保存对话框,OFN_OVERWRITEPROMPT覆盖前提示。WriteString写文本行,注意\n在文本模式下会被转成\r\n。
5.2 加一个简单的语法高亮
CListCtrl 本身不支持富文本,但可以换用CRichEditCtrl或者自绘列表。更简单的做法是:在列表里加一列“类型”,根据类型给不同颜色。CListCtrl 支持SetItemText后设置文字颜色,但需要处理NM_CUSTOMDRAW消息。
常见做法是响应NM_CUSTOMDRAW,在CDDS_ITEMPREPAINT阶段根据行内容设置clrText。比如G04注释行用灰色,D码行用蓝色,坐标行用黑色。这样一眼就能区分指令类型,调试时效率高很多。
5.3 从“分行显示”到“按功能分组”
分行显示是线性的,但 Gerber 数据其实有结构:孔径定义、绘图指令、结束符。二次开发时,可以在解析完所有行后,按类型分组统计。比如统计有多少个D码、多少个坐标点、注释有多少行。这些统计信息可以显示在状态栏或单独的面板里。
我一般会加一个Analyze函数,遍历所有行,用 map 统计类型出现次数。这个函数不复杂,但能让工具从“看文本”升级到“看数据分布”,对后续写解析器很有帮助。
5.4 一个容易忽略的验证方法
改完解析逻辑后,怎么验证对不对?最直接的办法是拿一个已知正确的 Gerber 文件,用专业软件(比如开源的 Gerber 查看器)打开,记下总行数、D 码数量、坐标范围,然后和你的工具输出对比。如果行数对不上,多半是换行符或空行处理有问题;如果 D 码数量对不上,多半是解析时把某些行漏了或重复了。
我自己的习惯是:每次改完ReadLines或Parse,先拿三个不同来源的 Gerber 文件跑一遍,一个 Windows 换行、一个 Unix 换行、一个带中文注释。三个都过了,才认为这次改动是稳的。从那以后我每次动文件读取相关的代码,都强制走一遍这个三文件验证,省得后面在界面层查半天查不出问题。希望帮到你。
本文还有配套的精品资源,点击获取