简介:使用C++与qrencode库生成二维码的完整工程,面向需要在Windows下集成二维码功能的Visual Studio开发者。压缩包共34个文件,包含qrencode库源码(10个h头文件与9个c实现)、VS2015/2019/2022工程文件(sln、vcxproj及过滤器)、调用示例main.cpp,以及编译生成的exe、lib、BMP二维码样例和调试辅助文件,整体约10.98MB,可直接打开工程查看运行效果。已有751人学习下载。工程演示了从qrencode源码编译、链接到调用QRcode_encodeString、QRcode_writeBMP生成BMP二维码的完整流程,并额外提供获取本机MAC地址的示例,便于构建“设备信息+二维码”的应用场景。开发者既可快速移植到自有项目,也可参照工程结构理解qrencode各模块的调用关系,实现定制化二维码生成功能。 最近手头有个工具软件需要临时加一个生成二维码的功能,客户现场环境五花八门,没法指望对方给装一堆运行时组件,所以选型要尽量轻量、可静态编译、不引入太多依赖。折腾了两三天,最后用 C++ 配合 qrencode 库把整个完整工程捋顺了,从编译库到封装接口再到输出图片,一条龙全打通。这篇就把完整的工程结构、编译细节、核心API用法和踩过的坑都写清楚,给同样需要在 C++ 项目里生成二维码的朋友一个能直接抄作业的参考。
qrencode 是一个用 C 语言编写的开源二维码编码库,生成速度快、内存占用小,不依赖其他第三方库就能完成二维码编码。它不属于特别新的项目,但胜在极其稳定,嵌入式、桌面端、服务端都有人用。本文针对的场景是 Windows 和 Linux 主流环境,编译器覆盖 MSVC、MinGW 和 GCC,最终产物是一个可直接调用的二维码生成模块,既能输出原始像素矩阵,又能保存为 PPM/SVG 文件,还能和 OpenCV 无缝对接显示。
1. 方案选型:为什么是 qrencode 而不是其他二维码库
二维码生成库市面上的选择不少,除了 qrencode 之外,常见的还有 zxing-cpp、libqrencode 的旧版本(其实是同一个家族)、QRCodeGenerator(一个单头文件库)、以及各种绑定到 OpenCV 的二维码模块。在实际工程选型的时候,我比较看重四个维度:依赖是否干净、接口是否稳定、能否静态编译、后续维护成本高不高。
拿 zxing-cpp 来说,它的主要强项是二维码识别,生成只是附带功能,整体体积偏大,为了一个生成功能引入整个识别库,有种杀鸡用牛刀的感觉。QRCodeGenerator 那个单头文件库确实方便,但它的编码能力偏弱,对中文等非 ASCII 字符支持不够好,二进制模式也受限。qrencode 则专门针对二维码编码做优化,API 简单清晰,核心就那几个函数,底层依赖只有 libpng(如果要用 PNG 输出,不用可以关掉),在 CMake 里配置起来非常舒服。
这里特别说一下版本选择,qrencode 的 3.x 和 4.x 系列 API 基本兼容,目前推荐直接用 4.1.1 这个版本,修掉了不少内存边界问题,编译选项也更干净。社区里有些老教程还在用 3.4.4,那个版本编码中文时需要手动处理编码模式,新版已经做了优化,直接用 QR_MODE_8 就能处理 UTF-8 字节流。
选 qrencode 还有一个很重要的原因是它的许可证是 LGPL,商用项目里可以以动态库方式使用,或者干脆做成独立的命令行工具进程,规避许可证传染问题。如果项目本身是开源的,直接静态编译也没压力。
2. 编译前置准备:Windows 与 Linux 环境全打通
工欲善其事,必先利其器。qrencode 虽然是个轻量库,但想在Windows 上用 MSVC 完整编译出来,还是有几个细节要处理。这里我把 Windows 和 Linux 两条路线都讲一遍,读者可以根据自己的实际开发环境任选其一。
2.1 Windows 端 VSCode 与 MSVC 环境配置
Windows 下最省心的方式不是直接去改 qrencode 的源码,而是用 vcpkg 或者 CMake 的 FetchContent 直接把库拉下来编译。如果已经在用 VSCode 写 C++,第一步要确认本机装了 Visual Studio Build Tools(或者完整版 VS),因为 VSCode 默认调用的就是 MSVC 工具链。
我的建议是直接用 CMake + Visual Studio 2022 生成器,在项目根目录执行以下命令:
cmake -S . -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release如果你的 VSCode 是搭配 MinGW 工具链用的,也没问题,qrencode 的 CMake 里面没有特别针对 MSVC 的独占语法,用 MinGW Makefiles 生成器也能顺利编过,只是要注意 Release 和 Debug 的库别搞混,否则链接时会报一堆莫名其妙的 LNK 错误。
2.2 Linux 端编译与静态库制作
Linux 下就简单多了,Ubuntu/Debian 系统可以直接用 apt 装现成的库,但既然是“完整工程”,我更建议源码编译一次,这样能拿到最新版,也能自己控制编译选项。
# 下载源码(国内镜像或官方git都行) git clone https://github.com/fukuchi/libqrencode.git cd libqrencode mkdir build && cd build cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF make -j$(nproc)这里有个关键参数是 BUILD_SHARED_LIBS=OFF,生成的是静态库 libqrencode.a。在很多需要部署到客户现场的项目里,静态库是最省心的,不会出现“目标机器缺 dll/so 运行不了”的问题。如果你确实需要动态库,把 OFF 改成 ON 即可,但那就需要在交付时把动态库一起带上。
2.3 在 CMake 工程中集成 qrencode
整个工程采用 CMake 管理,结构清晰,核心目标是把 qrencode 和主程序分开编译,将来不管你是想换成别的二维码库,还是想给 qrencode 加补丁,都能在独立目录里操作。主程序的 CMakeLists.txt 核心片段如下:
cmake_minimum_required(VERSION 3.16) project(QRCodeGenerator LANGUAGES C CXX) # 引入 qrencode 子目录(也可以直接用 find_package) add_subdirectory(third_party/qrencode) # 生成可执行文件 add_executable(qrgen src/QRCodeGenerator.cpp src/main.cpp ) # 链接 qrencode 静态库 target_link_libraries(qrgen PRIVATE qrencode) # 头文件路径 target_include_directories(qrgen PRIVATE src)如果你的 qrencode 是自己用 vcpkg 安装的,就不需要 add_subdirectory,改成 find_package(qrencode REQUIRED) 就行,但要注意 vcpkg 默认安装的依赖会直接带进来,部署时路径问题要额外小心。
3. 核心 API 解析:QRcode_encodeString 的每个参数都不白给
qrencode 最核心的函数就是 QRcode_encodeString,这个函数会把输入字符串编码成二维码矩阵,返回一个 QRcode 结构体指针。QRcode 结构的内容如下:
typedef struct { int version; // 二维码版本号 1-40 int width; // 二维码矩阵边长 unsigned char *data; // 位图数据,length = width * width } QRcode;data 数组里每一位表示一个模块(黑点或者白点),值为 0 或 1。这个设计非常直接,没有任何压缩或编码,拿到手就能用。
QRcode_encodeString 的函数签名是:
QRcode *QRcode_encodeString(const char *string, int version, QRecLevel level, QRencodeMode hint, int casesensitive);这五个参数每一个都有说法。第一个是内容字符串,如果是中文必须先转成 UTF-8 编码;第二个 version 是二维码版本,从 1 到 40,传入 0 表示让库自动选择最合适的最小版本;第三个 level 是纠错等级,L 最低能恢复约 7% 的数据,M 约 15%,Q 约 25%,H 约 30%,需要根据使用场景权衡,一般推荐 M 或 Q;第四个 hint 是编码模式,传 QR_MODE_8 表示字节流模式,也是兼容性最好的模式;第五个 casesensitive 表示内容是否区分大小写,通常传 1。
这里我实际调过不同参数组合,一个重要结论是:版本号尽量让库自动去算,除非你有特殊需求必须固定某个版本。因为二维码的尺寸和容量是直接关联的,你强行指定一个更小的版本,内容太多时库会返回 NULL,反而显得代码不健壮。纠错等级建议至少 M,如果用 H 级别内容容量会削减约 30%。
#include <qrencode.h> #include <memory> std::unique_ptr<QRcode, decltype(&QRcode_free)> generateQRCode( const std::string& content, QRecLevel level = QR_ECLEVEL_M) { QRcode* code = QRcode_encodeString(content.c_str(), 0, level, QR_MODE_8, 1); if (!code) { throw std::runtime_error("qr encode failed: content too long or invalid"); } return {code, QRcode_free}; }注意这里用 unique_ptr 自定义删除器来管理 QRcode 内存,避免忘记调用 QRcode_free。二维码生成可能发生在热路径上,内存泄漏是低级错误,这种 RAII 写法值得坚持。
4. 完整工程实现:从像素矩阵到可用的二维码图片
有了核心 API 的封装,下一步就是把它变成能真正用的图片。这里我分三个层次来实现:第一种是输出最简单、零依赖的 PPM 格式,方便调试;第二种是生成带放大和留白效果的 SVG 矢量图,方便网页端和打印场景;第三种是直接用 OpenCV 的 Mat 承接像素矩阵,内存里直接出图,不做磁盘 IO。
4.1 零依赖输出 PPM 格式
PPM 是 Netpbm 格式的一种,结构极其简单,文件头是 P6 加宽高加最大颜色值,后面直接跟 RGB 二进制数据。用 C++ 写一个 PNG 解码器很难,但写 PPM 编码器只需要几行代码。
void saveAsPPM(const QRcode* code, const std::string& filename, int scale = 8) { int size = code->width * scale; std::ofstream file(filename, std::ios::binary); file << "P6\n" << size << " " << size << "\n255\n"; std::vector<char> row(size * 3, 0); for (int y = 0; y < size; ++y) { auto* dst = row.data(); for (int x = 0; x < size; ++x) { char color = code->data[(y / scale) * code->width + (x / scale)] ? 0 : 255; *dst++ = color; *dst++ = color; *dst++ = color; } file.write(row.data(), row.size()); } }为什么默认 scale 选 8?因为二维码的标准模块宽度一般是 4 到 6 个像素,扫描设备才能可靠识别,8 是保守值,打印出来在手机摄像头下很清晰。实际上你可以把它做成配置项,让调用方根据使用场景去调整。
4.2 输出 SVG 矢量图
SVG 的好处是无限缩放不模糊,打印到 A4 纸上也能保证边缘锐利。qrencode 官方其实附带了一个命令行工具 qrencode,它生成 SVG 的模式可以借鉴,核心就是用 path 元素绘制一个个黑色的方块。
void saveAsSVG(const QRcode* code, const std::string& filename, int margin = 4) { int size = code->width + margin * 2; std::ofstream file(filename); file << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"" << size << "\" height=\"" << size << "\" viewBox=\"0 0 " << size << " " << size << "\" shape-rendering=\"crispEdges\">\n"; file << "<rect width=\"100%\" height=\"100%\" fill=\"white\"/>\n"; file << "<path fill=\"black\" d=\""; for (int y = 0; y < code->width; ++y) { for (int x = 0; x < code->width; ++x) { if (code->data[y * code->width + x]) { file << "M" << (x + margin) << " " << (y + margin) << "h1v1h-1z"; } } } file << "\"/>\n</svg>\n"; }margin 参数就是二维码四周的空白区域,根据二维码规范,四周留白至少要有 4 个模块宽,否则扫码识别率会明显下降。这里用 path 而不是逐个画 rect,是为了控制文件体积,SVG 文件里如果嵌入了大量 rect 节点,几百字节能膨胀到几十 KB,没必要。
4.3 与 OpenCV 集成:直接在内存中显示
如果你已经在用 OpenCV 做图像处理,完全没有必要先落盘再读盘,直接通过 QRcode 结构体构造 cv::Mat 就能显示或用于后续图像拼接。
#include <opencv2/opencv.hpp> cv::Mat qrcodeToMat(const QRcode* code, int scale = 8) { int size = code->width * scale; cv::Mat img(size, size, CV_8UC1, cv::Scalar(255)); for (int y = 0; y < code->width; ++y) { for (int x = 0; x < code->width; ++x) { if (code->data[y * code->width + x]) { cv::rectangle(img, cv::Rect(x * scale, y * scale, scale, scale), cv::Scalar(0), cv::FILLED); } } } return img; }这种集成方式特别适合那些需要在界面上实时预览二维码的场景,比如桌面客户端里点一个按钮就在窗口中弹出二维码,而不是生成一个临时文件再让用户手动打开。上面的代码里我用了 cv::rectangle,因为二维码数据本质上是二值图像,用 rectangle 逐个填充黑块远比逐像素写数据直观,而且性能足够,一个版本 5 的二维码也就是 37×37 个模块,循环开销完全可以忽略。
5. 可编译运行的完整工程:main.cpp 示例与运行效果
工程最终包含两个关键文件:QRCodeGenerator 模块和测试入口 main.cpp。main.cpp 里我写了三个生成用例,分别对应英文数字、中文内容、以及特殊符号场景,读者跑一遍就能直观看到不同内容对二维码尺寸的影响。
#include "QRCodeGenerator.h" #include <iostream> int main() { try { // 英文和数字,容量大,二维码相对小 auto qr1 = generateQRCode("HELLO QRCODE 12345"); saveAsPPM(qr1.get(), "output_simple.ppm", 8); // 中文内容,UTF-8 字节流 auto qr2 = generateQRCode("https://example.com?id=10086&from=cpp"); saveAsSVG(qr2.get(), "output_url.svg", 4); // 含有换行和特殊符号的内容 std::string multiLine = "Name: Test\nPhone: 13800138000\nAddress: BeiJing\n"; auto qr3 = generateQRCode(multiLine); saveAsPPM(qr3.get(), "output_multiline.ppm", 6); std::cout << "generate done." << std::endl; } catch (const std::exception& e) { std::cerr << "error: " << e.what() << std::endl; return 1; } return 0; }在实际工程中,我不会直接把保存函数暴露给业务层,而是封装成一个 QRCodeService 类,让上层只能拿到像素矩阵或者图片缓存。在米哈游那样的服务器端应用里,二维码经常是用 base64 字符串直接输出给前端 JSON 接口,张图片落地会产生大量临时文件,还要考虑清理问题。所以我还额外提供了一个编码为 PNG 字符串的接口,用第三方头文件库 stb_image_write.h 写 PNG 编码器,无缝嵌入到 HTTP 接口里。
std::string encodeToPNGBase64(const QRcode* code, int scale = 8) { int size = code->width * scale; std::vector<unsigned char> rgba(size * size * 4); for (int y = 0; y < size; ++y) { for (int x = 0; x < size; ++x) { bool black = code->data[(y / scale) * code->width + (x / scale)]; int idx = (y * size + x) * 4; rgba[idx] = black ? 0 : 255; rgba[idx + 1] = black ? 0 : 255; rgba[idx + 2] = black ? 0 : 255; rgba[idx + 3] = 255; } } int len = 0; unsigned char* png = stbi_write_png_to_mem(rgba.data(), size * 4, size, size, 4, &len); std::string encoded = base64Encode(png, len); free(png); return encoded; }stb_image_write 是一个公共领域(public domain)的单头文件库,全世界几千个项目都在用,质量非常可靠,只用它的 PNG 编码器不会引入任何风险。base64 编码自己写或者引用一个小开源库都行,核心思路是确保网络传输时图片数据不会乱码。
6. 常见问题与排查技巧实录
这章是我实际折腾过程中踩坑的记录,网上资料比较零散,专门整理成速查表,建议收藏。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 链接提示 LNK2019/undefined reference | qrencode 库没有正确链接,或者库编译架构和主程序不一致 | 检查 CMake 里 target_link_libraries,确认 x86/x64 架构一致 |
| 运行时提示找不到 libqrencode.dll | 使用了动态库但没有设置 PATH 或拷贝 DLL | 改用静态库,或把 DLL 放到可执行文件目录 |
| 中文内容生成的二维码扫码结果是乱码 | 源文件字符串不是 UTF-8 编码,或编译器把窄字符串按本地代码页处理 | 确保字符串以 UTF-8 保存,且构造 std::string 时从 UTF-8 字节流构造 |
| 生成的二维码怎么扫都识别不了 | 版本号过高/纠错等级太低/四周留白不够 | 自动计算版本,至少使用 M 级纠错,margin 不小于 4 模块宽度 |
| 同样的内容生成出来的二维码每次不同 | 这是正常的,二维码本来就有容错冗余,不同编码模式会改变图案 | 无需处理,内容识别率不受影响 |
| 在 Windows 下用 MSVC 编译通过但运行崩溃 | 新版本 MSVC 对栈缓冲区安全性更严格,可能数组越界 | 打开 AddressSanitizer 或调试器栈回溯,检查 data 数组访问边界 |
6.1 最容易忽略的编码问题
中文乱码是工程师最容易忽略的问题。C++ 的字符串处理和二维码编码之间存在着编码断层的坑。QRcode_encodeString 接收的是字节流,它不关心字符编码语义,只是把它当成一串字节进行 8-bit 模式编码。所以如果你在 Windows 上用 GBK 编码的中文直接传进去,手机扫码的时候解码出来的是乱码。这个问题的根源是源字符串的编码,而不是 qrencode 本身。
解决办法是:工程内统一用 UTF-8。在 C++20 之前没有原生 UTF-8 字符串字面量,最省事的方案是让所有进入二维码模块的数据都先经过一个转码函数。如果工程本来就在用 ICU,直接用 ucnv_convert 转;如果不想引入庞大的 ICU,Windows 上用 MultiByteToWideChar 加 WideCharToMultiByte 两连击也能解决,Linux 上直接用 iconv。
#ifdef _WIN32 std::string toUtf8(const std::wstring& wide) { if (wide.empty()) return {}; int size = WideCharToMultiByte(CP_UTF8, 0, wide.data(), (int)wide.size(), nullptr, 0, nullptr, nullptr); std::string result(size, 0); WideCharToMultiByte(CP_UTF8, 0, wide.data(), (int)wide.size(), result.data(), size, nullptr, nullptr); return result; } #endif这个转码函数虽然模板化,但只适用于 Windows 平台,Linux 下默认就是 UTF-8,不存在这个问题。
6.2 关于容错等级的一个真实案例
我之前做一个票据打印项目,为了图省事把纠错等级设成了 QR_ECLEVEL_L,结果打印机的墨水浓度稍微偏淡,有一批订单二维码扫不出来。后来把纠错等级提升到 H,虽然二维码尺寸变大了一些,但扫描成功率基本恢复到 100%。如果你的二维码需要打印出来贴在包装箱上,有可能会沾水、被磨损,这时候 H 等级是非常必要的。代码上的改动就一行,但可别因为这一行影响整个项目交付。
6.3 线程安全与性能问题
qrencode 的编码算法完全运行在调用线程栈上,不持有共享状态,因此同一个 QRcode 结构指针可以被多个线程同时使用,只要各自负责各自的 encode 和 free 即可。理论上性能不是瓶颈,实测一个中等长度的 URL 编码大约耗时 0.3 毫秒,比图片保存还快。如果是在服务器端高并发生成二维码,瓶颈往往在图片编码和网络 IO,不在二维码自身生成环节,所以通常不需要引入线程池专门优化二维码生成。
7. 写在最后的工程化建议
到这里整个工程已经能跑通了:qrencode 编译链完整、CMake 工程清晰、核心封装可直接复用、OpenCV 显示不落盘、PNG 输出不依赖额外重量级库。这套方案我实际用了一年多,从本地工具到服务端接口都有覆盖,整体非常省心。
最后分享一个工程化的小技巧:所有对外接口尽量返回像素矩阵或者字节流,而不是直接落盘。原因很简单,上层业务可能是网页接口、可能是桌面客户端、也可能是打印模块,各自需要的数据格式都不一样,接收方必须自己掌控输出的时机和形态。如果你在底层就强行生成文件,后续做接口适配会很难受。这也是为什么我反复强调 QRcode 结构体本身的意义,它是整个项目的核心中枢。
如果想继续扩展,可以考虑给二维码加 Logo、加渐变颜色、做艺术二维码。这些都是在拿到像素矩阵之后做的美化工作,和 qrencode 本身关系不大。但注意加 Logo 会遮挡模块数据,必须用高纠错等级并且 Logo 面积不超过二维码面积的 1/4,否则识别率会断崖式下跌。我自己试过用 OpenCV 绘制中心圆角矩形再嵌入 Logo,效果不错,但这是商业化定制方向了,基础功能稳定之后有空再聊。
本文还有配套的精品资源,点击获取