1# 如何创建子窗口并与主窗口通信
2
3## 场景介绍
4应用开发过程中,经常需要创建弹窗(子窗口)用来承载跟当前内容相关的业务,比如电话应用的拨号弹窗;阅读应用中长按当前内容触发的编辑弹窗;购物应用经常出现的抽奖活动弹窗等。
5本文为大家介绍如何创建子窗口并实现子窗口与主窗口的数据通信。
6
7## 效果呈现
8本例最终效果如下:
9
10![subwindow-mainwindow-communication](figures/subwindow-mainwindow-communication.gif)
11
12## 环境要求
13本例基于以下环境开发,开发者也可以基于其他适配的版本进行开发:
14
15- IDE: DevEco Studio 4.0 Beta1
16- SDK: Ohos_sdk_public 4.0.7.5 (API Version 10 Beta1)
17
18
19## 实现思路
20本例关键特性及实现方案如下:
21- 点击“创建子窗口”按钮创建子窗口:使用window模块的createSubWindow方法创建子窗口,在创建时设置子窗口的大小、位置、内容等。
22- 子窗口可以拖拽:通过gesture属性为子窗口绑定PanGesture拖拽事件,使用moveWindowTo方法将窗口移动到拖拽位置,呈现拖拽效果。
23- 点击主窗口的“子窗口数据+1”按钮,子窗口中的数据加1,反之亦然,即实现主窗口和子窗口间的数据通信:将数据变量存储在AppStorage中,在主窗口和子窗口中引用该数据,并通过@StorageLink与AppStorage中的数据进行双向绑定,从而实现主窗口和子窗口之间的数据联动。
24> ![icon-note.gif](../device-dev/public_sys-resources/icon-note.gif) **说明:**
25> 本文使用AppStorage实现主窗口和子窗口之间的数据传递,除此之外,Emitter和EventHub等方式也可以实现,开发者可以根据实际业务需要进行选择。
26
27
28## 开发步骤
29由于本例重点讲解子窗口的创建以及主窗口和子窗口之间的通信,所以开发步骤会着重讲解相关内容的开发,其余内容不做赘述,全量代码可参考完整代码章节。
301. 创建子窗口。
31    使用createSubWindow方法创建名为“hiSubWindow”的子窗口,并设置窗口的位置、大小、显示内容。将创建子窗口的动作放在自定义成员方法showSubWindow()中,方便后续绑定到按钮上。具体代码如下:
32    ```ts
33    showSubWindow() {
34        // 创建应用子窗口。
35        this.windowStage.createSubWindow("hiSubWindow", (err, data) => {
36          if (err.code) {
37            console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err));
38            return;
39          }
40          this.sub_windowClass = data;
41          console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data));
42          // 子窗口创建成功后,设置子窗口的位置
43          this.sub_windowClass.moveWindowTo(300, 300, (err) => {
44            if (err.code) {
45              console.error('Failed to move the window. Cause:' + JSON.stringify(err));
46              return;
47            }
48            console.info('Succeeded in moving the window.');
49          });
50          // 设置子窗口的大小
51          this.sub_windowClass.resize(350, 350, (err) => {
52            if (err.code) {
53              console.error('Failed to change the window size. Cause:' + JSON.stringify(err));
54              return;
55            }
56            console.info('Succeeded in changing the window size.');
57          });
58          // 为子窗口加载对应的目标页面。
59          this.sub_windowClass.setUIContent("pages/SubWindow",(err) => {
60            if (err.code) {
61              console.error('Failed to load the content. Cause:' + JSON.stringify(err));
62              return;
63            }
64            console.info('Succeeded in loading the content.');
65            // 显示子窗口。
66            this.sub_windowClass.showWindow((err) => {
67              if (err.code) {
68                console.error('Failed to show the window. Cause: ' + JSON.stringify(err));
69                return;
70              }
71              console.info('Succeeded in showing the window.');
72            });
73            this.sub_windowClass.setWindowBackgroundColor('#E8A027')
74          });
75        })
76      }
77    ```
782. 实现子窗口可拖拽。
79    为页面内容绑定PanGesture拖拽事件,拖拽事件发生时获取到触摸点的位置信息,使用@Watch监听到位置变量的变化,然后调用窗口的moveWindowTo方法将窗口移动到对应位置,从而实现拖拽效果。
80
81    具体代码如下:
82    ```ts
83    import window from '@ohos.window';
84
85    interface Position {
86      x: number,
87      y: number
88    }
89
90    @Entry
91    @Component
92    struct SubWindow{
93      ...
94      // 创建位置变量,并使用@Watch监听,变量发生变化调用moveWindow方法移动窗口
95      @State @Watch("moveWindow") windowPosition: Position = { x: 0, y: 0 };
96      private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });
97      private subWindow: window.Window
98      // 通过悬浮窗名称“hiSubWindow”获取到创建的悬浮窗
99      aboutToAppear() {
100        this.subWindow = window.findWindow("hiSubWindow")
101      }
102      // 将悬浮窗移动到指定位置
103      moveWindow() {
104        this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y);
105      }
106
107      build(){
108        Column(){
109          Text(`AppStorage保存的数据:${this.storData}`)
110            .fontSize(12)
111            .margin({bottom:10})
112          Button('主窗口数据+1')
113            .fontSize(12)
114            .backgroundColor('#A4AE77')
115            .onClick(()=>{
116              this.storData += 1
117            })
118        }
119        .height('100%')
120        .width('100%')
121        .alignItems(HorizontalAlign.Center)
122        .justifyContent(FlexAlign.Center)
123        .gesture(
124          PanGesture(this.panOption)
125            .onActionStart((event: GestureEvent) => {
126              console.info('Pan start');
127            })
128            // 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition
129            .onActionUpdate((event: GestureEvent) => {
130              this.windowPosition.x += event.offsetX;
131              this.windowPosition.y += event.offsetY;
132            })
133            .onActionEnd(() => {
134              console.info('Pan end');
135            })
136        )
137      }
138    }
139    ```
1403. 实现主窗口和子窗口间的数据通信。本例中即实现点击主窗口的“子窗口数据+1”按钮,子窗口中的数据加1,反之亦然。本例使用应用全局UI状态存储AppStorage来实现对应效果。
141    - 在创建窗口时触发的onWindowStageCreate回调中将自定义数据变量“data”存入AppStorage。
142        ```ts
143        onWindowStageCreate(windowStage: window.WindowStage) {
144            // 将自定义数据变量“data”存入AppStorage
145            AppStorage.SetOrCreate('data', 1);
146            ...
147            windowStage.loadContent('pages/Index', (err, data) => {
148              if (err.code) {
149                hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
150                return;
151              }
152              hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
153            });
154          }
155        ```
156    - 在主窗口中定义变量“storData”,并使用@StorageLink将其与AppStorage中的变量“data”进行双向绑定,这样一来,“mainData”的变化可以传导至“data”,并且该变化可以被UI框架监听到,从而完成UI状态刷新。
157        ```ts
158        ...
159        // 使用@StorageLink将"mainData"与AppStorage中的变量"data"进行双向绑定
160        @StorageLink('data') mainData: number = 1;
161        ...
162        build() {
163            Row() {
164              Column() {
165                Text(`AppStorage保存的数据:${this.mainData}`)
166                  .margin({bottom:30})
167                Button('子窗口数据+1')
168                  .backgroundColor('#A4AE77')
169                  .margin({bottom:30})
170                  .onClick(()=>{
171                    // 点击,storData的值加1
172                    this.mainData += 1
173                  })
174              ...
175              }
176              .width('100%')
177            }
178            .height('100%')
179          }
180        ```
181    - 在子窗口中定义变量“subData”,并使用@StorageLink将其与AppStorage中的变量“data”进行双向绑定。由于主窗口的“mainData”也与“data”进行了绑定,因此,“mainData”的值可以通过“data”传递给“subData”,反之亦然。这样就实现了主窗口和子窗口之间的数据同步。
182        ```ts
183        ...
184        // 使用@StorageLink将"subData"与AppStorage中的变量"data"进行双向绑定
185          @StorageLink('data') subData: number = 1;
186        ...
187          build(){
188            Column(){
189              Text(`AppStorage保存的数据:${this.subData}`)
190                .fontSize(12)
191                .margin({bottom:10})
192              Button('主窗口数据+1')
193                .fontSize(12)
194                .backgroundColor('#A4AE77')
195                .onClick(()=>{
196                  // 点击,subData的值加1
197                  this.subData += 1
198                })
199            }
200            ...
201          }
202        ```
203
204## 完整代码
205本例完整代码如下:
206EntryAbility文件代码:
207```ts
208// EntryAbility.ts
209import AbilityConstant from '@ohos.app.ability.AbilityConstant';
210import hilog from '@ohos.hilog';
211import UIAbility from '@ohos.app.ability.UIAbility';
212import Want from '@ohos.app.ability.Want';
213import window from '@ohos.window';
214
215let sub_windowClass = null;
216export default class EntryAbility extends UIAbility {
217
218  destroySubWindow() {
219    // 销毁子窗口。当不再需要子窗口时,可根据具体实现逻辑,使用destroy对其进行销毁。
220    sub_windowClass.destroyWindow((err) => {
221      if (err.code) {
222        console.error('Failed to destroy the window. Cause: ' + JSON.stringify(err));
223        return;
224      }
225      console.info('Succeeded in destroying the window.');
226    });
227  }
228
229  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
230
231    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
232  }
233
234  onDestroy() {
235    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
236  }
237
238  onWindowStageCreate(windowStage: window.WindowStage) {
239    // 将自定义数据变量“data”存入AppStorage
240    AppStorage.SetOrCreate('data', 1);
241    AppStorage.SetOrCreate('window', windowStage);
242    // 为主窗口添加加载页面
243    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
244
245    windowStage.loadContent('pages/Index', (err, data) => {
246      if (err.code) {
247        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
248        return;
249      }
250      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
251    });
252  }
253
254  onWindowStageDestroy() {
255    this.destroySubWindow();
256    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
257  }
258
259  onForeground() {
260    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
261  }
262
263  onBackground() {
264    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
265  }
266}
267```
268主窗口代码:
269```ts
270// Index.ets
271import window from '@ohos.window';
272
273@Entry
274@Component
275struct Index {
276  // 使用@StorageLink将"mainData"与AppStorage中的变量"data"进行双向绑定
277  @StorageLink('data') mainData: number = 1;
278  @StorageLink('window') storWindow:window.WindowStage = null
279  private windowStage = this.storWindow
280  private sub_windowClass = null
281
282  showSubWindow() {
283    // 创建应用子窗口。
284    this.windowStage.createSubWindow("hiSubWindow", (err, data) => {
285      if (err.code) {
286        console.error('Failed to create the subwindow. Cause: ' + JSON.stringify(err));
287        return;
288      }
289      this.sub_windowClass = data;
290      console.info('Succeeded in creating the subwindow. Data: ' + JSON.stringify(data));
291      // 子窗口创建成功后,设置子窗口的位置、大小及相关属性等。
292      this.sub_windowClass.moveWindowTo(300, 300, (err) => {
293        if (err.code) {
294          console.error('Failed to move the window. Cause:' + JSON.stringify(err));
295          return;
296        }
297        console.info('Succeeded in moving the window.');
298      });
299      this.sub_windowClass.resize(350, 350, (err) => {
300        if (err.code) {
301          console.error('Failed to change the window size. Cause:' + JSON.stringify(err));
302          return;
303        }
304        console.info('Succeeded in changing the window size.');
305      });
306      // 为子窗口加载对应的目标页面。
307      this.sub_windowClass.setUIContent("pages/SubWindow",(err) => {
308        if (err.code) {
309          console.error('Failed to load the content. Cause:' + JSON.stringify(err));
310          return;
311        }
312        console.info('Succeeded in loading the content.');
313        // 显示子窗口。
314        this.sub_windowClass.showWindow((err) => {
315          if (err.code) {
316            console.error('Failed to show the window. Cause: ' + JSON.stringify(err));
317            return;
318          }
319          console.info('Succeeded in showing the window.');
320        });
321        this.sub_windowClass.setWindowBackgroundColor('#E8A027')
322      });
323    })
324  }
325
326  build() {
327    Row() {
328      Column() {
329        Text(`AppStorage保存的数据:${this.mainData}`)
330          .margin({bottom:30})
331        Button('子窗口数据+1')
332          .backgroundColor('#A4AE77')
333          .margin({bottom:30})
334          .onClick(()=>{
335            // 点击,storData的值加1
336            this.mainData += 1
337          })
338        Button('创建子窗口')
339          .backgroundColor('#A4AE77')
340          .onClick(()=>{
341            // 点击弹出子窗口
342            this.showSubWindow()
343          })
344      }
345      .width('100%')
346    }
347    .height('100%')
348  }
349}
350```
351子窗口代码:
352```ts
353// SubWindow.ets
354import window from '@ohos.window';
355
356interface Position {
357  x: number,
358  y: number
359}
360
361@Entry
362@Component
363struct SubWindow{
364  // 使用@StorageLink将"subData"与AppStorage中的变量"data"进行双向绑定
365  @StorageLink('data') subData: number = 1;
366  // 创建位置变量,并使用@Watch监听,变量发生变化调用moveWindow方法移动窗口
367  @State @Watch("moveWindow") windowPosition: Position = { x: 0, y: 0 };
368  private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All });
369  private subWindow: window.Window
370  // 通过悬浮窗名称“hiSubWindow”获取到创建的悬浮窗
371  aboutToAppear() {
372    this.subWindow = window.findWindow("hiSubWindow")
373  }
374  // 将悬浮窗移动到指定位置
375  moveWindow() {
376    this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y);
377  }
378
379  build(){
380    Column(){
381      Text(`AppStorage保存的数据:${this.subData}`)
382        .fontSize(12)
383        .margin({bottom:10})
384      Button('主窗口数据+1')
385        .fontSize(12)
386        .backgroundColor('#A4AE77')
387        .onClick(()=>{
388          // 点击,subData的值加1
389          this.subData += 1
390        })
391    }
392    .height('100%')
393    .width('100%')
394    .alignItems(HorizontalAlign.Center)
395    .justifyContent(FlexAlign.Center)
396    .gesture(
397      PanGesture(this.panOption)
398        .onActionStart((event: GestureEvent) => {
399          console.info('Pan start');
400        })
401        // 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition
402        .onActionUpdate((event: GestureEvent) => {
403          this.windowPosition.x += event.offsetX;
404          this.windowPosition.y += event.offsetY;
405        })
406        .onActionEnd(() => {
407          console.info('Pan end');
408        })
409    )
410  }
411}
412```
413## 参考
414- [窗口开发](../application-dev/windowmanager/application-window-stage.md)
415- [AppStorage:应用全局的UI状态存储](../application-dev/quick-start/arkts-appstorage.md)