ARTICLE DETAIL

资讯详情

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

C++实现Excel报表图片插入:libxlsxwriter结合CMake与VS2019实战指南

C++实现Excel报表图片插入:libxlsxwriter结合CMake与VS2019实战指南 最近做一个小工具需要在 C 程序里把设备截图自动塞进 Excel 报表里。查了一圈方案最后锁定了 libxlsxwriter 这个库配合 CMake 和 VS2019 把整个工程跑通过程不算复杂但坑确实不少。这篇文章就把完整实践过程写出来包括环境搭建、核心代码、参数含义以及我在实际编译运行中踩过的几个典型问题给同样在折腾 C 写 Excel 的朋友做个参考。libxlsxwriter 是一个用 C 语言实现的 Excel 文件生成库支持写入单元格、格式化、公式、图表以及我今天要重点讲的图片插入功能。它生成的是 xlsx 格式兼容 Excel 2007 以上的版本。选择它而不是直接用 Excel COM 或者 Python 脚本原因很简单我需要在一个纯 C 桌面程序里无界面地完成报表生成COM 依赖本机装了 OfficePython 方案又多了个解释器依赖libxlsxwriter 编译成静态库后扔到程序里就能用干净利落。整个项目我用的组合是 VS2019 社区版 CMake 3.20 libxlsxwriter 最新 release 源码。VS2019 的 CMake 支持已经很成熟直接用 IDE 打开 CMakeLists.txt 就能识别工程不需要额外安装插件对只装了 VS 的朋友来说非常友好。下面按步骤展开。1. 项目思路与方案选型1.1 libxlsxwriter 的核心能力libxlsxwriter 本质上是一个写 xlsx 文件的 C 库不依赖任何 Office 软件。它的工作方式非常直观先创建一个 workbook 对象再往里面添加 worksheet然后像填表格一样往单元格里写数据最后调用 workbook_close 把文件写到磁盘。整个过程像是在操作一个内存中的表格模型所有内容在 close 时统一序列化成 xlsx 包。它支持的图片插入接口主要有两组。一组是worksheet_insert_image把图片放在某个单元格的“锚点”上图片浮在单元格表面可以调整偏移量和缩放比例另一组是worksheet_embed_image这是新版本加入的功能把图片真正嵌入单元格内部类似 Excel 365 里的“插入单元格内图片”功能图片会随单元格大小变化而缩放。我们报表场景里更多是第一种图片作为一个独立的视觉元素放在指定位置自由度更高。图片格式方面它内部支持 PNG、JPEG、BMP、GIF非动画、TIFF、WMF、EMF 等常见格式。不过要注意虽然可以插入 GIF但 Excel 本身只显示 GIF 的第一帧动画效果不会保留。1.2 为什么不用其他方案在确定这个方案之前我认真比较过几个常见的 C 写 Excel 图片的路径。表格如下方案优点缺点适用场景Excel COM 组件通过 OLE功能最全图片控制精细能设置锚点属性必须装 Office启动慢跨平台无望C 调用接口繁琐本机已装 Office、对排版要求极高的场景Python openpyxl / xlsxwriter生态成熟API 友好需要打包 Python 运行时或转成 exe性能一般脚本化快速处理手写 XML 打进 zip完全可控无第三方依赖工作量巨大Excel 的 OOXML 规范细节多一个标签写错文件就打不开学习研究和极端定制场景libxlsxwriter编译成静态库后零外部依赖性能好API 足够用C 接口风格字符串处理需要自己注意生命周期C 桌面程序嵌入报表生成功能对我这个项目来说关键决策点有两个一是目标机器不能保证安装 Office所以 COM 方案直接出局二是程序本身就是 C 的不想为了报表功能额外引入 Python。libxlsxwriter 正好卡在这个位置上静态编译后整个库才几百 KB 级别非常舒服。2. 环境准备源码编译与 CMake 工程搭建2.1 获取 libxlsxwriter 源码与依赖libxlsxwriter 的源码托管在 GitHub 上直接搜项目名就能找到。下载最新 release 的源码包解压后你会看到目录里有 CMakeLists.txt也就是说项目本身支持 CMake 构建省了很多事。在 Windows 上编译它需要注意一个隐藏依赖zlib。xlsx 文件本质是一个 zip 压缩包libxlsxwriter 内部要用 zlib 来做压缩。CMake 配置时会自动查找 zlib如果你的系统里没有构建就会失败。我用的是 vcpkg 来装依赖一条命令就能搞定vcpkg install zlib:x64-windows如果你不想引入 vcpkg 这么重的工具也可以用 CMake 的 FetchContent 直接拉 zlib 源码但那样整个构建流程会复杂不少。个人建议Windows 上用 vcpkg 管理依赖是最省心的方式。2.2 编译并安装 libxlsxwriter拿到源码后编译步骤很简单。在源码根目录下执行cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE[vcpkg路径]/scripts/buildsystems/vcpkg.cmake -DBUILD_SHARED_LIBSOFF cmake --build build --config Release cmake --install build解释一下这几个参数的含义。-DBUILD_SHARED_LIBSOFF表示生成静态库这样最终链接进 exe 后不依赖额外的 dll发布时不会出现“找不到 DLL”的问题我个人强烈建议静态编译。--install会把头文件和库文件复制到一个统一的安装目录默认在C:\Program Files (x86)\libxlsxwriter下如果不想装到系统目录可以加-DCMAKE_INSTALL_PREFIXD:/third_party/libxlsxwriter指定一个自定义路径。编译过程如果顺利几分钟就结束你会得到xlsxwriter_static.lib或类似名字的静态库文件以及一个 include 目录里面有xlsxwriter.h。这个头文件是唯一的入口头文件所有 API 声明都在里面。2.3 在 VS2019 中创建 CMake 工程VS2019 对 CMake 工程的支持已经非常完善不需要像老版本那样手动生成 vcxproj。具体操作是新建项目选择“CMake”类型然后在 CMakeLists.txt 里写构建配置。我的 CMakeLists.txt 是这样写的cmake_minimum_required(VERSION 3.15) project(ExcelImageDemo) set(CMAKE_CXX_STANDARD 14) find_package(OpenSSL REQUIRED) # 仅当需要加密扩展时才要一般不需要可注释掉 add_executable(excel_image_demo main.cpp) # 指定 libxlsxwriter 头文件路径和库路径 target_include_directories(excel_image_demo PRIVATE ${LIBXLSXWRITER_INCLUDE_DIR}) target_link_libraries(excel_image_demo PRIVATE ${LIBXLSXWRITER_LIB} ${ZLIB_LIBRARIES})一个小技巧是我习惯在 CMakeLists.txt 开头用set(LIBXLSXWRITER_ROOT D:/third_party/libxlsxwriter)这样的变量把第三方库路径固定下来然后用target_include_directories指向它。这样整个工程目录整洁别人接手时也容易看出依赖在哪。重点提醒一下VS2019 的 CMake 集成默认会生成 debug 版本的构建目录如果你前面编译的是 Release 版本的 libxlsxwriter链接时会出现运行库不匹配的警告甚至报错。我踩过一次这个坑后面第 4 部分会详细说。3. 核心代码实现插入图片的完整链路3.1 最小可运行示例先上一个最小可运行的代码在你自己的 main.cpp 里写上这段编译通过且能生成带图片的 xlsx 文件整个链路就算跑通了。#include xlsxwriter.h #include iostream int main() { // 1. 创建 workbook参数是输出文件名 lxw_workbook* workbook workbook_new(report_with_image.xlsx); if (workbook nullptr) { std::cerr Failed to create workbook std::endl; return -1; } // 2. 添加一个 worksheet lxw_worksheet* worksheet workbook_add_worksheet(workbook, 设备截图); // 3. 写点文字内容 worksheet_write_string(worksheet, 0, 0, 设备运行状态, nullptr); worksheet_write_string(worksheet, 1, 0, 截图如下, nullptr); // 4. 插入图片行索引 2列索引 0也就是 C3 单元格附近 lxw_image_options options {0}; options.x_offset 20; // 距离单元格左边 20 像素 options.y_offset 10; // 距离单元格顶部 10 像素 lxw_error err worksheet_insert_image_opt(worksheet, 2, 0, device_screenshot.png, options); if (err ! LXW_NO_ERROR) { std::cerr Insert image error code: err std::endl; } // 5. 关闭 workbook此时文件才真正写入磁盘 err workbook_close(workbook); if (err ! LXW_NO_ERROR) { std::cerr Close workbook error code: err std::endl; return -1; } std::cout Report generated successfully! std::endl; return 0; }这段代码的核心逻辑只有五个步骤建 workbook、加 worksheet、写文字、插图片、关文件。注意workbook_close一定要调用它的作用不仅是释放内存更是把内存中的表格数据序列化并写入磁盘。如果忘了调你会得到一个 0 字节的文件或者干脆没有文件生成。worksheet_insert_image_opt和worksheet_insert_image的区别在于前者多了一个lxw_image_options参数可以用来设置图片位置偏移、缩放比例、超链接等。即使不需要这些特性也建议用带 options 的版本因为结构体里所有字段默认值为 0不会影响基本插入行为。3.2 控制图片的位置、尺寸与超链接报表里图片往往不是随手一放就行经常需要精确控制图片相对单元格的位置和显示尺寸。lxw_image_options结构体里常用字段如下字段类型作用x_offsetint32_t图片左边相对单元格左边框的像素偏移y_offsetint32_t图片顶部相对单元格顶部边框的像素偏移x_scaledouble水平方向缩放倍数1.0 表示原始尺寸y_scaledouble垂直方向缩放倍数urlchar*图片的超链接地址descriptionchar*图片的替代文字辅助功能使用举个例子如果你想插入一张原始宽度 800 像素的图片但希望它在表格里显示为 400 像素宽同时保持宽高比可以把x_scale和y_scale都设为 0.5lxw_image_options options {0}; options.x_scale 0.5; options.y_scale 0.5; options.x_offset 0; options.y_offset 0; worksheet_insert_image_opt(worksheet, 5, 1, wide_screenshot.png, options);这里有一个容易混淆的地方x_offset和y_offset的单位是像素而不是单元格。很多初学者以为把x_offset设成 3 就是偏移 3 列其实不是。在默认 DPI 下Excel 的一行高度大约 20 像素一列宽度大约 64 像素所以如果你想把图片精确放到某个单元格里的特定位置需要换算成像素值。想让图片带超链接只需要设置options.url这个功能在生成带附件的报表时很好用比如截图旁边放一个“查看大图”的链接options.url https://example.com/appendix/report_full.png;3.3 从内存缓冲区插入图片大部分场景下图片已经在磁盘上了直接传路径就行。但有些时候图片是程序动态生成的裁剪结果或者从网络下载后直接存在于内存 buffer 中这时候可以把文件先落到磁盘再插入但那样多一次 IO而且临时文件管理也麻烦。libxlsxwriter 提供了worksheet_insert_image_buffer系列接口直接从内存 buffer 插入图片。#include xlsxwriter.h #include vector #include fstream int main() { std::vectorunsigned char image_data; std::ifstream file(device_screenshot.png, std::ios::binary); if (file) { image_data.assign(std::istreambuf_iteratorchar(file), std::istreambuf_iteratorchar()); file.close(); } else { return -1; } lxw_workbook* workbook workbook_new(buffer_image_demo.xlsx); lxw_worksheet* worksheet workbook_add_worksheet(workbook, Sheet1); // 指向图片数据的指针以及数据长度单位是字节 lxw_error err worksheet_insert_image_buffer(worksheet, 0, 0, image_data.data(), image_data.size()); if (err ! LXW_NO_ERROR) { std::cerr Error: err std::endl; } workbook_close(workbook); return 0; }这个接口有一点需要特别注意libxlsxwriter 内部需要根据文件头判断图片格式所以 buffer 里的数据必须完整不能是流式压缩数据的一部分。我曾经踩过坑从网络流里分块读取 PNG 数据时没有读完整结果插入后 Excel 打开提示文件损坏。另外worksheet_insert_image_buffer没有带lxw_image_options的重载版本如果你需要设置缩放或偏移只能先写到本地临时文件再用worksheet_insert_image_opt来插入。3.4 多 Sheet 与批量插入的实战模式实际报表很少只有一张图片。我这次的需求是一张 Sheet 对应一台设备每个 Sheet 里放该设备的运行截图和关键参数。这种场景下核心逻辑是循环创建 Sheet 并插入图片。std::vectorstd::string device_ids {DEV001, DEV002, DEV003}; std::vectorstd::string screenshot_files {shot1.png, shot2.png, shot3.png}; lxw_workbook* workbook workbook_new(multi_device_report.xlsx); for (size_t i 0; i device_ids.size(); i) { // 每个设备单独一个 Sheet名字最长 31 个字符且不能包含特殊字符 std::string sheet_name 设备_ device_ids[i]; lxw_worksheet* worksheet workbook_add_worksheet(workbook, sheet_name.c_str()); worksheet_write_string(worksheet, 0, 0, 设备ID, nullptr); worksheet_write_string(worksheet, 0, 1, device_ids[i].c_str(), nullptr); worksheet_write_string(worksheet, 2, 0, 状态截图, nullptr); // 给图片加个边框方便查看这里用单元格格式实现更优雅 lxw_format* border workbook_add_format(workbook); format_set_border(border, LXW_BORDER_THIN); lxw_image_options options {0}; options.x_offset 4; options.y_offset 4; options.x_scale 0.8; options.y_scale 0.8; worksheet_insert_image_opt(worksheet, 3, 0, screenshot_files[i].c_str(), options); } workbook_close(workbook);这种循环模式下我建议把“图片固定放在第 4 行、A 列偏移 4 像素缩放 0.8 倍”这些参数定义成常量统一管理。后续如果产品经理要求调整图片位置只需要改一处不用在循环里翻来找去。开发过程中我体会最深的就是报表格式这种东西需求永远在变参数化设计能极大减少改动成本。4. 常见问题与避坑指南4.1 链接错误LNK2019 / LNK2001 解决实录这是我在 VS2019 里遇到的第一个大坑。明明按照示例代码写了编译没问题链接时却报一大堆LNK2019: 无法解析的外部符号指向workbook_new、worksheet_insert_image这些函数。排查下来的原因有两个供参考。第一个原因是库文件没有正确链接或者链接顺序不对。VS2019 的链接器对库的顺序有要求静态库的依赖必须放在被依赖库的后面。如果你用target_link_libraries(excel_image_demo PRIVATE xlsxwriter_static.lib zlib.lib)这样的写法顺序是没问题的但如果你手动在项目属性里加依赖就容易把 zlib 放在前面导致 zlib 的符号解析失败。第二个原因是调试和发布的配置混用。有些教程里编译的是 Debug 版本的 libxlsxwriter但 CMake 配置时CMAKE_BUILD_TYPE却用了 Release两边运行库设置不一致比如一头是/MT另一头是/MD就会出现莫名其妙的链接失败。解决办法是保持两边配置一致我后来的做法是直接用 CMake 同时生成 Debug 和 Release 两个构建目录各用各的库。4.2 图片插入成功但 Excel 打开报错或显示不出来图片插进去了代码也没报错但 Excel 打开文件时提示“发现不可读取的内容”或者图片位置显示空白。这个问题我排查了很久最终定位到两个可能原因。第一个是图片路径中的中文或特殊字符。libxlsxwriter 内部对文件路径的处理在 Windows 上对中文支持并不完善如果路径里有中文目录名比如C:\Users\张三\Desktop\截图.png读取图片文件时可能会失败但并不会返回错误码只是最终生成的 xlsx 里嵌入了一个损坏或空的图片数据。解决方法是插入前先用标准库或 Windows API 把图片完整读入内存然后用worksheet_insert_image_buffer插入。这样图片数据已经在内存里了完全绕开路径问题。第二个原因是图片格式。我一开始用的是一张 .ico 格式的图标文件代码运行没报错但 Excel 打开后图片区域是空白的。查文档才发现它虽然支持很多格式但对 ico 这类格式没有任何保证。后来我统一在程序里把图片转成 PNG 再插入就再也没出过这种问题。4.3 生成的 xlsx 文件大小为 0 字节或文件损坏这个问题的原因非常明确调用了workbook_new之后没有调用workbook_close就退出了程序。因为 libxlsxwriter 的工作机制是在内存中维护整个工作簿模型只有workbook_close时才会把所有数据打包写入文件。哪怕你已经调了worksheet_insert_image只要没有 close就不会有任何数据落盘。从经验出发我建议用 RAII 的方式管理 workbook 指针比如自己写一个简单的封装类在析构函数里确保 close 被调用。这样即使中途异常退出文件也不会损坏漏写。4.4 CMake 配置相关的坑配置 CMake 时最容易碰到的错误是CMake Error: Could not find a package configuration file provided by xlsxwriter这个报错是因为 CMake 的find_package找不到库的配置文件。libxlsxwriter 并没有提供官方 CMake config 文件所以不能直接用find_package(xlsxwriter)只能通过target_include_directories和target_link_libraries手动指定路径。写 CMakeLists.txt 时用绝对路径或变量定义的方式明确指向你编译好的 include 目录和 lib 文件才是最稳的做法。另外一个相关问题是 VS2019 自带的 CMake 版本可能比较旧。如果你用 VS2019 直接打开 CMakeLists.txt还需要在 VS 的“工具 - 选项 - CMake”里确认 CMake 路径建议选择你系统里安装的较新版本 CMake比如 3.20 以上而不是 VS 内置的那个否则有些新写法会不被识别。4.5 运行时找不到 DLL这个问题只在你编译的是动态库版本时出现。如果你按照我说的用了静态库这一步基本可以跳过。但如果你确实用了共享库构建程序启动时会提示找不到xlsxwriter.dll或zlib.dll。解决办法有两种把 dll 复制到 exe 所在目录或者把 dll 所在路径加入系统 PATH。从部署角度讲我还是强烈建议静态编译虽然 exe 会大一点点但省去各种 dll 缺失的烦恼尤其在给客户部署时尤为重要。5. 关于图片性能与报表体积的优化经验5.1 控制图片体积避免 Excel 臃肿如果你要插入几十张设备截图每张截图都是几 MB 的 PNG那么生成的 xlsx 文件会非常庞大Excel 打开会卡顿甚至直接无响应。我发现最好的做法是在程序里先对图片做压缩处理再插入。libxlsxwriter 本身不提供图片缩放处理功能它只是把图片数据原样嵌入。所以需要程序自己先做预处理。可以用 C 配合 stb_image 和 stb_image_write 这类轻量库来做图片解码、缩放、重新编码也可以先用外部工具统一把截图压缩成 WebP 或 JPEG 再插入。我的实测结果是一张 1920x1080 的 PNG 截图大约 1.5MB如果先缩放到 640x360 并转成 JPEG 质量设 80体积能降到 60KB 左右生成的 Excel 打开速度明显加快。5.2 大批量插入时的内存占用上面提到libxlsxwriter 在内存中维护整个工作簿所以如果你一次插入 1000 张图片内存占用会线性上升。每张图片在内存里都保存着原始数据和嵌入后的关系记录1000 张图片可能轻松吃掉 200~300MB 内存。这个在数据量不大的场景下可以忽略但如果你是批量处理工具建议分批写入、及时关闭比如每 100 张数据生成一个独立的 xlsx 文件或者用流式逻辑控制单次工作簿内的图片数量。5.3 单元格行高列宽的配合图片插入后如果单元格太小图片默认不会自动调整单元格大小图片会覆盖在相邻单元格上。虽然这不影响图片显示但如果你希望图片完全在一个单元格区域内需要程序里主动设置行高和列宽。代码很简单worksheet_set_row(worksheet, 3, 60); // 第 4 行高度设为 60 像素 worksheet_set_column(worksheet, 0, 0, 20, nullptr); // A 列宽度设为 20这样图片放在第 4 行 A 列时单元格看起来会更协调。不过要注意行高列宽的单位和图片偏移的像素计算方式不同具体要多调几次找到合适的值。6. 更进一步的玩法与单元格格式和公式结合6.1 给带图片的报表加入汇总数据图片插进去了报表要更实用还得有数据。libxlsxwriter 提供了完整的公式支持可以在单元格里写求和、平均值等 Excel 函数。lxw_workbook* workbook workbook_new(report_with_formula.xlsx); lxw_worksheet* worksheet workbook_add_worksheet(workbook, 汇总); lxw_format* bold workbook_add_format(workbook); format_set_bold(bold); worksheet_write_string(worksheet, 0, 0, 设备名, bold); worksheet_write_string(worksheet, 0, 1, 运行时长(小时), bold); worksheet_write_string(worksheet, 1, 0, DEV001, nullptr); worksheet_write_number(worksheet, 1, 1, 120.5, nullptr); worksheet_write_string(worksheet, 2, 0, DEV002, nullptr); worksheet_write_number(worksheet, 2, 1, 87.2, nullptr); // 在 B4 写一个 SUM 公式 worksheet_write_formula(worksheet, 3, 1, SUM(B2:B3), nullptr); // 在图片旁边放公式结果区域 worksheet_insert_image(worksheet, 5, 0, device_screenshot.png); workbook_close(workbook);因为图片本质上是一个浮层它不会挡住单元格里的数据所以你可以把图片放在数据区域的旁边形成“左边数据、右边截图”的布局。这个设计在处理设备巡检报表时特别好用一张 Sheet 既能看数字又能看图。6.2 多 Sheet 间的联动libxlsxwriter 支持在公式中跨 Sheet 引用比如SUM(Sheet2!A1:A10)。如果你需要按设备生成多个 Sheet最后加一个汇总 Sheet 把所有数据归拢用这个特性非常合适。我在实际项目里就是这样做的每个设备的 Sheet 里既有数据也有截图最后的汇总 Sheet 用workbook_add_worksheet(workbook, 汇总)创建公式引用各设备 Sheet 的关键单元格整体报表逻辑清晰。6.3 利用条件格式高亮异常状态如果设备状态有异常希望在报表里醒目标出可以用条件格式。libxlsxwriter 对条件格式的支持有一定范围常用的“数值区间高亮”“文本包含高亮”都能做。虽然和图片插入没有直接关系但结合起来效果非常好——异常的状态数字会变红旁边的截图又能直观展示现场情况整个报表的实用价值一下拉满。7. 对整个方案的最终评价与改进建议7.1 libxlsxwriter 的局限在哪里再好的工具也有短板。libxlsxwriter 目前不支持读取和修改已有的 xlsx 文件它只能从零创建。如果你有“打开一个现有模板往里面填图片”的需求这个库就无能为力了。另外它的内存模型决定了不适合超大文件的生成如果你要写包含数万行数据的报表可能需要考虑其他方案或者分批输出。图片插入方面的限制主要是两个一是对动态图片格式如 GIF 动画支持有限二是没有图片裁剪能力图片只能整体缩放。如果你需要精确的裁剪功能需要在插入前用图像处理库预处理。7.2 为什么这个组合仍然值得推荐尽管有这些局限C libxlsxwriter CMake VS2019 这个组合在“程序自动生成带图片的 Excel 报表”这个细分领域里仍然是效率很高的选择。它不需要装 Office不依赖 Python 运行时编译产物干净API 简单清晰一个 500 行的工程就能完全掌握核心用法。而且 CMake VS2019 的组合让工程配置非常直观不管是自己用还是给团队维护都容易上手。我这次做完后最大的感受是libxlsxwriter 的文档其实写得相当仔细每个接口都有示例代码。真正的坑往往不在库里而在环境配置和图片格式处理上。你把环境配好、把图片规范定好后面就是纯粹的填表逻辑速度会非常快。7.3 后续扩展方向如果你是在做类似的项目我建议后续往这几个方向扩展一是封装一个 C 的报表类把 “创建 workbook、添加 sheet、插入图片” 这几个高频操作统一管理起来二是加入图片池机制相同的图片只插入一次通过引用复用进一步压缩报表体积三是在 CMake 层面把 libxlsxwriter 作为 FetchContent 依赖自动拉取并编译新同事拉代码后一条命令就能构建不用再手动装库。根据我个人经验把这几个点做好之后这个报表生成模块就能稳定地嵌在各种桌面工具、自动化脚本里成为真正意义上的“基础设施”。如果只是零散地写几个 API 调用后面每次需求变更都要重新翻文档那维护成本就会高很多。
返回列表