在这里插入图片描述

你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的App!
📌 关注本专栏《零基础学鸿蒙开发》,一起变强!
每一节内容我都会持续更新,配图+代码+解释全都有,欢迎点个关注,不走丢,我是小白酷爱学习,我们一起上路 🚀

前言

有时候,Dialog 或者 AlertDialog 并不能满足需求——需要在窗口外显示内容、需要一块完全独立的渲染区域、或者需要跨 UIAbility 叠加一个自定义 UI。这时候,很多开发者自然而然会想到"子窗口"。

子窗口在 HarmonyOS 中是一个相对独立的能力,并不是 ArkUI 弹层的替代方案,也不是 Dialog 的增强版。在动手写代码之前,理解它的定位,比理解接口本身更重要。

一、子窗口解决什么问题

ArkUI 里的 Dialog、Sheet、Overlay,本质上都是在当前 UIAbility 的 Window 内做事情——组件叠加、层级管理、动画,都在同一个渲染树里完成。绝大多数弹层场景,这个范围已经足够用了。

子窗口(SubWindow)是另一种东西。它通过 Window 模块创建,是一个独立的窗口实例,拥有自己的渲染上下文,可以加载独立的 UI 页面。与主窗口是同一个 WindowStage 下的兄弟关系。

用一张对比来理解:

维度ArkUI Overlay / Dialog子窗口(SubWindow)
渲染上下文与主窗口共享独立
加载内容ArkUI 组件独立 .ets 页面
位置控制通过布局参数通过 moveWindowTo / resize
使用条件随时可用需 Stage 模型 + WindowStage
适用场景普通弹层、确认框、提示浮窗、独立交互面板

官方开发指南对此有明确说明:移动设备上的普通弹层场景,通常应优先考虑组件 Overlay,而不是滥用子窗口。 这句话的分量值得认真对待——子窗口是一个系统级的窗口资源,生命周期需要手动管理,用错场景反而会增加维护成本。

二、官方限制先看清楚

在动手之前,有几个前提条件需要确认:

模型要求:子窗口能力仅在 Stage 模型下可用。FA 模型没有对应接口。

API Level:WindowStage.createSubWindow、Window.setUIContent、Window.showWindow、Window.destroyWindow 均从 API 9 起支持。

入口条件:createSubWindow 需要通过 WindowStage 实例调用,而 WindowStage 对象只能在 UIAbility 的生命周期回调(onWindowStageCreate)中获取或保存下来。

设备说明:官方文档中,子窗口主要以 Phone / Tablet / 2in1 为说明背景。移动端(Phone)日常弹层场景,官方明确建议优先使用 Overlay 而非子窗口。

权限:创建普通子窗口不需要特别的权限声明,但若需要设置窗口为系统级浮层(SystemAlertWindow 等),则涉及系统权限,普通应用通常不可用。本文只讨论普通应用可用的子窗口。

三、最小实践场景

这次用一个最小示例验证子窗口的完整生命周期:在点击按钮时创建并展示子窗口,在子窗口内点击关闭按钮后销毁它。

整个场景只涉及两个文件:

  • MainAbility.ets —— 持有 WindowStage,提供 createSubWindow 调用入口
  • SubWindowPage.ets —— 子窗口加载的 UI 页面

四、核心代码实现

4.1 在 UIAbility 中保存 WindowStage

WindowStage 对象只在 onWindowStageCreate 回调里可以拿到,如果后续还需要用它创建子窗口,需要自己保存下来。

// MainAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class MainAbility extends UIAbility {
  // 保存 WindowStage 供后续创建子窗口使用
  private windowStage: window.WindowStage | null = null;

  onWindowStageCreate(windowStage: window.WindowStage): void {
    this.windowStage = windowStage;
    // 加载主窗口页面
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        console.error('Failed to load content. Cause: ' + JSON.stringify(err));
        return;
      }
    });
  }
}

WindowStage 不能在 onWindowStageCreate 之外凭空获取,这是最常见的一个理解偏差——不少开发者会去找"全局获取当前 WindowStage"的 API,实际上并不存在这样的接口,只能在回调里保存下来再传递使用。

4.2 创建子窗口并加载页面

以下代码演示 createSubWindow → setUIContent → showWindow 的完整调用链。

// 假设 windowStage 已从 UIAbility 传递过来
async function createAndShowSubWindow(windowStage: window.WindowStage): Promise<void> {
  let subWindow: window.Window | null = null;

  try {
    // 第一步:创建子窗口,名称需唯一
    subWindow = await windowStage.createSubWindow('MySubWindow');
    console.info('SubWindow created.');

    // 第二步:设置子窗口的初始尺寸和位置(可选,未设置则使用默认值)
    await subWindow.moveWindowTo(100, 200);
    await subWindow.resize(400, 300);

    // 第三步:为子窗口加载 UI 页面
    await subWindow.setUIContent('pages/SubWindowPage');
    console.info('SubWindow content loaded.');

    // 第四步:显示子窗口
    await subWindow.showWindow();
    console.info('SubWindow is now visible.');

  } catch (err) {
    console.error('SubWindow operation failed: ' + JSON.stringify(err));
  }
}

真正需要关注的是这几个地方:

  • createSubWindow 的 name 参数在同一个 WindowStage 下必须唯一,重复创建同名子窗口会报错;
  • setUIContent 的路径是相对于模块根目录的页面路径(与 router.pushUrl 格式一致),页面必须在 main_pages.json 中声明;
  • showWindow 要在 setUIContent 完成之后调用,否则显示的是空白窗口;
  • 以上接口均返回 Promise,建议用 async/await 处理,避免回调嵌套导致顺序控制出错。

4.3 销毁子窗口

子窗口需要显式销毁,不会随组件卸载自动回收。

async function destroySubWindow(subWindow: window.Window): Promise<void> {
  try {
    await subWindow.destroyWindow();
    console.info('SubWindow destroyed.');
  } catch (err) {
    console.error('Failed to destroy SubWindow: ' + JSON.stringify(err));
  }
}

4.4 SubWindowPage 页面(最小示例)

子窗口加载的页面和普通 ArkUI 页面写法完全一致,只是它运行在独立的渲染上下文中。

// pages/SubWindowPage.ets
@Entry
@Component
struct SubWindowPage {
  build() {
    Column({ space: 16 }) {
      Text('这是子窗口')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      Button('关闭')
        .onClick(() => {
          // 在子窗口页面内无法直接调用 destroyWindow
          // 需要通过消息机制通知主窗口侧执行销毁
          // 例如使用 EventHub 或 AppStorage 协调
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .backgroundColor('#FFFFFF')
  }
}

这里先不展开子窗口与主窗口之间的通信,按照官方接口定义,子窗口页面本身并不能直接持有创建时的 Window 实例,需要通过 UIAbility 上下文或全局状态(如 AppStorage)做桥接。这是子窗口在实际项目里比弹层组件麻烦的根本原因。

4.5 子窗口需要在 main_pages.json 中声明

{
  "src": [
    "pages/Index",
    "pages/SubWindowPage"
  ]
}

这一步很容易漏掉,加载时会直接抛出找不到页面的错误。

五、几个关键点拆开看

关于 createSubWindow 的时机

createSubWindow 只能在 WindowStage 实例可用的情况下调用,也就是说,UIAbility 已经进入 onWindowStageCreate 阶段之后。如果在 onCreate 阶段或未持有 WindowStage 的地方调用,会因为没有有效 WindowStage 而失败。建议把 windowStage 的引用通过 AbilityContext 或自定义单例妥善管理。

关于 setUIContent 的路径格式

路径格式是 'pages/SubWindowPage',不需要加 .ets 后缀,也不需要加 / 前缀。这和 router.pushUrl 的 url 格式一致,但如果写错大小写在真机上可能表现不同(部分设备文件系统区分大小写)。

关于尺寸和位置

子窗口创建后默认尺寸是全屏,位置是 (0, 0)。如果希望它看起来像一个浮层,需要主动调用 moveWindowTo 和 resize 设置。这两个接口的参数单位是 px(物理像素),不是 vp。在不同分辨率设备上写固定像素值会有适配问题,实际项目里建议通过 display.getDefaultDisplaySync().densityPixels 换算后再传入。

关于销毁时机

如果页面离开(UIAbility 进入 onWindowStageDestroy)时没有主动销毁子窗口,系统行为取决于具体场景。安全的做法是在 onWindowStageDestroy 或其他合适的生命周期回调里主动调用 destroyWindow,避免资源泄漏。

六、容易踩坑的地方

同名子窗口重复创建

createSubWindow 传入的名称在同一 WindowStage 下必须唯一。如果用户多次触发创建按钮,而每次都用了同一个名称,第二次调用会出错。实际写法里,要么在创建前判断子窗口实例是否已存在,要么在销毁时把引用置空再允许再次创建。

在子窗口页面中拿不到 Window 实例

子窗口加载的页面(SubWindowPage.ets)并不会自动获得指向当前 Window 的引用。如果需要在子窗口页面内部触发关闭,必须提前把 Window 实例存入 AppStorage 或通过其他方式传递。这一点比 Dialog 的 close() 回调麻烦不少。

setUIContent 调用前后的页面状态

如果多次对同一个 Window 实例调用 setUIContent,行为上会替换当前加载的页面。在子窗口生命周期内需要切换内容时这是可以用的,但要注意旧页面的状态会被清除。

Overlay 与子窗口混用

这里很容易出现一个理解偏差:把 ArkUI 的 overlay 属性和 Window 模块的子窗口混为一谈。前者是 ArkUI 组件属性,设置后组件渲染在当前窗口层级最顶部;后者是系统级的独立窗口。两者不能互相替代,也不是同一套 API。如果只是做普通的页面内浮层,overlay 属性或 CustomDialog 已经足够,没有必要引入子窗口。

七、排查思路

如果子窗口没有按预期显示,建议按这个顺序检查:

  1. 确认 API Level:工程的 build-profile.json5 中 compileSdkVersion 和 compatibleSdkVersion 是否满足 API 9 及以上。

  2. 确认 Stage 模型:module.json5 中 "type" 字段是否为 "stage",FA 模型无法使用 WindowStage。

  3. 确认 WindowStage 不为空:createSubWindow 调用时 windowStage 引用是否已经被赋值,是否是在 onWindowStageCreate 之后使用。

  4. 确认页面路径注册:SubWindowPage 是否在 main_pages.json 的 src 数组里声明。

  5. 确认调用顺序:是否按照 createSubWindow → setUIContent → showWindow 的顺序调用,且每一步都等待 Promise resolve 后再进行下一步。

  6. 确认尺寸不为零:如果 resize 传入了错误的 px 值(例如误用了 vp 值导致窗口非常小),子窗口可能显示了但不可见。

开发经验总结

  • 子窗口不是 Dialog 的高级版,它的定位是"独立渲染上下文",适合需要跨 UIAbility 层级或完全独立交互面板的场景。普通弹层用 Overlay 或 CustomDialog 更简单、更可控。

  • WindowStage 引用只能在 onWindowStageCreate 中获取,记得保存下来,后续创建子窗口都依赖它。

  • setUIContent 的页面路径必须先在 main_pages.json 中注册,这一步很容易遗漏。

  • moveWindowTo 和 resize 的参数单位是物理像素(px),多分辨率适配时需要换算。

  • 子窗口需要手动销毁,建议在 UIAbility 的 onWindowStageDestroy 里做兜底清理。

如果你正在考虑用子窗口实现某个弹层效果,可以先问自己一个问题:这个 UI 是否真的需要脱离当前窗口的渲染树独立存在?如果答案是否定的,Overlay 几乎总是更合适的选择。

❤️ 如果本文帮到了你…

  • 请点个赞,让我知道你还在坚持阅读技术长文!
  • 请收藏本文,因为你以后一定还会用上!
  • 如果你在学习过程中遇到bug,请留言,我帮你踩坑!
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐