简介:这份资源面向使用 Visual Studio 与 MFC 进行桌面开发的 C++ 学习者,聚焦 Gerber 文件解析与界面展示这一典型 PCB 场景。Gerber 是印制电路板制造中常用的图形描述格式,程序通过解析其指令与坐标数据,将内容逐行呈现在 List 控件中,便于设计人员检查与验证。资源包共 25 个文件,以 h 头文件与 cpp 源文件为主,另含 vcxproj、sln 等工程配置及 rc 资源脚本、ico 图标与 ReadMe 说明,压缩包约 150KB,结构完整可直接编译运行。目前已有 801 人学习下载。读者可从中掌握 Gerber 格式解析思路、CListCtrl 数据填充方式、CFileDialog 文件选择流程以及 MFC 消息处理与异常保护写法,是提升 C++ 与 PCB 领域实践能力的一份参考范例。
1. 从一堆 Gerber 文件里把线路“读”出来:这个 MFC 小工具到底能干什么
如果你在 PCB 行业待过,一定遇到过这种场景:板厂或者客户甩过来一个压缩包,里面躺着几十个 Gerber 文件,后缀五花八门——.gbr、.art、.ger、.gtl、.gbl,还有钻孔文件.drl。你想快速确认某一层到底画了什么,用 CAM 软件打开吧,启动慢、操作重;用文本编辑器直接看吧,满屏都是G04、%FSLAX46Y46*%、D10*这种指令,眼睛都要看花。这个项目就是冲着这个痛点来的:用 VS + MFC + VC 写一个轻量级 Gerber 文件读取器,把文件内容按行解析出来,分行显示在界面上,让你不用开重型软件就能快速浏览线路数据。
它适合两类人:一类是刚接触 PCB 制造、想搞懂 Gerber 格式到底长什么样的新手,拿它当学习工具,一行行看指令怎么组织;另一类是做 CAM 工具链、需要在自己的 MFC 工程里嵌入 Gerber 解析能力的开发者,可以直接把核心解析逻辑抠出来复用。整个项目基于 MFC 对话框框架,用 C++ 写,编译出来就是一个 exe,双击就能跑,不依赖任何第三方库。下面我从工程结构、核心解析代码、界面交互到二次开发,把这个项目拆开讲一遍。
2. 工程结构与技术选型:为什么用 MFC 而不是 Qt
2.1 项目目录长什么样
拿到源码后,第一件事是看目录结构。这个项目是典型的 VS MFC 对话框工程,目录大致如下:
GerberReader/ ├── GerberReader.sln # VS 解决方案文件 ├── GerberReader/ │ ├── GerberReader.vcxproj # 工程文件 │ ├── GerberReader.h # 主对话框类声明 │ ├── GerberReader.cpp # 主对话框类实现 │ ├── GerberReaderDlg.h # 对话框头文件 │ ├── GerberReaderDlg.cpp # 对话框实现,核心逻辑在这里 │ ├── GerberParser.h # Gerber 解析类声明 │ ├── GerberParser.cpp # Gerber 解析类实现 │ ├── resource.h # 资源 ID 定义 │ ├── GerberReader.rc # 资源脚本,包含对话框布局 │ └── res/ # 图标等资源 └── README.md核心就三个文件:GerberReaderDlg.cpp负责界面和文件读取,GerberParser.cpp负责把文件内容拆成一行行可读的指令,resource.h管控件 ID。没有多余的依赖,干净利落。
2.2 为什么选 MFC 而不是 Qt
这个问题我被问过很多次。Qt 跨平台、界面漂亮,但在这个场景下 MFC 反而更合适。原因有三:第一,目标用户是 Windows 桌面端的 PCB 工程师,MFC 编译出来的 exe 体积小、启动快,不需要带一堆 Qt DLL;第二,很多老牌 CAM 软件本身就是 MFC 写的,如果要做插件或者嵌入现有系统,MFC 的兼容性更好;第三,这个工具的功能很单一——读文件、分行显示,用 MFC 的CListBox或者CRichEditCtrl就能搞定,没必要上 Qt 那套信号槽。
当然,MFC 的坑也不少,后面避坑章节会细说。这里先记住一点:如果你要二次开发,建议保持 MFC 框架不动,只改解析逻辑和显示控件,这样最省事。
2.3 对话框布局与控件说明
打开GerberReader.rc,能看到主对话框上摆了这几个控件:
| 控件 ID | 类型 | 作用 |
|---|---|---|
| IDC_BTN_OPEN | Button | 触发文件选择对话框 |
| IDC_EDIT_PATH | Edit Box | 显示当前打开的文件路径 |
| IDC_LIST_LINES | ListBox | 分行显示 Gerber 文件内容 |
| IDC_STATIC_COUNT | Static Text | 显示总行数 |
| IDC_BTN_CLEAR | Button | 清空列表 |
布局很简单,左边一个按钮加路径框,下面一个大 ListBox 占满剩余空间。这种布局的好处是窗口可以自由拉伸,ListBox 会自动调整大小。如果你要加功能,比如搜索、过滤、高亮,可以在右侧或者底部再加控件。
3. 核心解析逻辑:Gerber 文件到底怎么读
3.1 Gerber 文件的基本结构
在写代码之前,得先搞清楚 Gerber 文件长什么样。Gerber 格式是 PCB 行业的标准光绘格式,本质上是一系列指令的集合。一个典型的 Gerber 文件开头是这样的:
G04 This is a comment* %FSLAX46Y46*% %MOMM*% %ADD10C,0.150000*% D10* X100000Y100000D02* X150000Y100000D01* M02*每一行以*结尾表示指令结束,%包裹的是扩展指令,G04是注释,D01是画线,D02是移动,D03是闪绘。我们的目标不是解析这些指令的几何含义,而是先把它们按行拆出来,让用户能看到原始内容。所以解析逻辑的核心就是:按行读取、去掉行尾的*、识别指令类型、分行显示。
3.2 文件读取与分行处理
先看GerberParser.cpp里的读取函数:
// GerberParser.cpp #include "pch.h" #include "GerberParser.h" #include <fstream> #include <sstream> // 读取 Gerber 文件,返回按行分割的字符串数组 std::vector<CString> CGerberParser::ReadGerberFile(const CString& filePath) { std::vector<CString> lines; std::ifstream file(filePath, std::ios::in | std::ios::binary); if (!file.is_open()) { return lines; // 文件打不开,返回空数组 } std::string rawLine; while (std::getline(file, rawLine)) { // 去掉行尾的 \r(Windows 换行符) if (!rawLine.empty() && rawLine.back() == '\r') { rawLine.pop_back(); } // 跳过空行 if (rawLine.empty()) { continue; } // 转成 CString 方便 MFC 显示 CString line(rawLine.c_str()); lines.push_back(line); } file.close(); return lines; }这段代码有几个关键点。第一,用std::ifstream以二进制模式打开文件,避免 Windows 下文本模式对换行符的自动转换干扰。第二,std::getline按\n分割,但 Windows 文件行尾是\r\n,所以手动去掉\r。第三,空行直接跳过,因为 Gerber 文件里空行没有意义,显示出来反而占地方。第四,返回std::vector<CString>而不是std::vector<std::string>,是为了后续直接往 MFC 控件里塞,省一次转换。
参数说明:filePath是文件的完整路径,支持中文路径(因为用了std::ifstream的宽字符版本需要额外处理,这里简化了,实际项目中建议用_wfopen或者CFile)。返回值是行内容的数组,如果文件打不开就返回空数组,调用方需要判断。
3.3 指令分类与格式化显示
光把行读出来还不够,Gerber 文件里有些行是注释,有些是参数设置,有些是坐标数据。为了显示更清晰,可以给每行加个前缀标记:
// 根据行内容判断指令类型,返回带标记的显示字符串 CString CGerberParser::FormatLine(const CString& rawLine) { CString formatted; CString trimmed = rawLine; trimmed.Trim(); // 去掉首尾空格 if (trimmed.IsEmpty()) { return _T(""); } // G04 开头的是注释 if (trimmed.Left(3) == _T("G04")) { formatted.Format(_T("[注释] %s"), trimmed.GetString()); } // % 开头的是扩展指令 else if (trimmed[0] == _T('%')) { formatted.Format(_T("[参数] %s"), trimmed.GetString()); } // D 开头的是绘图指令 else if (trimmed[0] == _T('D') && trimmed.GetLength() > 1 && _istdigit(trimmed[1])) { formatted.Format(_T("[绘图] %s"), trimmed.GetString()); } // X/Y 开头的是坐标数据 else if (trimmed[0] == _T('X') || trimmed[0] == _T('Y')) { formatted.Format(_T("[坐标] %s"), trimmed.GetString()); } // 其他归为未知指令 else { formatted.Format(_T("[其他] %s"), trimmed.GetString()); } return formatted; }这段代码的逻辑很直白:根据行首字符判断指令类型,加上[注释]、[参数]、[绘图]、[坐标]这样的前缀。这样用户在 ListBox 里一眼就能看出哪行是注释、哪行是实际数据。Trim()去掉首尾空格,避免因为缩进导致判断失误。_istdigit是 MFC 提供的字符判断函数,比手动比较'0'到'9'更简洁。
注意:Gerber 规范里指令类型远不止这几种,比如M02是文件结束,G01是线性插值模式。这里只做了最粗的分类,目的是让显示更友好,不是做完整的语法解析。如果你要二次开发做真正的 CAM 处理,需要引入更完整的解析器。
3.4 把解析结果塞进 ListBox
解析完了,接下来是显示。在GerberReaderDlg.cpp里,打开文件按钮的响应函数大概是这样:
// GerberReaderDlg.cpp void CGerberReaderDlg::OnBnClickedBtnOpen() { CFileDialog dlg(TRUE, _T("gbr"), NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, _T("Gerber Files (*.gbr;*.art;*.ger)|*.gbr;*.art;*.ger|All Files (*.*)|*.*||"), this); if (dlg.DoModal() != IDOK) { return; } CString filePath = dlg.GetPathName(); SetDlgItemText(IDC_EDIT_PATH, filePath); // 清空旧数据 CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LINES); pList->ResetContent(); // 解析文件 CGerberParser parser; std::vector<CString> lines = parser.ReadGerberFile(filePath); // 逐行格式化并添加到 ListBox for (size_t i = 0; i < lines.size(); ++i) { CString formatted = parser.FormatLine(lines[i]); if (!formatted.IsEmpty()) { pList->AddString(formatted); } } // 更新总行数显示 CString countText; countText.Format(_T("总行数: %d"), (int)lines.size()); SetDlgItemText(IDC_STATIC_COUNT, countText); }这段代码的流程是:弹出文件选择对话框,拿到路径后清空 ListBox,调用解析器读文件,逐行格式化后AddString到列表里,最后更新行数统计。CFileDialog的过滤器设置成只显示 Gerber 常见后缀,避免用户选错文件。ResetContent清空列表比逐条删除效率高。
参数说明:CFileDialog第一个参数TRUE表示打开文件对话框,第二个参数是默认扩展名,第三个是初始文件名,第四个是标志位,第五个是过滤器字符串。GetPathName返回用户选择的完整路径。AddString每次添加一行,如果行数特别多(比如几万行),可以考虑用SetRedraw(FALSE)暂停重绘,添加完再SetRedraw(TRUE)和Invalidate(),避免界面卡顿。
4. 避坑与常见问题:那些年我踩过的 MFC 坑
4.1 中文路径读不出来
现象:文件路径里有中文,std::ifstream打开失败,返回空数组。
原因:std::ifstream的构造函数接受const char*,在 Windows 下默认用 ANSI 编码,中文路径会乱码。
解决:改用CFile或者_wfopen,或者把CString转成std::wstring再用宽字符版本的std::ifstream。我一般直接用 MFC 的CFile,省心:
CFile file; if (!file.Open(filePath, CFile::modeRead)) { return lines; }4.2 ListBox 添加大量数据时界面卡死
现象:打开一个几万行的 Gerber 文件,界面直接无响应,要等好几秒才恢复。
原因:每次AddString都会触发一次重绘,几万次重绘累积起来非常慢。
解决:添加前pList->SetRedraw(FALSE),添加完pList->SetRedraw(TRUE)再pList->Invalidate()。如果数据量特别大,建议改用CListCtrl的虚拟列表模式,只渲染可见区域。
4.3 换行符处理不干净导致多出空行
现象:ListBox 里每隔一行就有一个空行。
原因:Gerber 文件在 Windows 下是\r\n,std::getline按\n分割后每行末尾还留着\r,显示时\r被当成回车,导致看起来像空行。
解决:读取后手动去掉\r,就是前面代码里rawLine.pop_back()那一步。如果文件来自 Linux 系统,只有\n,这步也不会出错。
4.4 文件编码问题导致乱码
现象:Gerber 文件里有中文注释,显示出来是乱码。
原因:Gerber 文件可能是 UTF-8 编码,而 MFC 默认用 ANSI 显示。
解决:读取时检测 BOM,如果是 UTF-8 就转成CString的 Unicode 版本。简单做法是用MultiByteToWideChar转换:
int len = MultiByteToWideChar(CP_UTF8, 0, rawLine.c_str(), -1, NULL, 0); wchar_t* buf = new wchar_t[len]; MultiByteToWideChar(CP_UTF8, 0, rawLine.c_str(), -1, buf, len); CString line(buf); delete[] buf;4.5 对话框拉伸后控件不跟着变
现象:拉大窗口,ListBox 还是原来大小,右边留一大片空白。
原因:MFC 对话框默认不处理WM_SIZE消息,控件位置固定。
解决:在对话框类里重写OnSize,手动计算控件新位置:
void CGerberReaderDlg::OnSize(UINT nType, int cx, int cy) { CDialogEx::OnSize(nType, cx, cy); if (m_listLines.GetSafeHwnd()) { m_listLines.MoveWindow(10, 50, cx - 20, cy - 60); } }注意要在OnInitDialog里把控件绑定到成员变量,否则GetSafeHwnd返回空。
5. 二次开发与改造思路:从“能看”到“能用”
5.1 加一个搜索框,快速定位指令
现在只能一行行翻,文件大了很痛苦。加个搜索框是最实用的改造。在对话框上放一个Edit Box和一个Button,点击按钮时遍历 ListBox 内容,找到匹配的行并选中:
void CGerberReaderDlg::OnBnClickedBtnSearch() { CString keyword; GetDlgItemText(IDC_EDIT_SEARCH, keyword); if (keyword.IsEmpty()) return; CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LINES); int count = pList->GetCount(); for (int i = 0; i < count; ++i) { CString text; pList->GetText(i, text); if (text.Find(keyword) != -1) { pList->SetCurSel(i); break; } } }这个逻辑是找到第一个匹配就停,如果要找下一个,可以记录上次位置继续往下搜。Find是CString的成员函数,返回子串位置,找不到返回 -1。
5.2 把解析结果导出成 CSV
有时候需要把 Gerber 内容导出来做分析。加一个导出按钮,把 ListBox 内容写成 CSV 文件:
void CGerberReaderDlg::OnBnClickedBtnExport() { CFileDialog dlg(FALSE, _T("csv"), _T("output.csv"), OFN_OVERWRITEPROMPT, _T("CSV Files (*.csv)|*.csv||"), this); if (dlg.DoModal() != IDOK) return; CStdioFile file; if (!file.Open(dlg.GetPathName(), CFile::modeCreate | CFile::modeWrite)) { return; } CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LINES); int count = pList->GetCount(); for (int i = 0; i < count; ++i) { CString text; pList->GetText(i, text); text.Replace(_T(","), _T(";")); // 避免逗号冲突 file.WriteString(text + _T("\n")); } file.Close(); }CStdioFile是 MFC 封装的文件类,WriteString直接写一行。注意把内容里的逗号替换掉,否则 CSV 列会错位。
5.3 用 CListCtrl 替代 ListBox 支持多列
ListBox 只能显示一列,如果想同时显示行号、指令类型、原始内容,就得换CListCtrl。改造步骤:把资源里的 ListBox 换成 ListCtrl,在OnInitDialog里插入三列:
m_listCtrl.InsertColumn(0, _T("行号"), LVCFMT_LEFT, 60); m_listCtrl.InsertColumn(1, _T("类型"), LVCFMT_LEFT, 80); m_listCtrl.InsertColumn(2, _T("内容"), LVCFMT_LEFT, 600);然后添加数据时用InsertItem和SetItemText。这样显示更结构化,也方便排序和过滤。
5.4 验证改造是否成功
改完之后怎么确认没改坏?我的习惯是准备三个测试文件:一个只有几行的最小 Gerber 文件,一个几千行的正常文件,一个包含中文注释和特殊字符的边界文件。每次改完都跑一遍这三个文件,看行数对不对、显示有没有乱码、搜索能不能命中。特别是中文路径和 UTF-8 编码这两个点,最容易翻车。
从那以后我每次改 MFC 的文件读取逻辑,都强制走一遍“中文路径 + 大文件 + 特殊字符”这三件套,省得后面返工。希望帮到你。
本文还有配套的精品资源,点击获取