鸿蒙PC Dock 栏隐藏“打开新窗口“功能实现详解

鸿蒙PC Dock 栏隐藏"打开新窗口"功能实现详解

问题背景

在鸿蒙 PC 平台上开发 Electron 应用时,dock 栏右键菜单默认会显示"打开新窗口"选项。对于单实例应用,需要完全隐藏这些选项,确保用户无法通过 dock 栏创建多个应用实例。

问题现象

在 dock 栏右键菜单中,通常会看到两个"打开新窗口"相关的选项:

  1. 系统默认的"打开新窗口" :由系统根据应用的 launchType 自动生成
  2. 快捷方式定义的"打开新窗口" :由应用在 shortcuts_config.json 中定义的快捷方式生成

这两个选项都会导致创建新的应用实例,对于单实例应用来说是不期望的行为。

解决方案

通过多层配置 + 代码拦截的方式,实现完全隐藏 dock 栏的"打开新窗口"功能。

完整实现步骤

步骤 1:应用级别配置(AppScope/app.json5)

设置应用为单实例模式:

json 复制代码
{
  "app": {
    "bundleName": "com.nutpi.min",
    "vendor": "example",
    "versionCode": 1000000,
    "versionName": "1.0.0",
    "icon": "$media:app_icon",
    "label": "$string:app_name",
    "configuration": "$profile:configuration",
    "multiAppMode": {
      "multiAppModeType": "appClone",
      "maxCount": 1
    }
  }
}

关键配置说明

  • multiAppModeType: "appClone":应用克隆模式,但限制最大数量为 1
  • maxCount: 1:最多只允许 1 个实例

注意 :也可以使用 "multiAppModeType": "singleInstance",但某些版本可能不支持,使用 appClone + maxCount: 1 更兼容。

步骤 2:Ability 级别配置(electron/src/main/module.json5)

设置 EntryAbility 为单例模式:

json 复制代码
{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "launchType": "singleton",  // ✅ 关键:单例模式
        "removeMissionAfterTerminate": true,
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home",
              "entity.system.browsable"
            ],
            "actions": [
              "action.system.home",
              "ohos.want.action.viewData"
            ],
            "uris": []
          }
        ]
        // ✅ 移除了 shortcuts metadata 引用
      }
    ]
  }
}

关键配置说明

  • launchType: "singleton":Ability 单例模式,系统不会创建新实例
  • 移除了 metadata 中的 ohos.ability.shortcuts 引用:避免显示快捷方式菜单

步骤 3:移除快捷方式配置(shortcuts_config.json)

清空或删除快捷方式定义:

修改前

json 复制代码
{
  "shortcuts": [
    {
      "shortcutId": "OpenNewWindow",
      "label": "$string:OpenNewWindow_label",
      "wants": [
        {
          "bundleName": "com.huawei.ohos_electron",
          "moduleName": "electron",
          "abilityName": "EntryAbility",
          "parameters": {
            "openInNewWindow": "true"
          }
        }
      ]
    }
  ]
}

修改后

json 复制代码
{
  "shortcuts": []
}

关键说明

  • 快捷方式配置会在 dock 栏右键菜单中生成额外的"打开新窗口"选项
  • 清空 shortcuts 数组后,自定义的快捷方式菜单项将不再显示

步骤 4:AbilityStage 层面拦截(WebAbilityStage.ets)

onAcceptWant 方法中拦截 openInNewWindow 参数:

typescript 复制代码
onAcceptWant(want: Want): string {
  LogUtil.info(TAG, "ability stage on accept want :" + JSON.stringify(want));
  let instanceKey = want.parameters?.instanceKey;
  if (instanceKey) {
    return instanceKey.toString();
  }

  // ✅ 单实例模式:拦截 openInNewWindow,不创建新窗口,返回现有窗口 ID
  if (want.parameters?.openInNewWindow) {
    LogUtil.warn(TAG, "openInNewWindow detected, but single instance mode is enabled. Returning existing window ID.");
    // 不创建新窗口,返回现有窗口的 ID
    if (GlobalThisHelper.isLaunched()) {
      let lastActiveBrowserId = GlobalThisHelper.getLastActiveBrowserId();
      if (lastActiveBrowserId !== undefined) {
        LogUtil.info(TAG, "Returning last active browser ID: " + lastActiveBrowserId);
        return lastActiveBrowserId;
      }
      let result = this.nativeContext?.ExecuteCommand(
        CommandType.kGetLastActiveWidget, { is_sync: true });
      let widgetId = result?.last_widget_Id;
      if (widgetId) {
        LogUtil.info(TAG, "Returning last active widget ID: " + ConfigData.WINDOW_PREFIX + widgetId);
        return ConfigData.WINDOW_PREFIX + widgetId;
      }
    }
    // 如果没有现有窗口,返回默认窗口 ID(不会创建新窗口)
    LogUtil.warn(TAG, "No existing window found, returning default window ID");
    return ConfigData.DEFAULT_WINDOW_ID;
  }

  // ... 其他逻辑
}

工作原理

  • 当系统尝试创建新窗口时,onAcceptWant 会被调用
  • 检测到 openInNewWindow 参数后,不创建新窗口
  • 返回现有窗口的 ID,系统会复用现有窗口

步骤 5:Ability 层面拦截(WebAbility.ets)

onNewWant 方法中拦截并激活现有窗口:

typescript 复制代码
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
  const params = want?.parameters;
  
  // ✅ 单实例模式:如果检测到 openInNewWindow,激活现有窗口而不是创建新窗口
  if (params?.openInNewWindow) {
    LogUtil.warn(TAG, 'openInNewWindow detected in onNewWant, but single instance mode is enabled. Activating existing window.');
    let windowClass = this.abilityManager?.getProxy(this.xcomponentId)?.getWindow();
    if (windowClass) {
      // 显示窗口并激活(showWindow 会自动将窗口带到前台)
      windowClass.showWindow()
        .then(() => {
          LogUtil.info(TAG, 'Window activated successfully');
        })
        .catch((err: BusinessError) => {
          LogUtil.error(TAG, 'Failed to show window. Cause: ' + JSON.stringify(err));
        });
    }
    return;
  }
  
  // ... 其他逻辑
}

工作原理

  • onNewWant 在 Ability 接收到新的启动请求时调用
  • 检测到 openInNewWindow 后,激活现有窗口而不是创建新窗口
  • 使用 showWindow() 将窗口带到前台

步骤 6:BaseAbility 层面拦截(WebBaseAbility.ets)

onCreate 方法中拦截:

typescript 复制代码
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
  this.initParameters(want);

  if (CheckEmptyUtils.isEmpty(this.xcomponentId)) {
    LogUtil.info(TAG, 'onCreate, Allocate xcomponent id');
    // ✅ 单实例模式:即使检测到 openInNewWindow,也不创建新窗口
    if (want.parameters?.openInNewWindow) {
      LogUtil.warn(TAG, 'openInNewWindow detected in onCreate, but single instance mode is enabled.');
      // 使用现有窗口 ID
      if (GlobalThisHelper.isLaunched()) {
        let lastActiveBrowserId = GlobalThisHelper.getLastActiveBrowserId();
        if (lastActiveBrowserId !== undefined) {
          this.xcomponentId = lastActiveBrowserId;
          LogUtil.info(TAG, 'Using existing window ID: ' + this.xcomponentId);
          return;
        }
      }
      // 如果没有现有窗口,使用默认 ID(不会创建新窗口)
      this.xcomponentId = ConfigData.DEFAULT_WINDOW_ID;
      LogUtil.warn(TAG, 'No existing window found, using default window ID');
      return;
    }
    // ... 正常创建逻辑
  }
}

工作原理

  • onCreate 在 Ability 创建时调用
  • 检测到 openInNewWindow 后,使用现有窗口 ID 而不是创建新窗口

步骤 7:窗口层面配置(WebAbility.ets)

禁用 dock 栏悬停显示:

typescript 复制代码
// 在 onWindowStageCreate 中
if (this.deviceInfoAdapter?.isNormalWindowMode() === false) {
  window.setWindowTitleMoveEnabled(!this.hideTitleBar);
  windowStage.setWindowModal(this.isModal);
  // ✅ 禁用 dock 栏的"打开新窗口"功能
  // setTitleAndDockHoverShown(标题栏悬停显示, dock栏悬停显示)
  // false 表示禁用,true 表示启用
  window.setTitleAndDockHoverShown(false, false);
} else {
  LogUtil.info(TAG, 'tablet free multi-window model is disabled');
  // ✅ PC 模式下也禁用 dock 栏的"打开新窗口"功能
  try {
    window.setTitleAndDockHoverShown(false, false);
  } catch (exception) {
    LogUtil.warn(TAG, 'Failed to set dock hover shown: ' + JSON.stringify(exception));
  }
}

工作原理

  • setTitleAndDockHoverShown(false, false) 禁用标题栏和 dock 栏的悬停菜单
  • 虽然可能无法完全隐藏菜单项,但可以禁用悬停显示

步骤 8:Electron 层面防护(main.js)

添加单实例锁机制:

javascript 复制代码
const { app, BrowserWindow } = require('electron');

let mainWindow;

// ✅ 单实例锁,防止从 dock 栏打开新窗口
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
  // 如果已经有实例在运行,则退出
  app.quit();
} else {
  // 监听第二个实例启动事件(从 dock 栏右键菜单"打开新窗口"触发)
  app.on('second-instance', (event, commandLine, workingDirectory) => {
    // 如果主窗口存在,则激活并显示它
    if (mainWindow) {
      if (mainWindow.isMinimized()) {
        mainWindow.restore();
      }
      mainWindow.focus();
      mainWindow.show();
    }
  });
}

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: false,
      contextIsolation: true,
    },
  });

  mainWindow.loadFile('index.html');
  
  // 主窗口关闭时清理
  mainWindow.on('closed', () => {
    mainWindow = null;
  });
  
  return mainWindow;
}

// ... 其他代码

工作原理

  • app.requestSingleInstanceLock() 确保只有一个应用实例运行
  • 如果用户尝试启动第二个实例(如通过 dock 栏"打开新窗口"),会触发 second-instance 事件
  • 新实例立即退出,并激活现有窗口

完整配置架构图

复制代码
┌─────────────────────────────────────────────────────────┐
│  应用级别配置 (AppScope/app.json5)                        │
│  multiAppMode: {                                          │
│    multiAppModeType: "appClone",                          │
│    maxCount: 1                                             │
│  }                                                         │
└─────────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────────┐
│  Ability级别配置 (electron/src/main/module.json5)        │
│  launchType: "singleton"                                  │
│  移除 shortcuts metadata 引用                             │
└─────────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────────┐
│  快捷方式配置 (shortcuts_config.json)                     │
│  shortcuts: []  // 清空快捷方式                            │
└─────────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────────┐
│  AbilityStage层面 (WebAbilityStage.ets)                   │
│  onAcceptWant() 拦截 openInNewWindow                      │
│  返回现有窗口 ID,不创建新窗口                             │
└─────────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────────┐
│  Ability层面 (WebAbility.ets)                             │
│  onNewWant() 拦截并激活现有窗口                            │
└─────────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────────┐
│  BaseAbility层面 (WebBaseAbility.ets)                     │
│  onCreate() 使用现有窗口 ID                                │
└─────────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────────┐
│  窗口层面 (WebAbility.ets)                                │
│  setTitleAndDockHoverShown(false, false)                  │
└─────────────────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────────────────┐
│  Electron层面 (main.js)                                   │
│  requestSingleInstanceLock()                              │
│  second-instance 事件处理                                  │
└─────────────────────────────────────────────────────────┘

关键配置对比

修改前

app.json5:

json 复制代码
{
  "multiAppMode": {
    "multiAppModeType": "multiInstance",
    "maxCount": 1
  }
}

module.json5:

json 复制代码
{
  "launchType": "specified",  // ❌ 允许创建多个实例
  "metadata": [
    {
      "name": "ohos.ability.shortcuts",
      "resource": "$profile:shortcuts_config"  // ❌ 引用快捷方式
    }
  ]
}

shortcuts_config.json:

json 复制代码
{
  "shortcuts": [
    {
      "shortcutId": "OpenNewWindow",  // ❌ 定义快捷方式
      "label": "$string:OpenNewWindow_label",
      "wants": [
        {
          "parameters": {
            "openInNewWindow": "true"
          }
        }
      ]
    }
  ]
}

结果:dock 栏右键菜单显示两个"打开新窗口"选项

修改后

app.json5:

json 复制代码
{
  "multiAppMode": {
    "multiAppModeType": "appClone",  // ✅ 应用克隆模式
    "maxCount": 1                     // ✅ 限制为 1 个实例
  }
}

module.json5:

json 复制代码
{
  "launchType": "singleton",  // ✅ 单例模式
  // ✅ 移除了 shortcuts metadata
}

shortcuts_config.json:

json 复制代码
{
  "shortcuts": []  // ✅ 清空快捷方式
}

结果:dock 栏右键菜单不再显示"打开新窗口"选项

实现效果

✅ 已实现的功能

  1. 完全隐藏快捷方式菜单项 :通过清空 shortcuts_config.json,移除了自定义的"打开新窗口"选项

  2. 系统菜单项被拦截:虽然系统可能仍显示"打开新窗口",但点击后不会创建新窗口

  3. 多层防护机制:从配置到代码,多个层面都进行了拦截

  4. 单实例运行:应用确保只有一个主窗口实例

测试验证

  1. 编译应用

    bash 复制代码
    hvigor assembleHap --mode module -p module=electron@default
  2. 验证步骤

    • 右键点击 dock 栏中的应用图标
    • 验证:菜单中不再显示"打开新窗口"选项
    • 如果系统仍显示(系统限制),点击后验证不会创建新窗口
  3. 多次测试

    • 尝试通过不同方式启动应用
    • 验证:始终只有一个窗口实例

技术要点总结

1. 配置层级

  • 应用级别multiAppMode 控制应用实例数量
  • Ability级别launchType 控制 Ability 实例模式
  • 快捷方式shortcuts 数组控制右键菜单项

2. 代码拦截点

  • AbilityStage.onAcceptWant:拦截窗口创建请求
  • Ability.onNewWant:拦截新启动请求
  • BaseAbility.onCreate:拦截创建请求

3. 窗口激活机制

当检测到 openInNewWindow 时:

  1. 不创建新窗口
  2. 获取现有窗口引用
  3. 调用 showWindow() 激活窗口
  4. 窗口自动带到前台

4. Electron 单实例锁

javascript 复制代码
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
  app.quit();  // 第二个实例立即退出
} else {
  app.on('second-instance', () => {
    // 激活现有窗口
  });
}

常见问题

Q1: 为什么需要多层拦截?

A: 鸿蒙系统在不同阶段可能会触发窗口创建,多层拦截确保所有路径都被覆盖:

  • onAcceptWant:系统级别拦截
  • onNewWant:Ability 级别拦截
  • onCreate:实例创建级别拦截

Q2: appClonesingleInstance 的区别?

A:

  • singleInstance:严格单实例,系统完全不显示"打开新窗口"
  • appClone + maxCount: 1:应用克隆模式但限制为 1 个实例,兼容性更好

Q3: 为什么清空 shortcuts 数组而不是删除文件?

A:

  • 保留文件结构,避免配置解析错误
  • 如果完全删除文件,需要同时移除 module.json5 中的 metadata 引用
  • 清空数组更安全,不会影响其他配置

Q4: 如果系统仍显示"打开新窗口"怎么办?

A:

  • 这是系统级别的限制,无法完全移除
  • 但通过代码拦截,点击后不会创建新窗口
  • 系统会激活现有窗口,实现单实例效果

最佳实践

  1. 配置一致性 :确保 app.json5module.json5 中的配置一致

  2. 代码拦截:即使配置正确,也建议添加代码拦截作为双重保障

  3. 日志记录:在拦截点添加日志,便于调试和排查问题

  4. 测试验证:在不同版本的鸿蒙系统上测试,确保兼容性

总结

通过配置 + 代码拦截的组合方案,成功实现了隐藏 dock 栏"打开新窗口"功能:

配置层面

  • 应用级别:multiAppModeType: "appClone", maxCount: 1
  • Ability级别:launchType: "singleton"
  • 快捷方式:清空 shortcuts 数组

代码层面

  • AbilityStage 拦截:onAcceptWant
  • Ability 拦截:onNewWant
  • BaseAbility 拦截:onCreate
  • Electron 防护:单实例锁

效果

  • dock 栏右键菜单不再显示"打开新窗口"选项
  • 应用以单实例模式运行
  • 多层防护确保万无一失
相关推荐
穆雄雄2 小时前
Qt-for-鸿蒙PC Slider 组件开源鸿蒙开发实践
qt·开源·harmonyos
爱笑的眼睛113 小时前
HarmonyOS网络请求Kit使用详解:从基础到分布式场景实践
华为·harmonyos
爱笑的眼睛1112 小时前
HarmonyOS后台代理提醒机制深度解析与实践
华为·harmonyos
爱编程的喵喵15 小时前
《华为数据之道》发行五周年暨《数据空间探索与实践》新书发布会召开,共探AI时代数据治理新路径
人工智能·华为
ins_lizhiming15 小时前
在华为910B GPU服务器上运行DeepSeek-R1-0528模型
人工智能·pytorch·python·华为
ins_lizhiming15 小时前
华为昇腾910B服务器上部署Qwen3-30B-A3B并使用EvalScope推理性能测试
人工智能·华为
IT考试认证15 小时前
华为AI认证 H13-321 HCIP-AI V2.0题库
人工智能·华为·题库·hcip-ai·h13-321
IT考试认证18 小时前
华为AI认证 H13-323 HCIP-AI Solution Architect 题库
人工智能·华为·题库·hcip-ai·h13-323
爱笑的眼睛1119 小时前
ArkTS接口与泛型在HarmonyOS应用开发中的深度应用
华为·harmonyos