
从普通子窗迁移到 HarmonyOS 7 独立子窗打开顺序、失败回收和后台行为在电脑上的编辑工具里把检查面板拆成一个辅助窗口方便一边看主页面一边对照信息。打开 zLevelAboveParentLoosened 后主窗切到后台面板却没有一起消失。另一处问题更隐蔽窗口创建成功就立即 showWindow页面还没加载完成看起来像“窗口接口没有生效”。这两个现象不应混着修。前者涉及独立子窗在自由窗口状态下的生命周期后者是应用初始化顺序。下面分别处理正常打开的检查面板以及加载失败后的窗口回收。只讨论应用自己的子窗不用它冒充画中画或系统全局悬浮窗。版本与验证SubWindowOptions.zLevelAboveParentLoosened 起始版本为26.0.0子窗口开发指南更新于2026-09-09本文于2026-09-27核对参数、页面加载、显示和销毁接口。窗口初始化流程与两组故障测试已在本机 Node.js 运行ArkTS 适配代码未在 API 26 SDK 或真机编译运行窗口层级与自由窗口模式的表现仍需设备端验收。“独立”具体放开了什么创建时把 SubWindowOptions.zLevelAboveParentLoosened 设为 true得到独立子窗。它的默认值是 false不是所有子窗口都自动具备这项能力。官方指南限定了一个重要环境在自由窗口状态下独立子窗不跟随主窗前后台切换只跟随主窗一起销毁主窗与独立子窗可以通过点击调整层级。这不是永远置顶也不是脱离主窗长期存在。场景应怎样理解普通子窗改为独立子窗重新审查原先依赖主窗前后台联动的行为主窗切后台自由窗口状态下不能再假定子窗自动隐藏主窗销毁独立子窗仍跟随销毁不是另一个独立应用想在任意应用之上显示内容应研究对应窗口类型不能拿独立子窗参数替代普通手机全屏模式不要直接套用自由窗口状态的层级结论非自由窗口状态下子窗只在主窗口范围显示自由窗口状态下子窗可以超出主窗口范围。跨设备适配时要记录设备形态和当前窗口模式而不是只写“在电脑上测过”。案例一检查面板先准备完整再显示建议把打开过程看成有顺序的操作创建 → 设置尺寸 → 加载内容 → 显示。官方提醒子窗未完成页面加载就调用 showWindow可能处于前台却不可见。没有设置大小也可能产生意外自由窗口状态下默认大小可能是当前物理屏幕大小而不是想象中的小面板。先把这个流程与具体窗口 API 分离便于验证顺序以及故障时是否回收资源。下面代码放在 PanelFlow.ets。onCleanupFailure 必须是不会抛异常的本地记录函数负责保留待处理窗口不在这里进行无限重试。export interface PanelPort { configure(): Promisevoid; load(): Promisevoid; show(): Promisevoid; destroy(): Promisevoid; } export async function preparePanel( create: () PromisePanelPort, onCleanupFailure: (panel: PanelPort) void ): PromisePanelPort { const panel await create(); try { await panel.configure(); await panel.load(); await panel.show(); return panel; } catch (setupError) { try { await panel.destroy(); } catch { onCleanupFailure(panel); } throw setupError; } }创建本身失败时没有窗口可以回收因此 create 放在 try 之前。创建成功后的配置、加载和显示失败才进入销毁分支。原始失败仍然向调用方传播不能回收之后就返回“打开成功”。接下来用真实的窗口接口适配这个流程。configure 采用官方的 resize 回调形式load/show/destroy 采用已提供的 Promise 形式。参数800×500只是演示值应结合目标设备窗口限制调整不是通用推荐尺寸。import { window } from kit.ArkUI; import { PanelPort, preparePanel } from ./PanelFlow; class NativePanel implements PanelPort { private nativeWindow: window.Window; constructor(nativeWindow: window.Window) { this.nativeWindow nativeWindow; } configure(): Promisevoid { return new Promisevoid((resolve, reject) { this.nativeWindow.resize(800, 500, (error) { if (error?.code) reject(new Error(resize failed: error.code)); else resolve(); }); }); } load(): Promisevoid { return this.nativeWindow.setUIContent(pages/InspectPanel); } show(): Promisevoid { return this.nativeWindow.showWindow(); } destroy(): Promisevoid { return this.nativeWindow.destroyWindow(); } } export async function openInspectPanel( stage: window.WindowStage, uniqueName: string, cleanupQueue: PanelPort[] ): PromisePanelPort { return preparePanel(async () { const options: window.SubWindowOptions { title: 检查面板, decorEnabled: true, zLevelAboveParentLoosened: true }; const created await stage.createSubWindowWithOptions(uniqueName, options); if (created undefined) throw new Error(window was not created); return new NativePanel(created); }, (panel: PanelPort) { cleanupQueue.push(panel); }); }调用方持有返回的 PanelPort在面板不再需要时调用 destroy并在成功后移除自己的引用。WindowStage 应来自当前主窗口的生命周期例如 UIAbility.onWindowStageCreate 的参数不要从另一个窗口或者已销毁的实例里借一个旧引用。pages/InspectPanel 也必须存在并在工程 main_pages.json 中登记路径与 src 项一致不能写相对文件路径。下面是这个页面的最小内容便于区分“窗口没显示”与“内容路径错误”Entry Component struct InspectPanel { build() { Column({ space: 16 }) { Text(检查面板).fontSize(24) Text(先确认此页面加载成功再测试窗口模式和层级。) }.padding(24).width(100%).height(100%) } }如果打开按钮可能被连续点击调用方还要持有一次正在打开的任务在任务结束前复用或拒绝第二次点击。不要用相同窗口名反复创建来“试到成功”命名、已打开状态和当前任务应由同一个窗口所有者维护。本文的 preparePanel 只管一次初始化不负责全局去重。用测试锁住异步顺序以下测试直接接在 PanelFlow 代码后运行不需要重写一份流程。FakePanel 是故障注入替身事件数组记录实际调用顺序它不创建操作系统窗口。class FakePanel implements PanelPort { events: string[] []; failAt: string ; cleanupFails: boolean false; private async step(name: string): Promisevoid { this.events.push(name); if (this.failAt name) throw new Error(name); } configure(): Promisevoid { return this.step(configure); } load(): Promisevoid { return this.step(load); } show(): Promisevoid { return this.step(show); } async destroy(): Promisevoid { this.events.push(destroy); if (this.cleanupFails) throw new Error(destroy); } } function expect(value: boolean, message: string): void { if (!value) throw new Error(message); } const ready new FakePanel(); const cleanup: PanelPort[] []; const opened await preparePanel(async () ready, p cleanup.push(p)); expect(opened ready, returns the created panel); expect(ready.events.join(,) configure,load,show, ordered setup); expect(cleanup.length 0, no cleanup needed on success); await opened.destroy(); expect(ready.events[3] destroy, owner can close the opened panel); console.log(open case passed);案例二页面加载失败不留下一个无内容窗口把目标页面路径故意写错是一个可控的设备端故障注入方式。预期不是“继续 show 看看”而是收到加载失败后回收刚创建的窗口同时保留错误用于定位。在宿主测试中不依赖特定设备错误码直接让 load 拒绝。再增加一条销毁也失败的路径此时保留引用到待回收队列而不是丢掉引用后声称已清理。const broken new FakePanel(); broken.failAt load; let rejected false; try { await preparePanel(async () broken, p cleanup.push(p)); } catch { rejected true; } expect(rejected, load failure must reach caller); expect(broken.events.join(,) configure,load,destroy, never show after load failure); const stuck new FakePanel(); stuck.failAt show; stuck.cleanupFails true; rejected false; try { await preparePanel(async () stuck, p cleanup.push(p)); } catch { rejected true; } expect(rejected, show failure still reaches caller); expect(cleanup.length 1 cleanup[0] stuck, retain exact pending resource); expect(stuck.events.join(,) configure,load,show,destroy, cleanup attempted once); console.log(rollback case passed);本机输出 open case passed、rollback case passed。它验证调用顺序、错误传播和失败回收记录不证明系统窗口已销毁。生产代码应给待回收队列增加窗口标识、失败阶段与错误码结合实际窗口生命周期处理避免盲目定时重试已经随主窗销毁的对象。destroyWindow 的状态异常也可能表示对象已经不可用不能只靠重试次数判断。从旧子窗迁移验收表要多一列窗口模式验收项自由窗口状态非自由窗口状态显示范围子窗可超出主窗范围子窗只在主窗范围显示主窗切前后台核对独立子窗不自动跟随的行为不照搬自由窗口结论按目标设备验证点击主窗/子窗观察层级调整不要求永久置顶验证普通窗口交互与遮挡关闭主窗确认子窗随之销毁同时确认没有保留失效引用加载失败不进入show执行回收或记录待回收同样验证失败流程应用自己的异步请求不会因为窗口对象销毁就自然完成业务清理。若面板还订阅日志、持有计时器或等待网络需要由所有者一并取消或使旧回调失效不要把“系统销毁了窗”当成“所有应用资源都清干净”。如果产品明确要求主窗切后台时面板一起消失就不要无理由地开启独立子窗。普通子窗的联动行为更贴合这种需求。真正需要辅助面板与主页面分别操作时再启用独立子窗并让关闭、恢复和回收语义有明确归属。这次改造保留了什么改变了什么保留的是创建、加载、显示、销毁这些窗口基础操作改变的是自由窗口下对主窗前后台联动的假设。把初始化顺序抽成 preparePanel既能复用于检查面板也能复用于其他子窗内容测试不再依赖碰巧遇到一次页面加载失败。没有必要先写一套庞大的窗口管理框架。先做到一次打开有完整结果、一次失败有明确回收、一个窗口有唯一所有者再按真实需求增加任务去重和页面会话管理。这样比只打开一个新参数然后到处补隐藏逻辑更容易维护。官方来源子窗口开发指导独立子窗行为、初始化与回收SubWindowOptionszLevelAboveParentLoosenedsetUIContent页面路径与加载结果showWindow 与 destroyWindow