Пользовательский интерфейс в Qt

Адаптивная верстка в QML: Layouts, anchors и масштабирование под разные экраны

Введение

Qt Quick приложения часто должны работать на экранах самых разных размеров и соотношений сторон — от мобильных телефонов до настольных мониторов. QML предоставляет два взаимодополняющих механизма для построения адаптивного интерфейса: anchors (якоря, привязывающие края элементов друг к другу) и Layouts (Row/Column/GridLayout, автоматически распределяющие пространство между дочерними элементами). В статье разберём, когда использовать каждый из них и как комбинировать для действительно адаптивной верстки.

Концепция

Anchors — низкоуровневый, но очень гибкий механизм: каждый элемент может привязать любой из своих краёв (left, right, top, bottom, centerIn и так далее) к краю родителя или другого элемента, с возможностью отступов (margins). Это хорошо подходит для фиксированной композиции элементов друг относительно друга, но не предназначено для динамического распределения элементов переменного числа.

Layouts (RowLayout, ColumnLayout, GridLayout) автоматически располагают дочерние элементы в ряд, столбец или сетку, с поддержкой растягивающихся пропорций через Layout.fillWidth/Layout.fillHeight и Layout.preferredWidth. Это естественный выбор, когда число элементов может меняться, или когда нужно равномерное распределение доступного пространства, аналогично flexbox в CSS.

Пример кода: Адаптивная верстка в QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

ApplicationWindow {
    id: window
    visible: true
    width: 480
    height: 320
    minimumWidth: 320

    // Корневой layout — адаптивно перестраивается под ширину окна
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 16
        spacing: 12

        // Заголовок, привязанный через anchors внутри собственного Item
        Item {
            Layout.fillWidth: true
            height: 40

            Text {
                anchors.left: parent.left
                anchors.verticalCenter: parent.verticalCenter
                text: "Панель управления"
                font.pixelSize: 20
            }

            Button {
                anchors.right: parent.right
                anchors.verticalCenter: parent.verticalCenter
                text: "Настройки"
            }
        }

        // Адаптивная сетка карточек: число столбцов зависит от ширины
        GridLayout {
            Layout.fillWidth: true
            Layout.fillHeight: true
            columns: window.width > 600 ? 3 : (window.width > 400 ? 2 : 1)
            rowSpacing: 10
            columnSpacing: 10

            Repeater {
                model: 6
                Rectangle {
                    Layout.fillWidth: true
                    Layout.fillHeight: true
                    Layout.minimumHeight: 80
                    color: "#e0e0e0"
                    radius: 8

                    Text {
                        anchors.centerIn: parent
                        text: "Карточка " + (index + 1)
                    }
                }
            }
        }

        // Панель кнопок внизу — равномерно растягивается по горизонтали
        RowLayout {
            Layout.fillWidth: true
            spacing: 8

            Button { text: "Отмена"; Layout.fillWidth: true }
            Button { text: "Применить"; Layout.fillWidth: true }
        }
    }
}

Пояснения к коду

Корневой ColumnLayout растягивается на всё окно через anchors.fill: parent, а его дочерние элементы (заголовок, сетка карточек, панель кнопок) располагаются друг под другом в столбик автоматически, без необходимости вручную вычислять координаты Y каждого следующего элемента. Внутри блока заголовка использован anchors, а не Layout — поскольку там всего два элемента с простым относительным расположением (текст слева, кнопка справа), anchors даёт более прямой и компактный код, чем RowLayout с растягивающимся спейсером.

GridLayout.columns вычисляется динамически на основе текущей ширины окна (window.width) — это простой, но эффективный способ адаптивной перестройки сетки карточек: при сужении окна число столбцов уменьшается, и карточки автоматически перераспределяются по новой сетке без какого-либо ручного кода перестроения.

Подводные камни

  • Смешивание anchors и явных x/y/width/height для одного и того же элемента приводит к неоднозначному, иногда циклическому определению геометрии — Qt выводит предупреждение в рантайме о «binding loop», и поведение становится труднопредсказуемым; для одного элемента стоит выбрать либо anchors, либо явные геометрические свойства, либо участие в Layout, но не смешивать подходы одновременно.
  • anchors.fill: parent внутри элемента, который сам находится внутри Layout, может конфликтовать с тем, как Layout пытается управлять размерами этого элемента — Layout-менеджеры ожидают, что размеры дочерних элементов определяются через Layout.* свойства, а не через independent anchors к родителю, отличному от самого Layout.
  • Захардкоженные пиксельные пороги адаптивности (window.width > 600) работают для конкретных протестированных размеров, но не масштабируются автоматически под очень большие 4K-мониторы или очень маленькие встраиваемые экраны без дополнительной настройки — для по-настоящему универсальной адаптивности иногда используют относительные единицы или явные breakpoints, синхронизированные с дизайн-системой проекта.
  • Игнорирование Layout.minimumWidth/minimumHeight для контента с фиксированным минимальным размером (например, текст, который не может быть короче своего содержимого) может привести к обрезанию контента при сильном сжатии окна — явное указание минимальных размеров предотвращает это, ценой того, что Layout не сможет сжать элемент ниже этого порога.