鸿蒙新特性实战:@ohos.multimodalInput.pointer 打造指针样式实验室
前言
在 HarmonyOS 的 2in1 设备、平板桌面模式和支持外接鼠标的设备上,鼠标指针是与用户交互的重要视觉元素。一个合适的光标样式能显著提升操作直观性——文字编辑时显示 I 形光标、拖拽时显示抓取手势、加载时显示等待图标、链接上显示手形指针。
然而大多数鸿蒙开发者对指针管理几乎没有概念——因为传统手机应用不需要考虑这个。随着 HarmonyOS 向全场景设备扩展,2in1(如 MateBook E)和平板生产力模式的用户越来越多,指针样式的管理就变得必要了。
HarmonyOS NEXT 提供了 @ohos.multimodalInput.pointer 模块,它包含 30+ 种系统指针样式、设置/获取/显隐切换、自定义光标图片等完整能力。本文把这个模块的核心功能封装成一个可视化的"指针样式实验室",让你直观体验每一种光标的样式和用法。
全文含完整可运行代码,适合需要在 2in1/平板桌面模式下开发应用的开发者。
一、pointer 模块概述
1.1 什么是 pointer 模块
@ohos.multimodalInput.pointer 是 HarmonyOS 的指针属性管理模块,属于 @kit.InputKit。它管理的是鼠标/触控板光标的显示样式和可见性,不涉及触摸事件或手势识别。
该模块的核心能力可以归纳为:
- 样式设置:将当前窗口(或全局)的指针样式切换为 30+ 种系统预定义样式之一
- 样式查询:读取当前窗口(或全局)的指针样式
- 可见性控制:切换指针的显示/隐藏状态
- 自定义光标:使用
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 整体设计
实验室页面分为四个功能区:
- 当前状态面板:显示当前指针样式名称 + 可见性开关 + 刷新按钮
- 分类标签栏:6 个分类按钮(方向/操作/调整/滚动/截屏/特殊),点击切换下方网格
- 样式网格:3 列 Grid 布局,展示当前分类下的所有 PointerStyle,点击立即应用
- 切换历史:记录每次样式切换的时间、样式名和枚举名
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 表示全局窗口。这个选择有三个好处:
- 无需获取当前窗口 ID(省去
window.getLastWindow()的异步操作) - 样式作用于全局,切换 App 窗口后仍然有效
- 简化了错误处理:不需要处理窗口不存在的情况
异常吞噬
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;
focusX 和 focusY 指定光标的"热点"(点击生效的位置),默认为图片中心。图片尺寸取决于系统设置,通常在 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:
- 核心 API:
setPointerStyleSync(设置)、getPointerStyleSync(获取)、setPointerVisibleSync(显隐)、isPointerVisibleSync(检查) - 30+ 种 PointerStyle:涵盖方向箭头(13 种)、操作手势(9 种)、调整大小(4 种)、中键滚动(9 种)、截屏和特殊(3 种)六大类
- windowId = -1:全局窗口的特殊值,简化 Demo 代码,无需获取窗口句柄
- 设备兼容:在纯触屏设备上是合法 no-op,在 2in1/平板桌面模式下正常工作
- 分类网格 UI:用 Grid + 分类标签条组织 30+ 种样式,避免冗长列表
指针样式的管理是"细节中的专业"——它不会直接影响功能实现,但在 2in1 和平板桌面模式下,合适的光标样式是高质量用户体验的重要组成部分。当你的鸿蒙应用需要在 MateBook E 或 MatePad Pro 上以桌面模式运行时,花半小时做一下指针样式的优化,用户能感受到这份用心。
更多推荐


所有评论(0)