Последнее обновление:
February 13, 2021

Есть мысль... Жми, напиши!
Что имеем: Постов : 142 Авторов: 1 Категорий: 22

Прокси-контейнер в 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.

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

Views :

21

Layout.fillWidth: true и Layout.preferredWidth/Layout.minimumWidth зависимость (очередная хитрость)

Сталкиваюсь иногда с некоторыми хитростями в QML, о которых, по всей видимости, приходится только догадываться, ибо то ли я проглядел это в документации, то ли этого действительно в ней не указано.

Так вот, задача: нужно три колонки одинаковой ширины.

Делаем:

import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Layouts 1.3

Window {
    visible: true
    width: 640
    height: 480
    title: qsTr("QMLTips&Tricks24")

    RowLayout {
        anchors.fill: parent
        spacing: 0

        Rectangle {
            color: "blue"
            Layout.fillHeight: true
            Layout.fillWidth: true            
        }

        Rectangle {
            color: "orange"
            Layout.fillHeight: true
            Layout.fillWidth: true
        }


        Rectangle{
            color: "green"
            Layout.fillHeight: true
            Layout.fillWidth: true            
        }

    }
}

Соответственно на выходе получаем:

Получилось как и задумывали.

Обновим задачу:  Оранжевая колонка должна иметь предпочтительную ширину 200 пикселей.
Делаем:

Rectangle {
    color: "orange"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Получаем:

Что-то не то, да..? 

Вот тут начинается самое интересное.

Дело в том, что Layout.prefferedWidth (а так же Layout.minimumWidth) управляет пропорционально шириной относительно соседей, когда задано Layout.fillWidth: true

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

Делаем:

Rectangle {
    color: "blue"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Rectangle {
    color: "orange"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Rectangle{
    color: "green"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Получаем:

То есть как и задумывали, три одинаковые колонки.

Теперь наглядный пример насчёт пропорционально соседям изменяемости размеров. Если мы у синего прямоугольника зададим желаемую ширину в 50, а у зелёного 100, то соответственно синий будет в 4 раза меньше оранжевого (200/50=4), а зелёный буде в два раза меньше оранжевого (200/100=2). 

То есть родитель Layout (RowLayout/ColumnLayout/GridLayout) берёт от потомков наибольший желаемый размер и относительно него пропорционально выставляет ширину всем потомкам.
Повторюсь, что так QML себя ведёт, только когда задано Layout.fillWidth: true

Делаем:

Rectangle {
    color: "blue"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 50
}

Rectangle {
    color: "orange"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 200
}

Rectangle{
    color: "green"
    Layout.fillHeight: true
    Layout.fillWidth: true
    Layout.preferredWidth: 100
}

Получаем:

Короче, считаем, что при указании preferredWidth, minimumWidth мы работаем с пропорциями относительно остальных в одном контейнере и всё. 

Почему именно так, а не иначе?  

По-моему как раз для того, что бы можно было задать поведение при растягивании/сжимании…

 

Аналогичная ситуация произойдёт, если мы оранжевой колонке захотим указать minimumWidth: 100 при fillWidth:true

Что делать, если нужно, что бы все колонки имели одинаковую ширину и заполняли всё пространство (т.е. fillWidth: true), но при этом у них (у всех или у некоторых) должна быть задана разная минимальная ширина? 

Я в таком случае прописываю у всех:  preferredWidth наибольшее из minimumWidth.  Но стоит учитывать, что при приближении к минимальному значению размер изменяется пропорционально.

Надеюсь, теперь больше с этим проблем не возникнет.

Кстати, такое же поведение будет если у компонента задано свойство implicitWidth или implicitHeight, потому что если мы не задали явно Layout.preferredWidth или  Layout.fillHeight, то берутся как раз они. Наверное вы спрашиваете: как быть, если это не просто прямоугольник, а какой-то компонент? То всё просто — оберните его в Item или Rectangle или задайте явно Layout.preferredWidth

Кстати, вот документация на лэйауты: тынц.

Вот как-то так =)

Views :

559