一万字拆透HarmonyOS 6.0窗口开发,从懵逼到上手
写了这么多,最后给你总结一份窗口开发的最佳实践清单✅ 主窗口篇在中集中完成所有窗口初始化操作使用AppStorage在UIAbility和页面之间共享安全区域等窗口信息全屏沉浸式效果务必配合安全区域适配一起做同时监听和,应对折叠屏等场景在中取消所有监听✅ 子窗口篇子窗口名称保持唯一,建议用有意义的命名(如创建后必须调用才会显示不需要时主动调销毁悬浮球等场景设置避免抢焦点注意坐标单位是px,需要自己
小邢哥 | 14年编程老炮,拆解技术脉络,记录程序员的进化史
开篇:为什么你必须搞懂"窗口"?
兄弟们,我是小邢哥。
写了14年代码,从Symbian写到Android,从iOS写到Flutter,现在又杀到了HarmonyOS的战场上。要说这些年我踩过的坑,能绕地球两圈。但有一个坑,是所有移动端开发者都绕不过去的——
窗口。
你可能会说:"窗口有什么好讲的?不就是一个页面吗?我写个Page,往里面堆组件,不就完事了?"
如果你真的这么想,那说明你还停留在"能用就行"的阶段。
我跟你打个比方。你去看一栋写字楼,表面上看就是一栋楼、一堆房间。但你仔细想想——哪些房间是办公室?哪些是会议室?哪些是设备间(放电表、水泵的那种)?每种房间的管理规则一样吗?办公室你可以随便装修,设备间你能随便改吗?
窗口也是一样。你屏幕上看到的每一个东西——你的App界面、顶部的状态栏、底部的导航栏、弹出来的音量条、下拉的通知栏——**它们全都是"窗口"**,只不过类型不同、归属不同、管理规则也不同。
搞懂窗口体系,你才能真正理解:
-
为什么你的App有时候会被状态栏遮挡?
-
为什么全屏沉浸式效果不好实现?
-
为什么子窗口跟主窗口的生命周期绑定在一起?
-
为什么有些弹窗不会出现在"最近任务"里?
这些问题的答案,全都藏在HarmonyOS的窗口体系里。
今天这篇文章,我要把HarmonyOS 6.0下的窗口开发从概念到实战给你拆个底朝天。读完这一篇,你就能真正"拿下"窗口开发,而不是在那似懂非懂地复制粘贴代码。
走,上车。
第一章:先搞清楚——HarmonyOS的窗口到底是什么?
1.1 窗口的本质:屏幕上的"地盘划分"
在聊代码之前,咱们先把概念理清楚。
你拿起手机,看着屏幕。屏幕就那么大,但上面同时显示着好几样东西——最上面一条是状态栏(显示时间、电量、信号),中间一大块是你正在用的App,最下面一条是导航栏(返回、主页、最近任务),有时候还会弹出一个音量条、一个通知横幅……
这些东西,在HarmonyOS的底层逻辑里,全都是"窗口"。
你可以把屏幕想象成一张桌子,每个窗口就是桌子上的一张纸。有的纸大(App主界面),有的纸小(音量条),有的纸在最上面(弹窗),有的纸在最下面(壁纸)。这些纸层层叠叠地铺在桌子上,你看到的画面就是从上往下透视的结果。
这个"管理桌子上所有纸张"的系统,就是窗口管理模块。
在HarmonyOS里,这个模块叫做 @ohos.window——它负责窗口的创建、销毁、大小调整、层级管理、属性设置等一切事务。

1.2 两大阵营:系统窗口 vs 应用窗口
HarmonyOS把所有窗口分成了两大阵营:
| 类型 | 说明 | 举例 |
|---|---|---|
| 系统窗口 | 完成系统特定功能的窗口 | 状态栏、导航栏、通知栏、音量条、壁纸、锁屏 |
| 应用窗口 | 与应用显示相关的窗口 | App主界面、App弹窗、悬浮窗 |
系统窗口你基本不用自己创建——它们是操作系统自己管理的。你作为应用开发者,能做的是"跟系统窗口协商",比如让你的App界面延伸到状态栏下面(沉浸式),或者获取状态栏的高度来做布局适配。
应用窗口才是你的主战场。你的App界面就是应用窗口,你弹出的对话框、浮窗也是应用窗口(的子类型)。
应用窗口又细分为两种:
| 子类型 | 说明 | 是否出现在"最近任务"中 |
|---|---|---|
| 应用主窗口 | App的主界面,承载核心内容 | ✅ 是 |
| 应用子窗口 | 弹窗、悬浮窗等辅助界面 | ❌ 否 |
这个区分非常重要——应用子窗口的生命周期跟随应用主窗口。也就是说,主窗口被销毁了,子窗口也跟着没了。就像老板跑路了,员工也得散伙。
1.3 窗口层级:谁在上面,谁在下面?
既然屏幕上同时存在多个窗口,那谁显示在最上面、谁被遮在下面,就必须有个规则。
HarmonyOS给每个窗口分配了一个层级(Z-Order)。可以简单理解为:
最上层 ──→ 系统弹窗(如来电界面、权限请求)
↑ 应用子窗口(弹窗、悬浮窗)
| 应用主窗口(App主界面)
| 系统装饰窗口(状态栏、导航栏)
最底层 ──→ 壁纸
一般来说,系统窗口的层级 > 应用窗口的层级。这就是为什么来电界面能直接覆盖你正在玩的游戏——系统窗口"权力"更大。
而在应用内部,子窗口默认在主窗口之上,所以你弹出的对话框会遮挡主界面内容。
好了,概念讲到这里,你应该已经对HarmonyOS的窗口体系有了一个全局认知。接下来,我们进入实战——在Stage模型下,应用窗口到底怎么开发。
第二章:Stage模型——窗口开发的"主战场"
2.1 为什么要强调Stage模型?
如果你关注过HarmonyOS的发展历程,你应该知道它经历过两套应用模型:
-
FA模型(Feature Ability):早期的应用模型,类似Android的Activity架构
-
Stage模型:从HarmonyOS 3.0开始主推的新模型,也是6.0版本下唯一推荐使用的模型
华为官方已经明确表态:Stage模型是未来。新项目必须用Stage模型,FA模型将逐步退出历史舞台。
所以,今天这篇文章所有的窗口开发内容,都基于Stage模型。如果你还在用FA模型写代码,建议你赶紧迁移过来——别等华为彻底不兼容了再抓瞎。
2.2 Stage模型的核心概念:UIAbility + WindowStage
在Stage模型下,窗口开发绑定着两个核心概念:
UIAbility:可以简单理解为"一个可以显示界面的应用组件"。每个UIAbility对应一个独立的任务,会出现在"最近任务"管理界面中。
WindowStage:UIAbility的"窗口舞台"。你可以把UIAbility想象成一个剧团,WindowStage就是这个剧团的舞台。所有的窗口操作——加载页面、设置属性、创建子窗口——都在WindowStage上进行。

它们的关系是这样的:
UIAbility(剧团)
└── WindowStage(舞台)
├── 主窗口(正在表演的主角)——自动创建,不用你管
├── 子窗口1(配角/道具)——你手动创建
└── 子窗口2(配角/道具)——你手动创建
重点来了——
当UIAbility启动的时候,系统会自动为它创建一个WindowStage,WindowStage里自动包含一个主窗口。你不需要手动创建主窗口。
这跟Android不一样。在Android里,你得自己调setContentView()把布局塞进Activity。但在HarmonyOS Stage模型里,你只需要在UIAbility的onWindowStageCreate()回调中,告诉WindowStage"加载哪个页面"就行了。
// UIAbility的生命周期回调
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// 告诉WindowStage:加载pages/Index这个页面作为主窗口内容
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('Failed to load content. Cause: ' + JSON.stringify(err));
return;
}
console.info('Succeeded in loading content.');
});
}
}
看到了吗?就这几行代码,你的App主窗口就有了。WindowStage帮你处理了窗口创建、显示、生命周期管理等所有底层工作。
2.3 UIAbility的生命周期与窗口的关系
要搞懂窗口开发,你必须搞懂UIAbility的生命周期,因为窗口的创建和销毁跟它是绑在一起的。
UIAbility的生命周期回调如下:
onCreate() ──→ UIAbility被创建
↓
onWindowStageCreate() ──→ WindowStage被创建(窗口可用了!)
↓
onForeground() ──→ 进入前台(用户看得到了)
↓
onBackground() ──→ 进入后台(用户看不到了)
↓
onWindowStageDestroy() ──→ WindowStage被销毁(窗口没了!)
↓
onDestroy() ──→ UIAbility被销毁
关键认知:
-
onWindowStageCreate()是你操作窗口的第一个时机——在这里加载页面、设置窗口属性、注册窗口事件监听。 -
onWindowStageDestroy()是你清理窗口资源的最后时机——在这里释放子窗口、取消监听。 -
主窗口随WindowStage自动创建和销毁,你不需要手动管理它的生命周期。
-
但子窗口需要你手动创建和销毁——系统不会帮你干这个事。
理解了这些,你就掌握了Stage模型下窗口开发的"骨架"。接下来我们往"骨架"上填肉。
第三章:应用主窗口开发——你的"主舞台"
3.1 获取主窗口对象
虽然主窗口是自动创建的,但你经常需要拿到它的引用来做各种操作(比如设置全屏、获取尺寸、监听变化等)。
获取主窗口对象有两种方式:
方式一:通过WindowStage获取
onWindowStageCreate(windowStage: window.WindowStage): void {
// 获取主窗口
let mainWindow = windowStage.getMainWindowSync();
console.info('主窗口ID: ' + mainWindow.getWindowProperties().id);
}
getMainWindowSync() 是同步方法,直接返回主窗口对象,简单粗暴。
方式二:通过window模块获取最后显示的窗口
import { window } from '@kit.ArkUI';
// 获取最后显示的窗口(通常就是当前主窗口)
let lastWindow = await window.getLastWindow(this.context);
一般来说,方式一更精确、更推荐。方式二适用于一些特殊场景,比如你在一个工具类里没有WindowStage的引用。
3.2 设置主窗口属性——最常用的几个操作
拿到主窗口对象后,你可以对它进行各种属性设置。下面我挑几个开发中最常用、最实用的操作来讲。
3.2.1 设置窗口全屏 / 沉浸式
这是被问得最多的问题之一——"我怎么让App全屏显示?怎么让内容延伸到状态栏和导航栏下面?"
在HarmonyOS 6.0中,沉浸式的实现方式是通过设置窗口的布局是否延伸到安全区域之外。
onWindowStageCreate(windowStage: window.WindowStage): void {
let mainWindow = windowStage.getMainWindowSync();
// 获取窗口属性
let windowProperties = mainWindow.getWindowProperties();
// 设置窗口为全屏布局(内容延伸到状态栏和导航栏区域)
mainWindow.setWindowLayoutFullScreen(true).then(() => {
console.info('设置全屏布局成功');
}).catch((err: Error) => {
console.error('设置全屏布局失败: ' + JSON.stringify(err));
});
// 加载页面
windowStage.loadContent('pages/Index');
}
但是——全屏之后,你的内容可能会被状态栏和导航栏遮挡!
这时候你需要获取安全区域(Safe Area)的信息,然后在页面布局中做适配。安全区域就是"不会被系统UI遮挡的区域"。
// 获取安全区域
let avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
let topHeight = avoidArea.topRect.height; // 状态栏高度
let bottomHeight = avoidArea.bottomRect.height; // 导航栏高度
console.info('状态栏高度: ' + topHeight);
console.info('导航栏高度: ' + bottomHeight);
然后在你的ArkTS页面里,给顶部和底部留出对应的padding:
@Entry
@Component
struct Index {
@State topPadding: number = 0;
@State bottomPadding: number = 0;
aboutToAppear(): void {
// 这里通过某种方式拿到安全区域高度并赋值
// 具体方式取决于你的架构设计(比如通过AppStorage传递)
}
build() {
Column() {
// 你的页面内容
Text('Hello HarmonyOS')
.fontSize(24)
}
.width('100%')
.height('100%')
.padding({
top: this.topPadding,
bottom: this.bottomPadding
})
}
}
这样你就实现了"全屏沉浸式 + 内容不被遮挡"的效果。背景可以延伸到状态栏下面,但文字、按钮等交互内容安全地待在安全区域内。
小邢哥踩坑提醒:
很多人设置完全屏后发现内容被挡了就慌了,以为代码写错了。其实不是——全屏模式下内容延伸到整个屏幕是正常行为,你需要自己处理安全区域适配。这不是Bug,这是设计。HarmonyOS给了你自由,但自由意味着责任。
3.2.2 设置状态栏和导航栏的样式
全屏沉浸式搞定之后,你还得解决一个问题——状态栏上的文字(时间、电量)默认是黑色的,如果你的App背景也是深色,那状态栏文字就看不清了。
你可以设置系统栏(状态栏 + 导航栏)的内容颜色:
// 获取主窗口
let mainWindow = windowStage.getMainWindowSync();
// 设置状态栏属性
let systemBarProperties: window.SystemBarProperties = {
statusBarContentColor: '#FFFFFF', // 状态栏文字颜色设为白色
navigationBarContentColor: '#FFFFFF', // 导航栏按钮颜色设为白色
};
mainWindow.setWindowSystemBarProperties(systemBarProperties).then(() => {
console.info('系统栏样式设置成功');
}).catch((err: Error) => {
console.error('系统栏样式设置失败: ' + JSON.stringify(err));
});
你也可以设置状态栏和导航栏的背景颜色,或者让它们透明:
let systemBarProperties: window.SystemBarProperties = {
statusBarColor: '#00000000', // 状态栏背景透明
statusBarContentColor: '#FFFFFF', // 状态栏文字白色
navigationBarColor: '#00000000', // 导航栏背景透明
navigationBarContentColor: '#FFFFFF', // 导航栏按钮白色
};
这样就能实现完美的沉浸式效果——系统栏变透明,你的App背景自然地延伸到整个屏幕,同时状态栏文字颜色跟你的设计风格协调。
3.2.3 监听窗口尺寸变化
在手机上你可能觉得窗口大小是固定的,但如果你的App要适配平板、折叠屏、PC(2in1设备),窗口尺寸变化就是家常便饭。
用户可能会分屏、调整窗口大小、折叠/展开屏幕——你的App必须能响应这些变化。
onWindowStageCreate(windowStage: window.WindowStage): void {
let mainWindow = windowStage.getMainWindowSync();
// 监听窗口尺寸变化
mainWindow.on('windowSizeChange', (size: window.Size) => {
console.info('窗口宽度变了: ' + size.width);
console.info('窗口高度变了: ' + size.height);
// 在这里更新你的UI布局
// 比如:宽度 > 某个阈值时切换为双栏布局
});
// 加载页面
windowStage.loadContent('pages/Index');
}
同样重要的是——在WindowStage销毁时取消监听:
onWindowStageDestroy(): void {
let mainWindow = this.windowStage.getMainWindowSync();
// 取消所有窗口事件监听
mainWindow.off('windowSizeChange');
console.info('窗口事件监听已取消');
}
不取消监听会导致内存泄漏,这是老生常谈了,但每一代新框架出来都有人在这里翻车,永远有人不信邪。
3.2.4 设置窗口亮度
有些App需要在特定场景下调整屏幕亮度,比如扫码页面需要提高亮度、阅读模式需要降低亮度。
let mainWindow = windowStage.getMainWindowSync();
// 设置窗口亮度(0.0 - 1.0,-1.0表示跟随系统亮度)
mainWindow.setWindowBrightness(0.8).then(() => {
console.info('亮度设置成功');
});
// 恢复跟随系统亮度
mainWindow.setWindowBrightness(-1).then(() => {
console.info('已恢复系统亮度');
});
注意:这个设置只影响当前窗口,不影响系统全局亮度。当你的App退到后台,亮度会恢复正常。这是一个很优雅的设计——不会影响用户的全局设置。
3.2.5 设置屏幕常亮
视频播放、导航、游戏等场景下,你不希望屏幕自动熄灭。
let mainWindow = windowStage.getMainWindowSync();
// 设置屏幕常亮
mainWindow.setWindowKeepScreenOn(true).then(() => {
console.info('屏幕常亮已开启');
});
// 取消屏幕常亮
mainWindow.setWindowKeepScreenOn(false).then(() => {
console.info('屏幕常亮已关闭');
});
小邢哥踩坑提醒: 别忘了在不需要的时候关掉屏幕常亮!我见过不止一个App在退出视频播放页面后忘了关这个,用户的电量哗哗地掉,最后在应用商店被打一星差评。
3.2.6 设置窗口方向
有些App需要锁定横屏(比如游戏)或竖屏(比如社交App),或者允许用户自由旋转。
let mainWindow = windowStage.getMainWindowSync();
// 锁定竖屏
mainWindow.setPreferredOrientation(window.Orientation.PORTRAIT).then(() => {
console.info('已锁定竖屏');
});
// 锁定横屏
mainWindow.setPreferredOrientation(window.Orientation.LANDSCAPE).then(() => {
console.info('已锁定横屏');
});
// 跟随传感器自动旋转
mainWindow.setPreferredOrientation(window.Orientation.AUTO_ROTATION).then(() => {
console.info('已设置自动旋转');
});
3.3 主窗口开发完整示例
下面给你一个完整的、实际开发中常用的主窗口配置示例。这个示例实现了:沉浸式全屏 + 状态栏适配 + 窗口尺寸监听。
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG: string = 'EntryAbility';
export default class EntryAbility extends UIAbility {
private mainWindow: window.Window | null = null;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, TAG, 'onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(0x0000, TAG, 'onWindowStageCreate');
// 1. 获取主窗口
this.mainWindow = windowStage.getMainWindowSync();
// 2. 设置全屏沉浸式
this.mainWindow.setWindowLayoutFullScreen(true).then(() => {
hilog.info(0x0000, TAG, '全屏布局设置成功');
});
// 3. 设置系统栏样式(透明背景 + 白色文字)
let systemBarProps: window.SystemBarProperties = {
statusBarColor: '#00000000',
statusBarContentColor: '#FFFFFF',
navigationBarColor: '#00000000',
navigationBarContentColor: '#FFFFFF',
};
this.mainWindow.setWindowSystemBarProperties(systemBarProps);
// 4. 获取安全区域信息并存储到AppStorage(供页面使用)
let avoidArea = this.mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
let topHeight = px2vp(avoidArea.topRect.height);
let bottomHeight = px2vp(avoidArea.bottomRect.height);
AppStorage.setOrCreate('safeAreaTop', topHeight);
AppStorage.setOrCreate('safeAreaBottom', bottomHeight);
// 5. 监听安全区域变化(折叠屏展开/收起等场景)
this.mainWindow.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
let newTop = px2vp(data.area.topRect.height);
let newBottom = px2vp(data.area.bottomRect.height);
AppStorage.setOrCreate('safeAreaTop', newTop);
AppStorage.setOrCreate('safeAreaBottom', newBottom);
}
});
// 6. 监听窗口尺寸变化
this.mainWindow.on('windowSizeChange', (size: window.Size) => {
let windowWidth = px2vp(size.width);
let windowHeight = px2vp(size.height);
AppStorage.setOrCreate('windowWidth', windowWidth);
AppStorage.setOrCreate('windowHeight', windowHeight);
hilog.info(0x0000, TAG, `窗口尺寸变化: ${windowWidth} x ${windowHeight}`);
});
// 7. 加载主页面
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
hilog.error(0x0000, TAG, 'loadContent失败: ' + JSON.stringify(err));
return;
}
hilog.info(0x0000, TAG, '页面加载成功');
});
}
onForeground(): void {
hilog.info(0x0000, TAG, 'onForeground');
}
onBackground(): void {
hilog.info(0x0000, TAG, 'onBackground');
}
onWindowStageDestroy(): void {
hilog.info(0x0000, TAG, 'onWindowStageDestroy');
// 取消所有监听,防止内存泄漏
if (this.mainWindow) {
this.mainWindow.off('windowSizeChange');
this.mainWindow.off('avoidAreaChange');
}
}
onDestroy(): void {
hilog.info(0x0000, TAG, 'onDestroy');
}
}
对应的页面文件 pages/Index.ets:
@Entry
@Component
struct Index {
@StorageProp('safeAreaTop') topPadding: number = 0;
@StorageProp('safeAreaBottom') bottomPadding: number = 0;
@StorageProp('windowWidth') windowWidth: number = 0;
@StorageProp('windowHeight') windowHeight: number = 0;
build() {
Column() {
// 顶部安全区域占位
Row()
.width('100%')
.height(this.topPadding)
.backgroundColor('#3B82F6') // 跟状态栏背景融合
// 自定义标题栏
Row() {
Text('我的App')
.fontSize(20)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height(56)
.backgroundColor('#3B82F6')
.justifyContent(FlexAlign.Center)
// 主内容区域
Column() {
Text('Hello HarmonyOS 6.0!')
.fontSize(28)
.margin({ top: 40 })
Text(`窗口尺寸: ${Math.round(this.windowWidth)} x ${Math.round(this.windowHeight)}`)
.fontSize(16)
.fontColor('#666666')
.margin({ top: 16 })
Text(`安全区域: 上${Math.round(this.topPadding)} 下${Math.round(this.bottomPadding)}`)
.fontSize(16)
.fontColor('#666666')
.margin({ top: 8 })
}
.width('100%')
.layoutWeight(1)
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
// 底部安全区域占位
Row()
.width('100%')
.height(this.bottomPadding)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
这个示例你可以直接拿去用,覆盖了实际开发中最常见的主窗口操作场景。

第四章:应用子窗口开发——弹窗、悬浮窗、覆盖层
4.1 什么时候需要子窗口?
在讲怎么创建子窗口之前,先搞清楚一个问题——什么时候你需要用子窗口?
你可能会说:"弹窗不是用CustomDialog或者AlertDialog组件就能做吗?为什么还需要子窗口?"
问得好。ArkUI提供的Dialog组件确实能满足大部分弹窗需求,但在以下场景中,子窗口是更好的选择:
| 场景 | 为什么用子窗口 |
|---|---|
| 悬浮窗 / 浮球 | Dialog不能脱离页面布局独立存在,子窗口可以 |
| 画中画 | 需要一个独立的、可拖动的、有自己页面内容的小窗口 |
| 自定义Toast | 系统Toast样式有限,用子窗口可以完全自定义 |
| 全局Loading | 需要覆盖在当前所有页面之上,包括页面切换过程中 |
| 独立的交互浮层 | 比如视频通话时的浮动控制面板 |
| 多窗口协作 | 平板/折叠屏场景下,一个App需要同时显示多个独立内容区域 |
简单来说——当你需要一个"独立于主页面布局"的浮动界面时,用子窗口。
4.2 创建子窗口
创建子窗口需要通过WindowStage的 createSubWindow() 方法。下面是完整的创建流程:
import { window } from '@kit.ArkUI';
async function createSubWindow(windowStage: window.WindowStage): Promise<window.Window | null> {
try {
// 1. 创建子窗口(参数是窗口名称,必须唯一)
let subWindow: window.Window = await windowStage.createSubWindow('mySubWindow');
console.info('子窗口创建成功');
// 2. 设置子窗口的位置和大小(单位:px)
await subWindow.moveWindowTo(100, 300); // 移动到屏幕坐标(100, 300)
await subWindow.resize(800, 600); // 设置宽800px、高600px
// 3. 设置子窗口的背景颜色
await subWindow.setWindowBackgroundColor('#CC000000'); // 半透明黑色
// 4. 加载子窗口的页面内容
await subWindow.setUIContent('pages/SubWindowPage');
// 5. 显示子窗口
await subWindow.showWindow();
console.info('子窗口已显示');
return subWindow;
} catch (err) {
console.error('创建子窗口失败: ' + JSON.stringify(err));
return null;
}
}
几个关键点:
-
窗口名称必须唯一:如果你创建了一个叫
'mySubWindow'的子窗口,再创建一个同名的会报错。 -
moveWindowTo的坐标是相对于屏幕左上角的绝对坐标,单位是px(物理像素),不是vp。 -
resize的单位也是px。如果你想用vp,需要自己做转换:let pxValue = vp2px(vpValue)。 -
子窗口创建后默认不显示,必须调用
showWindow()才会出现在屏幕上。 -
子窗口有自己独立的页面,通过
setUIContent()加载。这个页面和主窗口的页面是完全独立的组件树。
4.3 子窗口的页面
子窗口的页面跟主窗口的页面写法完全一样,就是一个普通的ArkTS组件:
// pages/SubWindowPage.ets
@Entry
@Component
struct SubWindowPage {
@State message: string = '我是子窗口';
build() {
Column() {
Text(this.message)
.fontSize(24)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
Button('关闭子窗口')
.margin({ top: 20 })
.onClick(() => {
this.closeSubWindow();
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor('#CC333333')
.borderRadius(16)
}
private async closeSubWindow(): Promise<void> {
try {
// 通过窗口名称找到子窗口
let subWindow = await window.findWindow('mySubWindow');
// 销毁子窗口
await subWindow.destroyWindow();
console.info('子窗口已销毁');
} catch (err) {
console.error('关闭子窗口失败: ' + JSON.stringify(err));
}
}
}
4.4 管理子窗口的生命周期
子窗口最重要的一件事——你创建了它,就要负责销毁它。
跟主窗口不同,主窗口的生命周期由系统自动管理,但子窗口是你手动创建的,所以也要你手动销毁。
销毁子窗口的三个时机:
-
用户主动关闭——比如点了"关闭"按钮,调用
destroyWindow() -
业务逻辑触发——比如某个流程结束了,不需要悬浮窗了
-
UIAbility销毁时——在
onWindowStageDestroy()中清理所有子窗口
onWindowStageDestroy(): void {
// 销毁所有子窗口
try {
let subWindow = window.findWindow('mySubWindow');
subWindow.destroyWindow();
} catch (err) {
// 如果子窗口已经被销毁了,findWindow会抛异常,这里catch住就行
console.info('子窗口已不存在或已销毁');
}
}
小邢哥踩坑提醒: 子窗口的生命周期跟随主窗口。主窗口被销毁了,子窗口也会被系统自动清理。但是——不要依赖这个机制! 你应该主动在 onWindowStageDestroy() 中做清理。原因有二:一是保持代码可预测,二是避免子窗口销毁时有未完成的异步操作导致异常。
4.5 子窗口的事件监听
子窗口同样支持事件监听。最常用的有:
// 监听子窗口尺寸变化
subWindow.on('windowSizeChange', (size: window.Size) => {
console.info(`子窗口尺寸变化: ${size.width} x ${size.height}`);
});
// 监听子窗口获取/失去焦点
subWindow.on('windowEvent', (eventType: number) => {
// eventType: 1 = 获取焦点, 2 = 失去焦点
// 4 = 变为可见, 5 = 变为不可见
if (eventType === 1) {
console.info('子窗口获得焦点');
} else if (eventType === 2) {
console.info('子窗口失去焦点');
}
});
记得在子窗口销毁前取消监听:
subWindow.off('windowSizeChange');
subWindow.off('windowEvent');
await subWindow.destroyWindow();
4.6 一个实用案例:可拖拽的悬浮球
下面给你一个实际开发中常见的案例——实现一个可拖拽的悬浮球子窗口。
在UIAbility中创建悬浮球子窗口:
private async createFloatingBall(windowStage: window.WindowStage): Promise<void> {
try {
let floatWindow = await windowStage.createSubWindow('floatingBall');
// 设置悬浮球大小(60vp x 60vp)
let size = vp2px(60);
await floatWindow.resize(size, size);
// 设置初始位置(屏幕右侧中间)
let displayInfo = display.getDefaultDisplaySync();
let x = displayInfo.width - size - vp2px(16); // 距右边缘16vp
let y = displayInfo.height / 2 - size / 2; // 垂直居中
await floatWindow.moveWindowTo(x, y);
// 设置透明背景
await floatWindow.setWindowBackgroundColor('#00000000');
// 加载悬浮球页面
await floatWindow.setUIContent('pages/FloatingBall');
// 设置子窗口不可获取焦点(防止点击悬浮球时主窗口失去焦点)
await floatWindow.setWindowFocusable(false);
// 设置子窗口支持触摸穿透(悬浮球之外的区域点击穿透到下层)
await floatWindow.setWindowTouchable(true);
// 显示悬浮球
await floatWindow.showWindow();
} catch (err) {
console.error('创建悬浮球失败: ' + JSON.stringify(err));
}
}
悬浮球页面 pages/FloatingBall.ets:
import { window } from '@kit.ArkUI';
@Entry
@Component
struct FloatingBall {
@State offsetX: number = 0;
@State offsetY: number = 0;
private startX: number = 0;
private startY: number = 0;
private windowStartX: number = 0;
private windowStartY: number = 0;
build() {
Stack() {
Circle()
.width(60)
.height(60)
.fill('#3B82F6')
.shadow({
radius: 8,
color: '#4D3B82F6',
offsetX: 0,
offsetY: 4
})
Image($r('app.media.ic_chat'))
.width(28)
.height(28)
.fillColor('#FFFFFF')
}
.width(60)
.height(60)
.gesture(
PanGesture()
.onActionStart((event: GestureEvent) => {
// 记录触摸起始位置和窗口起始位置
this.startX = event.offsetX;
this.startY = event.offsetY;
this.getWindowPosition();
})
.onActionUpdate((event: GestureEvent) => {
// 计算窗口应该移动到的新位置
let newX = this.windowStartX + event.offsetX - this.startX;
let newY = this.windowStartY + event.offsetY - this.startY;
this.moveWindow(newX, newY);
})
)
.onClick(() => {
// 点击悬浮球的逻辑,比如打开某个功能面板
console.info('悬浮球被点击');
})
}
private async getWindowPosition(): Promise<void> {
try {
let floatWin = await window.findWindow('floatingBall');
let properties = floatWin.getWindowProperties();
this.windowStartX = properties.windowRect.left;
this.windowStartY = properties.windowRect.top;
} catch (err) {
console.error('获取窗口位置失败');
}
}
private async moveWindow(x: number, y: number): Promise<void> {
try {
let floatWin = await window.findWindow('floatingBall');
await floatWin.moveWindowTo(x, y);
} catch (err) {
// 移动过于频繁时可能会有偶发异常,忽略即可
}
}
}
这个悬浮球可以在屏幕上自由拖动,点击后触发自定义逻辑。这是很多App(比如客服系统、悬浮工具箱)的常见需求。
第五章:窗口属性深入——那些你可能忽略的细节
5.1 获取窗口属性
除了前面讲的那些"设置"操作,你经常还需要"获取"窗口的各种属性信息:
let mainWindow = windowStage.getMainWindowSync();
let properties: window.WindowProperties = mainWindow.getWindowProperties();
// 窗口矩形区域(位置和大小)
console.info('窗口位置: ' + JSON.stringify(properties.windowRect));
// { left: 0, top: 0, width: 1080, height: 2340 }
// 窗口ID
console.info('窗口ID: ' + properties.id);
// 窗口是否全屏
console.info('是否全屏: ' + properties.isFullScreen);
// 窗口是否处于前台
console.info('是否前台: ' + properties.isFocused);
// 窗口类型
console.info('窗口类型: ' + properties.type);
WindowProperties 是一个信息宝库,几乎所有你想知道的窗口状态都能从这里获取。
5.2 窗口与Display的关系
window 模块管的是窗口本身,但有时候你需要知道屏幕(Display)的信息——屏幕分辨率、屏幕密度、屏幕方向等。
这就需要用到 @ohos.display 模块:
import { display } from '@kit.ArkUI';
// 获取默认屏幕信息
let displayInfo: display.Display = display.getDefaultDisplaySync();
console.info('屏幕宽度: ' + displayInfo.width); // px
console.info('屏幕高度: ' + displayInfo.height); // px
console.info('屏幕密度: ' + displayInfo.densityDPI); // DPI
console.info('屏幕缩放比: ' + displayInfo.densityPixels); // 类似Android的density
console.info('屏幕方向: ' + displayInfo.rotation); // 0=竖屏 1=90度 2=180度 3=270度
窗口大小 ≠ 屏幕大小。 在全屏模式下,窗口大小约等于屏幕大小。但在分屏、悬浮窗等模式下,窗口可能只占屏幕的一部分。
5.3 窗口模式
在支持多窗口的设备(平板、折叠屏、PC)上,窗口可以有不同的模式:
// 获取当前窗口模式
let mode = mainWindow.getWindowProperties().mode;
// 常见窗口模式:
// window.WindowMode.UNDEFINED - 未定义
// window.WindowMode.FULLSCREEN - 全屏
// window.WindowMode.PRIMARY - 分屏主窗口
// window.WindowMode.SECONDARY - 分屏副窗口
// window.WindowMode.FLOATING - 自由窗口(悬浮窗)
5.4 窗口的触摸和焦点管理
在多窗口场景下,焦点和触摸的管理变得很重要:
// 设置窗口是否可以获取焦点
await subWindow.setWindowFocusable(false);
// 用途:悬浮球、Toast等不需要获取焦点的窗口
// 设置窗口是否可触摸
await subWindow.setWindowTouchable(true);
// 用途:某些覆盖层需要接收触摸事件
// 主动请求焦点
await subWindow.requestFocus();
// 用途:需要让某个子窗口获取输入焦点(比如弹出输入框)
第六章:多设备适配——折叠屏、平板、PC的窗口开发
6.1 为什么要关心多设备?
HarmonyOS从第一天起就在喊"一次开发、多端部署"。到了6.0版本,这不再是口号——鸿蒙生态已经覆盖手机、平板、折叠屏、PC(2in1设备)、智慧屏、车机等。
你的App大概率需要在不同形态的设备上运行。
而不同设备最大的差异之一,就是窗口的行为不同:
| 设备 | 窗口特点 |
|---|---|
| 手机 | 全屏为主,窗口大小基本固定 |
| 折叠屏 | 展开/折叠时窗口大小会剧烈变化 |
| 平板 | 支持分屏,窗口可能只占屏幕一半 |
| PC | 自由窗口模式,用户可以随意调整大小 |
6.2 折叠屏适配
折叠屏是最"折腾人"的设备。用户一折一展,屏幕尺寸瞬间翻倍或减半,你的App必须能无缝响应。
核心策略:监听窗口尺寸变化 + 响应式布局。
// 在UIAbility中监听尺寸变化
mainWindow.on('windowSizeChange', (size: window.Size) => {
let widthVp = px2vp(size.width);
let heightVp = px2vp(size.height);
// 根据宽度决定布局模式
if (widthVp >= 840) {
// 大屏模式(展开态/平板/PC)——使用双栏布局
AppStorage.setOrCreate('layoutMode', 'large');
} else if (widthVp >= 520) {
// 中屏模式(折叠屏半开/竖屏平板)——使用自适应布局
AppStorage.setOrCreate('layoutMode', 'medium');
} else {
// 小屏模式(手机/折叠屏折叠态)——使用单栏布局
AppStorage.setOrCreate('layoutMode', 'small');
}
});
页面侧响应布局模式:
@Entry
@Component
struct Index {
@StorageProp('layoutMode') layoutMode: string = 'small';
build() {
if (this.layoutMode === 'large') {
// 双栏布局
Row() {
this.NavigationPanel()
this.ContentPanel()
}
} else {
// 单栏布局
Column() {
this.ContentPanel()
}
}
}
@Builder
NavigationPanel() {
Column() {
Text('导航面板')
}
.width('30%')
.height('100%')
.backgroundColor('#F0F0F0')
}
@Builder
ContentPanel() {
Column() {
Text('内容面板')
}
.layoutWeight(1)
.height('100%')
}
}
6.3 安全区域的动态适配
折叠屏展开/折叠时,安全区域也会变化。前面我们已经监听了 avoidAreaChange 事件,这里再强调一下它的重要性:
mainWindow.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
// 系统安全区域变化(状态栏、导航栏高度变了)
let topHeight = px2vp(data.area.topRect.height);
let bottomHeight = px2vp(data.area.bottomRect.height);
AppStorage.setOrCreate('safeAreaTop', topHeight);
AppStorage.setOrCreate('safeAreaBottom', bottomHeight);
}
if (data.type === window.AvoidAreaType.TYPE_CUTOUT) {
// 刘海屏/挖孔屏区域变化
// 折叠屏展开后可能没有刘海了,或者刘海位置变了
let cutoutInfo = data.area;
AppStorage.setOrCreate('cutoutArea', cutoutInfo);
}
});
小邢哥踩坑提醒: 折叠屏从折叠态展开到展开态的瞬间,windowSizeChange 和 avoidAreaChange 可能会先后触发多次(因为屏幕尺寸是渐变的,不是瞬间完成的)。如果你在回调里有大量UI更新操作,记得做防抖(debounce)处理,避免短时间内频繁重绘导致卡顿。
6.4 分屏场景
平板和PC支持分屏——你的App窗口可能只占屏幕的一半甚至三分之一。
这时候 windowSizeChange 的监听就尤为重要。你的布局必须能适应各种宽高比,不能只按"全屏"来设计。
// 判断是否处于分屏模式
mainWindow.on('windowSizeChange', (size: window.Size) => {
let displayInfo = display.getDefaultDisplaySync();
let screenWidth = displayInfo.width;
let windowWidth = size.width;
if (windowWidth < screenWidth * 0.9) {
// 窗口宽度明显小于屏幕宽度,大概率是分屏或自由窗口模式
console.info('当前处于分屏/自由窗口模式');
}
});
第七章:窗口开发中的常见问题与最佳实践
7.1 问题清单
Q1:为什么我的页面内容被状态栏挡住了?
A:你设置了 setWindowLayoutFullScreen(true) 但没有处理安全区域适配。全屏模式下,页面内容会延伸到状态栏区域,你需要手动给顶部留出安全区域的高度。参考第三章3.2.1节。
Q2:子窗口创建了但看不到?
A:三个可能原因——
-
忘了调
showWindow()。创建子窗口后默认不显示。 -
子窗口的位置设到了屏幕之外。检查
moveWindowTo()的坐标。 -
子窗口的大小设成了0。检查
resize()的参数。
Q3:子窗口的背景是黑色的,我想要透明背景?
A:调用 subWindow.setWindowBackgroundColor('#00000000') 设置完全透明背景。注意八位色值,前两位是透明度。
Q4:多次创建同名子窗口报错?
A:子窗口名称必须唯一。如果你需要重复创建,先调 destroyWindow() 销毁旧的,再创建新的。或者用 window.findWindow() 检查窗口是否已存在:
try {
let existingWindow = window.findWindow('mySubWindow');
// 窗口已存在,直接使用或先销毁
await existingWindow.destroyWindow();
} catch (err) {
// 窗口不存在,可以创建
}
let newWindow = await windowStage.createSubWindow('mySubWindow');
Q5:px2vp 和 vp2px 转换不准?
A:确保你在正确的上下文中使用。px2vp() 和 vp2px() 是ArkTS的全局函数,但它们依赖当前设备的屏幕密度。在不同设备上,同样的vp值对应的px值是不同的。
Q6:横竖屏切换时布局错乱?
A:你的布局可能使用了固定的px值而不是响应式的百分比或vp值。建议:
-
宽高尽量用百分比或
layoutWeight -
监听
windowSizeChange动态调整布局 -
使用ArkUI的响应式断点(如
@StorageProp+ 布局模式判断)
7.2 最佳实践总结
写了这么多,最后给你总结一份窗口开发的最佳实践清单:
✅ 主窗口篇
-
在
onWindowStageCreate()中集中完成所有窗口初始化操作 -
使用
AppStorage在UIAbility和页面之间共享安全区域等窗口信息 -
全屏沉浸式效果务必配合安全区域适配一起做
-
同时监听
windowSizeChange和avoidAreaChange,应对折叠屏等场景 -
在
onWindowStageDestroy()中取消所有监听
✅ 子窗口篇
-
子窗口名称保持唯一,建议用有意义的命名(如
'floatingBall'、'videoCallPanel') -
创建后必须调用
showWindow()才会显示 -
不需要时主动调
destroyWindow()销毁 -
悬浮球等场景设置
setWindowFocusable(false)避免抢焦点 -
注意坐标单位是px,需要自己做vp/px转换
✅ 多设备适配篇
-
用窗口宽度(而非设备类型)来决定布局模式
-
布局使用百分比 +
layoutWeight,避免固定px值 -
对频繁触发的窗口事件做防抖处理
-
测试时覆盖手机、折叠屏折叠态/展开态、平板横竖屏等场景
✅ 通用篇
-
所有窗口操作的异步方法都要做
try-catch错误处理 -
事件监听
on()和取消监听off()必须成对出现 -
使用
hilog输出窗口相关日志,方便调试 -
窗口属性的设置建议集中管理,不要散落在各处
第八章:一个完整的实战项目——带子窗口的视频播放器
光说理论不过瘾,我们来做一个完整的实战项目,把前面讲的所有知识串起来。
8.1 需求描述
做一个视频播放器App:
-
主窗口全屏沉浸式显示视频列表
-
点击视频后进入播放页面,横屏全屏播放
-
播放时支持弹出一个悬浮控制面板(子窗口),显示播放进度、音量、亮度控制
-
悬浮面板可以拖拽位置
-
退出播放时恢复竖屏
8.2 项目结构
entry/src/main/ets/
├── entryability/
│ └── EntryAbility.ets // UIAbility
├── pages/
│ ├── Index.ets // 视频列表主页
│ ├── PlayerPage.ets // 播放页面
│ └── FloatingControl.ets // 悬浮控制面板(子窗口页面)
└── utils/
└── WindowUtil.ets // 窗口工具类
8.3 核心代码
窗口工具类 WindowUtil.ets:
import { window, display } from '@kit.ArkUI';
export class WindowUtil {
private static subWindowMap: Map<string, window.Window> = new Map();
/**
* 设置沉浸式全屏
*/
static async setImmersiveMode(mainWindow: window.Window): Promise<void> {
await mainWindow.setWindowLayoutFullScreen(true);
await mainWindow.setWindowSystemBarProperties({
statusBarColor: '#00000000',
statusBarContentColor: '#FFFFFF',
navigationBarColor: '#00000000',
navigationBarContentColor: '#FFFFFF',
});
}
/**
* 获取安全区域信息(返回vp单位)
*/
static getSafeAreaInsets(mainWindow: window.Window): {
top: number, bottom: number, left: number, right: number
} {
let avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
return {
top: px2vp(avoidArea.topRect.height),
bottom: px2vp(avoidArea.bottomRect.height),
left: px2vp(avoidArea.leftRect.width),
right: px2vp(avoidArea.rightRect.width),
};
}
/**
* 设置屏幕方向
*/
static async setOrientation(mainWindow: window.Window,
orientation: window.Orientation): Promise<void> {
await mainWindow.setPreferredOrientation(orientation);
}
/**
* 创建子窗口
*/
static async createSubWindow(
windowStage: window.WindowStage,
name: string,
contentPage: string,
rect: { x: number, y: number, width: number, height: number }
): Promise<window.Window | null> {
try {
// 如果同名窗口已存在,先销毁
if (WindowUtil.subWindowMap.has(name)) {
await WindowUtil.destroySubWindow(name);
}
let subWin = await windowStage.createSubWindow(name);
await subWin.moveWindowTo(vp2px(rect.x), vp2px(rect.y));
await subWin.resize(vp2px(rect.width), vp2px(rect.height));
await subWin.setWindowBackgroundColor('#00000000');
await subWin.setUIContent(contentPage);
await subWin.setWindowFocusable(false);
await subWin.showWindow();
WindowUtil.subWindowMap.set(name, subWin);
return subWin;
} catch (err) {
console.error(`创建子窗口[${name}]失败: ` + JSON.stringify(err));
return null;
}
}
/**
* 销毁子窗口
*/
static async destroySubWindow(name: string): Promise<void> {
try {
let subWin = WindowUtil.subWindowMap.get(name);
if (subWin) {
subWin.off('windowSizeChange');
await subWin.destroyWindow();
WindowUtil.subWindowMap.delete(name);
console.info(`子窗口[${name}]已销毁`);
}
} catch (err) {
console.error(`销毁子窗口[${name}]失败: ` + JSON.stringify(err));
WindowUtil.subWindowMap.delete(name);
}
}
/**
* 销毁所有子窗口
*/
static async destroyAllSubWindows(): Promise<void> {
for (let name of WindowUtil.subWindowMap.keys()) {
await WindowUtil.destroySubWindow(name);
}
}
/**
* 设置屏幕常亮
*/
static async setKeepScreenOn(mainWindow: window.Window, keepOn: boolean): Promise<void> {
await mainWindow.setWindowKeepScreenOn(keepOn);
}
}
UIAbility EntryAbility.ets:
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { WindowUtil } from '../utils/WindowUtil';
import { hilog } from '@kit.PerformanceAnalysisKit';
const TAG = 'EntryAbility';
export default class EntryAbility extends UIAbility {
windowStage: window.WindowStage | null = null;
mainWindow: window.Window | null = null;
onWindowStageCreate(windowStage: window.WindowStage): void {
this.windowStage = windowStage;
this.mainWindow = windowStage.getMainWindowSync();
// 设置沉浸式
WindowUtil.setImmersiveMode(this.mainWindow);
// 初始化安全区域信息
let insets = WindowUtil.getSafeAreaInsets(this.mainWindow);
AppStorage.setOrCreate('safeAreaTop', insets.top);
AppStorage.setOrCreate('safeAreaBottom', insets.bottom);
// 监听安全区域变化
this.mainWindow.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
AppStorage.setOrCreate('safeAreaTop', px2vp(data.area.topRect.height));
AppStorage.setOrCreate('safeAreaBottom', px2vp(data.area.bottomRect.height));
}
});
// 监听窗口尺寸变化
this.mainWindow.on('windowSizeChange', (size: window.Size) => {
AppStorage.setOrCreate('windowWidth', px2vp(size.width));
AppStorage.setOrCreate('windowHeight', px2vp(size.height));
});
// 将windowStage和mainWindow存入全局,供页面使用
AppStorage.setOrCreate<window.WindowStage>('windowStage', windowStage);
AppStorage.setOrCreate<window.Window>('mainWindow', this.mainWindow);
// 加载主页
windowStage.loadContent('pages/Index');
}
onWindowStageDestroy(): void {
// 清理所有子窗口
WindowUtil.destroyAllSubWindows();
// 取消监听
if (this.mainWindow) {
this.mainWindow.off('windowSizeChange');
this.mainWindow.off('avoidAreaChange');
}
}
onDestroy(): void {
hilog.info(0x0000, TAG, 'onDestroy');
}
}
播放页面中创建悬浮控制面板:
// pages/PlayerPage.ets
import { window } from '@kit.ArkUI';
import { WindowUtil } from '../utils/WindowUtil';
@Entry
@Component
struct PlayerPage {
@StorageProp('windowStage') windowStage: window.WindowStage | null = null;
@StorageProp('mainWindow') mainWindow: window.Window | null = null;
@State isControlVisible: boolean = false;
async aboutToAppear(): Promise<void> {
if (this.mainWindow) {
// 进入播放页:横屏 + 屏幕常亮
await WindowUtil.setOrientation(this.mainWindow, window.Orientation.LANDSCAPE);
await WindowUtil.setKeepScreenOn(this.mainWindow, true);
}
}
async aboutToDisappear(): Promise<void> {
// 退出播放页:恢复竖屏 + 取消常亮 + 销毁控制面板
if (this.mainWindow) {
await WindowUtil.setOrientation(this.mainWindow, window.Orientation.PORTRAIT);
await WindowUtil.setKeepScreenOn(this.mainWindow, false);
}
await WindowUtil.destroySubWindow('floatingControl');
}
build() {
Stack() {
// 视频播放区域(这里用黑色背景模拟)
Column()
.width('100%')
.height('100%')
.backgroundColor('#000000')
// 点击屏幕显示/隐藏控制面板
Column()
.width('100%')
.height('100%')
.onClick(async () => {
if (this.isControlVisible) {
await WindowUtil.destroySubWindow('floatingControl');
this.isControlVisible = false;
} else {
await this.showFloatingControl();
this.isControlVisible = true;
}
})
}
}
private async showFloatingControl(): Promise<void> {
if (!this.windowStage) return;
// 在屏幕底部创建控制面板
let windowWidth = AppStorage.get<number>('windowWidth') || 400;
let windowHeight = AppStorage.get<number>('windowHeight') || 300;
let panelWidth = windowWidth * 0.8;
let panelHeight = 120;
let panelX = (windowWidth - panelWidth) / 2;
let panelY = windowHeight - panelHeight - 40;
await WindowUtil.createSubWindow(
this.windowStage,
'floatingControl',
'pages/FloatingControl',
{ x: panelX, y: panelY, width: panelWidth, height: panelHeight }
);
}
}
悬浮控制面板页面 FloatingControl.ets:
@Entry
@Component
struct FloatingControl {
@State progress: number = 35;
@State volume: number = 70;
build() {
Column() {
// 播放进度
Row() {
Text('03:25')
.fontSize(12)
.fontColor('#FFFFFF')
Slider({ value: this.progress, min: 0, max: 100 })
.layoutWeight(1)
.margin({ left: 8, right: 8 })
.trackColor('#66FFFFFF')
.selectedColor('#3B82F6')
.onChange((value: number) => {
this.progress = value;
})
Text('10:00')
.fontSize(12)
.fontColor('#FFFFFF')
}
.width('100%')
.padding({ left: 16, right: 16 })
// 控制按钮行
Row() {
Image($r('app.media.ic_previous'))
.width(28).height(28).fillColor('#FFFFFF')
Image($r('app.media.ic_pause'))
.width(36).height(36).fillColor('#FFFFFF')
.margin({ left: 24, right: 24 })
Image($r('app.media.ic_next'))
.width(28).height(28).fillColor('#FFFFFF')
Blank()
// 音量控制
Image($r('app.media.ic_volume'))
.width(24).height(24).fillColor('#FFFFFF')
Slider({ value: this.volume, min: 0, max: 100 })
.width(80)
.margin({ left: 4 })
.trackColor('#66FFFFFF')
.selectedColor('#FFFFFF')
.onChange((value: number) => {
this.volume = value;
})
}
.width('100%')
.padding({ left: 16, right: 16, top: 8 })
}
.width('100%')
.height('100%')
.backgroundColor('#CC1A1A1A')
.borderRadius(16)
.justifyContent(FlexAlign.Center)
}
}
这个项目把主窗口操作(全屏、方向、常亮)和子窗口操作(创建、显示、销毁悬浮面板)串联在了一起,是一个非常典型的窗口开发实战案例。
第九章:窗口开发的未来——HarmonyOS 6.0之后还有什么?
9.1 跨设备窗口流转
HarmonyOS最令人兴奋的能力之一是跨设备流转。想象一下——你在手机上看视频,走到客厅的智慧屏前,视频窗口自动"流转"到大屏上,你的手机变成遥控器。
这背后涉及的就是窗口在不同设备之间的迁移和协同。虽然应用层的窗口API目前主要聚焦在单设备上,但华为已经在系统层面做了大量底层准备。未来,跨设备的窗口管理API很可能会逐步开放给开发者。
9.2 自由窗口模式的成熟
在PC(2in1设备)和平板上,自由窗口模式会越来越主流。用户可以像操作Windows窗口一样拖拽、调整HarmonyOS App的窗口大小。
这对开发者的布局能力提出了更高的要求——你的App不能只适配几种固定尺寸,而要能在任意宽高比下都能正常工作。响应式布局不再是"加分项",而是"必须项"。
9.3 窗口与AI的结合
随着端侧大模型的发展,未来的智能助手可能会以"悬浮窗口"的形式常驻在系统中——用户随时可以召唤它、跟它对话、让它帮忙操作其他App。
这种"AI悬浮助手"的实现,底层就是子窗口技术。如果你掌握了窗口开发,你就有能力参与这一波AI+OS融合的浪潮。
结语:窗口虽小,乾坤很大
写到这里,一万多字了。
回头看看,窗口开发这个话题,表面上看就是"创建窗口、设置属性、加载页面"这几件事。但真正深入下去,你会发现它牵扯到——
-
应用模型(Stage模型的UIAbility + WindowStage)
-
生命周期管理(创建、前后台切换、销毁)
-
布局适配(安全区域、沉浸式、响应式)
-
多窗口管理(子窗口创建、焦点、触摸、销毁)
-
多设备适配(折叠屏、平板、PC)
-
系统交互(状态栏、导航栏、屏幕方向、亮度)
每一个展开都是一个技术深度。
我一直觉得,**做开发最怕的不是"不会",而是"以为自己会了"**。
很多人写了几年代码,窗口相关的API也用了不少,但从来没有系统地梳理过"窗口到底是什么、有哪些类型、它们之间什么关系、背后的设计逻辑是什么"。遇到问题就搜一搜、抄一抄,解决了就忘了。下次遇到类似的问题,还是得从头搜。
这篇文章就是要帮你把这件事一次性搞通。
从概念到代码,从单设备到多设备,从主窗口到子窗口,从基础操作到实战项目——读完这一篇,你对HarmonyOS 6.0的窗口开发应该有了一个完整、清晰、可落地的认知框架。
以后遇到任何窗口相关的问题,你都知道它属于哪个层面、该用什么API、要注意哪些坑。
这就是体系化学习的价值。
我是小邢哥,14年编程老炮。 拆解技术脉络,记录程序员的进化史。
如果这篇文章帮到了你,点个赞、收藏一下——你的支持是我继续写的动力。
下一篇,我们聊聊HarmonyOS 6.0的通知与后台任务。
我们下篇见 ✌️
更多推荐



所有评论(0)