ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenXLSX实战:C++与MFC项目中高效读写Excel文件指南

OpenXLSX实战:C++与MFC项目中高效读写Excel文件指南 简介面向VS2019与MFC框架的C开发者这份资源提供OpenXLSX库的完整应用示例可用来解决在Windows桌面程序中读写Excel XLSX表格的常见需求适合需要快速交付桌面报表功能的程序员参考。压缩包共包含143个文件总大小128.69MB文件类型涵盖50个hpp头文件与25个cpp源码文件对应OpenXLSX核心及MFC工程实现、6个h资源头文件、2个lib/2个exe编译产物、2个rc/rc2界面资源以及sln、vcxproj工程文件和pdb、tlog等调试记录既可直接运行查看效果也适合对照学习源码结构。目前已有3681人学习下载。资源内默认带有一个MFC Application完整示例演示了创建新工作簿、读取已有表格、写入文本/数字/公式、设置单元格样式、调整工作表顺序等操作并对VS2019环境下OpenXLSX库的导入、编译与链接细节做了适配通过这个示例读者能快速掌握C/MFC项目中集成OpenXLSX的方法进而应用到数据分析、报表生成、自动化办公等真实场景提升Excel任务的处理效率。 直接上结论如果你需要在C项目里读写字.xlsx格式的Excel文件OpenXLSX是一个值得认真考虑的开源库。我最近在一个MFC桌面工具里接入了这套库用来导出数据报表和读取设备配置表整个过程踩了一堆坑也积累了不少经验今天整理出来分享给大家。先说清楚OpenXLSX是什么。它是一个基于C11标准开发的Excel读写库只支持.xlsx格式Office 2007之后的文件格式不支持老的.xls。库本身不依赖微软的Excel COM组件也不需要安装Office环境这对做工具类软件分发来说是一个非常实在的优势。它通过直接解析Excel底层的XML文件来实现读写所以在目标机器上不需要预装任何额外组件。这次项目里我用的是VS2019编译环境开发框架是MFC对话框程序功能上主要包括把界面上的表格数据导出成Excel报表、批量导入Excel里的配置项、以及把程序的输出结果写回模板Excel文件。这三个场景基本覆盖了绝大多数桌面工具对接Excel的需求。1. 为什么选择OpenXLSX而不是其他方案1.1 主流C读写Excel方案对比在决定用OpenXLSX之前其实我对比过市面上常见的几种方案各有优缺点后来的取舍思路可以给你一个参考。先说最常见的很多人会直接用Excel的COM接口通过#import导入Excel类型库来操作。这个方案功能最全Excel能做的操作它基本都能做但它要求目标机器上装了Office而且运行时必须启动Excel进程速度和稳定性都受影响。程序一旦在客户端崩溃经常能看到后台挂着一堆EXCEL.EXE进程体验很差。其次是ODBC方式。Excel可以作为数据源通过ODBC连接用SQL语句读写表格内容。这个方案的优点是写出来的代码像操作数据库一样清晰但格式控制能力几乎为零单元格合并、列宽、字体这些统统管不了而且对文件路径和系统配置比较敏感经常出现驱动不匹配的问题。还有class SimpleXlsxWriter这类只写不读的库。如果你只需要生成报表这类库确实轻量但遇到需要回读、修改字段、追加数据的需求就完全没办法了。OpenXLSX在这几个方案里算是比较平衡的一个。它能同时支持读写不需要外部依赖代码结构清晰API设计符合C开发者的直觉。当然它也有短板比如不支持.xls老格式样式相关功能也不够丰富但对我这次的项目场景来说是够用的。1.2 OpenXLSX能做什么、不能做什么我实际测试下来OpenXLSX能覆盖的工作包括创建工作簿、读写单元格、批量填充数据、读取行列范围、设置单元格的基本样式字体、加粗、填充色、列宽、行高、合并单元格以及读写单元格的公式和日期格式。这些基本覆盖了企业办公场景下百分之八九十的Excel操作需求。它目前做不到的包括图表的创建、数据透视表、宏、条件格式这类高级功能。如果你需要生成带图表的复杂报表OpenXLSX可能撑不住得考虑更重的方案或者用模板文件来绕。1.3 API风格与上手成本OpenXLSX的接口设计非常贴合C开发者的习惯。整体操作入口是一个XLDocument对象对应一个Excel工作簿通过它的workbook()方法拿到工作簿再用名字获取指定的工作表worksheet。读一个单元格的值只需要一行代码写入也只需要调用worksheet.cell(A1).value()并赋值。这套设计跟Python的openpyxl非常像如果你之前用过openpyxl写脚本再看OpenXLSX会觉得很亲切。2. VS2019编译环境配置全流程2.1 源码获取与依赖说明OpenXLSX的源码托管在GitHub上直接clone或者下载zip包都行。它的依赖管理做得比较舒服核心依赖只有一个叫做zippy的zip解压库而且是以submodule方式内嵌在仓库里的。这就意味着clone的时候需要加上--recurse-submodules参数否则后续编译会报zlib等头文件找不到的错误。我最初就是在这步栽了跟头。直接git clone了主仓库CMake配置时提示找不到zippy库折腾了半天才发现submodule没拉下来。补上了之后一切顺畅。2.2 CMake编译步骤OpenXLSX官方推荐用CMake来构建安装包我们先准备好依赖库再用CMake配置生成。用VS2019自带的CMake支持也可以但如果你是第一次配置我更推荐用命令行的方式生成的工程清晰后续调试也好受控。但总体上要注意一点编译出来的库要与你项目的运行架构、运行库保持一致这也是我们做MFC程序时最容易忽视的一个点。具体编译步骤可以按照下面的思路来整理。git clone --recurse-submodules https://github.com/OpenXLSX/OpenXLSX.git cd OpenXLSX mkdir build cd build cmake .. -G Visual Studio 16 2019 -A x64 -DCMAKE_CONFIGURATION_TYPESDebug;Release cmake --build . --config Release cmake --install . --config Release --prefix D:/OpenXLSX_install上面这条命令把编译产物安装到D:/OpenXLSX_install目录下里面会包含include、lib、bin三个子目录。编译完成后你需要做的就是把这些路径配置到你的工程属性中。2.3 在MFC工程中引用OpenXLSX配置工程的时候要特别注意几个关键点下面按顺序给你梳理清楚。首先在你的MFC工程属性中把D:/OpenXLSX_install/include加入“C/C → 常规 → 附加包含目录”。然后确认“C/C → 语言 → C语言标准”设置为C17OpenXLSX的代码对C14支持有些边缘问题直接用C17最省心。其次进入“链接器 → 常规 → 附加库目录”加上D:/OpenXLSX_install/lib。然后在“链接器 → 输入 → 附加依赖项”里填入OpenXLSX.lib。这里有个关键点如果你Debug和Release都编译了每次切换配置都要去确认lib路径对应的是Debug还是Release子目录这个细节直接决定你能不能链接成功。最后还有一项容易被忽略就是运行库类型。如果OpenXLSX配置的是动态运行时/MD你的MFC工程也要使用“多线程DLL”运行库否则链接时会报出一堆莫名其妙的LNK2038错误。实际上我建议两边的配置要一致尽量都是/MD避免后续动态链接和CRT函数冲突的问题。2.4 动态库文件处理如果你编译的是动态版本OpenXLSX默认会生成dll编译完成后记得把OpenXLSX.dll复制到你的程序输出目录一般是Debug或Release目录。如果漏掉这一步编译可以过但程序运行时会在启动阶段报“找不到OpenXLSX.dll”这种报错对使用这套库的用户来说非常典型。另外需要注意的是zippy依赖是静态绑定的所以运行目录里不需要多放一个zippy的dll这算是个好消息。3. 核心功能与实操代码3.1 创建工作簿并写入数据最常见的场景就是创建一个全新的Excel文件把C里的数据结构写进去。下面这段代码展示了从创建文件到写入数据的完整链路。它解决了工作中“数据导出”的需求核心代码逻辑也简单直接。#include OpenXLSX.hpp using namespace OpenXLSX; void CreateAndWriteDemo() { XLDocument doc; doc.create(D:/report_demo.xlsx); auto wks doc.workbook().worksheet(Sheet1); // 写入表头 wks.cell(A1).value() 序号; wks.cell(B1).value() 设备名称; wks.cell(C1).value() 温度数值; // 写入数据行 for (int i 0; i 10; i) { wks.cell(i 2, 1).value() i 1; wks.cell(i 2, 2).value() Device_ std::to_string(i); wks.cell(i 2, 3).value() 35.5 i * 0.5; } // 设置列宽方便查看 wks.column(A).width() 8; wks.column(B).width() 20; wks.column(C).width() 15; doc.save(); doc.close(); }这一段代码看起来简单但有两个细节值得提一下。第一个是cell(i 2, 1)这种用行列数字索引的方式它和cell(A1)字符串方式可以混用如果数据量比较大用数字索引写循环会更顺手。第二个是写入字符串值时OpenXLSX的value()接口能直接接收const char*和std::string不需要额外做转换。3.2 读取已有Excel文件读取文件是另一个高频场景特别是在做配置导入功能时。下面这段代码展示了如何遍历一个工作表中的数据并处理不同类型的单元格值。这部分也是我在实际项目中用得最多的一个能力。void ReadExistingFile() { XLDocument doc; doc.open(D:/device_config.xlsx); auto wks doc.workbook().worksheet(config); // 获取数据的行数和列数 uint32_t rowCount wks.rowCount(); uint32_t colCount wks.columnCount(); for (uint32_t row 1; row rowCount; row) { for (uint32_t col 1; col colCount; col) { auto cell wks.cell(row, col); if (!cell.hasValue()) continue; if (cell.value().type() XLValueType::Integer) { int val cell.value().asint(); // 处理整数 } else if (cell.value().type() XLValueType::Float) { double val cell.value().asdouble(); // 处理浮点数 } else if (cell.value().type() XLValueType::String) { std::string val cell.value().asstd::string(); // 处理字符串 } } } doc.close(); }这里要特别注意rowCount()和columnCount()的语义。OpenXLSX返回的行列数不是“最后一格有数据的行列”而是整个工作表占用的范围可以理解为Excel里的“已使用区域”。如果你的表格只有最右下角一个格子里有数据这个函数返回的行列数可能大得出乎意料。实际操作中我习惯先遍历判断hasValue()再决定是否处理能省下很多不必要的类型转换。3.3 单元格样式的设置OpenXLSX虽然不像COM接口那样能做所有样式但常用的格式化需求还是能满足的。下面是我在一个演示里用过的样式设置段生成出来的报表观感跟手工做的已经差别不大了。void ApplyStyles(XLWorksheet wks) { auto fontA1 wks.cell(A1).font(); fontA1.setFontName(微软雅黑); fontA1.setFontSize(11); fontA1.setBold(true); // 设置单元格填充色 auto fillA1 wks.cell(A1).fill(); fillA1.setFillColor(XLColor(150, 200, 255)); }在样式方面有个使用上的提示OpenXLSX中的font()和fill()返回的都是代理对象意味着你获取到之后直接在它身上调用set函数是不够的要写回到单元格本身。不过我实测发现如果想直接操作单一单元格调用cell.setFont(...)再配合前面那种获取方式效果更稳定。如果需要设置一整行或一整列样式的批量格式循环这样一行一列地设置也没问题只是要注意别在MFC主线程里对超大数据量做这种批量操作否则界面会卡得比较明显。3.4 合并单元格与更多细节合并单元格在我做报表标题时经常用到OpenXLSX提供了mergeCells接口。它的参数是左上角和右下角的单元格引用合并之后写入左上角单元格的值会在整个合并区域展示。wks.mergeCells(XLCellRange(A1:C1)); wks.cell(A1).value() 设备运行状态汇总表;与此对应的还有unmergeCells接口用于取消合并。这里有个坑合并单元格之后hasValue()的判断行为会变得复杂如果你发现读回来的数据缺一块多半是跟合并区域有关。4. MFC对话框程序中的集成示例4.1 创建MFC应用并配置工程链接在VS2019里创建MFC应用时我建议你先用“MFC应用”向导生成一个完整的Dialog程序然后把OpenXLSX的配置加上去不要一开始就追求把工程配置做成空项目再转MFC那样会多花不少时间处理字符集和框架文件的连接问题。创建好之后把前文提到的那三个工程配置项附加包含目录、附加库目录、附加依赖项加上再把OpenXLSX.dll放到输出目录就可以敲代码了。这里有一个很重要的Visual Studio细节要给MFC开发人员提个醒MFC工程默认的字符集是“Unicode字符集”而OpenXLSX内部用的是UTF-8字符串。当你从MFC的CString里取出宽字符串再传给OpenXLSX时不能直接做CString到std::string的强制转换要用CT2A之类的转换宏处理好编码否则中文路径和中文内容会出现乱码。这个坑非常典型我第一次用的时候就栽在这里。4.2 导出报表到Excel的完整代码下面梳理一个简单但完整的导出示例界面上有一个CListCtrl里面装了一些设备数据点击“导出”按钮后把列表内容写进新的Excel文件。在这一环节MFC和OpenXLSX的衔接方式是我今天分享的重点之一因为很多人写到这里才发现字符转换和列表遍历都是细节活。void CExcelDemoDlg::OnBtnExport() { // 1. 获取界面上的数据 CListCtrl listCtrl m_listDevice; int rowCount listCtrl.GetItemCount(); if (rowCount 0) { AfxMessageBox(_T(没有可导出的数据)); return; } std::string strPath D:/device_export.xlsx; // 2. 创建并写入Excel文件 try { OpenXLSX::XLDocument doc; doc.create(strPath); auto wks doc.workbook().worksheet(Sheet1); // 表头 CString headerName; for (int col 0; col 3; col) { headerName listCtrl.GetColumn(col) -1 ? _T() : m_arrHeader[col]; wks.cell(1, col 1).value() CT2A((LPCTSTR)headerName, CP_UTF8); } // 数据 for (int row 0; row rowCount; row) { for (int col 0; col 3; col) { CString itemText listCtrl.GetItemText(row, col); std::string strItem CT2A((LPCTSTR)itemText, CP_UTF8); wks.cell(row 2, col 1).value() strItem; } } doc.save(); doc.close(); } catch (const std::exception ex) { std::string errMsg 导出失败: ; errMsg ex.what(); AfxMessageBox(CT2A(errMsg.c_str(), CP_UTF8)); return; } AfxMessageBox(_T(导出成功)); }代码逻辑本身不复杂但有几个地方我重点注释了表头部分从CListCtrl的列信息里取名字数据区遍历所有可见行的所有列中文内容通过CT2A转成UTF-8再传给OpenXLSX整个写入过程用try/catch包住以保证异常可见、不至于让程序闪退。4.3 在MFC里维护Excel业务流程时的额外考虑在MFC程序里写Excel业务和平常在命令行测试有不少区别。首先是UI线程的问题如果Excel数据量很大几十万行的写入会占用较长时间这种时候你需要把写入逻辑扔到工作线程里同时用PostMessage给主窗口发送进度消息和完成通知。其次是文件占用问题如果你用Excel打开了目标文件程序再去写同一个路径打开、保存环节会报异常这时候要统一做异常提示告知用户先关闭Excel中的同名文件。然后是中文路径问题MFC程序经常在中文系统下运行用户在文件对话框里选择的路径常常包含中文和空格OpenXLSX对中文路径支持得很好但前提是你在传给API之前把编码处理好不然路径字符串变成乱码文件自然就打不开。4.4 读取Excel后刷新MFC界面读取Excel并回显到界面上的流程相当于把上面的流程反过来操作但有一个动态更新的场景值得特别说明一下。如果你希望Excel文件在程序运行时被其他进程修改了程序需要实时刷新读取我的做法是把“读取文件”的函数封装成一个独立的刷新函数配合一个定时器周期性调用。读取时先打开工作簿把每个单元格的字符串取出来再通过SetItemText更新到CListCtrl。注意这里不能直接往控件里塞成百上千条数据最好先LockWindowUpdate装填完毕再解锁这样界面不会闪烁。5. 常见问题与排查技巧实录5.1 编译链接阶段的坑我用OpenXLSX VS2019 MFC这个组合时遇到最多的问题集中在编译阶段。下面的表格整理了高频报错和对应解法都是我在实际项目里实践验证过的。报错信息原因分析解决方法LNK2038运行时库不匹配工程和库的/MD、/MT不一致统一改成多线程DLL(/MD)找不到OpenXLSX.h头文件附加包含目录没配好检查include目录路径无法打开OpenXLSX.lib附加库目录或依赖项名错误确认lib目录与依赖项名称C2872项符号冲突Windows头文件中存在重名定义使用OpenXLSX::前缀限定命名空间编译速度极慢头文件庞大且未启用PCH将OpenXLSX.hpp放入stdafx.h预编译头第4条“符号冲突”值得展开讲细一点。MFC的Windows头文件定义了大量宏和类型OpenXLSX内部又用到了一些通用名字一旦重名编译器就会跳出来告诉你“不明确的符号”。处理办法很简单引用OpenXLSX时使用完整限定名OpenXLSX::XLDocument而不是using namespace OpenXLSX;就可以在很大程度上避开这类冲突。5.2 运行时崩溃和异常运行时最常见的问题是打开文件时抛异常原因通常是文件被Excel进程占用、路径不存在、文件不是合法的xlsx格式或者是.xls老格式。如果打开文件这一步频繁失败可以加一个std::filesystem::exists的路径预检查同时用try/catch捕获异常打印异常消息辅助定位。在处理大文件时如果程序内存占用飙高不要一次性把所有单元格数据搬到内存再写出去可以逐行读取、逐行写入这样在数据量较大的情况下体验会流畅不少。5.3 常见翻车场景与避坑提醒写文件时如果目标文件已经存在OpenXLSX默认会直接覆盖整个文档而不是增量修改。我一开始就想当然地认为它跟Excel的“另存为”一样安全结果丢过两次数据。现在的习惯是在打开工作簿前先检查路径下是否已有旧文件如果有先按要求备份一份再覆盖。还有一个跟编码有关的坑OpenXLSX对UTF-8字符串支持很好但在MFC的“Unicode字符集”环境下CString默认是UTF-16如果你不先转成UTF-8就直接传给库函数中文内容会变成乱码。我封装了一个CStringToUtf8的小工具函数所有进OpenXLSX的字符都过一遍这个转换整体稳定性提高了很多。关于样式和合并单元格这里也提一下OpenXLSX对样式支持得比较基础合并单元格、条件格式、数据验证这些高级功能目前覆盖不全。如果你要生成带复杂格式的报表建议先在Excel里做好一个带样式的模板文件再用OpenXLSX往模板的单元格里填数据这个方法比代码里逐格设置样式稳定得多。6. 总结与个人经验分享把OpenXLSX接入MFC工程的过程其实就是一次典型的C第三方库集成实战。这里分享几个我实际操作中积累的小心得不一定系统但很实用。代码风格上我强烈建议在MFC代码里对OpenXLSX做一层薄封装比如封装一个ExcelHelper类把创建文件、写入表头、批量写入数据行这些操作都收进去。这样MFC界面代码里调用时只需两三行后续如果替换底层库也只需要改封装类界面代码完全不用动。这个做法在我后来的几个项目里都延续了下来维护成本很低。性能方面如果你遇到大数据量写入的场景Excel文件写入的速度瓶颈往往不在库本身而在于频繁的单元格操作和字符串转换。我这里实测在Release模式下写5万行10列的数据耗时大约2到3秒作为桌面工具的报表导出功能完全够用。如果你要跨程序调度那就需要考虑把数据写入丢到工作线程避免界面卡顿。最后再提醒一下编译配置的事VS2019下用CMake编译OpenXLSX一定要保持Debug和Release使用的运行库配置一致并统一C标准为C17。把这个基础打牢后面的开发会顺畅很多。本文还有配套的精品资源点击获取
返回列表