QML

Адаптивный и responsive UI в Qt Quick: разные экраны и разрешения

Введение

QML-приложения часто должны работать на устройствах с очень разными размерами экрана и плотностью пикселей (от небольшого встроенного дисплея до большого desktop-монитора с высоким DPI) — в этой статье разберём принципы построения адаптивного интерфейса, корректно масштабирующегося под эти различия.

Концепция

Использование относительных, привязанных к размеру родителя величин (через anchors и привязки свойств, статьи 481, 485) вместо фиксированных, абсолютных пиксельных значений — базовый принцип адаптивности, уже неявно затронутый в предыдущих статьях цикла. Дополнительно, для учёта различий в плотности пикселей разных устройств (DPI), Qt Quick предоставляет единицы измерения, не зависящие напрямую от физических пикселей экрана (системы масштабирования, привязанные к настройкам ОС или явно вычисляемому коэффициенту масштабирования), что предотвращает ситуацию, когда интерфейс, спроектированный под одно физическое разрешение, выглядит непропорционально мелким или крупным на устройстве с существенно иной плотностью пикселей. Для принципиально разных категорий устройств (телефон против десктопа) часто нужна не просто пропорциональная адаптация одного и того же макета, а структурно иной макет (например, многоколоночный для широкого экрана десктопа против однокол��ночного для узкого экрана телефона), что требует явной логики выбора подходящего варианта макета в зависимости от доступного пространства.

Пример кода: Адаптивный и responsive UI в Qt Quick

// Базовая адаптивность через относительные величины и anchors (продолжение принципов статьи 485)
import QtQuick

Rectangle {
    id: root
    anchors.fill: parent

    Rectangle {
        id: card
        width: Math.min(parent.width * 0.9, 600) // адаптивная ширина, но не более 600px на больших экранах
        height: 200
        anchors.centerIn: parent
        color: "white"
        border.color: "lightgray"
    }
}
// Структурно различный макет в зависимости от доступной ширины — не просто масштабирование
import QtQuick
import QtQuick.Layouts

Item {
    id: root
    anchors.fill: parent

    property bool isWideLayout: width > 600 // явный порог переключения между структурами макета

    RowLayout {
        anchors.fill: parent
        visible: root.isWideLayout // широкий экран — элементы РЯДОМ, в одну строку

        Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; color: "lightblue" }
        Rectangle { Layout.fillWidth: true; Layout.fillHeight: true; color: "lightgreen" }
    }

    ColumnLayout {
        anchors.fill: parent
        visible: !root.isWideLayout // узкий экран — элементы ДРУГ ПОД ДРУГОМ

        Rectangle { Layout.fillWidth: true; Layout.preferredHeight: parent.height / 2; color: "lightblue" }
        Rectangle { Layout.fillWidth: true; Layout.preferredHeight: parent.height / 2; color: "lightgreen" }
    }
}
// Учёт плотности пикселей через Screen.pixelDensity для физически осмысленных размеров
import QtQuick

Item {
    // Размер элемента, ориентированный на физический размер (примерно 1 см), а не произвольное число пикселей
    property real oneCentimeterInPixels: Screen.pixelDensity * 10 // pixelDensity обычно в пикселях на мм

    Rectangle {
        width: oneCentimeterInPixels
        height: oneCentimeterInPixels
        color: "orange"
    }
}

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

width: Math.min(parent.width * 0.9, 600) показывает практичный паттерн «адаптивно, но с ограничением» — элемент масштабируется пропорционально доступному пространству на небольших экранах, но не становится неоправданно широким на очень больших экранах, где простое пропорциональное масштабирование привело бы к избыточно растянутому, визуально некомфортному результату. Пример со структурно различным RowLayout/ColumnLayout, переключаемыми через visible в зависимости от isWideLayout, показывает принципиально иной уровень адаптивности — не просто масштабирование одного и того же макета, а полная замена структуры расположения элементов в зависимости от доступного пространства, что часто необходимо для действительно качественного, удобного интерфейса на принципиально разных категориях устройств. Screen.pixelDensity показывает способ привязать размер элемента к физическому, а не произвольному пиксельному измерению, что помогает поддерживать визуально сопоставимый, не «слишком мелкий» или «слишком крупный» размер элементов интерфейса на устройствах с существенно различающейся плотностью пикселей экрана.

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

  • Тестирование адаптивности только при ручном изменении размера окна на десктопе разработчика, без проверки на реальных, разнообразных устройствах или хотя бы их корректной эмуляции — поведение макета при динамическом изменении размера окна десктопа может не полностью отражать реальное поведение на физически разных устройствах (особенно с учётом различий в плотности пикселей, статья про Screen.pixelDensity), и для приложений, действительно ориентированных на широкий спектр устройств, стоит проверить реальное поведение на представительном наборе реальных или эмулированных конфигураций экрана.
  • Выбор порога переключения между структурными вариантами макета (isWideLayout) произвольно, без анализа реальных, типичных размеров экранов целевой аудитории приложения — слишком высокий или слишком низкий порог может привести к тому, что макет переключается в неподходящий момент (например, узкий, «мобильный» макет используется даже на сравнительно широком окне десктопа, или наоборот), и выбор конкретного порогового значения должен основываться на реальном анализе диапазона размеров экрана, актуального для целевой аудитории конкретного приложения.
  • Игнорирование того, что переключение между структурно разными вариантами макета (как в примере с RowLayout/ColumnLayout) создаёт оба варианта одновременно (даже если один из них скрыт через visible: false), что может быть неэффективно по используемой памяти и вычислительным ресурсам для сложных макетов — для действительно ресурсоёмких альтернативных макетов стоит рассмотреть Loader с динамической загрузкой только реально нужного, активного в данный момент варианта, вместо постоянного существования обоих вариантов одновременно с одним из них просто скрытым.
  • Применение единственного, фиксированного коэффициента масштабирования для всего интерфейса без разбора, не учитывающего, что разные типы элементов (текст, изображения, интерактивные зоны нажатия) могут требовать разных принципов адаптации (текст должен оставаться читаемым с учётом реальной плотности пикселей, тогда как декоративные изображения могут масштабироваться более свободно, а интерактивные зоны нажатия должны соответствовать минимальным физическим размерам, удобным для нажатия пальцем, независимо от общего коэффициента масштабирования остального интерфейса).