Skip to main content

09窗口管理

小窗(悬浮窗)

窗口管理

入库类中:创建窗口和销毁窗口的核心代码。


设计小窗参数接口

呈现小窗口时需要指定小窗口的位置、尺寸、内容。

src/main/ets/model/SubWindowOption.ets

export default interface SubWindowOption {
// 小窗名称
subWindowName:string
// 小窗位置
x: number
y: number
// 小窗尺寸
width: number
height: number
// 小窗页面路由地址
subWindowPageUrl: string
}

设计工具类

src/main/ets/common/MySubWindow.ets

import { window } from '@kit.ArkUI';
import SubWindowOption from '../model/SubWindowOption';

class MySubWindow{
windowStage_: window.WindowStage | null = null;
// 小窗口对象
sub_windowClass: window.Window | null = null;

// 创建小窗的方法
async showSubWindow(subWindowOption:SubWindowOption) {
// 1.创建应用子窗口
if (this.windowStage_ == null) {
console.error('dxin => 小窗无法创建,因为 windowStage_ is null');
} else {
this.sub_windowClass = await this.windowStage_.createSubWindow(subWindowOption.subWindowName)
// 2 同步代码 设置小窗 位置和尺寸
this.sub_windowClass.moveWindowToAsync(subWindowOption.x, subWindowOption.y)
this.sub_windowClass.resizeAsync(subWindowOption.width, subWindowOption.height)
// 窗口里面得有内容
this.sub_windowClass.setUIContent(subWindowOption.subWindowPageUrl, () => {
// 让自己显示出来 需要问号或者叹号判空
//this.sub_windowClass!.showWindow()
this.sub_windowClass?.showWindow()
})
}
}

destroySubWindow() {
// 4.销毁子窗口。当不再需要子窗口时,可根据具体实现逻辑,使用destroy对其进行销毁。
//this.sub_windowClass!.destroyWindow()
this.sub_windowClass?.destroyWindow();
}
}
let mySubWindow = new MySubWindow()
export default mySubWindow

在入口类中初始化工具类的windowStage_

src/main/ets/entryability/EntryAbility.ets

  onWindowStageCreate(windowStage: window.WindowStage): void {

windowStage.loadContent('pages/Guide', (err) => {
if (err.code) {
return;
}
// 工具类中的那个主舞台对象 要被真正的主舞台(入口类的)对象赋值
mySubWindow.windowStage_ = windowStage
});
}

设计小窗页面

src/main/ets/pages/SubWindowContent.ets

import mySubWindow from '../common/MySubWindow'

@Entry
@Component
struct SubWindowContent {
build() {
Column({ space: 30 }) {
Text('QQ小窗口').fontSize(22).fontColor('red')
Button('关闭当前小窗口')
.onClick(() => {
mySubWindow.destroySubWindow()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}

在页面中调用工具类开启小窗

src/main/ets/pages/Index.ets

Button('开启QQ的小窗')
.onClick(() => {
mySubWindow.showSubWindow({
subWindowName:"QQ ",
x:50,
y:250,
width:500,
height:500,
subWindowPageUrl:"pages/SubWindowContent"
})
})

问题和优化

无法关闭多个小窗。

以上案例中准备了一个QQ的按钮,点击即可显示QQ的小窗,同时也可在显示的小窗上点击关闭自己。

既然封装工具类,传入参数。目的就是为了创建多个小窗。而以上代码则会出现问题。例如:

src/main/ets/pages/Index.ets 再准备一个按钮,准备打开微信的小窗

Button('开启微信的小窗')
.onClick(() => {
mySubWindow.showSubWindow({
subWindowName:"wechat",
x:700,
y:250,
width:500,
height:500,
subWindowPageUrl:"pages/Wechat"
})
})

补充微信小窗对应的页面文件:src/main/ets/pages/Wechat.ets

import mySubWindow from '../common/MySubWindow'

@Entry
@Component
struct Wechat {
build() {
Column({ space: 30 }) {
Text('wechat小窗口').fontSize(22).fontColor('red')
Button('关闭当前小窗口')
.onClick(() => {
mySubWindow.destroySubWindow()
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}

此时可以拉起两个小窗口,但是关闭操作无法按照预期实现,分析小窗工具类会发现。每次点击按钮打开窗口都会执行showSubWindow()方法并创建当前小窗对象,紧接着将创建的小窗对象赋值给工具类的sub_windowClass属性。

那么:连续创建两个或者多个窗口时,该sub_windowClass属性存储的是最后一个小窗对象。

所以,点击任何一个小窗的关闭按钮,均会调用和执行工具类中的destroySubWindow()方法,销毁的正是sub_windowClass属性中存储的小窗,即,最后一个小窗。 因此,除最后一个小窗可被关闭外,其他小窗对象均无法被关闭。

实现关闭多个小窗

经过以上分析,无法关闭多个小窗的主要原因是:新创建的小窗对象存在普通变量中导致后来者居上,前者被覆盖。

很容易想到解决办法:将所有的小窗对象使用数组存起来。

随即而来的问题是,如果数组中存放的是单纯的小窗对象,我们在点击不同的关闭按钮时,又该如何从数组中找到我们需要的那个小窗对象呢。

此时,你很容易想到的是:给每一个存入数组的小窗对象添加字的标记即可。恰好,创建的每一个小窗对象,本就有自己的名字。刚好用来作为标记。

设计数组中的数据类型

小窗对象+该对象的名字。

src/main/ets/model/SubWindowDescript.ets

import { window } from "@kit.ArkUI"

export default interface SubWindowDescript{
subWindowName:string
subWindowObj:window.Window
}

优化工具类

使用数组存储替换之前的变量存储

src/main/ets/common/MySubWindow.ets

import { window } from '@kit.ArkUI';
import SubWindowDescript from '../model/SubWindowDescript';
import SubWindowOption from '../model/SubWindowOption';

class MySubWindow{
windowStage_: window.WindowStage | null = null;
// 创建一个数组
mySubWindowArr: SubWindowDescript [] = [ ]

async showSubWindow(options: SubWindowOption) {
if (this.windowStage_) {
// 连续创建新小窗 此处会重新赋值 覆盖上一个小窗对象 所以应该设计成 当前小窗对象数组 根据名称对应
let currentSubWindowObj = await this.windowStage_.createSubWindow(options.subWindowName)
// 存入数组
this.mySubWindowArr.push({
subWindowName:options.subWindowName,
subWindowObj: currentSubWindowObj
})

// 使用当前小窗对象 同步 设置窗口位置
currentSubWindowObj.moveWindowToAsync(options.x, options.y)
// 同步 设置窗口尺寸
currentSubWindowObj.resizeAsync(options.width, options.height)
// 设置窗口的UI内容 指定页面路由 (需要提前准备好UI页面)
currentSubWindowObj.setUIContent(options.subWindowPageUrl, () => {
// 让我自己显示出来 ? 避免this.mySubWindowObj 是null的场景
currentSubWindowObj?.showWindow()
})
} else {
console.error(`dxin => 你的 windowStage_ 是null 创建不出小窗哦`)
}
}

destroySubWindow(subWindowName:string){
// 根据小窗名称 从数组中查出来 当前需要被关闭的那个小窗对象
this.mySubWindowArr.find(item => item.subWindowName === subWindowName)?.subWindowObj?.destroyWindow()
}
}
let mySubWindow = new MySubWindow()
export default mySubWindow

在小窗中调用关闭方法时传入参数

注意,销毁小窗的方法需要的参数,必须是创建该小窗时的小窗名字。

如果有可能手误拼错单词,可设计到string.json或者常量文件中进行引用,以提高正确性和复用性。

关闭QQ小窗/关闭微信小窗,二选一写人对应页面文件的点击事件中

 Button('关闭当前小窗口')
.onClick(() => {
mySubWindow.destroySubWindow("QQ")
mySubWindow.destroySubWindow("wechat")
})

小窗去重终极版工具类

以上设计中,每次都会将创建的小窗对象存入数组。即便是关闭后再次创建的同一个小窗。这样重复操作多次,会导致数组中有很多重复项。且无法关闭后来创建的小窗,因为从数组中查出来的小窗对象是早就被关闭过的对象。当然,我们可以在上文插入数组时,使用unshift()替换push()。即可解决无法关闭最新小窗的bug

但是数组重复项问题依旧存在,避免数组越来越臃肿,以及不必使用unshift()插入数据。可进行数组插入前校验。

校验逻辑:如果已经存在当前名称的小窗,先删除之前的。然后插入当前的。你简直是个天才。

import { window } from '@kit.ArkUI';
import SubWindowDescript from '../model/SubWindowDescript';
import SubWindowOption from '../model/SubWindowOption';

class MySubWindow{
windowStage_: window.WindowStage | null = null;
// 创建一个数组
mySubWindowArr: SubWindowDescript [] = [ ]

async showSubWindow(options: SubWindowOption) {
if (this.windowStage_) {
// 连续创建新小窗 此处会重新赋值 覆盖上一个小窗对象 所以应该设计成 当前小窗对象数组 根据名称对应
let currentSubWindowObj = await this.windowStage_.createSubWindow(options.subWindowName)
// 如果有同名小窗,删除再插入。保证只有一份。
const exists = this.mySubWindowArr.some(item => item.subWindowName === options.subWindowName)
if (exists){
// 删除同名小窗对象
this.mySubWindowArr = this.mySubWindowArr.filter(item => item.subWindowName !== options.subWindowName)
}
// 此时再插入,可保证去重问题
this.mySubWindowArr.push({
subWindowName:options.subWindowName,
subWindowObj: currentSubWindowObj
})

// 使用当前小窗对象 同步 设置窗口位置
currentSubWindowObj.moveWindowToAsync(options.x, options.y)
// 同步 设置窗口尺寸
currentSubWindowObj.resizeAsync(options.width, options.height)
// 设置窗口的UI内容 指定页面路由 (需要提前准备好UI页面)
currentSubWindowObj.setUIContent(options.subWindowPageUrl, () => {
// 让我自己显示出来 ? 避免this.mySubWindowObj 是null的场景
currentSubWindowObj?.showWindow()
})
} else {
console.error(`dxin => 你的 windowStage_ 是null 创建不出小窗哦`)
}
}

destroySubWindow(subWindowName:string){
// 根据小窗名称 从数组中查出来 当前需要被关闭的那个小窗对象
this.mySubWindowArr.find(item => item.subWindowName === subWindowName)?.subWindowObj?.destroyWindow()
}
}
let mySubWindow = new MySubWindow()
export default mySubWindow