
窗口管理到底是什么
HarmonyOS NEXT开发中,窗口管理是一个容易被忽略但影响面很广的话题。很多初学者在搭建第一个页面时,直接在@Entry装饰的组件里写UI,确实能跑,但对"窗口"这个概念没有清晰认知。
等到了需要做多任务管理、悬浮窗口、分屏适配、应用间交互时,问题就来了------窗口怎么创建?生命周期怎么管控?为什么有时候页面显示不出来?这些问题都指向同一个核心概念:窗口管理。
简单说,窗口是界面显示的容器。你在手机上看到的所有内容,不管是应用主界面、弹窗还是悬浮球,最终都被绘制在某个窗口上。HarmonyOS的窗口系统不是简单的视图层次,它有明确的分层结构和生命周期管理机制,WindowStage就是这个机制的入口。
窗口系统的分层结构
HarmonyOS的窗口系统分三层:
| 层级 | 作用 | 常见场景 |
|---|---|---|
| 应用窗口 | 承载应用主界面 | 首页、详情页 |
| 系统窗口 | 系统级UI元素 | 状态栏、导航栏 |
| 悬浮窗口 | 漂浮在其他窗口之上 | 悬浮球、Toast提示 |
实际开发中,开发者主要操作的是应用窗口 和悬浮窗口。应用窗口又分为主窗口和子窗口,主窗口对应应用的主界面,子窗口可以用于模态弹窗等场景。
窗口的分层顺序决定了渲染叠放关系。上层窗口会覆盖下层窗口,这个顺序由窗口类型和创建顺序共同决定。理解这一点,才能正确管理悬浮窗口和弹窗的显示层级。
WindowStage的核心角色
WindowStage是窗口生命周期的管理者。每个UIAbility实例都对应一个WindowStage对象。它的生命周期和UIAbility紧密绑定,主要负责三件事:
- 创建和绑定主窗口
- 配置窗口属性(大小、位置、可触摸性等)
- 管理窗口生命周期(创建、显示、隐藏、销毁)
真正的开发中,90%的窗口操作都是在WindowStage的回调中完成的。官方文档提到onWindowStageCreate回调,但这个回调的触发时机和限制条件,不少人都理解得不够准确。
获取WindowStage并创建主窗口
在UIAbility中获取WindowStage的方式很直接。UIAbility的onCreate方法会传入WindowStage实例:
typescript
// Ability.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import Window from '@ohos.window';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: Window.WindowStage): void {
console.info('WindowStage创建成功');
// 加载主页面
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
console.error('页面加载失败,错误码: ' + err.code);
return;
}
console.info('页面加载成功');
});
}
onWindowStageDestroy(): void {
console.info('WindowStage销毁');
}
}
这段代码的核心是loadContent方法。它把指定的页面文件加载到主窗口中。注意这里有个常见误区:loadContent是异步操作,不能假设加载完成后立即可以获取窗口实例。
等页面加载完成,可以通过getMainWindow获取窗口实例,然后对窗口进行属性配置:
typescript
// Ability.ts
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: Window.WindowStage): void {
windowStage.loadContent('pages/Index', async () => {
try {
// 获取主窗口实例
let mainWindow = await windowStage.getMainWindow();
// 设置窗口属性
await mainWindow.setWindowLayoutFullScreen(true); // 全屏布局
mainWindow.setWindowBackgroundColor('#FFFFFF'); // 设置背景色
// 获得窗口的宽高
let properties = await mainWindow.getWindowProperties();
console.info('窗口宽度: ' + properties.windowRect.width);
} catch (error) {
console.error('窗口操作失败: ' + error.code);
}
});
}
}
getMainWindow返回的是Window实例,通过它可以控制窗口的显示属性、事件监听等。注意点 :这个方法必须在loadContent之后调用,否则返回的窗口可能还没有完全初始化。
窗口生命周期的实际行为
窗口生命周期并非只有'onWindowStageCreate'和'onWindowStageDestroy'两个节点。实际运行中,还有一些容易被忽略的状态变化:
窗口可见性变化 :应用切换到后台时,窗口并没有销毁,只是变为不可见。这时候onWindowStageCreate不会再次触发。官方文档中没有单独说明窗口可见性变化的回调,需要开发者通过window.on('windowVisibility')监听:
typescript
async function initVisibilityListener(windowStage: Window.WindowStage) {
try {
let mainWindow = await windowStage.getMainWindow();
mainWindow.on('windowVisibility', (data) => {
console.info('窗口可见性变化: ' + data.visible);
});
} catch (error) {
console.error('设置可见性监听失败');
}
}
窗口大小变化:分屏、旋转屏幕都会触发窗口大小变化。如果不监听窗口大小变化事件,界面布局可能不会自适应:
typescript
mainWindow.on('windowSizeChange', (data) => {
console.info('窗口大小变化为: ' + data.width + 'x' + data.height);
// 通知UI组件更新布局
});
这两个事件是窗口管理中最常用的监听回调,但很多初学者只关注了创建和销毁,忽略了这两个。
常见问题
问题1:onWindowStageCreate为什么只执行一次?
现象 :应用切换到后台再切回来,onWindowStageCreate不再触发,导致部分初始化逻辑没执行。
原因 :onWindowStageCreate只在WindowStage首次创建时执行。应用进入后台时,WindowStage处于ACTIVE状态但不可见,切换到前台时不会重新创建。这符合正常的生命周期设计,但容易被误以为需要重新初始化。
解决方案 :把需要重复执行的逻辑放在页面的onPageShow回调中,或者监听窗口可见性变化事件:
typescript
@Entry
@Component
struct Index {
onPageShow(): void {
console.info('页面显示,执行初始化');
// 重新加载数据或刷新UI
}
}
问题2:窗口属性设置不及时导致页面显示异常
现象:某些设备上,页面加载后窗口大小或全屏状态没有立即生效,出现短暂的白边或错位。
原因 :setWindowLayoutFullScreen等属性设置是异步操作,可能在页面渲染完成后才生效。如果在loadContent的回调中立即设置窗口属性,由于窗口尚未完全就绪,属性设置可能被忽略。
解决方案 :在loadContent的回调中,使用await等待窗口属性设置完成:
typescript
windowStage.loadContent('pages/Index', async () => {
let mainWindow = await windowStage.getMainWindow();
// 确保窗口准备就绪
await mainWindow.setWindowLayoutFullScreen(true);
// 再执行其他操作
});
最佳实践
-
不要在
build()中频繁创建窗口句柄。窗口实例是重量级对象,应该在UIAbility中保存引用,避免反复获取。 -
窗口事件监听务必在
onWindowStageDestroy中取消。否则可能导致内存泄漏或异常回调:
typescript
onWindowStageDestroy(windowStage: Window.WindowStage): void {
try {
let mainWindow = await windowStage.getMainWindow();
mainWindow.off('windowVisibility');
mainWindow.off('windowSizeChange');
} catch (error) {
console.error('取消监听失败');
}
}
- 将窗口配置逻辑封装到独立的类中。随着项目变大,窗口操作会涉及多处逻辑,集中管理更容易维护。推荐模式:
typescript
// WindowManager.ts
export class WindowManager {
private mainWindow: Window.Window | null = null;
async init(windowStage: Window.WindowStage): void {
this.mainWindow = await windowStage.getMainWindow();
// 统一配置窗口
}
setFullScreen(): void {
this.mainWindow?.setWindowLayoutFullScreen(true);
}
}
Demo入口
下面是完整的页面入口示例,集成了窗口配置和事件监听:
typescript
// pages/Index.ets
import Window from '@ohos.window';
@Entry
@Component
struct Index {
@State windowWidth: number = 0;
@State windowHeight: number = 0;
build() {
Column() {
Text('窗口宽度: ' + this.windowWidth)
Text('窗口高度: ' + this.windowHeight)
Button('切换全屏')
.onClick(() => {
this.toggleFullScreen();
})
}
.width('100%')
.height('100%')
.onPageShow(() => {
this.getWindowInfo();
})
}
private async getWindowInfo(): void {
let context = getContext(this) as any;
let mainWindow = await context.windowStage.getMainWindow();
let properties = await mainWindow.getWindowProperties();
this.windowWidth = properties.windowRect.width;
this.windowHeight = properties.windowRect.height;
}
private async toggleFullScreen(): void {
let context = getContext(this) as any;
let mainWindow = await context.windowStage.getMainWindow();
let currentState = await mainWindow.isWindowLayoutFullScreen();
await mainWindow.setWindowLayoutFullScreen(!currentState);
}
}
FAQ
Q:为什么真机上窗口全屏设置正常,模拟器上不生效?
A:模拟器对窗口属性的支持有限,某些属性(如全屏、沉浸式)在模拟器上会忽略。建议以真机测试为准。如果你只在模拟器上验证全屏功能,这个功能可能永远无法通过测试。
Q:创建悬浮窗口时,为什么不能通过getMainWindow获取?
A:悬浮窗口和主窗口是独立的窗口实例。getMainWindow只返回当前WindowStage关联的主窗口。创建悬浮窗口需要使用createSubWindow方法,并通过windowClass参数指定窗口类型为WindowType.TYPE_FLOAT。
Q:页面返回后,之前设置的窗口属性会丢失吗?
A:不会。窗口属性是持久化的,只要WindowStage没有销毁,属性会一直保留。如果页面返回后发现窗口属性不对,多半是页面逻辑中重新设置了属性,而不是属性丢失。排查时可以检查onPageShow或构造函数中是否存在重置操作。
示例代码地址:项目地址