- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

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

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

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

Я хочу предложить совсем другой вариант — более простой и удобный.

Singleton + JavaScript Promise

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

Создаем 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

Источник [2]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/qt-2/451210

Ссылки в тексте:

[1] JavaScript Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise

[2] Источник: https://habr.com/ru/articles/1032896/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1032896