
简介这份资源面向使用 Visual Studio 与 MFC 进行桌面开发的 C 学习者以及需要查看 PCB 设计文件的硬件工程师解决 Gerber 文件内容难以直观浏览与逐行核对的问题。资源包共 25 个文件约 150KB以 h 头文件与 cpp 源文件为主辅以 vcxproj、sln 工程配置、rc 资源脚本、ico 图标及 ReadMe 说明构成一个可直接编译运行的 MFC 工程。项目围绕 Gerber 格式解析展开涉及 RS-274X 指令集、坐标与单位处理并通过 CListCtrl 将解析结果逐行呈现同时借助 CFileDialog 完成文件选择与读取流程。代码中划分了文件读取、字符串处理、页面显示等模块便于读者理解 MFC 消息机制、控件重载与异常处理思路。目前已有 801 人学习适合希望提升 C 工程实践与 PCB 数据解析能力的开发者参考。1. 从一块 Gerber 文件说起为什么 MFC 里读它总翻车如果你在 PCB 行业待过大概率遇到过这种需求客户丢过来一个.gbr文件让你在 Windows 桌面端把它读出来按行显示在界面上最好还能高亮坐标和 D 码。听起来简单但真动手写你会发现 Gerber 根本不是「文本文件」那么简单——它是一套有状态机的矢量图形描述格式RS-274X 标准里藏着光圈定义、坐标格式省略、模态指令这些坑。用 VS 建一个 MFC 工程VC 环境下用 CStdioFile 逐行读十有八九会在%ADD或G54D10这类行上卡住。这篇笔记拆的就是一个「读取 Gerber 文件并分行显示」的 MFC 小工具适合刚接触 CAM 前端、需要快速做文件预览的 VC 开发者也适合想搞懂 Gerber 解析边界的老手。它不追求渲染出完整 PCB 图形只解决一件事把文件内容可靠地拆行、解析、显示出来为后续的图形化或数据提取打底。2. Gerber 分行读取的底层逻辑RS-274X 状态机与 MFC 文件流2.1 为什么不能直接按\n切Gerber 文件在 RS-274X 规范里行结束符理论上可以是LF、CR、CRLF甚至有些老设备导出时混用。更麻烦的是一条完整的「命令」不一定只占一行——比如坐标数据可以跨行而%包裹的扩展指令如%FSLAX46Y46*%内部用*分隔末尾才用%收尾。如果你用CStdioFile::ReadString按行读遇到%块跨行时就会把一条指令拆成两半显示出来全是碎片。常见做法是先把整个文件按二进制读进CByteArray或std::string然后自己写一个分词器按*和%作为分隔符切分而不是按换行符。这样得到的「逻辑行」才是 Gerber 解析器真正要处理的最小单元。分行显示时再把逻辑行按原始换行位置还原成物理行或者直接按逻辑行显示——取决于你是想「看原始文件」还是「看解析结果」。2.2 MFC 里读文件的三种姿势与选型在 VC 环境下MFC 提供了CFile、CStdioFile、CArchive几条路。CStdioFile适合文本行读写但遇到二进制或混合编码就力不从心CFile最底层读写字节流最稳CArchive通常配合序列化读 Gerber 这种非结构化文本反而绕。我一般会这样选用CFile以CFile::modeRead | CFile::typeBinary打开一次性读入ULONGLONG长度然后转成std::string做后续分词。这样既避开了 MFC 文本模式对\r\n的自动转换也方便处理文件里可能出现的非 ASCII 字节比如某些注释里的中文。// 用 CFile 二进制读取整个 Gerber 文件 CFile file; if (!file.Open(strPath, CFile::modeRead | CFile::typeBinary)) { AfxMessageBox(_T(文件打开失败)); return; } ULONGLONG len file.GetLength(); std::string buffer; buffer.resize(static_castsize_t(len)); file.Read(buffer[0], static_castUINT(len)); file.Close(); // 按 * 和 % 切分逻辑行保留原始换行信息 std::vectorstd::string logicalLines; std::string current; for (char ch : buffer) { if (ch * || ch %) { current.push_back(ch); logicalLines.push_back(current); current.clear(); } else if (ch \r || ch \n) { // 换行符单独记录用于还原物理行 if (!current.empty()) { logicalLines.push_back(current); current.clear(); } logicalLines.push_back(std::string(1, ch)); } else { current.push_back(ch); } } if (!current.empty()) logicalLines.push_back(current);这段代码的关键点*和%都作为逻辑行结束符换行符单独存成一个元素这样后续既可以按逻辑行显示也可以按物理行还原。参数上CFile::typeBinary必须加否则 MFC 会在读到0x1A时提前截断。buffer.resize之后直接Read到buffer[0]是 C11 起保证连续存储的写法比GetBuffer更干净。2.3 分行显示时怎么处理坐标省略和模态指令Gerber 的坐标格式由%FSLAX46Y46*%这类指令定义X46表示整数 4 位、小数 6 位但实际数据里经常省略前导零。比如X1000Y2000D01*在 4.6 格式下表示X0.001000、Y0.002000。如果你只是分行显示原始文本那没问题但如果要在显示时顺便解析出坐标值就必须先读FS指令再按格式补零。另一个坑是模态指令D01、D02、D03一旦出现后续坐标行如果省略 D 码就沿用上一个。分行显示时我建议在每行前面加一个行号并在解析结果里标注「继承的 D 码」这样调试时一眼能看出状态机有没有跑偏。// 简单的坐标解析示例假设已读到 FS 指令格式为 4.6 struct GerberState { int intDigits 4; int decDigits 6; int lastDCode 0; double lastX 0.0, lastY 0.0; }; // 解析一行坐标数据简化版 bool ParseCoordLine(const std::string line, GerberState state, CString display) { // 查找 X 和 Y size_t xPos line.find(X); size_t yPos line.find(Y); size_t dPos line.find(D); double x state.lastX, y state.lastY; int dCode state.lastDCode; auto parseNum [](size_t pos, int digits) - double { if (pos std::string::npos) return 0.0; size_t end line.find_first_of(XYD*, pos 1); std::string numStr line.substr(pos 1, end - pos - 1); // 补零到总位数 while (numStr.length() static_castsize_t(digits)) numStr 0 numStr; double val atof(numStr.c_str()); return val / pow(10, state.decDigits); }; if (xPos ! std::string::npos) x parseNum(xPos, state.intDigits state.decDigits); if (yPos ! std::string::npos) y parseNum(yPos, state.intDigits state.decDigits); if (dPos ! std::string::npos) { dCode atoi(line.substr(dPos 1, 2).c_str()); state.lastDCode dCode; } state.lastX x; state.lastY y; display.Format(_T(X%.6f Y%.6f D%02d), x, y, dCode); return true; }这里parseNum里的补零逻辑是核心Gerber 允许省略前导零也允许省略后导零具体看FS指令里的L还是T。上面按「省略前导零」处理实际项目里要根据FSLA还是FSTA切换。pow(10, decDigits)那一步如果decDigits是变量记得用pow而不是整数除法否则精度直接崩。3. 在 VS MFC 里搭一个可用的 Gerber 分行查看器3.1 工程创建与界面控件布局在 VS 里新建一个 MFC 应用选「基于对话框」还是「单文档」看需求。如果只是分行显示对话框足够拖一个CListCtrl做行显示一个CButton做打开文件一个CEdit做过滤关键字。CListCtrl设成Report风格加两列行号和内容。行号用InsertItem的nItem参数自动生成内容用SetItemText填。注意CListCtrl默认只显示第一列要在OnInitDialog里调SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES)否则选中整行时只有第一列高亮看起来像半成品。列宽用SetColumnWidth设内容列给个 800 像素行号列 60 像素足够。3.2 文件读取与分行填充的完整流程把 2.2 的读取逻辑封装成一个LoadGerberFile函数在按钮响应里调用。填充CListCtrl时如果文件超过几千行直接InsertItem会卡 UI。常见做法是先用SetRedraw(FALSE)关重绘填完再SetRedraw(TRUE)Invalidate。更稳的是用虚拟列表LVS_OWNERDATA但那个要处理LVN_GETDISPINFO代码量翻倍。对于 Gerber 这种通常几万行以内的文件关重绘就够了。void CGerberViewDlg::LoadGerberFile(const CString strPath) { // 读取文件复用 2.2 的 CFile 逻辑 std::vectorstd::string lines ReadGerberLines(strPath); m_listCtrl.SetRedraw(FALSE); m_listCtrl.DeleteAllItems(); int nItem 0; for (const auto line : lines) { // 跳过纯换行符或者按需显示 if (line \r || line \n) continue; CString strLine(line.c_str()); m_listCtrl.InsertItem(nItem, _T()); // 行号列稍后填 m_listCtrl.SetItemText(nItem, 0, CString(std::to_wstring(nItem 1).c_str())); m_listCtrl.SetItemText(nItem, 1, strLine); nItem; } m_listCtrl.SetRedraw(TRUE); m_listCtrl.Invalidate(); }ReadGerberLines里把 2.2 的分词逻辑包一层返回std::vectorstd::string。注意CString构造时如果line里有非 ASCII 字节直接CString(line.c_str())在 Unicode 工程下会乱码。稳妥做法是用CA2T或MultiByteToWideChar显式转换编码按CP_ACP或CP_UTF8试。Gerber 文件一般是 ASCII但注释里偶尔有中文这一步不能省。3.3 关键字过滤与高亮显示分行显示之后用户往往想快速定位%ADD或某个 D 码。在CEdit里输入关键字遍历CListCtrl的每一行匹配到的用SetItemText改颜色——但CListCtrl原生不支持单行变色得用NM_CUSTOMDRAW消息。在OnCustomDraw里判断lplvcd-nmcd.dwItemSpec对应的行是否包含关键字是就改clrText和clrTextBk。// 在对话框类里加 ON_NOTIFY(NM_CUSTOMDRAW, IDC_LIST1, OnCustomDraw) void CGerberViewDlg::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) { LPNMLVCUSTOMDRAW lplvcd reinterpret_castLPNMLVCUSTOMDRAW(pNMHDR); *pResult CDRF_DODEFAULT; if (lplvcd-nmcd.dwDrawStage CDDS_PREPAINT) { *pResult CDRF_NOTIFYITEMDRAW; } else if (lplvcd-nmcd.dwDrawStage CDDS_ITEMPREPAINT) { int nItem static_castint(lplvcd-nmcd.dwItemSpec); CString strText m_listCtrl.GetItemText(nItem, 1); if (!m_strFilter.IsEmpty() strText.Find(m_strFilter) ! -1) { lplvcd-clrTextBk RGB(255, 255, 0); // 黄色高亮 } *pResult CDRF_DODEFAULT; } }m_strFilter在CEdit的EN_CHANGE里更新然后m_listCtrl.Invalidate()触发重绘。注意NM_CUSTOMDRAW里不要做耗时操作否则滚动会卡。如果关键字很多建议先在LoadGerberFile时把匹配行号存进std::setOnCustomDraw里只查表别每次Find。4. 避坑与排查Gerber 分行显示里最容易翻车的五件事4.1 现象读出来的行数比实际少一半原因CStdioFile::ReadString在文本模式下遇到0x1ACtrlZ会当 EOF 处理而某些 Gerber 文件里恰好有这个字节。解决改用CFile二进制模式读或者CStdioFile打开时加CFile::typeBinary。已经用ReadString的检查文件里有没有0x1A有就换方案。4.2 现象%块被拆成多行显示错乱原因按\n分行但%FSLAX46Y46*%这类指令内部没有换行末尾的%和下一行之间可能有\r\n导致%单独成行。解决按 2.2 的逻辑用*和%作为逻辑行分隔符换行符只作为显示时的参考。如果必须按物理行显示在解析时把%块合并显示时再按原始位置拆开。4.3 现象坐标解析出来全是 0 或者数量级不对原因FS指令里的L和T没区分。FSLA表示省略前导零FSTA表示省略后导零。代码里统一按补前导零处理遇到FSTA的文件就全错。解决在读FS指令时记下L还是T解析坐标时按对应规则补零。FSLAX46Y46*%里L后面的A是绝对坐标I是增量坐标也要区分。4.4 现象CListCtrl插入几万行后界面卡死原因每次InsertItem都触发重绘和消息循环行数一多就指数级变慢。解决SetRedraw(FALSE)包住整个填充过程填完再SetRedraw(TRUE)Invalidate。如果还卡改用虚拟列表LVS_OWNERDATA只在实际绘制时提供数据。4.5 现象中文注释显示成乱码原因Gerber 文件编码可能是 GBK 或 UTF-8而 MFC 工程默认 UnicodeCString构造时按CP_ACP解释遇到 UTF-8 就乱。解决读文件后先检测 BOM有 UTF-8 BOM 就按 UTF-8 转宽字符没有 BOM 的用MultiByteToWideChar分别试CP_UTF8和CP_ACP看哪个能解出合法字符。稳妥点就在界面加个编码下拉框让用户手动选。5. 进阶把分行结果导出成结构化 CSV 并做坐标校验分行显示只是第一步真正干活时往往要把解析结果导出去给别的工具用。我一般会在LoadGerberFile之后加一个「导出 CSV」按钮把每行的行号、原始内容、解析出的 X/Y/D 码写进文件。这样既方便用 Excel 核对也能拿 Python 做二次校验。void CGerberViewDlg::ExportToCsv(const CString strPath) { CStdioFile file; if (!file.Open(strPath, CFile::modeCreate | CFile::modeWrite | CFile::typeText)) { AfxMessageBox(_T(导出失败)); return; } file.WriteString(_T(行号,原始内容,X,Y,D码\n)); GerberState state; int nCount m_listCtrl.GetItemCount(); for (int i 0; i nCount; i) { CString strLine m_listCtrl.GetItemText(i, 1); CString strDisplay; // 这里调用 2.3 的 ParseCoordLine注意传 CT2A 转 std::string CT2A asciiLine(strLine); ParseCoordLine(std::string(asciiLine), state, strDisplay); CString strOut; strOut.Format(_T(%d,\%s\,%s\n), i 1, strLine, strDisplay); file.WriteString(strOut); } file.Close(); }导出之后用 Python 的pandas读 CSV检查 X/Y 是否在板框范围内D 码是否都在%ADD里定义过。这一步能抓出很多解析阶段的隐性错误——比如某个D码没定义但分行显示时看不出来一校验就露馅。import pandas as pd df pd.read_csv(gerber_export.csv) # 检查 D 码是否都在已知集合里 known_dcodes {10, 11, 12} # 从 %ADD 指令里提取 unknown df[~df[D码].isin(known_dcodes)] if not unknown.empty: print(发现未定义 D 码) print(unknown[[行号, 原始内容, D码]])从那以后我每次写完 Gerber 解析模块都强制走一遍「导出 CSV → Python 校验」的流程比在 MFC 里肉眼盯强太多。希望帮到你。本文还有配套的精品资源点击获取