1. 为什么我建议在 Windows 上直接用 VS2022 构建 ZXing C++
先聊点背景。ZXing 这个库,做扫码相关开发的人应该都不陌生,Java 版用得最多,但 C++ 移植版的价值一直被低估。移动端、桌面端、甚至服务端的图像识别场景里,C++ 版可以直接嵌入现有程序,不依赖 JVM,内存和体积都可控。不过问题在于,C++ 版的构建体验和 Java 版完全两个世界,尤其对 Windows 开发者来说,CMake 配置、依赖管理、静态库导出这些环节,每一步都可能卡住。
我在实际项目里把 ZXing C++ 集成到 Windows 桌面程序时,踩遍了构建相关的坑。最开始的坑是拿 VS2022 直接打开根目录的 CMakeLists.txt,结果一堆接口找不到、符号不匹配,后来才发现问题出在 CMake 预设和依赖查找上。这篇文章就围绕 VS2022 环境下的 CMake 配置和构建过程,把完整流程、参数含义、遇到的具体报错全部捋一遍。
适合谁看?想在 Windows 上把 ZXing C++ 用起来的 C++ 开发者,尤其是用 CMake + VS2022 组合、又不想折腾 MinGW 和 vcpkg 的人。也适合那些已经在用 ZXing C++ 但构建时依赖不干净、链接老出问题的朋友,里面的排查思路应该能帮你少走弯路。
2. 构建前的整体思路:核心库和测试模块要分开对待
ZXing C++ 的仓库结构很清晰,但第一次接触的人容易被目录里的多个 CMakeLists 弄晕。根目录的 CMakeLists.txt 是总入口,核心源码都在 core 目录下,而 zxing 目录是封装层,里面是面向业务的高层 API。构建策略上,我建议分两层理解:先明确你要的是纯核心库还是带封装层的完整库,再决定怎么配置 CMake 参数。
2.1 核心库和封装层的角色划分
核心库本质上是纯算法实现,包含条码格式检测、解码器、图像二值化、纠错算法等模块,但它不负责你从相机或者文件里读到的具体数据格式。封装层则处理图像源适配、多格式统一入口、结果类型透出等事务,实际业务代码基本都只跟封装层打交道。
构建时如果只需要解码功能并自行管理输入源,只编译 core 就够;如果想把文件路径、像素缓冲、图像灰度数据直接丢给库去处理,就需要把 zxing 目录也编进来。大多数新手项目建议直接用后者,节省不少对接成本。
2.2 不同使用场景下 CMake 开关的选择
ZXing C++ 提供了一组 CMake 选项,默认值已经被维护者调得比较合理,但并不是每个默认值都适合 Windows 静态构建。下面是我实测下来比较稳的一组配置:
option(BUILD_SHARED_LIBS "Build shared libraries" OFF) option(BUILD_TESTING "Build tests" OFF) option(BUILD_EXAMPLES "Build examples" OFF) option(ZXING_READERS "Build readers" ON) option(ZXING_WRITERS "Build writers" ON) option(ZXING_EXPERIMENTAL_API "Build experimental API" OFF)关键点在于 BUILD_SHARED_LIBS。如果你只是做一个内部工具或插件模块,静态库可以避免 DLL 随包分发的麻烦;但如果你的程序里多个模块都要加载 ZXing,且模块之间需要共享同一份库实例,建议开成 ON,减少代码段重复加载的问题。
还有 ZXING_EXPERIMENTAL_API 这个开关,默认 OFF。如果你需要一些还没进入稳定接口的条码类型支持,比如某些新零售里的特殊码制,可以开启看看,但接口变化频繁,生产环境慎用。
2.3 为什么不用 vcpkg 来省事
肯定有人问:vcpkg 一条 install 命令不就能装好吗?确实方便,我也用过,但后来放弃了,原因有两个。第一,vcpkg 拉取的是固定版本源码,如果仓库更新了新码制或者修复了解码 bug,你没法快速跟进,必须等 vcpkg 端口更新。第二,vcpkg 默认构建参数和项目实际需求有偏差,比如你要定制启用的码制列表,vcpkg 直装的二进制就没法做裁剪,体积和性能都吃亏。
所以我最终采用的是源码直编方案:把 ZXing C++ 源码放进项目工程的 third_party 目录里,通过 add_subdirectory 方式参与构建,这样既能跟随上游源码更新,又能在同一个 CMake 工程里统一管理所有第三方依赖。
3. VS2022 环境下 CMake 配置的完整实操拆解
这一节是全文重点,直接给可以照抄的配置流程。我会按顺序列出操作步骤,并解释每一步背后的原因,避免你在复制粘贴后不知道为什么出了问题。
3.1 编译器、CMake 版本与生成器的选择
VS2022 自带的 CMake 版本一般会跟随 Visual Studio 更新,但我建议单独安装最新稳定版 CMake,避免某些老版本的 FindXXX 模块在新源码下解析失败。我当前环境是 VS2022 17.8 版本、CMake 3.28、Windows 10 SDK 10.0.19041。
生成器选择上,如果你是通过 VS2022 打开 CMake 工程,它会默认使用 Visual Studio 17 2022 生成器,配合 MSBuild 构建。如果你更习惯命令行,也可以直接用:
cmake -S . -B build -G "Visual Studio 17 2022" -A x64注意 -A x64 必须显式指定。如果你也和我一样用的默认 Win32 平台,后面链接阶段会报一堆 LNK2019 之类的错误,因为 ZXing C++ 的源码在 32 位和 64 位下的某些类型长度假设不一样,混用必然出问题。
3.2 通过 add_subdirectory 嵌入到现有工程的配置方法
假设你的工程结构是这样的:
MyApp/ ├─ CMakeLists.txt ├─ src/ └─ third_party/ └─ zxing-cpp/ ├─ CMakeLists.txt ├─ core/ └─ zxing/在 MyApp/CMakeLists.txt 里加一行:
add_subdirectory(third_party/zxing-cpp)这时 ZXing C++ 就会把自己的 target 导入到当前 CMake 作用域中。默认产生的主要 target 有 ZXingCore 和 ZXing,前者对应核心库,后者对应封装层。业务代码里直接 link ZXing 就行,因为封装层已经包含了核心库的链接依赖。
但有个细节要留意:ZXing 这个 target 的 include 路径如果没被正确传递,你调用 #include "zxing/ZXing.h" 时会找不到头文件。这通常是因为 ZXing 的 target 用了 INTERFACE_INCLUDE_DIRECTORIES 属性,而你的工程没有开启 CMP0079 策略。实测解决方法是把最低 CMake 版本设为 3.20:
cmake_minimum_required(VERSION 3.20)或者显式添加依赖目标的 include 路径,但第一种方法更干净。
3.3 核心参数的选择与计算逻辑
在构建之前,有几个参数值得单独说明:
- CMAKE_BUILD_TYPE:只有单配置生成器才用得上,VS 属于多配置生成器,所以这个参数在 VS2022 下会被忽略,构建时通过 --config Release 或 Debug 切换。
- CMAKE_RUNTIME_OUTPUT_DIRECTORY:如果构建的是动态库,DLL 会默认输出到 build/bin/Release 之类的路径,建议统一设置到项目根目录下的 bin 文件夹,方便调试器找到 DLL。
- CMAKE_DEBUG_POSTFIX:给 Debug 版库文件加后缀,比如 ZXingCore-d.lib,这样 Release 和 Debug 可以共存,不会相互覆盖。
我项目里的最终配置如下:
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) add_subdirectory(third_party/zxing-cpp) target_link_libraries(MyApp PRIVATE ZXing ZXingCore)3.4 命令行配置和 VS 图形界面的等效操作
习惯用 VS 图形界面的朋友其实不用碰命令行,在“配置 CMake 预设”界面里把相关变量填进去就行。但新手要明白一点:VS 的 CMake 缓存界面是在调用 CMake 之后才会刷新出来的,如果你改了根目录 CMakeLists.txt,需要右键根目录选择“删除缓存并重新配置”,否则老缓存会让你配置半天却发现没效果。
命令行方式则更直接:
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF cmake --build build --config Release两条命令足以完成配置和构建。构建完成后,在 build/lib/Release 目录下能找到 ZXingCore.lib 和 ZXing.lib,业务代码里链接 ZXing.lib 即可。
4. 编码字符集与预处理器定义:容易忽略却致命的配置
这个坑我印象特别深。ZXing C++ 源码涉及大量字符处理,默认情况下如果项目字符集是 MBCS 或者多字节字符串,编出来的解码结果会有乱码风险。VS2022 默认字符集是 Unicode,但 CMake 工程本身不会帮你设置,需要显式在 CMakeLists 中声明。这一步很多人忽略,等到程序运行后识别中文内容出现乱码才排查半天。
在 target 上添加编译定义即可:
target_compile_definitions(ZXing PRIVATE UNICODE _UNICODE)还有一个更隐蔽的预处理器:_CRT_SECURE_NO_WARNINGS。ZXing C++ 源码里带了一些 C 风格字符串操作函数,VS 的 SDL 检查会报警告,虽然是 warning 不是 error,但会污染构建输出。建议直接在全局加上:
add_compile_definitions(_CRT_SECURE_NO_WARNINGS)如果你做的是纯控制台测试程序,不涉及宽字符界面的转换,这两个宏可加可不加;但一旦和 MFC、Qt 或者 C# 互操作场景挂钩,务必保留。
5. 实操过程:从拉到源码到生成第一份库文件
前几节讲的是配置逻辑和方法,这里开始完整跑一遍流程,把每一步该干什么、看到什么结果算成功、出错可能在哪都列清楚。
5.1 获取源码并固定版本
ZXing C++ 的代码在 GitHub 上可以直接拉取,但我不建议直接拉主分支,因为主分支是开发版本,API 变动频繁。我会用 tag 固定稳定版本,比如拉到某个发布版本后,再记录一下提交号:
git clone --branch v2.2.0 --depth 1 https://github.com/zxing-cpp/zxing-cpp.git third_party/zxing-cpp固定版本的好处很明显:几个月后项目回来看,构建依然可复现。如果你贪新鲜跟踪最新版,可能某天拉了代码后构建突然失败,新 API 的迁移成本还得自己承担。我在生产项目里吃过这个亏,后来就老实了。
5.2 在 VS2022 里创建工程并配置 CMake
我采用的方式是在 VS2022 中创建一个空的 CMake 工程,然后在 CMakeLists.txt 里写构建配置。具体步骤如下:
- 新建项目,选择 CMake 项目模板。
- 在 CMakeLists.txt 里设置项目名称和最低版本。
- 复制仓库源码到 third_party 目录。
- 写入 add_subdirectory 和 link 指令。
这里输出一段完整的 CMakeLists.txt 作为示例,实测可以直接编译:
cmake_minimum_required(VERSION 3.20) project(ZxingCppDemo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) add_compile_definitions(_CRT_SECURE_NO_WARNINGS) add_subdirectory(third_party/zxing-cpp) add_executable(ZxingCppDemo src/main.cpp) target_link_libraries(ZxingCppDemo PRIVATE ZXing ZXingCore)这段配置里 add_compile_definitions 会作用到所有 target,包括第三方库本身,所以第三方库里的安全警告也会被抑制,构建输出干净很多。
配置完成后,VS2022 会从顶端工具栏的项目选择器里出现两个目标:ALL_BUILD 和 ZxingCppDemo。选择 ZxingCppDemo,然后点生成。如果一切顺利,你会看到输出提示 ZXingCore.lib 和 ZXing.lib 已经生成到指定目录。
5.3 编写最简单的调用代码验证构建产物
构建成功不代表链接成功。写一段最简单的 demo 验证扫码码制和图像读取逻辑,是必经阶段。下面这段代码读取一张包含 QR 码的图片,输出识别的文本内容:
#include <iostream> #include "zxing/ZXing.h" #include "zxing/BarcodeFormat.h" #include "zxing/DecodeHints.h" #include "ImageSource.h" int main() { auto source = zxing::ImageViewSource::Factory("qrcode.png"); if (!source) { std::cerr << "无法读取图片" << std::endl; return -1; } zxing::DecodeHints hints; hints.setFormats(zxing::BarcodeFormat::QRCode); auto result = zxing::ReadBarcode(source, hints); if (result.isValid()) { std::wcout << L"识别结果: " << result.text() << std::endl; } else { std::cout << "识别失败" << std::endl; } return 0; }注意,ZXing 2.x 的 API 和 1.x 差别不小,比如 ReadBarcode 的参数从原来的图像缓冲变成了统一的 ImageViewSource 类型。如果你参考的是旧示例代码,编译会报参数不匹配。这里我用的是 2.x 的写法。
5.4 动态库与静态库输出位置的确认
构建完成后,实际文件产出位置取决于生成器配置。VS2022 的 Visual Studio 生成器下,Release 动态库会输出到 build/bin/Release,而静态库会输出到 build/lib/Release。如果你把输出目录统一到了 ${CMAKE_BINARY_DIR}/bin,那么所有配置的 DLL 都会在 build/bin 下面,后续调试部署就很方便。
不过有个坑:即使你设置了统一输出目录,ZXing 内部的测试和示例 target 如果开着,它们的输出也会混进同一个目录。前面建议把 BUILD_TESTING 和 BUILD_EXAMPLES 关掉,就是为了避免这些无关产物干扰。如果不关,每次编译测试模块会多花不少时间,而且失败的测试 target 还会拖累整体构建。
6. 常见链接错误与排查技巧实录
这部分是我最想分享的,很多问题不是配置对就能避免的,必须踩过一次才知道怎么处理。下面几个错误是我在 Windows + VS2022 + CMake 下实测常见的。
6.1 链接时找不到关于线程和图像编解码的符号
如果你构建时看到下面这行错误:
error LNK2019: 无法解析的外部符号 "void __cdecl zxing::ReadBarcode(...)"先别急着怀疑源码问题,八成是链接目标选错了。你需要确认业务 target 是否 link 了 ZXing 而不是只 link 了 ZXingCore。ZXing 封装层函数在 ZXing.lib 里,ZLXingCore 里只有纯算法符号。如果你只 link ZXingCore,那么 ReadBarcode、ImageViewSource 这些符号确实找不到。
还有一个容易被忽略的原因:编译和链接时的运行时库不一致。如果你的主工程设置了多线程调试 DLL(/MDd),而 ZXing 编译时用的是多线程 DLL(/MD),Debug 和 Release 混用也会导致符号冲突。可以先统一运行时库,再排查链接错误。
6.2 头文件缺失导致的编译失败
fatal error C1083: 无法打开包括文件: "zxing/ZXing.h"原因基本就是 include 路径没配好。在使用 add_subdirectory 方式嵌入时,ZXing 的 target 提供的 INTERFACE_INCLUDE_DIRECTORIES 会通过 target_link_libraries 自动传递,所以理论上不需要手动添加路径。但如果你用的是旧版本 CMake,策略没启用,INTERFACE 属性不会被读取,这时候手动加上也是办法:
target_include_directories(ZxingCppDemo PRIVATE ${CMAKE_SOURCE_DIR}/third_party/zxing-cpp/core/src ${CMAKE_SOURCE_DIR}/third_party/zxing-cpp/zxing/src)不过我更推荐老老实实把 cmake_minimum_required 提到 3.20 以上,因为手动维护两个 include 路径很容易在源码结构调整后失配。
6.3 调试版和发布版链接混用导致的崩溃
这个坑藏得比较深。我的工程默认是 Debug 配置,而 ZXing 的三方库用 Release 构建,程序运行时总在解码函数内部崩溃。排查下来是 CRT 版本不一致:Debug 版链接了调试堆函数,Release 版用的是发布堆,两边在内存布局上的假设不同,一碰就出事。
解决方案有两个,任选其一:
- 始终使用同一个配置编译所有模块,Debug 就全 Debug,Release 就全 Release。
- 调试 ZXing 相关功能时,单独构建一个 Debug 版的 ZXing,并设置 CMAKE_DEBUG_POSTFIX 来区分库文件:
set(CMAKE_DEBUG_POSTFIX "d")这样 Release 下链接 ZXing.lib,Debug 下链接 ZXingd.lib,互不干扰。我在项目里用的是第二种方案,以后调试崩溃问题方便很多。
6.4 识别中文乱码的排查记录
前面提到字符集问题,这里记录一个实测场景。程序从一张白色背景的图片里识别中文网址,Release 构建下运行,结果 win32 控制台输出全是乱码。一开始以为解码器对中文支持有问题,后来把输出重定向到文件才发现,实际上解码内容是对的,只是控制台代码页不对。
这种情况不是库的问题,是程序输出端的问题。解决方案是在 main 开头调用设置控制台代码页为 UTF-8:
SetConsoleOutputCP(CP_UTF8);并且把 wcout 的 locale 也设置正确。对于真正的业务程序,比如你要把识别结果传给上层界面,建议直接用 UTF-8 字符串传输,不要在控制台层面纠结。
6.5 构建速度慢怎么办
ZXing C++ 全量编译在普通笔记本上大概需要 2 到 4 分钟,如果打开全部测试和示例,时间会翻倍。Windows 构建时建议关注两点:
- 关闭测试和示例构建。
- 启用 /MP 多进程编译选项。
在 CMake 层面可以这样设置:
add_compile_options(/MP)这条命令只能加到 MSVC 编译器下才有效,所以最好判断一下编译器再添加。实测在 8 核机器上,从 3 分钟降到 1 分半左右,效果很明显。
7. 常用功能拼装:把 ZXing 和实际业务结合到一起
很多朋友在库构建成功后就停住了,但实际业务里的问题往往不在于二维码识别本身,而在于如何把识别结果接入到业务流程中。这里分享几个我在真实项目里用过的功能组合,属于“构建完成之后”的实战内容。
7.1 从相机帧实时识别
工业场景常常需要摄像头对准流水线拍下一帧画面,识别条码后触发下一步动作。ZXing C++ 本身不采集图像,你需要从相机 SDK 拿像素缓冲,然后构造 ImageViewSource 传入识别接口。这个过程关键是像素格式对齐:ZXing 默认支持灰度图和 BGRA/RGBA 彩色图,如果你从相机拿到的是 YUV422 或其他格式,需要先转成灰度或 BGRA,否则识别率会断崖式下降。
我的经验是,相机 SDK 通常自带格式转换接口,但性能不一定好。如果 FPS 要求高,建议自己写一个 SSE/AVX 加速的转换函数,几十行代码的事,能省下不少 CPU。
7.2 批量识别多码制
一图多码这种情况,ZXing 的 ReadBarcode 只能返回第一个成功结果。想要拿全所有码,需要用 ReadBarcodes 接口,它返回一个 vector。实际项目里,货箱上可能同时贴有 Code128 和 QR 码,用单一解码器轮询效率很低,此时把 hints 里的 formats 设置成多值:
hints.setFormats(zxing::BarcodeFormat::Any);然后逐个检查结果类型。这种做法的好处是代码简洁,但要注意 Any 模式下,某些码制之间容易互相误检,导致同一块区域被报告成多个结果。实际使用时可以结合位置信息过滤重复结果。
7.3 图像预处理对识别率的影响
ZXing 内部的二值化算法已经做了不少优化,但现实场景中光照不均会严重影响识别率。我的做法是在输入前先做一次自适应直方图均衡或对比度增强。不要直接依赖库内部的锐化,因为处理大图和批量图片时,外部统一预处理更可控。
这里给一个实际案例:某项目在黄暗灯光下测试,直接识别率只有 70%,加了一步基于灰度统计的 auto-contrast 处理后,识别率提升到 94% 以上。差别之大,值得大家在业务代码里内置一个可选的预处理开关。
8. 最后的一点补充与踩坑后记
构建 ZXing C++ 这件事本身并不复杂,但“从零到能跑通示例”中间涉及很多 Windows 平台特有的细节。就我的经验来说,最大的心得有两点。
第一点是构建配置不要贪多求新,能用默认开启读者和写者就够了,实验性质的 API 在稳定下来之前尽量别碰。第二点是源码直编比 vcpkg 更好掌控,尤其是你要做码制裁剪、定制字符集、调整多线程模型的时候,源码级的修改权限能省去很多转入二进制的麻烦。
还有一个小技巧:如果你在 VS2022 里反复切换 Debug/Release 配置,发现 ZXing.lib 和其他库文件总是出现在奇怪的目录,建议检查一下 CMAKE_RUNTIME_OUTPUT_DIRECTORY 和 CMAKE_ARCHIVE_OUTPUT_DIRECTORY 是否设置成相对路径。相对路径在不同配置下会被解析成不同位置,最好用 ${CMAKE_BINARY_DIR} 开头绝对路径,从根本上避免路径混乱。
后续如果遇到图片格式变更或者需要新增条码码制,直接修改 third_party/zxing-cpp 源码里的对应注册表即可,CMake 不会因为你动了某些源文件的宏定义就强制重新生成,增量构建很友好,这也是我一直沿用这套方案的原因。