☰
C++集成qrencode实现二维码生成:从编译到PNG输出完全指南
2026/10/27 9:50:43 网站建设 项目流程

简介:面向 C++ 开发者的一套 qrencode 完整集成工程,基于 Visual Studio 2015 构建,同时兼容 2019/2022,可用于生成二维码、将文本或 URL 写入 BMP,并集成获取本机 MAC 地址的示例逻辑。工程共 34 个文件,压缩包约 10.98MB,包含 qrencode 源码(h/c)、VS 解决方案与工程文件(sln/vcxproj)、编译好的可执行程序与静态库(exe/lib)以及生成的二维码 BMP 样例,目录划分清晰,可直接打开解决方案编译运行。已有 751 人学习使用。通过该工程可快速掌握 qrencode 的编译与调用方式,了解 QRcode_encodeString、QRcode_writeBMP 等接口的实际用法,同时获得从 VS2015 迁移到新版编译器的配置经验,适合需要在本机生成二维码、调试 MAC 地址获取逻辑或进一步定制功能的开发者参考和二次扩展。 用C++做二维码生成,选型的时候我其实犹豫了很久。网上搜了一圈,能用的库不少,但真正轻量、稳定、文档清楚的就那么几个。qrencode是我最后留下的那个——它能把一段文本、一个链接、一串WiFi配置信息变成一张比特矩阵图,而你只需要调用两个核心函数就能拿到这个矩阵。剩下的,就是你自己决定怎么把它画成图片、印到物料上、输出到终端里,或者直接塞进网络协议。这篇文章我会把一套完整的、可以直接编译运行的qrencode工程拆开讲清楚,从库的编译、API调用、矩阵输出到常见坑位,全部过一遍。适合已经会写C++、但第一次接触二维码生成,或者被各种半成品项目搞到头皮发麻的朋友。

1. 为什么偏偏是qrencode:二维码编码库的选型复盘

不少人一提到二维码就想到ZBar或者zxing-cpp,但这两个库本质上都是识别方向的,侧重解析已经存在的二维码图片。而qrencode是纯编码库,只负责把数据变成二维码的二进制矩阵。你可以把它理解为“翻译官”:你把原始内容给它,它给你一份用户能扫的图,中间涉及到的RS纠错、数据分块、掩码优化这些计算,它全包了。

和别的方案对比下来,qrencode的优势非常突出:

  • 它没有任何运行时依赖,编译产物就是一个静态库或者动态库,不会拖家带口。
  • 生成的矩阵信息非常干净,直接暴露给你的是一个二维数组,你想画成什么格式都行。
  • 官方源码本身就很小,整个项目编完也就几百KB,非常适合嵌入到工具类程序里。
  • 跨平台能力强,Windows、Linux、macOS都能编,CMake工程直接就能跑。

我当时做的一个离线工具需要在Windows环境给每台设备生成专属二维码标签,生产电脑不能装大型依赖库,也碰不到网络。qrencode这种“我只要拿到矩阵就行,剩下的我自己画”的思路,对我这种需求简直量身定做。当然,它也有不足:它不提供图片编码能力,PNG、JPEG这些得靠你自己写或者引入第三方库。但反过来想,这正是我喜欢它的原因——它把职责边界划得很清楚,编码归编码,渲染归渲染,不越界。

2. 完整的工程组装:从库编译到CMake接入

2.1 两种方式拿到qrencode库

第一种是直接用vcpkg安装,这是最省心的一条路:

vcpkg install qrencode:x86-windows-static

如果你想用动态库,把x86-windows-static换成x64-windows即可。vcpkg会帮你把库编译好,并且自动处理头文件路径和链接路径,配合CMake的find_package()使用非常舒服。

第二种是自己编译官方源码,好处是可以修改一些编译参数,比如关闭某些不需要的模块。qrencode的源码托管在GitHub上,下载后用CMake生成对应平台的工程文件,编译过程基本不会出幺蛾子:

git clone https://github.com/fukuchi/libqrencode.git cd libqrencode mkdir build && cd build cmake .. -DCMAKE_INSTALL_PREFIX=./install cmake --build . --config Release cmake --install .

编译完成后你会在install目录下看到头文件qrencode.h和库文件libqrencode.lib(Windows)或libqrencode.a(Linux静态库)。

2.2 CMakeLists.txt的编写细节

我在工程里通常这样组织CMake文件:

cmake_minimum_required(VERSION 3.16) project(QRGenDemo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(PNG REQUIRED) # 如果要把矩阵输出为png,这里会用到 # 指向你自己的qrencode安装目录 set(QRENCODE_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/third_party/qrencode/include") set(QRENCODE_LIBRARY "${CMAKE_SOURCE_DIR}/third_party/qrencode/lib/libqrencode.a") add_executable(qr_demo main.cpp) target_include_directories(qr_demo PRIVATE ${QRENCODE_INCLUDE_DIR}) target_link_libraries(qr_demo PRIVATE ${QRENCODE_LIBRARY} ${PNG_LIBRARIES})

这里有个细节很多人会忽略:qrencode的头文件是C接口,如果你在纯C++工程里调用它,需要在包含头文件的地方加一层extern "C"保护,否则会因为名字修饰(Name Mangling)导致链接失败。更稳妥的做法是在qrencode.h前面统一加上:

extern "C" { #include "qrencode.h" }

我早期直接被这个坑过一回,链接报错满屏的unresolved external symbol,排查了半天才发现是C和C++的符号规则不同。

3. 核心API的使用逻辑:从输入字符串到比特矩阵

qrencode的核心API其实就两个:QRcode_encodeString()和QRcode_encodeData()。前者接收C字符串,后者接收二进制字节流,区别在于底层数据是否按字符串处理。二维码里如果包含中文、二进制文件内容或者加密数据,我建议直接用QRcode_encodeData(),避免字符串结尾的空字符把数据截断。

const char* content = "https://example.com"; int version = 0; // 0表示自动选择最小版本 QRecLevel level = QR_ECLEVEL_M; // 纠错级别:L、M、Q、H QRcode* qrcode = QRcode_encodeString(content, version, level, QR_MODE_8, 1); if (qrcode == nullptr) { // 编码失败,通常意味着数据超长或者version设置不合理 return -1; }

返回的QRcode结构体是这个库最核心的数据结构,定义大致如下:

typedef struct { int version; // 实际使用的二维码版本 int width; // 矩阵边长(像素点单位) unsigned char* data; // 矩阵数据,按行优先存储 } QRcode;

width代表矩阵的边长,比如一个V4版本的二维码边长是33。data数组里每个字节只有两个值:0表示空白,1表示黑色模块。你不需要关心它的内部排布,直接把它当作一张位图去渲染就行。

注意version这个参数,如果你设成0,库会自己计算最短能装下你数据的最小版本;如果你指定一个具体的版本号,比如5,但数据量超过V5的容量,编码会直接失败返回NULL。所以在不确定数据长度的情况下,永远优先用0。

3.1 输入长度和版本号的容量换算

在平时写业务代码时,你可能不需要深究版本容量表,但真遇到“我这段数据印出来扫不了”这类问题时,懂一点底层逻辑有助于快速排查。

二维码从V1到V40,每个版本承载能力不同。V1是21×21,每升一个版本宽度加4。比如V2是25×25,V3是29×29,依此类推。字节模式(QR_MODE_8)下,V1只能承载17个字节,V2能承载32个字节,V10大约271个字节,V40则可以达到2953个字节。中文按UTF-8编码的话一个字通常占3个字节,所以如果你的内容超过250个汉字,最好用H级别纠错以外的方案并主动调高version,否则生成很容易失败。实践里我会在代码里加个保护判断,一旦QRcode_encodeString返回空指针,就自动降级重试:

int tryVersion = 0; QRcode* qrcode = nullptr; // 先用自动版本试一次 qrcode = QRcode_encodeString(content, 0, QR_ECLEVEL_M, QR_MODE_8, 1); if (!qrcode) { // 如果失败,用H级之前先降级到L级再试,L级容量最大 qrcode = QRcode_encodeString(content, 0, QR_ECLEVEL_L, QR_MODE_8, 1); }

4. 把比特矩阵变成图片:一个函数搞定PNG输出

拿到矩阵只是第一步,工程落地一般都希望直接输出成图片文件。这里我给你两种方案,一种懒人方案,一种极简方案。

懒人方案是直接集成libpng。你先把qrencode的矩阵放大若干倍,生成一张带边距的位图,然后调用libpng的写接口。但libpng的API有点啰嗦,要写好多行样板代码。后来我改用stb_image_write这个单头文件库,几行代码就能把内存中的像素数组写成PNG或者BMP:

#define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" void saveQRcodeAsPNG(QRcode* qrcode, const char* filename, int scale, int margin) { int width = qrcode->width; int imgSize = (width + margin * 2) * scale; std::vector<unsigned char> image(imgSize * imgSize); for (int y = 0; y < imgSize; ++y) { for (int x = 0; x < imgSize; ++x) { bool black = false; int mx = x / scale - margin; int my = y / scale - margin; if (mx >= 0 && mx < width && my >= 0 && my < width) { black = qrcode->data[my * width + mx] & 0x01; } image[y * imgSize + x] = black ? 0 : 255; } } stbi_write_png(filename, imgSize, imgSize, 1, image.data(), imgSize); }

几个关键参数的经验值:scale是每个模块的像素尺寸,屏幕显示用4~6就够清晰了;如果需要打印,建议8起步。margin是静区宽度,按QR规范至少需要4个模块宽,但实际打印场景里我建议加厚到6甚至8个模块,尤其是要贴在曲面或者粗糙材质上的时候,多一点静区能明显提升扫码成功率。

4.1 彩色二维码和Logo嵌入的实现思路

上面代码生成的是黑白二值PNG,如果你想要彩色的,也很简单。把单通道改成三通道,stbi_write_png里的通道数参数变成3,然后自己定义前景色和背景色的RGB值。比如背景白色(255,255,255),前景深蓝色(32,64,128),扫出来基本没压力。

嵌Logo的话稍微麻烦一点。标准做法是先生成一张不带Logo的高纠错级别二维码(H级纠错,25%~30%的容错率),再把Logo图片缩放后贴到矩阵正中央,覆盖的区域大概占整体宽度的15%~20%。注意别贴太大,否则纠错救不回来。我自己写过一版实现,覆盖率控制在18%的时候,无论是微信还是支付宝都能秒扫。这个逻辑的核心其实就是矩阵局部像素的覆盖替换,并不复杂,但很多朋友在第一次实现时会忽略纠错级别要提前设成H,导致加了Logo之后扫不出来。

5. 完整工程示例:一次生成一个或多个二维码的可执行程序

上面讲得再多都不如直接给出一份能跑的完整代码。演示工程放在一个main.cpp里,功能包含三部分:从命令行读取内容,调qrencode生成矩阵,把矩阵写成PNG文件。同时也支持批量模式,从一个文本文件里读多行内容,一次生成多个文件。

#include <cstdio> #include <cstdlib> #include <cstring> #include <string> #include <vector> extern "C" { #include "qrencode.h" } #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" static void generateSingleQR(const std::string& text, const std::string& outfile, int scale, int margin, QRecLevel level) { QRcode* code = QRcode_encodeString(text.c_str(), 0, level, QR_MODE_8, 1); if (!code) { fprintf(stderr, "encode failed for: %s\n", text.c_str()); return; } int side = (code->width + margin * 2) * scale; std::vector<unsigned char> px(side * side, 255); for (int y = 0; y < side; ++y) { for (int x = 0; x < side; ++x) { int mx = x / scale - margin; int my = y / scale - margin; if (mx < 0 || mx >= code->width || my < 0 || my >= code->width) { continue; } if (code->data[my * code->width + mx] & 0x01) { px[y * side + x] = 0; } } } stbi_write_png(outfile.c_str(), side, side, 1, px.data(), side); QRcode_free(code); } int main(int argc, char** argv) { if (argc < 3) { printf("Usage: qr_demo <text> <output.png> [scale] [margin]\n"); printf(" or: qr_demo --batch <input.txt> [scale] [margin]\n"); return 1; } int scale = argc > 3 ? atoi(argv[3]) : 8; int margin = argc > 4 ? atoi(argv[4]) : 6; if (strcmp(argv[1], "--batch") == 0) { FILE* fp = fopen(argv[2], "r"); if (!fp) { perror("open batch file failed"); return 1; } char line[1024]; int idx = 0; while (fgets(line, sizeof(line), fp)) { line[strcspn(line, "\r\n")] = 0; if (strlen(line) == 0) continue; char outname[64]; snprintf(outname, sizeof(outname), "qrcode_%03d.png", idx++); generateSingleQR(line, outname, scale, margin, QR_ECLEVEL_M); printf("%s -> %s\n", line, outname); } fclose(fp); } else { generateSingleQR(argv[1], argv[2], scale, margin, QR_ECLEVEL_M); printf("done\n"); } return 0; }

这段代码可以直接编译运行,你只需要把qrencode库和头文件、stb_image_write.h放到对应的include路径。批量模式下,每行内容对应一个二维码,文件名按序号递增。实测下来,从一个几千行的设备信息文件生成全部二维码图片,耗时不到两秒。

5.1 编译环境的差异提醒

如果你在Linux上编,fopen、printf这些都没问题;如果拿到Windows的MSVC环境,注意两点。第一,main函数参数里的命令行中文编码可能是GBK,直接传给QRcode_encodeString会出现中文乱码,扫码内容自然也不对。我的处理办法是在传入前先转成UTF-8,或者强制要求调用方以UTF-8方式传递参数。第二,如果用了静态库,记得和动态库类似,要把对应运行时库设置成/MT,否则会有符号冲突的报错。

6. 我在集成qrencode时踩过的坑和调优经验

6.1 汉字编码导致的扫码乱码

这个问题我见得太多了。qrencode本身只管比特流,不管字符编码。你用GBK编码的中文喂给QRcode_encodeString,手机扫码后按UTF-8解码,自然就是一串乱码。解决办法很简单:所有进入二维码的数据,统一先转成UTF-8再编码。Windows下用MultiByteToWideChar和WideCharToMultiByte做一轮转换就能搞定,Linux下直接用iconv。

如果你连的嵌入式设备不支持转码函数,还有一个土办法:预先在PC端把UTF-8编码后的内容处理好,写成文件或者协议缓存,设备端只负责透传原样编码,不做任何转换。代码层面看起来就是:

std::string utf8_content = convertToUtf8(original_gbk_content); // 自行实现或调用iconv

6.2 版本和纠错级别的最优组合

实际项目里,我建议直接遵循以下选择逻辑:

使用场景推荐纠错级别推荐静区宽度自定义说明
屏幕展示、小程序码M4速度快,容量大
打印在不干胶/纸张上Q6~8防打印墨迹扩散
贴到塑料外壳/曲面H8防畸变影响识别
需要嵌入LogoH6中心区域会被遮盖,H级才能兜底

如果内容本身很短,比如一个URL,直接选H级也没问题,V1~V2版本容量足够。但如果内容有几百个字符,你又要H级纠错,版本会涨很多,生成的图片会偏密、点阵会小,这时候就有点得不偿失。我的习惯是先看内容长度,再决定纠错级别,而不是无脑拉满。

6.3 内存释放和多次生成的稳定性

QRcode_encodeString返回的QRcode*每次都要调用QRcode_free(code)释放。在循环里批量生成时,漏掉这行代码,内存占用会一路飙升。我早期跑过一个批量生成工具,没释放内存,结果处理到第3000张图片时程序直接崩了,排查半天才发现是积压的内存把堆挤爆了。后来我在封装接口里用了RAII的思路,弄了个智能指针包装:

using QRcodePtr = std::unique_ptr<QRcode, decltype(&QRcode_free)>; QRcodePtr code(QRcode_encodeString(...), QRcode_free);

这样即使中途抛出异常,内存也不会泄漏。

6.4 矩阵复用与线程安全

qrencode在4.x版本之后内部有全局锁,理论上多线程调用是安全的。但我还是建议在业务层做一层互斥,尤其是你会在临界区里同时处理矩阵数据和图片写入时。因为QRcode返回的data指针是独立的堆内存,线程间只读不写没问题;但你如果为了省内存自作聪明搞共享,那迟早要出事。

另外提一句,qrencode有个比较隐晦的API叫做QRcode_encodeStringMulti(),可以把一段长文本拆分到多个二维码中生成序列码。这个功能在不支持V40以上超大容量的老标准设备上比较实用,但我实测下来很多扫码App对多码序列的支持并不好,能不用就别用,宁可把内容精简一点,或者直接用DataMatrix替换。

7. 从纯C++到跨界:qrencode在其他语言里的变体

做工程久了你会发现,二维码生成这种需求,不只在C++里出现。qrencode因为太基础了,被各种语言包装了一遍。Python里有qrencode模块,Node.js有qrcode库,甚至前端也有纯JS版本的二维码生成器。它们的底层算法基本都参考了同一份QR标准实现。

但用C++原生去写有一个别的方式比不了的优势:你可以直接控制内存布局,把二维码矩阵输出到任何你想让它去的地方——比如通过网络socket发送给打印机、直接写进一个共享内存区域、或者塞进一个视频帧的固定位置。这些场景下,如果转到Python或者JS,性能和实时性都会打折扣。

我最近在为一家工厂做的项目里,就是把qrencode嵌进了一个实时检测软件,摄像头检测到产品到位后,C++端立刻生成包含产品序列号的二维码矩阵,再由底层编码器直接叠加到工业相机的输出流上,整个过程不落盘,延迟控制在几十毫秒内。这种无缝衔接,正是C++工程的价值所在。如果你以后遇到类似“二维码不只要生成,还要嵌到某个实时系统里”的需求,思路完全可以照搬。

最后再分享一个我个人用的习惯:生成完二维码图片之后,一定要用手机实际扫一遍再交付。不是所有二维码都能被你手里的扫码器正确识别,尤其当你改了颜色、加了背景或者嵌了Logo之后,机器验证比代码自测靠谱得多。拿一张A4纸,打出来,贴到和真实使用场景差不多的距离上扫一扫,这个步骤我从来不敢省。

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

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

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

立即咨询