Как правильно управлять диалогами в QML: Singleton + JavaScript Promise

от автора

Почему управление диалогами в 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/