当前位置: 首页 > news >正文

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

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

1. 开篇引导

在HarmonyOS开发中,窗口管理是一个比较基础但容易踩坑的模块。很多人在刚接触window.createWindowwindow.createSubWindow时,会误以为它们功能类似,结果在真机上一跑就崩。

本篇文章专注于应用主窗口的创建与基础属性控制,包括窗口大小、位置、全屏模式。理解这些API的边界条件,比你背下参数列表更有用。

2. 基本概念与场景

什么场景需要手动管理窗口?

默认情况下,UIAbility会自动为应用创建主窗口。但以下场景需要你显式操作:

  • 自定义启动窗口大小:应用首次启动时,需要以指定非全屏尺寸显示(如悬浮工具类应用)
  • 动态切换窗口布局:从竖屏切到横屏,或普通窗口切到全屏
  • 多窗口协同:同时展示多个子窗口(在后续文章细讲)

createWindowcreateSubWindow区别

特性createWindowcreateSubWindow
用途创建应用主窗口创建依附于主窗口的子窗口
生命周期独立于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;}}}

注意事项:

  1. 先销毁默认窗口windowStage.getMainWindowSync()获取的默认窗口应在显式创建前关闭?这里官方文档没有明确要求,但经验表明,在createWindow之前调用destroyWindow销毁默认窗口可以避免资源冲突。
  2. 尺寸单位是vpwidthheight的单位是vp(虚拟像素),而非px。
  3. 位置坐标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,期望是半屏大小,结果窗口比预期小一圈。

原因createWindowwidthheightvp单位,而非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. 最佳实践

  1. 永远不要让主窗口操作在onWindowStageCreate之外执行:虽然可以到处拿到windowStage,但在onWindowStageCreate回调之后,窗口资源可能已被回收。始终在UIAbility中持有mainWindow引用。

  2. 使用resetSize代替setWindowSizesetWindowSize在某些API版本会导致窗口闪烁。resetSize更稳定,且支持vp单位。

  3. 全屏切换时保存窗口位置:切换到全屏时,用局部变量保存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()的详细信息,许多坑都能从属性值中找到线索。

http://www.jsqmd.com/news/1082637/

相关文章:

  • 3步实战指南:如何用qmc-decoder快速解锁加密音乐文件
  • 【MES】如何通俗简单地理解MES系统
  • 3秒图片格式转换终极指南:Chrome右键菜单一键保存JPG/PNG/WebP
  • okbiye 数据分析模块:告别 SPSS 操作难题,一键自动生成论文可用 DOCX 统计报告
  • JBoss高危漏洞复现与安全加固实战指南
  • 如何选择合适的嵌入式核心板产品?
  • IPXWrapper终极指南:5分钟让Windows 10/11完美运行经典IPX游戏
  • 计算机毕业设计之基于微信小程序的校园拼车系统的设计与实现
  • 终极宝可梦随机化器:Universal Pokemon Randomizer ZX完全使用指南
  • SkyJM-Gen 重磅开源:让文生图裁判模型“自己写打分细则“,效果登顶专用裁判模型
  • 17.Excel报表自动化(下):一键生成生产报表
  • Java毕业设计-基于 SpringBoot 的高校学生评教系统的设计与实现 基于 SpringBoot 的校园评教管理系统的设计与实现(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • 4346346
  • iOS智能背景移除终极指南:3行代码实现专业级抠图效果
  • 树莓派摄像头应用编译指南:从源码构建到二次开发
  • Git 常用指令精炼速查
  • 实战指南:掌握番茄小说下载器的本地化部署与高效使用
  • 如何高效解决Windows快捷键冲突:专业级键盘映射优化指南
  • 区间邻域中2项预倾斜复形的面结构:代数、组合与几何的交叉研究
  • 引力波数据分析:基线规范与残差增益计算的核心技术与实践
  • Qwerty Learner:21天打造专业级英语打字肌肉记忆的终极指南
  • 极客上线-企业出海软件定制解决方案,覆盖三端系统建设
  • 串口 tx、rx、GND这个3根线 分别是由谁提供电压,嵌入式板子,还是电脑usb
  • 变系数Camassa-Holm方程小色散渐近解:从多重尺度法到尖峰孤子
  • 抖音音频提取终极指南:5分钟掌握开源下载器批量下载技巧
  • MTKClient终极指南:高效管理联发科设备的开源专业工具
  • 终极AMD Ryzen调试工具SMUDebugTool:5分钟快速上手硬件性能优化
  • Redis使用教程
  • 告别元数据管理烦恼:ExifToolGUI图形化工具终极指南
  • RAG实战指南:解决大模型知识滞后与幻觉的核心方案