Dialog 不够用时怎么办?创建第一个 HarmonyOS 子窗口【鸿蒙心迹】

你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的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 已经足够,没有必要引入子窗口。
七、排查思路
如果子窗口没有按预期显示,建议按这个顺序检查:
-
确认 API Level:工程的
build-profile.json5中compileSdkVersion和compatibleSdkVersion是否满足 API 9 及以上。 -
确认 Stage 模型:
module.json5中"type"字段是否为"stage",FA 模型无法使用WindowStage。 -
确认 WindowStage 不为空:
createSubWindow调用时windowStage引用是否已经被赋值,是否是在onWindowStageCreate之后使用。 -
确认页面路径注册:
SubWindowPage是否在main_pages.json的src数组里声明。 -
确认调用顺序:是否按照
createSubWindow→setUIContent→showWindow的顺序调用,且每一步都等待 Promise resolve 后再进行下一步。 -
确认尺寸不为零:如果
resize传入了错误的 px 值(例如误用了 vp 值导致窗口非常小),子窗口可能显示了但不可见。
开发经验总结
-
子窗口不是 Dialog 的高级版,它的定位是"独立渲染上下文",适合需要跨 UIAbility 层级或完全独立交互面板的场景。普通弹层用 Overlay 或 CustomDialog 更简单、更可控。
-
WindowStage引用只能在onWindowStageCreate中获取,记得保存下来,后续创建子窗口都依赖它。 -
setUIContent的页面路径必须先在main_pages.json中注册,这一步很容易遗漏。 -
moveWindowTo和resize的参数单位是物理像素(px),多分辨率适配时需要换算。 -
子窗口需要手动销毁,建议在 UIAbility 的
onWindowStageDestroy里做兜底清理。
如果你正在考虑用子窗口实现某个弹层效果,可以先问自己一个问题:这个 UI 是否真的需要脱离当前窗口的渲染树独立存在?如果答案是否定的,Overlay 几乎总是更合适的选择。
❤️ 如果本文帮到了你…
- 请点个赞,让我知道你还在坚持阅读技术长文!
- 请收藏本文,因为你以后一定还会用上!
- 如果你在学习过程中遇到bug,请留言,我帮你踩坑!
更多推荐



所有评论(0)