《悬浮工具球》一、窗口开发概述使用指南
HarmonyOS窗口开发概述使用指南:从零到一掌握应用窗口管理全流程
适用版本:HarmonyOS NEXT(API 23+)/ DevEco Studio 6.1+
关键词:窗口开发、WindowStage、子窗口、全屏布局、安全区避让、沉浸式体验
效果
一、前言:为什么需要理解窗口开发?
在HarmonyOS应用开发中,窗口(Window) 是用户与应用交互的第一入口。无论是应用的主界面展示、弹窗提示、悬浮工具球,还是沉浸式全屏体验,都离不开窗口模块的支撑。
很多开发者在接触HarmonyOS窗口开发时,会遇到以下困惑:
- 主窗口和窗口有什么区别?什么时候用子窗口?
- 全屏布局和沉浸式开发到底怎么配置?
- 安全区域(状态栏、导航条)怎么避让?
- 窗口生命周期如何与应用生命周期关联?
本文基于HarmonyOS官方文档,结合实战开发经验,系统梳理窗口开发的核心概念、关键API和典型使用场景,并通过一个完整示例带你快速上手。
二、核心概念:窗口体系全景图
2.1 窗口的分类
HarmonyOS中的窗口分为两大类:
| 窗口类型 | 说明 | 典型场景 |
|---|---|---|
| 系统窗口 | 由系统创建和管理 | 状态栏、导航条、输入法面板、音量调节 |
| 应用窗口 | 由应用创建,用于显示应用界面 | 主页面、弹窗、悬浮球、画中画 |
应用窗口进一步分为:
| 子类型 | 特点 | 生命周期 |
|---|---|---|
| 应用主窗口 | 显示应用主界面,在任务管理界面可见 | 跟随UIAbility |
| 应用子窗口 | 弹窗、悬浮球等辅助窗口,不在任务管理界面显示 | 跟随应用主窗口 |
关键理解:子窗口的生命周期跟随主窗口,主窗口销毁时子窗口也会自动销毁。
2.2 窗口模式
HarmonyOS支持三种应用窗口模式:
| 模式 | 说明 | 适用设备 |
|---|---|---|
| 全屏模式 | 主窗口铺满整个屏幕 | 手机、平板(默认) |
| 分屏模式 | 多个应用并排显示 | 平板、折叠屏 |
| 自由窗口模式 | 窗口可自由调整大小和位置,类似PC窗口 | PC、2in1设备 |
2.3 核心对象关系
在Stage模型下,窗口开发涉及三个核心对象,它们的关系层层递进:
UIAbility(应用入口)
└── WindowStage(窗口管理器)
├── 主窗口 Window(应用主界面)
└── 子窗口 Window(弹窗、悬浮球等)
- UIAbility:应用入口,一个UIAbility实例对应一个WindowStage
- WindowStage:窗口管理器,负责创建和管理窗口
- Window:具体窗口实例,提供窗口属性设置、位置移动、尺寸调整等能力
三、窗口生命周期
理解窗口生命周期是正确开发窗口基础。在Stage模型下,窗口的生命周期与UIAbility紧密关联:
UIAbility.onCreate()
↓
UIAbility.onWindowStageCreate(windowStage) ← 窗口创建,加载主页面
↓
[应用运行中,窗口可见]
↓
UIAbility.onWindowStageDestroy() ← 窗口即将销毁,释放资源
↓
UIAbility.onDestroy() ← Ability销毁
关键生命周期回调
| 回调方法 | 触发时机 | 典型操作 |
|---|---|---|
onWindowStageCreate |
WindowStage创建完成 | 加载主页面、创建子窗口、设置全屏 |
onWindowStageDestroy |
WindowStage即将销毁 | 释放子窗口、清理窗口资源 |
onForeground |
应用切到前台 | 恢复窗口状态 |
onBackground |
应用切到后台 | 保存窗口状态 |
监听窗口生命周期事件
还可以通过on('windowStageEvent')监听WindowStage的生命周期变化:
windowStage.on('windowStageEvent', (eventType: window.WindowStageEventType) => {
// eventType 包括:
// SHOWN - 窗口显示
// HIDDEN - 窗口隐藏
// ACTIVE - 窗口获得焦点
// INACTIVE - 窗口失去焦点
switch (eventType) {
case window.WindowStageEventType.SHOWN:
console.info('窗口已显示');
break;
case window.WindowStageEventType.ACTIVE:
console.info('窗口获得焦点');
break;
}
});
四、关键开发场景与API使用
4.1 加载主页面
在onWindowStageCreate中,通过loadContent为主窗口加载页面:
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('加载页面失败:', JSON.stringify(err));
return;
}
console.info('页面加载成功');
});
}
注意:
loadContent是异步方法,但AppStorage.setOrCreate('windowStage', windowStage)等全局数据的设置必须在loadContent之前完成,否则组件初始化时无法读取到全局数据。获取主窗口、设置全屏等操作可在loadContent回调中执行。
4.2 获取主窗口并设置全屏
windowStage.loadContent('pages/Index', (err) => {
if (err.code) return;
// 获取主窗口实例
const mainWindow: window.Window = windowStage.getMainWindowSync();
// 设置全屏布局
mainWindow.setWindowLayoutFullScreen(true).then(() => {
console.info('全屏布局设置成功');
}).catch((err: BusinessError) => {
console.error('全屏布局设置失败:', JSON.stringify(err));
});
});
4.3 安全区域避让
全屏布局下,内容会被状态栏和导航条遮挡。需要获取避让区域并动态调整布局:
// 获取状态栏高度(顶部安全区)
const sysArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
const topInset = sysArea.topRect.height; // 单位:px
// 获取导航条高度(底部安全区)
const navArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
const bottomInset = navArea.bottomRect.height; // 单位:px
// 保存到全局,供页面使用
AppStorage.setOrCreate('topInset', topInset);
AppStorage.setOrCreate('bottomInset', bottomInset);
动态监听安全区变化(如折叠屏展开、横竖屏切换):
mainWindow.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
AppStorage.setOrCreate('topInset', data.area.topRect.height);
} else if (data.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
AppStorage.setOrCreate('bottomInset', data.area.bottomRect.height);
}
});
4.4 创建子窗口
子窗口是悬浮球、弹窗等场景的核心载体:
// 通过WindowStage创建子窗口
windowStage.createSubWindow('mySubWindow', (err, subWin) => {
if (err.code > 0) {
console.error('创建子窗口失败:', err.message);
return;
}
// 设置子窗口页面
subWin.setUIContent('subwindow/SubPage');
// 设置位置(单位:px)
subWin.moveWindowTo(500, 1000);
// 设置大小(单位:px,需vp2px转换)
subWin.resize(200, 200);
// 设置背景透明
subWin.setWindowBackgroundColor('#00000000');
// 显示子窗口
subWin.showWindow();
});
4.5 窗口焦点切换
当子窗口需要操作后把焦点还给主窗口:
// 获取子窗口ID
const subWinId = window.findWindow('mySubWindow').getWindowProperties().id;
// 获取主窗口ID
const mainWinId = windowStage.getMainWindowSync().getWindowProperties().id;
// 焦点从子窗口转移到主窗口
window.shiftAppWindowFocus(subWinId, mainWinId);
五、完整示例:从零搭建带子窗口的全屏应用
下面通过一个完整的示例,演示窗口开发的核心流程。
5.1 工程结构
entry/src/main/ets/
├── entryability/
│ └── EntryAbility.ets // 应用入口,配置窗口
├── pages/
│ └── Index.ets // 主页面
└── subwindow/
└── SubPage.ets // 子窗口页面
5.2 EntryAbility — 窗口配置入口
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// 1. 在loadContent之前设置AppStorage,确保组件初始化时可读取
AppStorage.setOrCreate('windowStage', windowStage);
// 2. 获取主窗口,配置全屏
const win = windowStage.getMainWindowSync();
win.setWindowLayoutFullScreen(true);
// 3. 获取安全区域
const topArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
AppStorage.setOrCreate('topInset', topArea.topRect.height);
const bottomArea = win.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
AppStorage.setOrCreate('bottomInset', bottomArea.bottomRect.height);
// 4. 监听安全区变化
win.on('avoidAreaChange', (data) => {
if (data.type === window.AvoidAreaType.TYPE_SYSTEM) {
AppStorage.setOrCreate('topInset', data.area.topRect.height);
} else if (data.type === window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
AppStorage.setOrCreate('bottomInset', data.area.bottomRect.height);
}
});
// 5. 加载主页面(此时AppStorage已就绪)
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
console.error('加载失败:', JSON.stringify(err));
return;
}
console.info('页面加载成功');
});
}
}
5.3 Index — 主页面(加载子窗口)
import { window } from '@kit.ArkUI';
@Entry
@ComponentV2
struct Index {
private windowStage: window.WindowStage = AppStorage.get('windowStage') as window.WindowStage;
aboutToAppear(): void {
this.createSubWindow();
}
createSubWindow(): void {
this.windowStage.createSubWindow('demoSubWin', (err, win) => {
if (err.code > 0) return;
win.setUIContent('subwindow/SubPage');
win.setWindowBackgroundColor('#00000000');
win.moveWindowTo(800, 1500);
win.resize(this.getUIContext().vp2px(60), this.getUIContext().vp2px(60));
win.showWindow();
});
}
build() {
Column() {
Text('主页面内容')
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
5.4 SubPage — 子窗口页面
@Entry
@ComponentV2
struct SubPage {
build() {
Column() {
Text('Hi')
.fontSize(20)
.fontColor('#FFFFFF')
}
.width('100%')
.height('100%')
.backgroundColor('#4A90D9')
.borderRadius(30)
.justifyContent(FlexAlign.Center)
}
}
5.5 路由配置
在 main_pages.json 中注册子窗口页面:
{
"src": [
"pages/Index",
"subwindow/SubPage"
]
}
六、开发注意事项与最佳实践
6.1 AppStorage全局数据时序
这是最容易踩坑的点。AppStorage.setOrCreate必须在loadContent之前调用,否则组件初始化时@Local变量读取AppStorage.get()将返回undefined:
// ❌ 错误写法:在loadContent回调中设置AppStorage
windowStage.loadContent('pages/Index', (err) => {
if (err.code) return;
AppStorage.setOrCreate('windowStage', windowStage); // 组件已初始化,读取为undefined!
});
// ✅ 正确写法:在loadContent之前设置AppStorage
AppStorage.setOrCreate('windowStage', windowStage);
windowStage.loadContent('pages/Index', (err) => {
if (err.code) return;
});
6.2 异步操作时序
loadContent和getMainWindowSync都是异步相关操作,确保在loadContent回调中再执行依赖页面加载完成的窗口操作:
// ❌ 错误写法:loadContent和getMainWindowSync并行调用
windowStage.loadContent('pages/Index');
const win = windowStage.getMainWindowSync(); // 可能获取到未加载完成的窗口
// ✅ 正确写法:在loadContent回调中操作
windowStage.loadContent('pages/Index', (err) => {
if (err.code) return;
const win = windowStage.getMainWindowSync();
win.setWindowLayoutFullScreen(true);
});
6.3 单位转换
窗口API(moveWindowTo、resize等)的参数单位为物理像素(px),而ArkUI组件的属性单位为虚拟像素(vp)。必须使用vp2px进行转换:
const sizeVp = 60; // vp
const sizePx = this.getUIContext().vp2px(sizeVp); // 转换为px
win.resize(sizePx, sizePx);
6.4 子窗口页面注册
子窗口加载的页面路径必须在main_pages.json中注册,否则setUIContent会报错:
{
"src": [
"pages/Index",
"subwindow/SubPage"
]
}
6.5 窗口背景透明
子窗口默认有白色背景,如果需要透明效果(如悬浮球),必须设置背景色为透明:
win.setWindowBackgroundColor('#00000000');
6.6 资源释放
在onWindowStageDestroy中释放子窗口资源,避免内存泄漏:
onWindowStageDestroy(): void {
// 销毁子窗口
const subWin = window.findWindow('mySubWindow');
if (subWin) {
subWin.destroyWindow();
}
}
七、常见问题FAQ
Q1:子窗口创建失败,报错code 1300005?
这通常是因为窗口名称重复。每个子窗口的名称必须唯一,不能创建两个同名子窗口。
Q2:全屏设置后内容被状态栏遮挡?
全屏布局下需要手动获取安全区域并设置padding。参考本文"4.3 安全区域避让"章节。
Q3:子窗口拖拽不跟手?
确保使用物理像素(px)单位进行窗口移动操作,并在手势回调中直接更新窗口位置,避免异步查询窗口属性导致延迟。
Q4:如何在子窗口中访问主窗口的数据?
通过AppStorage进行全局数据共享。在主页面的aboutToAppear中将需要共享的数据存入AppStorage,在子窗口中通过AppStorage.get()获取。
八、总结
HarmonyOS窗口开发的核心知识点:
| 知识点 | 要点 |
|---|---|
| 核心对象 | UIAbility → WindowStage → Window,层层递进 |
| 主窗口配置 | loadContent加载页面 → getMainWindowSync获取窗口 → setWindowLayoutFullScreen全屏 |
| 安全区避让 | getWindowAvoidArea获取避让区域 + on(‘avoidAreaChange’)动态监听 |
| 子窗口创建 | createSubWindow → setUIContent → moveWindowTo → resize → showWindow |
| 单位转换 | 窗口API使用px,组件使用vp,用vp2px/px2vp转换 |
| 时序保障 | 窗口操作放在loadContent回调中,避免并行调用 |
掌握以上核心内容,你就具备了HarmonyOS窗口开发的基础能力。在此基础上,可以进一步探索悬浮工具球、画中画、自由窗口等高级场景。
参考文档:
更多推荐




所有评论(0)