Pull to refresh

Model-View в QML. Часть вторая: Кастомные представления

Reading time11 min
Views32K
Не всегда готовые представления идеально подходят. Рассмотрим компоненты, которые позволяют создать полностью кастомизированное представление и добиться большой гибкости в построении интерфейса. И еще от меня небольшой бонус для терпеливых читателей :)

Model-View в QML:
  1. Model-View в QML. Часть нулевая, вводная
  2. Model-View в QML. Часть первая: Представления на основе готовых компонентов
  3. Model-View в QML. Часть вторая: Кастомные представления
  4. Model-View в QML. Часть третья: Модели в QML и JavaScript
  5. Model-View в QML. Часть четвертая: C++-модели


1. PathView

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

Основное предназначение PathView — это не просто отображать данные, а еще и делать так, чтобы это выглядело визуально привлекательно. Именно с помощью этого компонента делаются такие вещи, как CoverFlow (популярный вариант отображения обложек альбомов в мультимедиа проигрывателях).

1) простой пример

Начнем с простого примера, расположив элементы вдоль кривой (если быть совсем точным, то используется квадратичная кривая Безье)

import QtQuick 2.0

Rectangle {
    width: 500
    height: 200

    PathView {
        id: view

        anchors.fill: parent
        model: 30

        path: Path {
            startX: 0
            startY: 0

            PathQuad {
                x: view.width
                y: 0
                controlX: view.width / 2
                controlY: view.height
            }
        }
        delegate: Rectangle {
            width: 20
            height: 20
            color: "orchid"
            border {
                color: "black"
                width: 1
            }
        }
    }
}

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

Параметры startX и startY описывают координаты начала пути. Заканчивается же он там, где заканчивается последний его участок. У участка же наоборот, задается не начало, а конец, при помощи параметров x и y. В нашем случае, кривая строится по трем точкам: помимо конца и начала, нужна еще одна координата, от которой зависит, каким будет изгиб. За ее координаты отвечают свойства controlX и controlY. Для участка пути координаты задаются относительно родителя, т.е. объекта Path. Есть еще специальные свойства, которые позволяют задавать координаты относительно начала пути. Такие свойства имеют префикс relative (например, relativeControlY).

Посмотрим, что у нас получилось:



Поскольку все элементы помещены в PathView, то мы можем потащить элементы мышью и будут перемещаться вдоль пути.

2) замкнутый путь

В предыдущем примере путь незамкнутый. После того, как элемент достигает его пути, он появляется в начале. Ненамного сложнее сделать так, чтобы путь был замкнутым. Для этого нужно, чтобы координаты его начала и конца совпадали. У объекта Path есть даже специальное свойство closed, которое отражает, замкнут ли он.

Немного переделаем первый пример и сделаем замкнутый путь из двух кривых Безье (PathQuad):

import QtQuick 2.0

Rectangle {
    width: 400
    height: 400

    PathView {
        id: view

        anchors.fill: parent
        model: 50

        path: Path {
            startX: view.width / 2
            startY: view.height / 2

            PathQuad {
                relativeX: 0
                y: view.height
                controlX: view.width
                controlY: 0
            }
            PathQuad {
                relativeX: 0
                y: view.height / 2
                controlX: 0
                controlY: 0
            }
        }
        delegate: Rectangle {
            width: 20
            height: 20
            color: "hotpink"
            border {
                color: "black"
                width: 1
            }
        }
    }
}

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



3) элементы пути

Помимо рассмотренной кривой, в QtQuick есть кубические кривые Безье — то же что и квадратичная, но с двумя контрольными точками (PathCubic), кривая с произвольным количеством точек (PathCurve), дуга — т.е. часть окружности (PathArk) и прямая линия (PathLine). В добавок, можно задать кривую описанием в формате SVG при помощи компонента PathSvg. Все эти компоненты можно комбинировать и составить из них нужный путь.

Есть еще дополнительные компоненты, которые не контролируют размещение элементов и их параметры. Одним из одним из них является PathPercent, позволяющий контролировать распределение элементов по участкам пути. По умолчанию элементы распределяются равномерно, но с помощью этого компонента можно для частей пути задавать, какая часть элементов количество там будет располагаться. Для этого после участка пути помещается объект PathPercent, параметром value которого содержит часть элементов для этого пути (например, 0,5 для половины элементов).

Рассмотрим это на примере:

import QtQuick 2.0

Rectangle {
    width: 500
    height: 200

    PathView {
        id: view

        anchors.fill: parent
        model: 20

        path: Path {
            startX: 0
            startY: height

            PathCurve {
                x: view.width / 5
                y: view.height / 2
            }
            PathCurve {
                x: view.width / 5 * 2
                y: view.height / 4
            }
            PathPercent { value: 0.49 }
            PathLine {
                x: view.width / 5 * 3
                y: view.height / 4
            }
            PathPercent { value: 0.51 }
            PathCurve {
                x: view.width / 5 * 4
                y: view.height / 2
            }
            PathCurve {
                x: view.width
                y: view.height
            }
            PathPercent { value: 1 }
        }
        delegate: Rectangle {
            width: 20
            height: 20
            color: "orchid"
            border {
                color: "black"
                width: 1
            }
        }
    }
}

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



Другим дополнительным элементом пути является PathAttribute, позволяющий контролировать параметры элементов, в зависимости от их расположения на пути. В делегате эти параметры будут доступны через присоединенные свойства PathView.имя, где имя задается при помощи свойства name.

PathAttribute задает значения параметров в той точке пути, в которой он находится. Значения параметров элементов на участке пути, который находится между двумя объектами PathAttribute будут плавно переходить от значений одного PathAttribute к другому. Если с одной стороны этого объекта нет, то будут браться нулевые значения.

Сделаем так, чтобы центральный объект был в два раза больше и чтобы крайние элементы были полупрозрачными:

import QtQuick 2.0

Rectangle {
    property int itemSize: 20

    width: 500
    height: 200

    PathView {
        id: view

        anchors.fill: parent
        model: 20

        path: Path {
            startX: 0
            startY: height

            PathAttribute { name: "size"; value: itemSize }
            PathAttribute { name: "opacity"; value: 0.5 }
            PathCurve {
                x: view.width / 5
                y: view.height / 2
            }
            PathCurve {
                x: view.width / 5 * 2
                y: view.height / 4
            }
            PathPercent { value: 0.49 }
            PathAttribute { name: "size"; value: itemSize * 2 }
            PathAttribute { name: "opacity"; value: 1 }
            PathLine {
                x: view.width / 5 * 3
                y: view.height / 4
            }
            PathAttribute { name: "size"; value: itemSize * 2 }
            PathAttribute { name: "opacity"; value: 1 }
            PathPercent { value: 0.51 }
            PathCurve {
                x: view.width / 5 * 4
                y: view.height / 2
            }
            PathCurve {
                x: view.width
                y: view.height
            }
            PathPercent { value: 1 }
            PathAttribute { name: "size"; value: itemSize }
            PathAttribute { name: "opacity"; value: 0.5 }
        }
        delegate: Rectangle {
            width: PathView.size
            height: PathView.size
            color: "orchid"
            opacity: PathView.opacity
            border {
                color: "black"
                width: 1
            }
        }
    }
}

И получим элементы, в которых плавно изменяется размер и прозрачность:



Если не нужен плавный переход, а нужно просто сделать разного размера элементы, можно окружить каждый такой участок пути объектами PathView, а между соседними участками вставить дополнительные участки пути нулевой длины, на которых и будет проходить переход от одних значений параметров к другим. Но за счет нулевого размера, элементов там нет и видно этого не будет.

Чтобы продемонстрировать, немного доработаем предыдущий пример и сделаем так, чтобы каждая из трех частей пути была окружена объектами PathAttribute, а между этими частями поместим объекты PathLine нулевой длины.

import QtQuick 2.0

Rectangle {
    property int itemSize: 20

    width: 500
    height: 200

    PathView {
        id: view

        anchors.fill: parent
        model: 20

        path: Path {
            startX: 0
            startY: height

            PathAttribute { name: "size"; value: itemSize }
            PathAttribute { name: "opacity"; value: 0.5 }
            PathCurve {
                x: view.width / 5
                y: view.height / 2
            }
            PathCurve {
                x: view.width / 5 * 2
                y: view.height / 4
            }
            PathAttribute { name: "size"; value: itemSize }
            PathAttribute { name: "opacity"; value: 0.5 }
            PathPercent { value: 0.49 }

            PathLine { relativeX: 0; relativeY: 0 } // разделитель

            PathAttribute { name: "size"; value: itemSize * 2 }
            PathAttribute { name: "opacity"; value: 1 }
            PathLine {
                x: view.width / 5 * 3
                y: view.height / 4
            }
            PathAttribute { name: "size"; value: itemSize * 2 }
            PathAttribute { name: "opacity"; value: 1 }
            PathPercent { value: 0.51 }

            PathLine { relativeX: 0; relativeY: 0 } // разделитель

            PathAttribute { name: "size"; value: itemSize }
            PathAttribute { name: "opacity"; value: 0.5 }
            PathCurve {
                x: view.width / 5 * 4
                y: view.height / 2
            }
            PathCurve {
                x: view.width
                y: view.height
            }
            PathPercent { value: 1 }
            PathAttribute { name: "size"; value: itemSize }
            PathAttribute { name: "opacity"; value: 0.5 }
        }
        delegate: Rectangle {
            width: PathView.size
            height: PathView.size
            color: "orchid"
            opacity: PathView.opacity
            border {
                color: "black"
                width: 1
            }
        }
    }
}

В итоге получим маленькие полупрозрачные элементы по краям и один большой и непрозрачный элемент в центре:



4) CoverFlow

В начале раздела я упоминал про CoverFlow. В качестве бонуса тем, кто дочитал до этого места, небольшой примерчик реализации :)

import QtQuick 2.0

Rectangle {
    property int itemAngle: 60
    property int itemSize: 300

    width: 1200
    height: 400

    ListModel {
        id: dataModel

        ListElement {
            color: "orange"
            text: "first"
        }
        ListElement {
            color: "lightgreen"
            text: "second"
        }
        ListElement {
            color: "orchid"
            text: "third"
        }
        ListElement {
            color: "tomato"
            text: "fourth"
        }
        ListElement {
            color: "skyblue"
            text: "fifth"
        }
        ListElement {
            color: "hotpink"
            text: "sixth"
        }
        ListElement {
            color: "darkseagreen"
            text: "seventh"
        }
    }

    PathView {
        id: view

        anchors.fill: parent
        model: dataModel
        pathItemCount: 6

        path: Path {
            startX: 0
            startY: height / 2

            PathPercent { value: 0.0 }
            PathAttribute { name: "z"; value: 0 }
            PathAttribute { name: "angle"; value: itemAngle }
            PathAttribute { name: "origin"; value: 0 }
            PathLine {
                x: (view.width - itemSize) / 2
                y: view.height / 2
            }
            PathAttribute { name: "angle"; value: itemAngle }
            PathAttribute { name: "origin"; value: 0 }
            PathPercent { value: 0.49 }
            PathAttribute { name: "z"; value: 10 }


            PathLine { relativeX: 0; relativeY: 0 }

            PathAttribute { name: "angle"; value: 0 }
            PathLine {
                x: (view.width - itemSize) / 2 + itemSize
                y: view.height / 2
            }
            PathAttribute { name: "angle"; value: 0 }
            PathPercent { value: 0.51 }

            PathLine { relativeX: 0; relativeY: 0 }

            PathAttribute { name: "z"; value: 10 }
            PathAttribute { name: "angle"; value: -itemAngle }
            PathAttribute { name: "origin"; value: itemSize }
            PathLine {
                x: view.width
                y: view.height / 2
            }
            PathPercent { value: 1 }
            PathAttribute { name: "z"; value: 0 }
            PathAttribute { name: "angle"; value: -itemAngle }
            PathAttribute { name: "origin"; value: itemSize }
        }
        delegate: Rectangle {
            property real rotationAngle: PathView.angle
            property real rotationOrigin: PathView.origin

            width: itemSize
            height: width
            z: PathView.z
            color: model.color
            border {
                color: "black"
                width: 1
            }
            transform: Rotation {
                axis { x: 0; y: 1; z: 0 }
                angle: rotationAngle
                origin.x: rotationOrigin
            }

            Text {
                anchors.centerIn: parent
                font.pointSize: 32
                text: model.text
            }
        }
    }
}

Для начала, посмотрим получившийся результат, а затем разберем реализацию. А получилось у нас примерно такое:



Все элементы, кроме центрального мы поворачиваем вокруг оси Y. Для этого мы задаем делегатам трансформацию вращения при помощи компонента Rotation. В свойстве axis нужно установить 1 для тех осей, вокруг которых объект будет поворачиваться.
У элементов мы меняем несколько параметров: угол поворота, расположение по оси Z и точку поворота (origin). С углом все просто и очевидно: элементы, которые находятся слева поворачиваются на 60 градусов, а те что справа соответственно на -60. А вот на остальных параметрах стоит остановиться поподробнее.

Координата Z определяет, какой элемент будет находится «выше», т.е. когда два объекта в каком-то месте пересекаются, тот объект, у которого меньше Z будет перекрыт объектом, у которого координата Z больше. По умолчанию в PathView элемент с большим индексом перекрывает предыдущий. В CoverFlow для элементов слева нужно чтобы было наоборот: «выше» находятся те элементы, которые ближе к центру. Если ничего не предпринять, то последний элемент будет налазить на предпоследний, а тот в свою очередь на элемент перед ним и т.д. Поэтому мы меняем координату Z так, чтобы чем дальше находился от центра элемент, тем «ниже» он был. В нашем примере размеры такие, что элементы не перекрываются, но если немного уменьшить ширину окна, то перекрытие сразу появится:



Наконец, точка поворота. Мы задаем точку на нашем прямоугольнику, вокруг которой будет происходит поворот. По умолчанию это верхний левый угол, т.е. точка с координатами (0, 0). Т.к. мы вращаем элемент вокруг оси Y, то сама координата Y здесь значения не имеет. А вот на X обратить внимание стоит. В случае элементов, находящихся в левой части, эту координаты мы устанавливаем в 0 и поворачиваем элемент вокруг левого края и получается, что правый край визуально становится дальше. Если сделать так же для элементов справа, то получится, что элементы слева мы поворачиваем «от себя», а элементы справа — «к себе», т.е. левый край будет близко, а правый станет еще ближе и правая сторона будет больше. В итоге, мы получаем такую ситуацию, что элементы слева и справа будут разного размера, что нам совсем не нужно. Мы все элементы поворачиваем «от себя» и для этого в элементах справа смещаем точку поворота в правый верхний угол, чтобы они вращались вокруг своего правого края.

В предыдущих примерах в PathView отображались все элементы из модели. Количество одновременно отображаемых элементов можно ограничить при помощи параметра pathItemCount. Здесь я установил его равным шести.

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

Краткий итог

PathView — это компонент, ориентированный в первую очередь на создание привлекательного интерфейса. Данный инструмент обладает большой гибкостью, позволяя разместить элементы не только по прямой линии, но и по произвольному пути, а также менять параметры делегата в зависимости от того, в какой части пути он находится.

2. Свое представление

QML дает нам инструменты сделать свое представление, если у нас возникнет такая потребность. Это не сильно сложно и реализуется при помощи комбинирования простых элементов.

Для начала нам нужно создавать объекты делегата на каждый элемент модели. Для этого мы применим специальный компонент — Repeater. Он занимается исключительно созданием элементов, никакого позиционирования и т.п. вещей он не делает. Используется он точно так же, как и компоненты *View: мы задаем ему модель и делегата и он будет создавать экземпляры делегата на каждый элемент модели.

Для позиционирования мы можем применить элементы Row и Column, в которые и поместим наш Repeater. Элементы созданные при помощи Repeater становятся дочерними элементами его родителя, т.е. в данном случае Row или Column, которые позиционируют свои элементы в виде строки или столбца соответственно.

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

Для примера сделаем так, чтобы элементы располагались не вертикально, а горизонтально:

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360

    ListModel {
        id: dataModel

        ListElement {
            color: "orange"
            text: "first"
        }
        ListElement {
            color: "lightgreen"
            text: "second"
        }
        ListElement {
            color: "orchid"
            text: "third"
        }
        ListElement {
            color: "tomato"
            text: "fourth"
        }
    }

    Flickable {
        anchors.fill: parent
        contentWidth: row.width

        Row {
            id: row

            height: parent.height

            Repeater {
                model: dataModel
                delegate: Item {
                    height: parent.height
                    width: 100

                    Rectangle {
                        anchors.margins: 5
                        anchors.fill: parent
                        color: model.color
                        border {
                            color: "black"
                            width: 1
                        }

                        Text {
                            anchors.centerIn: parent
                            renderType: Text.NativeRendering
                            text: model.text
                        }
                    }
                }
            }
        }
    }
}

Высоту элемента Row мы задаем фиксированной, а ширина автоматически будет меняться, в зависимости от суммарной ширины его дочерних элементов. У Flickable мы устанавливаем contentWidth — это, как нетрудно догадаться, ширина его содержимого. Если она больше ширины самого Flickable, он даст возможность их прокрутки. В нашем примере последний элемент как раз не влазит и можно убедиться, что прокрутка работает.



Как видим, библиотека QtQuick позволяет обойтись без использования готовых представлений и создать из простых компонентов свое, которое будет также хорошо работать.

Выводы

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

PathView предназначен для создания отображения, ориентированного на красивый внешний вид и анимацию и позволяет задавать траекторию движения элементов, варьировать параметры элемента, в зависимости от его расположения и плотность расположения элементов на разных участках пути.
Tags:
Hubs:
Total votes 15: ↑15 and ↓0+15
Comments4

Articles