在鸿蒙电脑的自由多窗模式里,主窗切到后台后,一个辅助面板仍留在屏幕上。这不一定是窗口没关掉。HarmonyOS 7(API 26)的独立子窗本来就允许这种行为:它不跟随主窗的前后台切换,但会随主窗销毁。

真正容易出错的地方有两个:按钮连点创建出多扇窗;主窗切后台时按旧子窗的思路把面板销毁,回来后又创建一次。两种问题看起来都像“窗口状态乱了”,处理方式却不同。

主窗、独立子窗与销毁阶段示意

先把能力边界说准

华为的子窗口开发指导于 2026 年 9 月 9 日更新,明确写了:从 API 26.0.0 开始,可以在 createSubWindowWithOptions()SubWindowOptions 中将 zLevelAboveParentLoosened 设为 true,创建独立子窗。在自由窗口状态下,它不随主窗切换前后台,只随主窗销毁;主窗和子窗可以通过点击调整层级。

这句话有两个限制不能省略。第一,这不是应用外全局悬浮窗。第二,非自由窗口状态下,子窗仍只在主窗范围内显示。手机上做一个应用内提示层,未必需要子窗,普通组件或弹层通常更直接。

现象应先判断什么不应直接做什么
主窗切后台,辅助面板还在是否启用了独立子窗,设备是否处于自由窗口状态把它认作内存泄漏
连点按钮出现多扇辅助面板创建 Promise 是否共享;是否仅在创建完成后才记录实例每次点击都重新调用创建接口
子窗白屏页面路径、setUIContentshowWindow 的先后及错误回调只重试 showWindow,不检查加载失败
主窗销毁后面板消失是否属于预期的父子窗生命周期把独立子窗当成永久系统窗

案例一:连点“打开面板”,创建了三扇窗

复现条件是创建子窗需要一点时间,用户在第一次创建完成前连续点击。只用 if (this.window) 拦截不够,因为那时 window 还没赋值,三个调用都能进入 createSubWindowWithOptions()

这里要锁住的是完整的创建流程,而不只是创建接口本身。创建、设置大小、加载页面、显示全部完成前,后续点击应复用同一个 opening Promise。加载失败时还要销毁已经创建的半成品,否则下一次重试更难判断当前窗口是哪一个。

下面是我抽出的窗口流程控制器。它不直接依赖系统 API,port 负责接入真正的 HarmonyOS 窗口;这样“是否重复创建”“失败是否清理”可以先做自动化测试。

export class IndependentSubwindowController {
  constructor(port) {
    this.port = port;
    this.window = null;
    this.opening = null;
    this.closing = null;
  }

  async open() {
    if (this.closing) await this.closing;
    if (this.window) return this.port.show(this.window);
    if (this.opening) return this.opening;

    const opening = this.createAndShow();
    this.opening = opening;
    try {
      await opening;
    } finally {
      if (this.opening === opening) this.opening = null;
    }
  }

  async createAndShow() {
    const win = await this.port.create();
    try {
      await this.port.configure(win);
      await this.port.load(win);
      await this.port.show(win);
      this.window = win;
    } catch (error) {
      try {
        await this.port.destroy(win);
      } catch {
        // 保留最初的加载错误,避免清理错误覆盖现场。
      }
      throw error;
    }
  }

  async close() {
    if (this.opening) {
      try {
        await this.opening;
      } catch {
        return;
      }
    }
    if (this.closing) return this.closing;
    if (!this.window) return;

    const win = this.window;
    this.window = null;
    const closing = this.port.destroy(win);
    this.closing = closing;
    try {
      await closing;
    } finally {
      if (this.closing === closing) this.closing = null;
    }
  }
}

port.create() 在 API 26 的接入点对应以下调用;页面路径、尺寸和位置要换成自己的工程值。华为文档建议在 showWindow() 前设置尺寸和位置。未设置尺寸时,自由窗口状态下可能以当前物理屏幕大小显示,非自由窗口状态下可能以主窗大小显示。

import { window } from '@kit.ArkUI';

const options: window.SubWindowOptions = {
  title: 'IndependentPanel',
  decorEnabled: true,
  zLevelAboveParentLoosened: true
};

// windowStage 从 UIAbility 的 onWindowStageCreate 获取并传入。
const panel: window.Window = await windowStage.createSubWindowWithOptions(
  'IndependentPanel', options
);

// 接下来按顺序完成 resize / moveWindowTo、setUIContent、showWindow,
// 各步都处理异步失败;不用“创建成功”代替“页面显示成功”。

这里没有把上面的控制器说成完整的 ArkTS 工程:窗口 API 的接入、页面注册和设备显示还需要在 API 26 SDK 工程中核验。能在本地复现的是流程控制逻辑,而不是系统绘制结果。

案例二:主窗切后台,面板“没有一起消失”

这次先别急着修。用自由窗口模式做四步验证:

  1. 打开主窗,创建独立子窗并确认两扇窗都可点击。
  2. 点击另一应用,让主窗退到后台,观察独立子窗是否仍能单独显示与获焦。
  3. 再点回主窗,确认没有额外创建第二个同名面板。
  4. 销毁主窗,确认独立子窗跟随销毁;若业务要求提前关闭,主动调用 destroyWindow(),不要仅等待主窗生命周期。

第二步和第四步是两种不同的生命周期事件。把“主窗不在前台”误写成“主窗已销毁”,就会破坏 API 26 新能力。反过来,若辅助面板包含敏感信息,产品确实希望退后台时隐藏,也应该明确做隐藏/关闭策略,并验证回前台时是否复用旧实例,而不是靠窗口默认行为碰运气。

我实际验证了哪部分

我把上面控制器的同一份代码用 Node.js 跑了三个测试:连续三次打开只创建一扇窗;创建过程中关闭只销毁一次;页面加载失败会清理半成品并允许重试。结果是 3 pass / 0 fail。这证明了流程控制器的并发和失败分支,不证明 API 26 SDK 已编译通过,也不证明鸿蒙电脑、平板的实际窗口层级完全一致。

要完成设备验收,至少保留这几条日志:创建调用次数、创建返回的窗口名、页面加载回调、显示回调、主动销毁原因,以及主窗前后台/销毁时间。截图要分别覆盖自由窗口和非自由窗口状态。只看一张“子窗还在”的截图,无法判断是预期独立层级还是重复创建。

哪种做法更合适

若需求只是应用内提示、确认框或轻量浮层,先选组件级弹层;它更容易跟随主窗布局,也不需要处理第二扇窗口。若需求是在鸿蒙电脑自由多窗里提供可独立切换层级的工具面板,才考虑 API 26 独立子窗。选择后用一个控制器统一创建、加载、显示、关闭与失败清理,页面按钮不要各自调用窗口 API。

以后排查“窗口关不掉”时,我会先问三个问题:是否真用了 zLevelAboveParentLoosened;当前是不是自由窗口状态;看到的是同一扇窗,还是连点后留下的多扇窗。这三个答案通常比先改 z-index 或重启页面更有用。

参考:子窗口开发指导 · 子窗口常见问题 · 自由窗口简介

Logo

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

更多推荐