《HarmonyOS技术精讲-窗口管理》第二篇:创建与控制主窗口

运行效果图

1. 开篇引导

在HarmonyOS开发中,窗口管理是一个比较基础但容易踩坑的模块。很多人在刚接触window.createWindowwindow.createSubWindow时,会误以为它们功能类似,结果在真机上一跑就崩。

本篇文章专注于应用主窗口的创建与基础属性控制,包括窗口大小、位置、全屏模式。理解这些API的边界条件,比你背下参数列表更有用。

2. 基本概念与场景

什么场景需要手动管理窗口?

默认情况下,UIAbility会自动为应用创建主窗口。但以下场景需要你显式操作:

  • 自定义启动窗口大小:应用首次启动时,需要以指定非全屏尺寸显示(如悬浮工具类应用)
  • 动态切换窗口布局:从竖屏切到横屏,或普通窗口切到全屏
  • 多窗口协同:同时展示多个子窗口(在后续文章细讲)

createWindowcreateSubWindow 区别

特性 createWindow createSubWindow
用途 创建应用主窗口 创建依附于主窗口的子窗口
生命周期 独立于UIAbility生命周期 跟随主窗口销毁而销毁
是否必须设置context 必须传递UIAbilityContext 必须传递UIAbilityContext
典型场景 应用主界面 弹窗、悬浮小窗

关键点:createWindow创建的是独立的“窗口实体”,可以独立控制大小、位置、显示状态。createSubWindow则无法独立存在,且其宿主必须是已存在的主窗口。

3. 环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机 / 平板

4. 核心实现

4.1 基础结构

在开始操作窗口前,先确保module.json5中配置了window权限(其实主窗口操作不需要额外权限,但建议检查"supportedWindowModes"字段):

{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "launchType": "singleton",
        "supportedWindowModes": ["fullscreen", "floating"]
      }
    ]
  }
}

4.2 在UIAbility中显式创建主窗口

为什么需要显式创建? 默认的主窗口会在onWindowStageCreate回调中传递过来,但如果你需要自定义窗口尺寸,必须在回调里重新创建并替换它。

下面这段代码实现了:应用启动后创建一个宽高为屏幕一半、居中显示的窗口。

// EntryAbility.ets
import { UIAbility, AbilityConstant, Want, window } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

export default class EntryAbility extends UIAbility {
  private mainWindow: window.Window | null = null;

  onWindowStageCreate(windowStage: window.WindowStage): void {
    // 获取屏幕显示区域信息
    const display = windowStage.getMainWindowSync()?.getWindowProperties();
    if (!display) {
      // 如果取不到屏幕信息,走默认流程
      windowStage.loadContent('pages/Index', (err) => {});
      return;
    }

    // 计算窗口大小:取屏幕宽高的一半
    const screenWidth = display.windowRect.width;
    const screenHeight = display.windowRect.height;

    const targetWidth = Math.floor(screenWidth * 0.5);
    const targetHeight = Math.floor(screenHeight * 0.5);

    // 计算居中位置
    const posX = Math.floor((screenWidth - targetWidth) / 2);
    const posY = Math.floor((screenHeight - targetHeight) / 2);

    // 显式创建主窗口(注意:这里要销毁默认窗口然后自己创建)
    windowStage.createWindow({
      name: "main",
      windowType: window.WindowType.TYPE_APP,
      ctx: this.context,
      width: targetWidth,
      height: targetHeight
    }).then((win: window.Window) => {
      this.mainWindow = win;

      // 移动窗口到居中位置
      win.moveWindowTo(posX, posY).then(() => {
        console.info("moveWindowTo success");
      }).catch((err: BusinessError) => {
        console.error(`moveWindowTo failed: ${JSON.stringify(err)}`);
      });

      // 设置窗口的背景色为纯白
      win.setWindowBackgroundColor("#FFFFFF");

      // 加载内容
      win.loadContent('pages/Index', (err) => {
        if (err) {
          console.error(`loadContent failed: ${JSON.stringify(err)}`);
        } else {
          console.info("loadContent success");
        }
      });

      // 显示窗口
      win.showWindow().catch((err: BusinessError) => {
        console.error(`showWindow failed: ${JSON.stringify(err)}`);
      });
    }).catch((err: BusinessError) => {
      console.error(`createWindow failed: ${JSON.stringify(err)}`);
    });

    // 注意:默认的windowStage上的MainWindow已被我们创建的取代
    // 之后不要再调用windowStage.getMainWindowSync()
  }

  onWindowStageDestroy(): void {
    if (this.mainWindow) {
      this.mainWindow.destroyWindow();
      this.mainWindow = null;
    }
  }
}

注意事项:

  1. 先销毁默认窗口windowStage.getMainWindowSync()获取的默认窗口应在显式创建前关闭?这里官方文档没有明确要求,但经验表明,在createWindow之前调用destroyWindow销毁默认窗口可以避免资源冲突。
  2. 尺寸单位是vpwidthheight的单位是vp(虚拟像素),而非px。
  3. 位置坐标moveWindowTo的坐标原点在屏幕左上角(包含状态栏区域),如果你的应用需要避开状态栏,需要手动计算偏移。

4.3 动态切换到全屏

在主窗口已经半屏显示后,用户点击按钮切换到全屏。这里使用setWindowLayoutMode

// 在某个组件(如Button)的点击事件中
private fullScreenSwitch(): void {
  if (!this.mainWindow) {
    return;
  }

  // 获取当前窗口属性判断当前是全屏还是普通
  const props = this.mainWindow.getWindowProperties();
  const isFullScreen = 
    props.windowLayoutMode === window.WindowLayoutMode.WINDOW_LAYOUT_MODE_FULLSCREEN;

  if (isFullScreen) {
    // 退出全屏:回到之前半屏尺寸
    this.mainWindow?.resetSize(360, 640);
    this.mainWindow?.moveWindowTo(0, 0);
  } else {
    // 进入全屏
    this.mainWindow?.setWindowLayoutMode(
      window.WindowLayoutMode.WINDOW_LAYOUT_MODE_FULLSCREEN
    ).then(() => {
      console.info("set fullscreen mode success");
    }).catch((err: BusinessError) => {
      console.error(`set fullscreen failed: ${JSON.stringify(err)}`);
    });
  }
}

注意setWindowLayoutMode会让窗口充满显示区域(包括状态栏区域),但状态栏的显示与否需要配合setWindowLayoutFullScreen一起使用。如果你只希望状态栏保留但窗口填满,用setWindowLayoutMode即可。

5. 踩坑记录

坑1:窗口大小单位理解错误导致的UI异形

现象:在1080*2400分辨率的手机上,设置width: 540, height: 1200,期望是半屏大小,结果窗口比预期小一圈。

原因createWindowwidthheightvp单位,而非px。对1080px宽的屏幕,默认density为3,实际1vp=3px。所以540vp = 1620px,超出屏幕宽度,导致窗口自动缩放填充。

解法:先通过getWindowProperties().windowRect获取屏幕实际vp尺寸。示例中的Math.floor(screenWidth * 0.5)正是基于vp计算。

坑2:UIAbility与窗口生命周期同步问题

现象:在某些设备版本(如HarmonyOS 4.0以前),win.showWindow()onWindowStageCreate回调中调用后,页面内容不可见。

原因showWindow()默认是异步的,但UIAbility的生命周期状态变化可能先于窗口显示完成。当UIAbility状态进入FOREGROUND时,窗口还未完全绘制。

解法:在win.showWindow()then回调中再执行loadContent,或者使用win.on('windowEvent', callback)监听窗口显示事件,在确认显示后再加载内容。

const SHOW_EVENT = 'windowSizeChange';
win.on(SHOW_EVENT, () => {
  win.loadContent('pages/Index');
});
win.showWindow();

6. 最佳实践

  1. 永远不要让主窗口操作在onWindowStageCreate之外执行:虽然可以到处拿到windowStage,但在onWindowStageCreate回调之后,窗口资源可能已被回收。始终在UIAbility中持有mainWindow引用。

  2. 使用resetSize代替setWindowSizesetWindowSize在某些API版本会导致窗口闪烁。resetSize更稳定,且支持vp单位。

  3. 全屏切换时保存窗口位置:切换到全屏时,用局部变量保存posX, posY, width, height,全屏退出后恢复。避免丢失用户调好的窗口位置。

7. FAQ

Q:为什么真机上窗口创建的尺寸和模拟器不一致?

A:模拟器通常使用固定的物理分辨率(如1920*1080),且默认的density为2。真机的density可能为2.5或3。建议在真机上调试窗口尺寸逻辑,并在@Entry组件中通过getContext().window.getWindowProperties()打印实际vp值。

Q:createWindow后必须showWindow吗?

A:是的。createWindow只是创建了一个窗口对象,但未显示。不调用showWindow,窗口永远不可见。另外,在onWindowStageCreate中调用createWindow后,记得destroyWindow默认窗口,否则系统会报资源冲突。

Q:setWindowLayoutMode(FULLSCREEN)后,状态栏消失怎么办?

A:setWindowLayoutMode(FULLSCREEN)会使窗口填满整个显示区域,包括状态栏。如果需要保留状态栏,请使用setWindowLayoutMode(NON_FULLSCREEN)或单独设置setWindowLayoutFullScreen(false)

8. Demo入口

// pages/Index.ets
@Entry
@Component
struct Index {
  // 通过@LocalStorage或全局状态获取mainWindow引用
  @StorageLink('mainWindow') mainWindow: window.Window = undefined;

  build() {
    Column() {
      Button('切换全屏')
        .onClick(() => {
          // 调用之前定义的fullScreenSwitch方法
          // 这里通过回调或事件总线触发
        })
    }
    .height('100%')
    .width('100%')
  }
}

需要注意的是,在UIAbility中通过AppStorage.setOrCreate('mainWindow', win)将窗口引用传递给组件层,避免在组件中直接getContext()获取UIAbility上下文,防止内存泄漏。

示例代码地址:项目地址

如果你在实践过程中遇到其他问题,建议先打印getWindowProperties()的详细信息,许多坑都能从属性值中找到线索。

Logo

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

更多推荐