Как известно, в 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.
Вот в общем то и всё =) Получился прокси-контейнер, как я его называю.