如何实现多个独立窗口
==========

#### 问题现象

如何实现一个应用的窗口多开功能，点击按钮可以打开一个独立的窗口，每个窗口都互不干扰，可以单独的打开与关闭？

#### 背景知识

*   [UIAbility](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-app-ability-uiability)是包含UI界面的应用组件，继承自Ability，提供UIAbility组件创建、销毁、前后台切换等生命周期回调，同时也具备后台通信能力。
*   [startAbility](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-inner-application-uiabilitycontext#startability)启动一个Ability。
*   UIAbility实例通过WindowStage持有了一个主窗口，详见：[主窗口的生命周期](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/window-overview#主窗口的生命周期)，该主窗口为ArkUI提供了绘制区域，可以加载不同的ArkUI页面。

#### 解决方案

打开多个互不干扰的独立窗口，可以通过startAbility打开多个UIAbility实例实现，每个UIAbility实例通过WindowStage持有一个主窗口，各个主窗口可以实现独立的功能。参考示例如下：

1.  DocumentListPage.ets：通过EntryAbility加载该页面，该页面通过startAbility打开DocumentAbility加载DocumentPage页面。
    
    import { common, Want } from '@kit.AbilityKit';
    
    @Entry
    @Component
    struct DocumentListPage {
      private index: number \= 1;
      @State docs: number\[\] \= \[\];
      private context \= this.getUIContext().getHostContext() as common.UIAbilityContext;
    
      build() {
        Row() {
          Column({ space: 10 }) {
            Button('新建文档')
              .onClick(() \=\> {
              _  // 添加文档_
                this.docs.push(this.index);
               _ // 跳转的目的地want,参数须根据实际情况替换_
                let want: Want \= {
                  deviceId: '',
                  bundleName: 'com.example.myapplication',
                  moduleName: 'entry',
                  abilityName: 'DocumentAbility',
                  parameters: {
                    instanceKey: 'idx\_' \+ this.index++
                  }
                };
             _   // 跳转_
                this.context.startAbility(want);
              });
    
            ForEach(this.docs, (idx: number) \=\> {
              Row({ space: 10 }) {
                Image('')
                  .width(20)
                  .aspectRatio(1)
                  .backgroundColor(Color.Blue);
    
                Text(\`文档${idx}\`)
                  .fontSize(18)
                  .onClick(() \=\> {
                    _// 跳转的目的地want,参数须根据实际情况替换_
                    let want: Want \= {
                      deviceId: '',
                      bundleName: 'com.example.myapplication',
                      moduleName: 'entry',
                      abilityName: 'DocumentAbility',
                      parameters: {
                        instanceKey: 'idx\_' \+ idx
                      }
                    };
                  _  // 跳转_
                    this.context.startAbility(want);
                  });
              }.width('100%');
            });
          }
          .width('100%')
          .height('100%')
          .padding(20);
        };
      }
    }
    
2.  DocumentAbility.ets：提供DocumentPage页面的UIAbility组件创建、销毁、前后台切换等生命周期回调。
    
    import { UIAbility } from '@kit.AbilityKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';
    import { window } from '@kit.ArkUI';
    
    export default class DocumentAbility extends UIAbility {
      onCreate(): void {
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
      }
    
      onDestroy(): void {
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
      }
    
      onWindowStageCreate(windowStage: window.WindowStage): void {
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    
        windowStage.loadContent('pages/DocumentPage', (err) \=\> {
          if (err.code) {
            hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
            return;
          }
          hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.');
        });
      }
    
      onWindowStageDestroy(): void {
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
      }
    
      onForeground(): void {
       _ // Ability has brought to foreground_
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
      }
    
      onBackground(): void {
        _// Ability has back to background_
        hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
      }
    };
    
3.  DocumentPage.ets：DocumentAbility拉起的页面详情，支持跳转回DocumentListPage页面。
    
    import { common, Want } from '@kit.AbilityKit';
    
    @Entry
    @Component
    struct DocumentPage {
      private context \= this.getUIContext().getHostContext() as common.UIAbilityContext;
      @State content: string \= '';
    
      build() {
        Row() {
          Column({ space: 10 }) {
            Row({ space: 10 }) {
              SymbolGlyph($r('sys.symbol.arrow\_left\_circle'))
                .fontSize(50)
                .onClick(() \=\> {
                  _// 跳转的目的地want,参数须根据实际情况替换_
                  let want: Want \= {
                    deviceId: '',
                    bundleName: 'com.example.myapplication',
                    moduleName: 'entry',
                    abilityName: 'EntryAbility',
                  };
                _  // 跳转_
                  this.context.startAbility(want);
                });
              Text('这里是标题，点前面符号跳转')
                .fontSize(30)
                .fontWeight(FontWeight.Bold);
            }
            .width('100%');
    
            Divider();
            TextArea({
              placeholder: '请输入文档内容',
              text: this.content
            })
              .layoutWeight(1)
              .onChange(val \=\> this.content \= val);
          }
          .width('100%')
          .height('100%')
          .padding(10);
        };
      }
    }
    
4.  module.json5：abilities增加DocumentAbility配置。
    
    "abilities": \[
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility\_desc",
        "icon": "$media:layered\_image",
        "label": "$string:EntryAbility\_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start\_window\_background",
        "exported": true,
        "skills": \[
          {
            "entities": \[
              "entity.system.home"
            \],
            "actions": \[
              "ohos.want.action.home"
            \]
          }
        \]
      },
      {
        "name": "DocumentAbility",
        "srcEntry": "./ets/entryability/DocumentAbility.ets",
        "description": "$string:DocumentAbility\_desc",
        "icon": "$media:layered\_image",
        "label": "$string:DocumentAbility\_label",
        "launchType": "specified",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start\_window\_background"
      }
    \],
    

#### 常见FAQ

Q：如何确认当前打开的UIAbility的数量？

A：可以在[UIAbility](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-app-ability-uiability#uiability)的生命周期中记录当前已打开的UIAbility数量，示例代码如下：

import { ConfigurationConstant, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN \= 0x0000;
_//_ _已打开的UIAbility数量_
let uiAbilityNum \= 0;

export default class WindowsNumAbility extends UIAbility {
  onCreate(): void {
    try {
      this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR\_MODE\_NOT\_SET);
    } catch (err) {
      hilog.error(DOMAIN, 'testTag', 'Failed to set colorMode. Cause: %{public}s', JSON.stringify(err));
    }
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy(): void {
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
   _ // 销毁一个UIAbility时，UIAbility数量减少1，并存入AppStorage_
    uiAbilityNum \-= 1;
    AppStorage.setOrCreate('uiAbilityNum', uiAbilityNum);
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
   _ // Main window is created, set main page for this ability_
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
    _//_ _每次完成WindowStage创建，UIAbility数量增加1，并存入AppStorage_
    uiAbilityNum += 1;
    AppStorage.setOrCreate('uiAbilityNum', uiAbilityNum);

   _ // 第一个窗口加载Index页面，后续新创建的窗口加载NewIndex页面_
    if (uiAbilityNum \=== 1) {
      windowStage.loadContent('pages/Index', (err) \=\> {
        if (err.code) {
          hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
          return;
        }
        hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
      });
    } else {
      windowStage.loadContent('pages/NewIndex', (err) \=\> {
        if (err.code) {
          hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
          return;
        }
        hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
      });
    }
  }

  onWindowStageDestroy(): void {
   _ // Main window is destroyed, release UI related resources_
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground(): void {
   _ // Ability has brought to foreground_
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground(): void {
   _ // Ability has back to background_
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
  }
};