HarmonyOS鸿蒙Next中pc如何实现截图功能

HarmonyOS鸿蒙Next中pc如何实现截图功能 鸿蒙pc端如何实现截图功能,流程是什么样的

6 回复

【背景知识】

HarmonyOS开发中有三种截图方式,分别是组件截图、窗口截图和屏幕截图:

  • getComponentSnapshot组件截图:提供获取组件截图的能力,包括已加载的组件的截图和没有加载的组件的截图。组件截图只能够截取组件大小的区域,如果组件的绘制超出了它的区域,或子组件的绘制超出了父组件的区域,这些在组件区域外绘制的内容不会在截图中呈现。兄弟节点堆叠在组件区域内,截图不会显示兄弟组件。
  • snapshot:提供窗口截图能力,使用前需要用getLastWindow()createWindow()findWindow()中的任一方法获取到Window实例(windowClass)。
  • 屏幕截图:提供屏幕截图能力,根据screenshot.pickscreenshot.capture文档描述,仅2in1设备或平板设备可以使用,如果是手机使用则会报错:“Failed to pick. Code: {“code”:801}”。

其中常见的截图方式为组件截图、窗口截图,两者区别如下:

维度 组件截图 窗口截图
截取范围 组件区域内。 整个window窗口。
技术实现 componentSnapshot.get。 window.getLastWindow获取窗口后win.snapshot。
安全性 更安全(可避免敏感信息泄露)。 需处理敏感信息(如密码、支付界面)。
业务场景 组件内容转成图片,比如商品信息。 用户分享界面、崩溃时自动截取上下文发送报告。

【解决方案】

  • 截图实现方式。
    • 组件截图实现: 组件截图可参考官网示例使用组件截图
    • 窗口截图实现: 用window.getLastWindow获取到Window实例,然后使用snapshot方法保存窗口截图。
import { window } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct Snapshot {
  @State pixmap: PixelMap | string = '';

  build() {
    Column() {
      Button('点击').onClick(() => {
        let context = this.getUIContext().getHostContext() as Context as common.UIAbilityContext;
        window.getLastWindow(context).then(win => {
          let promise = win.snapshot();
          promise.then((pixelMap: image.PixelMap) => {
            this.pixmap = pixelMap;
          });
        });
      });
      if (this.pixmap) {
        Image(this.pixmap)
          .borderWidth(1)
          .width(300);
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(Color.Pink);
  }
}
  • 屏幕截图实现: 参考官网screenshot.capture实现。
  • 截图常见场景。
    • 场景一:长截图场景。 当用户截图分享和保存(如聊天记录、网页文章、活动海报等)的内容较长的时候,需要用户多次截图来保证内容完整性,这时可以参考官网长截图实现。
    • 场景二:截图添加水印场景。 当用户截图后需要对图片添加版权保护信息、标识或者艺术效果的时候,可以参照官网水印添加实现。
  • 截图后保存到相册。 三种截图方式在调用API后都会返回image.PixelMap数据,这时可以利用SaveButton实现图片保存到相册,完整示例代码如下:

1.在EntryAbility.ets中保存windowStage。

import { ConfigurationConstant, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  onCreate(): void {
    try {
      this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
    } catch (err) {
      hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
    }
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy(): void {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    AppStorage.setOrCreate('windowStage', windowStage);
    // Main window is created, set main page for this ability
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      AppStorage.setOrCreate('windowStage',windowStage);
      hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
    });
  }

  onWindowStageDestroy(): void {
    // Main window is destroyed, release UI related resources
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
    // Ability has brought to foreground
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
    // Ability has back to background
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
  }
};

2.WatermarkShot.ets。

import { image } from '@kit.ImageKit';
import { window } from '@kit.ArkUI';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

export interface ImagePixelMap {
  pixelMap: image.PixelMap;
  width: number;
  height: number;
}

export async function imageSource2PixelMap(pixelMap: image.PixelMap): Promise<ImagePixelMap> {
  return pixelMap.getImageInfo().then((imageInfo: image.ImageInfo) => {
    const height = imageInfo.size.height;
    const width = imageInfo.size.width;
    const result: ImagePixelMap = { pixelMap, width, height };
    return result;
  });
}

export function addWatermark(
  imagePixelMap: ImagePixelMap,
  drawWatermark?: (OffscreenContext: OffscreenCanvasRenderingContext2D) => void
): image.PixelMap {
  let windowStage = AppStorage.get('windowStage') as window.WindowStage;
  const height = windowStage.getMainWindowSync().getUIContext().px2vp(imagePixelMap.height);
  const width = windowStage.getMainWindowSync().getUIContext().px2vp(imagePixelMap.width);
  const offScreenCanvas = new OffscreenCanvas(width, height);
  const offScreenContext = offScreenCanvas.getContext('2d');
  offScreenContext.drawImage(imagePixelMap.pixelMap, 0, 0, width, height);
  if (drawWatermark) {
    drawWatermark(offScreenContext);
  } else {
    offScreenContext.fillStyle = '#10000000';
    offScreenContext.font = '16vp';
    const rotationAngle = -30;
    const watermarkHeight = 120;
    const watermarkWidth = 120;
    const watermarkText = '水印水印水印';
    const colCount = Math.ceil(imagePixelMap.height / watermarkHeight);
    const rowCount = Math.ceil(imagePixelMap.width / watermarkWidth);
    for (let col = 0; col <= colCount; col++) {
      let row = 0;
      for (; row <= rowCount; row++) {
        const angle = rotationAngle * Math.PI / 180;
        offScreenContext.rotate(angle);
        const positionX = rotationAngle > 0 ? watermarkHeight * Math.tan(angle) : 0;
        const positionY = rotationAngle > 0 ? 0 : watermarkWidth * Math.tan(-angle);
        offScreenContext.fillText(watermarkText, positionX, positionY);
        offScreenContext.rotate(-angle);
        offScreenContext.translate(0, watermarkHeight);
      }
      offScreenContext.translate(0, -watermarkHeight * row);
      offScreenContext.translate(watermarkWidth, 0);
    }
  }
  return offScreenContext.getPixelMap(0, 0, width, height);
}

@Entry
@Component
struct WatermarkShot {
  @State pixmap: image.PixelMap | undefined = undefined;
  saveButtonOptions: SaveButtonOptions = {
    icon: SaveIconStyle.FULL_FILLED,
    text: SaveDescription.SAVE_IMAGE,
    buttonType: ButtonType.Capsule
  };

  private saveToPhoto() {
    let packOpts: image.PackingOption = { format: 'image/jpeg', quality: 100 };
    const imagePackerApi = image.createImagePacker();
    imagePackerApi.packToData(this.pixmap, packOpts).then(async (buffer: ArrayBuffer) => {
      let file: fs.File | undefined = undefined;
      try {
        let context = this.getUIContext().getHostContext() as Context as common.UIAbilityContext;
        let helper = photoAccessHelper.getPhotoAccessHelper(context);
        let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
        file = fs.openSync(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
        fs.writeSync(file.fd, buffer);
        fs.closeSync(file);
      } catch (error) {
        try {
          if (file) {
            fs.close(file);
          }
        } catch (e) {

        }
      }
    });
  }

  build() {
    Column() {
      Row() {
        Text('添加水印页面');
      }.margin({ top: 30, bottom: 200 });

      SaveButton(this.saveButtonOptions)
        .onClick(() => {
          let context = this.getUIContext().getHostContext() as Context as common.UIAbilityContext;
          window.getLastWindow(context).then(win => {
            let promise = win.snapshot();
            promise.then(async (pixelMap: image.PixelMap) => {
              const imagePixelMap = await imageSource2PixelMap(pixelMap);
              this.pixmap = addWatermark(imagePixelMap);
              this.saveToPhoto();
            }).catch((err: BusinessError) => {
              console.error(`Failed to snapshot window. Cause code: ${err.code}, message: ${err.message}`);
            });
          });
        });
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center);
  }
}

更多关于HarmonyOS鸿蒙Next中pc如何实现截图功能的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


[@ohos.screenshot](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-screenshot#screenshotpick) 提供屏幕截图的能力。

capture(options?: CaptureOption): Promise<image.PixelMap>

获取屏幕全屏截图,使用Promise异步回调。

此接口可以通过设置不同的displayId截取不同屏幕的截图,且只能截取全屏;pick接口可实现区域截屏。

需要权限

API version 22前,需申请ohos.permission.CAPTURE_SCREEN权限;

从API version 22开始,需要申请ohos.permission.CAPTURE_SCREEN权限或ohos.permission.CUSTOM_SCREEN_RECORDING权限。

import { BusinessError } from '@kit.BasicServicesKit';
import { image } from '@kit.ImageKit';

let captureOption: screenshot.CaptureOption = {
  "displayId": 0
};
try {
  let promise = screenshot.capture(captureOption);
  promise.then((pixelMap: image.PixelMap) => {
    console.info('Succeeded in saving screenshot. Pixel bytes number: ' + pixelMap.getPixelBytesNumber());
    pixelMap.release(); // PixelMap使用完后及时释放内存
  }).catch((err: BusinessError) => {
    console.error(`Failed to save screenshot. Code: ${err.code}, message: ${err.message}`);
  });
} catch (exception) {
  console.error(`Failed to save screenshot. Code: ${exception.code}, message: ${exception.message}`);
};

可以直接调用系统提供的组件级截图接口 getUIContext().getComponentSnapshot(),把任意组件输出成 PixelMap,再借助 image.ImagePacker 生成 PNG 文件,最后通过 photoAccessHelper 存入系统相册。该方案还能通过循环滚屏和拼接实现“长截图”效果,适合需要对应用内部界面做精细化截图的场景

在HarmonyOS鸿蒙Next中,PC端可通过系统API实现截图功能。主要使用ScreenCapture类,调用captureScreen()方法获取屏幕图像数据。开发者需申请ohos.permission.CAPTURE_SCREEN权限,并在配置文件中声明。截取图像后,可保存为PixelMap对象或指定格式文件。

在HarmonyOS Next的PC应用开发中,实现截图功能的核心是使用ScreenCapture(屏幕捕获)相关API。主要流程如下:

  1. 申请权限:首先需要在应用的module.json5配置文件中声明必要的权限,例如ohos.permission.CAPTURE_SCREEN(捕获屏幕)用于全屏截图。如果涉及隐私区域,需同步进行动态权限申请。

  2. 创建ImageReceiver:通过image.createImageReceiver()方法创建一个ImageReceiver对象,并设置其参数(如尺寸、格式)。它将作为截图图像的接收器。

  3. 获取窗口/屏幕对象:若要截取当前应用窗口,可通过window.getLastWindow()获取窗口对象。若要截取整个屏幕,则需要使用ScreenCapture相关接口获取屏幕对象。

  4. 执行截图

    • 窗口截图:调用窗口对象的capture()方法,将前面创建的ImageReceiver传入,即可捕获该窗口的图像。
    • 屏幕截图:使用ScreenCapture模块的接口(如captureScreen)并指定ImageReceiver来捕获整个屏幕。
  5. 处理图像数据:截图操作是异步的。需要监听ImageReceiveron('imageArrival')事件。当事件触发时,从ImageReceiver中获取最新的Image对象,然后可以将其转换为PixelMap进行进一步处理(如保存到文件、显示在UI上或进行编辑)。

关键代码结构示意(以窗口截图为例):

// 1. 创建ImageReceiver
let imageReceiver = image.createImageReceiver(720, 1280, 4, 8); // 示例参数

// 2. 监听图像到达事件
imageReceiver.on('imageArrival', () => {
  let img = imageReceiver.readNextImage();
  if (img) {
    // 获取PixelMap并处理,例如保存
    let pixelMap = img.createPixelMap();
    // ... 保存pixelMap到文件或显示
    img.release(); // 释放资源
  }
});

// 3. 获取窗口并截图
let windowClass = null;
try {
  windowClass = window.getLastWindow(this.context);
} catch (err) {
  // 错误处理
}
if (windowClass) {
  windowClass.capture(imageReceiver).then(() => {
    // 截图已触发,等待imageArrival事件
  });
}

注意事项

  • 隐私与权限:截取非应用自身的窗口或全屏涉及用户隐私,必须明确告知用户并获得授权(动态权限弹窗),且通常需要在系统设置中开启相关权限开关。
  • 性能:截图操作,尤其是全屏截图,涉及大量图像数据传输,应注意频率,避免性能开销。
  • API差异:HarmonyOS Next的API与旧版本HarmonyOS可能存在差异,开发时请务必以当前版本的官方API文档和DevEco Studio中的接口提示为准。

建议直接查阅HarmonyOS Next的官方开发文档(特别是窗口管理屏幕捕获相关章节)和API参考,以获取最准确、详细的接口定义、参数说明和示例代码。

回到顶部