Administrator
发布于 2026-09-03 / 3,666 阅读
0
0

鸿蒙多窗口开发:主/子/悬浮窗与 WindowStage 生命周期

鸿蒙百篇系列第 65 篇 · 高级

大多数鸿蒙开发者一辈子只写过 entry/pages/Index.ets,对窗口的理解就是"一个全屏界面"。但当你想做悬浮歌词、自由窗口的小窗模式、聊天应用的多开、企业级多任务,或者车机/平板上的分屏体验时,WindowStage + Window + SubWindow 这一套机制就必须搞清楚。这一篇从 WindowStage 的生命周期开始,到悬浮窗的工程约束结束,把窗口这个被低估的子系统讲透。

一、UIAbility、WindowStage、Window 三层关系

Stage 模型下,每个 UIAbility 都对应一个 WindowStage,每个 WindowStage 上挂 1 + N 个 Window:

UIAbility
└── WindowStage
    ├── MainWindow (有且仅有一个)
    └── SubWindow ... (子窗口,可多个)
        ├── Popup
        ├── Custom Dialog
        └── 全局悬浮窗 (FloatWindow, 受权限管控)

WindowStage 跟 UIAbility 一样有生命周期:

import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 主窗口创建
    windowStage.getMainWindow((err, mainWindow) => {
      // ...
    });
    // 加载主页面
    windowStage.loadContent('pages/Index', (err) => { /* ... */ });
  }

  onWindowStageDestroy(): void {
    // 资源回收
  }

  onWindowStageWillDestroy(): void {
    // 确认能销毁前的最后机会
  }
}

W3C 的 Window 事件名跟传统桌面有点不一样。Stage 模型下监听到的事件是这些:

事件

含义

触发现场

SHOWN

主窗进入前台

全屏启动、悬浮窗拉起

RESUMED

主窗进入可交互

上滑悬停返回应用

PAUSED

主窗进入不可交互

退到后台、系统管控

HIDDEN

主窗完全进入后台

上滑返回桌面

FOREGROUND

从后台恢复

切回前台

订阅方式(API 20 之前/之后有区别):

windowStage.on('windowStageEvent', (event: window.WindowStageEventType) => {
  switch (event) {
    case window.WindowStageEventType.PAUSED:
      this.pauseAudio();
      break;
    case window.WindowStageEventType.RESUMED:
      this.resumeAudio();
      break;
    case window.WindowStageEventType.HIDDEN:
      this.flushState();
      break;
  }
});

二、主窗口的常用属性

主窗口是用户最先看到的,所有"应用对系统"的属性都在主窗口上设置:

const mainWindow = await windowStage.getMainWindow();

await mainWindow.setWindowBackgroundColor('#FFFFFF');  // 窗口未渲染时的占位色
await mainWindow.setWindowBrightness(0.5);             // 亮度覆盖 0.0-1.0
await mainWindow.setWindowTouchable(true);
await mainWindow.setWindowKeepScreenOn(true);          // 播放视频时常用
await mainWindow.setResizeByScrollEnabled(true);        // 自由窗口模式
await mainWindow.setWindowSystemBarEnable(['status', 'navigation']); // 显示系统栏

主窗口最常用的就是"沉浸式 + 系统栏样式"。沉浸式代码:

// 方式一:setWindowSystemBarEnable 隐藏
await mainWindow.setWindowSystemBarEnable([]);

// 方式二:expand 区域
await mainWindow.setWindowLayoutArea({
  topRect: { start: 0, end: 80 },     // 状态栏高度
  bottomRect: { start: 2200, end: 2360 } // 导航栏高度
});

沉浸式一定要小心 —— 用户拿不到状态栏反而会迷路。我的经验是"沉浸式 + 提供呼出按钮"。

三、子窗口:弹窗、菜单、自定义控件

子窗口的常见用法:

async function showPlayerWidget() {
  const subWindow = await windowStage.createSubWindow('player_widget');
  await subWindow.moveWindowTo(20, 200);
  await subWindow.resize(200, 60);
  await subWindow.setWindowBackgroundColor('#1A1A1A');
  await subWindow.setUIContent('pages/PlayerWidget');
  await subWindow.showWindow();
}

为什么不直接用 @Component + 半透明层?因为:

  • 子窗口可以独立拖拽移动

  • 子窗口的层级由系统管理,永远浮在主窗口之上

  • 子窗口关闭不会卸载主界面

  • 子窗口的 z 轴位置稳定,不会被遮挡

下面是做一个"全局迷你播放器"的完整片段:

import window from '@ohos.window';

class MiniPlayerWindow {
  private subWindow: window.Window | null = null;

  async show(stage: window.WindowStage) {
    if (this.subWindow !== null) return;

    this.subWindow = await new Promise((resolve, reject) => {
      stage.createSubWindow('mini_player', (err, w) => {
        if (err) reject(err); else resolve(w);
      });
    });

    await this.subWindow.moveWindowTo(20, 100);
    await this.subWindow.resize(200, 60);
    await this.subWindow.setWindowBackgroundColor('#CC000000');
    await this.subWindow.setUIContent('pages/MiniPlayer');
    await this.subWindow.showWindow();
  }

  async hide() {
    if (this.subWindow === null) return;
    await this.subWindow.destroyWindow();
    this.subWindow = null;
  }
}

子窗口销毁不能忘。每开一个 createSubWindow 都得对应一个 destroyWindow,否则累积的子窗口会让系统变卡。实战中子窗口管理要写到 class 里,把"打开/关闭"配成对的方法。

四、悬浮窗:受限开放的能力

悬浮窗(FloatWindow)跟子窗口的关键差别是"应用退到后台时仍然可见"。常见场景:

  • 音乐 App 的"桌面歌词"

  • 计时器、时钟的"常驻浮窗"

  • 录屏工具的"操作录屏窗口"

悬浮窗需要权限:

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.SYSTEM_FLOAT_WINDOW",
        "reason": "用于在桌面显示常驻播放控件",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      }
    ]
  }
}

注意 ohos.permission.SYSTEM_FLOAT_WINDOW 不是普通权限,AGC 上需要走 ACL 申请(普通应用不一定能过)。普通开发需求可以用 setWindowSystemBarEnable 配合子窗口做"伪悬浮",但应用退出后台后会被回收。

代码层面:

// API 12+ 推荐接口
const floatWin = await window.createWindow({
  name: 'float_clock',
  windowType: window.WindowType.TYPE_FLOAT,
  ctx: this.context
});

await floatWin.moveWindowTo(20, 100);
await floatWin.resize(120, 60);
await floatWin.setUIContent('pages/FloatClock');
await floatWin.showWindow();

floatWin.on('touchOutside', () => {
  floatWin.hideWindow();
});

API 12 之前的代码用 window.create() + WindowType.FLOAT,逻辑一致,但 create 的方式略有不同。

五、自由窗口:PC、平板、折叠屏

折叠屏、平板、PC 设备的窗口通常是"自由窗口"——可以拖拽、缩放。Stage 模型默认会跑全屏,但 module.json5 里可以打开:

{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "supportWindowMode": ["split", "free"]
      }
    ]
  }
}

三种窗口模式对比:

模式

描述

用户体验

全屏

默认,铺满屏幕

手机、平板首选

分屏 split

上半/下半

视频+笔记、电话+聊天

自由 free

任何位置、大小可调

PC、折叠屏

代码侧其实不需要特别处理 —— 系统会把当前 window 状态广播给应用。但布局上一定要做"响应式":

@StorageLink('windowSize') windowSize: Size = { width: 0, height: 0 };

aboutToAppear() {
  window.getLastWindow(getContext(this)).then(win => {
    win.on('windowSizeChange', size => {
      this.windowSize = size;
    });
  });
}

六、跨设备窗口:分布式流转

如果你的应用支持"接续"或"跨设备迁移",窗口会"跟着"任务迁移。这是 Stage 模型下 onContinue / onNewWant 配合 Want 的能力:

onContinue(want: Want, options): AbilityConstant.ContinueState {
  // 把当前页面序列化到 want.parameters
  want.parameters = {
    page: 'pages/DetailPage',
    bundle: 'com.example.app',
    deepLink: '/article/123'
  };
  return AbilityConstant.ContinueState.AGREE;
}

接收方在新设备的 WindowStage 上恢复:

onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
  const page = want.parameters?.['page'] as string;
  // 加载新页面
  if (page) {
    this.windowStage.loadContent(page, () => {});
  }
}

这是分布式流转最简版。真正的"任务迁移"还会包含 UI 状态、网络会话、媒体播放位置,需要额外的 Session Kit。

七、几个坑

  1. 子窗口生命周期 ≠ 主窗口。主窗口销毁时子窗口会被强制销毁,但你主动销毁主窗口,子窗口未必自动销毁。要在 onWindowStageDestroy 里清理

  2. 悬浮窗权限的申请时点。不能在 UI 渲染完之后申请,会被拒。应该按"需求驱动"原则,要用的时候再申请

  3. setUIContent 是异步的。多次调用之间的状态不保证,"先 resize 再 setUIContent 再 show"的顺序对,但中间出错就难调

  4. back 键和子窗口。未指定路由的情况下,按 back 关闭最后一个显示的子窗口,否则返回桌面

  5. 自由窗口的 Size 是物理像素。做自适应布局时记得除以 vp 系数

八、性能与调试建议

  • 频繁创建/销毁子窗口是高开销。复用子窗口 + 切内容更划算

  • setResizeByScrollEnabled 打开后要监听 resize 事件以重排布局,否则会出现"留白"

  • 调试时打开 DevEco Studio 的 Window Inspector,可以看到真实窗口树,比 logcat 直观

九、和前几篇的衔接

  • 第 4 篇讲的页面路由是"在主窗口里导航"。这一篇讲"在主窗口外开辟新窗口"

  • 第 9 篇分布式里提到"跨设备迁移",本篇是用窗口 API 接的实现

  • 第 60 篇里提到的 H5 容器,弹窗逻辑也是建在子窗口上的

十、小结

窗口子系统是鸿蒙里被低估的能力。从主窗口到子窗口,再到悬浮窗、自由窗口、跨设备流转,每一层都有自己的适用场景。理解 WindowStage 生命周期、子窗口管理、悬浮窗权限边界,可以让你在做大屏、Pad、PC 应用时少走很多弯路。

下一篇进入图形栈与 Surface,把第 61 篇 RenderNode 之后那一层——XComponent 与 EGL 自绘——讲清楚。


评论