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

в 6:29, , рубрики: Model View Controller, mvc, QML, qt, qt quick, Qt Software, Программирование, Проектирование и рефакторинг, метки: , , , ,

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

Содержание:

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


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

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

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

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

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

В 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 и сам его перемещает за текущим элементом.

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

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

Еще один важный момент касательно видимости присоединенных свойств в делегате. В отличие от данных модели, присоединенные свойства действительны только в самом делегате, но не в его дочерних объектах. Т.е. мы не можем использовать 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"
    }
}

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

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

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.

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

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

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. Если элемент будет меньше ячейки — будут отступы. Если больше — будут налезать друг на друга :)

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

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

Помимо возможности расположения элементов сеткой и отсутствия 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.

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

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

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

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

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

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

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

Выводы

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

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

Автор: BlackRaven86

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js