Почему управление диалогами в 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)
ссылка на оригинал статьи https://habr.com/ru/articles/1032896/