HarmonyOS NEXT 打印机开发实战:基于 Print Kit 全面解析

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 搜索不到打印机

排查步骤

  1. 确认打印机与设备处于同一 Wi-Fi 局域网
  2. 确认打印机支持 Mopria / IPP over Wi-Fi 协议
  3. 在系统设置 → 更多连接 → 打印 中确认打印服务已启用
  4. 确认安装了对应品牌的 Printer Extension
  5. 尝试重启打印机与 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 等多种场景。

如果本文对你有帮助,欢迎点赞收藏!有问题欢迎在评论区留言交流。


参考资料

相关推荐
三声三视2 小时前
Electron + 鸿蒙分布式投屏:PC 端一键推送画面到鸿蒙设备全实战
分布式·electron·harmonyos·鸿蒙·桌面
UnicornDev2 小时前
【Flutter x HarmonyOS 6】魔方计时APP——挑战页面的UI设计
flutter·ui·华为·harmonyos·鸿蒙
三声三视2 小时前
鸿蒙 ArkTS 后台任务全攻略:短时任务、长驻任务与延迟任务实战,告别应用被系统杀掉的困境
华为·harmonyos·鸿蒙
HwJack203 小时前
深潜 HarmonyOS APP开发中AVSession 音视频会话管理
华为·音视频·harmonyos
枫叶丹43 小时前
【HarmonyOS 6.0】模拟点击检测:鸿蒙6.0全面狙击自动化作弊行为
开发语言·华为·自动化·harmonyos
坚果派·白晓明3 小时前
【鸿蒙PC三方库移植适配框架解读系列】第六篇:关键注意事项与最佳实践
c语言·开发语言·c++·华为·harmonyos·开源鸿蒙
Random_index3 小时前
#Harmony篇:@ohos/axios和Navigation(this.stack)
harmonyos
音视频牛哥16 小时前
大牛直播SDK(SmartMediaKit)鸿蒙NEXT RTSP/RTMP低延迟播放器集成与实践指南
音视频·harmonyos·大牛直播sdk·鸿蒙rtmp播放器·鸿蒙rtsp播放器·鸿蒙next rtsp播放器·鸿蒙next rtmp播放器
廖松洋(Alina)18 小时前
02数据模型与单词仓库-鸿蒙PC端Electron开发
前端·华为·electron·开源·harmonyos·鸿蒙