
简介这份资源面向使用 MFC 进行桌面开发的程序员聚焦于将 CListCtrl 控件中的列表数据导出到 Excel 表格这一常见需求。内容围绕 COM 自动化接口展开涵盖 Excel 应用程序对象创建、工作簿与工作表操作、逐行逐列读取 ListCtrl 单元格数据并写入 Excel Range以及保存为 xlsx 格式和释放 COM 对象的完整流程适合具备一定 C 与 MFC 基础、需要实现数据导出的开发者参考。压缩包共 24 个文件约 156KB以 h 头文件与 cpp 源文件为主包含 CApplication、CWorkbook、CWorksheet、CRange 等 Excel 封装类另有 vcxproj、sln 工程文件及 rc 资源文件可直接用 Visual Studio 打开编译。目前已有 2392 人学习下载。通过阅读示例工程 testimportexcel 的源码读者可掌握 MFC 与 Excel 集成的关键写法理解数据格式转换与错误处理思路并据此扩展出符合自身项目需求的导出功能。1. 从 MFC 的 ListCtrl 到 Excel一个被低估的导出需求做过 MFC 桌面开发的人大概率都遇到过这个场景界面上一个 CListCtrl 跑得好好的几十上百行数据展示得清清楚楚结果产品经理或者测试同事过来说一句——“能不能加个导出 Excel 的按钮”听起来简单真动手才发现坑不少。MFC 本身没有提供任何 Excel 导出的原生接口ListCtrl 的数据又藏在控件内部你得先把数据取出来再想办法写到 Excel 能识别的格式里。这个需求在工业软件、设备调试工具、数据采集上位机里特别常见。操作员在界面上看到实时数据但最终要交给别人做报表、做分析Excel 就是最通用的交付格式。所以“MFC 下导出 ListCtrl 数据到 Excel”这件事本质上解决的是桌面程序数据落地的问题——让界面里的数据变成一份能发出去、能二次处理的文件。适合谁看如果你正在用 MFC 做工具类软件需要把列表数据导出成 Excel不管你是刚接触 MFC 的新手还是写过几年 C 的老手这篇内容都能帮你把路走通。我会从最稳的方案讲起把每一步的代码和参数说清楚再把我自己踩过的坑摊开给你看。2. 方案选型为什么我不推荐直接调 Excel COM2.1 三条路的技术对比在 MFC 里导出 Excel常见做法有三条路。第一条是走 OLE/COM 自动化直接创建 Excel.Application 对象通过 COM 接口往单元格里写数据。第二条是生成 CSV 文件用逗号分隔Excel 能直接打开。第三条是生成 Excel 原生支持的 XML 格式SpreadsheetML或者用第三方库写 xlsx。先给结论如果你的需求只是“把 ListCtrl 里的数据变成 Excel 能打开的文件”CSV 是最稳的选择。如果客户明确要求 .xlsx 后缀、要求多 Sheet、要求格式字体、颜色、合并单元格那才需要考虑 COM 或第三方库。COM 方案的问题在于它依赖目标机器上装了 Excel。没装 Excel 的机器上CoCreateInstance 直接失败。而且 COM 调用速度慢几百行数据可能还好上万行就会明显卡顿。更麻烦的是Excel 进程有时候不会正常退出任务管理器里残留一堆 EXCEL.EXE用户会以为你的程序有 bug。我见过一个项目因为 COM 导出没处理好释放逻辑每次导出都残留一个 Excel 进程跑一天下来机器内存被吃满。CSV 方案的优势是零依赖、速度快、代码量少。缺点是格式能力弱不能设字体颜色不能合并单元格中文编码需要处理一下后面会讲。但对于大多数“导出数据给同事看”的场景CSV 完全够用。SpreadsheetML 方案介于两者之间生成的是 XML 文件Excel 2003 以后都能打开支持一定的格式控制不依赖 Excel 安装。缺点是文件体积偏大而且新版 Excel 打开时会有格式警告。2.2 我的选型建议我一般会这样判断需求场景推荐方案理由纯数据导出无格式要求CSV零依赖代码简单速度快需要多 Sheet、格式控制SpreadsheetML不依赖 Excel格式能力够用客户明确要求 .xlsx第三方库如 libxlsxwriter生成真正的 xlsx不依赖 Excel内部工具机器都装了 OfficeCOM格式能力最强但要注意释放注意如果你的程序要分发给外部用户永远不要假设对方装了 Excel。CSV 或 SpreadsheetML 才是安全牌。2.3 CSV 方案的核心实现先看怎么从 CListCtrl 里把数据取出来。假设你的 ListCtrl 变量叫 m_list有若干列和若干行// 获取 ListCtrl 的行数和列数 int nRowCount m_list.GetItemCount(); int nColCount m_list.GetHeaderCtrl()-GetItemCount(); // 准备一个二维结构存数据 CStringArray arrRowData; for (int nRow 0; nRow nRowCount; nRow) { CString strLine; for (int nCol 0; nCol nColCount; nCol) { CString strText m_list.GetItemText(nRow, nCol); // CSV 转义如果内容里有逗号或引号需要用双引号包裹 if (strText.Find(_T(,)) ! -1 || strText.Find(_T()) ! -1) { strText.Replace(_T(\), _T(\\)); // 引号转义 strText _T(\) strText _T(\); } strLine strText; if (nCol nColCount - 1) strLine _T(,); } arrRowData.Add(strLine); }这段代码的逻辑很直接外层循环遍历行内层循环遍历列用 GetItemText 取出每个单元格的文本。关键在 CSV 转义那几行——如果单元格内容本身包含逗号不转义的话 Excel 打开时列就错位了。比如内容是“北京,上海”不处理的话会被当成两列。处理方式是用双引号把整个字段包起来字段内部的双引号替换成两个双引号。接下来把数据写到文件// 写 CSV 文件注意编码问题 CStdioFile file; // 用 UTF-8 编码打开避免中文乱码 if (file.Open(strFilePath, CFile::modeCreate | CFile::modeWrite | CFile::typeText)) { // 写 UTF-8 BOM让 Excel 正确识别编码 unsigned char bom[] { 0xEF, 0xBB, 0xBF }; file.Write(bom, 3); // 写表头 CString strHeader; for (int nCol 0; nCol nColCount; nCol) { CString strColName; // 获取列标题 LVCOLUMN lvc { 0 }; lvc.mask LVCF_TEXT; lvc.pszText strColName.GetBuffer(256); lvc.cchTextMax 256; m_list.GetColumn(nCol, lvc); strColName.ReleaseBuffer(); strHeader strColName; if (nCol nColCount - 1) strHeader _T(,); } file.WriteString(strHeader _T(\n)); // 写数据行 for (int i 0; i arrRowData.GetSize(); i) { file.WriteString(arrRowData[i] _T(\n)); } file.Close(); }这里有几个参数需要说明。CFile::modeCreate表示如果文件不存在就创建存在就覆盖。CFile::typeText表示文本模式会自动处理换行符。UTF-8 BOM 那三个字节很关键——没有它Excel 打开中文 CSV 会乱码这是血泪经验。BOM 是字节顺序标记Excel 靠它来判断文件编码。提示如果你的项目是 Unicode 字符集CString 内部就是宽字符写文件时需要转成 UTF-8。上面的代码假设你已经做了转换或者项目本身就是多字节字符集。Unicode 项目下建议用 WideCharToMultiByte 转成 UTF-8 再写。3. 用 SpreadsheetML 生成带格式的 Excel 文件3.1 SpreadsheetML 的结构长什么样CSV 搞不定格式的时候SpreadsheetML 是我的第二选择。它本质是一个 XML 文件Excel 2003 及以上版本都能打开。文件结构大致是这样?xml version1.0 encodingUTF-8? ?mso-application progidExcel.Sheet? Workbook xmlnsurn:schemas-microsoft-com:office:spreadsheet xmlns:ssurn:schemas-microsoft-com:office:spreadsheet Styles Style ss:IDheader Font ss:Bold1 ss:Color#FFFFFF/ Interior ss:Color#4472C4 ss:PatternSolid/ /Style /Styles Worksheet ss:Name数据表 Table Row Cell ss:StyleIDheaderData ss:TypeString列名/Data/Cell /Row /Table /Worksheet /Workbook关键点?mso-application progidExcel.Sheet?这行处理指令告诉系统用 Excel 打开这个 XML。ss:StyleID引用样式定义可以实现加粗、背景色、字体颜色等效果。ss:Type指定数据类型String 是字符串Number 是数字。3.2 从 ListCtrl 生成 SpreadsheetML 的完整流程生成 XML 比 CSV 多几步但逻辑不复杂。我一般会封装一个函数接收 ListCtrl 指针和输出路径BOOL ExportToSpreadsheetML(CListCtrl* pList, LPCTSTR lpszFilePath) { CString strXml; // XML 头 strXml _T(?xml version\1.0\ encoding\UTF-8\?\n); strXml _T(?mso-application progid\Excel.Sheet\?\n); strXml _T(Workbook xmlns\urn:schemas-microsoft-com:office:spreadsheet\\n); strXml _T( xmlns:ss\urn:schemas-microsoft-com:office:spreadsheet\\n); // 样式定义表头加粗、蓝底白字 strXml _T(Styles\n); strXml _T(Style ss:ID\header\\n); strXml _T(Font ss:Bold\1\ ss:Color\#FFFFFF\/\n); strXml _T(Interior ss:Color\#4472C4\ ss:Pattern\Solid\/\n); strXml _T(/Style\n); strXml _T(/Styles\n); // 工作表 strXml _T(Worksheet ss:Name\导出数据\\nTable\n); int nColCount pList-GetHeaderCtrl()-GetItemCount(); int nRowCount pList-GetItemCount(); // 表头行 strXml _T(Row\n); for (int nCol 0; nCol nColCount; nCol) { CString strColName; LVCOLUMN lvc { 0 }; lvc.mask LVCF_TEXT; lvc.pszText strColName.GetBuffer(256); lvc.cchTextMax 256; pList-GetColumn(nCol, lvc); strColName.ReleaseBuffer(); strXml _T(Cell ss:StyleID\header\Data ss:Type\String\); strXml EscapeXml(strColName); // XML 转义 strXml _T(/Data/Cell\n); } strXml _T(/Row\n); // 数据行 for (int nRow 0; nRow nRowCount; nRow) { strXml _T(Row\n); for (int nCol 0; nCol nColCount; nCol) { CString strText pList-GetItemText(nRow, nCol); strXml _T(CellData ss:Type\String\); strXml EscapeXml(strText); strXml _T(/Data/Cell\n); } strXml _T(/Row\n); } strXml _T(/Table\n/Worksheet\n/Workbook\n); // 写文件UTF-8 编码 return WriteUtf8File(lpszFilePath, strXml); }EscapeXml 函数负责把、、、、替换成对应的 XML 实体否则内容里有这些字符会导致 XML 解析失败。这个坑我踩过——ListCtrl 里有一列是文件路径路径里有符号导出的文件 Excel 直接报错打不开。WriteUtf8File 负责把 CString 转成 UTF-8 并写入文件。Unicode 项目下用 WideCharToMultiByte 转换BOOL WriteUtf8File(LPCTSTR lpszPath, const CString strContent) { // 宽字符转 UTF-8 int nLen WideCharToMultiByte(CP_UTF8, 0, strContent, -1, NULL, 0, NULL, NULL); if (nLen 0) return FALSE; char* pBuf new char[nLen]; WideCharToMultiByte(CP_UTF8, 0, strContent, -1, pBuf, nLen, NULL, NULL); CFile file; if (!file.Open(lpszPath, CFile::modeCreate | CFile::modeWrite)) { delete[] pBuf; return FALSE; } file.Write(pBuf, nLen - 1); // 不写结尾的 \0 file.Close(); delete[] pBuf; return TRUE; }参数说明CP_UTF8指定目标编码-1表示源字符串以 null 结尾nLen - 1是为了不把结尾的 null 字符写进文件。3.3 多 Sheet 和列宽控制SpreadsheetML 支持多 Worksheet只需要在 Workbook 里放多个Worksheet节点就行。列宽通过Column ss:Width120/设置放在Table开头Table Column ss:Width80/ Column ss:Width150/ Column ss:Width200/ Row.../Row /Table列宽单位是磅point大概 80 磅对应 10 个字符宽度。这个值需要根据你的实际列内容调整太窄了内容显示不全太宽了浪费空间。我一般会先导出一次看看效果再微调。注意SpreadsheetML 文件用 Excel 打开时如果 XML 格式有误Excel 会弹一个“文件格式与扩展名不匹配”的警告。虽然点“是”还能打开但用户体验不好。建议导出后用 XML 解析器验证一下格式是否正确。4. 避坑与排查那些让我加班到凌晨的细节4.1 中文乱码现象、原因、解决现象导出的 CSV 文件用 Excel 打开中文全是乱码英文和数字正常。原因Excel 默认用系统本地编码简体中文 Windows 是 GBK打开 CSV而你的文件是 UTF-8 编码两者不匹配。解决在文件开头写 UTF-8 BOM三个字节EF BB BF。Excel 看到 BOM 就知道这是 UTF-8 文件会用正确编码解析。如果不想写 BOM也可以把文件保存成 GBK 编码但 GBK 不支持某些特殊字符跨平台也不友好。我一般优先用 UTF-8 BOM。4.2 ListCtrl 虚拟列表取不到数据现象ListCtrl 设置了 LVS_OWNERDATA 虚拟列表模式用 GetItemText 取数据返回空。原因虚拟列表模式下数据不在控件内部控件只在需要显示时通过 LVN_GETDISPINFO 回调向外部要数据。GetItemText 自然取不到。解决虚拟列表的数据源在你自己的数据结构里数组、vector、数据库查询结果等直接从数据源取不要走 ListCtrl。如果你的代码里 ListCtrl 和数据源是分离的导出时直接遍历数据源就行反而更简单。4.3 COM 导出后 Excel 进程残留现象用 COM 方式导出后任务管理器里残留 EXCEL.EXE 进程多次导出后内存被吃满。原因COM 对象没有正确释放或者 Release 顺序不对。Excel 的 COM 接口层级是 Application → Workbooks → Workbook → Worksheets → Worksheet → Range每一层都要 Release。解决严格按创建顺序的逆序释放并且调用m_app.Quit()后再 Release Application。更稳妥的做法是用CoInitialize/CoUninitialize配对并在异常路径上也保证释放。如果嫌麻烦直接用 CSV 或 SpreadsheetML绕开 COM。4.4 大数据量导出卡死界面现象ListCtrl 里有几万行数据点击导出按钮后界面卡住用户以为程序死了。原因导出操作在 UI 线程同步执行耗时过长导致消息循环阻塞。解决把导出逻辑放到工作线程UI 线程弹一个进度条对话框。注意工作线程不能直接操作 MFC 控件需要把 ListCtrl 数据先拷贝到线程安全的结构里或者用 PostMessage 通知 UI 线程更新进度。如果数据量特别大还可以考虑分页写入避免一次性占用太多内存。4.5 文件被占用导致导出失败现象导出时提示文件无法写入或者写入后内容不完整。原因目标文件正在被 Excel 打开Windows 文件锁不允许写入。解决导出前检查文件是否可写如果被占用就提示用户先关闭文件。或者导出一个带时间戳的新文件名避免冲突。我一般会在文件名里加日期时间比如导出数据_20250101_143022.csv既避免冲突也方便追溯。5. 进阶技巧让导出功能更耐用5.1 封装成可复用的导出类每次写导出都复制粘贴代码不是长久之计。我习惯封装一个CListCtrlExporter类把 CSV 和 SpreadsheetML 两种格式都支持了class CListCtrlExporter { public: enum Format { CSV, SpreadsheetML }; // 导出入口format 指定格式bIncludeHeader 控制是否包含表头 static BOOL Export(CListCtrl* pList, LPCTSTR lpszPath, Format format CSV, BOOL bIncludeHeader TRUE); private: static BOOL ExportToCsv(CListCtrl* pList, LPCTSTR lpszPath, BOOL bIncludeHeader); static BOOL ExportToXml(CListCtrl* pList, LPCTSTR lpszPath, BOOL bIncludeHeader); static CString EscapeCsv(const CString strText); static CString EscapeXml(const CString strText); static BOOL WriteUtf8File(LPCTSTR lpszPath, const CString strContent); };这样调用方只需要一行CListCtrlExporter::Export(m_list, _T(D:\\导出数据.csv), CListCtrlExporter::CSV);参数说明第一个参数是 ListCtrl 指针第二个是输出路径第三个是格式枚举第四个控制是否写表头。封装之后新增格式只需要在类里加一个分支调用方不用改。5.2 导出前的数据校验不是所有 ListCtrl 内容都适合直接导出。我一般会在导出前做几项检查检查项处理方式行数为 0提示“没有数据可导出”直接返回单元格内容超长截断或提示避免 Excel 单元格溢出内容含非法字符过滤或转义避免文件损坏目标路径无权限提前检测给出明确错误提示这些检查看起来琐碎但能避免很多“导出成功但文件打不开”的尴尬。5.3 验证导出结果是否正确的习惯导出功能写完不是终点验证才是。我现在的习惯是每次改完导出逻辑都用一个包含边界数据的 ListCtrl 跑一遍——空行、超长文本、特殊字符、中文英文混排、数字和字符串混合。导出后用 Excel 打开逐列检查有没有错位、乱码、截断。从那以后我每次写完导出功能都强制走一遍这个验证流程先造边界数据再导出再打开检查。看起来多花几分钟但省下了后面被用户反馈“导出的文件有问题”的返工时间。希望帮到你。本文还有配套的精品资源点击获取