Почему управление диалогами в QML почти всегда сделано плохо
Уже не первый раз сталкиваюсь в проектах на Qt QML с проблемой управления диалогами и всплывающими окнами.
QML — декларативный язык и это здорово! Мы описываем, что хотим видеть на экране, и, если всё сделали правильно, при запуске программы получаем желаемый результат.
Но иногда хочется динамики — и именно с диалогами начинаются проблемы, которые все решают по-разному. Кто-то продолжает так же декларативно описывать диалог для очередного экрана приложения. Да, так можно поступить, но у этого подхода есть несколько проблем.
Первая — код начинает разрастаться. Даже если вынести диалог в отдельный компонент, его всё равно придётся «тюнить» каждый раз перед отображением, что не очень удобно.
Вторая проблема, как по мне, куда хуже — при создании экрана в приложении будут созданы и все дочерние элементы. То есть диалог может потреблять память, хотя по факту пользователь может так им и не воспользоваться.
Другой вариант, который тоже часто встречается — это обёртка диалога в Component и его непосредственное создание в нужный момент. С точки зрения потребления памяти это уже лучше, но проблему лишнего кода это не решает. Зачастую из-за подготовки такого диалога кода может оказаться даже больше. К тому же нужно не забывать вызывать destroy() для всех динамически созданных объектов, когда они больше не нужны.
Всё становится ещё хуже, если один и тот же диалог нужен в нескольких местах. В большинстве случаев люди либо не парятся, либо им просто некогда — и в итоге мы видим обычную копипасту тут и там.
Я хочу предложить совсем другой вариант — более простой и удобный.
Singleton + JavaScript Promise
Я хочу предложить совсем другой вариант, который проще и удобнее: это связка 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
)
Автор: slavacpp
