HarmonyOS NEXT 打印机开发实战:基于 Print Kit 全面解析
- [HarmonyOS NEXT 打印机开发实战:基于 Print Kit 全面解析](#HarmonyOS NEXT 打印机开发实战:基于 Print Kit 全面解析)
-
- 前言
- [一、Print Kit 架构解析](#一、Print Kit 架构解析)
-
- [1.1 整体架构](#1.1 整体架构)
- [1.2 核心类说明](#1.2 核心类说明)
- 二、环境准备
-
- [2.1 module.json5 权限配置](#2.1 module.json5 权限配置)
- [2.2 依赖引入](#2.2 依赖引入)
- [三、核心 API 详解](#三、核心 API 详解)
-
- [3.1 PrintAttributes 打印属性](#3.1 PrintAttributes 打印属性)
- 四、实战:打印纯文本
-
- [4.1 TextPrintAdapter 实现](#4.1 TextPrintAdapter 实现)
- [4.2 PrintViewModel:封装打印逻辑](#4.2 PrintViewModel:封装打印逻辑)
- 五、实战:打印图片
- [六、实战:打印 PDF 文件](#六、实战:打印 PDF 文件)
- 七、打印机发现与管理
- [八、完整页面 UI 实现](#八、完整页面 UI 实现)
- 九、权限动态申请
- 十、常见问题与解决方案
-
- [10.1 权限被拒绝(Error Code 201)](#10.1 权限被拒绝(Error Code 201))
- [10.2 搜索不到打印机](#10.2 搜索不到打印机)
- [10.3 打印内容乱码](#10.3 打印内容乱码)
- [10.4 大文件打印超时](#10.4 大文件打印超时)
- 十一、总结
HarmonyOS NEXT 打印机开发实战:基于 Print Kit 全面解析
前言
随着 HarmonyOS NEXT 的持续演进,华为在办公生态领域的布局日趋完善。Print Kit(打印服务套件)作为 HarmonyOS API 11 正式引入的系统级能力,为开发者提供了一套完整、标准化的打印解决方案。本文将深入剖析 Print Kit 的架构设计,并通过完整的 ArkTS 代码示例,带你从零到一实现一个功能完备的打印功能模块。
适合读者:具备 HarmonyOS 基础开发经验,了解 ArkTS 语法的开发者。
本文涵盖内容:
- Print Kit 整体架构与核心概念
- 打印权限配置与环境准备
- PrintJob 生命周期管理
- 打印文档(文本 / 图片 / PDF)完整实现
- 自定义打印参数(纸张、方向、份数、双面)
- Wi-Fi 打印机发现与连接
- 常见问题排查
一、Print Kit 架构解析
1.1 整体架构
HarmonyOS Print Kit 采用 Client-Service 分层架构:
┌─────────────────────────────────────────┐
│ 应用层 (App) │
│ PrintDocumentAdapter / PrintJob │
└──────────────────┬──────────────────────┘
│ IPC
┌──────────────────▼──────────────────────┐
│ Print Manager Service │
│ (系统服务,管理打印队列、打印机发现) │
└──────────────────┬──────────────────────┘
│
┌──────────────────▼──────────────────────┐
│ Printer Extension │
│ (OEM 厂商实现,Wi-Fi / USB 打印机) │
└─────────────────────────────────────────┘
1.2 核心类说明
| 类 / 接口 | 作用 |
|---|---|
print.PrintManager |
打印管理器,发起打印任务的入口 |
print.PrintJob |
表示一个打印任务,携带打印参数 |
print.PrintAttributes |
打印属性(纸张、方向、质量、范围) |
print.PrintDocumentAdapter |
开发者实现此接口,提供实际打印内容 |
print.PrintPageRange |
打印页码范围 |
print.PrinterInfo |
打印机信息(名称、能力、状态) |
二、环境准备
2.1 module.json5 权限配置
json
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.PRINT",
"reason": "$string:print_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
2.2 依赖引入
typescript
import { print } from '@kit.PrintKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { fileIo } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
import { util } from '@kit.ArkTS';
三、核心 API 详解
3.1 PrintAttributes 打印属性
typescript
let attributes: print.PrintAttributes = {
pageSize: print.PrintPageSize.ISO_A4,
orientation: print.PrintOrientationMode.PORTRAIT,
printQuality: print.PrintQuality.NORMAL,
copies: 1,
duplexMode: print.PrintDuplexMode.NONE,
pageRange: { startPage: 0, endPage: 0, pages: [] },
colorMode: print.PrintColorMode.COLOR
};
PrintPageSize 常用枚举:
| 枚举值 | 尺寸 |
|---|---|
ISO_A4 |
210 × 297 mm |
ISO_A3 |
297 × 420 mm |
ISO_A5 |
148 × 210 mm |
NA_LETTER |
216 × 279 mm(美式信纸) |
ISO_B5 |
176 × 250 mm |
四、实战:打印纯文本
4.1 TextPrintAdapter 实现
typescript
import { print } from '@kit.PrintKit';
import { fileIo } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
import { BusinessError } from '@kit.BasicServicesKit';
class TextPrintAdapter implements print.PrintDocumentAdapter {
private content: string = '';
constructor(content: string) {
this.content = content;
}
onStartLayoutWrite(
jobId: string,
oldAttrs: print.PrintAttributes,
newAttrs: print.PrintAttributes,
fd: number,
writeResultCallback: (jobId: string, result: print.PrintFileCreationState) => void
): void {
try {
let encoder = new util.TextEncoder();
let encoded = encoder.encodeInto(this.content);
fileIo.writeSync(fd, encoded.buffer);
writeResultCallback(jobId, print.PrintFileCreationState.PRINT_FILE_CREATED);
} catch (err) {
let error = err as BusinessError;
console.error(`写入失败: ${error.code} - ${error.message}`);
writeResultCallback(jobId, print.PrintFileCreationState.PRINT_FILE_CREATION_FAILED);
}
}
onJobStateChanged(jobId: string, state: print.PrintJobState): void {
switch (state) {
case print.PrintJobState.PRINT_JOB_PREPARE: {
console.info(`任务 ${jobId} 准备中`);
break;
}
case print.PrintJobState.PRINT_JOB_RUNNING: {
console.info(`任务 ${jobId} 打印中`);
break;
}
case print.PrintJobState.PRINT_JOB_COMPLETED: {
console.info(`任务 ${jobId} 已完成`);
break;
}
case print.PrintJobState.PRINT_JOB_BLOCKED: {
console.warn(`任务 ${jobId} 被阻塞(缺纸/卡纸)`);
break;
}
case print.PrintJobState.PRINT_JOB_FAIL: {
console.error(`任务 ${jobId} 失败`);
break;
}
default:
break;
}
}
}
4.2 PrintViewModel:封装打印逻辑
typescript
import { print } from '@kit.PrintKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
@ObservedV2
class PrintViewModel {
@Trace isPrinting: boolean = false;
@Trace statusText: string = '就绪';
@Trace selectedCopies: number = 1;
@Trace isLandscape: boolean = false;
@Trace duplexEnabled: boolean = false;
async printText(context: common.UIAbilityContext, text: string): Promise<void> {
if (this.isPrinting) {
return;
}
this.isPrinting = true;
this.statusText = '正在提交打印任务...';
try {
let adapter = new TextPrintAdapter(text);
let attributes: print.PrintAttributes = {
pageSize: print.PrintPageSize.ISO_A4,
orientation: this.isLandscape
? print.PrintOrientationMode.LANDSCAPE
: print.PrintOrientationMode.PORTRAIT,
printQuality: print.PrintQuality.HIGH,
copies: this.selectedCopies,
duplexMode: this.duplexEnabled
? print.PrintDuplexMode.LONG_EDGE
: print.PrintDuplexMode.NONE,
colorMode: print.PrintColorMode.COLOR
};
let printTask = await print.print(
'HarmonyOS 文本打印',
adapter,
attributes,
context
);
this.statusText = '已提交到打印队列';
printTask.on('succeed', () => {
this.isPrinting = false;
this.statusText = '✓ 打印完成';
});
printTask.on('fail', () => {
this.isPrinting = false;
this.statusText = '✗ 打印失败';
});
printTask.on('cancel', () => {
this.isPrinting = false;
this.statusText = '已取消打印';
});
} catch (err) {
let error = err as BusinessError;
this.isPrinting = false;
this.statusText = `错误: ${error.message}`;
console.error(`打印失败 code=${error.code}`);
}
}
}
五、实战:打印图片
typescript
import { print } from '@kit.PrintKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
class ImagePrintAdapter implements print.PrintDocumentAdapter {
private pixelMap: image.PixelMap;
constructor(pixelMap: image.PixelMap) {
this.pixelMap = pixelMap;
}
onStartLayoutWrite(
jobId: string,
oldAttrs: print.PrintAttributes,
newAttrs: print.PrintAttributes,
fd: number,
writeResultCallback: (jobId: string, result: print.PrintFileCreationState) => void
): void {
let imagePacker = image.createImagePacker();
let packOption: image.PackingOption = { format: 'image/jpeg', quality: 95 };
imagePacker.packToFile(this.pixelMap, fd, packOption)
.then(() => {
imagePacker.release();
writeResultCallback(jobId, print.PrintFileCreationState.PRINT_FILE_CREATED);
})
.catch((err: BusinessError) => {
console.error(`图片打包失败: ${err.message}`);
imagePacker.release();
writeResultCallback(jobId, print.PrintFileCreationState.PRINT_FILE_CREATION_FAILED);
});
}
onJobStateChanged(jobId: string, state: print.PrintJobState): void {
console.info(`图片打印任务 ${jobId} 状态: ${state}`);
}
}
六、实战:打印 PDF 文件
typescript
import { print } from '@kit.PrintKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
class PdfPrintAdapter implements print.PrintDocumentAdapter {
private pdfPath: string = '';
constructor(pdfPath: string) {
this.pdfPath = pdfPath;
}
onStartLayoutWrite(
jobId: string,
oldAttrs: print.PrintAttributes,
newAttrs: print.PrintAttributes,
fd: number,
writeResultCallback: (jobId: string, result: print.PrintFileCreationState) => void
): void {
try {
let srcFile = fileIo.openSync(this.pdfPath, fileIo.OpenMode.READ_ONLY);
const CHUNK = 65536;
let buffer = new ArrayBuffer(CHUNK);
let readLen = 0;
do {
readLen = fileIo.readSync(srcFile.fd, buffer, { offset: 0, length: CHUNK });
if (readLen > 0) {
fileIo.writeSync(fd, buffer, { offset: 0, length: readLen });
}
} while (readLen === CHUNK);
fileIo.closeSync(srcFile);
writeResultCallback(jobId, print.PrintFileCreationState.PRINT_FILE_CREATED);
} catch (err) {
let error = err as BusinessError;
console.error(`PDF 读取失败: ${error.message}`);
writeResultCallback(jobId, print.PrintFileCreationState.PRINT_FILE_CREATION_FAILED);
}
}
onJobStateChanged(jobId: string, state: print.PrintJobState): void {
console.info(`PDF 打印任务 ${jobId} 状态: ${state}`);
}
}
七、打印机发现与管理
typescript
import { print } from '@kit.PrintKit';
import { BusinessError } from '@kit.BasicServicesKit';
@ObservedV2
class PrinterViewModel {
@Trace printers: print.PrinterInfo[] = [];
@Trace isDiscovering: boolean = false;
@Trace selectedPrinterId: string = '';
async startDiscover(): Promise<void> {
this.isDiscovering = true;
this.printers = [];
try {
print.on('printerAdded', (printerInfo: print.PrinterInfo) => {
this.printers = [...this.printers, printerInfo];
console.info(`发现打印机: ${printerInfo.printerName}`);
});
print.on('printerRemoved', (printerInfo: print.PrinterInfo) => {
this.printers = this.printers.filter(
(p: print.PrinterInfo) => p.printerId !== printerInfo.printerId
);
});
print.on('printerStateChanged', (printerInfo: print.PrinterInfo) => {
let index = this.printers.findIndex(
(p: print.PrinterInfo) => p.printerId === printerInfo.printerId
);
if (index >= 0) {
let updated = [...this.printers];
updated[index] = printerInfo;
this.printers = updated;
}
});
await print.startDiscoverPrinter([]);
} catch (err) {
let error = err as BusinessError;
console.error(`搜索失败: ${error.code} - ${error.message}`);
this.isDiscovering = false;
}
}
async stopDiscover(): Promise<void> {
try {
await print.stopDiscoverPrinter();
print.off('printerAdded');
print.off('printerRemoved');
print.off('printerStateChanged');
} catch (err) {
let error = err as BusinessError;
console.error(`停止搜索失败: ${error.message}`);
} finally {
this.isDiscovering = false;
}
}
async queryCapability(printerId: string): Promise<void> {
try {
let cap = await print.queryPrinterCapability(printerId);
console.info(`支持纸张: ${JSON.stringify(cap.pageSizes)}`);
console.info(`支持分辨率: ${JSON.stringify(cap.resolutions)}`);
} catch (err) {
let error = err as BusinessError;
console.error(`查询能力失败: ${error.message}`);
}
}
}
八、完整页面 UI 实现
typescript
import { print } from '@kit.PrintKit';
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@ComponentV2
struct PrintPage {
@Local private vm: PrintViewModel = new PrintViewModel();
@Local private printerVm: PrinterViewModel = new PrinterViewModel();
private context = getContext(this) as common.UIAbilityContext;
private printContent: string = 'HarmonyOS 打印测试文档\n\n打印时间:' + new Date().toLocaleString();
aboutToAppear(): void {
this.printerVm.startDiscover();
}
aboutToDisappear(): void {
this.printerVm.stopDiscover();
}
@Builder
private renderSettings(): void {
Column({ space: 14 }) {
Text('打印设置').fontSize(16).fontWeight(FontWeight.Medium).alignSelf(ItemAlign.Start)
Row() {
Text('打印份数').fontSize(14).flexGrow(1)
Counter() {
Text(`${this.vm.selectedCopies}`).fontSize(14)
}
.onInc(() => {
if (this.vm.selectedCopies < 99) {
this.vm.selectedCopies++;
}
})
.onDec(() => {
if (this.vm.selectedCopies > 1) {
this.vm.selectedCopies--;
}
})
}
.width('100%')
Row() {
Text('横向打印').fontSize(14).flexGrow(1)
Toggle({ type: ToggleType.Switch, isOn: this.vm.isLandscape })
.onChange((isOn: boolean) => { this.vm.isLandscape = isOn; })
}
.width('100%')
Row() {
Text('双面打印').fontSize(14).flexGrow(1)
Toggle({ type: ToggleType.Switch, isOn: this.vm.duplexEnabled })
.onChange((isOn: boolean) => { this.vm.duplexEnabled = isOn; })
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(12)
.shadow({ radius: 6, color: '#18000000', offsetX: 0, offsetY: 2 })
}
@Builder
private renderPrinterList(): void {
Column({ space: 8 }) {
Row() {
Text('可用打印机').fontSize(16).fontWeight(FontWeight.Medium).flexGrow(1)
if (this.printerVm.isDiscovering) {
LoadingProgress().width(20).height(20).color('#2196F3')
}
}
.width('100%')
if (this.printerVm.printers.length === 0) {
Text('未发现打印机,请确认与打印机处于同一 Wi-Fi')
.fontSize(13).fontColor('#999').textAlign(TextAlign.Center)
.width('100%').padding(16)
} else {
ForEach(this.printerVm.printers, (printer: print.PrinterInfo) => {
Row({ space: 12 }) {
Column({ space: 4 }) {
Text(printer.printerName).fontSize(14).fontColor('#333')
Text(printer.printerId).fontSize(11).fontColor('#999')
}
.flexGrow(1)
.alignItems(HorizontalAlign.Start)
Radio({ value: printer.printerId, group: 'printerGroup' })
.checked(this.printerVm.selectedPrinterId === printer.printerId)
.onChange((checked: boolean) => {
if (checked) {
this.printerVm.selectedPrinterId = printer.printerId;
this.printerVm.queryCapability(printer.printerId);
}
})
}
.width('100%').padding(12).backgroundColor('#FAFAFA').borderRadius(8)
})
}
}
.width('100%').padding(16).backgroundColor(Color.White)
.borderRadius(12).shadow({ radius: 6, color: '#18000000', offsetX: 0, offsetY: 2 })
}
build() {
Column({ space: 16 }) {
Row() {
Text('HarmonyOS 打印').fontSize(22).fontWeight(FontWeight.Bold)
.fontColor('#1a1a1a').flexGrow(1)
Text(this.vm.statusText).fontSize(13)
.fontColor(this.vm.isPrinting ? '#FF8C00' : '#4CAF50')
}
.width('100%').padding({ top: 16, bottom: 4 })
Scroll() {
Column({ space: 16 }) {
this.renderPrinterList()
this.renderSettings()
Column({ space: 8 }) {
Text('打印内容预览').fontSize(14).fontWeight(FontWeight.Medium).alignSelf(ItemAlign.Start)
Text(this.printContent).fontSize(13).fontColor('#555')
.maxLines(6).textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%').padding(16).backgroundColor(Color.White).borderRadius(12)
Button(this.vm.isPrinting ? '打印中...' : '开始打印')
.width('100%').height(52).fontSize(17).fontWeight(FontWeight.Medium)
.backgroundColor(this.vm.isPrinting ? '#BDBDBD' : '#2196F3')
.borderRadius(12).enabled(!this.vm.isPrinting)
.onClick(() => { this.vm.printText(this.context, this.printContent); })
}
}
.scrollBar(BarState.Off).flexGrow(1)
}
.width('100%').height('100%')
.padding({ left: 20, right: 20, bottom: 24 })
.backgroundColor('#F0F2F5')
}
}
九、权限动态申请
typescript
import { abilityAccessCtrl, Permissions, common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
async function requestPrintPermission(context: common.UIAbilityContext): Promise<boolean> {
let atManager = abilityAccessCtrl.createAtManager();
let permissions: Permissions[] = ['ohos.permission.PRINT'];
try {
let result = await atManager.requestPermissionsFromUser(context, permissions);
let granted = result.authResults[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
if (!granted) {
console.warn('用户拒绝了打印权限');
}
return granted;
} catch (err) {
let error = err as BusinessError;
console.error(`权限申请失败: ${error.message}`);
return false;
}
}
十、常见问题与解决方案
10.1 权限被拒绝(Error Code 201)
现象 :调用 print.print() 抛出 code=201 异常。
解决 :在 aboutToAppear() 中提前调用 requestPrintPermission(),确保权限已授予再发起打印。
10.2 搜索不到打印机
排查步骤:
- 确认打印机与设备处于同一 Wi-Fi 局域网
- 确认打印机支持 Mopria / IPP over Wi-Fi 协议
- 在系统设置 → 更多连接 → 打印 中确认打印服务已启用
- 确认安装了对应品牌的 Printer Extension
- 尝试重启打印机与 Wi-Fi 路由器
10.3 打印内容乱码
原因:写入时编码不匹配。
解决:统一使用 UTF-8,对老式打印机附加 BOM 头:
typescript
let bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
fileIo.writeSync(fd, bom.buffer, { offset: 0, length: 3 });
// 再写入 UTF-8 正文
10.4 大文件打印超时
解决 :使用 TaskPool 在子线程执行文件 I/O:
typescript
import taskpool from '@ohos.taskpool';
import { fileIo } from '@kit.CoreFileKit';
@Concurrent
function copyFileToFd(srcPath: string, fd: number): boolean {
let srcFile = fileIo.openSync(srcPath, fileIo.OpenMode.READ_ONLY);
let buffer = new ArrayBuffer(65536);
let readLen = 0;
do {
readLen = fileIo.readSync(srcFile.fd, buffer, { offset: 0, length: 65536 });
if (readLen > 0) {
fileIo.writeSync(fd, buffer, { offset: 0, length: readLen });
}
} while (readLen > 0);
fileIo.closeSync(srcFile);
return true;
}
十一、总结
| 功能点 | API | 关键说明 |
|---|---|---|
| 发起打印 | print.print() |
返回 PrintTask,通过事件监听状态 |
| 提供内容 | onStartLayoutWrite |
向系统 fd 写入文件 |
| 状态监听 | onJobStateChanged |
追踪任务生命周期 |
| 发现打印机 | print.startDiscoverPrinter() |
配合 printerAdded 事件 |
| 查询能力 | print.queryPrinterCapability() |
获取纸张、分辨率、双面支持 |
| 权限申请 | requestPermissionsFromUser() |
动态申请打印权限 |
HarmonyOS Print Kit 以标准化接口 + 分层架构 为核心设计,将应用与底层打印硬件彻底解耦。结合 V2 状态管理体系 (@ObservedV2 + @Trace)和 MVVM 架构分层思路,可以构建高可维护性的打印模块,轻松应对文本、图片、PDF 等多种场景。
如果本文对你有帮助,欢迎点赞收藏!有问题欢迎在评论区留言交流。
参考资料