系统托盘:实现最小化到托盘与托盘菜单(88)
在鸿蒙HarmonyOSPC 端应用开发中实现系统托盘状态栏驻留与菜单交互核心依赖于Desktop Extension Kit中的statusBarManager模块。与传统的桌面应用不同鸿蒙通过状态栏图标StatusBar Icon来实现托盘功能支持左键快捷操作和右键菜单。以下是实现系统托盘、最小化驻留及菜单交互的完整架构与实战代码一、 基础架构配置与状态栏接入首先需要在module.json5中配置用于承载后台托盘进程的UIAbility并通过statusBarManager将应用接入系统状态栏。核心代码示例import { statusBarManager } from kit.DesktopExtensionKit; import { common } from kit.AbilityKit; // 1. 接入状态栏并配置左键快捷操作QuickOperation async function loadStatusBar(context: common.UIAbilityContext) { let operation: statusBarManager.QuickOperation { abilityName: BackGroundAbility, // 后台承载 Ability title: 我的应用, // 鼠标悬停时的气泡提示 height: 300, // 左键点击弹出的快捷窗口高度 moduleName: entry // 必须配置否则悬停气泡不显示 }; await statusBarManager.addStatusBarIcon(context, operation); } // 2. 配置右键菜单StatusBarGroupMenu // 注意鸿蒙 PC 端托盘支持右键弹出快捷菜单二、 核心逻辑拦截关闭事件与后台驻留为了实现“点击窗口关闭按钮时应用不退出而是最小化到托盘”需要拦截EntryAbility的销毁生命周期并启动一个隐藏的后台 Ability 来维系托盘图标。核心代码示例import { contextConstant } from kit.AbilityKit; // 在 EntryAbility 中拦截关闭事件 onPrepareToTerminate(): boolean { // 1. 隐藏主界面但不销毁 Ability this.context.hideAbility().then(() { console.info(主窗口已隐藏应用驻留托盘); }); // 2. 维系系统托盘启动后台进程绑定到状态栏 let want: Want { bundleName: com.example.myapp, abilityName: BackGroundAbility }; let options: StartOptions { processMode: contextConstant.ProcessMode.NEW_PROCESS_ATTACH_TO_STATUS_BAR_ITEM, startupVisibility: contextConstant.StartupVisibility.STARTUP_HIDE // 启动后隐藏 }; this.context.startAbility(want, options); // 返回 true 拦截默认的销毁行为 return true; }三、 交互实现托盘点击恢复与菜单控制当用户与托盘图标交互时通过注册事件监听器来执行相应的业务逻辑如重新显示主窗口。核心代码示例// 在 EntryAbility 的 onCreate 中注册状态栏图标点击事件 statusBarManager.on(statusBarIconClick, (eventData: emitter.EventData) { let data eventData.data; if (data data[iconClickType] leftClick) { // 左键点击将隐藏的 Ability 重新显示到前台 this.context.showAbility().then(() { console.info(从托盘恢复主窗口); }); } });悬停气泡不显示如果在配置QuickOperation时遗漏了moduleName参数鼠标悬停在托盘图标上时将不会弹出任何气泡提示。左右键功能区分目前鸿蒙 PC 端的托盘图标明确区分了左右键功能。左键leftClick触发QuickOperation业务弹窗右键触发StatusBarGroupMenu快捷菜单暂不支持通过代码动态拦截并自定义左键/右键的底层事件映射。后台进程保活必须使用NEW_PROCESS_ATTACH_TO_STATUS_BAR_ITEM模式启动后台 Ability这样系统才会将该进程与状态栏图标强绑定防止托盘图标在应用最小化后被系统回收。四、 视觉进阶动态图标与暗黑模式自适应鸿蒙 PC 的托盘区域会根据系统主题亮色/暗色自动切换背景。为了保证图标的高对比度开发者必须同时提供黑白两套PixelMap并通过StatusBarMenuItemOptions一次性注入由系统内核择机渲染。核心代码示例import { statusBarManager } from kit.DesktopExtensionKit; import { image } from kit.ImageKit; import { common } from kit.AbilityKit; // 封装图标资源转换逻辑 async function createIconSet(context: common.UIAbilityContext, whiteName: string, blackName: string): PromisestatusBarManager.StatusBarItemIcon { const resMgr context.resourceManager; const wSource image.createImageSource(resMgr.getRawFileContentSync(whiteName).buffer); const bSource image.createImageSource(resMgr.getRawFileContentSync(blackName).buffer); return { white: await wSource.createPixelMap(), black: await bSource.createPixelMap() }; } // 更新托盘图标及状态 async function updateTrayAppearance(context: common.UIAbilityContext, isActive: boolean) { const iconSet isActive ? await createIconSet(context, tray_active_white.png, tray_active_black.png) : await createIconSet(context, tray_idle_white.png, tray_idle_black.png); const updateItem: statusBarManager.StatusBarMenuItem { menuCode: APP_MAIN_STATUS, // 锚点必须与初始化时一致 title: isActive ? 服务运行中 : 服务已暂停, options: { icon: iconSet, selected: isActive // 选中态通常伴随高亮背景 } }; await statusBarManager.updateStatusBarMenuItem(context, updateItem); }五、 交互进阶二级菜单的“精细化手术”当托盘右键菜单包含复杂的子项如切换账号、多设备列表时全量刷新菜单会导致性能损耗。updateStatusBarSubMenuItem允许开发者像使用“微型手术刀”一样仅针对特定的叶子节点进行标题或状态的局部重载。核心代码示例// 局部更新二级菜单例如更新当前在线的设备名称 async function updateSubMenuItem(context: common.UIAbilityContext, deviceName: string) { const subMenuItem: statusBarManager.StatusBarSubMenuItem { subTitle: 当前设备: ${deviceName}, menuCode: SUB_DEVICE_001, // 二级菜单的唯一标识 menuAction: { abilityName: EntryAbility, moduleName: entry, notifyOnly: true // 仅通知业务逻辑不自动拉起 Ability } }; await statusBarManager.updateStatusBarSubMenuItem(context, subMenuItem); }六、 业务扩展模拟未读消息红点Badge目前鸿蒙 PC 的托盘 API 暂未提供原生的 Badge角标接口。对于 IM 或邮件类应用需要通过 Canvas 动态绘制带数字红点的图标并实时替换托盘图标。核心代码示例// 利用 Canvas 动态合成带红点的托盘图标 async function createBadgeIcon(context: common.UIAbilityContext, count: number): PromisestatusBarManager.StatusBarItemIcon { // 注意PC端需引入 canvas 库或使用系统绘图 API // 此处以伪代码展示核心思路 // 1. 绘制基础托盘图标 (24x24 vp) // 2. 在右上角绘制红色圆圈 // 3. 绘制白色数字文本 (若 count 99 则显示 99) // 4. 导出为 PixelMap const pixelMap await renderBadgeToPixelMap(count); return { white: pixelMap, black: pixelMap // 红点图标通常不需要黑白双态 }; } // 收到新消息时触发 async function onNewMessageReceived(context: common.UIAbilityContext, unreadCount: number) { const badgeIcon await createBadgeIcon(context, unreadCount); const updateItem: statusBarManager.StatusBarMenuItem { menuCode: APP_MAIN_STATUS, title: ${unreadCount} 条未读消息, options: { icon: badgeIcon } }; await statusBarManager.updateStatusBarMenuItem(context, updateItem); }七、 架构优化跨进程状态同步与防抖托盘图标运行在独立的PCService进程中而业务逻辑在主 UI 进程中。频繁的跨进程通信IPC会导致系统开销。架构建议增量更新原则务必使用准确的menuCode作为锚点调用updateStatusBarMenuItem进行局部覆盖严禁全量重建菜单树。UI 防抖Debounce在业务侧如 WebSocket 推送高频消息时不要每次收到消息都立即调用托盘更新 API。应在 UI 进程内做 500ms 左右的防抖处理合并状态后再发起跨进程调用。精细化错误捕获跨进程调用极易受系统资源调度影响务必对BusinessError进行精细化捕获避免因托盘更新失败导致主业务逻辑崩溃。// 安全的托盘更新封装 async function safeUpdateTray(context: common.UIAbilityContext, item: statusBarManager.StatusBarMenuItem) { try { await statusBarManager.updateStatusBarMenuItem(context, item); } catch (err) { const error err as BusinessError; // 过滤掉非致命错误避免日志风暴 console.error([TrayUpdateError] Code: ${error.code}, Msg: ${error.message}); } }