光标样式:自定义鼠标光标形状(Cursor)(75)

发布时间:2026/6/25 18:49:56
光标样式:自定义鼠标光标形状(Cursor)(75) 在鸿蒙HarmonyOSPC 端和平板端应用中自定义鼠标光标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 场景或全屏应用中如果应用意外崩溃或失去焦点如AltTab切出系统通常会自动解锁光标。但在应用重新激活onForeground时务必检查并重置光标状态防止出现“光标不可见”或“光标被锁死”的极端 Bug。多窗口光标隔离在多窗口如分屏、画中画场景下每个窗口拥有独立的Window实例。自定义光标或锁定光标时必须获取当前活动窗口的 ID 进行操作避免误改其他后台窗口的光标状态。性能与内存监控频繁切换自定义光标如在列表中快速滑动时会产生大量的PixelMap创建与销毁开销。务必复用PixelMap对象并在组件销毁aboutToDisappear时统一释放资源防止内存泄漏。