资讯动态

Qt项目集成Zint库:高效生成二维码并封装为QImage控件

发布时间:2026/10/21 19:42:13 来源:尧图企业网站定制
1. 为什么我在 Qt 项目里选 Zint 而不是自绘二维码先说个背景。去年我做了一个设备信息追溯系统Qt Widgets 为主需要把一串序列号、批次号、URL 拼成二维码显示在窗体上还要支持导出 PNG。一开始图省事让美工直接做了几张模板图把产品批次写死。结果第一次试产就被产线同事打回来了批次一变二维码就得重新让人做图响应速度根本跟不上。最怕的是客户拿手机一扫扫出来还是上一批的序列号——这种问题一旦流出责任很难扯清。后来我认真调研了一轮发现所谓的“在 Qt 里显示二维码”其实有几种路线自己画、用 QZXing、用 qrencode还有本文要讲的 Zint。很多人第一反应是“二维码不就是黑白方块吗自己写个算法画出来不就行了”。这个想法风险很大因为二维码的编码规则比你想象得繁琐包括模式选择、版本判定、纠错级别、掩码计算、数据分段光是这些就能写上千行代码而且自测时可能“刚好能用”一旦遇到中文、长文本、特殊字符就会出现各种扫不出来的情况。1.1 二维码图片的“一次性”问题自绘方案的另一个致命问题是不好应对动态内容。真正的业务场景里二维码内容往往由数据库字段实时拼接比如“设备IDXXXX批次YYYY生产日期ZZZZ”可能还要拼上追溯系统的查询 URL。这种内容必须每次运行时用代码生成不能依赖静态图片。动态生成之后还牵扯到显示清晰度。二维码里一个模块最小的黑/白方块在正经生成工具里是带缩放和留白控制的自绘方案很容易忽略“静区”这个细节。二维码四边需要至少 4 个模块宽的空白很多自绘代码会把二维码画得满满当当边界一点不留扫的时候识别率直线下降。这块我在试产时亲身踩过所以选择现成的编码库几乎是必然。1.2 常见开源方案的横向对比我把自己调研过的几个方案整理成了一张表方便后来者少走弯路方案生成能力解码能力Qt 集成难度跨平台典型问题自绘算法弱无极高一般编码规则复杂兼容性差QZXing较弱强低好侧重解码生成质量听天由命qrencode强无中等好只负责编码位图渲染和扩展要自己做Zint强无低好需要花时间封装文档风格偏传统QZXing 我确实在项目早期用过它的解码能力不错但生成二维码这块并不算核心长板至少在当时我测试的版本里生成样式和边距控制都偏弱。qrencode 是真心好用的纯编码库但它的输出是模块矩阵你还得自己把 0/1 矩阵转成 QImage留白、缩放、前景背景色都要现写。Zint 则把这些都做了一个函数调用完直接给你位图甚至 SVG省了不少事。1.3 Zint 真正省心的地方Zint 是一个开源条码生成库支持 QR Code、DataMatrix、Aztec、Code 128、EAN 等几十种条码类型。它的核心设计是把“符号编码”和“渲染输出”分成两层编码层负责把文本变成条码数据结构渲染层负责把数据画成点阵位图或矢量图形。对 Qt 开发者来说最直接的价值是它内部输出的是 RGB 像素数据你可以非常方便地转成QImage不需要自己去扫描 0/1 矩阵。它还能顺带处理静区、缩放比例、前景色、背景色甚至能直接渲染出 SVG。这些能力加起来让“在 Qt 里显示二维码”这件事从“自己实现一个编码器”降级成“封装一个控件”难度不是一个量级。2. 环境准备源码编译、链接和 Qt 工程接入接 Zint 的第一步不是写代码而是把库弄到工程里。我一开始以为这步很麻烦实际做下来比想象中简单但有几个细节如果没注意后面编译时会白白折腾几小时。2.1 获取 Zint 并快速编译Zint 的源码托管在开源社区官方提供源码包。Windows 下如果你用 MSVC可以直接用 CMake 生成 Visual Studio 工程然后编译如果你用 MinGW也可以直接用 CMake 生成 Makefile。Linux 下如果用的是较新的发行版包管理器里一般有现成的开发包直接安装即可但我个人更推荐自己编译一次因为打包给别人部署时自编译可以统一运行时依赖。编译时有个关键点Zint 默认会开启 PNG 输出支持这需要依赖第三方库。如果你只需要把二维码显示在 Qt 界面上根本用不上它内部生成 PNG 的功能——你拿到的位图自己转成 QImage保存 PNG 的活儿交给 Qt 就行。所以在 CMake 配置时可以把相关选项关掉减少依赖和编译时间。2.2 qmake 和 CMake 两种接入写法如果你的项目还在用 qmake接入方式比较简单在.pro文件里写上INCLUDEPATH $$PWD/third_party/zint/include LIBS -L$$PWD/third_party/zint/lib -lzint这里假设你把 Zint 的头文件和库文件放在了项目下的third_party/zint目录。Windows 上如果是动态库还需要在部署时把对应 DLL 放到 exe 旁边或者放到系统 PATH 里。用 MinGW 编译出来的库文件名通常是libzint.dll.a和zint.dll链接名称写作-lzint即可。如果你用的是 CMake官方并没有提供一个完整的find_package(Zint)模块至少在我常用的几个版本里没有。更稳妥的做法是直接指定头文件和库路径set(ZINT_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/third_party/zint/include) set(ZINT_LIBRARY ${CMAKE_SOURCE_DIR}/third_party/zint/lib/libzint.a) add_executable(myapp main.cpp) target_include_directories(myapp PRIVATE ${ZINT_INCLUDE_DIR}) target_link_libraries(myapp PRIVATE ${ZINT_LIBRARY})这里我特意写成一个.a静态库原因见下一节。2.3 Zint 库什么时候适合做成静态库我在项目里最终使用的是静态库而不是动态库理由很现实这是要交给产线同事安装的软件动态库如果版本不匹配运行时报“找不到 zint.dll”或者“找不到符号”是很常见的事。静态库把所有实现都编进 exe部署时只需要一个可执行文件省去了一大堆环境配置。当然静态库也不是没有缺点。Zint 的静态库如果和项目编译器不一致链接时可能出现 ABI 不兼容的问题。比如你用 MSVC 编译 Qt却拿 MinGW 编译的 Zint 静态库去链接十有八九会报一堆莫名其妙的LNK错误。这一点在后面的踩坑章节我会专门展开。还有一点许可证。Zint 的开源许可在不同版本上描述有细微差别如果你的项目是闭源商业软件建议在引入前把许可证文本完整读一遍必要时咨询团队里的法务。这里我不替任何人做决定但“用别人写的库之前先看明白许可”这个习惯值得每个开发者养成。3. 从字符串到 QImage核心 API 调用与像素级细节环境就绪后真正核心的工作就是把一段QString变成一张能显示在屏幕上的二维码图。Zint 的 API 是纯 C 风格的函数数量不多但要理解几个结构体的含义。3.1 理解 ZBarcode_Create 和 ZBarcode_EncodeZint 的核心对象是zint_symbol你可以把它理解为一个“条码生成会话”。所有参数都挂在它上面比如条码类型、缩放比例、静区大小、前景色、背景色等。zint_symbol* symbol ZBarcode_Create();这个函数会分配并初始化一个符号对象。紧接着要设置条码类型symbol-symbology BARCODE_QRCODE;BARCODE_QRCODE是 Zint 定义的一个常量对应二维码类型。如果你未来可能生成 DataMatrix 或 Code 128只要替换这个常量即可其余渲染流程完全一样。这是 Zint 比 qrencode 更省心的地方之一。然后是编码函数ZBarcode_Encode。它的职责是把一段字节数据按照指定的条码规格编码成符号矩阵并按symbol-scale等参数渲染成位图。int err ZBarcode_Encode(symbol, data, length);返回值为 0 表示成功非 0 表示失败。失败时错误信息会写到symbol-errtxt里这可比很多函数返回一个干巴巴的负数好用多了。3.2 生成一张完整的 QImage 的代码下面这段代码是我项目里最初版的核心函数后面我会在这个基础上逐步封装// qrzint_generator.h #pragma once #include QImage #include QString class QrGenerator { public: static QImage makeQrImage(const QString text, int scale 4); }; // qrzint_generator.cpp #include qrzint_generator.h #include cstring extern C { #include zint.h } QImage QrGenerator::makeQrImage(const QString text, int scale) { zint_symbol* sym ZBarcode_Create(); if (!sym) { return QImage(); } sym-symbology BARCODE_QRCODE; sym-scale scale 0 ? scale : 4; sym-whitespace 4; // 二维码四边静区单位是模块数 sym-input_mode DATA_MODE_UTF8; // 关键让 UTF-8 中文内容正确编码 QByteArray utf8 text.toUtf8(); int err ZBarcode_Encode(sym, reinterpret_castunsigned char*(utf8.data()), utf8.size()); if (err ! 0) { // 编码失败errtxt 里有具体原因 ZBarcode_Delete(sym); return QImage(); } int width sym-bitmap_width; int height sym-bitmap_height; QImage img(width, height, QImage::Format_RGB888); const unsigned char* src sym-bitmap; for (int y 0; y height; y) { memcpy(img.scanLine(y), src static_castsize_t(y) * width * 3, static_castsize_t(width) * 3); } ZBarcode_Delete(sym); return img; }这段代码就是“字符串到二维码图像”的最短路径。编码成功后sym-bitmap指向一块 RGB 像素数据每像素 3 字节顺序是 R、G、B。bitmap_width和bitmap_height分别给出图像的宽高。你可能会疑惑为什么这里没有出现透明通道或灰度图的概念——Zint 默认按 RGB888 输出背景是白色前景是黑色非常规整。3.3 位图拷贝为什么要按行处理有位读者朋友可能会问为什么不能直接一次性memcpy整个sym-bitmap到QImage的缓冲里这个问题问得好原因藏在 QImage 的内存布局里。QImage的扫描行scanLine为了保证内存对齐每一行可能有几个字节的填充。比如图像宽度是 70 像素Format_RGB888每行需要 210 字节但实际每行可能被对齐到 212 字节甚至更多。如果你用一个大memcpy直接把连续的height * width * 3字节拷进去第二行开始就会错位图像显示出来就是斜的或者错乱的。正确做法是逐行拷贝也就是上面代码里那个for循环做的事情。每次拷贝一行目标位置是img.scanLine(y)源位置按实际像素宽度偏移。这样无论 QImage 内部怎么对齐都不会错位。3.4 缩放、边距和容错级别的设置Zint 的scale字段直接控制输出位图的物理尺寸。默认值为 1表示一个二维码模块对应 1 像素。但这个尺寸在屏幕上太小了扫起来很吃力我在界面里常用的是 4 或 6。whitespace是静区也就是二维码四周的留白。QR Code 规范要求至少 4 个模块宽度所以我把值固定为 4。不要把这个值设成 0那是给自己挖坑。容错级别纠错级别也是很多人关心的。Zint 里二维码的纠错级别可以通过符号对象的参数来设置不同版本字段含义略有差异。我的建议是如果只是显示常规文本用默认值足够如果二维码会被打印在不平整的包装袋或金属表面上可以适当提高纠错级别。考虑到不同 Zint 版本的字段命名不完全一致代码里我会加一段按版本判断的宏避免换库版本后编译报错。4. 把它封装成能直接上生产的 QrCodeWidget核心函数能跑通之后下一步就是做成可以复用、可以上生产的控件。这一步不是简单地把生成函数丢到界面上就完事而是要考虑到界面卡顿、缩放显示、颜色定制、导出需求等问题。4.1 控件接口设计我最终的控件类长这样// qrcodewidget.h #pragma once #include QWidget class QrCodeWidget : public QWidget { Q_OBJECT public: explicit QrCodeWidget(QWidget* parent nullptr); QSize sizeHint() const override; QSize minimumSizeHint() const override; public slots: void setText(const QString text); void setScale(int scale); void setForegroundColor(const QColor color); void setBackgroundColor(const QColor color); signals: void imageReady(); protected: void paintEvent(QPaintEvent* event) override; void resizeEvent(QResizeEvent* event) override; private: void regenerate(); QString m_text; int m_scale 4; QColor m_fgColor Qt::black; QColor m_bgColor Qt::white; QPixmap m_pixmap; };接口设计遵循一个原则控件的使用方只需要告诉它“内容是什么”而不需要关心“二维码怎么生成”。这样在多个页面复用控件时代码非常干净。4.2 后台线程生成与输入缓冲策略二维码生成虽然快但遇到长文本或高频刷新时界面仍可能出现肉眼可见的卡顿。尤其是当用户在输入框里每敲一个字符就触发一次重新生成时主线程频繁编码、分配内存、拷贝位图界面会明显掉帧。我的做法是把生成过程丢到子线程里。具体来说setText只更新文本并记录一个“需要重新生成”的标记然后通过QtConcurrent::run在后台线程里调用生成函数拿到QImage后回传到主线程设置给m_pixmap再触发更新。这里有一个容易踩的细节不要把zint_symbol对象扔到多个线程里并发使用。Zint 底层有些状态是共享的建议每个生成任务独立创建和销毁符号对象或者干脆用一个串行的任务队列。我在项目里选择了后者——把生成任务全部丢进QThreadPool通过设置最大线程数为 1从源头上规避并发问题。4.3 自定义色彩与白边内边距的实现控件如果固定只能显示黑底白纹界面就会显得很生硬。我加了前景色和背景色两个属性。实际上Zint 本身也支持设置颜色字段但考虑到版本差异和兼容性我选择了一条更稳的路径先按默认黑白生成QImage然后在内存里把黑色像素替换成目标前景色白色像素替换成目标背景色。void QrCodeWidget::applyColor(QImage img) { QRgb fg m_fgColor.rgb(); QRgb bg m_bgColor.rgb(); for (int y 0; y img.height(); y) { QRgb* line reinterpret_castQRgb*(img.scanLine(y)); for (int x 0; x img.width(); x) { if (line[x] qRgb(0, 0, 0)) { line[x] fg; } else { line[x] bg; } } } }注意这个函数要求img的格式转为Format_RGB32才能逐像素用QRgb操作转了之后再统一用QPainter绘制到界面上。4.4 PNG / SVG 导出导出的需求在项目里迟早会出现。PNG 导出直接用 Qt 的QImage::save即可bool ok m_pixmap.toImage().save(filePath, PNG);SVG 导出更适合需要印刷放大的场景这时候要利用 Zint 的矢量渲染能力。将symbol-output_options设为BARCODE_RENDER_SVG编码后调用ZBarcode_Buffer把 SVG 内容取出来。不过这个方法得到的 SVG 文本相对简单只包含基本的路径和形状直接用于文档编辑或网页展示都足够。我个人更习惯把它存成.svg文件再交给设计同事因为他们用矢量软件打开后修改起来非常方便。5. 真实项目中遇到的五个坑这里把我在项目中实际踩过的几个问题整理出来每个都是花过时间排查的。5.1 高频生成时内存持续上涨最开始我把ZBarcode_Create和ZBarcode_Delete的配对写漏了一次导致每生成一张二维码就泄漏一次内存。做批量打印测试时连续生成几千张二维码后进程内存从几十 MB 涨到 1GB 多最后直接卡死。排查过程很枯燥但也很有代表性我用性能分析工具抓取内存分配堆栈发现泄漏集中在 Zint 编码函数里。于是回头检查代码发现有一个分支在编码失败时只return了没有调用ZBarcode_Delete。修掉这一处之后再用同样的批量测试跑了一晚上内存曲线几乎是一条直线。经验就一条所有ZBarcode_Create创建出来的对象无论编码成功还是失败都必须配对释放。最好在函数入口创建出口统一删除不要写多个return路径。5.2 UTF-8 中文内容扫不出来这是一个非常典型的坑。你按前面的代码生成中文内容的二维码肉眼看起来图案很正常但拿手机一扫要么扫出乱码要么直接提示“无法识别”。问题出在symbol-input_mode上。Zint 默认的输入模式并不总是把字节数据当作 UTF-8 处理如果不显式设置DATA_MODE_UTF8系统可能对编码后的字节做了额外转义识别结果自然不对。在我用过的几个 Zint 版本里加上这行设置后中文、日文、特殊符号基本都正常了。还有一点传给ZBarcode_Encode的数据长度是字节数不是字符数所以一定要用utf8.size()而不是text.size()。5.3 库文件与编译器 ABI 不匹配项目早期在 Windows 上用的是 Qt MSVC 版本Zint 库却是用 MinGW 编译的。当时链接时报了一堆莫名其妙的错误我花了一个下午确认“代码没问题、头文件没问题、路径没问题”最后才发现是 ABI 不匹配。避免这个问题的办法很简单Zint 的编译工具链必须与 Qt 的编译器一致。用 MSVC 的 Qt 就编译 MSVC 版的 Zint用 MinGW 的 Qt 就编译 MinGW 版的。不要混用混用就是给自己找麻烦。5.4 新旧版本颜色字段名不一致如果你去看不同版本的 Zint 头文件会发现和颜色相关的字段名有过变动。有的版本用fgcolor有的版本用fgcolour英式写法。我第一次升级 Zint 版本时编译就报了这个错。稳妥的处理方式是在自己的封装层里做一个编译期判断根据头文件中的版本宏来选择字段名这样即使换了 Zint 版本业务代码也不用改动。如果你只是简单用黑白二维码其实可以像我前面那样根本不依赖 Zint 的颜色字段直接用 QImage 替换颜色这样无论库怎么升级都不会受影响。5.5 高分屏上看起来发虚最后这个问题严格来说不是 Zint 的错而是 Qt 高分屏缩放和位图尺寸之间的关系。如果你把缩放设得很低比如scale 2生成的位图物理尺寸很小控件把它拉伸显示到高 DPI 屏幕上时会显得模糊发虚。解决办法有两个方向。一个是把 Zint 的scale调大让位图分辨率大于显示分辨率再用SmoothPixmapTransform做平滑缩放。另一个是直接改用 SVG 渲染矢量图无论怎么缩放都不会模糊。对于固定大小的二维码我更推荐前者代码改动量最小。6. 封装完成之后还能怎么扩展这套方案稳定运行了半年多期间我只做过两处扩展。第一处是增加了一个“校验失败自动重试”的逻辑如果调用方传入的文本里含有 Zint 不支持的字符生成函数返回空的 QImage控件会显示一个醒目的错误提示图标并且把原始文本打印到日志里方便定位。第二处是把生成函数从“返回 QImage”扩展成了“返回结构体”结构体里除了图像还包含二维码的模块尺寸、实际像素宽高、静区大小这些元信息。这些信息在做打印排版时特别有用因为能精确计算二维码在纸张上的物理尺寸而不是靠猜。如果后面有需要我还考虑过把这套控件做成独立的动态库供其他 Qt 项目以插件方式加载。但至少就当前项目而言一个封装良好的QrCodeWidget类已经足够没有必要为了架构而架构。最后分享一个小技巧如果你在界面里同时显示多个二维码不要为每个控件单独维护一个 Zint 会话而是统一维护一个生成线程和结果缓存。二维码内容一般不长同一文本在同一生命周期内反复生成是纯浪费。加一个QCache按字符串做 key命中缓存直接返回实测下来在列表滚动场景里能省掉 70% 以上的生成时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑