《HarmonyOS技术精讲-窗口管理》第二篇:创建与控制主窗口
《HarmonyOS技术精讲-窗口管理》第二篇:创建与控制主窗口

1. 开篇引导
在HarmonyOS开发中,窗口管理是一个比较基础但容易踩坑的模块。很多人在刚接触window.createWindow和window.createSubWindow时,会误以为它们功能类似,结果在真机上一跑就崩。
本篇文章专注于应用主窗口的创建与基础属性控制,包括窗口大小、位置、全屏模式。理解这些API的边界条件,比你背下参数列表更有用。
2. 基本概念与场景
什么场景需要手动管理窗口?
默认情况下,UIAbility会自动为应用创建主窗口。但以下场景需要你显式操作:
- 自定义启动窗口大小:应用首次启动时,需要以指定非全屏尺寸显示(如悬浮工具类应用)
- 动态切换窗口布局:从竖屏切到横屏,或普通窗口切到全屏
- 多窗口协同:同时展示多个子窗口(在后续文章细讲)
createWindow 和 createSubWindow 区别
| 特性 | createWindow |
createSubWindow |
|---|---|---|
| 用途 | 创建应用主窗口 | 创建依附于主窗口的子窗口 |
| 生命周期 | 独立于UIAbility生命周期 | 跟随主窗口销毁而销毁 |
是否必须设置context |
必须传递UIAbilityContext |
必须传递UIAbilityContext |
| 典型场景 | 应用主界面 | 弹窗、悬浮小窗 |
关键点:createWindow创建的是独立的“窗口实体”,可以独立控制大小、位置、显示状态。createSubWindow则无法独立存在,且其宿主必须是已存在的主窗口。
3. 环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机 / 平板
4. 核心实现
4.1 基础结构
在开始操作窗口前,先确保module.json5中配置了window权限(其实主窗口操作不需要额外权限,但建议检查"supportedWindowModes"字段):
{
"module": {
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"launchType": "singleton",
"supportedWindowModes": ["fullscreen", "floating"]
}
]
}
}
4.2 在UIAbility中显式创建主窗口
为什么需要显式创建? 默认的主窗口会在onWindowStageCreate回调中传递过来,但如果你需要自定义窗口尺寸,必须在回调里重新创建并替换它。
下面这段代码实现了:应用启动后创建一个宽高为屏幕一半、居中显示的窗口。
// EntryAbility.ets
import { UIAbility, AbilityConstant, Want, window } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
private mainWindow: window.Window | null = null;
onWindowStageCreate(windowStage: window.WindowStage): void {
// 获取屏幕显示区域信息
const display = windowStage.getMainWindowSync()?.getWindowProperties();
if (!display) {
// 如果取不到屏幕信息,走默认流程
windowStage.loadContent('pages/Index', (err) => {});
return;
}
// 计算窗口大小:取屏幕宽高的一半
const screenWidth = display.windowRect.width;
const screenHeight = display.windowRect.height;
const targetWidth = Math.floor(screenWidth * 0.5);
const targetHeight = Math.floor(screenHeight * 0.5);
// 计算居中位置
const posX = Math.floor((screenWidth - targetWidth) / 2);
const posY = Math.floor((screenHeight - targetHeight) / 2);
// 显式创建主窗口(注意:这里要销毁默认窗口然后自己创建)
windowStage.createWindow({
name: "main",
windowType: window.WindowType.TYPE_APP,
ctx: this.context,
width: targetWidth,
height: targetHeight
}).then((win: window.Window) => {
this.mainWindow = win;
// 移动窗口到居中位置
win.moveWindowTo(posX, posY).then(() => {
console.info("moveWindowTo success");
}).catch((err: BusinessError) => {
console.error(`moveWindowTo failed: ${JSON.stringify(err)}`);
});
// 设置窗口的背景色为纯白
win.setWindowBackgroundColor("#FFFFFF");
// 加载内容
win.loadContent('pages/Index', (err) => {
if (err) {
console.error(`loadContent failed: ${JSON.stringify(err)}`);
} else {
console.info("loadContent success");
}
});
// 显示窗口
win.showWindow().catch((err: BusinessError) => {
console.error(`showWindow failed: ${JSON.stringify(err)}`);
});
}).catch((err: BusinessError) => {
console.error(`createWindow failed: ${JSON.stringify(err)}`);
});
// 注意:默认的windowStage上的MainWindow已被我们创建的取代
// 之后不要再调用windowStage.getMainWindowSync()
}
onWindowStageDestroy(): void {
if (this.mainWindow) {
this.mainWindow.destroyWindow();
this.mainWindow = null;
}
}
}
注意事项:
- 先销毁默认窗口:
windowStage.getMainWindowSync()获取的默认窗口应在显式创建前关闭?这里官方文档没有明确要求,但经验表明,在createWindow之前调用destroyWindow销毁默认窗口可以避免资源冲突。 - 尺寸单位是vp:
width和height的单位是vp(虚拟像素),而非px。 - 位置坐标:
moveWindowTo的坐标原点在屏幕左上角(包含状态栏区域),如果你的应用需要避开状态栏,需要手动计算偏移。
4.3 动态切换到全屏
在主窗口已经半屏显示后,用户点击按钮切换到全屏。这里使用setWindowLayoutMode:
// 在某个组件(如Button)的点击事件中
private fullScreenSwitch(): void {
if (!this.mainWindow) {
return;
}
// 获取当前窗口属性判断当前是全屏还是普通
const props = this.mainWindow.getWindowProperties();
const isFullScreen =
props.windowLayoutMode === window.WindowLayoutMode.WINDOW_LAYOUT_MODE_FULLSCREEN;
if (isFullScreen) {
// 退出全屏:回到之前半屏尺寸
this.mainWindow?.resetSize(360, 640);
this.mainWindow?.moveWindowTo(0, 0);
} else {
// 进入全屏
this.mainWindow?.setWindowLayoutMode(
window.WindowLayoutMode.WINDOW_LAYOUT_MODE_FULLSCREEN
).then(() => {
console.info("set fullscreen mode success");
}).catch((err: BusinessError) => {
console.error(`set fullscreen failed: ${JSON.stringify(err)}`);
});
}
}
注意:setWindowLayoutMode会让窗口充满显示区域(包括状态栏区域),但状态栏的显示与否需要配合setWindowLayoutFullScreen一起使用。如果你只希望状态栏保留但窗口填满,用setWindowLayoutMode即可。
5. 踩坑记录
坑1:窗口大小单位理解错误导致的UI异形
现象:在1080*2400分辨率的手机上,设置width: 540, height: 1200,期望是半屏大小,结果窗口比预期小一圈。
原因:createWindow的width和height是vp单位,而非px。对1080px宽的屏幕,默认density为3,实际1vp=3px。所以540vp = 1620px,超出屏幕宽度,导致窗口自动缩放填充。
解法:先通过getWindowProperties().windowRect获取屏幕实际vp尺寸。示例中的Math.floor(screenWidth * 0.5)正是基于vp计算。
坑2:UIAbility与窗口生命周期同步问题
现象:在某些设备版本(如HarmonyOS 4.0以前),win.showWindow()在onWindowStageCreate回调中调用后,页面内容不可见。
原因:showWindow()默认是异步的,但UIAbility的生命周期状态变化可能先于窗口显示完成。当UIAbility状态进入FOREGROUND时,窗口还未完全绘制。
解法:在win.showWindow()的then回调中再执行loadContent,或者使用win.on('windowEvent', callback)监听窗口显示事件,在确认显示后再加载内容。
const SHOW_EVENT = 'windowSizeChange';
win.on(SHOW_EVENT, () => {
win.loadContent('pages/Index');
});
win.showWindow();
6. 最佳实践
-
永远不要让主窗口操作在
onWindowStageCreate之外执行:虽然可以到处拿到windowStage,但在onWindowStageCreate回调之后,窗口资源可能已被回收。始终在UIAbility中持有mainWindow引用。 -
使用
resetSize代替setWindowSize:setWindowSize在某些API版本会导致窗口闪烁。resetSize更稳定,且支持vp单位。 -
全屏切换时保存窗口位置:切换到全屏时,用局部变量保存
posX, posY, width, height,全屏退出后恢复。避免丢失用户调好的窗口位置。
7. FAQ
Q:为什么真机上窗口创建的尺寸和模拟器不一致?
A:模拟器通常使用固定的物理分辨率(如1920*1080),且默认的density为2。真机的density可能为2.5或3。建议在真机上调试窗口尺寸逻辑,并在@Entry组件中通过getContext().window.getWindowProperties()打印实际vp值。
Q:createWindow后必须showWindow吗?
A:是的。createWindow只是创建了一个窗口对象,但未显示。不调用showWindow,窗口永远不可见。另外,在onWindowStageCreate中调用createWindow后,记得destroyWindow默认窗口,否则系统会报资源冲突。
Q:setWindowLayoutMode(FULLSCREEN)后,状态栏消失怎么办?
A:setWindowLayoutMode(FULLSCREEN)会使窗口填满整个显示区域,包括状态栏。如果需要保留状态栏,请使用setWindowLayoutMode(NON_FULLSCREEN)或单独设置setWindowLayoutFullScreen(false)。
8. Demo入口
// pages/Index.ets
@Entry
@Component
struct Index {
// 通过@LocalStorage或全局状态获取mainWindow引用
@StorageLink('mainWindow') mainWindow: window.Window = undefined;
build() {
Column() {
Button('切换全屏')
.onClick(() => {
// 调用之前定义的fullScreenSwitch方法
// 这里通过回调或事件总线触发
})
}
.height('100%')
.width('100%')
}
}
需要注意的是,在UIAbility中通过AppStorage.setOrCreate('mainWindow', win)将窗口引用传递给组件层,避免在组件中直接getContext()获取UIAbility上下文,防止内存泄漏。
示例代码地址:项目地址
如果你在实践过程中遇到其他问题,建议先打印getWindowProperties()的详细信息,许多坑都能从属性值中找到线索。
更多推荐


所有评论(0)