各位鸿蒙开发者小伙伴们,今天咱们来聊个超实用的技能 —— 把咱们自己开发的应用接入鸿蒙系统的状态栏!先跟大家掰扯掰扯,为啥要做这个事儿?你想啊,状态栏是用户每天解锁手机、用平板时第一眼就能看到的地方,要是能在这儿显示咱们应用的自定义图标,用户点一下图标还能弹出咱们定制的弹窗,右键点一下还有专属菜单,这不光让应用更显眼,用户操作也更方便,体验直接拉满!

不过先跟大家打个预防针:咱们这次用到的 DeskTop Extension Kit(桌面拓展服务) 相关 API,目前只在 2in1 设备(比如鸿蒙的二合一平板)上生效,其他设备暂时用不了哈。别到时候在手机上测试发现没效果,还以为自己代码写错了,白着急半天~

接下来咱们就从 “能实现啥效果”“要用到哪些接口”“具体怎么开发”“遇到问题咋解决” 这几个方面,一步步给大家讲透,每个重点都配了完整的 ArkTS 代码,跟着敲就行,保准你看完就能上手!

一、先搞明白:应用接入状态栏,到底能实现啥?

在写代码之前,咱们得先清楚最终的效果,心里有个谱。按照鸿蒙的状态栏扩展能力,咱们的应用接入后能实现 3 个核心功能:

  1. 状态栏显示自定义图标:不是系统默认的那种图标,是咱们自己设计的,比如音乐 APP 可以放个小音符,天气 APP 放个小太阳,辨识度拉满;
  2. 左键点击弹自定义弹窗:用户用鼠标左键点一下状态栏的图标,就能弹出咱们定制的页面,比如音乐 APP 弹窗里显示播放控制,天气 APP 显示实时温度;
  3. 右键点击显自定义菜单:右键点图标,能弹出咱们设置的菜单选项,比如 “打开应用主页”“查看详情”“退出服务” 这些;
  4. 应用退出图标自动消失:不用咱们手动处理,只要应用进程销毁了,状态栏的图标就会跟着不见,省得占着状态栏位置。

这几个功能咱们后面都会一个个实现,现在先有个整体印象,接下来咱们先把 “工具” 备好 —— 也就是要用到的 API 接口。

二、核心接口拆解:每个接口是干啥的?附代码示例

要实现状态栏接入,鸿蒙给咱们提供了 9 个核心 API,都在statusBarManager里。我不跟大家玩虚的,每个接口都用大白话讲清楚 “能干嘛”,再给个实际能用的代码示例,参数含义也标明白,保证你一看就懂。

1. addToStatusBar:把应用图标 “钉” 到状态栏(核心中的核心)

作用:这是最关键的接口,没有之一!它的功能就是把咱们配置好的图标、弹窗、菜单信息,一起加到状态栏里,让图标显示出来。
参数

  • context:上下文,简单说就是咱们应用当前的运行环境,没它啥操作都做不了;
  • statusBarItem:包含图标、左键弹窗、右键菜单的完整配置信息,是个 “大包裹”。

ArkTS 代码示例

import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';

// 假设咱们已经配置好了statusBarItem(后面会讲怎么配置)
const context: common.Context | undefined = this.getUIContext().getHostContext();
if (!context) {
  console.error('获取上下文失败,没法添加状态栏图标!');
  return;
}

// 配置好的状态栏图标信息(后面会详细讲怎么构建)
const statusBarItem: statusBarManager.StatusBarItem = {
  icons: { /* 图标配置 */ },
  quickOperation: { /* 左键弹窗配置 */ },
  statusBarGroupMenu: [ /* 右键菜单配置 */ ]
};

try {
  // 调用接口,把图标加到状态栏
  statusBarManager.addToStatusBar(context, statusBarItem);
  console.info('太棒了!图标成功加到状态栏~');
} catch (error) {
  // 万一失败了,打印错误信息,方便排查
  console.error(`加图标到状态栏失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
}

2. removeFromStatusBar:把图标从状态栏 “拿掉”

作用:如果咱们想主动移除状态栏的图标(比如用户手动关闭服务时),就用这个接口,不用等应用退出。
参数:只需要context,因为系统能通过上下文找到对应的应用图标。

ArkTS 代码示例

import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';

const removeStatusBarIcon = () => {
  const context: common.Context | undefined = this.getUIContext().getHostContext();
  if (!context) {
    console.error('获取上下文失败,没法移除状态栏图标!');
    return;
  }

  try {
    statusBarManager.removeFromStatusBar(context);
    console.info('图标成功从状态栏移除~');
  } catch (error) {
    console.error(`移除状态栏图标失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
  }
};

// 调用函数移除图标(比如用户点击“关闭服务”按钮时)
removeStatusBarIcon();

3. updateQuickOperationHeight:调整左键弹窗的高度

作用:咱们给左键弹窗设置了初始高度后,如果后面弹窗里的内容变多了(比如从显示 1 条信息变成 3 条),原来的高度不够用,就用这个接口改高度。
参数

  • context:上下文;
  • height:新的高度,单位是 vp(鸿蒙的虚拟像素,适配不同屏幕)。

ArkTS 代码示例

import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';

// 把弹窗高度从300vp改成400vp
const updatePopupHeight = () => {
  const context: common.Context | undefined = this.getUIContext().getHostContext();
  if (!context) {
    console.error('获取上下文失败,没法更新弹窗高度!');
    return;
  }

  const newHeight = 400; // 新的弹窗高度,根据内容调整
  try {
    statusBarManager.updateQuickOperationHeight(context, newHeight);
    console.info(`弹窗高度成功更新为${newHeight}vp~`);
  } catch (error) {
    console.error(`更新弹窗高度失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
  }
};

// 调用函数更新高度(比如弹窗内容加载完成后)
updatePopupHeight();

4. updateStatusBarMenu:更新右键菜单内容

作用:右键菜单不是一成不变的,比如音乐 APP 在播放时显示 “暂停”,暂停时显示 “播放”,这时候就用这个接口动态更新菜单。
参数

  • context:上下文;
  • statusBarGroupMenus:新的菜单配置,格式和初始配置一样。

ArkTS 代码示例

import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';

// 更新右键菜单(比如把“子菜单项”改成“暂停播放”)
const updateRightMenu = () => {
  const context: common.Context | undefined = this.getUIContext().getHostContext();
  if (!context) {
    console.error('获取上下文失败,没法更新右键菜单!');
    return;
  }

  // 1. 构建新的子菜单项
  const newSubMenus: Array<statusBarManager.StatusBarSubMenuItem> = [];
  const subMenuAction: statusBarManager.StatusBarMenuAction = {
    abilityName: "EntryAbility" // 点击菜单要跳转的Ability
  };
  const newSubMenu: statusBarManager.StatusBarSubMenuItem = {
    subTitle: "暂停播放", // 新的子菜单文字
    menuAction: subMenuAction
  };
  newSubMenus.push(newSubMenu);

  // 2. 构建新的一级菜单
  const newMenuItems: Array<statusBarManager.StatusBarMenuItem> = [];
  const newMenuItem: statusBarManager.StatusBarMenuItem = {
    title: "音乐控制", // 一级菜单文字
    subMenu: newSubMenus // 关联新的子菜单
  };
  newMenuItems.push(newMenuItem);

  // 3. 构建新的菜单组
  const newGroupMenus: Array<statusBarManager.StatusBarGroupMenu> = [];
  newGroupMenus.push(newMenuItems);

  // 4. 调用接口更新菜单
  try {
    statusBarManager.updateStatusBarMenu(context, newGroupMenus);
    console.info('右键菜单成功更新~');
  } catch (error) {
    console.error(`更新右键菜单失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
  }
};

// 调用函数更新菜单(比如音乐播放状态变化时)
updateRightMenu();

5. updateStatusBarIcon:更换状态栏图标

作用:应用状态变了,图标也得跟着变,比如 WiFiAPP 在连接成功时显示彩色图标,断开时显示灰色图标,就用这个接口换图标。
参数

  • context:上下文;
  • statusBarIcon:新的图标配置,包含白色和黑色两种(适配不同状态栏主题)。

ArkTS 代码示例

import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { common } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
import { resourceManager } from '@kit.LocalizationKit';

// 更换状态栏图标(比如从“连接成功”图标换成“断开”图标)
const updateStatusBarIcon = async () => {
  const context: common.Context | undefined = this.getUIContext().getHostContext();
  if (!context) {
    console.error('获取上下文失败,没法更新图标!');
    return;
  }

  // 1. 获取资源管理器
  const resourceMgr: resourceManager.ResourceManager = context.resourceManager;

  // 2. 读取新的白色图标(假设新图标叫disconnectWhite.png,放在rawfile里)
  const newWhiteFileData = resourceMgr.getRawFileContentSync('disconnectWhite.png');
  const newWhiteBuffer = newWhiteFileData.buffer;
  const newWhiteImageSource = image.createImageSource(newWhiteBuffer);
  const newWhitePixelMap = await newWhiteImageSource.createPixelMap();

  // 3. 读取新的黑色图标(disconnectBlack.png)
  const newBlackFileData = resourceMgr.getRawFileContentSync('disconnectBlack.png');
  const newBlackBuffer = newBlackFileData.buffer;
  const newBlackImageSource = image.createImageSource(newBlackBuffer);
  const newBlackPixelMap = await newBlackImageSource.createPixelMap();

  // 4. 构建新的图标信息
  const newIcon: statusBarManager.StatusBarIcon = {
    white: newWhitePixelMap,
    black: newBlackPixelMap
  };

  // 5. 调用接口更新图标
  try {
    statusBarManager.updateStatusBarIcon(context, newIcon);
    console.info('状态栏图标成功更新~');
  } catch (error) {
    console.error(`更新状态栏图标失败!错误码:${(error as Error).name},原因:${(error as Error).message}`);
  }
};

// 调用函数更新图标(比如WiFi连接状态变化时)
updateStatusBarIcon();

6. on ('statusBarIconClick'):监听图标点击事件

作用:如果咱们没给左键弹窗配置abilityName(后面会讲),就需要自己处理左键点击逻辑,比如点击图标弹出提示,这时候就用这个接口监听点击事件。
参数

  • type:固定写'statusBarIconClick'
  • callback:点击后的回调函数,能拿到点击类型(比如左键点击是leftClickType)。

ArkTS 代码示例

import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { emitter } from '@kit.BasicServicesKit';

// 1. 定义点击事件的处理函数
private onStatusBarIconClick = (eventData: emitter.EventData) => {
  const data = eventData.data as { iconClickType: string };
  if (data) {
    // 判断点击类型(目前主要是左键点击)
    switch (data.iconClickType) {
      case 'leftClickType':
        console.info('用户左键点击了状态栏图标!');
        // 这里写自定义逻辑,比如弹出提示框
        promptAction.showToast({ message: '你点击了我的图标~' });
        break;
      default:
        console.info(`未知的点击类型:${data.iconClickType}`);
        break;
    }
  }
};

// 2. 注册监听(比如在页面onPageShow时调用)
const registerIconClickListen = () => {
  statusBarManager.on('statusBarIconClick', this.onStatusBarIconClick);
  console.info('状态栏图标点击监听已注册~');
};

// 调用注册函数
registerIconClickListen();

7. off ('statusBarIconClick'):注销图标点击监听

作用:监听不能一直挂着,比如页面销毁时不注销,会导致内存泄漏,应用可能卡顿,所以一定要用这个接口注销。
参数

  • type:固定'statusBarIconClick'
  • callback:要注销的回调函数,必须和注册时的是同一个。

ArkTS 代码示例

import { statusBarManager } from '@kit.DeskTopExtensionKit';

// 注销图标点击监听(比如在页面onPageHide或onDestroy时调用)
const unregisterIconClickListen = () => {
  statusBarManager.off('statusBarIconClick', this.onStatusBarIconClick);
  console.info('状态栏图标点击监听已注销~');
};

// 调用注销函数(页面销毁时)
this.onPageHide(() => {
  unregisterIconClickListen();
});

8. on ('rightMenuClick'):监听右键菜单点击事件

作用:右键菜单里的每个选项被点击时,咱们要知道点了哪个,然后执行对应逻辑(比如点击 “打开主页” 就跳转到应用主页),就用这个接口监听。
参数

  • type:固定'rightMenuClick'
  • callback:回调函数,能拿到menuCode(菜单标识,用来区分点了哪个菜单)。

ArkTS 代码示例

import { statusBarManager } from '@kit.DeskTopExtensionKit';
import { emitter } from '@kit.BasicServicesKit';
import { router } from '@kit.ArkUI';

// 1. 定义右键菜单点击的处理函数
private onRightMenuClick = (eventData: emitter.EventData) => {
  const data = eventData.data as { menuCode: string };
  if (data) {
    const menuCode = data.menuCode;
    console.info(`用户点击了右键菜单,menuCode:${menuCode}`);
    // 根据menuCode处理不同逻辑(menuCode在配置菜单时设置)
    switch (menuCode) {
      case 'openHome':
        // 点击“打开主页”,跳转到应用主页
        router.pushUrl({ url: 'pages/Index' });
        break;
      case 'showDetail':
        // 点击“查看详情”,跳转到详情页
        router.pushUrl({ url: 'pages/Detail' });
        break;
      default:
        console.info(`未知的菜单标识:${menuCode}`);
        break;
    }
  }
};

// 2. 注册右键菜单点击监听
const registerRightMenuListen = () => {
  statusBarManager.on('rightMenuClick', this.onRightMenuClick);
  console.info('右键菜单点击监听已注册~');
};

// 调用注册函数
registerRightMenuListen();

9. off ('rightMenuClick'):注销右键菜单点击监听

作用:和图标点击监听一样,右键菜单的监听也要在不用的时候注销,避免内存泄漏。
参数

  • type:固定'rightMenuClick'
  • callback:要注销的回调函数,和注册时一致。

ArkTS 代码示例

import { statusBarManager } from '@kit.DeskTopExtensionKit';

// 注销右键菜单点击监听(页面销毁时)
const unregisterRightMenuListen = () => {
  statusBarManager.off('rightMenuClick', this.onRightMenuClick);
  console.info('右键菜单点击监听已注销~');
};

// 页面销毁时调用
this.onPageDestroy(() => {
  unregisterRightMenuListen();
});

三、实战开发!从 0 到 1 接入状态栏(7 步走)

讲完了接口,咱们就进入最核心的实战环节!这部分咱们按照 “导入模块→新建 Ability→配置文件→预置图标→配置图标 / 弹窗 / 菜单→接入状态栏→测试” 的步骤来,每一步都给完整代码,跟着敲就行,别偷懒~

第一步:导入必备模块(就像做饭先备食材)

咱们开发任何功能,第一步都是导入需要的模块,状态栏接入也不例外。这里要导入 5 个核心模块,每个模块的作用我都标在注释里了:

// 1. 桌面拓展服务:核心的状态栏管理API都在这儿
import { statusBarManager, StatusBarViewExtensionAbility } from '@kit.DeskTopExtensionKit';
// 2. Ability相关:管理状态栏弹窗的会话(比如加载弹窗页面)
import { UIExtensionContentSession, Want, common } from '@kit.AbilityKit';
// 3. 图片处理:把咱们的图片转换成代码能识别的PixelMap
import { image } from '@kit.ImageKit';
// 4. 资源管理:读取rawfile里的图标资源
import { resourceManager } from '@kit.LocalizationKit';
// 5. 事件监听:处理图标和菜单的点击事件
import { emitter } from '@kit.BasicServicesKit';
// 6. UI相关:比如弹窗提示、路由跳转(可选,根据业务加)
import { promptAction, router } from '@kit.ArkUI';

这一步很简单,但别漏导模块,不然代码里会报 “找不到 xxx” 的错误,到时候查起来麻烦~

第二步:新建 StatusBarViewExtensionAbility 和弹窗页面

咱们前面说过,左键点击图标会弹出自定义弹窗,这个弹窗的 “架子” 就是StatusBarViewExtensionAbility(状态栏扩展 Ability),而弹窗里显示的内容,需要一个单独的页面(比如叫StatusBarPage.ets)。

2.1 新建 MyStatusBarViewAbility.ets(管理弹窗会话)

首先,在entry/src/main/ets目录下,新建一个文件夹statusbarviewextensionability(名字可以自己改,但后面配置路径要对应),然后在这个文件夹里新建MyStatusBarViewAbility.ets文件,代码如下:

import { StatusBarViewExtensionAbility, statusBarManager } from '@kit.DeskTopExtensionKit';
import { UIExtensionContentSession, Want } from '@kit.AbilityKit';

// 定义日志标签,方便调试时区分日志
const TAG = 'MyStatusBarViewExtAbility';

// 继承StatusBarViewExtensionAbility,这是状态栏弹窗的基类
export default class MyStatusBarViewAbility extends StatusBarViewExtensionAbility {
  // 1. Ability创建时调用(初始化操作放这儿)
  onCreate() {
    console.info(TAG, `Ability创建啦!`);
    // 这里可以做一些初始化,比如初始化数据、注册监听
  }

  // 2. 弹窗会话创建时调用(核心!指定弹窗显示哪个页面)
  onSessionCreate(want: Want, session: UIExtensionContentSession) {
    console.info(TAG, `弹窗会话创建啦,要加载的Ability:${want.abilityName}`);
    // 关键代码:指定弹窗显示的页面(路径是pages/StatusBarPage,后面会新建这个页面)
    session.loadContent('pages/StatusBarPage');
  }

  // 3. Ability进入前台时调用(比如弹窗显示时)
  onForeground() {
    console.info(TAG, `Ability进入前台啦!`);
  }

  // 4. Ability进入后台时调用(比如弹窗隐藏时)
  onBackground() {
    console.info(TAG, `Ability进入后台啦!`);
  }

  // 5. 弹窗会话销毁时调用(释放会话资源)
  onSessionDestroy(session: UIExtensionContentSession) {
    console.info(TAG, `弹窗会话销毁啦!`);
    // 这里可以释放会话相关的资源,比如注销监听
  }

  // 6. Ability销毁时调用(最后一步,释放所有资源)
  onDestroy() {
    console.info(TAG, `Ability销毁啦!`);
    // 这里释放全局资源,比如取消网络请求、注销全局监听
  }
}

重点提醒

  • onSessionCreate里的session.loadContent('pages/StatusBarPage')是核心,路径千万别写错!如果页面放在其他目录,比如pages/statusBar/StatusBarPage,那路径就要改成对应的;
  • 每个生命周期函数的作用都标在注释里了,调试时可以看日志,判断哪个环节出了问题。
2.2 新建 StatusBarPage.ets(弹窗内容页面)

接下来,在entry/src/main/ets/pages目录下,新建StatusBarPage.ets文件,这个页面就是用户左键点击图标时弹出来的内容。咱们先做个简单的测试页面,显示 “这是我的状态栏弹窗” 和一个按钮:

import { Column, Text, Button, StyleSheet, promptAction } from '@kit.ArkUI';

// 弹窗页面的组件
@Entry
@Component
struct StatusBarPage {
  build() {
    // 垂直布局,居中显示内容
    Column({ space: 20, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
      // 标题文字
      Text('这是我的状态栏弹窗')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .textColor('#333333');

      // 测试按钮,点击弹出提示
      Button('点击我试试')
        .width(200)
        .height(40)
        .fontSize(16)
        .backgroundColor('#007AFF')
        .onClick(() => {
          promptAction.showToast({
            message: '弹窗按钮被点击啦!',
            duration: 2000
          });
        });
    }
    // 页面宽度占满,高度用咱们后面配置的300vp
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF');
  }
}

// 样式(也可以直接写在组件里,分开写更清晰)
const styles = StyleSheet.create({
  container: {
    width: '100%',
    height: '100%',
    backgroundColor: '#FFFFFF',
    flexDirection: FlexDirection.Column,
    alignItems: ItemAlign.Center,
    justifyContent: FlexAlign.Center,
    padding: 20
  },
  title: {
    fontSize: 18,
    fontWeight: FontWeight.Bold,
    color: '#333333',
    marginBottom: 20
  },
  button: {
    width: 200,
    height: 40,
    fontSize: 16,
    backgroundColor: '#007AFF'
  }
});

小技巧:弹窗页面的高度不用在这里写死,后面会通过QuickOperation配置统一设置,这里写height('100%')就行,会自动适配配置的高度。

第三步:配置 module.json5(给 Ability “上户口”)

鸿蒙里的任何 Ability(包括咱们新建的MyStatusBarViewAbility),都必须在module.json5里配置,不然系统找不到它,弹窗也弹不出来。这一步就像给 Ability “上户口”,很关键!

3.1 找到 module.json5 文件

路径是entry/src/main/module.json5,别找错了,不是其他目录下的文件~

3.2 配置 extensionAbilities

module.json5module字段下,找到extensionAbilities(如果没有就新建),然后添加如下配置:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "srcPath": "./src/main/ets",
    "deviceTypes": ["tablet", "2in1"], // 因为只支持2in1设备,这里可以加上
    "extensionAbilities": [
      {
        "name": "MyStatusBarViewAbility", // 必须和咱们新建的Ability类名一致
        "icon": "$media:startIcon", //  Ability的图标(可以用应用默认图标)
        "description": "状态栏扩展Ability,负责显示左键弹窗", // 描述,自己写清楚就行
        "type": "statusBarView", // 类型必须是statusBarView,系统才会识别为状态栏扩展
        "exported": true, // 必须设为true,允许系统调用这个Ability
        "srcEntry": "./ets/statusbarviewextensionability/MyStatusBarViewAbility.ets", // Ability文件的路径,千万别写错!
        "skills": [
          {
            "entities": ["entity.system.extension.statusBarView"], // 声明这是状态栏扩展能力
            "actions": ["action.system.statusBarView"]
          }
        ]
      }
    ],
    // 其他配置(比如abilities、reqPermissions等)...
  }
}

重点检查这 3 个字段

  1. name:必须和MyStatusBarViewAbility.ets里的类名完全一致,大小写都不能错;
  2. type:必须是statusBarView,写别的系统不认;
  3. srcEntry:路径要和你实际的文件路径对应,比如你把MyStatusBarViewAbility.ets放在了ets/statusBarExt文件夹下,路径就要改成./ets/statusBarExt/MyStatusBarViewAbility.ets

这一步错了,后面弹窗肯定弹不出来,所以一定要仔细检查!

第四步:预置状态栏图标资源(给图标 “找个家”)

状态栏图标有两个要求:

  1. 尺寸必须是24vp*24vp:因为状态栏空间很小,太大了会变形,太小了看不清;
  2. 要准备两张图:一张白色(适配深色状态栏)、一张黑色(适配浅色状态栏),不然在某些主题下图标会看不见。
4.1 新建 rawfile 文件夹

如果entry/src/main/resources目录下没有rawfile文件夹,就新建一个(名字必须是rawfile,系统只认这个名字)。

4.2 放入图标文件

把准备好的两张 24vp*24vp 的图片放进rawfile里,比如命名为testWhite.png(白色图标)和testBlack.png(黑色图标)。

小提醒

  • 图片格式建议用 PNG,支持透明背景,显示效果更好;
  • 文件名可以自己改,但后面代码里要对应上,别到时候代码里读的是testWhite.png,实际放的是whiteIcon.png,那就会报错。

第五步:配置应用图标信息(把图片转成代码能认的格式)

咱们把图片放进rawfile后,代码不能直接用图片文件,得转换成鸿蒙的PixelMap格式(相当于图片在代码里的 “身份证”),然后构建StatusBarIcon对象,这就是状态栏图标的最终配置。

咱们在index.ets(应用主页)里写这段代码,因为一般是应用启动后就把图标加到状态栏:

import { common } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
import { resourceManager } from '@kit.LocalizationKit';
import { statusBarManager } from '@kit.DeskTopExtensionKit';

@Entry
@Component
struct Index {
  // 应用启动后,在页面加载完成时配置图标并添加到状态栏
  async onPageShow() {
    await this.configStatusBarIcon();
  }

  // 配置状态栏图标信息的函数
  private async configStatusBarIcon() {
    // 1. 获取上下文(关键!没有context啥都干不了)
    const context: common.Context | undefined = this.getUIContext().getHostContext();
    if (!context) {
      console.error('获取宿主上下文失败,没法配置状态栏图标!');
      return;
    }

    try {
      // 2. 获取资源管理器(用来读取rawfile里的图片)
      const resourceMgr: resourceManager.ResourceManager = context.resourceManager;

      // 3. 读取白色图标,转换成PixelMap
      // 3.1 读取rawfile里的testWhite.png文件
      const whiteFileData = resourceMgr.getRawFileContentSync('testWhite.png');
      if (!whiteFileData) {
        console.error('读取白色图标失败,文件可能不存在!');
        return;
      }
      // 3.2 把文件数据转成buffer
      const whiteBuffer = whiteFileData.buffer;
      // 3.3 创建图片源
      const whiteImageSource = image.createImageSource(whiteBuffer);
      // 3.4 转成PixelMap(异步操作,所以用await)
      const whitePixelMap = await whiteImageSource.createPixelMap();

      // 4. 读取黑色图标,转换成PixelMap(和白色图标步骤一样)
      const blackFileData = resourceMgr.getRawFileContentSync('testBlack.png');
      if (!blackFileData) {
        console.error('读取黑色图标失败,文件可能不存在!');
        return;
      }
      const blackBuffer = blackFileData.buffer;
      const blackImageSource = image.createImageSource(blackBuffer);
      const blackPixelMap = await blackImageSource.createPixelMap();

      // 5. 构建StatusBarIcon对象(这就是最终的图标配置)
      const statusBarIcon: statusBarManager.StatusBarIcon = {
        white: whitePixelMap, // 白色图标,适配深色状态栏
        black: blackPixelMap // 黑色图标,适配浅色状态栏
      };

      console.info('状态栏图标配置成功!接下来配置弹窗和菜单~');
      // 这里先存下icon,后面整合的时候要用
      this.statusBarIcon = statusBarIcon;
    } catch (error) {
      console.error(`配置状态栏图标失败!错误信息:${(error as Error).message}`);
    }
  }

  // 后面还要用的变量,先定义在这里
  private statusBarIcon?: statusBarManager.StatusBarIcon; // 图标配置
  private context?: common.Context; // 上下文

  build() {
    // 主页内容(这里随便写,比如一个“添加状态栏图标”的按钮)
    Column({ space: 30, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center })
      .width('100%')
      .height('100%') {
      Text('应用主页')
        .fontSize(24)
        .fontWeight(FontWeight.Bold);

      Button('添加图标到状态栏')
        .width(250)
        .height(50)
        .fontSize(18)
        .backgroundColor('#007AFF')
        .onClick(() => {
          // 后面会在这里写“整合配置并添加到状态栏”的代码
          this.addIconToStatusBar();
        });
    }
  }
}

常见错误排查

  • 如果报 “读取图片失败”,先检查图片文件名是不是和代码里一致,再检查图片是不是在rawfile文件夹里;
  • 如果报 “createPixelMap 失败”,检查图片尺寸是不是 24vp*24vp,格式是不是 PNG。

第六步:配置左键弹窗和右键菜单

图标配置好了,接下来要配置两个关键交互:左键弹窗(QuickOperation)和右键菜单(statusBarGroupMenu)。

6.1 配置左键弹窗(QuickOperation)

QuickOperation里包含弹窗的标题、高度、对应的 Ability(就是咱们新建的MyStatusBarViewAbility),代码继续写在index.ets里,新增一个函数:

// 在Index结构体里新增这个函数
private configQuickOperation(): statusBarManager.QuickOperation {
  // 构建左键弹窗配置
  const quickOperation: statusBarManager.QuickOperation = {
    abilityName: "MyStatusBarViewAbility", // 必须和module.json5里配置的Ability名一致
    title: "我的应用弹窗", // 弹窗的标题,用户能看到
    height: 300, // 弹窗高度,单位vp,根据内容调整(比如内容多就设400)
    moduleName: 'entry' // 模块名,就是咱们的entry模块,可选但建议加上,避免冲突
  };
  return quickOperation;
}

重点abilityName必须和module.json5里的name完全一致,不然系统找不到对应的 Ability,弹窗就弹不出来!

6.2 配置右键菜单(statusBarGroupMenu)

右键菜单是 “菜单组→一级菜单→子菜单” 的层级结构,咱们先做个简单的菜单:一级菜单叫 “应用操作”,子菜单叫 “打开主页” 和 “查看详情”。代码继续写在index.ets里,新增函数:

// 在Index结构体里新增这个函数
private configRightMenu(): Array<statusBarManager.StatusBarGroupMenu> {
  // 1. 配置子菜单1:打开主页
  const subMenu1: statusBarManager.StatusBarSubMenuItem = {
    subTitle: "打开主页", // 子菜单文字
    menuCode: "openHome", // 菜单标识,后面监听点击时用这个区分
    menuAction: {
      abilityName: "EntryAbility", // 点击子菜单要跳转的Ability(比如应用的主Ability)
      moduleName: "entry"
    }
  };

  // 2. 配置子菜单2:查看详情
  const subMenu2: statusBarManager.StatusBarSubMenuItem = {
    subTitle: "查看详情",
    menuCode: "showDetail",
    menuAction: {
      abilityName: "DetailAbility", // 假设咱们有个DetailAbility,没有的话可以先写EntryAbility
      moduleName: "entry"
    }
  };

  // 3. 把两个子菜单放进子菜单数组
  const subMenus: Array<statusBarManager.StatusBarSubMenuItem> = [subMenu1, subMenu2];

  // 4. 配置一级菜单(关联子菜单)
  const menuItem: statusBarManager.StatusBarMenuItem = {
    title: "应用操作", // 一级菜单文字
    subMenu: subMenus, // 关联上面的子菜单
    // 注意:一级菜单的menuAction和subMenu不能都为空,不然菜单显示了点了没反应
    // menuAction: { ... } // 如果不想要子菜单,也可以直接给一级菜单配menuAction
  };

  // 5. 把一级菜单放进菜单数组
  const menuItems: Array<statusBarManager.StatusBarMenuItem> = [menuItem];

  // 6. 把菜单数组放进菜单组(菜单组可以包含多个一级菜单,这里先放一个)
  const groupMenus: Array<statusBarManager.StatusBarGroupMenu> = [menuItems];

  return groupMenus;
}

小提醒

  • menuCode要唯一,后面监听右键点击时,就是通过这个menuCode判断用户点了哪个菜单;
  • 一级菜单的menuActionsubMenu不能都为空,不然菜单显示了但点击没反应,至少要有一个。

第七步:整合所有配置,调用 addToStatusBar 接入状态栏

前面咱们已经配置好了图标(statusBarIcon)、左键弹窗(QuickOperation)、右键菜单(groupMenus),现在要把它们整合到StatusBarItem里,然后调用addToStatusBar接口,把图标加到状态栏!

7.1 新增 addIconToStatusBar 函数

index.ets的 Index 结构体里,新增这个核心函数:

// 整合所有配置,把图标添加到状态栏
private async addIconToStatusBar() {
  // 1. 获取上下文(前面已经获取过,这里再检查一遍)
  const context: common.Context | undefined = this.getUIContext().getHostContext();
  if (!context) {
    console.error('获取上下文失败,没法添加状态栏图标!');
    promptAction.showToast({ message: '添加失败:上下文获取失败' });
    return;
  }

  // 2. 检查图标配置是否存在(前面configStatusBarIcon函数里赋值的)
  if (!this.statusBarIcon) {
    console.error('图标配置不存在,先配置图标!');
    promptAction.showToast({ message: '添加失败:图标未配置' });
    // 如果没配置,先调用配置图标函数
    await this.configStatusBarIcon();
    // 配置后再检查一次,如果还是没有,就返回
    if (!this.statusBarIcon) {
      return;
    }
  }

  // 3. 获取弹窗和菜单配置
  const quickOperation = this.configQuickOperation();
  const groupMenus = this.configRightMenu();

  // 4. 整合所有配置到StatusBarItem
  const statusBarItem: statusBarManager.StatusBarItem = {
    icons: this.statusBarIcon, // 图标配置
    quickOperation: quickOperation, // 左键弹窗配置
    statusBarGroupMenu: groupMenus // 右键菜单配置(可选,不想要可以不写)
  };

  // 5. 调用addToStatusBar接口,添加到状态栏
  try {
    statusBarManager.addToStatusBar(context, statusBarItem);
    console.info('图标成功添加到状态栏!');
    promptAction.showToast({ message: '图标已添加到状态栏', duration: 2000 });

    // 6. 注册点击监听(可选,根据需要加)
    this.registerClickListeners();
  } catch (error) {
    const errorMsg = `添加失败:${(error as Error).message}`;
    console.error(errorMsg);
    promptAction.showToast({ message: errorMsg, duration: 2000 });
  }
}

// 注册图标和菜单的点击监听(前面讲过的on接口)
private registerClickListeners() {
  // 注册图标点击监听
  statusBarManager.on('statusBarIconClick', this.onStatusBarIconClick);
  // 注册右键菜单点击监听
  statusBarManager.on('rightMenuClick', this.onRightMenuClick);
  console.info('点击监听已注册~');
}

// 图标点击的回调函数(前面讲过的)
private onStatusBarIconClick = (eventData: emitter.EventData) => {
  const data = eventData.data as { iconClickType: string };
  if (data && data.iconClickType === 'leftClickType') {
    console.info('用户左键点击了状态栏图标,弹窗会自动弹出~');
    // 因为咱们配置了quickOperation.abilityName,所以这里不用手动处理弹窗,系统会自动加载
  }
};

// 右键菜单点击的回调函数(前面讲过的)
private onRightMenuClick = (eventData: emitter.EventData) => {
  const data = eventData.data as { menuCode: string };
  if (!data) return;

  const menuCode = data.menuCode;
  console.info(`用户点击了右键菜单:${menuCode}`);

  // 根据menuCode处理业务
  switch (menuCode) {
    case 'openHome':
      // 打开主页(如果当前就在主页,不用跳转)
      if (router.getCurrentUrl() !== 'pages/Index') {
        router.pushUrl({ url: 'pages/Index' });
        promptAction.showToast({ message: '正在打开主页~' });
      } else {
        promptAction.showToast({ message: '已经在主页啦~' });
      }
      break;
    case 'showDetail':
      // 打开详情页(假设详情页路径是pages/Detail)
      router.pushUrl({ url: 'pages/Detail' });
      promptAction.showToast({ message: '正在打开详情页~' });
      break;
    default:
      promptAction.showToast({ message: `未知菜单:${menuCode}` });
      break;
  }
};
7.2 绑定按钮点击事件

咱们在build函数里有个 “添加图标到状态栏” 的按钮,现在把addIconToStatusBar函数绑定到按钮的onClick事件上(前面已经写了,这里再确认一下):

Button('添加图标到状态栏')
  .width(250)
  .height(50)
  .fontSize(18)
  .backgroundColor('#007AFF')
  .onClick(() => {
    this.addIconToStatusBar(); // 点击按钮调用添加函数
  });

四、进阶操作:这些功能你可能也需要

咱们已经实现了基础的状态栏接入,但实际开发中,可能还需要动态更新图标、菜单,或者主动移除图标,这些进阶操作咱们也讲一讲,都是前面接口的实际应用。

1. 动态更新状态栏图标

比如音乐 APP 在播放时显示 “播放中” 图标,暂停时显示 “已暂停” 图标,代码如下(在index.ets里新增函数):

// 动态更新状态栏图标(参数是新图标的文件名,比如“playWhite.png”)
private async updateStatusBarIcon(newWhiteIconName: string, newBlackIconName: string) {
  const context: common.Context | undefined = this.getUIContext().getHostContext();
  if (!context) {
    console.error('获取上下文失败,没法更新图标!');
    return;
  }

  try {
    const resourceMgr: resourceManager.ResourceManager = context.resourceManager;

    // 读取新的白色图标
    const newWhiteFileData = resourceMgr.getRawFileContentSync(newWhiteIconName);
    const newWhiteBuffer = newWhiteFileData.buffer;
    const newWhiteImageSource = image.createImageSource(newWhiteBuffer);
    const newWhitePixelMap = await newWhiteImageSource.createPixelMap();

    // 读取新的黑色图标
    const newBlackFileData = resourceMgr.getRawFileContentSync(newBlackIconName);
    const newBlackBuffer = newBlackFileData.buffer;
    const newBlackImageSource = image.createImageSource(newBlackBuffer);
    const newBlackPixelMap = await newBlackImageSource.createPixelMap();

    // 构建新的图标配置
    const newIcon: statusBarManager.StatusBarIcon = {
      white: newWhitePixelMap,
      black: newBlackPixelMap
    };

    // 调用接口更新图标
    statusBarManager.updateStatusBarIcon(context, newIcon);
    console.info('状态栏图标更新成功!');
    promptAction.showToast({ message: '图标已更新', duration: 2000 });

    // 更新本地存储的图标配置
    this.statusBarIcon = newIcon;
  } catch (error) {
    console.error(`更新图标失败:${(error as Error).message}`);
    promptAction.showToast({ message: `更新图标失败:${(error as Error).message}` });
  }
}

// 调用示例(比如音乐播放状态变化时)
private onMusicPlayStatusChange(isPlaying: boolean) {
  if (isPlaying) {
    // 播放中,更新为播放图标
    this.updateStatusBarIcon('playWhite.png', 'playBlack.png');
  } else {
    // 已暂停,更新为暂停图标
    this.updateStatusBarIcon('pauseWhite.png', 'pauseBlack.png');
  }
}

2. 动态更新右键菜单

比如用户登录后,右键菜单显示 “个人中心”,未登录时显示 “登录”,代码如下:

// 根据登录状态更新右键菜单
private updateRightMenuByLoginStatus(isLoggedIn: boolean) {
  const context: common.Context | undefined = this.getUIContext().getHostContext();
  if (!context) {
    console.error('获取上下文失败,没法更新菜单!');
    return;
  }

  let subMenus: Array<statusBarManager.StatusBarSubMenuItem> = [];

  if (isLoggedIn) {
    // 已登录,显示“个人中心”和“退出登录”
    const subMenu1: statusBarManager.StatusBarSubMenuItem = {
      subTitle: "个人中心",
      menuCode: "userCenter",
      menuAction: { abilityName: "UserCenterAbility", moduleName: "entry" }
    };
    const subMenu2: statusBarManager.StatusBarSubMenuItem = {
      subTitle: "退出登录",
      menuCode: "logout",
      menuAction: { abilityName: "EntryAbility", moduleName: "entry" }
    };
    subMenus = [subMenu1, subMenu2];
  } else {
    // 未登录,显示“登录”
    const subMenu: statusBarManager.StatusBarSubMenuItem = {
      subTitle: "登录",
      menuCode: "login",
      menuAction: { abilityName: "LoginAbility", moduleName: "entry" }
    };
    subMenus = [subMenu];
  }

  // 构建一级菜单和菜单组
  const menuItem: statusBarManager.StatusBarMenuItem = {
    title: isLoggedIn ? "用户操作" : "请登录",
    subMenu: subMenus
  };
  const menuItems: Array<statusBarManager.StatusBarMenuItem> = [menuItem];
  const groupMenus: Array<statusBarManager.StatusBarGroupMenu> = [menuItems];

  // 调用接口更新菜单
  try {
    statusBarManager.updateStatusBarMenu(context, groupMenus);
    console.info(`右键菜单已更新为${isLoggedIn ? '登录后' : '未登录'}状态~`);
  } catch (error) {
    console.error(`更新菜单失败:${(error as Error).message}`);
  }
}

// 调用示例(登录状态变化时)
this.onLoginStatusChange = (isLoggedIn: boolean) => {
  this.updateRightMenuByLoginStatus(isLoggedIn);
};

3. 主动移除状态栏图标

比如用户点击 “退出服务” 按钮时,要把图标从状态栏移除,代码如下:

// 主动移除状态栏图标
private removeStatusBarIcon() {
  const context: common.Context | undefined = this.getUIContext().getHostContext();
  if (!context) {
    console.error('获取上下文失败,没法移除图标!');
    return;
  }

  try {
    statusBarManager.removeFromStatusBar(context);
    console.info('图标已从状态栏移除~');
    promptAction.showToast({ message: '图标已移除', duration: 2000 });

    // 移除图标后,注销点击监听
    this.unregisterClickListeners();
  } catch (error) {
    console.error(`移除图标失败:${(error as Error).message}`);
    promptAction.showToast({ message: `移除图标失败:${(error as Error).message}` });
  }
}

// 注销所有点击监听
private unregisterClickListeners() {
  statusBarManager.off('statusBarIconClick', this.onStatusBarIconClick);
  statusBarManager.off('rightMenuClick', this.onRightMenuClick);
  console.info('所有点击监听已注销~');
}

// 绑定到按钮点击事件
Button('移除状态栏图标')
  .width(250)
  .height(50)
  .fontSize(18)
  .backgroundColor('#FF3B30')
  .onClick(() => {
    this.removeStatusBarIcon();
  });

五、踩坑指南!常见问题怎么解决?

咱们开发的时候,肯定会遇到各种问题,比如图标不显示、弹窗弹不出来、菜单点了没反应,我整理了几个常见问题,帮大家快速排查:

1. 状态栏不显示图标

可能原因 1:context 获取失败

  • 排查:看日志里有没有 “getHostContext failed”,如果有,说明 context 没获取到;
  • 解决:确保在 UI 页面中调用this.getUIContext().getHostContext(),并且页面已经初始化完成(比如在onPageShow里调用,别在build里直接调用)。

可能原因 2:module.json5 配置错误

  • 排查:检查extensionAbilities里的nametypesrcEntry是否正确;
  • 解决:name和 Ability 类名一致,typestatusBarViewsrcEntry路径和实际文件路径一致。

可能原因 3:图片资源问题

  • 排查:看日志里有没有 “读取图片失败”“createPixelMap 失败”;
  • 解决:图片放在rawfile里,文件名和代码一致,尺寸是 24vp*24vp,格式是 PNG。

2. 左键点击图标不弹弹窗

可能原因 1:QuickOperation 的 abilityName 错误

  • 排查:检查quickOperation.abilityName是否和module.json5里的name一致;
  • 解决:改成完全一致的名字,大小写都不能错。

可能原因 2:StatusBarPage 路径错误

  • 排查:看onSessionCreate里的session.loadContent路径是否正确;
  • 解决:比如页面在pages/StatusBarPage,路径就写'pages/StatusBarPage',别多写或少写斜杠。

3. 右键菜单不显示或点击没反应

可能原因 1:statusBarGroupMenu 配置错误

  • 排查:检查是否构建了 “菜单组→一级菜单→子菜单” 的结构,一级菜单的subMenumenuAction是否为空;
  • 解决:确保每个一级菜单至少有subMenumenuActionmenuCode唯一。

可能原因 2:没注册右键菜单点击监听

  • 排查:看有没有调用statusBarManager.on('rightMenuClick', ...)
  • 解决:在添加图标后,注册右键菜单监听。

4. 图标显示变形或模糊

可能原因:图片尺寸不是 24vp*24vp

  • 排查:用图片查看工具检查图片尺寸;
  • 解决:重新制作 24vp*24vp 的图片,别拉伸或压缩原有图片。

六、总结:整个流程再捋一遍

咱们今天从 “为啥做” 到 “怎么做”,把鸿蒙应用接入状态栏的整个流程讲透了,最后再帮大家捋一遍关键步骤,方便记忆:

  1. 备食材:导入需要的模块(桌面拓展、Ability、图片处理等);
  2. 搭架子:新建StatusBarViewExtensionAbility(管理弹窗会话)和StatusBarPage(弹窗内容);
  3. 上户口:在module.json5里配置extensionAbilities,让系统识别 Ability;
  4. 备图标:把 24vp*24vp 的黑白图标放进rawfile
  5. 做配置:分别配置图标(StatusBarIcon)、左键弹窗(QuickOperation)、右键菜单(statusBarGroupMenu);
  6. 整合接入:把所有配置放进StatusBarItem,调用addToStatusBar添加到状态栏;
  7. 加功能:根据需要添加动态更新图标 / 菜单、主动移除图标、监听点击事件等进阶功能。

只要跟着这 7 步走,每个步骤仔细检查,别犯前面说的常见错误,你肯定能成功把应用接入状态栏!如果在开发中遇到其他问题,也可以去华为开发者官网(就是咱们参考的那个链接)看更详细的接口文档,或者在鸿蒙开发者社区提问,大家一起交流解决~

希望这篇文章能帮到各位小伙伴,祝大家在鸿蒙开发的路上越走越顺,开发出更多好用又好看的应用!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐