在鸿蒙(HarmonyOS)PC 端和平板端应用中,自定义鼠标光标(Cursor)是提供精准操作反馈和打造专业级桌面体验的重要一环。开发者可以通过系统内置样式(setCursor)和自定义图像资源(setCustomCursor)两种方式来控制光标形状。
以下是实现光标样式自定义的核心策略与代码示例:
一、 使用系统内置光标样式(setCursor)
对于常见的交互场景,鸿蒙提供了丰富的内置光标样式(如手型、十字准星、文本输入、方向箭头等)。官方推荐通过getUIContext().getCursorController()获取实例来控制光标,以避免 UI 上下文不明确的问题。
核心代码示例:
import { pointer } from '@kit.InputKit'; Row() .width(200) .height(200) .backgroundColor(Color.Green) .onHover((flag: boolean) => { if (flag) { // 鼠标悬停时,更改为系统内置的手型光标 this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.HAND); } else { // 鼠标离开时,恢复为默认的箭头光标 this.getUIContext().getCursorController().restoreDefault(); } })二、 自定义图像光标(setCustomCursor)
当系统内置样式无法满足需求时(例如:设计软件中的画笔、取色器、缩放手柄等),可以使用pointer.setCustomCursor加载自定义的图像资源。此功能从 API version 15 开始支持。
核心代码示例:
import { image } from '@kit.ImageKit'; import { pointer } from '@kit.InputKit'; import { window } from '@kit.ArkUI'; // 1. 加载自定义光标资源(如 SVG 或 PNG) getContext().resourceManager.getMediaContent($r("app.media.custom_cursor")).then((buffer) => { const svgBuffer: ArrayBuffer = buffer.slice(0); let imgSource: image.ImageSource = image.createImageSource(svgBuffer); // 2. 设置光标的解码尺寸(最大限制为 256 x 256px) let decodingOptions: image.DecodingOptions = { desiredSize: { width: 32, height: 32 } }; imgSource.createPixelMap(decodingOptions).then((pixelMap) => { window.getLastWindow(getContext(), (error, win) => { if (error) return; let windowId = win.getWindowProperties().id; // 3. 配置自定义光标(设置焦点坐标,即点击触发的精准位置) let customCursor: pointer.CustomCursor = { pixelMap: pixelMap, focusX: 0, // 焦点水平坐标 focusY: 0 // 焦点垂直坐标 }; let config: pointer.CursorConfig = { followSystem: false // 是否跟随系统设置调整大小 }; // 4. 应用自定义光标 pointer.setCustomCursor(windowId, customCursor, config).then(() => { console.log('自定义光标设置成功'); }); }); }); });三、 全局光标的显示与隐藏(setPointerVisible)
在某些沉浸式场景下(如全屏视频播放、3D 游戏或演示模式),需要完全隐藏鼠标光标。可以通过setPointerVisible接口进行全局控制。
核心代码示例:
import { pointer } from '@kit.InputKit'; // 进入全屏播放时隐藏光标 pointer.setPointerVisible(false, (error) => { if (error) { console.error(`隐藏光标失败: ${JSON.stringify(error)}`); return; } console.log('光标已隐藏'); }); // 退出全屏时恢复显示 pointer.setPointerVisible(true, (error) => { if (error) { console.error(`显示光标失败: ${JSON.stringify(error)}`); } });桌面级光标交互开发建议
- 及时恢复默认状态:自定义光标通常具有极强的场景指向性。务必在组件的
onHover(false)回调中调用restoreDefault(),防止光标移出区域后仍保持特殊样式。 - 精准设置焦点(FocusX/FocusY):在自定义图像光标时,务必根据图像内容合理设置
focusX和focusY。例如,准星光标的焦点应在正中心,而画笔光标的焦点应在笔尖处。 - 注意性能与资源释放:自定义光标依赖
PixelMap,在页面销毁或光标不再使用时,应主动调用pixelMap.release()释放内存,避免内存泄漏。 - 状态变更时的重设机制:当应用窗口布局改变、页面跳转或光标移出再回到窗口时,系统可能会将光标切换回默认样式。开发者需要在相关生命周期或回调中重新设置自定义光标样式。
四、 多区域光标状态隔离与嵌套处理
在复杂的嵌套布局中(例如:地图应用中的地图区域、可拖拽的侧边栏、普通文本区),鼠标在不同区域移动时会频繁触发光标切换。为了防止状态混乱,建议将光标控制逻辑与组件的onHover深度绑定,并确保内层组件离开时能正确恢复外层状态。
核心代码示例:
Column() { // 外层普通区域 Text('普通文本区域') .onHover((isHover) => { if (isHover) { this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.TEXT); } else { this.getUIContext().getCursorController().restoreDefault(); } }) // 内层拖拽调整区域 Divider() .height(4) .onHover((isHover) => { if (isHover) { // 内层组件悬停时,覆盖外层光标 this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.RESIZE_VERTICAL); } else { // 离开内层组件时,恢复默认或交由外层处理 this.getUIContext().getCursorController().restoreDefault(); } }) }五、 沉浸式场景下的光标自动隐藏与显现
在全屏视频播放、3D 渲染或演示模式下,光标长时间静止会遮挡视线。鸿蒙支持通过setPointerVisible结合定时器,实现“移动时显示,静止 N 秒后自动隐藏”的沉浸式体验。
核心代码示例:
import { pointer } from '@kit.InputKit'; @State private hideCursorTimer: number = -1; // 在鼠标移动或悬停时触发 .onHover((isHover) => { if (isHover && this.isFullScreen) { // 1. 显示光标 pointer.setPointerVisible(true); // 2. 清除上一次的隐藏定时器 if (this.hideCursorTimer !== -1) clearTimeout(this.hideCursorTimer); // 3. 设置新的隐藏定时器(例如 3 秒无操作后隐藏) this.hideCursorTimer = setTimeout(() => { pointer.setPointerVisible(false); }, 3000); } })六、 动态调整自定义光标焦点(FocusX/FocusY)
在某些特殊交互中(如:根据缩放级别改变画笔粗细,或切换不同的取色器模式),光标的图像可能发生变化,此时必须同步更新focusX和focusY,否则会导致“点击位置”与“视觉光标位置”发生偏移。
核心代码示例:
// 假设用户切换了画笔大小,需要重新加载光标 private async updateBrushCursor(size: number) { const pixelMap = await this.generateBrushPixelMap(size); // 动态生成不同大小的画笔图像 const customCursor: pointer.CustomCursor = { pixelMap: pixelMap, focusX: size / 2, // 【关键】焦点始终保持在画笔正中心 focusY: size / 2 }; const config: pointer.CursorConfig = { followSystem: false }; const windowId = this.getWindowId(); await pointer.setCustomCursor(windowId, customCursor, config); }七、 构建全局光标状态机(CursorStateManager)
在大型 PC 应用中,如果在各个页面分散调用setCursor和restoreDefault,极易导致光标状态残留(例如:从拖拽页面跳转到新页面,光标依然是拖拽手型)。建议封装一个全局的光标状态管理类,统一管理当前应用的光标上下文。
核心代码示例:
export class CursorStateManager { private static currentStyle: pointer.PointerStyle = pointer.PointerStyle.DEFAULT; private static uiContext: UIContext; static init(context: UIContext) { this.uiContext = context; } // 安全地设置光标 static set(style: pointer.PointerStyle) { if (this.currentStyle !== style) { this.currentStyle = style; this.uiContext.getCursorController().setCursor(style); } } // 安全地恢复默认光标 static restore() { if (this.currentStyle !== pointer.PointerStyle.DEFAULT) { this.currentStyle = pointer.PointerStyle.DEFAULT; this.uiContext.getCursorController().restoreDefault(); } } } // 在组件中使用,避免重复调用和状态错乱 Row() .onHover((isHover) => { if (isHover) CursorStateManager.set(pointer.PointerStyle.HAND); else CursorStateManager.restore(); })八、 结合鼠标事件与修饰键的联动控制
在 PC 端,光标样式往往需要与鼠标的按键状态(如按下左键)或键盘修饰键(如按住Shift或Ctrl)联动。通过onMouse事件,可以捕获这些复合状态,从而动态切换光标。
核心代码示例:
import { MouseEvent, Action, Button } from '@kit.InputKit'; Rectangle() .width(300).height(200) .onMouse((event: MouseEvent) => { // 当按下左键且按住 Shift 键时,切换为十字准星(常用于框选) if (event.action === Action.BUTTON_DOWN && event.button === Button.LEFT) { if (event.shiftKey) { this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.CROSS); } } // 释放按键时恢复默认 else if (event.action === Action.BUTTON_UP) { this.getUIContext().getCursorController().restoreDefault(); } })九、 拖拽交互的光标反馈(onDrag)
在文件管理器或看板应用中,当用户拖拽元素时,系统默认的拖拽反馈可能不够直观。可以在onDragStart时将光标切换为“抓取/禁止”状态,并在onDragEnd时恢复。
核心代码示例:
Column() .onDragStart(() => { // 拖拽开始时,切换为“移动/抓取”光标 this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.MOVE); return new DragItem('拖拽的数据'); }) .onDragEnd(() => { // 拖拽结束时,务必恢复默认光标 this.getUIContext().getCursorController().restoreDefault(); })十、 文本编辑区的光标与底层控制器联动
在富文本编辑器或代码编辑器中,除了视觉上的光标(Caret),还需要控制底层的输入光标(Pointer)。结合 API 23 新增的底层能力,可以实现更精细的文本交互。
核心代码示例:
// 当进入文本编辑模式时 TextInput({ controller: this.textController }) .onFocus(() => { // 视觉指针切换为文本输入样式 this.getUIContext().getCursorController().setCursor(pointer.PointerStyle.TEXT); }) .onBlur(() => { this.getUIContext().getCursorController().restoreDefault(); }) // 在自定义工具栏中,通过控制器直接操作底层光标(如模拟退格删除) Button('删除前一字符') .onClick(() => { // 直接调用底层控制器,无需手动截取字符串,避免光标抖动 this.textController.deleteBackward(); })十一、 游戏与 3D 场景的光标锁定(Pointer Lock)
在 3D 渲染、全景视频或第一人称游戏中,需要鼠标无限移动且不被屏幕边界限制。鸿蒙支持通过setPointerLock锁定光标,使其在物理鼠标移动时仅触发相对坐标变化,而不影响系统级绝对坐标。
核心代码示例:
// 进入 3D 场景时锁定光标 Button('进入沉浸模式') .onClick(async () => { try { const windowClass = await window.getLastWindow(getContext(this)); // 锁定光标 await windowClass.setPointerLock(true); console.info('光标已锁定,可自由旋转视角'); } catch (error) { console.error('锁定光标失败', error); } }) // 退出沉浸模式时解锁 Button('退出沉浸模式') .onClick(async () => { const windowClass = await window.getLastWindow(getContext(this)); await windowClass.setPointerLock(false); this.getUIContext().getCursorController().restoreDefault(); })补充建议
- 光标与焦点的解耦:视觉光标(Pointer)代表“鼠标在哪”,而焦点(Focus)代表“键盘输入给谁”。在 PC 开发中,不要将两者混淆。例如,鼠标悬停在按钮上改变了光标,但不应该自动让按钮获取键盘焦点(除非用户明确点击)。
- 全局状态兜底:在 3D 场景或全屏应用中,如果应用意外崩溃或失去焦点(如
Alt+Tab切出),系统通常会自动解锁光标。但在应用重新激活(onForeground)时,务必检查并重置光标状态,防止出现“光标不可见”或“光标被锁死”的极端 Bug。 - 多窗口光标隔离:在多窗口(如分屏、画中画)场景下,每个窗口拥有独立的
Window实例。自定义光标或锁定光标时,必须获取当前活动窗口的 ID 进行操作,避免误改其他后台窗口的光标状态。 - 性能与内存监控:频繁切换自定义光标(如在列表中快速滑动时)会产生大量的
PixelMap创建与销毁开销。务必复用PixelMap对象,并在组件销毁(aboutToDisappear)时统一释放资源,防止内存泄漏。