Read the original article:Hierarchy Relationship Between Custom Dialogs and Sub-Windows
Problem Description
This article explores the layering behavior between custom dialogs and sub-windows in HarmonyOS applications. Specifically, it investigates whether these UI elements are stacked based on the order they are invoked, and how their visibility is managed across multiple windows.
Background Knowledge
What is the hierarchy relationship between custom dialogs and sub-windows? Are they stacked based on the order they are invoked?
-
Basic Custom Dialogs (CustomDialog) displayed via the CustomDialogController class must be created and managed depending on specific components. Each dialog must be separately declared with its corresponding
CustomDialogControllerwithin a component. - openCustomDialog is used to open global custom dialogs, decoupling dialog content from pages.
- Sub-windows can be created using the createSubWindow method under
WindowStage.
This content compares basic custom dialogs implemented via the CustomDialogControllerclass and global custom dialogs implemented via openCustomDialogacross three scenarios. These comparisons help determine the layering relationship between the two types of custom dialogs and sub-windows (conclusions are summarized at the end):
Solution
Scenario 1: Open a sub-window first from the main window, then open a custom dialog from the main window.
In this case, both types of custom dialogs are layered above the main window but below the sub-window, and they pop up in the center of the main window.
Scenario 2: Open a sub-window first from the main window, then open a custom dialog from the sub-window interface.
In this case, both custom dialogs are layered above the sub-window and pop up in the center of the sub-window.
Scenario 3: From the main window interface, open a sub-window via a single click, then open a custom dialog after a setTimeout delay.
In this case, the basic custom dialog is layered above the main window but below the sub-window, popping up in the center of the main window; the global custom dialog is layered above the sub-window and pops up in the center of the sub-window.
Below are some core code snippets:
Implementation of the basic custom dialog:
@CustomDialog
struct CustomDialogExample {
controller?: CustomDialogController
cancel: () => void = () => {
}
confirm: () => void = () => {
}
build() {
Column() {
Text('This is a basic custom pop-up window.')
.fontSize(10)
Button('close')
.fontSize(10)
.onClick(() => {
if (this.controller !== undefined) {
this.controller.close()
}
})
.padding({
left: 5,
right: 5,
top: 0,
bottom: 0
})
}
.padding(10)
}
}
Implementation of the global custom dialog:
export class OMDialog {
private uiContext: UIContext = new UIContext
private viewNode: ComponentContent<Object> | null = null
static instance: OMDialog = new OMDialog()
static init(uiContext: UIContext) {
OMDialog.instance.uiContext = uiContext
}
open() {
let view = new ComponentContent(this.uiContext, wrapBuilder(customDialogBuilder), 1);
this.uiContext.getPromptAction()
.openCustomDialog(view, { isModal: false, offset: { dx: 0, dy: 0 } })
.catch(() => {
// TODO: Implement error handling.
})
.then(() => {
this.viewNode = view
})
}
close() {
this.uiContext.getPromptAction().closeCustomDialog(this.viewNode).catch(() => {
// TODO: Implement error handling.
})
}
}
@Builder
function customDialogBuilder(type: number) {
TestComponent()
}
@Component
struct TestComponent {
build() {
Column() {
Text('Global custom pop-up window')
.fontSize(10)
.fontColor(Color.Black)
Row() {
Button('Next').onClick(() => {
})
Blank().width(50)
Button('Close').onClick(() => {
OMDialog.instance.close()
})
}
}
.padding(20)
.backgroundColor($r('app.color.start_window_background'))
}
}
Creation and destruction of the sub-window:
export class SubWindowManager {
public windowClass: window.Window | null = null;
public creatSubWindow(): void {
let windowStage = AppStorage.get('windowStage') as window.WindowStage;
windowStage.createSubWindow('ResizeSubWindow', (err, windowClass) => {
this.windowClass = windowClass
if (err.code > 0) {
console.error(`failed to create subWindow Cause: ${err.message}`);
return;
}
try {
// Set up a sub-window loading page
this.windowClass.setUIContent('pages/SubWindowPage', () => {
});
// Set the top-left corner coordinates of the sub-window
this.windowClass.moveWindowTo(80, 50);
// Set the size of the sub-window
this.windowClass.resize(350, 200);
// Display Subwindow
this.windowClass.showWindow();
} catch (err) {
console.error(`failed to create subWindow Cause:${err}`);
}
})
}
public closeSubWindow(): void {
if (this.windowClass) {
this.windowClass.destroyWindow()
}
}
}
Full example code:
The path to the first file is: project_name\entry\src\main\ets\entryability.
The paths to the remaining files are: project_name\entry\src\main\ets\pages.
The windowStage can be stored in the onWindowStageCreate method of EntryAbility.
import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
console.info(`want: ${want}`);
console.info(`launchParam: ${launchParam}`);
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');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
AppStorage.setOrCreate('windowStage', windowStage);
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
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.');
});
}
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');
}
};
// Utils.ets
import { ComponentContent, UIContext, window } from '@kit.ArkUI';
export class OMDialog {
private uiContext: UIContext = new UIContext
private viewNode: ComponentContent<Object> | null = null
static instance: OMDialog = new OMDialog()
static init(uiContext: UIContext) {
OMDialog.instance.uiContext = uiContext
}
open() {
let view = new ComponentContent(this.uiContext, wrapBuilder(customDialogBuilder), 1);
this.uiContext.getPromptAction()
.openCustomDialog(view, { isModal: false, offset: { dx: 0, dy: 0 } })
.then(() => {
this.viewNode = view
});
}
close() {
this.uiContext.getPromptAction().closeCustomDialog(this.viewNode);
}
}
@Builder
function customDialogBuilder(type: number) {
TestComponent()
}
@Component
struct TestComponent {
build() {
Column() {
Text('Global custom pop-up window')
.fontSize(10)
.fontColor(Color.Black)
Row() {
Button('Close').onClick(() => {
OMDialog.instance.close()
})
}
}
.padding(20)
.backgroundColor($r('app.color.start_window_background'))
}
}
export class SubWindowManager {
public windowClass: window.Window | null = null;
public creatSubWindow(): void {
let windowStage = AppStorage.get('windowStage') as window.WindowStage;
windowStage.createSubWindow('ResizeSubWindow', (err, windowClass) => {
this.windowClass = windowClass
if (err.code > 0) {
console.error(`failed to create subWindow Cause: ${err.message}`);
return;
}
try {
// Set up a sub-window loading page
this.windowClass.setUIContent('pages/SubWindowPage', () => {
});
// Set the top-left corner coordinates of the sub-window
this.windowClass.moveWindowTo(80, 50);
// Set the size of the sub-window
this.windowClass.resize(350, 200);
// Display Subwindow
this.windowClass.showWindow();
} catch (err) {
console.error(`failed to create subWindow Cause:${err}`);
}
})
}
public closeSubWindow(): void {
if (this.windowClass) {
this.windowClass.destroyWindow()
}
}
}
Index:
// Index.ets
import { ArcList, ArcListAttribute, ArcListItemAttribute, ArcListItem, window } from '@kit.ArkUI';
import { OMDialog, SubWindowManager } from './OMDialog';
@CustomDialog
struct CustomDialogExample {
controller?: CustomDialogController
cancel: () => void = () => {
}
confirm: () => void = () => {
}
build() {
Column() {
Text('This is a basic custom pop-up window.')
.fontSize(10)
Button('Close')
.onClick(() => {
if (this.controller !== undefined) {
this.controller.close()
}
})
.padding({ left: 10, right: 10 })
.margin({ top: 10 })
}
.padding(10)
}
}
@Entry
@Component
export default struct Index{
@State message: string = 'Hello World';
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample(),
})
subWindowManager = new SubWindowManager();
build() {
ArcList() {
ArcListItem() {
Text('Open the basic custom dialog in the main window.')
.onClick(() => {
if (this.dialogController != null) {
this.dialogController.open()
}
})
.padding({ left: 5, right: 5 })
.margin({ top: 5, bottom: 5 })
}
ArcListItem() {
Text('Open customdialog in the main window.')
.onClick(() => {
OMDialog.instance.open();
})
.padding({ left: 5, right: 5 })
.margin({ top: 5, bottom: 5 })
}
ArcListItem() {
Text('In the main window, first open the sub-window, then open the basic custom dialog.')
.onClick(() => {
this.subWindowManager.creatSubWindow()
setTimeout(() => {
if (this.dialogController != null) {
this.dialogController.open()
}
}, 3000)
})
.padding({ left: 5, right: 5 })
.margin({ top: 5, bottom: 5 })
}
ArcListItem() {
Text('In the main window, first open the sub-window and then open the custom dialog.')
.onClick(() => {
this.subWindowManager.creatSubWindow()
setTimeout(() => {
OMDialog.instance.open();
}, 3000)
})
.padding({ left: 5, right: 5 })
.margin({ top: 5, bottom: 5 })
}
ArcListItem() {
Text('Open the sub-window')
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.subWindowManager.creatSubWindow()
})
.padding({ left: 5, right: 5 })
.margin({ top: 5, bottom: 5 })
}
ArcListItem() {
Text('Close Subwindow')
.fontWeight(FontWeight.Bold)
.onClick(() => {
this.subWindowManager.closeSubWindow()
})
.padding({ left: 5, right: 5 })
.margin({ top: 5, bottom: 5 })
}
ArcListItem() {
Button('Payment Button')
.fontWeight(FontWeight.Bold)
.onClick(() => {
let subWindowID: number = 0;
try {
subWindowID = window.findWindow('ResizeSubWindow').getWindowProperties().id
} catch (error) {
// TODO: Implement error handling.
}
// Obtain the main window ID
let windowStage = AppStorage.get('windowStage') as window.WindowStage;
let mainWindowID: number = windowStage.getMainWindowSync().getWindowProperties().id
// Shift the focus from the main window to the character window.
let promise = window.shiftAppWindowFocus(mainWindowID, subWindowID);
promise.then(() => {
OMDialog.instance.open();
console.info('Succeeded in shifting app window focus');
})
})
.padding({ left: 5, right: 5 })
.margin({ top: 5, bottom: 5 })
}
ArcListItem() {
Button('After 3 seconds, a prompt pop-up will appear.')
.fontWeight(FontWeight.Bold)
.onClick(() => {
let subWindowID: number = 0;
setTimeout(() => {
try {
subWindowID = window.findWindow('ResizeSubWindow').getWindowProperties().id
} catch (error) {
// TODO: Implement error handling.
}
// Obtain the main window ID
let windowStage = AppStorage.get('windowStage') as window.WindowStage;
let mainWindowID: number = windowStage.getMainWindowSync().getWindowProperties().id
// Shift the focus from the sub-window to the main window.
let promise = window.shiftAppWindowFocus(subWindowID, mainWindowID).catch(() => {
// TODO: Implement error handling.
});
promise.then(() => {
OMDialog.instance.open();
console.info('Succeeded in shifting app window focus');
})
}, 3000)
})
.padding({ left: 5, right: 5 })
.margin({ top: 5, bottom: 5 })
}
}
.margin({ left: 10, right: 10 })
}
}
import { OMDialog } from '../components/OMDialog'
@CustomDialog
struct CustomDialogExample {
controller?: CustomDialogController
cancel: () => void = () => {
}
confirm: () => void = () => {
}
build() {
Column() {
Text('This is a basic custom pop-up window.')
.fontSize(10)
Button('close')
.fontSize(10)
.onClick(() => {
if (this.controller !== undefined) {
this.controller.close()
}
})
.padding({
left: 5,
right: 5,
top: 0,
bottom: 0
})
}
.padding(10)
}
}
@Entry
@Component
struct SubWindowPage {
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample(),
})
build() {
Column() {
Text('open custom dialog')
.onClick(() => {
OMDialog.instance.open();
})
Text('Open the basic custom dialog')
.onClick(() => {
if (this.dialogController != null) {
this.dialogController.open()
}
})
.fontWeight(FontWeight.Bold)
}
.height('100%')
.width('100%')
.backgroundColor('#aaa')
}
}
Based on the above comparisons of the two types of custom dialogs across three scenarios, we can draw the following conclusions:
- A custom dialog determines the window it belongs to when it opens; the dialog is part of that window. When the window containing the custom dialog is at a lower layer than another window, the dialog will also be below that window.
- The layering of custom dialogs is not related to the order in which they are invoked but is determined by the window they belong to.
- For basic custom dialogs implemented via the
CustomDialogControllerclass, since the dialog controller used to open the dialog must be declared within a component, it is bound to that component and exists within the same window. That is, if the basic custom dialog is invoked from the main window, it will appear above the main window but below higher-layered windows. - For global custom dialogs implemented via openCustomDialog, their window depends on the UI context of their anchor point. The window they belong to is determined when they open—wherever the focus is at that moment, they will open on that window:
- In Scenario 1, they are invoked by a click on the main window page, so the anchor is on the main window, and their layer is above the main window but below the sub-window.
- In Scenario 2, they are invoked by a click on the sub-window page, so the anchor is on the sub-window, and their layer is above the sub-window page.
- In Scenario 3, although they are invoked by a click on the main window, the timing is after the sub-window is opened, so the anchor is on the sub-window, and their layer is above the sub-window page.
Based on the above conclusions, we can clearly understand the layering behavior of custom dialogs in multi-window scenarios, ensuring they correctly attach to the intended window. For example, when using the openCustomDialogglobal custom dialog, focus can be shifted between windows to make it open on the desired window. Below are two example scenarios:
- For UI design purposes, a payment button needs to be placed on the main window page and cannot be placed on the sub-window page. If we have already opened a sub-window and click the button on the main window, the dialog will open on the main window. If the dialog overlaps with the sub-window, it will be obscured. However, since the dialog content is important and must be interacted with by the user, we can shift focus from the main window to the sub-window when opening the dialog, making it open on the sub-window and avoiding occlusion.
subWindowID = window.findWindow('ResizeSubWindow').getWindowProperties().id;
// Obtain the main window ID
let windowStage = AppStorage.get('windowStage') as window.WindowStage;
let mainWindowID: number = windowStage.getMainWindowSync().getWindowProperties().id
// Shift the focus from the main window to the character window.
let promise = window.shiftAppWindowFocus(mainWindowID, subWindowID));
promise.then(() => {
OMDialog.instance.open();
console.info('Succeeded in shifting app window focus');
});
- A button on the main window page schedules a program, and a prompt dialog should appear when the program starts. If the user is currently operating in the sub-window, the focus is on the sub-window, and the dialog will also appear on the sub-window. However, since this prompt dialog is of lower importance, we don’t want it to interrupt the user’s operation on the sub-window. Therefore, we can shift focus to the main window before opening the dialog, making it appear on the main window.
Summary
- Custom dialogs are associated with the window context in which they are opened.
- Layering order of dialogs depends on the associated window, not the invocation sequence.
- Basic custom dialogs (via CustomDialogController) are bound to the window context of their host component.
- Global custom dialogs (via openCustomDialog) depend on the UI context (focus) at the time of invocation.
- Focus management can be used to control where global dialogs appear in multi-window scenarios.




Top comments (0)