QML

Модели и виды (Models/Views) в Qt Quick: ListView, GridView, Repeater

Введение

Многие реальные приложения отображают список или сетку однородных элементов (список сообщений, каталог товаров, список файлов) — в этой статье разберём систему моделей и видов Qt Quick: Repeater для простого повторения элементов по фиксированному или динамическому списку, и более продвинутые ListView/GridView, оптимизированные для больших, потенциально прокручиваемых наборов данных.

Концепция

Repeater создаёт экземпляр заданного делегата (шаблона визуального представления, статья 484) для каждого элемента модели — простой, но не оптимизированный для очень больших наборов данных способ, поскольку создаёт визуальные объекты для абсолютно всех элементов модели сразу, независимо от того, видны ли они реально на экране в данный момент. ListView/GridView решают эту проблему через виртуализацию — создают (и удерживают в памяти) визуальные представления только для элементов, реально видимых в текущей области прокрутки (плюс небольшой буфер сверху/снизу для плавности), что даёт принципиально лучшую производительность и потребление памяти для списков с большим количеством элементов (тысячи и более), для которых Repeater создал бы избыточное количество визуальных объектов одновременно.

Пример кода: Модели и виды (Models/Views) в Qt Quick

// Repeater — простой случай, подходящий для небольшого, фиксированного набора элементов
import QtQuick

Column {
    Repeater {
        model: ["Понедельник", "Вторник", "Среда", "Четверг", "Пятница"]
        delegate: Text {
            text: "День: " + modelData // modelData — встроенное имя для текущего элемента простой модели
        }
    }
}
// ListView с моделью объектов — типичный, идиоматичный паттерн отображения списка
import QtQuick

ListView {
    width: 300; height: 400
    model: ListModel {
        ListElement { name: "Заказ #1001"; amount: 1500 }
        ListElement { name: "Заказ #1002"; amount: 2300 }
        ListElement { name: "Заказ #1003"; amount: 890 }
    }

    delegate: Rectangle {
        width: ListView.view.width
        height: 50
        color: index % 2 === 0 ? "white" : "lightgray" // index — встроенное имя текущего индекса

        Row {
            anchors.verticalCenter: parent.verticalCenter
            anchors.left: parent.left
            anchors.leftMargin: 10
            spacing: 20

            Text { text: name } // name — поле текущего элемента модели, доступное напрямую по имени
            Text { text: amount + " ₽" }
        }
    }

    // Виртуализация в действии — только видимые delegate реально существуют как объекты,
    // независимо от того, сколько ListElement реально объявлено в модели
}
// GridView — аналогичная виртуализация, но для сеточного, а не линейного расположения
GridView {
    width: 400; height: 400
    cellWidth: 100; cellHeight: 100
    model: 50 // числовая модель — просто 50 элементов с индексами от 0 до 49

    delegate: Rectangle {
        width: 90; height: 90
        color: Qt.hsla(index / 50, 0.7, 0.5, 1.0) // цвет, зависящий от индекса элемента
    }
}

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

Repeater с простым JavaScript-массивом строк показывает базовое использование с modelData — для простых, не структурированных моделей (массив примитивных значений) modelData даёт прямой доступ к текущему элементу без необходимости в более сложной структуре модели. ListView с ListModel/ListElement показывает более структурированный, типичный для реальных приложений случай — каждый ListElement определяет набор именованных полей (name, amount), доступных в делегате непосредственно по имени (без необходимости явно писать modelData.name), что упрощает синтаксис делегата. width: ListView.view.width в делегате ListView — идиоматичный способ заставить делегат заполнять всю доступную ширину списка, автоматически адаптируясь при изменении размера самого ListView.

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

  • Использование Repeater для очень большого количества элементов (тысячи и более) вместо ListView/GridView — поскольку Repeater создаёт визуальные объекты для всех элементов сразу, независимо от их видимости, это может привести к заметному замедлению создания сцены и избыточному потреблению памяти для действительно больших наборов данных, тогда как виртуализация ListView/GridView спроектирована именно для эффективной работы с такими объёмами.
  • Создание сложных, «тяжёлых» делегатов с большим количеством вложенных элементов или дорогих в вычислении привязок для ListView/GridView с очень частой прокруткой — хотя виртуализация ограничивает количество ОДНОВРЕМЕННО существующих визуальных объектов, каждое появление нового делегата при прокрутке (когда элемент входит в видимую область) требует создания и инициализации этого визуального объекта, и слишком «тяжёлые» делегаты могут заметно снижать плавность быстрой прокрутки, несмотря на сам факт использования виртуализации.
  • Изменение модели (добавление/удаление/изменение элементов) напрямую, «в обход» методов, специально предназначенных для уведомления ListView/GridView об этом изменении (для ListModel — append(), remove(), setProperty(), а не прямая, неконтролируемая модификация внутреннего состояния) — корректное, ожидаемое поведение виртуализированного вида (правильное обновление визуального представления, анимация добавления/удаления, статья 487) зависит от использования предназначенных для этого методов изменения модели, информирующих вид о характере произошедшего изменения, а не произвольной модификации данных, способной оставить вид в несогласованном с реальными данными состоянии.
  • Игнорирование разницы между index (порядковый номер элемента в текущем представлении модели) и каким-либо стабильным, уникальным идентификатором элемента данных (если модель поддерживает сортировку/фильтрацию, статья 489) — index может меняться при изменении порядка отображения элементов (сортировка, фильтрация), и код, ошибочно полагающийся на index как на стабильный, неизменный идентификатор конкретного элемента данных (а не его текущую, потенциально изменяющуюся позицию в списке), может работать некорректно после применения сортировки или фильтрации к той же модели.