Model-View в QML. Часть четвертая: C++-модели

Поскольку основное предназначение QML — это создание интерфейсов, то в соответствии с шаблоном MVC, на нем реализуются представление и контроль. Для реализации же модели, совершенно логично напрашивается C++. Здесь у нас будет гораздо меньше ограничений и мы сможем реализовать модель любой сложности. Кроме того, если значительная часть программы написана на C++ и данные поступают именно оттуда, то лучше всего там же поместить и модель.

От использования такой модели может отпугнуть кажущаяся сложность реализации. Я не стану спорить с тем, что C++ не самый простой язык. Он посложнее QML и требует больше осторожности, чтобы не выстрелить себе в ногу, это факт. Несмотря на это, на практике не все так уж и страшно.

Во-первых, не будем забывать, что мы пишем не на чистом С++, а с использованием Qt. Такие вещи как parent-child в QObject, implicit sharing для контейнеров, сигналы и слоты, QVariant и многое другое очень сильно упрощают и автоматизируют работу с памятью, чем избавляют разработчика от массы головной боли и повышают надежность. Иногда даже создается впечатление, что пишешь на динамическом языке программирования. Это же сокращает пропасть между QML и C++, делая переход между ними более-менее плавным.

Во-вторых, все модели QML в конечном итоге приводятся к этим самым C++-моделям, только мы получаем упрощенный вариант и не самое максимальное быстродействие. Если уже есть понимание, как работать с моделями на QML, то с C++-моделями будет справиться проще. Мы просто узнаем в процессе чуть больше низкоуровневой информации, заодно улучшится понимание, как все это работает.

В общем, освоить C++-модели очень даже стоит. В особенности это касается QAbstractItemModel, с которой мы и начнем.

Model-View в QML:


1. C++-модель QAbstractItemModel

Это стандартная модель из фреймворка Qt Model-View. Этот класс обладает богатыми возможностями и позволяет строить модели различной сложности.

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

QAbstractItemModel наоборот, более обобщенная версия. Каждый элемент таблицы может иметь еще и дочерние элементы, тоже организованные в виде таблицы. Таким образом, при помощи этой таблицы можно организовать древовидную структуру. В Qt есть принятое правило, что дочерние элементы могут иметь только элементы первого столбца и при использовании представлений из Qt, таких как QTreeView нужен именно такой формат, но никто не запрещает организовать модель так, как удобно. Как примером такой модели, можно привести класс QFileSystemModel. В качестве первого столбца — имена файлов или каталогов. У элементов этого столбца также могут быть дочерние элементы, если это каталог. Остальные столбцы содержат различную информацию о файле — размер, время модификации и т.п. Такую структуру данных можно встретить в любом файловом менеджере:


1d9363e8ad3b45e9a68dedb8d7e7684f.png

Между моделью и представлением можно вставить специальную прокси-модель. Такие модели перехватывают вызовы к основной модели и могут скрывать определенные элементы, менять их порядок, влиять на получение и запись данных и т.п. В Qt есть готовый класс QSortFilterProxyModel, которая может представлять данные модели в отсортированном и/или отфильтрованном виде. Если ее функционала недостаточно, можно создать свою прокси-модель, отнаследовавшись от этого класса или от QAbstractProxyModel.

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

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

Для модели-списка нужно создать производный класс от QAbstractListModel и определить такие методы:


  • rowCount () — возвращает количество строк, в нашем случае это количество элементов;
  • data () — возвращает данные элемента;
  • roleNames () — возвращает список ролей, которые доступны в делегате. По умолчанию определены следующие роли: display, decoration, edit, toolTip, statusTip и whatsThis. В четвертой версии Qt вместо переопределения этой функции нужно было вызвать функцию setRoleNames (), которая устанавливала нужные имена ролей.

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

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

Если нам нужна модель с древовидной структурой, мы используем QAbstractItemModel. У этой модели надо будет дополнительно определить такие функции:


  • parent () — возвращает индекс родителя элемента;
  • index () — возвращает индекс элемента.

В моделях Qt, обращение к элементам идет через специальные индексы — объекты типа QModelIndex. Они содержат в себе номер строки и столбца, индекс родительского элемента и некоторые дополнительные данные. Корневой элемент модели имеет недействительный индекс QModelIndex (). Так что если у нас простой список или таблица — у всех элементов родительский элемент будет именно таким. В случае дерева, такой родитель будет только у элементов верхнего уровня. Функция index () получает индекс родителя и номер строки и столбца элемента, должна возвращать индекс элемента. Индексы создаются при помощи функции createIndex ().

По сути, сложности начинаются тогда, когда нам нужна вложенность, а так все достаточно просто.

В качестве примера рассмотрим модель-список. Данные будем хранить в этом же объекте в виде списка строк. Еще сделаем функцию add (), которая будет добавлять еще один элемент в модель и пометим ее специальным макросом Q_INVOKABLE, чтобы ее можно было вызывать из QML.

Определение класса:

#include 
#include 

class TestModel : public QAbstractListModel
{
    Q_OBJECT

public:
    enum Roles {
        ColorRole = Qt::UserRole + 1,
        TextRole
    };

    TestModel(QObject *parent = 0);

    virtual int rowCount(const QModelIndex &parent) const;
    virtual QVariant data(const QModelIndex &index, int role) const;
    virtual QHash roleNames() const;

    Q_INVOKABLE void add();

private:
    QStringList m_data;
};

Мы определяем две роли ColorRole и TextRole и используем для них значения больше Qt: UserRole — именно там заканчиваются зарезервированные значения для Qt. Соответственно, для пользовательских ролей надо использовать значения начиная с Qt: UserRole.

Реализация методов класса:

TestModel::TestModel(QObject *parent):
    QAbstractListModel(parent)
{
    m_data.append("old");
    m_data.append("another old");
}

int TestModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid()) {
        return 0;
    }

    return m_data.size();
}

QVariant TestModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid()) {
        return QVariant();
    }

    switch (role) {
    case ColorRole:
        return QVariant(index.row() < 2 ? "orange" : "skyblue");
    case TextRole:
        return m_data.at(index.row());
    default:
        return QVariant();
    }
}

QHash TestModel::roleNames() const
{
    QHash = QAbstractListModel::roleNames();
    roles[ColorRole] = "color";
    roles[TextRole] = "text";

    return roles;
}

void TestModel::add()
{
    beginInsertRows(QModelIndex(), m_data.size(), m_data.size());
    m_data.append("new");
    endInsertRows();

    m_data[0] = QString("Size: %1").arg(m_data.size());
    QModelIndex index = createIndex(0, 0, static_cast(0));
    emit dataChanged(index, index);
}

Поскольку QML обращается к ролям используя строковые имена вместо целочисленных констант, мы определим для них имена: color и text. Перед добавлением мы вызываем специальную функцию beginInsertRows (), которая издаст нужные сигналы, чтобы представление было в курсе, что готовится добавление элементов и куда они будут добавляться. А после, вызываем функцию endInsertRows (), которая опять таки издаст сигналы о том, что в модель добавились элементы. Все добавления нужно оборачивать таким образом. Есть подобные функции и для удаления и перемещения элементов.

В функции add () меняем текст первого элемента, чтобы он показывал количество элементов в списке. После этого издаем сигнал dataChanged (), чтобы информировать об этом представление. Сигналу передаем параметрами начальный и конечный индекс изменившихся данных (у нас один и тот же). Индекс получаем при помощи функции createIndex (), которой параметрами передается строка, столбец и указатель на приватные данные. В качестве последнего обычно используется указатель на объект с данными, но в нашем случае можно упростить и всегда использовать NULL.

В качестве программы на QML немного переделаем второй пример. C++-модель реализована в виде подключаемого модуля (плагина). В начале файла добавим его импортирование:

import TestModel 1.0

Создадим объект этого типа и используем его в качестве модели:

TestModel {
    id: dataModel
}

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


b06a279a004a44e0b2b0fb3d89096748.png

Для редактирования данных модели в делегате предусмотрен стандартный интерфейс и для его использования необходимом в нашей модели переопределить метод setData (). Возможность редактирования данных QAbstractItemModel из QML появилась в Qt 5.

Добавим в заголовочный файл такие объявления:

virtual bool setData(const QModelIndex &index, const QVariant &value, int role);
virtual Qt::ItemFlags flags(const QModelIndex &index) const;

и в файл реализации определения:

bool TestModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (!index.isValid()) {
        return false;
    }

    switch (role) {
    case ColorRole:
        return false;   // This property can not be set
    case TextRole:
        m_data[index.row()] = value.toString();
        break;
    default:
        return false;
    }

    emit dataChanged(index, index, QVector() << role);

    return true;
}

Qt::ItemFlags TestModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::ItemIsEnabled;

    return QAbstractListModel::flags(index) | Qt::ItemIsEditable;
}

Мы добавили в модель возможность редактировать свойство text прямо из делегата при помощи подобного кода:

model.text = "Some new text"

Отредактируем делегат из нашего примера и добавим в него такой элемент:

MouseArea {
    anchors.fill: parent
    onDoubleClicked: model.text = "Edited"
}

Теперь при двойном клике на элементе, его текст будет меняться на «Edited».

Флаг Qt: ItemIsEditable используется для отображений Qt, чтобы показать, что элемент можно редактировать, поэтому метод flags () необходимо переопределить. На данный момент в QML этот флаг не проверяется и модель будет редактируемой и без его установки, но я бы рекомендовал не пренебрегать им, т.к. в будущих версиях проверку на это могут добавить.


2. C++-списки

В качестве модели можно использовать списки строк либо объектов типа QObject.

Сделаем простой класс с свойством типа QStringList:

#include 
#include 

class TestModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QStringList data READ data CONSTANT)
public:
    TestModel(QObject *parent = 0);

    QStringList data() const;
};

TestModel::TestModel(QObject *parent):
    QObject(parent)
{

}

QStringList TestModel::data() const
{
    return QStringList() << "orange" << "skyblue";
}

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

model: dataModel.data

а в качестве текста используется индекс элемента:

text: model.index

Такой список работает также, как и массив JavaScript. Соответственно, это пассивная модель и добавление/удаление элементов не влияет на представление.


3. QQmlListProperty

Этот класс позволяет сделать список, который можно наполнять как в C++, так и в QML. Наполнение в QML выполняется статически при создании объекта (как это делается с ListModel). В C++ можно и добавлять/удалять элементы, так что если сделать специальный метод и пометить его макросом Q_INVOKABLE, то можно будет это делать и из QML.

В списках такого типа могут хранится объекты типа QObject и производных от него типов. В типе стоит определить все свойства, которые будут использоваться (при помощи Q_PROPERTY).

Рассмотрим пример такого объекта.

#include 

class Element : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString color READ color WRITE setColor NOTIFY colorChanged)
    Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged)
public:
    explicit Element(QObject *parent = 0);

    QString color() const;
    void setColor(QString color);
    QString text() const;
    void setText(QString text);

signals:
    void colorChanged(QString color);
    void textChanged(QString text);

private:
    QString m_color;
    QString m_text;
};

Element::Element(QObject *parent) :
    QObject(parent)
{
}

QString Element::color() const
{
    return m_color;
}

void Element::setColor(QString color)
{
    if (m_color == color) {
        return;
    }

    m_color = color;

    emit colorChanged(m_color);
}

QString Element::text() const
{
    return m_text;
}

void Element::setText(QString text)
{
    if (m_text == text) {
        return;
    }

    m_text = text;

    emit textChanged(m_text);
}

Мы создали простой класс, содержащий два свойства — color и text, геттеры, сеттеры и нотификаторы для них.

Для того, чтобы объекты этого типа можно было использовать в QQmlListProperty, это тип должен быть виден в QML. А для этого нужно зарегистрировать этот тип при помощи функции qmlRegisterType (). Я использую C++-плагин, поэтому регистрирую этот тип в специальном обработчике, вместе с моделью:

void TestModelPlugin::registerTypes(const char *uri)
{
    qmlRegisterType(uri, 1, 0, "TestModel");
    qmlRegisterType(uri, 1, 0, "Element");
}

Для того, чтобы использовать QQmlListProperty, нужно создать в каком-либо объекте свойство типа QQmlListProperty, где T — это тип объектов, которые нужно хранить. В нашем случае, будет свойство типа QQmlListProperty.

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

Итак, код класса нашей модели:

#include 
#include 

class Element;

class TestModel : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty data READ data NOTIFY dataChanged)
    Q_CLASSINFO("DefaultProperty", "data")
public:
    TestModel(QObject *parent = 0);

    QQmlListProperty data();

    Q_INVOKABLE void add();

signals:
    void dataChanged();

private:
    static void appendData(QQmlListProperty *list, Element *value);
    static int countData(QQmlListProperty *list);
    static Element *atData(QQmlListProperty *list, int index);
    static void clearData(QQmlListProperty *list);

    QList m_data;
};

TestModel::TestModel(QObject *parent):
    QObject(parent)
{
    Element *element = new Element(this);
    element->setProperty("color", "lightgreen");
    element->setProperty("text", "eldest");

    m_data << element;
}

QQmlListProperty TestModel::data()
{
    return QQmlListProperty(static_cast(this), static_cast(&m_data),
                                     &TestModel::appendData, &TestModel::countData,
                                     &TestModel::atData, &TestModel::clearData);
}

void TestModel::add()
{
    Element *element = new Element(this);
    element->setProperty("color", "skyblue");
    element->setProperty("text", "new");
    m_data.append(element);

    emit dataChanged();
}

void TestModel::appendData(QQmlListProperty *list, Element *value)
{
    QList *data = static_cast *>(list->data);
    data->append(value);
}

int TestModel::countData(QQmlListProperty *list)
{
    QList *data = static_cast *>(list->data);
    return data->size();
}

Element *TestModel::atData(QQmlListProperty *list, int index)
{
    QList *data = static_cast *>(list->data);
    return data->at(index);
}

void TestModel::clearData(QQmlListProperty *list)
{
    QList *data = static_cast *>(list->data);
    qDeleteAll(data->begin(), data->end());
    data->clear();
}

Как и в примере с QAbstractItemModel, тут есть метод add () для добавления элементов и в конструкторе тоже добавляется элемент.

В методе data () создается объект типа QQmlListProperty. В конструкторе он получает родителя (QObject), указатель на приватные данные, который будет доступен в функциях для работы со списком и сами функции. Во всех функциях первым аргументом передается указатель на объект типа QQmlListProperty у которого в свойстве data находятся наши приватные данные. Я поместил туда список, в котором фактически хранятся объекты типа Element.

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

Для демонстрации такой модели возьмем немного переделанный второй пример.

Подключаем C++-плагин:

import ListProperty_Plugin 1.0
Определяем модель:
TestModel {
    id: dataModel

    data: [
        Element {
            color: "orange"
            text: "old"
        },
        Element {
            color: "lightgray"
            text: "another old"
        }
    ]
}

Свойство data определяется как обычный список. Поскольку мы зарегистрировали тип Element, то такие объекты теперь можно создавать в QML. Стоит заметить, что определение здесь элементов массива data не заменяет те элементы, которые уже есть. Эти элементы добавятся к тому, который определен в конструкторе класса TestModel.

В качестве модели используется не сам объект типа TestModel, а все то же свойство data:

model: dataModel.data

Данные в делегате доступны через modelData:

color: modelData.color

и

text: modelData.text

В свойство data элементы можно добавить только статически, так что используем для этого написанную нами функцию add ():

onClicked: dataModel.add()

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


2e1091e1d83c4e63b3b968b055eda4a4.png

В классе TestModel мы указали data как свойство по умолчанию (при помощи директивы Q_CLASSINFO). Это дает нам возможность определять объекты Element прямо в объекте TestModel и они сами добавятся в нужное свойство. Так что можно упростить определение модели и переписать его так:

TestModel {
    id: dataModel

    Element {
        color: "orange"
        text: "old"
    }
    Element {
        color: "lightgray"
        text: "another old"
    }
}

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


Резюме

Разработка моделей является важной частью не только программирования на QML, но и программирования в целом. Как говорил Фред Брукс: «Покажите блок-схемы, скройте таблицы и я буду озадачен, покажите мне ваши таблицы и, скорее всего, блок-схемы мне не потребуются, они будут очевидны». Именно данные являются центральной темой в программировании. Проектирование структур данных и доступа к ним является ответственной задачей и во многом определяет архитектуру программы.

Знание инструментов, которые мы рассмотрели в этой и предыдущей части помогут вам организовать ваши данные наиболее подходящим образом, а затем вокруг данных и саму программу. Поскольку в QML концепция Model-View является одной из основополагающих, то этих инструментов хватает.

Я рассмотрел различные способы создания моделей. По своему опыту могу сказать, что самые используемые это QAbstractItemModel, ListModel и JavaScript-массивы. Так что именно на них я рекомендую в первую очередь обратить внимание.

© Habrahabr.ru