Введение
Статья 488 использовала ListModel, удобный для простых, небольших, статичных наборов данных, объявляемых прямо в QML — для реальных приложений, где данные приходят из БД (SQL-цикл основного плана), сети, или требуют сложной логики (сортировка, фильтрация, лень загрузка), используется QAbstractListModel на стороне C++, экспонируемый в QML — этот подход даёт полный контроль над данными при сохранении удобного, виртуализированного отображения через ListView/GridView со стороны QML.
Концепция
QAbstractListModel — базовый класс Qt для реализации пользовательских моделей данных (концептуально аналогичный использованию с QSqlTableModel, статья 437 SQL-цикла, но полностью настраиваемый под произвольный источник и структуру данных) — обязательные переопределяемые методы включают rowCount() (общее количество элементов), data() (значение конкретного поля конкретного элемента) и roleNames() (отображение числовых «ролей» на текстовые имена, используемые в QML-делегатах для доступа к полям модели по имени, аналогично name/amount в примере статьи 488). Экземпляр такой модели, созданный на стороне C++, передаётся в QML через контекстное свойство или регистрацию типа (статья 490) и используется в ListView.model точно так же, как и встроенный ListModel.
Пример кода: QAbstractListModel и интеграция
// Пользовательская модель данных на C++ — список заказов из произвольного источника
#include <QAbstractListModel>
struct Order { QString name; double amount; };
class OrderListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles { NameRole = Qt::UserRole + 1, AmountRole };
explicit OrderListModel(QObject *parent = nullptr) : QAbstractListModel(parent) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return m_orders.size();
}
QVariant data(const QModelIndex &index, int role) const override
{
if (!index.isValid() || index.row() >= m_orders.size()) return {};
const Order &order = m_orders.at(index.row());
switch (role) {
case NameRole: return order.name;
case AmountRole: return order.amount;
default: return {};
}
}
QHash<int, QByteArray> roleNames() const override
{
return { {NameRole, "name"}, {AmountRole, "amount"} }; // соответствие имени поля в QML
}
void addOrder(const Order &order)
{
beginInsertRows(QModelIndex(), m_orders.size(), m_orders.size());
m_orders.append(order);
endInsertRows(); // обязательное уведомление видов QML о добавлении нового элемента
}
private:
QList<Order> m_orders;
};
// Использование C++ модели в QML — синтаксически идентично встроенному ListModel
import QtQuick
ListView {
width: 300; height: 400
model: orderListModel // экземпляр OrderListModel, переданный из C++ (статья 490)
delegate: Rectangle {
width: ListView.view.width
height: 40
Text { text: name + ": " + amount + " ₽" } // те же имена ролей, что в roleNames()
}
}
Пояснения к коду
rowCount()/data()/roleNames() — три обязательных метода, формирующих минимальный, необходимый контракт модели — roleNames() особенно важен для интеграции с QML, поскольку именно он определяет, под какими именами ("name", "amount") поля модели становятся доступны непосредственно в QML-делегатах, аналогично встроенным полям ListElement. addOrder() с явными вызовами beginInsertRows()/endInsertRows() показывает обязательный паттерн уведомления — без этих вызовов, окружающих изменение внутреннего списка m_orders, подключённый ListView не узнает о добавлении нового элемента и не обновит визуальное представление, даже если сами данные модели были корректно изменены. Использование model: orderListModel в QML выглядит синтаксически идентично использованию встроенного ListModel из статьи 488 — это и есть основная практическая ценность данного подхода: вся сложность реального источника данных полностью скрыта за тем же, простым и единообразным интерфейсом модели, ожидаемым ListView/GridView.
Подводные камни
- Изменение внутренних данных модели (
m_orders) без соответствующих вызововbeginInsertRows()/endInsertRows()/beginRemoveRows()/endRemoveRows()/dataChanged()— это, аналогично подводному камню статьи 488 проListModel, нарушает контракт уведомления подключённых видов об изменениях, иListViewможет продолжать отображать устаревшие данные или, что хуже, обратиться к уже несуществующему индексу, что может привести к ошибке выполнения, если внутренний список изменился, аListViewне был об этом корректно уведомлен. - Неэффективная реализация
data()с дорогими, повторяющимися вычислениями при каждом обращении к конкретному полю — посколькуdata()вызывается потенциально очень часто (для каждого видимого делегата, для каждой запрошенной роли), эта функция должна быть максимально быстрой, и любые дорогие вычисления (обращение к БД, сложная агрегация) должны быть выполнены заранее, с сохранением уже готового результата во внутренней структуре модели, а не пересчитываться заново при каждом вызовеdata(). - Использование
Qt::UserRoleбез явного добавления смещения (Qt::UserRole + 1,Qt::UserRole + 2) для разных пользовательских ролей —Qt::UserRoleсам по себе является базовым значением, зарезервированным как начальная точка для пользовательских ролей, и использование его напрямую без смещения для нескольких разных ролей привело бы к их совпадению (всем ролям было бы присвоено одно и то же числовое значение), что сделало бы невозможным их различение вdata(). - Прямое владение и передача указателя на объект модели в QML без явного контроля его времени жизни, особенно при динамическом создании/удалении модели во время выполнения — если C++ код удаляет объект модели, пока QML-сцена всё ещё держит ссылку на него (через
model: orderListModel), это может привести к обращению к уже освобождённой памяти; для надёжного управления временем жизни такая модель обычно либо передаётся как контекстное свойство с понятным, долгоживущим владельцем (статья 490), либо явно управляется черезQQmlEngine::setObjectOwnership().