Почему управление диалогами в QML почти всегда сделано плохо

Уже не первый раз сталкиваюсь в проектах на Qt QML с проблемой управления диалогами и всплывающими окнами.

QML - декларативный язык и это здорово! Мы описываем, что хотим видеть на экране, и, если всё сделали правильно, при запуске программы получаем желаемый результат.

Но иногда хочется динамики — и именно с диалогами начинаются проблемы, которые все решают по-разному.

Кто-то продолжает так же декларативно описывать диалог для очередного экрана приложения. Да, так можно поступить, но у этого подхода есть несколько проблем.

Первая — код начинает разрастаться. Даже если вынести диалог в отдельный компонент, его всё равно придётся «тюнить» каждый раз перед отображением, что не очень удобно.

Вторая проблема, как по мне, куда хуже — при создании экрана в приложении будут созданы и все дочерние элементы. То есть диалог может потреблять память, хотя по факту пользователь может так им и не воспользоваться.

Другой вариант, который тоже часто встречается — это обёртка диалога в Component и его непосредственное создание в нужный момент. С точки зрения потребления памяти это уже лучше, но проблему лишнего кода это не решает. Зачастую из-за подготовки такого диалога кода может оказаться даже больше. К тому же нужно не забывать вызывать destroy() для всех динамически созданных объектов, когда они больше не нужны.

Всё становится ещё хуже, если один и тот же диалог нужен в нескольких местах. В большинстве случаев люди либо не парятся, либо им просто некогда — и в итоге мы видим обычную копипасту тут и там.

Я хочу предложить совсем другой вариант, который проще и удобнее: это связка QML Singleton и JavaScript Promise.

Создаем Singleton от QtObject и добавляем в него`readonly property Component`, в котором будет находится экземпляр нашего диалога, и, который мы будем создавать только тогда, когда он нам действительно нужен. В качестве диалога я выбрал стандартный Dialog из модуля QtQuick.Controls. Быстрый и простой вариант выглядит так:

readonly property Component instancer: Component {

Dialog {

id: dlg

anchors.centerIn: Overlay.overlay

property variant context: null

property string text: ""

modal: true

standardButtons: Dialog.Yes | Dialog.No

closePolicy: Dialog.CloseOnEscape | Dialog.CloseOnPressOutside

// Действия по кнопке сбросят контекст, чтобы не было повторного вызова

// reject при закрытии диалога

onAccepted: { context?.accept?.(); context = null; }

onRejected: { context?.reject?.(); context = null; }

Label { width: parent.width; text: dlg.text; visible: text }

}

}

Здесь в качестве свойства context выступает обычный Object из JavaScript, в котором два свойства: accept и reject. Оба этих свойства - функции, которые связаны с нашим Promise.

Теперь добавим в наш Singleton функцию, которая будет показывать диалог и возвращать Promise:

function open(options, parent) {

return new Promise((resolve, reject) => {

const context = Object.freeze({

accept: resolve,

reject: reject,

});

options.context = context;

const dialog = root.instancer.createObject(parent, options);

dialog.closed.connect(() => {

dialog.context?.reject?.();

dialog.destroy();

});

dialog.open();

});

}

Тут мы как раз создаем наш context, который передается в диалог и вызывает accept если пользователь подтверждает действие, либо reject в противном случае.

Дальше создаем наш диалог из компонента и подписываемся на его событие закрытия, чтобы вызвать destroy и освободить память. Здесь так же может вызваться reject, но только в том случае, если пользователь закрыл диалог не по кнопке, а, например, нажал ESC на клавиатуре.

Собственно, на этом все, нашим Singleton, теперь им можно пользоваться. Например вот так, по нажатию на кнопку:

Button {

text: "Confirm"

onClicked: {

ConfirmDialog.open({

title: "Dialog title",

text: "Dialog body text",

}, root)

.then(() => console.log("Accepted"))

.catch(() => console.log("Rejected"));

}

}

Не забываем только добавить pragma Singleton в начало файла нашего Singleton и правильно зарегистрировать его в CMake:

set_source_files_properties(

qml/ConfirmDialog.qml

PROPERTIES

QT_QML_SINGLETON_TYPE TRUE

)

Что думаете?

Больше и удобнее в тг канале -

Начать дискуссию