简介:面向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是什么。它是一个基于C++11标准开发的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_TYPES="Debug;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++语言标准”设置为C++17,OpenXLSX的代码对C++14支持有些边缘问题,直接用C++17最省心。
其次,进入“链接器 → 常规 → 附加库目录”,加上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().as<int>(); // 处理整数 } else if (cell.value().type() == XLValueType::Float) { double val = cell.value().as<double>(); // 处理浮点数 } else if (cell.value().type() == XLValueType::String) { std::string val = cell.value().as<std::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++标准为C++17。把这个基础打牢,后面的开发会顺畅很多。
本文还有配套的精品资源,点击获取