鸿蒙百篇系列第 65 篇 · 高级
大多数鸿蒙开发者一辈子只写过 entry/pages/Index.ets,对窗口的理解就是"一个全屏界面"。但当你想做悬浮歌词、自由窗口的小窗模式、聊天应用的多开、企业级多任务,或者车机/平板上的分屏体验时,WindowStage + Window + SubWindow 这一套机制就必须搞清楚。这一篇从 WindowStage 的生命周期开始,到悬浮窗的工程约束结束,把窗口这个被低估的子系统讲透。
一、UIAbility、WindowStage、Window 三层关系
Stage 模型下,每个 UIAbility 都对应一个 WindowStage,每个 WindowStage 上挂 1 + N 个 Window:
UIAbility
└── WindowStage
├── MainWindow (有且仅有一个)
└── SubWindow ... (子窗口,可多个)
├── Popup
├── Custom Dialog
└── 全局悬浮窗 (FloatWindow, 受权限管控)
WindowStage 跟 UIAbility 一样有生命周期:
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// 主窗口创建
windowStage.getMainWindow((err, mainWindow) => {
// ...
});
// 加载主页面
windowStage.loadContent('pages/Index', (err) => { /* ... */ });
}
onWindowStageDestroy(): void {
// 资源回收
}
onWindowStageWillDestroy(): void {
// 确认能销毁前的最后机会
}
}
W3C 的 Window 事件名跟传统桌面有点不一样。Stage 模型下监听到的事件是这些:
订阅方式(API 20 之前/之后有区别):
windowStage.on('windowStageEvent', (event: window.WindowStageEventType) => {
switch (event) {
case window.WindowStageEventType.PAUSED:
this.pauseAudio();
break;
case window.WindowStageEventType.RESUMED:
this.resumeAudio();
break;
case window.WindowStageEventType.HIDDEN:
this.flushState();
break;
}
});
二、主窗口的常用属性
主窗口是用户最先看到的,所有"应用对系统"的属性都在主窗口上设置:
const mainWindow = await windowStage.getMainWindow();
await mainWindow.setWindowBackgroundColor('#FFFFFF'); // 窗口未渲染时的占位色
await mainWindow.setWindowBrightness(0.5); // 亮度覆盖 0.0-1.0
await mainWindow.setWindowTouchable(true);
await mainWindow.setWindowKeepScreenOn(true); // 播放视频时常用
await mainWindow.setResizeByScrollEnabled(true); // 自由窗口模式
await mainWindow.setWindowSystemBarEnable(['status', 'navigation']); // 显示系统栏
主窗口最常用的就是"沉浸式 + 系统栏样式"。沉浸式代码:
// 方式一:setWindowSystemBarEnable 隐藏
await mainWindow.setWindowSystemBarEnable([]);
// 方式二:expand 区域
await mainWindow.setWindowLayoutArea({
topRect: { start: 0, end: 80 }, // 状态栏高度
bottomRect: { start: 2200, end: 2360 } // 导航栏高度
});
沉浸式一定要小心 —— 用户拿不到状态栏反而会迷路。我的经验是"沉浸式 + 提供呼出按钮"。
三、子窗口:弹窗、菜单、自定义控件
子窗口的常见用法:
async function showPlayerWidget() {
const subWindow = await windowStage.createSubWindow('player_widget');
await subWindow.moveWindowTo(20, 200);
await subWindow.resize(200, 60);
await subWindow.setWindowBackgroundColor('#1A1A1A');
await subWindow.setUIContent('pages/PlayerWidget');
await subWindow.showWindow();
}
为什么不直接用 @Component + 半透明层?因为:
子窗口可以独立拖拽移动
子窗口的层级由系统管理,永远浮在主窗口之上
子窗口关闭不会卸载主界面
子窗口的 z 轴位置稳定,不会被遮挡
下面是做一个"全局迷你播放器"的完整片段:
import window from '@ohos.window';
class MiniPlayerWindow {
private subWindow: window.Window | null = null;
async show(stage: window.WindowStage) {
if (this.subWindow !== null) return;
this.subWindow = await new Promise((resolve, reject) => {
stage.createSubWindow('mini_player', (err, w) => {
if (err) reject(err); else resolve(w);
});
});
await this.subWindow.moveWindowTo(20, 100);
await this.subWindow.resize(200, 60);
await this.subWindow.setWindowBackgroundColor('#CC000000');
await this.subWindow.setUIContent('pages/MiniPlayer');
await this.subWindow.showWindow();
}
async hide() {
if (this.subWindow === null) return;
await this.subWindow.destroyWindow();
this.subWindow = null;
}
}
子窗口销毁不能忘。每开一个 createSubWindow 都得对应一个 destroyWindow,否则累积的子窗口会让系统变卡。实战中子窗口管理要写到 class 里,把"打开/关闭"配成对的方法。
四、悬浮窗:受限开放的能力
悬浮窗(FloatWindow)跟子窗口的关键差别是"应用退到后台时仍然可见"。常见场景:
音乐 App 的"桌面歌词"
计时器、时钟的"常驻浮窗"
录屏工具的"操作录屏窗口"
悬浮窗需要权限:
// module.json5
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.SYSTEM_FLOAT_WINDOW",
"reason": "用于在桌面显示常驻播放控件",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "always"
}
}
]
}
}
注意 ohos.permission.SYSTEM_FLOAT_WINDOW 不是普通权限,AGC 上需要走 ACL 申请(普通应用不一定能过)。普通开发需求可以用 setWindowSystemBarEnable 配合子窗口做"伪悬浮",但应用退出后台后会被回收。
代码层面:
// API 12+ 推荐接口
const floatWin = await window.createWindow({
name: 'float_clock',
windowType: window.WindowType.TYPE_FLOAT,
ctx: this.context
});
await floatWin.moveWindowTo(20, 100);
await floatWin.resize(120, 60);
await floatWin.setUIContent('pages/FloatClock');
await floatWin.showWindow();
floatWin.on('touchOutside', () => {
floatWin.hideWindow();
});
API 12 之前的代码用 window.create() + WindowType.FLOAT,逻辑一致,但 create 的方式略有不同。
五、自由窗口:PC、平板、折叠屏
折叠屏、平板、PC 设备的窗口通常是"自由窗口"——可以拖拽、缩放。Stage 模型默认会跑全屏,但 module.json5 里可以打开:
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"supportWindowMode": ["split", "free"]
}
]
}
}
三种窗口模式对比:
代码侧其实不需要特别处理 —— 系统会把当前 window 状态广播给应用。但布局上一定要做"响应式":
@StorageLink('windowSize') windowSize: Size = { width: 0, height: 0 };
aboutToAppear() {
window.getLastWindow(getContext(this)).then(win => {
win.on('windowSizeChange', size => {
this.windowSize = size;
});
});
}
六、跨设备窗口:分布式流转
如果你的应用支持"接续"或"跨设备迁移",窗口会"跟着"任务迁移。这是 Stage 模型下 onContinue / onNewWant 配合 Want 的能力:
onContinue(want: Want, options): AbilityConstant.ContinueState {
// 把当前页面序列化到 want.parameters
want.parameters = {
page: 'pages/DetailPage',
bundle: 'com.example.app',
deepLink: '/article/123'
};
return AbilityConstant.ContinueState.AGREE;
}
接收方在新设备的 WindowStage 上恢复:
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
const page = want.parameters?.['page'] as string;
// 加载新页面
if (page) {
this.windowStage.loadContent(page, () => {});
}
}
这是分布式流转最简版。真正的"任务迁移"还会包含 UI 状态、网络会话、媒体播放位置,需要额外的 Session Kit。
七、几个坑
子窗口生命周期 ≠ 主窗口。主窗口销毁时子窗口会被强制销毁,但你主动销毁主窗口,子窗口未必自动销毁。要在
onWindowStageDestroy里清理悬浮窗权限的申请时点。不能在 UI 渲染完之后申请,会被拒。应该按"需求驱动"原则,要用的时候再申请
setUIContent是异步的。多次调用之间的状态不保证,"先 resize 再 setUIContent 再 show"的顺序对,但中间出错就难调back 键和子窗口。未指定路由的情况下,按 back 关闭最后一个显示的子窗口,否则返回桌面
自由窗口的 Size 是物理像素。做自适应布局时记得除以
vp系数
八、性能与调试建议
频繁创建/销毁子窗口是高开销。复用子窗口 + 切内容更划算
setResizeByScrollEnabled打开后要监听resize事件以重排布局,否则会出现"留白"调试时打开 DevEco Studio 的 Window Inspector,可以看到真实窗口树,比 logcat 直观
九、和前几篇的衔接
第 4 篇讲的页面路由是"在主窗口里导航"。这一篇讲"在主窗口外开辟新窗口"
第 9 篇分布式里提到"跨设备迁移",本篇是用窗口 API 接的实现
第 60 篇里提到的 H5 容器,弹窗逻辑也是建在子窗口上的
十、小结
窗口子系统是鸿蒙里被低估的能力。从主窗口到子窗口,再到悬浮窗、自由窗口、跨设备流转,每一层都有自己的适用场景。理解 WindowStage 生命周期、子窗口管理、悬浮窗权限边界,可以让你在做大屏、Pad、PC 应用时少走很多弯路。
下一篇进入图形栈与 Surface,把第 61 篇 RenderNode 之后那一层——XComponent 与 EGL 自绘——讲清楚。