Программист C++/Qt и QML
0,0
рейтинг
24 июня 2013 в 10:29

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

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

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++-модели


Представление в 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 {
            id: listDelegate

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

onClicked: console.log(ListView.isCurrentItem)

И на всех элементах он будет выдавать false, даже на текущем.

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

Первый способ хорош тем, что не создается дополнительное свойство. Второй способ стоит использовать, если реализация делегата выносится в отдельный QML-файл. Например, если вынести его в файл ListDelegate.qml, можно написать такой код:

ListView {
    delegate: ListDelegate {
        isCurrent:  ListView.isCurrentItem
    }
}

Это позволяет не привязывать реализацию делегата к ListView и поменять отображение (например на Repeater + Column) без изменений в коде делегата.

У 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
карма
35,0
рейтинг 0,0
Программист C++/Qt и QML
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (10)

  • +1
    Сразу скажу, что модель должна все равно быть в виде списка. Передать туда настоящую C++-модель таблицы, т.е. Производный класс от QAbstractTableModel не получится — будет видна только первая колонка.
    А какой тогда смысл в TableVIew?
    • +1
      Смысл в том, что можно отображать данные в виде таблицы. Я немного дополнил статью:
      Один элемент модели соответствует строке таблицы, а данные для столбцов берутся из ролей элемента.
      • +1
        А, вот так, ясно.
    • 0
      Передать в QML-представление C++ таблицу, унаследованную от QAbstractTableModel вполне себе можно, просто надо перед этим поиграться с делегатами в этой самой модели.
      Подробнее с примерами:
      qt-project.org/wiki/How_to_use_a_QSqlQueryModel_in_QML
      • 0
        Это по сути то же самое, что и модель-список с разными ролями для одного элемента, только тут еще и костыль для преобразования из столбцов в роли. Если модель с данными строго в виде таблицы и ее менять нежелательно, на мой взгляд, лучше написать прокси-модель, которая будет переводить таблицу в список.
  • +2
    Радует то, что производительность в QtQuick 2 выросла настолько, что теперь без шуток его можно использовать в продакшене. Летает даже на андроиде :)
    • +1
      Да, производительность неплохая. Теперь хорошо бы баги починили :)
  • 0
    Интересна подкапотная реализация работы с делегатами, то есть как все работает under the hood, так сказать. Поясню. Портирую настольное приложение на андроид, QML + C++. По некоторым причинам пришлось отказаться от ListView и пришлось изобретать свой велосипед соблюдая принципы MVC. Отрисовка данных в делегаты сделана практически в лоб, для каждого элемента в списке создается инстанс делегата и свойства связываются с элементом модели, все это на JS. Как думаете, по какому принципу это реализовано в ListView, так же либо есть код на C++ отвечающий за это?
    • 0
      ListView написан на плюсах. Если интересно, реализацию можно посмотреть в qtdeclarative/src/quick/items/qquicklistview. Вообще, из стандартных компонентов на QML написаны только QtQuick Controls, да и то частично.

      пришлось изобретать свой велосипед

      Использовали Column/Row + Repeater + Flickable или какой-то другой способ?
      • 0
        Да, конечно, именно с этого и начинал. Довольно сложные делегаты используются (с точки зрения логики), плюс нужны особые жесты, а Flickable не дает все что нужно. С велосипедом код вышел проще, и моя отрисовка через делегаты на чистом JS сносно работает, но хотелось бы найти способ оптимальнее, думал может сталкивались. Спасибо за наводку

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