Реклама

среда, 5 марта 2014 г.

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

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


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 предназначен для создания отображения, ориентированного на красивый внешний вид и анимацию и позволяет задавать траекторию движения элементов, варьировать параметры элемента, в зависимости от его расположения и плотность расположения элементов на разных участках пути.

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

В этой части моего цикла статей про Model-View в QML мы начнем рассматривать представления и начнем с тех, которые делаются на основе готовых компонентов.

Представление в MVC обеспечивает отображение данных. Это такая часть программы, которая определяет, как будут выглядеть данные и, в конечном итоге, что увидит пользователь.

Я уже говорил, что реализация представления в Qt имеет одну существенную особенность: представление здесь объединено с контролем. Зачем так сделано? В графическом интерфейсе нередко одни и те же элементы отвечают за отображение данных и их изменение. В качестве примера можно вспомнить табличный процессор. Каждая ячейка не только отображает данные но и отвечает за их изменение, а значит выполняет функции не только представления, но и контроля. Так что решение объединить их в одном элементе вполне логичное.

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



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

В QML все точно также за исключением того, что делегат пока не может редактировать данные модели.

Подводя итог, у представления в QML есть три задачи:
  1. создавать экземпляры делегата для каждого элемента в модели;
  2. расположить эти элементы требуемым образом;
  3. обеспечить навигацию по элементам.

1. ListView

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

Этот компонент дает нам возможность отобразить объекты в виде списка. Вопросы навигации также решены — компонент обрабатывает события от мыши и клавиатуры, позволяя листать элементы жестами используя мышь или сенсорный экран, при помощи скролла мыши, а также с клавиатуры.

1) простые примеры использования

У ListView (да и у большинства других представлений) есть понятие текущего элемента. Какой элемент текущий определяется свойством currentIndex и сам текущий элемент доступен через свойство currentItem. Также, у каждого делегата есть присоединенное свойство ListView.isCurrentItem, которое будет иметь значение true, если этот элемент текущий. Это дает нам возможность выделить текущий элемент, чтобы он отображался как-то по-другому.

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

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

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"
        }
    }

    ListView {
        id: view

        anchors.margins: 10
        anchors.fill: parent
        spacing: 10
        model: dataModel
        clip: true

        highlight: Rectangle {
            color: "skyblue"
        }
        highlightFollowsCurrentItem: true

        delegate: Item {
            property var view: ListView.view
            property var isCurrent: ListView.isCurrentItem

            width: view.width
            height: 40

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

                Text {
                    anchors.centerIn: parent
                    renderType: Text.NativeRendering
                    text: "%1%2".arg(model.text).arg(isCurrent ? " *" : "")
                }

                MouseArea {
                    anchors.fill: parent
                    onClicked: view.currentIndex = model.index
                }
            }
        }
    }
}

Мы используем присоединенное свойство ListView.isCurrentItem в делегате для определения, является ли этот элемент текущим и в текущем элементе отображаем помимо текста еще звездочку (символ *). Чтобы по клику мышью на элементе он мог установить себя текущим нам нужен доступ к объекту ListView и мы получаем при помощи свойства ListView.view. Здесь это свойство используется для демонстрации, его не обязательно использовать и можно обращаться к этому объекту напрямую, т.к. этот объект и так находится в области видимости делегата. Но если делегат определен в другом qml-файле, то в области видимости делегата уже не будет объекта ListView и это свойство как раз позволит получить к нему доступ.

В качестве подсветки используется простой цветной прямоугольник. Размер его устанавливается ListView и сам его перемещает за текущим элементом.

Запустив программу, мы можем менять текущий элемент кликом мыши и видеть, как подсветка перемещается за ним:



Еще один важный момент касательно видимости присоединенных свойств в делегате. В отличие от данных модели, присоединенные свойства действительны только в самом делегате, но не в его дочерних объектах. Т.е. мы не можем использовать ListView.isCurrentItem в элементе Text и нам приходится использовать для этого промежуточное свойство isCurrent. Эта особенность может быть неочевидна, учитывая что сами присоединенные свойства в объекте видны. Как пример, можно заменить обработчик на клик в MouseArea на следующий:

onClicked: console.log(ListView.isCurrentItem)

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

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

header: Rectangle {
    width: view.width
    height: 40
    border {
        color: "black"
        width: 1
    }

    Text {
        anchors.centerIn: parent
        renderType: Text.NativeRendering
        text: "Header"
    }
}

footer: Rectangle {
    width: view.width
    height: 40
    border {
        color: "black"
        width: 1
    }

    Text {
        anchors.centerIn: parent
        renderType: Text.NativeRendering
        text: "Footer"
    }
}

В итоге получим примерно такой результат:



2) Секции

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

Рассмотрим это на следующем примере.

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360

    ListModel {
        id: dataModel

        ListElement {
            type: "bird"
            text: "penguin"
        }
        ListElement {
            type: "bird"
            text: "raven"
        }
        ListElement {
            type: "reptile"
            text: "lizard"
        }
        ListElement {
            type: "reptile"
            text: "turtle"
        }
        ListElement {
            type: "reptile"
            text: "crocodile"
        }
    }

    ListView {
        id: view

        anchors.margins: 10
        anchors.fill: parent
        spacing: 10
        model: dataModel
        clip: true

        section.property: "type"
        section.delegate: Rectangle {
            width: view.width
            height: 40
            color: "lightgreen"
            Text {
                anchors.centerIn: parent
                renderType: Text.NativeRendering
                font.bold: true
                text: section
            }
        }

        delegate: Rectangle {
            width: view.width
            height: 40
            border {
                color: "black"
                width: 1
            }

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

Мы указываем поле type для разбиения на группы. Соответственно, все элементы с одинаковым значением этого поля объединяются в одну группу. Можно сделать так, чтобы в группу объединялись элементы, у которых первая буква совпадает (например для адресной книги). Для этого свойству section.criteria нужно установить значение ViewSection.FirstCharacter.

Запустив программу, мы получим такой результат:



3) О производительности

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

ListView может создавать элементы не только для той области, которая видна сейчас, а с некоторым запасом. Объекты в этой области создаются асинхронно, чтобы не мешать работе интерфейса. Соответственно, чем больше будет таких элементов, тем меньше вероятность лагов прокрутки, но и потребление памяти растет. Количество таких элементов контролируется специальным параметром — cacheBuffer. Он определяет размер области в пикселях за границей видимой части, для которой будут создаваться объекты. Чтобы понять, сколько будет дополнительно создано объектов, нужно поделить это значение на высоту (или ширину, если ListView имеет горизонтальное расположение), и умножить это значение на два, поскольку таких областей две.

Я, поработав некоторое время на пятой версии Qt, как-то собрал и запустил свой проект на четвертой версии. И заметил, что прокрутка элементов ощутимо лагает. Копнув чуть глубже, я заметил, что в Qt 5.0 по умолчанию cacheBuffer имеет значение 320, а в Qt 4.8 — 0. Увеличив размер кэша, прокрутка стала заметно плавнее. Но даже так заметно, что в пятой версии провели хорошую работу по ускорению — по сравнения с четвертой версией, разница видна невооруженным глазом.

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

Исходя из вышесказанного, можно сделать два вывода, касательно производительности:

  • нужно создавать максимально легкие делегаты, с минимальным количеством привязок (особенно это касается сложных JavaScript-выражений в привязках);
  • если есть проблемы с прокруткой, стоит поэкспериментировать с размером буфера.

2. GridView

Этот компонент похож ListView, но позволяет расположить элементы сеткой. Сетка построчно заполняется слева направо (по умолчанию). Соответственно, если элементов будет меньше, в конце будут пустые места.

Немного адаптированный для использования GridView первый пример:

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"
        }
    }

    GridView {
        id: view

        anchors.margins: 10
        anchors.fill: parent
        cellHeight: 100
        cellWidth: cellHeight
        model: dataModel
        clip: true

        highlight: Rectangle {
            color: "skyblue"
        }

        delegate: Item {
            property var view: GridView.view
            property var isCurrent: GridView.isCurrentItem

            height: view.cellHeight
            width: view.cellWidth

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

                Text {
                    anchors.centerIn: parent
                    renderType: Text.NativeRendering
                    text: "%1%2".arg(model.text).arg(isCurrent ? " *" : "")
                }

                MouseArea {
                    anchors.fill: parent
                    onClicked: view.currentIndex = model.index
                }
            }
        }
    }
}

В отличие от ListView, здесь нет свойства spacing. Вместо этого задается размер ячейки при помощи cellHeight и cellWidth. Если элемент будет меньше ячейки — будут отступы. Если больше — будут налезать друг на друга :)

Результат выполнения программы:



Помимо возможности расположения элементов сеткой и отсутствия spacing, этот компонент имеет еще одно отличие от ListView — нет секций. В остальном же все сказанное о ListView справедливо и для GridView.

3. TableView

Некоторые данные удобнее всего отображать в виде таблицы. Для этого в Qt применяется класс QTableView. В QML с появлением модуля QtQuick Controls, появился готовый компонент для создания табличного представления данных.

Сразу скажу, что модель должна все равно быть в виде списка. Один элемент модели соответствует строке таблицы, а данные для столбцов берутся из ролей элемента. Передать туда настоящую C++-модель таблицы, т.е. производный класс от QAbstractTableModel не получится — будет видна только первая колонка. 

В определении объекта TableView мы указываем, какие столбцы должны быть и какая роль из данных модели должна быть использована для каждого столбца.

Рассмотрим пример.

import QtQuick 2.0
import QtQuick.Controls 1.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"
        }
    }

    TableView {
        id: view

        anchors.margins: 10
        anchors.fill: parent
        model: dataModel
        clip: true

        TableViewColumn {
            width: 100
            title: "Color"
            role: "color"
        }
        TableViewColumn {
            width: 100
            title: "Text"
            role: "text"
        }

        itemDelegate: Item {
            Text {
                anchors.centerIn: parent
                renderType: Text.NativeRendering
                text: styleData.value
            }
        }
    }
}

Одна важная особенность касательно данных модели в делегате. Вид компонентов из QtQuick Controls настраивается при помощи стилей из QtQuick Controls Styles и по умолчанию используется такой стиль, чтобы компоненты выглядели как нативные для текущей платформы. По сути, эти компоненты объединяют модель и представление, а стиль является делегатом. Данные из модели в стиле доступны при помощи свойства styleData. В TableView делегат используется похожим образом со стилями и данные в нем доступны через объект styleData.

В результате получим такую таблицу:



В этом примере используется свойство itemDelegate, которое задает делегата для всех ячеек таблицы. Что делать, если для какого-то столбца нужно отображать данные немного по-другому? Можно задать делегат для данного конкретного столбца при его определении в TableViewColumn. Например, в первой колонке у нас цвет отображается текстом. Сделаем так, чтобы вместо этого ячейка закрашивалась этим цветом.

TableViewColumn {
    width: 100
    title: "Color"
    role: "color"
    delegate: Rectangle {
        color: styleData.value
    }
}

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



Для всей строки тоже есть свой делегат (свойство rowDelegate). С его помощью можно настроить такие вещи, как высота столбца, цвет фона и т.п.
TableView позволяет делать таблицы на чистом QML и отображать их так, чтобы они выглядели как нативные, но при этом позволяя гибко настроить их внешний вид. Такой компонент может сильно пригодиться для создания десктопных программ с интерфейсом на QML. Но не смотря на возможность выглядеть как десктопный компонент, TableView не работает с чистыми табличными моделями и может обработать только данные, представленные в виде списка.

Выводы

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

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

Model-View в QML. Часть нулевая, вводная

Одним из наиболее распространенных и эффективных приемов проектирования программ является использование шаблона программирования MVC (Model-View-Controller) — Модель-Представление-Контроль. MVC позволяет разделить части программы, отвечающие за хранение и доступ к данным, отображение данных и за взаимодействие с пользователем на отдельные слабо связанные модули. Подобное разделение ответственности упрощает структуру программы и позволяет вносить изменения в одну из этих частей не затрагивая остальные.

Такой подход активно применяется в Qt, а в QML вообще является краеугольным камнем. Так что тем, кто изучает QML понимание принципов MVC будет совсем не лишним.

Что такое MVC

Разделение ответственности является одним из основополагающих принципов программирования. Суть его состоит в том, чтобы разделять программу на функциональные блоки, каждый из которых отвечает за свою часть работы. Этими блоками могут быть и функции, и объекты, и модули и т.д. Блоки между собой связываются через определенные интерфейсы. Это дает некоторую независимость блоков относительно друг друга и снижает сложность системы.

Основные концепции графический интерфейс пользователя (GUI) были разработаны в лаборатории Xerox в 70-х годах двадцатого века. В их число входит и шаблон проектирования MVC, призванный снизить сложность и упростить архитектуру программ с графическим интерфейсом при помощи того самого разделения ответственности.

Итак, суть этого шаблона состоит в разделении программы на три компонента.

  • Модель. Отвечает за данные и обеспечивает доступ к ним.
  • Представление. Отвечает за отображение данных, полученных из модели.
  • Контроль. Отвечает за взаимодействие с пользователем. Может изменять данные в модели.


Схематически модель можно изобразить так:



Между этими компонентами есть определенные интерфейсы, позволяющие развязать их между собой. В идеале эти модули вообще должны независимы друг от друга и позволять вносить изменения или даже полностью заменить какой-либо модуль без переделки остальных.
С развитием графических интерфейсов и появлением новых видов программ, таких, например, как веб-приложения, такой вариант разделения не не везде стал подходить. Поэтому появились его разновидности, такие как MVVC, MVP и т.д. Я буду дальше все разновидности этой модели обобщенно называть MVC.

Немало современных фреймворков используют MVC и содержат в себе средства для разделения программы в соответствии с этим шаблоном. Поскольку одним из основных предназначений Qt является разработка графических интерфейсов пользователя, то в нем без MVC тоже не обошлось и Qt включает в себя свою вариацию — Model-View фреймворк.

MVC в Qt и QML

Основная идея применения данного шаблона в Qt — разделение данных и их отображения.



Здесь модель отвечает за данные и доступ к ним. Поскольку в графическом интерфейсе за отображение элементов и получение ввода от пользователя нередко отвечают одни и те же элементы, то вполне логична идея объединить представление и контроль. Именно так и сделано в Qt: представление не только отображает данные, но и выполняет функции контроля и отвечает за взаимодействие с пользователем. Но для того, чтобы из-за такого объединения не терять гибкость, было введено понятие делегата. Делегат позволяет определять, как эти данные будут отображаться и как пользователь может их изменять. Представление же, по сути, теперь является контейнером для экземпляров делегата.

В QML этот принцип тоже применяется, даже в гораздо большей степени. Как я уже говорил, Model-View является одной из основополагающих концепций QML. Одно из основных предназначений QML — это выделение интерфейса программы в отдельную часть, которая может быть очень гибкой и легко адаптироваться для разных нужд. Например, для десктопного приложение может быть один вариант интерфейса, а для мобильного другой. При этом ядро программы (модель) может быть написано на C++ и оставаться неизменным.

Также стоит отметить, что в QML делегаты могут только отображать данные, но не могут напрямую их редактировать, в отличие от Qt.
Несмотря на то, что интерфейсная часть является основным местом для применения QML, модель на нем тоже можно можно реализовать, есть для этого соответствующие инструменты. При желании, программу можно сделать практически полностью на QML/JavaScript и свести количество кода на C++ к минимуму. Именно с такой программы мы и начнем.

Простой пример MVC в QML

Рассмотрим программу, которая выводит пару цветных прямоугольников с текстом посередине.

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360

    ListModel {
        id: dataModel

        ListElement {
            color: "orange"
            text: "first"
        }
        ListElement {
            color: "skyblue"
            text: "second"
        }
    }

    ListView {
        id: view

        anchors.margins: 10
        anchors.fill: parent
        spacing: 10
        model: dataModel

        delegate: Rectangle {
            width: view.width
            height: 100
            color: model.color

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

Моделью здесь является объект типа ListModel. В качестве элементов модели используются два дочерних объекта специального типа ListElement, в которых мы определили два свойства: color и text.

Для отображения мы используем объект типа ListView, которому в качестве модели мы указываем id нашего объекта типа ListModel. Само по себе представление не знает, какие отображать наши данные, поэтому нужно задать ему еще и делегат. Делегатом в QML является тип объекта, экземпляр которого создается на каждый элемент модели. А ListView, в свою очередь, создает эти объекты, отображает их в виде списка и обеспечивает навигацию по ним.

В качестве делегата выступает прямоугольник с текстом посредине. Цвет прямоугольника и текст берутся из модели. Внутри делегата видны все свойства элементов модели — т.н. «роли». Они доступны через специальный объект model, а также и напрямую, т.е. можно писать просто color. Я предпочитаю писать полностью, так нагляднее, откуда берутся данные и не будет никаких конфликтов, если у делегата есть свойства с такими же именами.

Результат выполнения выглядит примерно так:



Динамика в Model-View

В MVC модель может быть как пассивной, так и активной.

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

Активная же модель может оповещать представления о том, что данные изменились. В свою очередь, представление может отобразить эти изменения. Именно такой вариант считается классической реализацией MVC.

В Qt и QML применяется активная модель. Это дает нам возможность изменять данные в модели и не беспокоится об отображении обновленных данных — представление все сделает за нас само.

Намного доработаем предыдущий пример: добавим кнопку, которая будет добавлять элементы в модель при нажатии.

import QtQuick 2.0

Rectangle {
    width: 360
    height: 360

    ListModel {
        id: dataModel
    }

    Column {
        anchors.margins: 10
        anchors.fill: parent
        spacing: 10

        ListView {
            id: view

            width: parent.width
            height: parent.height - button.height - parent.spacing
            spacing: 10
            model: dataModel
            clip: true

            delegate: Rectangle {
                width: view.width
                height: 40
                color: "skyblue"

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

        Rectangle {
            id: button

            width: 100
            height: 40
            anchors.horizontalCenter: parent.horizontalCenter
            border {
                color: "black"
                width: 1
            }

            Text {
                anchors.centerIn: parent
                renderType: Text.NativeRendering
                text: "Add"
            }

            MouseArea {
                anchors.fill: parent
                onClicked: dataModel.append({})
            }
        }
    }
}

Изначально модель не содержит никаких элементов. При нажатии на кнопку в модель добавляется пустой элемент без свойств. Модель информирует представление о том, что добавился новый элемент. На каждый новый элемент представление создает объект, тип которого мы указали в качестве делегата. Помимо тех свойств, которые мы описываем в элементах модели, в делегате будет доступно еще одно — index (или model.index), содержащее индекс элемента в модели и, соответственно, в отображении. Так что, несмотря на то, что мы добавляем пустой элемент в модель, одно свойство там все-таки есть и именно индекс мы используем в качестве текста.

Запустив программу и понажимав на кнопку получим примерно такой вид:



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

Небольшой вывод

Разработанная более 30 лет назад, технология MVC ничуть не утратила своей актуальности. И по сей день она является основой для проектирования программ с графическим интерфейсом; сейчас сложно представить себе такую программу, не использующую MVC.
Как технология, разработанная для создания графических интерфейсов, Qt и, особенно, QML имеют полную поддержку MVC. Необходимые инструменты входят в состав библиотек, более того, в самом Qt, достаточно четко прослеживается идеология этого шаблона.
Соответственно применение MVC здесь является естественным и именно так задумано разработчиками и именно с таким видением был спроектирован фреймворк.

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

вторник, 4 марта 2014 г.

Сборка и запуск Qt приложения на платформе Android.
Версия Qt - 5.1.1, QtCreator - 2.8.1. Актуально на сентябрь 2013.