В этой части моего цикла стсатей про Model-View в QML мы начнем рассматривать представления и начнем с тех, которые делаются на основе готовых компонентов.
Содержание:
- Model-View в QML. Часть нулевая, вводная
- Model-View в QML. Часть первая: Представления на основе готовых компонентов
Представление в MVC обеспечивает отображение данных. Это так часть программы, которая определяет, как будут выглядеть данные и, в конечном итоге, что увидит пользователь.
Я уже говорил, что реализация представления в Qt имеет одну существенную особенность: представление здесь объединено с контролем. Зачем так сделано? В графическом интерфейсе нередко одни и те же элементы отвечают за отображение данных и их изменение. В качестве примера можно вспомнить табличный процессор. Каждая ячейка не только отображает данные но и отвечает за их изменение, а значит выполняет функции не только представления, но и контроля. Так что решение объединить их в одном элементе вполне логичное.
Тут напрашивается мысль, что при объединении двух функций в одном элементе мы теряем гибкость и усложняется само представление. На самом деле нет. Для того, чтобы обеспечить возможность кастомизации представления данных и обработки пользовательского ввода вводится понятие делегата.
Делегат — это компонент, который отображает данные одного элемента модели и обеспечивает их редактирование. Экземпляры делегата создаются для каждого элемента модели и располагаются в представлении, которое по сути представление является контейнером. Именно делегат решает, как должны отображаться и редактироваться данные конкретного элемента и это дает нам большие возможности кастомизации. В приведенном выше примере с табличным процессором мы можем достаточно просто сделать так, чтобы данные в каждой ячейке редактировались при помощи спинбокса или выпадающего списка.
В QML все точно также за исключением того, что делегат пока не может редактировать данные модели.
Подводя итог, у представления в QML есть три задачи:
- создавать экземпляры делегата для каждого элемента в модели;
- расположить эти элементы требуемым образом;
- обеспечить навигацию по элементам.
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, ориентированный в первую очередь на реализацию представления, имеет для этого мощные, и в то же время достаточно простые инструменты. Среди стандартных компонентов есть готовые представления, которые берут на себя значительную часть работы, а пользователь остается только предоставить модель и делегата.
Экземпляры делегатов во многих представлениях создаются и удаляются на лету, поэтому для хорошей производительности и, соответственно, плавной анимации нужно стараться делать их максимально легкими.
Автор: BlackRaven86