《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.etsimport{UIAbility,AbilityConstant,Want,window}from'@kit.AbilityKit';import{BusinessError}from'@kit.BasicServicesKit';exportdefaultclassEntryAbilityextendsUIAbility{privatemainWindow:window.Window|null=null;onWindowStageCreate(windowStage:window.WindowStage):void{// 获取屏幕显示区域信息constdisplay=windowStage.getMainWindowSync()?.getWindowProperties();if(!display){// 如果取不到屏幕信息,走默认流程windowStage.loadContent('pages/Index',(err)=>{});return;}// 计算窗口大小:取屏幕宽高的一半constscreenWidth=display.windowRect.width;constscreenHeight=display.windowRect.height;consttargetWidth=Math.floor(screenWidth*0.5);consttargetHeight=Math.floor(screenHeight*0.5);// 计算居中位置constposX=Math.floor((screenWidth-targetWidth)/2);constposY=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)的点击事件中privatefullScreenSwitch():void{if(!this.mainWindow){return;}// 获取当前窗口属性判断当前是全屏还是普通constprops=this.mainWindow.getWindowProperties();constisFullScreen=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)监听窗口显示事件,在确认显示后再加载内容。
constSHOW_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@Componentstruct 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()的详细信息,许多坑都能从属性值中找到线索。
