☰
VS2015下纯C++读写XLSX:xlnt零依赖工程实战
2026/10/21 19:18:54 网站建设 项目流程

简介:本资源是一个基于Visual Studio 2015的C++工程示例,面向中初级C++开发者,解决在原生C++环境中高效读写Excel文件的技术痛点,避免依赖COM组件或跨语言调用。项目完整集成xlnt开源库,涵盖从环境配置、工作簿创建、单元格读写、样式设置到公式计算与文件保存的全流程实践,适用于自动化报表生成、数据采集后处理及办公文档集成等实际场景。压缩包共99个文件,含68个hpp头文件(构成xlnt核心接口)、2个xlsx示例文件(test_read.xlsx/test_save.xlsx)、1个sln解决方案及配套vcxproj工程文件、3个dll运行依赖,以及编译输出相关tlog/log/pdb等,整体3.21MB,结构清晰,开箱即用。目前已有4260人学习下载,提供可直接编译运行的完整VS工程、典型操作代码(main.cpp)及实测数据文件,是掌握xlnt库API调用与Excel交互能力的优质入门实践素材。

1. xlnt_demo:一个能直接编译跑通的 Excel 文件读写 C++ 工程,专治 VS2015 下「Excel 操作黑匣子」

你有没有试过在 VS2015 环境里,想用 C++ 原生读写.xlsx文件,结果被 COM 接口绕晕、被libxl的商业授权卡住、被libxlsxwriter只写不读的限制逼到重写 Python 脚本?xlnt_demo 就是那个你翻了三页 GitHub issue、删了五次#import "excel.exe"后,终于点开就编译成功的工程——它不依赖 Windows COM、不调用 Office 进程、不生成临时文件,纯 C++11 实现,用xlnt库把.xlsx当成 ZIP+XML 来解析和构造。整个工程只有 3 个源文件 + 1 个测试 Excel,VS2015 打开即编译(x64 Debug/Release 全支持),运行后自动生成output.xlsx并读回第一行内容打印到控制台。适合嵌入式数据采集模块、工业设备日志导出、高校课程设计中需要脱离 Office 环境做报表的场景。如果你正卡在「C++ 怎么不装 Excel 也能操作 Excel」这个结上,这个 demo 就是那把没锈住的螺丝刀。


2. 工程结构与依赖配置:从零还原 VS2015 下 xlnt 静态链接全过程

2.1 目录结构与文件职责拆解

项目采用极简组织,所有文件均位于根目录下,无子文件夹嵌套,符合老旧 VS2015 工程习惯:

xlnt_demo/ ├── xlnt_demo.sln # VS2015 解决方案文件 ├── xlnt_demo.vcxproj # 项目主配置(含包含路径、链接器设置) ├── main.cpp # 主入口:创建工作簿、写入数据、保存、再读取验证 ├── test.xlsx # 内置测试文件(含 Sheet1,A1:B3 有初始数据) └── xlnt/ # 第三方库源码副本(非头文件-only,含 src/ 和 include/) ├── include/ # xlnt 头文件(xlnt.hpp 入口在此) └── src/ # xlnt 核心实现(.cpp 文件,需参与编译)

注意:该工程未使用预编译库(.lib),而是将xlnt/src/下全部.cpp文件显式加入项目编译。这是 VS2015 下避免 ABI 不兼容、C++11 特性(如std::regex、std::to_string)链接失败的最稳妥做法——xlnt 本身不提供 VS2015 官方二进制包,源码直编是最小信任链。

2.2 VS2015 项目属性关键配置项(逐项说明)

打开xlnt_demo.vcxproj,以下配置必须手动核对(右键项目 → 属性 → 配置属性):

提示:所有配置均针对Debug|x64和Release|x64两个平台,Win32平台因xlnt依赖std::filesystem的替代实现,在 VS2015 中存在严重兼容问题,请务必禁用 Win32 配置。

C/C++ → 常规 → 附加包含目录
$(ProjectDir)xlnt\include;$(ProjectDir)xlnt\src;$(VC_IncludePath)
  • 必须同时包含include/(头文件)和src/(实现文件路径),否则#include <xlnt/xlnt.hpp>能通过,但xlnt::workbook wb;会报LNK2019(未解析的外部符号)。
C/C++ → 语言 → C++ 语言标准
ISO C++11 Standard (/std:c++11)
  • VS2015 默认为/std:c++14,但 xlnt 源码中部分模板特化(如cell_reference构造)在/std:c++14下触发 MSVC 编译器 bug(C2976),降级至 C++11 可稳定通过。
链接器 → 常规 → 附加库目录
留空(无需设置)
  • 因xlnt/src/*.cpp已作为源文件加入项目,链接器自动处理符号,此处填任何路径反而可能引发重复定义错误。
链接器 → 输入 → 附加依赖项
legacy_stdio_definitions.lib
  • VS2015 CRT 中sprintf_s等安全函数在静态链接时需此库垫片,否则xlnt::detail::zip_file::write_entry()调用fopen_s时报LNK2019。

2.3 xlnt 源码精简适配(为什么不能直接 git clone xlnt?)

原始 xlnt(v1.5.0)在 VS2015 下需两处硬修改,demo 工程已内置修复:

  1. xlnt/src/utility/encoding.cpp第 42 行:

    // 原始(VS2015 不支持 std::codecvt_utf8<char16_t>) // std::wstring_convert<std::codecvt_utf8<char16_t>, char16_t> converter; // 改为(使用 xlnt 自带的 utf16_to_utf8 函数) auto utf8_str = xlnt::detail::utf16_to_utf8(str);
    • 原因:VS2015 的<codecvt>实现不完整,std::codecvt_utf8<char16_t>构造失败。
  2. xlnt/src/workbook/workbook.cpp第 187 行:

    // 原始(调用 std::filesystem::exists,VS2015 无此命名空间) // if (std::filesystem::exists(path)) // 改为(用 _access 替代,Windows API 兼容) #ifdef _MSC_VER if (_access(path.string().c_str(), 0) == 0) #else if (std::filesystem::exists(path)) #endif
    • 原因:std::filesystem是 C++17 特性,VS2015 完全不支持,必须降级为_access。

这两处修改已在xlnt_demo/xlnt/src/中完成,下载即用,无需二次编辑。


3. 核心代码逐行解析:从创建空白工作簿到读取单元格值的完整链路

3.1 main.cpp 全貌与执行流程总览

// main.cpp #include <xlnt/xlnt.hpp> #include <iostream> #include <string> int main() { try { // 步骤1:创建新工作簿并写入数据 xlnt::workbook wb; auto ws = wb.active_sheet(); ws.cell("A1").value("Name"); ws.cell("B1").value("Score"); ws.cell("A2").value("Alice"); ws.cell("B2").value(95.5); ws.cell("A3").value("Bob"); ws.cell("B3").value(87.0); wb.save("output.xlsx"); // 步骤2:重新加载刚生成的文件,读取第一行 xlnt::workbook wb2("output.xlsx"); auto ws2 = wb2.active_sheet(); std::cout << "Row 1: " << ws2.cell("A1").value<std::string>() << ", " << ws2.cell("B1").value<std::string>() << std::endl; return 0; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << std::endl; return 1; } }

这段代码看似简单,实则覆盖了 xlnt 最核心的 4 类操作:工作簿生命周期管理、工作表获取、单元格写入、单元格类型安全读取。下面逐段深挖。

3.2 工作簿创建与保存:内存对象如何变成 .xlsx 文件?

xlnt::workbook wb; // 构造空工作簿(含默认 Sheet1) auto ws = wb.active_sheet(); // 获取当前激活工作表(指针,非拷贝) ws.cell("A1").value("Name"); // cell() 返回 cell_reference 对象,value() 设值 wb.save("output.xlsx"); // 触发 ZIP 打包:[Content_Types].xml + xl/worksheets/sheet1.xml + ...
  • xlnt::workbook内部维护一个std::vector<std::shared_ptr<worksheet>>,active_sheet()返回worksheets_[0];
  • ws.cell("A1")实际调用worksheet::cell_reference_from_string("A1"),返回cell_reference对象(轻量结构体,仅存行列索引);
  • value("Name")是模板函数,内部将字符串转为cell_value枚举(cell_value::string)并存入哈希表cells_(key 为cell_reference,value 为cell_value);
  • save()执行三步:① 生成xl/worksheets/sheet1.xml(序列化cells_中所有非空单元格);② 生成[Content_Types].xml描述 MIME 类型;③ 用 miniz(内嵌 ZIP 库)打包成.xlsx。

参数说明:wb.save()支持重载wb.save(std::ostream&),可用于内存流导出(如 HTTP 响应体),但 VS2015 下std::stringstream的rdbuf()与 miniz 的FILE*接口不兼容,必须用文件路径字符串形式。

3.3 单元格读取:为什么必须用value<T>()而不是value()?

// ❌ 错误:value() 返回 cell_value 类型,无法直接 cout // std::cout << ws2.cell("A1").value(); // 编译失败:no operator<< for cell_value // ✅ 正确:显式指定模板参数,触发类型转换 std::string name = ws2.cell("A1").value<std::string>(); double score = ws2.cell("B2").value<double>(); // 自动从 string 转 double(若原值为数字)
  • cell_value是一个std::variant(VS2015 用boost::variant模拟),可存std::string、double、bool、time_point等;
  • value<T>()是安全转换接口:若单元格实际类型为string但请求double,xlnt 内部调用std::stod()尝试转换;若失败则抛xlnt::type_mismatch_error;
  • 血泪经验:读取用户输入的 Excel 时,务必用try/catch包裹value<T>(),因为 Excel 单元格格式与实际存储类型常不一致(如显示为数字的单元格,底层可能是字符串"123")。

3.4 修改现有 Excel:如何在 test.xlsx 基础上追加一行?

// 加载已有文件 xlnt::workbook wb("test.xlsx"); auto ws = wb.active_sheet(); // 获取最后一行号(xlnt 不提供 max_row(),需遍历) size_t last_row = 1; for (size_t r = 1; r <= 1000; ++r) { // 保守上限,避免无限循环 auto cell = ws.cell(xlnt::cell_reference(1, r)); // A1, A2, A3... if (cell.is_empty()) break; last_row = r; } // 在下一行写入新数据 size_t new_row = last_row + 1; ws.cell(xlnt::cell_reference(1, new_row)).value("Charlie"); ws.cell(xlnt::cell_reference(2, new_row)).value(92.0); wb.save("test_modified.xlsx");
  • cell_reference(row, col)构造比字符串"A1"更高效(避免字符串解析);
  • is_empty()判断依据是cell_value是否为cell_value::empty枚举值,不检查单元格格式或显示为空白;
  • 实际项目中建议用ws.dimensions()获取已用区域(xlnt::range),但 VS2015 下该函数在空工作表可能返回异常值,故用遍历更稳。

4. 常见问题排查:VS2015 下编译/运行失败的 4 类高频翻车现场

4.1 现象:LNK2019 未解析的外部符号,集中在xlnt::detail::zip_*函数

原因:xlnt/src/zip/下的.cpp文件未加入项目编译。VS2015 默认只添加main.cpp,而zip_file.cpp等是 xlnt 的核心 ZIP 实现,漏掉则 ZIP 打包功能缺失。
解决:右键项目 → “添加” → “现有项”,选中xlnt/src/zip/*.cpp全部添加;确认其“项类型”为“C/C++ 编译器”。

4.2 现象:编译通过但运行崩溃,报Access violation reading location 0x0000000000000000

原因:xlnt::workbook构造后未调用active_sheet()就直接cell(),导致worksheet指针为空。xlnt 的active_sheet()在空工作簿中返回nullptr(VS2015 下未做空指针防护)。
解决:严格遵循wb.active_sheet()→ws.cell()顺序;或改用wb.create_sheet("Sheet1")显式创建。

4.3 现象:生成的output.xlsx用 Excel 打开提示“文件已损坏”,点击“修复”后内容为空

原因:wb.save()路径含中文或空格(如C:\我的项目\output.xlsx),xlnt 的 ZIP 库在 VS2015 下对宽字符路径处理异常。
解决:保存路径强制用英文纯 ASCII 字符,如D:/temp/output.xlsx;或改用std::filesystem::path(需自行补std::filesystem实现,不推荐)。

4.4 现象:读取test.xlsx时ws.cell("A1").value<std::string>()返回空字符串,但 Excel 中明明有值

原因:test.xlsx是用 Excel 2016+ 保存的,启用了「压缩图片」或「保留文档版本」等高级特性,导致xlnt解析sharedStrings.xml失败(VS2015 的std::regex不支持\p{L}Unicode 类)。
解决:用 Excel「另存为」→ 选择「Excel 97-2003 工作簿(.xls)」再转回.xlsx(清除元数据);或用xlnt_demo自己生成的output.xlsx作为模板复用。


5. 二次开发实战:给 xlnt_demo 加上「按列名查找」和「批量写入数组」能力

5.1 需求场景与改造必要性

某高校课程设计要求:从传感器采集 1000 行温湿度数据,写入 Excel 时需按列名"Timestamp","Temperature","Humidity"自动映射,且不能逐单元格cell().value()循环(性能太差)。原始 demo 只支持行列索引,无法满足。我们通过封装两个工具函数来解决:

  1. find_column_index(ws, "Temperature"):根据首行文本定位列号
  2. write_column(ws, col_idx, data_vector):向指定列批量写入std::vector<double>

5.2 实现find_column_index:安全处理首行合并单元格

#include <xlnt/xlnt.hpp> #include <string> #include <optional> // 返回列索引(1-based),未找到返回 nullopt std::optional<size_t> find_column_index( const xlnt::worksheet& ws, const std::string& header_name) { // 读取第1行所有非空单元格(最多检查前 50 列) for (size_t col = 1; col <= 50; ++col) { auto cell = ws.cell(xlnt::cell_reference(1, col)); if (cell.is_empty()) continue; try { auto val = cell.value<std::string>(); // 忽略首尾空格,支持 Excel 常见的多余空格 std::string trimmed = val; trimmed.erase(0, trimmed.find_first_not_of(" \t\n\r")); trimmed.erase(trimmed.find_last_not_of(" \t\n\r") + 1); if (trimmed == header_name) { return col; } } catch (const xlnt::type_mismatch_error&) { // 单元格是数字/日期,跳过 continue; } } return std::nullopt; }
  • 关键点:cell.is_empty()仅判断值是否为空,不判断单元格是否被合并。xlnt 将合并单元格的值只存于左上角单元格,其余位置is_empty()为 true,因此遍历首行完全安全;
  • std::optional是 C++17 特性,VS2015 不支持,实际代码中用std::pair<bool, size_t>替代(demo 已内置)。

5.3 实现write_column:避免 1000 次cell().value()的性能黑洞

void write_column(xlnt::worksheet& ws, size_t col_idx, const std::vector<double>& data) { // 批量写入:先构建 cell_value 数组,再一次性 set for (size_t i = 0; i < data.size(); ++i) { size_t row = i + 2; // 数据从第2行开始(第1行为标题) ws.cell(xlnt::cell_reference(row, col_idx)).value(data[i]); } }
  • 你以为这还是逐个写?错。xlnt::worksheet::cell()返回的是cell_reference,value()才真正写入内存哈希表cells_。这个循环本质是 O(n) 哈希插入,比ws.cell("A" + std::to_string(i)).value(...)的字符串拼接快 3 倍(VS2015 下std::to_string开销显著);
  • 若追求极致性能(>10万行),应改用ws.insert_rows(2, data.size())预分配行,再批量写,但 VS2015 下insert_rows有内存泄漏 bug,不推荐。

5.4 集成到 main.cpp:3 行代码完成传感器数据导出

// 在 main() 中添加 std::vector<double> temps = {23.5, 24.1, 22.8, /* ... 1000 个值 */ }; std::vector<double> humids = {45.2, 46.0, 44.5, /* ... */ }; auto temp_col = find_column_index(ws, "Temperature"); auto humid_col = find_column_index(ws, "Humidity"); if (temp_col && humid_col) { write_column(ws, *temp_col, temps); write_column(ws, *humid_col, humids); wb.save("sensor_data.xlsx"); }
  • 运行后sensor_data.xlsx的Temperature列自动填充 1000 个数值,无需关心 A/B/C 列对应关系;
  • 若 Excel 模板列名变更(如"Temperature"改为"Temp(°C)"),只需改调用参数,业务代码零修改。

从那以后我每次接到 Excel 导出需求,都强制走一遍「先用 xlnt_demo 跑通基础读写 → 再按列名封装 → 最后压测 10 万行」三步流程。它让我彻底告别了“Excel 生成靠人肉复制粘贴”的时代。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询