HarmonyOS ScanKit 二维码名牌:从编号到可核对的图像【共创稿事节】

一、开门之前,先确定这张名牌到底写了什么
设想一次小型技术交流活动:入口需要展示来宾编号,工作人员希望把同一个编号放进二维码,便于在另一台设备上读取。作为技术负责人,我会先把需求拆成两件事:人眼看到的编号是什么,扫码得到的文本是什么。只有把两者关系固定下来,后面讨论二维码尺寸、颜色和资源释放才有明确对象。
本文使用一个公开的示例编号,不包含姓名或联系方式。页面默认填入 GUEST-017,编码文本固定采用前缀、版本和编号拼接,得到 badge:v1:GUEST-017。前缀说明数据用途,版本为后续格式调整留出位置,编号则与卡片上展示的文字对应。它是普通文本,不会因为带有冒号就自动成为经过验证的业务凭证。
这里的任务是生成二维码图像,不需要打开摄像头,也没有相机预览页面。生成端拿到图像后交给界面展示,读取端再把二维码还原成文本。把两个方向分开,工程配置和排查范围都会更清晰:生成失败应先检查内容与生成参数,不能从扫描权限开始猜原因。
为了让资源所有权保持直观,这个页面每次进入只成功生成一张名牌。生成前可以修改编号,成功以后输入框与生成按钮都停止编辑;需要换编号时重新进入页面。这个边界适合讲清最小闭环,也避免在同一张图像仍被显示时,匆忙释放并替换它。

二、编号规则应先于二维码样式
编号在生成前去掉两端空白并转换成大写,然后只接受一到十二位字母、数字或短横线。这是本文为名牌定义的业务规则,不是二维码标准只能容纳这些字符。规则收窄以后,卡片排版、扫码文本核对和人工转录都比较直接,超出规则的输入在调用生成接口之前就得到可读提示。
不要分别拼接卡片文字和编码文本。代码先得到规范化编号,再从这个编号构造唯一的内容字符串,成功以后把同一字符串保存到页面。这样可以避免输入已经变成新值,图像却仍编码旧值的情况。生成期间禁用输入,也让异步任务拥有稳定的输入快照。
编码内容越长,图案通常需要承载更多信息。名牌没有必要把整份活动说明、页面布局或用户资料塞进同一二维码。当前只放一个用途明确的编号文本,读取方可以清楚地显示和检查它;以后若要查询后台记录,再为查询协议单独设计校验、有效期与访问规则。
这也说明生成成功与业务可信之间的边界。图像能够被读取,只证明读取端还原出一段文本,并不能证明持有人已经登记或拥有入场权限。当前示例的目标是编号图像化,页面没有把生成动作命名为授权或核验,避免界面文案赋予二维码并不存在的能力。
白底黑码是本例的固定配色。卡片外部可以采用温和的界面颜色,但编码区域不叠图标、不加纹理、不拉伸成非正方形。名牌的视觉识别来自旁边的标题和编号,二维码本身优先保持清楚的模块边界与对比度,这样也更容易比较不同读取设备的结果。
三、完整页面怎样接住异步结果
创建 BadgeQR Stage 模型 Empty Ability 工程,使用 HarmonyOS API 14 SDK,将下方代码放入默认 entry/src/main/ets/pages/Index.ets,保留模板加载该页面的入口和页面配置。生成能力与码制枚举从扫描工具包导入,图像类型从图像工具包导入,错误类型从基础服务工具包导入。
字符串生成接口从接口十一开始提供,本文使用的参数在接口十四中可用。它返回一个异步的像素图对象,随后可以交给图像组件展示。这里不调用相机扫描接口,不需要申请相机权限;也不把图像写入公共目录,因此没有额外文件访问流程。
import { generateBarcode, scanCore } from '@kit.ScanKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct Index {
private alive: boolean = false;
private ownedMap: image.PixelMap | undefined = undefined;
@State private badgeId: string = 'GUEST-017';
@State private busy: boolean = false;
@State private qr: image.PixelMap | undefined = undefined;
@State private payload: string = '';
@State private status: string = '确认编号后生成名牌';
aboutToAppear(): void { this.alive = true; }
private async releaseMap(map: image.PixelMap): Promise<void> {
try { await map.release(); }
catch (e) { console.error('BadgeQR PixelMap release failed'); }
}
aboutToDisappear(): void {
this.alive = false;
const owned = this.ownedMap;
this.ownedMap = undefined;
if (owned !== undefined) { this.releaseMap(owned); }
}
private async generate(): Promise<void> {
if (this.busy || this.qr !== undefined || !this.alive) { return; }
const id = this.badgeId.trim().toUpperCase();
if (!/^[A-Z0-9-]{1,12}$/.test(id)) {
this.status = '编号限 1 至 12 位字母、数字或短横线';
return;
}
this.badgeId = id;
const content = `badge:v1:${id}`;
this.busy = true;
this.status = '正在生成';
try {
const options: generateBarcode.CreateOptions = {
scanType: scanCore.ScanType.QR_CODE,
width: 600, height: 600,
margin: 10,
level: generateBarcode.ErrorCorrectionLevel.LEVEL_M,
backgroundColor: 0xFFFFFF,
pixelMapColor: 0x000000
};
const result = await generateBarcode.createBarcode(content, options);
if (!this.alive) {
await this.releaseMap(result);
return;
}
this.ownedMap = result;
this.qr = result;
this.payload = content;
this.status = '名牌已生成';
} catch (e) {
const error = e as BusinessError;
if (this.alive) { this.status = `生成失败:${error.code}`; }
} finally {
if (this.alive) { this.busy = false; }
}
}
build() {
Scroll() {
Column({ space: 18 }) {
Text('二维码名牌').fontSize(28).fontWeight(FontWeight.Bold)
Text('编号转成可读的二维码').fontSize(16).fontColor('#6D747D')
TextInput({ text: this.badgeId, placeholder: '输入名牌编号' })
.height(48).fontSize(20).maxLength(12)
.enabled(!this.busy && this.qr === undefined)
.onChange((value: string) => { this.badgeId = value; })
if (this.qr !== undefined) {
Column({ space: 12 }) {
Image(this.qr).width(240).height(240).objectFit(ImageFit.Contain)
Text(this.badgeId).fontSize(24).fontWeight(FontWeight.Bold)
Text(this.payload).fontSize(15).fontColor('#495469')
}.padding(36).width('100%').backgroundColor(Color.White).borderRadius(20)
}
Text(this.status).fontSize(18).fontColor('#4E657D')
Button(this.busy ? '正在生成' : '生成名牌')
.width('100%').backgroundColor('#4E657D')
.enabled(!this.busy && this.qr === undefined)
.onClick(() => { this.generate(); })
Text('本页生成一张名牌,离开页面后释放图像')
.fontSize(15).fontColor('#6D747D')
}.padding(24).width('100%')
}.width('100%').height('100%').backgroundColor('#F5F5F2')
}
}
图像生成尺寸是六百乘六百像素,页面展示尺寸是二百四十乘二百四十视口单位。这是生成分辨率与界面布局两种不同单位,不能因为数值不同就认定图像被错误压缩。页面保持正方形并使用等比例适配,周围另留白色内边距,让名牌内容与二维码边缘保持距离。
生成选项中的边距明确写为十像素,处于接口规定的有效范围内。它不是外层卡片的三十六视口单位,也不应被当成二维码模块数量来解释。真正检查显示效果时,需要观察最终图像四周是否保留干净空白,而不是只核对某一个字段的数字。
生成参数也不应该由输入框直接任意控制。当前宽高、边距、颜色和纠错等级都是页面固定配置,用户只决定业务编号。这样排查失败时能先把输入变化与图像配置变化分开。将来提供尺寸选择,也应给出经过检查的有限选项,而不是把任意数字原样传入接口。
纠错等级使用中等级。纠错能力并不意味着可以任意遮挡定位图案,也不能保证任何光照与拍摄角度都能识别。当前页面没有在二维码中嵌入标志,因此不需要用遮挡换取装饰。增加品牌元素时,可以优先放在编码区域之外,再重新完成读取检查。

四、看见图像以后,还要核对内容与生命周期
先使用默认编号生成名牌,再用另一台具备二维码读取能力的设备读取页面。预期得到的完整文本是 badge:v1:GUEST-017,要逐段确认前缀、版本、大小写和编号都一致。只看到读取器出现识别框,不能代替内容核对;读取器如果只展示摘要,也应打开它的完整结果。
接着重新进入页面,把编号改成带空白的小写形式,观察生成前的规范化是否按规则发生。再尝试空字符串、中文或者不允许的标点,应得到输入提示,并且不开始生成。输入框长度限制可以减少误输,但真正决定接受什么的是生成方法里的校验,不能只依靠控件限制。
如果接口返回失败,页面显示错误码并恢复按钮可用性。此时没有成功图像需要保留,用户可以修正输入或重新尝试。排查时先核对码制、尺寸和内容,再结合错误码看设备支持情况。不要把失败分支改成显示一张固定二维码,否则表面上有图,实际内容可能与当前编号无关。
更容易遗漏的是生成过程中离开页面。异步任务已经发出,页面退出并不自动让返回结果消失。代码在结果到达后检查页面是否仍存活:如果已经离开,立即释放新返回的像素图,不再把它写回页面。这个分支处理的是尚未交给界面使用的结果,因此所有权很清楚。
成功展示的像素图则由当前页面持有,直到页面销毁时释放。释放方法本身也是异步接口,代码接住它的失败分支,避免出现无人处理的异步异常。不要在图像刚生成后就立刻释放,也不要认为把状态变量改为空就已经主动释放了原生图像资源。

五、把名牌交付出去时,哪些条件仍需要保留
真正交付给活动使用时,首先保留内容核对步骤。编号清楚、码能够读、读出的文本与编号一致,三项应该同时成立。打印尺寸、纸张反光、屏幕亮度与读取距离都会影响实际读取体验,所以界面上显示正确以后,还应按最终使用方式检查,不能把一次近距离读取推断成所有条件都适用。
如果需求扩展为保存或分享,应明确输出的是完整名牌还是纯二维码。保存完整名牌需要保留人眼可读编号,保存纯二维码则要保证外部使用时仍有足够空白。无论采用哪种方式,都应从已经确认的同一内容生成结果,不要在导出路径里再次使用可能变化的输入框值。
另一个扩展方向是同页连续生成。这时就不能简单删除已有图像时禁止生成的守卫,还要设计旧图何时停止被使用、新图何时接管、过期异步结果如何处置,以及失败时是否保留旧图。当前一次一张的设计有意让这些责任清楚可见,后续扩展应该增加明确状态,而不是只多放一个按钮。
批量名牌还需要为每张图像保存对应的内容记录,避免异步任务完成顺序与列表顺序不同而造成错配。应按稳定编号关联结果,而不是假设第一个返回的图像一定属于第一条输入。当前只有一张名牌,但输入快照与结果内容绑定的原则可以直接延伸到批量场景。
版本前缀也值得保留。未来编号格式发生变化,读取方可以根据版本决定怎样解释;无法识别的版本应该有明确结果,不能猜测字段含义。生成端和读取端使用同一协议定义,才能让二维码长期承担可靠的数据交换入口,而不是一张只有当前页面知道含义的图片。
这张名牌最终只有一个短编号和一块黑白图案,但它经过的过程并不只有调用一次接口:先确定内容,固定输入快照,生成并展示,核对读取结果,再按页面生命周期释放资源。把这些步骤写成可追踪的代码,后续增加样式、导出或批量功能时,就有一个清楚的起点。
参考资料:
更多推荐



所有评论(0)