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. В якості першого стовпця — імена файлів або каталогів. Елементи цього стовпця також можуть бути дочірні елементи, якщо це каталог. Інші стовпці містять різну інформацію про файл розмір, час модифікації і т. п. Таку структуру даних можна зустріти в будь-якому файловому менеджері:

Між моделлю і поданням можна вставити спеціальну проксі-модель. Такі моделі перехоплюють виклики до основної моделі і можуть приховувати певні елементи, змінювати їх порядок, впливати на отримання та запис даних і т. п. В 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 <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 цей прапор не перевіряється і модель буде редагованої і без його установки, але я б рекомендував не нехтувати ним, т. к. в майбутніх версіях перевірку на це можуть додати.
2. C++-списки
У якості моделі можна використовувати списки рядків або об'єктів типу 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. Відповідно, це пасивна модель і додавання/видалення елементів не впливає на уявлення.
3. QQmlListProperty
Цей клас дозволяє зробити список, який можна наповнювати як в 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-масиви. Так що саме на них я рекомендую в першу чергу звернути увагу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.