Qt

Прокси-контейнер в QML или implicitWidth/implicitHeight

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

Сделать это можно несколькими способами:
Явно
через width/height
или при работе с Layout через minWidth/minHeight, preferredWidh/preferredHeight, fillWidth/fillHeight

Самое интересный, это не явный способ через implicitWidth/implicitHeight

Интересен он тем, что если размер явно не указан, то он устанавливается равным implicitWidth/implicitHeight соответственно. Этакий «желаемый размер».

Это крайне полезно, в случае использования своих компонентов — когда заранее не знаешь, нужно ли размер будет изменять и будет ли использоваться Layout или anchor.

Это было небольшое введение навсякий случай. Теперь ближе к теме.

В мое случае мне нужен был элемент-контейнер, к примеру что бы всё содержимое имело заранее заданный отступ от края контейнера. Вот так:

Что бы этот контейнер можно было использовать в нескольких местах — он будет в отдельном файле, к примеру MyContainer1.qml.
Это полезно, что бы каждый раз в разных местах не прописывать одно и то же, что бы в случае необходимости — изменить только в одном месте отступ и не забыть нигде.

Так вот, что бы это реализовать — нужно сначала всё содержимое помещать в какой-либо компонент, который бы в свою очередь имел отступ от края контейнера. Делаем:

import QtQuick 2.12

Rectangle {
    id: myContainer1
    default property alias content: contentWrapper.children
    color: "orange"
    clip: true

    Rectangle {
        id: contentWrapper
        anchors.fill: parent
        anchors.margins: 10
        color: "blue"
    }

}

И в main.qml используем его:

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.12

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")


    MyContainer1 {
        anchors.centerIn: parent
        width: 200
        height: 200

        Rectangle {
            id: content1
            color: "green"
            anchors.fill: parent
        }
    }
}

Получилось вроде бы то, что нужно.

Обратите внимание на свойство

default property alias content: contentWrapper.children

Благодаря ему, всё, что будет помещено внутрь MyContainer1 фактически окажется внутри компонента id: contentWrapper.
Иначе нам бы не сделать было отступ, если бы всё содержимое было напрямую в контейнере.

А благодаря anchors.fill: parent и anchors.margins: 10 у нас как раз и получается отступ от края контейнера.

Вроде бы всё хорошо, но есть НО:

1. Что делать, если мы хотим, что бы контейнер автоматически принимал размер содержимого? Например, если размер содержимого у нас динамический и может изменяться, ну пусть по клику мышки.
Если сейчас мы тупо уберём размеры у контейнера, то он просто перестанет отображаться…

Тут как раз поможет implicitWidth/implicitHeight — будем получать размер содержимого от contentWrapper правим MyComponent1.qml

import QtQuick 2.12

Rectangle {
    id: myContainer1
    default property alias content: contentWrapper.children

    color: "orange"

    implicitWidth: contentWrapper.childrenRect.width+contentWrapper.anchors.margins*2
    implicitHeight: contentWrapper.childrenRect.height+contentWrapper.anchors.margins*2

    Rectangle {
        id: contentWrapper
        anchors.fill: parent
        anchors.margins: 10
        color: "blue"
    }

}

Используем:

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.12

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")


    MyContainer1 {
        anchors.centerIn: parent

        Rectangle {
            id: content1
            color: "green"
            width: 150
            height: 150
        }
    }
}

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

А всё благодаря этим строкам:

implicitWidth: contentWrapper.childrenRect.width+contentWrapper.anchors.margins*2

На примере implicitWidth — мы получаем размер содержимого contentWrapper и не забываем учитывать отступы (они с двух сторон, так что умножаем на 2).

И вроде бы всё хорошо, но что если мы теперь всё же хотим явно задать размер контейнера? Например, что бы контейнер был всегда по размеру родителя, а содержимое было другого размера и в центре контейнера:

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Layouts 1.12

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("Hello World")


    MyContainer1 {
        anchors.fill: parent

        Rectangle {
            id: content1
            color: "green"
            width: 50
            height: 50
            anchors.centerIn: parent
        }

    }
}

Да, кажется визуально всё хорошо, но вот в консоле вывода у нас будут предупреждения:

qrc:/main.qml:12:5: QML MyContainer1: Binding loop detected for property «implicitWidth»

Происходит это из-за установки «anchors.centerIn: parent«,
а именно когда это срабатывает, то запрашивается ширина и высота компонента, которые у нас неявно задаются через implicitWidth, которые обновляются при срабатывании изменения childrenRect, НО на момент запроса они ещё не сработали, поэтому не обновились от childrenRect, поэтому принудительно запрашивается обновление значения.
Потом уже в порядке очерёдности срабатывает изменение childrenRect и обновляются значения implicitWidth, от чего срабатывает изменение ширины, от чего Qt нужно опять обработать «anchors.centerIn: parent» и тогда Qt считает, что происходит взаимо обновление свойств, короче зациклился, поэтому прекращает выполнение и выдаёт предупреждение.

Поэтому, мы воспользуемся Binding с delay: true, что бы Qt вызывал обновление не сразу, а как закончится очередной цикл обновления событий и понял, что свойства, по сути, не взаимоизменяемы.

import QtQuick 2.12
import QtQml 2.12

Rectangle {
    id: myContainer1
    default property alias content: contentWrapper.children

    color: "orange"


    Binding {
        target: myContainer1;
        property: "implicitWidth";
        value: contentWrapper.childrenRect.width+contentWrapper.anchors.margins*2;
        delayed: true;
    }

    Binding {
        target: myContainer1;
        property: "implicitHeight";
        value: contentWrapper.childrenRect.height+contentWrapper.anchors.margins*2;
        delayed: true;
    }


    Rectangle {
        id: contentWrapper
        anchors.fill: parent
        anchors.margins: 10
        color: "blue"
    }

}

Или, как вариант — просто вызывать обновление childrenRect раньше, например добавив к контейнеру свойства
property var contentWidth: contentWrapper.childrenRect.width
Но тогда связывание с implicitWidth нужно производить после завершения создания компонента, например по Component.onCompleted:

import QtQuick 2.12
import QtQml 2.12

Rectangle {
    id: myContainer1
    default property alias content: contentWrapper.children

    color: "orange"


    property int contentWidth: contentWrapper.childrenRect.width
    property int contentHeight: contentWrapper.childrenRect.height


    property bool componentCompleted: false
    Component.onCompleted:  { componentCompleted = true; }

    implicitWidth: (componentCompleted)? contentWidth : 0
    implicitHeight:  (componentCompleted)? contentHeight : 0


    Rectangle {
        id: contentWrapper
        anchors.fill: parent
        anchors.margins: 10
        color: "blue"
    }

}

Мне лично ближе первый вариант из-за некоторой оптимизации вызовов обновлений из-за delayed: true у Binging.

Вот в общем то и всё =) Получился прокси-контейнер, как я его называю.

Related posts

Упрощение работы с динамическими структурами в C++

Вывести время компиляции исходников __TIMESTAMP__ в виде Unix timestamp в C(C++)

QML DropArea for files