ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C++集成qrencode生成二维码:从编译到输出的完整工程实践

C++集成qrencode生成二维码:从编译到输出的完整工程实践 简介使用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_TYPERelease -DBUILD_SHARED_LIBSOFF make -j$(nproc)这里有个关键参数是 BUILD_SHARED_LIBSOFF生成的是静态库 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_ptrQRcode, 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::vectorchar 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?id10086fromcpp); 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::vectorunsigned 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 referenceqrencode 库没有正确链接或者库编译架构和主程序不一致检查 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。在 C20 之前没有原生 UTF-8 字符串字面量最省事的方案是让所有进入二维码模块的数据都先经过一个转码函数。如果工程本来就在用 ICU直接用 ucnv_convert 转如果不想引入庞大的 ICUWindows 上用 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效果不错但这是商业化定制方向了基础功能稳定之后有空再聊。本文还有配套的精品资源点击获取
返回列表