- PVSM.RU - https://www.pvsm.ru -
Поскольку основное предназначение QML — это создание интерфейсов, то в соответствии с шаблоном MVC, на нем реализуются представление и контроль. Для реализации же модели, совершенно логично напрашивается C++. Здесь у нас будет гораздо меньше ограничений и мы сможем реализовать модель любой сложности. Кроме того, если значительная часть программы написана на C++ и данные поступают именно оттуда, то лучше всего там же поместить и модель.
От использования такой модели может отпугнуть кажущаяся сложность реализации. Я не стану спорить с тем, что C++ не самый простой язык. Он посложнее QML и требует больше осторожности, чтобы не выстрелить себе в ногу, это факт. Несмотря на это, на практике не все так уж и страшно.
Во-первых, не будем забывать, что мы пишем не на чистом С++, а с использованием Qt. Такие вещи как parent-child в QObject, implicit sharing для контейнеров, сигналы и слоты, QVariant и многое другое очень сильно упрощают и автоматизируют работу с памятью, чем избавляют разработчика от массы головной боли и повышают надежность. Иногда даже создается впечатление, что пишешь на динамическом языке программирования. Это же сокращает пропасть между QML и C++, делая переход между ними более-менее плавным.
Во-вторых, все модели QML в конечном итоге приводятся к этим самым C++-моделям, только мы получаем упрощенный вариант и не самое максимальное быстродействие. Если уже есть понимание, как работать с моделями на QML, то с C++-моделями будет справиться проще. Мы просто узнаем в процессе чуть больше низкоуровневой информации, заодно улучшится понимание, как все это работает.
В общем, освоить C++-модели очень даже стоит. В особенности это касается QAbstractItemModel, с которой мы и начнем.
Model-View в QML:
Это стандартная модель из фреймворка Qt Model-View. Этот класс обладает богатыми возможностями и позволяет строить модели различной сложности.
Существуют три базовых класса для таких моделей. QAbstractTableModel представляет данные в виде таблицы, для доступа к данным используется номер строки и столбца. QAbstractListModel представляет данные в виде списка и, можно сказать, является частным случаем предыдущей модели с одним столбцом.
QAbstractItemModel наоборот, более обобщенная версия. Каждый элемент таблицы может иметь еще и дочерние элементы, тоже организованные в виде таблицы. Таким образом, при помощи этой таблицы можно организовать древовидную структуру. В Qt есть принятое правило, что дочерние элементы могут иметь только элементы первого столбца и при использовании представлений из Qt, таких как QTreeView нужен именно такой формат, но никто не запрещает организовать модель так, как удобно. Как примером такой модели, можно привести класс QFileSystemModel. В качестве первого столбца — имена файлов или каталогов. У элементов этого столбца также могут быть дочерние элементы, если это каталог. Остальные столбцы содержат различную информацию о файле — размер, время модификации и т.п. Такую структуру данных можно встретить в любом файловом менеджере:
Между моделью и представлением можно вставить специальную прокси-модель. Такие модели перехватывают вызовы к основной модели и могут скрывать определенные элементы, менять их порядок, влиять на получение и запись данных и т.п. В Qt есть готовый класс QSortFilterProxyModel, которая может представлять данные модели в отсортированном и/или отфильтрованном виде. Если ее функционала недостаточно, можно создать свою прокси-модель, отнаследовавшись от этого класса или от QAbstractProxyModel.
Представления в QML могут отображать только списки. При помощи VisualDataModel можно перемещаться по древовидной структуре, но отображать мы можем только элементы текущего уровня. Если данные нужно хранить в виде дерева и при этом отображать в QML, то стоит либо воспользоваться VisualDataModel либо писать свою прокси-модель, которая превратит это дерево в список.
Для того, чтобы создать свою модель, нам нужно отнаследоваться от одного из базовых классов для моделей и определить обязательные для этой модели методы. Я опишу кратко, что нужно сделать, более подробную информацию можно получить в документации [5]. Рассматривать будем в порядке возрастания сложности.
Для модели-списка нужно создать производный класс от QAbstractListModel и определить такие методы:
Этого достаточно, если не планируется редактировать данные модели при помощи делегата. Редактируемую модель рассмотрим чуть позже.
Для модели-таблицы добавляется еще метод columnCount(), возвращающий количество столбцов. Табличные представления в QML используют элементы из первого столбца и при отображении распределяют роли этого элемента как столбцы. Таким образом, для таблица в QML реализуется при помощи все того же списка и табличную модель вряд ли есть смысл использовать.
Если нам нужна модель с древовидной структурой, мы используем QAbstractItemModel. У этой модели надо будет дополнительно определить такие функции:
В моделях Qt, обращение к элементам идет через специальные индексы — объекты типа QModelIndex. Они содержат в себе номер строки и столбца, индекс родительского элемента и некоторые дополнительные данные. Корневой элемент модели имеет недействительный индекс QModelIndex(). Так что если у нас простой список или таблица — у всех элементов родительский элемент будет именно таким. В случае дерева, такой родитель будет только у элементов верхнего уровня. Функция index() получает индекс родителя и номер строки и столбца элемента, должна возвращать индекс элемента. Индексы создаются при помощи функции createIndex().
По сути, сложности начинаются тогда, когда нам нужна вложенность, а так все достаточно просто.
В качестве примера рассмотрим модель-список. Данные будем хранить в этом же объекте в виде списка строк. Еще сделаем функцию add(), которая будет добавлять еще один элемент в модель и пометим ее специальным макросом Q_INVOKABLE, чтобы ее можно было вызывать из QML.
Определение класса:
#include <QAbstractListModel>
#include <QStringList>
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<int, QByteArray> 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<int, QByteArray> TestModel::roleNames() const
{
QHash<int, QByteArray> = 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<void *>(0));
emit dataChanged(index, index);
}
Поскольку QML обращается к ролям используя строковые имена вместо целочисленных констант, мы определим для них имена: color и text. Перед добавлением мы вызываем специальную функцию beginInsertRows(), которая издаст нужные сигналы, чтобы представление было в курсе, что готовится добавление элементов и куда они будут добавляться. А после, вызываем функцию endInsertRows(), которая опять таки издаст сигналы о том, что в модель добавились элементы. Все добавления нужно оборачивать таким образом. Есть подобные функции и для удаления и перемещения элементов.
В функции add() меняем текст первого элемента, чтобы он показывал количество элементов в списке. После этого издаем сигнал dataChanged(), чтобы информировать об этом представление. Сигналу передаем параметрами начальный и конечный индекс изменившихся данных (у нас один и тот же). Индекс получаем при помощи функции createIndex(), которой параметрами передается строка, столбец и указатель на приватные данные. В качестве последнего обычно используется указатель на объект с данными, но в нашем случае можно упростить и всегда использовать NULL.
В качестве программы на QML немного переделаем второй пример. C++-модель реализована в виде подключаемого модуля (плагина). В начале файла добавим его импортирование:
import TestModel 1.0
Создадим объект этого типа и используем его в качестве модели:
TestModel {
id: dataModel
}
После запуска программы и добавления нескольких элементов получим примерно такой результат:
Для редактирования данных модели в делегате предусмотрен стандартный интерфейс и для его использования необходимом в нашей модели переопределить метод 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<int>() << 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 этот флаг не проверяется и модель будет редактируемой и без его установки, но я бы рекомендовал не пренебрегать им, т.к. в будущих версиях проверку на это могут добавить.
В качестве модели можно использовать списки строк либо объектов типа QObject.
Сделаем простой класс с свойством типа QStringList:
#include <QObject>
#include <QStringList>
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. Соответственно, это пассивная модель и добавление/удаление элементов не влияет на представление.
Этот класс позволяет сделать список, который можно наполнять как в C++, так и в QML. Наполнение в QML выполняется статически при создании объекта (как это делается с ListModel). В C++ можно и добавлять/удалять элементы, так что если сделать специальный метод и пометить его макросом Q_INVOKABLE, то можно будет это делать и из QML.
В списках такого типа могут хранится объекты типа QObject и производных от него типов. В типе стоит определить все свойства, которые будут использоваться (при помощи Q_PROPERTY).
Рассмотрим пример такого объекта.
#include <QObject>
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<TestModel>(uri, 1, 0, "TestModel");
qmlRegisterType<Element>(uri, 1, 0, "Element");
}
Для того, чтобы использовать QQmlListProperty, нужно создать в каком-либо объекте свойство типа QQmlListProperty, где T — это тип объектов, которые нужно хранить. В нашем случае, будет свойство типа QQmlListProperty.
Конструктор QQmlListProperty принимает в качестве аргументов методы, которые будет вызывать движок QML при работе со списком. Это методы для добавления и получения элемента, получения количества элементов и очистки списка. Обязательным является только первый, но лучше определить их все.
Итак, код класса нашей модели:
#include <QObject>
#include <QQmlListProperty>
class Element;
class TestModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<Element> data READ data NOTIFY dataChanged)
Q_CLASSINFO("DefaultProperty", "data")
public:
TestModel(QObject *parent = 0);
QQmlListProperty<Element> data();
Q_INVOKABLE void add();
signals:
void dataChanged();
private:
static void appendData(QQmlListProperty<Element> *list, Element *value);
static int countData(QQmlListProperty<Element> *list);
static Element *atData(QQmlListProperty<Element> *list, int index);
static void clearData(QQmlListProperty<Element> *list);
QList<Element*> 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<Element> TestModel::data()
{
return QQmlListProperty<Element>(static_cast<QObject *>(this), static_cast<void *>(&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<Element> *list, Element *value)
{
QList<Element*> *data = static_cast<QList<Element*> *>(list->data);
data->append(value);
}
int TestModel::countData(QQmlListProperty<Element> *list)
{
QList<Element*> *data = static_cast<QList<Element*> *>(list->data);
return data->size();
}
Element *TestModel::atData(QQmlListProperty<Element> *list, int index)
{
QList<Element*> *data = static_cast<QList<Element*> *>(list->data);
return data->at(index);
}
void TestModel::clearData(QQmlListProperty<Element> *list)
{
QList<Element*> *data = static_cast<QList<Element*> *>(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()
В итоге получим примерно такой результат:
В классе 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-массивы. Так что именно на них я рекомендую в первую очередь обратить внимание.
Автор: BlackRaven86
Источник [6]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/125707
Ссылки в тексте:
[1] Model-View в QML. Часть нулевая, вводная: http://habrahabr.ru/post/181712/
[2] Model-View в QML. Часть первая: Представления на основе готовых компонентов: http://habrahabr.ru/post/184416/
[3] Model-View в QML. Часть вторая: Кастомные представления: http://habrahabr.ru/post/190090/
[4] Model-View в QML. Часть третья: Модели в QML и JavaScript: http://habrahabr.ru/post/195706/
[5] в документации: https://doc.qt.io/qt-5/model-view-programming.html#model-subclassing-reference
[6] Источник: https://habrahabr.ru/post/302428/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.