《HarmonyOS技术精讲-窗口管理》第一篇:窗口基础概念与WindowStage

窗口管理到底是什么

HarmonyOS NEXT开发中,窗口管理是一个容易被忽略但影响面很广的话题。很多初学者在搭建第一个页面时,直接在@Entry装饰的组件里写UI,确实能跑,但对"窗口"这个概念没有清晰认知。

等到了需要做多任务管理、悬浮窗口、分屏适配、应用间交互时,问题就来了------窗口怎么创建?生命周期怎么管控?为什么有时候页面显示不出来?这些问题都指向同一个核心概念:窗口管理

简单说,窗口是界面显示的容器。你在手机上看到的所有内容,不管是应用主界面、弹窗还是悬浮球,最终都被绘制在某个窗口上。HarmonyOS的窗口系统不是简单的视图层次,它有明确的分层结构和生命周期管理机制,WindowStage就是这个机制的入口。

窗口系统的分层结构

HarmonyOS的窗口系统分三层:

层级 作用 常见场景
应用窗口 承载应用主界面 首页、详情页
系统窗口 系统级UI元素 状态栏、导航栏
悬浮窗口 漂浮在其他窗口之上 悬浮球、Toast提示

实际开发中,开发者主要操作的是应用窗口悬浮窗口。应用窗口又分为主窗口和子窗口,主窗口对应应用的主界面,子窗口可以用于模态弹窗等场景。

窗口的分层顺序决定了渲染叠放关系。上层窗口会覆盖下层窗口,这个顺序由窗口类型和创建顺序共同决定。理解这一点,才能正确管理悬浮窗口和弹窗的显示层级。

WindowStage的核心角色

WindowStage是窗口生命周期的管理者。每个UIAbility实例都对应一个WindowStage对象。它的生命周期和UIAbility紧密绑定,主要负责三件事:

  1. 创建和绑定主窗口
  2. 配置窗口属性(大小、位置、可触摸性等)
  3. 管理窗口生命周期(创建、显示、隐藏、销毁)

真正的开发中,90%的窗口操作都是在WindowStage的回调中完成的。官方文档提到onWindowStageCreate回调,但这个回调的触发时机和限制条件,不少人都理解得不够准确。

获取WindowStage并创建主窗口

在UIAbility中获取WindowStage的方式很直接。UIAbility的onCreate方法会传入WindowStage实例:

typescript 复制代码
// Ability.ts
import UIAbility from '@ohos.app.ability.UIAbility';
import Window from '@ohos.window';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: Window.WindowStage): void {
    console.info('WindowStage创建成功');
    
    // 加载主页面
    windowStage.loadContent('pages/Index', (err, data) => {
      if (err.code) {
        console.error('页面加载失败,错误码: ' + err.code);
        return;
      }
      console.info('页面加载成功');
    });
  }

  onWindowStageDestroy(): void {
    console.info('WindowStage销毁');
  }
}

这段代码的核心是loadContent方法。它把指定的页面文件加载到主窗口中。注意这里有个常见误区:loadContent是异步操作,不能假设加载完成后立即可以获取窗口实例。

等页面加载完成,可以通过getMainWindow获取窗口实例,然后对窗口进行属性配置:

typescript 复制代码
// Ability.ts
export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: Window.WindowStage): void {
    windowStage.loadContent('pages/Index', async () => {
      try {
        // 获取主窗口实例
        let mainWindow = await windowStage.getMainWindow();
        
        // 设置窗口属性
        await mainWindow.setWindowLayoutFullScreen(true); // 全屏布局
        mainWindow.setWindowBackgroundColor('#FFFFFF');    // 设置背景色
        
        // 获得窗口的宽高
        let properties = await mainWindow.getWindowProperties();
        console.info('窗口宽度: ' + properties.windowRect.width);
      } catch (error) {
        console.error('窗口操作失败: ' + error.code);
      }
    });
  }
}

getMainWindow返回的是Window实例,通过它可以控制窗口的显示属性、事件监听等。注意点 :这个方法必须在loadContent之后调用,否则返回的窗口可能还没有完全初始化。

窗口生命周期的实际行为

窗口生命周期并非只有'onWindowStageCreate'和'onWindowStageDestroy'两个节点。实际运行中,还有一些容易被忽略的状态变化:

窗口可见性变化 :应用切换到后台时,窗口并没有销毁,只是变为不可见。这时候onWindowStageCreate不会再次触发。官方文档中没有单独说明窗口可见性变化的回调,需要开发者通过window.on('windowVisibility')监听:

typescript 复制代码
async function initVisibilityListener(windowStage: Window.WindowStage) {
  try {
    let mainWindow = await windowStage.getMainWindow();
    mainWindow.on('windowVisibility', (data) => {
      console.info('窗口可见性变化: ' + data.visible);
    });
  } catch (error) {
    console.error('设置可见性监听失败');
  }
}

窗口大小变化:分屏、旋转屏幕都会触发窗口大小变化。如果不监听窗口大小变化事件,界面布局可能不会自适应:

typescript 复制代码
mainWindow.on('windowSizeChange', (data) => {
  console.info('窗口大小变化为: ' + data.width + 'x' + data.height);
  // 通知UI组件更新布局
});

这两个事件是窗口管理中最常用的监听回调,但很多初学者只关注了创建和销毁,忽略了这两个。

常见问题

问题1:onWindowStageCreate为什么只执行一次?

现象 :应用切换到后台再切回来,onWindowStageCreate不再触发,导致部分初始化逻辑没执行。

原因onWindowStageCreate只在WindowStage首次创建时执行。应用进入后台时,WindowStage处于ACTIVE状态但不可见,切换到前台时不会重新创建。这符合正常的生命周期设计,但容易被误以为需要重新初始化。

解决方案 :把需要重复执行的逻辑放在页面的onPageShow回调中,或者监听窗口可见性变化事件:

typescript 复制代码
@Entry
@Component
struct Index {
  onPageShow(): void {
    console.info('页面显示,执行初始化');
    // 重新加载数据或刷新UI
  }
}

问题2:窗口属性设置不及时导致页面显示异常

现象:某些设备上,页面加载后窗口大小或全屏状态没有立即生效,出现短暂的白边或错位。

原因setWindowLayoutFullScreen等属性设置是异步操作,可能在页面渲染完成后才生效。如果在loadContent的回调中立即设置窗口属性,由于窗口尚未完全就绪,属性设置可能被忽略。

解决方案 :在loadContent的回调中,使用await等待窗口属性设置完成:

typescript 复制代码
windowStage.loadContent('pages/Index', async () => {
  let mainWindow = await windowStage.getMainWindow();
  // 确保窗口准备就绪
  await mainWindow.setWindowLayoutFullScreen(true);
  // 再执行其他操作
});

最佳实践

  1. 不要在build()中频繁创建窗口句柄。窗口实例是重量级对象,应该在UIAbility中保存引用,避免反复获取。

  2. 窗口事件监听务必在onWindowStageDestroy中取消。否则可能导致内存泄漏或异常回调:

typescript 复制代码
onWindowStageDestroy(windowStage: Window.WindowStage): void {
  try {
    let mainWindow = await windowStage.getMainWindow();
    mainWindow.off('windowVisibility');
    mainWindow.off('windowSizeChange');
  } catch (error) {
    console.error('取消监听失败');
  }
}
  1. 将窗口配置逻辑封装到独立的类中。随着项目变大,窗口操作会涉及多处逻辑,集中管理更容易维护。推荐模式:
typescript 复制代码
// WindowManager.ts
export class WindowManager {
  private mainWindow: Window.Window | null = null;

  async init(windowStage: Window.WindowStage): void {
    this.mainWindow = await windowStage.getMainWindow();
    // 统一配置窗口
  }

  setFullScreen(): void {
    this.mainWindow?.setWindowLayoutFullScreen(true);
  }
}

Demo入口

下面是完整的页面入口示例,集成了窗口配置和事件监听:

typescript 复制代码
// pages/Index.ets
import Window from '@ohos.window';

@Entry
@Component
struct Index {
  @State windowWidth: number = 0;
  @State windowHeight: number = 0;

  build() {
    Column() {
      Text('窗口宽度: ' + this.windowWidth)
      Text('窗口高度: ' + this.windowHeight)
      Button('切换全屏')
        .onClick(() => {
          this.toggleFullScreen();
        })
    }
    .width('100%')
    .height('100%')
    .onPageShow(() => {
      this.getWindowInfo();
    })
  }

  private async getWindowInfo(): void {
    let context = getContext(this) as any;
    let mainWindow = await context.windowStage.getMainWindow();
    let properties = await mainWindow.getWindowProperties();
    this.windowWidth = properties.windowRect.width;
    this.windowHeight = properties.windowRect.height;
  }

  private async toggleFullScreen(): void {
    let context = getContext(this) as any;
    let mainWindow = await context.windowStage.getMainWindow();
    let currentState = await mainWindow.isWindowLayoutFullScreen();
    await mainWindow.setWindowLayoutFullScreen(!currentState);
  }
}

FAQ

Q:为什么真机上窗口全屏设置正常,模拟器上不生效?

A:模拟器对窗口属性的支持有限,某些属性(如全屏、沉浸式)在模拟器上会忽略。建议以真机测试为准。如果你只在模拟器上验证全屏功能,这个功能可能永远无法通过测试。

Q:创建悬浮窗口时,为什么不能通过getMainWindow获取?

A:悬浮窗口和主窗口是独立的窗口实例。getMainWindow只返回当前WindowStage关联的主窗口。创建悬浮窗口需要使用createSubWindow方法,并通过windowClass参数指定窗口类型为WindowType.TYPE_FLOAT

Q:页面返回后,之前设置的窗口属性会丢失吗?

A:不会。窗口属性是持久化的,只要WindowStage没有销毁,属性会一直保留。如果页面返回后发现窗口属性不对,多半是页面逻辑中重新设置了属性,而不是属性丢失。排查时可以检查onPageShow或构造函数中是否存在重置操作。


示例代码地址:项目地址