Динамические представления в QML

Добавлено 4 апреля 2022 в 21:20

Повторители (элементы Repeater) хорошо работают с ограниченными и статическими наборами данных, но в реальном мире модели обычно более сложны и больше по размеру. Здесь требуется более умное решение. Для этого Qt Quick предоставляет элементы ListView и GridView. Оба они основаны на области Flickable, поэтому пользователь может перемещаться по большему набору данных. В то же время они ограничивают количество одновременно создаваемых делегатов. Для большой модели это означает уменьшение количества элементов, отображаемых одновременно в сцене.

ListView
GridView

Эти два элемента схожи в своем использовании. Мы начнем с ListView, а затем опишем GridView в сравнении с первым. Обратите внимание, что GridView помещает список элементов в двумерную сетку слева направо или сверху вниз. Если вы хотите отобразить таблицу данных, вам нужно использовать TableView, который описан в разделе «Модели таблиц».

ListView похож на элемент Repeater. Он использует model, создает экземпляр делегата (delegate), и между делегатами может быть интервал (spacing). В приведенном ниже примере показано, как это может выглядеть.

import QtQuick
import "../common"

Background {
    width: 80
    height: 300

    ListView {
        anchors.fill: parent
        anchors.margins: 20
        clip: true
        model: 100
        delegate: GreenBox {
            required property int index
            width: 40
            height: 40
            text: index
        }
        spacing: 5
    }
}
ListView

Если модель содержит больше данных, чем может уместиться на экране, ListView отображает только часть списка. Однако, как следствие поведения Qt Quick по умолчанию, ListView не ограничивает область экрана, в которой отображаются делегаты. Это означает, что делегаты могут быть видны за пределами представления списка, а динамическое создание и уничтожение делегатов за пределами представления списка видно пользователю. Чтобы предотвратить это, необходимо активировать отсечение в элементе ListView, установив для свойства clip значение true. На приведенном ниже рисунке показан результат этого (слева) по сравнению со случаем, когда свойство clip имеет значение false (справа).

обрезка ListView

Для пользователя ListView представляет собой прокручиваемую область. Он поддерживает кинетическую прокрутку, что означает, что его можно быстро перемещать по содержимому. По умолчанию он также может растягиваться за пределы содержимого, а затем возвращаться назад, чтобы сообщить пользователю, что конец достигнут.

Поведение в конце представления управляется с помощью свойства boundsBehavior. Это перечислимое значение, которое может принимать следующие варианты:

  • Flickable.DragAndOvershootBounds (поведение по умолчанию), при котором представление можно как перетаскивать, так и пролистывать за свои границы;
  • Flickable.StopAtBounds, при котором представление никогда не будет выходить за свои границы;
  • Flickable.DragOverBounds (золотая середина) позволяет пользователю перетаскивать представление за его границы, но клики останавливаются на границе.

Позиции, в которых представление может останавливаться, можно ограничить. Это управляется с помощью свойства snapMode. Поведение по умолчанию, ListView.NoSnap, позволяет останавливать представление в любой позиции. Установив для свойства snapMode значение ListView.SnapToItem, представление всегда будет выравнивать верх элемента со своим верхом. Наконец, ListView.SnapOneItem, представление остановится на не более чем одном элементе от первого видимого элемента, когда кнопка мыши или касание были отпущены. Последний режим очень удобен при перелистывании страниц.

Ориентация

Представление в виде списка по умолчанию предоставляет список с вертикальной прокруткой, но горизонтальная прокрутка может быть столь же полезной. Направление ListView контролируется через свойство orientation. Для него можно задать либо значение по умолчанию, ListView.Vertical, либо ListView.Horizontal. Горизонтальный список показан ниже.

import QtQuick
import "../common"

Background {
    width: 480
    height: 80

    ListView {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 4
        clip: true
        model: 100
        orientation: ListView.Horizontal
        delegate: GreenBox {
            required property int index
            width: 40
            height: 40
            text: index
        }
    }
}
горизонтальный список

Как вы можете заметить, по умолчанию направление горизонтального потока – слева направо. Его можно контролировать с помощью свойства layoutDirection, которое может быть установлено либо в Qt.LeftToRight, либо в Qt.RightToLeft, в зависимости от направления потока.

Навигация с помощью клавиатуры и выделение

При использовании ListView на сенсорных устройствах достаточно самого представления. В сценарии с клавиатурой или даже просто клавишами со стрелками для выбора элемента необходим механизм для указания текущего элемента. В QML это называется выделением (highlighting).

Представления поддерживают делегат выделения, который отображается в представлении вместе с делегатами. Его можно считать дополнительным делегатом, но он создается только один раз и перемещается в ту же позицию, что и текущий элемент.

Это показано в примере ниже. Для этого необходимы два свойства. Во-первых, чтобы свойство focus было установлено значение true. Это дает ListView фокус клавиатуры. Во-вторых, свойство highlight указывает используемого делегата выделения. Делегат выделения получает координаты (x, y) и высоту (height) текущего элемента. Если ширина (width) не указана, также используется ширина текущего элемента.

В примере для ширины используется присоединенное свойство ListView.view.width. Присоединенные свойства, доступные для делегатов, обсуждаются далее в разделе о делегатах данной главы, но полезно знать, что те же самые свойства доступны и для делегатов выделения.

import QtQuick
import "../common"

Background {
    width: 240
    height: 300

    ListView {
        id: view

        anchors.fill: parent
        anchors.margins: 20

        focus: true

        model: 100
        delegate: numberDelegate
        highlight: highlightComponent
        
        spacing: 5
        clip: true
    }

    Component {
        id: highlightComponent

        GreenBox {
            width: ListView.view ? ListView.view.width : 0
        }
    }

    Component {
        id: numberDelegate

        Item {
            id: wrapper

            required property int index

            width: ListView.view ? ListView.view.width : 0
            height: 40

            Text {
                anchors.centerIn: parent
                font.pixelSize: 10
                text: wrapper.index
            }
        }
    }
}
делегат выделения

При использовании выделения вместе с ListView можно использовать ряд свойств для управления его поведением. highlightRangeMode управляет тем, как на выделение влияет то, что отображается в представлении:

  • Параметр по умолчанию ListView.NoHighlightRange означает, что выделение и видимый диапазон элементов в представлении вообще не связаны.
  • Значение ListView.StrictlyEnforceRange гарантирует, что выделение всегда будет видимым. Если действие попытается переместить выделение за пределы видимой части представления, текущий элемент изменится соответствующим образом, поэтому выделение останется видимым.
  • Золотая середина – это значение ListView.ApplyRange. Оно пытается сохранить выделение видимым, но не изменяет текущий элемент, чтобы обеспечить это. Вместо этого выделенному фрагменту разрешается перемещаться из поля зрения, если это необходимо.

В конфигурации по умолчанию представление отвечает за перемещение выделения в нужное положение. Скорость движения и изменение размера можно контролировать либо как скорость, либо как продолжительность. Используемые свойства: highlightMoveSpeed, highlightMoveDuration, highlightResizeSpeed ​​и highlightResizeDuration. По умолчанию скорость установлена ​​на 400 пикселей в секунду, а продолжительность установлена на -1, что указывает на то, что скорость и расстояние определяют продолжительность. Если установлены и скорость, и продолжительность, выбирается то, что обеспечивает самую быструю анимацию.

Чтобы более детально управлять движением выделения, можно для свойства highlightFollowCurrentItem установить значение false. Это означает, что представление больше не отвечает за перемещение делегата выделения. Вместо этого движением можно управлять с помощью Behavior или анимации.

В приведенном ниже примере свойство y делегата выделения привязано к присоединенному свойству ListView.view.currentItem.y. Это гарантирует, что выделение следует за текущим элементом. Однако, поскольку мы не позволяем представлению перемещать выделение, мы можем контролировать, как перемещается этот элемент. Это делается через Behavior on y. В приведенном ниже примере движение разделено на три этапа: исчезновение, перемещение, появление. Обратите внимание, как для создания более сложного движения можно использовать элементы SequentialAnimation и PropertyAnimation в сочетании с NumberAnimation.

Component {
    id: highlightComponent

    Item {
        width: ListView.view ? ListView.view.width : 0
        height: ListView.view ? ListView.view.currentItem.height : 0

        y: ListView.view ? ListView.view.currentItem.y : 0

        Behavior on y {
            SequentialAnimation {
                PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 0; duration: 200 }
                NumberAnimation { duration: 1 }
                PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 1; duration: 200 }
            }
        }

        GreenBox {
            id: highlightRectangle
            anchors.fill: parent
        }
    }
}

«Шапка» (header) и «подвал» (footer)

В каждый конец содержимого ListView можно вставить элементы header и footer. Их можно считать специальными делегатами, помещенными в начало или конец списка. Для горизонтального списка они будут отображаться не вверху или в внизу, а в начале или в конце, в зависимости от используемого значения layoutDirection.

В приведенном ниже примере показано, как можно использовать header и footer для улучшения восприятия начала и конца списка. Есть и другие способы использования этих специальных элементов списка. Например, их можно использовать для хранения кнопок для загрузки большего количества контента.

import QtQuick
import "../common"

Background {
    width: 240
    height: 300

    ListView {
        anchors.fill: parent
        anchors.margins: 20

        clip: true

        model: 4
        delegate: numberDelegate
        header: headerComponent
        footer: footerComponent

        spacing: 2
    }

    Component {
        id: headerComponent

        YellowBox {
            width: ListView.view ? ListView.view.width : 0
            height: 20
            text: 'Header'

        }
    }

    Component {
        id: footerComponent

        YellowBox {
            width: ListView.view ? ListView.view.width : 0
            height: 20
            text: 'Footer'
        }
    }

    Component {
        id: numberDelegate

        GreenBox {
            required property int index
            
            width: ListView.view.width
            height: 40
            
            text: 'Item #' + index
        }
    }
}
header и footer

Подсказка

Делегаты header и footer не учитывают свойство spacing (интервал между элементами) ListView, вместо этого они размещаются непосредственно рядом со следующим делегатом элемента в списке. Это означает, что любой интервал должен быть частью элементов верхнего и нижнего колонтитула.

GridView

Использование GridView очень похоже на использование ListView. Единственное реальное отличие состоит в том, что представление сетки помещает делегатов в двумерную сетку, а не в линейный список.

GridView

По сравнению с представлением списка, представление сетки не зависит от интервала и размера своих делегатов. Вместо этого оно использует свойства cellWidth и cellHeight для управления размерами делегатов контента. Затем каждый элемент делегата помещается в верхний левый угол каждой такой ячейки.

import QtQuick
import "../common"

Background {
    width: 220
    height: 300

    GridView {
        id: view
        anchors.fill: parent
        anchors.margins: 20

        clip: true

        model: 100

        cellWidth: 45
        cellHeight: 45

        delegate: GreenBox {
            required property int index
            width: 40
            height: 40
            text: index
        }
    }
}

GridView содержит «шапку» и «подвал», может использовать делегат выделения и поддерживает режимы привязки, а также различные варианты поведения границ. Он также может быть ориентирован в различных направлениях и ориентациях.

Ориентация управляется с помощью свойства flow. Для него можно задать значение GridView.LeftToRight или GridView.TopToBottom. Первое значение заполняет сетку слева направо, добавляя строки сверху вниз. Представление можно прокручивать в вертикальном направлении. Последнее значение добавляет элементы сверху вниз, заполняя представление слева направо. Направление прокрутки в этом случае горизонтальное.

В дополнение к свойству flow свойство layoutDirection может адаптировать направление сетки для языков слева направо или справа налево, в зависимости от используемого значения.

Теги

GridViewGUI / Графический интерфейс пользователяListViewMVD / model-view-delegate / модель-представление-делегатQMLQtQtQuickДелегатПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.