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 异步操作时序

loadContentgetMainWindowSync都是异步相关操作,确保在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(moveWindowToresize等)的参数单位为物理像素(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窗口开发的基础能力。在此基础上,可以进一步探索悬浮工具球、画中画、自由窗口等高级场景。


参考文档

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐