前言

在 HarmonyOS 的 2in1 设备、平板桌面模式和支持外接鼠标的设备上,鼠标指针是与用户交互的重要视觉元素。一个合适的光标样式能显著提升操作直观性——文字编辑时显示 I 形光标、拖拽时显示抓取手势、加载时显示等待图标、链接上显示手形指针。

然而大多数鸿蒙开发者对指针管理几乎没有概念——因为传统手机应用不需要考虑这个。随着 HarmonyOS 向全场景设备扩展,2in1(如 MateBook E)和平板生产力模式的用户越来越多,指针样式的管理就变得必要了。

HarmonyOS NEXT 提供了 @ohos.multimodalInput.pointer 模块,它包含 30+ 种系统指针样式、设置/获取/显隐切换、自定义光标图片等完整能力。本文把这个模块的核心功能封装成一个可视化的"指针样式实验室",让你直观体验每一种光标的样式和用法。

全文含完整可运行代码,适合需要在 2in1/平板桌面模式下开发应用的开发者。


一、pointer 模块概述

1.1 什么是 pointer 模块

@ohos.multimodalInput.pointer 是 HarmonyOS 的指针属性管理模块,属于 @kit.InputKit。它管理的是鼠标/触控板光标的显示样式和可见性,不涉及触摸事件或手势识别。

该模块的核心能力可以归纳为:

  1. 样式设置:将当前窗口(或全局)的指针样式切换为 30+ 种系统预定义样式之一
  2. 样式查询:读取当前窗口(或全局)的指针样式
  3. 可见性控制:切换指针的显示/隐藏状态
  4. 自定义光标:使用 PixelMap 图片作为自定义光标图案(API 10+)

1.2 导入方式

import pointer from '@ohos.multimodalInput.pointer';

这是 default import,来自 @kit.InputKit。注意模块路径是 @ohos.multimodalInput.pointer,不是 @kit.ArkTS

1.3 核心 API 一览

API 参数 返回值 说明
setPointerStyleSync(windowId, style) windowId: number, style: PointerStyle void 同步设置指针样式
getPointerStyleSync(windowId) windowId: number PointerStyle 同步获取当前样式
setPointerVisibleSync(visible) visible: boolean void 同步切换指针显隐
isPointerVisibleSync() boolean 同步检查指针是否可见

所有 API 都有对应的异步版本(Callback / Promise),从 API 10 开始也提供了同步版本(Sync 后缀)。在 Demo 中我们优先使用同步版本,因为它们的代码更简洁,且在 UI 线程中调用不会产生明显的性能影响。

windowId 的特殊值 -1

所有需要 windowId 的 API 都接受 -1 作为特殊值——它表示"全局窗口"。当你传入 -1 时,操作作用于全局鼠标指针,而非某个特定窗口。对于 Demo 来说,使用 -1 非常方便,因为不需要获取当前窗口的句柄。


二、PointerStyle 枚举:30+ 种系统光标

2.1 枚举结构

enum PointerStyle {
  DEFAULT,
  EAST, WEST, SOUTH, NORTH,
  WEST_EAST, NORTH_SOUTH,
  NORTH_EAST, NORTH_WEST, SOUTH_EAST, SOUTH_WEST,
  NORTH_EAST_SOUTH_WEST, NORTH_WEST_SOUTH_EAST,
  CROSS,
  CURSOR_COPY, CURSOR_FORBID,
  COLOR_SUCKER,
  HAND_GRABBING, HAND_OPEN, HAND_POINTING,
  HELP, MOVE,
  RESIZE_LEFT_RIGHT, RESIZE_UP_DOWN,
  SCREENSHOT_CHOOSE, SCREENSHOT_CURSOR,
  TEXT_CURSOR,
  ZOOM_IN, ZOOM_OUT,
  MIDDLE_BTN_EAST, MIDDLE_BTN_WEST, MIDDLE_BTN_SOUTH, MIDDLE_BTN_NORTH,
  MIDDLE_BTN_NORTH_SOUTH,
  MIDDLE_BTN_NORTH_EAST, MIDDLE_BTN_NORTH_WEST,
  MIDDLE_BTN_SOUTH_EAST, MIDDLE_BTN_SOUTH_WEST
}

这些样式可以分为六大类:

2.2 方向箭头类(13 种)

枚举值 含义 典型场景
DEFAULT 默认指针 通用场景
EAST/WEST/SOUTH/NORTH 单向箭头 提示可向某方向移动
WEST_EAST/NORTH_SOUTH 双向箭头 水平/垂直调整
NORTH_EAST/NORTH_WEST/SOUTH_EAST/SOUTH_WEST 对角箭头 对角线方向调整
NORTH_EAST_SOUTH_WEST/NORTH_WEST_SOUTH_EAST 双对角箭头 双向对角线调整

方向箭头主要用于窗口大小调整场景。例如,当光标悬停在窗口右边缘时,将光标设为 EAST(向东箭头),暗示用户可以向右拖拽改变窗口宽度。

2.3 操作手势类(9 种)

枚举值 含义 典型场景
CROSS 十字准星 精确选择、图形编辑
HAND_POINTING 手形指向 链接、按钮悬停
HAND_GRABBING 抓取手势 拖拽中(已抓住)
HAND_OPEN 张开手势 可拖拽提示(未抓住)
HELP 帮助问号 帮助图标悬停
MOVE 移动四向箭头 整体移动对象
TEXT_CURSOR I 形光标 文本选择、文字输入
CURSOR_COPY 复制光标 按住 Ctrl 拖拽复制
CURSOR_FORBID 禁止光标 不可操作区域

操作手势用户传达的是"可以做什么操作"。例如,在一个可拖拽的元素上,默认显示 HAND_OPEN(张开手),拖拽过程中切换为 HAND_GRABBING(抓握手),释放后恢复 HAND_OPEN

2.4 调整大小类(4 种)

枚举值 含义
RESIZE_LEFT_RIGHT 水平调整
RESIZE_UP_DOWN 垂直调整
ZOOM_IN 放大镜(带 +)
ZOOM_OUT 放大镜(带 -)

2.5 中键滚动类(9 种)

MIDDLE_BTN_ 为前缀的样式,表示按下鼠标中键后的自动滚动方向。这类光标在 2D 画布、地图、表格等需要多方向滚动的场景中比较实用。

2.6 截屏与特殊类(3 种)

枚举值 含义
SCREENSHOT_CHOOSE 截屏区域选择十字
SCREENSHOT_CURSOR 截屏光标
COLOR_SUCKER 取色器(吸管)

在这里插入图片描述
在这里插入图片描述

三、实战:指针样式实验室页面

3.1 整体设计

实验室页面分为四个功能区:

  1. 当前状态面板:显示当前指针样式名称 + 可见性开关 + 刷新按钮
  2. 分类标签栏:6 个分类按钮(方向/操作/调整/滚动/截屏/特殊),点击切换下方网格
  3. 样式网格:3 列 Grid 布局,展示当前分类下的所有 PointerStyle,点击立即应用
  4. 切换历史:记录每次样式切换的时间、样式名和枚举名

3.2 完整代码

import { router } from '@kit.ArkUI';
import pointer from '@ohos.multimodalInput.pointer';
import { FontSize, Spacing } from '../common/Constants';

interface StyleItem {
  name: string;
  label: string;
  value: pointer.PointerStyle;
}

interface StyleRecord {
  time: string;
  name: string;
  category: string;
}

@Entry
@Component
struct PointerStyleLabPage {
  @State currentStyle: string = '—';
  @State visible: boolean = true;
  @State selectedCategory: number = 0;
  @State records: StyleRecord[] = [];

  private categories: string[] = ['方向', '操作', '调整', '滚动', '截屏', '特殊'];

  // 方向类(13种)
  private directionalStyles: StyleItem[] = [
    { name: 'DEFAULT', label: '默认', value: pointer.PointerStyle.DEFAULT },
    { name: 'EAST', label: '→ 东', value: pointer.PointerStyle.EAST },
    { name: 'WEST', label: '← 西', value: pointer.PointerStyle.WEST },
    { name: 'SOUTH', label: '↓ 南', value: pointer.PointerStyle.SOUTH },
    { name: 'NORTH', label: '↑ 北', value: pointer.PointerStyle.NORTH },
    // ... 其余8种略
  ];

  // 操作/调整/滚动/截屏/特殊类略(完整代码见源文件)

  private allGroups: StyleItem[][] = [
    this.directionalStyles, this.actionStyles, this.resizeStyles,
    this.scrollStyles, this.screenshotStyles, this.specialStyles
  ];

  aboutToAppear(): void {
    this.refreshState();
  }

  private refreshState(): void {
    try {
      this.currentStyle = this.styleToString(
        pointer.getPointerStyleSync(-1));
    } catch (e) {
      this.currentStyle = '不可用';
    }
    try {
      this.visible = pointer.isPointerVisibleSync();
    } catch (e) {
      this.visible = false;
    }
  }

  private styleToString(style: pointer.PointerStyle): string {
    for (let i = 0; i < this.allGroups.length; i++) {
      const group: StyleItem[] = this.allGroups[i];
      for (let j = 0; j < group.length; j++) {
        if (group[j].value === style) {
          return group[j].label + ' (' + group[j].name + ')';
        }
      }
    }
    return '未知 (' + style + ')';
  }

  private applyStyle(item: StyleItem): void {
    try {
      pointer.setPointerStyleSync(-1, item.value);
    } catch (e) {
      // 在不支持指针的设备上忽略
    }
    this.currentStyle = item.label + ' (' + item.name + ')';

    // 追加历史
    const now: Date = new Date();
    const ts: string = now.getHours().toString().padStart(2, '0') +
      ':' + now.getMinutes().toString().padStart(2, '0') +
      ':' + now.getSeconds().toString().padStart(2, '0');
    this.records = [{ time: ts, name: item.label,
      category: item.name } as StyleRecord]
      .concat(this.records).slice(0, 30);
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('<').fontSize(28).fontColor('#FFFFFF')
          .onClick(() => { router.back(); })
        Text('指针样式实验室')
          .fontSize(FontSize.TITLE).fontColor('#FFFFFF')
          .fontWeight(FontWeight.Bold).margin({ left: Spacing.MD })
        Blank()
        Text('@ohos.multimodalInput.pointer')
          .fontSize(9).fontColor('#FFFFFFCC')
      }
      .width('100%')
      .padding({ left: Spacing.LG, right: Spacing.LG, top: 14, bottom: 14 })
      .backgroundColor('#1E3A5F')

      Scroll() {
        Column() {
          // 当前状态面板
          Column() {
            Row() {
              Text('当前样式')
                .fontSize(FontSize.CAPTION).fontColor('#64748B')
                .layoutWeight(1)
              Text(this.currentStyle)
                .fontSize(FontSize.BODY).fontFamily('monospace')
                .fontColor('#1A202C').fontWeight(FontWeight.Bold)
            }
            .width('100%').margin({ bottom: 8 })

            Row() {
              Text('指针可见性').fontSize(FontSize.CAPTION)
                .fontColor('#64748B').layoutWeight(1)
              Toggle({ type: ToggleType.Switch, isOn: this.visible })
                .selectedColor('#1E3A5F')
                .onChange((checked: boolean) => {
                  this.visible = checked;
                  try {
                    pointer.setPointerVisibleSync(checked);
                  } catch (e) { }
                })
            }
            .width('100%').margin({ bottom: 8 })

            Button('刷新状态')
              .fontSize(12).fontColor('#1E3A5F').fontWeight(FontWeight.Bold)
              .height(32).backgroundColor('#E8EEF4').borderRadius(6)
              .onClick(() => { this.refreshState(); })
          }
          .width('100%').padding(Spacing.MD)
          .backgroundColor('#FFFFFF').borderRadius(10)
          .margin({ bottom: Spacing.MD })

          // 分类标签 + 网格(略,完整代码见源文件)

          // 切换历史略
        }
        .width('100%')
        .padding({ left: Spacing.LG, right: Spacing.LG,
          top: Spacing.MD, bottom: Spacing.MD })
      }
      .layoutWeight(1).scrollBar(BarState.Off).backgroundColor('#F2F3F5')
    }
    .width('100%').height('100%').backgroundColor('#F2F3F5')
  }
}

3.3 关键设计

分类网格展示

30+ 种样式如果铺在一个列表中非常冗长。这里采用"分类标签 + 条件 Grid"的方案:6 个分类按钮整齐排列在一行,每个分类下用 3 列 Grid 展示。Grid 是 ArkUI 的网格布局组件,columnsTemplate: '1fr 1fr 1fr' 表示三等分,rowsGap/columnsGap(8) 控制间距。

点击任一 GridItem 调用 applyStyle(item),该方法内部调用 setPointerStyleSync(-1, item.value) 应用全局指针样式,然后更新 currentStyle 显示和历史记录。

windowId = -1 的妙用

在所有调用了 windowId 的 API 中,我们使用 -1 表示全局窗口。这个选择有三个好处:

  1. 无需获取当前窗口 ID(省去 window.getLastWindow() 的异步操作)
  2. 样式作用于全局,切换 App 窗口后仍然有效
  3. 简化了错误处理:不需要处理窗口不存在的情况

异常吞噬

setPointerStyleSync 等在纯手机(无指针设备)上可能抛出异常。Demo 用 try-catch 包裹每个 API 调用,静默吞噬异常——不影响页面功能,也不会 crash。在 2in1 或平板的桌面模式下,这些 API 正常工作,光标会立即变为所选样式。

StyleItem 接口和 as 断言

与第 54 篇文章一样,StyleItem 对象字面量在 ArkTS 严格模式下需要显式类型标记。每个样式条目都使用了 as StyleItem 断言,同时 allGroups 数组的类型声明为 StyleItem[][],编译器从声明中就能推断出数组元素的类型。


四、实战要点

4.1 指针样式仅在受支持的设备上生效

setPointerStyleSync 的调用在所有设备上都会成功(返回 void 且不抛异常),但样式的视觉变化只在连接了鼠标或使用触控板的设备上可见。在纯触屏手机上,这个 API 是一个"合法的 no-op"——调用成功但看不到效果。

因此,指针样式管理应该在业务逻辑中根据设备类型做条件判断:

import deviceInfo from '@ohos.deviceInfo';

if (deviceInfo.deviceType === '2in1' || deviceInfo.deviceType === 'tablet') {
  pointer.setPointerStyleSync(-1, pointer.PointerStyle.HAND_POINTING);
}

4.2 DEFAULT 不等于"恢复初始"

PointerStyle.DEFAULT 是系统默认光标(通常是向左上方的箭头)。调用 setPointerStyle(win, DEFAULT) 会设置为默认箭头,但不会自动恢复到应用启动时的状态。如果你想"恢复之前的样式",需要在切换前用 getPointerStyleSync 保存旧值。

4.3 窗口级 vs 全局级

windowId 为正数时,样式作用于特定窗口——光标在该窗口内显示为你设置的样式,移出窗口后恢复系统默认。当 windowId = -1 时,样式作用于全局——无论光标在屏幕的哪个位置都显示为该样式。

对于大多数应用场景,窗口级控制更灵活:你可以在文本区域将光标设为 TEXT_CURSOR,在链接上设为 HAND_POINTING,在可拖拽区域设为 MOVE

但窗口级控制需要使用 @ohos.window 获取窗口句柄,流程较为复杂:

import window from '@ohos.window';

const win = await window.getLastWindow(this.context);
const winId = win.getWindowProperties().id;
pointer.setPointerStyleSync(winId, pointer.PointerStyle.TEXT_CURSOR);

4.4 自定义光标图片

从 API 10 开始,pointer 支持使用 PixelMap 作为自定义光标:

function setCustomCursorSync(
  windowId: number,
  pixelMap: image.PixelMap,
  focusX?: number,
  focusY?: number
): void;

focusXfocusY 指定光标的"热点"(点击生效的位置),默认为图片中心。图片尺寸取决于系统设置,通常在 32×32 到 64×64 像素之间。


五、典型应用场景

5.1 文本编辑器

在文本编辑区域,将光标设为 TEXT_CURSOR(I 形),在工具栏按钮上设为 DEFAULT,在可拖拽的分割线上设为 RESIZE_LEFT_RIGHT

5.2 图片/图形编辑器

这是指针样式最丰富的场景:

  • 默认工具:CROSS(十字准星)
  • 裁剪工具:SCREENSHOT_CHOOSE
  • 取色工具:COLOR_SUCKER
  • 缩放工具:ZOOM_IN / ZOOM_OUT
  • 移动画布:MOVE

5.3 窗口布局调整

在分割线或窗口边缘,根据拖拽方向设置对应的箭头样式:

  • 右边缘:EAST(→)
  • 左下角:SOUTH_WEST(↙)
  • 顶部边缘:NORTH(↑)

5.4 加载/等待状态

在长时间操作(如导出文件、加载大图)期间,可以考虑设置特殊光标提示用户等待。虽然 pointer 没有"旋转等待圈"样式,但可以组合 CURSOR_FORBID + loading 动画使用。


六、小结

本文以"指针样式实验室"为 Demo,系统讲解了 HarmonyOS NEXT 的 @ohos.multimodalInput.pointer

  • 核心 APIsetPointerStyleSync(设置)、getPointerStyleSync(获取)、setPointerVisibleSync(显隐)、isPointerVisibleSync(检查)
  • 30+ 种 PointerStyle:涵盖方向箭头(13 种)、操作手势(9 种)、调整大小(4 种)、中键滚动(9 种)、截屏和特殊(3 种)六大类
  • windowId = -1:全局窗口的特殊值,简化 Demo 代码,无需获取窗口句柄
  • 设备兼容:在纯触屏设备上是合法 no-op,在 2in1/平板桌面模式下正常工作
  • 分类网格 UI:用 Grid + 分类标签条组织 30+ 种样式,避免冗长列表

指针样式的管理是"细节中的专业"——它不会直接影响功能实现,但在 2in1 和平板桌面模式下,合适的光标样式是高质量用户体验的重要组成部分。当你的鸿蒙应用需要在 MateBook E 或 MatePad Pro 上以桌面模式运行时,花半小时做一下指针样式的优化,用户能感受到这份用心。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐