一、前言
本文主要介绍如何使用C++来实现二维码的生成,使用到了开源库:qrencode
代码生成结果如下图,完全是可以扫描的:
二、qrencode库的基本使用
如果你目前在使用vcpkg
,那就非常简单了,直接运行以下命令进行安装即可:
.\vcpkg.exe install libqrencode:x64-windows
vcpkg
的使用教程可以参考另一篇文章:C++开源库配置
如果没有使用vcpkg,那么就需要先下载我编译好的库:qrencode
解压后有一个头文件与一个静态库文件(为 x64 Release 版本):
使用方法同样参考文章C++开源库配置的手动配置部分。
如果想要尝试自己编译这个库,可以参考这篇文章:windows 编译qrencode库
然后在代码中使用它也非常简单,基本只需要一个函数:
#include"qrencode.h"
#pragma comment(lib,"qrencode.lib")
int main(){
QRcode* pQRC = QRcode_encodeString("https://www.kucoding.com", 0, QR_ECLEVEL_H, QR_MODE_8, 1);
}
如果使用的vcpkg下载安装,那么直接包含头文件就可以使用了:
#include<qrencode.h>
int main(){
QRcode* pQRC = QRcode_encodeString("https://www.kucoding.com", 0, QR_ECLEVEL_H, QR_MODE_8, 1);
}
即,我们大部分情况只用得到这个库中的QRcode_encodeString
函数,该函数的作用是将字符串编码为符号串并返回,返回的QRcode
对象中就包含了二维码的数据
它有5个参数,但事实上除了第一个参数为我们要进行编码的字符串外,其它参数基本就是固定的:
- 要进行编码的字符串
- 版本,填0即可自动选择
- 纠错级别,基本就填这个值,即高级别即可
- 编码模式,现在一般都是采用的utf-8进行编码,所以这个值也是固定的
- 是否区分大小写,一般也是要区分的,所以基本固定填1
更多详细介绍可以参考官网说明:QRcode_encodeString
编码成功后,它会返回一个QRcode
对象,定义如下:
typedef struct {
int version; ///< 版本
int width; ///< 宽度
unsigned char *data; ///< 生成的数据
} QRcode;
由于二维码是长宽相等的正方形,所以data
的长度为 width*width
后面我们也主要是通过这两个数据来生成我们的二维码
由于他返回的是一个指针,所以使用完后我们还需要调用一个函数来释放内存:
QRcode_free(pQRC);
这里注意一下上面这个data
返回的数据格式,其含义是按照位来划分的,一个字节(char)有8位
每一位的含义如下(来源于官网):
MSB 76543210 LSB
|||||||`- 1=black/0=white
||||||`-- 1=ecc/0=data code area
|||||`--- format information
||||`---- version information
|||`----- timing pattern
||`------ alignment pattern
|`------- finder pattern and separator
`-------- non-data modules (format, timing, etc.)
但事实上,除了第0位,其它位一般我们都用不上,只要第0位为1,就说明该位置应该为黑色,为0则为白色
一般二维码都为黑白色,但后面自己生成二维码图片时,其实是可以更改生成的二维码颜色的
三、BMP图片生成原理
在众多图片格式中,BMP是其中比较简单的一种,所以我们这里采用BMP文件来保存二维码信息
BMP文件的具体格式可以参考我的另一篇文章:BMP文件格式分析
后面的内容是基于上面这篇文章写的,所以如果不懂BMP格式,请务必先看一下上面这篇文章
这里主要介绍如何用代码来生成一张BMP图片
比如我想生成一个简单的黑白相间的图片,代码应该怎么写呢?
为了简单起见,这里采用的是24位色,但由于二维码一般都是黑白的,所以其实采用1位色就行了,即:只要能够表示黑白两种颜色就行了
首先,我们需要包含头文件:
#include<Windows.h>
因为BMP几个结构都在其中
然后,我这里就想要画一个20*20
像素的图像,且黑白相间
所以第一个结构体可以这样写:
//每行20像素,每个像素用24位,即3个字节表示,所以每行为20*3个字节,然后有20行
int dataSize = 20 * 3 * 20;
BITMAPFILEHEADER kFileHeader{}; //紧跟{}代表初始化同时清零
kFileHeader.bfType = 0x4d42; // "BM"
kFileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dataSize; //文件大小(字节数)
kFileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
注意:
每行字节数量必须为4的倍数,不够用0补齐,这是BMP格式的硬性规定,但由于我们这里每行为20像素,即60个字节,可以被4整除,所以没有进行补齐
然后是第二结构体:
BITMAPINFOHEADER kInfoHeader{}; //紧跟{}代表初始化同时清零
kInfoHeader.biSize = sizeof(BITMAPINFOHEADER);
kInfoHeader.biWidth = 20; //图片宽度
kInfoHeader.biHeight = -20; //高度,为负数时,以左上角为图片原点,为负数时,以左下角为图片原点
kInfoHeader.biPlanes = 1; //平面数,一般都为1
kInfoHeader.biBitCount = 24; //位数,rgb各8字节,24位
其它基本是固定的,这里要注意宽高是像素宽高,不是字节,同时高度一定要写成负数,如果为正数,你就需要从图片的左下角开始往文件内部写入数据,会有点反直觉
然后就可以申请缓存区开始写数据了:
char *buf=new char[dataSize]; //申请存放图片数据的缓存区
for (int y = 0; y < 20; y++) { //第y行
for (int x = 0; x < 20; x++) { //第x列
if (y % 2 == 0) { //偶数行,为黑色
(buf + y * 20 * 3 + x * 3)[0] = 0; //r
(buf + y * 20 * 3 + x * 3)[1] = 0; //g
(buf + y * 20 * 3 + x * 3)[2] = 0; //b
}
else { //奇数行,为白色
(buf + y * 20 * 3 + x * 3)[0] = 0xFF; //r
(buf + y * 20 * 3 + x * 3)[1] = 0xFF; //g
(buf + y * 20 * 3 + x * 3)[2] = 0xFF; //b
}
}
}
因为图片是二位数据嘛,所以用两个for循环进行遍历赋值
注意这里遍历的是每行、每列的像素宽高,即20,而不是字节,每行的字节数应该为
20*3
同时注意内部的赋值方式:
(buf + y * 20 * 3 + x * 3)[0] = 0;
因为buf
是一个char
类型的数组,buf
本身记录的是该数组的第一个元素的地址
因此buf+n
就代表第n个元素的地址,这里加的是 y*20*3
(就代表第几行)和 x * 3
(第几列)
因为二维数组本质上也是一维数组,只不过编译器帮我们换算了,而这里我们采用的是自己来换算的方式
因为每一列为3个字节宽度,所以x需要乘以3,每一行共有20*3
的字节宽度,所以第几行就是首地址加上几个 20*3
即可
指针加上一个数字后依旧是指针,只是指针所指向的地址变了,其结果为每个像素(三个字节)的首地址,因此我们就可以用[]
来访问这三个字节
为偶数,我们就给这三个字节都赋值为0,即rgb三原色为0,就为黑色,否则rgb
都赋值为0XFF
,就为白色
最后,我们还需要将上面的所有数据写入文件中:
ofstream f("tmp.bmp");
f.write((const char*)&kFileHeader, sizeof(kFileHeader));
f.write((const char*)&kInfoHeader, sizeof(kInfoHeader));
f.write((const char*)buf, dataSize);
然后就可以在目录中看到生成的图片:
完整代码: