C++集成qrencode实现二维码生成:从编译到PNG输出完全指南

发布时间:2026/9/9 9:13:34
C++集成qrencode实现二维码生成:从编译到PNG输出完全指南 简介面向 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.libWindows或libqrencode.aLinux静态库。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×25V3是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::vectorunsigned 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::vectorunsigned 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); // 自行实现或调用iconv6.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_ptrQRcode, 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纸打出来贴到和真实使用场景差不多的距离上扫一扫这个步骤我从来不敢省。本文还有配套的精品资源点击获取

相关新闻