资讯动态

VS2015下编译xlnt静态库并读写xlsx实战

发布时间:2026/10/26 2:11:11 来源:尧图企业网站定制
简介这份面向C开发者与初学者的Excel读写示例工程基于开源库xlnt与Visual Studio 2015构建重点解决在C项目中不依赖COM或Office Interop即可创建、修改和读取xlsx工作簿的问题可用于报表生成、数据导入导出及自动化办公等场景。压缩包共99个文件、约3.21MB以68个hpp头文件构成库主体并配有sln与vcxproj工程文件、x64 Release下的dll/lib/exe及调试符号另含示例Excel工作簿和测试代码便于直接编译运行与断点调试。已有4260人学习下载。通过完整工程可观察工作簿与工作表管理、单元格读写、字体与样式设置、公式写入及保存流程也能对照库源码理解xlnt的接口组织与链接配置对想快速搭建C处理Excel环境、排查编译链接问题或二次开发的中级读者较有参考价值。1. 编译 xlnt 并读写 Excel为什么老项目里它仍是稳妥选择VS2015 工程里要读写.xlsx很多人第一反应是上 COM 调 Office结果部署机器没装 Excel 直接崩或者用 ODBC 驱动中文路径一改就翻车。xlnt_demo这个工程给出的答案是把 xlnt 作为静态库链进来纯 C 操作 xlsx不依赖任何 Office 运行时。它适合的场景很明确——工业上位机、MFC 报表导出、老代码库加一个 Excel 通道编译器版本锁死在 VS2015、短期内不打算升 C17 的那批项目。Excel C 这条路不好走xlnt 至少把 OOXML 那堆 XML 黑匣子包成了worksheet、cell、range这类顺手的对象代价是编译期和若干 ABI 细节下面把复现路径和坑一次说清。2. 环境准备与工程搭建VS2015 下把 xlnt 编成可用的静态库xlnt 是个 C14 的库源码用 CMake 组织。VS2015 的 MSVC 19.0 对 C14 支持基本够用但工程生成方式和依赖管理要绕一下直接照官方 README 走容易在cmake --build那步断掉。动手之前先把工具链核对清楚能省掉至少半天的玄学排查。2.1 工具链版本核对与依赖布局需要的东西不多但版本必须对得上组件建议版本说明Visual Studio2015MSVC 19.0工程文件是 v140 工具集CMake3.10 及以上低版本对 VS2015 generator 支持不全xlnt 源码1.5.x 分支该分支保留 C14 兼容写法第三方依赖libstudxml、utf8-cppxlnt 内部用于 XML 解析和编码转换依赖布局上有个常识xlnt 允许通过EXTERNAL_LIBSTUDXML指向外部已编译的 libstudxml也支持把源码放进third-party目录让它一起编。老项目我一般选后者因为外部预编译库的运行时/MT 还是 /MD一旦和主工程对不上链接期就是一堆LNK2005重复符号。把libstudxml和utf8-cpp的源码目录按 xlnt 期望的名字放好后面 CMake 配置就不用再指定路径。目录结构大概是这样保持这个层级能少改一堆变量xlnt_demo/ ├── third-party/ │ ├── libstudxml/ # 解压后的源码根目录 │ └── utf8-cpp/ # utf8.h 所在目录 ├── xlnt/ # xlnt 源码 ├── demo/ # 你自己的 VS2015 工程 └── build/ # CMake 生成目录单独建提示源码目录不要带中文和空格。CMake 在 Windows 下对含空格路径的字符串转义处理不稳配置阶段报的错往往指向一个看起来完全无关的变量。2.2 CMake 配置与静态库编译配置阶段要把生成器、架构、运行时库三件事同时钉死尤其是运行时库它决定了后面主工程能不能链接成功。下面这组命令在 VS2015 开发者命令提示符里执行cd build # 生成 VS2015 工程指定 32 位64 位把 Win32 换成 x64 cmake .. -G Visual Studio 14 2015 -A Win32 ^ -DCMAKE_INSTALL_PREFIX../install ^ -DSTATIC_RUNTIMEON ^ -DTESTSOFF ^ -DSAMPLESOFF # 编译 Release 并安装头文件lib cmake --build . --config Release --target install参数逐个说清楚-G Visual Studio 14 2015对应 v140 工具集写成别的名字 CMake 会直接报找不到生成器-A Win32是给 VS2015 的架构选项老版本 CMake 用-G ... Win64的写法两套别混用-DSTATIC_RUNTIMEON让 xlnt 用/MT编译这一步是整个流程里最容易忽略又最致命的开关主工程如果是/MT而库是/MD链接期会报一堆 CRT 符号冲突-DTESTSOFF -DSAMPLESOFF只是缩短编译时间xlnt 的测试用例不少全编要等挺久。编译成功后install目录下会有include/xlnt和lib/xlnt.libDebug 配置是xlntd.lib。如果--build阶段报的是libstudxml相关的语法错误基本可以确定是 C 标准没对齐检查一下 CMake 缓存里CMAKE_CXX_FLAGS有没有被外部环境变量污染。2.3 主工程引入头文件、库路径与运行时库对齐在自己工程的属性页里做三处配置缺一处就会在编译或链接阶段失败C/C → 常规 → 附加包含目录加install/include链接器 → 常规 → 附加库目录加install/lib链接器 → 输入 → 附加依赖项加xlnt.libDebug 用xlntd.lib。最关键的是第四件事属性页里没有它C/C → 代码生成 → 运行时库必须和编译 xlnt 时选的/MT或/MD一致。VS2015 新建工程默认是/MDd而上面我们用STATIC_RUNTIMEON编出来的是/MT不改这一项第一次链接必然是一屏LNK2038和LNK2005。要么把主工程改成/MT要么重新用-DSTATIC_RUNTIMEOFF编一版/MD的库两条路选一条走到底不要一半一半。还有个细节xlnt 的头文件大量使用模板和内联包含顺序上建议把自己的业务头文件放在 xlnt 之后避免 Windows.h 的宏比如min、max污染 xlnt 内部的std::min调用。真碰上了就在包含 xlnt 之前加#define NOMINMAX。3. 读写 xlsx 的核心 API从创建工作簿到单元格批量写入库链上之后剩下的就是 API 怎么用。xlnt 的接口设计偏“对象流”workbook管文件、worksheet管表、cell管值链式调用能省不少代码。但它的隐式转换和索引规则有几处和直觉不符写业务代码前先摸清这些边界比事后调试强。3.1 工作簿、工作表与单元格的基本操作一个最小的写入流程包含建表、写值、设格式、保存四步#include xlnt/xlnt.hpp int main() { xlnt::workbook wb; // 默认工作簿自带一张 Sheet1取不到就新建 xlnt::worksheet ws wb.active_sheet(); ws.title(Report); // 写表头列号从 1 开始行号也从 1 开始 ws.cell(A1).value(序号); ws.cell(B1).value(名称); ws.cell(C1).value(金额); // 批量写数据cell(列, 行) 的参数顺序是列在前 for (int i 1; i 100; i) { ws.cell(1, i 1).value(i); ws.cell(2, i 1).value(item_ std::to_string(i)); ws.cell(3, i 1).value(12.5 * i); } // 列宽按字符数估算太窄导出后要手动拉 ws.column_width(2, 24); // 数字格式金额保留两位小数 ws.cell(C2).number_format(0.00); wb.save(report.xlsx); return 0; }这里最坑的是cell()的重载cell(A1)收的是字符串引用cell(1, 2)收的是列、行两个整数顺序是列在前、行在后和很多人习惯的“先行后列”相反。写成cell(2, 1)会静默写到 B1 而不是 A2编译期不报错运行时数据全错位。字符串引用那边同理A1是合法的1A会抛异常捕获不到就是进程直接退出。value()是模板方法塞int、double、std::string都行内部按类型分派。但塞const char*时要小心某些版本对字面量推导会走到布尔重载结果单元格里出现 TRUE/FALSE这是血泪经验遇到类型诡异先显式包一层std::string。3.2 读取已有文件与类型判断读的流程比写多一步类型判断因为 xlsx 里每个单元格的底层类型和 C 类型不是一一对应xlnt::workbook wb; wb.load(report.xlsx); xlnt::worksheet ws wb.active_sheet(); // 按行遍历rows() 返回的是 range支持范围 for for (auto row : ws.rows(false)) { for (auto cell : row) { if (cell.has_value()) { switch (cell.data_type()) { case xlnt::cell_type::number: // 数值统一按 double 取整数自行转换 std::cout cell.valuedouble() ; break; case xlnt::cell_type::inline_string: case xlnt::cell_type::shared_string: std::cout cell.valuestd::string() ; break; default: std::cout [other] ; break; } } } }rows(false)的参数控制是否跳过空行传false表示不跳过遍历行号更连续。data_type()返回的枚举里字符串分inline_string和shared_string两种共享字符串是 xlsx 内部为省空间做的字符串池读取时都要按字符串处理只判断其中一种会漏掉大量文本单元格。需要注意用valuedouble()取数值是安全的但用valueint()取一个存了3.7的单元格不同版本行为不一致有的截断有的抛异常。业务上如果确定是整数列先取double再自己static_cast行为可控。3.3 合并单元格、样式与公式的写入报表类需求绕不开合并、边框和公式这三样在 xlnt 里的写法比想象中啰嗦// 合并 A1:C1合并后只有左上角单元格能写值 ws.merge_cells(A1:C1); ws.cell(A1).value(月度汇总); // 字体与对齐 xlnt::font f; f.bold(true); f.size(14); ws.cell(A1).font(f); xlnt::alignment al; al.horizontal(xlnt::horizontal_alignment::center); al.vertical(xlnt::vertical_alignment::center); ws.cell(A1).alignment(al); // 公式写字符串表达式不写等号 ws.cell(C101).formula(SUM(C2:C100)); // 边框要给四条边分别设 xlnt::border b; b.side(xlnt::border_side::begin, xlnt::border_style::thin); b.side(xlnt::border_side::end, xlnt::border_style::thin); b.side(xlnt::border_side::top, xlnt::border_style::thin); b.side(xlnt::border_side::bottom, xlnt::border_style::thin); ws.cell(A1).border(b); wb.save(report.xlsx);merge_cells之后往被合并的区域写值不会报错但保存后打开会被 Excel 判定为文件损坏提示“需要修复”这是最典型的翻车点之一。公式写入时formula()的参数不带前导带了会被当成普通文本而且公式的计算结果 xlnt 不会替你算需要 Excel 打开时自己重算程序里紧接着读这个单元格拿到的是空值。3.4 中文与编码处理中文能不能正常写进去取决于字符串来源。xlnt 内部按 UTF-8 存储而 VS2015 下std::string走的是本地代码页GBK直接塞中文进去文件打开就是乱码。常见做法是转换后再写#include windows.h // GBK - UTF-8写文件前统一转 std::string toUtf8(const std::string gbk) { int n MultiByteToWideChar(CP_ACP, 0, gbk.c_str(), -1, nullptr, 0); std::wstring w(n, 0); MultiByteToWideChar(CP_ACP, 0, gbk.c_str(), -1, w[0], n); int m WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, nullptr, 0, nullptr, nullptr); std::string u(m, 0); WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, u[0], m, nullptr, nullptr); // 去掉末尾的 \0 if (!u.empty() u.back() \0) u.pop_back(); return u; }两个MultiByteToWideChar和WideCharToMultiByte的调用是固定套路第一个算长度第二个做转换。注意返回值m包含结尾的\0构造出来的std::string尾部会多一个空字符写进单元格后虽然肉眼看不出来但做字符串比较时永远不相等必须手动pop_back。这段代码在 Demo 里一般包成一个工具函数所有外部传入的文本走一遍。Windows 上还有个更省事的办法工程属性里把C/C → 命令行加上/utf-8让源码和执行字符集都按 UTF-8 处理那就连转换函数都省了。但这招会影响整个工程里所有字符串字面量老代码如果依赖 GBK 的strlen结果加了之后会出现截断所以要评估存量代码后再动。4. 构建与部署过程中的坑xlnt 落地时的常见问题排查这类库的麻烦往往不在 API而在“我这台机器能跑换台机器就崩”。下面几条是复现过程中出现频率最高的按现象、原因、解决三段写遇到问题可以逐条比。4.1 链接期报 LNK2038 与 LNK2005现象主工程编译通过链接阶段报LNK2038: 检测到_ITERATOR_DEBUG_LEVEL的不匹配项或者成片的LNK2005: 已经在 xxx.obj 中定义。原因_ITERATOR_DEBUG_LEVEL是 Debug/Release 混链的典型信号也就是主工程 Debug 而 xlnt 库是 Release 版LNK2005则是运行时库不一致一边/MT一边/MD两套 CRT 都被拉进来。解决先确认install/lib里有没有xlntd.libDebug 版没有就是当初只编了 Release用cmake --build . --config Debug --target install补一遍再逐个项目核对属性页的运行时库选项主工程和库必须同档。最稳的做法是把运行时库写进 CMake 参数里固定下来主工程新建时第一时间改掉默认值不要等链接报错再回头找。4.2 运行时报缺少 libstudxml 符号或入口点现象链接通过但启动即崩调试器停在__scrt_common_main_seh或者报无法解析的外部符号指向xml::parser。原因libstudxml 没被正确编进 xlntEXTERNAL_LIBSTUDXML打开了但路径没指对或者 third-party 目录名和 xlnt 期望的不一致CMake 静默跳过了它。解决检查 CMake 配置输出里有没有libstudxml相关的编译条目没有就在配置阶段加-DEXTERNAL_LIBSTUDXMLOFF让它走内置源码编译。目录名严格按 xlnt 的查找规则命名大小写也要对。清空build目录重新配置一次缓存里的旧路径会干扰判断。4.3 中文路径或中文内容乱码现象wb.save(D:/报表/月报.xlsx)抛异常或生成的文件名是乱码单元格里的中文打开后显示为方块。原因Windows 文件 API 的窄字符版本按本地代码页解释路径xlnt 内部如果走的是std::ofstream的窄字符接口UTF-8 路径传进去就废了内容乱码则是前面说的 GBK/UTF-8 没转换。解决路径统一用相对路径或纯英文目录避开中文内容侧在写之前统一过一遍toUtf8。如果路径中文无法避免把目标目录换成短路径别名或者先用GetShortPathName转成 8.3 格式再传。4.4 大文件写出慢或内存暴涨现象写 5 万行以上的表程序内存持续上涨到几百兆保存一次要等十几秒。原因xlnt 是纯内存模型workbook里所有单元格对象都在堆上不是流式写。行数越多对象越多save时还要整体序列化成 XML。解决控制单表规模按业务分 sheet 或分文件落地写入循环里避免反复cell()查找同一位置能复用的cell_reference先取出来样式对象font、border不要在循环里创建提到循环外复用否则每个单元格都挂一份独立样式XML 体积会翻几倍。4.5 打开生成文件提示需要修复现象Excel 打开 xlsx 时弹修复提示修完发现合并单元格区域或公式列被清空了。原因合并区域内有多个单元格被写过值Excel 要求合并区只有左上角有值或者公式字符串带了前缀被写成了非法表达式。解决合并前先把多余单元格的值清空或者调整写入顺序先合并再只写左上角。公式检查一遍有没有多余的有就删掉。这个错误程序侧完全无感知只能靠打开文件肉眼验证所以导出功能上线前必须人工看一遍成品。5. 进阶技巧用模板文件与批量回写把导出做成可维护的流程API 会用了之后真正决定这套代码能不能长期维护的是有没有把“格式”和“数据”分开。我的做法是维护一个.xlsx模板文件表头、列宽、边框、打印区域、合并单元格全部在模板里画好程序只负责往里填数样式一行代码都不写。具体操作是先加载模板再写xlnt::workbook wb; wb.load(template.xlsx); xlnt::worksheet ws wb.active_sheet(); // 数据从第 3 行开始模板里前两行是表头和说明 int row 3; for (const auto item : records) { ws.cell(1, row).value(item.id); ws.cell(2, row).value(toUtf8(item.name)); ws.cell(3, row).value(item.amount); ws.cell(4, row).value(item.date); row; } // 模板里预置的公式区域要随行数扩展重新写一遍范围 ws.cell(3, row).formula(SUM(C3:C std::to_string(row - 1) )); wb.save(output_ timestamp() .xlsx);这套做法的好处是业务改版时不用动代码设计改个模板文件就行坏处是模板本身的单元格位置变成了隐式契约行号写死在代码里。缓解办法是把起始行、列号抽成常量或配置文件改模板时同步改一处配置比到处翻魔法数字强。再进一步是把“验证”也固化进流程。每次导出后不只看文件生成了没有而是重新读回来做一遍校验校验项判断方式失败含义行数ws.highest_row()与预期条数比对循环漏写或越界关键列非空逐行读第 1 列判断has_value()数据源字段缺失合计公式读公式单元格的文本比对范围串范围拼接写错文件可打开重新load一遍不抛异常合并区或公式非法这套回读校验的成本很低但能拦住绝大多数“文件生成了、打开是坏的”这类问题尤其是合并单元格那类程序侧无感知的错误。最后说个我踩过的教训早期为了图快我在一个导出函数里同时做了建表、写样式、写数据、合并单元格四件事两百多行堆在一个函数里。后来业务要加一列改完发现整张表边框全丢了——因为中途某个循环里font对象没复用把模板里的样式覆盖掉了。从那以后我每次做 Excel 导出都强制走“模板 只写数据 回读校验”这三步样式绝不进代码。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑