Введение
Для отображения динамических списков данных в QML (через ListView, GridView, Repeater) недостаточно простого JS-массива, когда данные приходят с сервера, регулярно обновляются или требуют эффективной работы с большими объёмами. QAbstractListModel — базовый класс Qt для реализации собственных моделей данных на C++, предоставляющих данные в QML через стандартный интерфейс модели/представления. В статье построим модель списка задач (todo list) с поддержкой добавления, удаления и изменения статуса элементов.
Концепция
QAbstractListModel требует переопределения как минимум rowCount() (число элементов) и data() (значение конкретной роли для конкретной строки). Роли данных (Qt::UserRole + N) объявляются через roleNames() — это сопоставление числового идентификатора роли со строковым именем, под которым свойство будет доступно в QML-делегате (model.title, model.completed). Любое изменение данных модели должно сопровождаться правильными сигналами (beginInsertRows/endInsertRows, dataChanged и так далее), без них представление в QML не узнает об изменении и не обновится визуально.
Пример кода: Модели данных в QML
// todolistmodel.h
#pragma once
#include <QAbstractListModel>
#include <QString>
struct TodoItem
{
QString title;
bool completed;
};
class TodoListModel : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
TitleRole = Qt::UserRole + 1,
CompletedRole
};
explicit TodoListModel(QObject *parent = nullptr) : QAbstractListModel(parent) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
if (parent.isValid()) return 0;
return m_items.size();
}
QVariant data(const QModelIndex &index, int role) const override
{
if (!index.isValid() || index.row() >= m_items.size()) return {};
const TodoItem &item = m_items.at(index.row());
switch (role) {
case TitleRole: return item.title;
case CompletedRole: return item.completed;
}
return {};
}
QHash<int, QByteArray> roleNames() const override
{
return {
{ TitleRole, "title" },
{ CompletedRole, "completed" }
};
}
Q_INVOKABLE void addItem(const QString &title)
{
beginInsertRows(QModelIndex(), m_items.size(), m_items.size());
m_items.append({title, false});
endInsertRows();
}
Q_INVOKABLE void toggleCompleted(int row)
{
if (row < 0 || row >= m_items.size()) return;
m_items[row].completed = !m_items[row].completed;
const QModelIndex idx = index(row);
emit dataChanged(idx, idx, {CompletedRole});
}
Q_INVOKABLE void removeItem(int row)
{
if (row < 0 || row >= m_items.size()) return;
beginRemoveRows(QModelIndex(), row, row);
m_items.removeAt(row);
endRemoveRows();
}
private:
QList<TodoItem> m_items;
};
// main.qml
import QtQuick
import QtQuick.Controls
import App.Models 1.0
ApplicationWindow {
visible: true
width: 360
height: 480
TodoListModel { id: todoModel }
Component.onCompleted: {
todoModel.addItem("Изучить QAbstractListModel");
todoModel.addItem("Написать тесты");
}
Column {
anchors.fill: parent
anchors.margins: 10
Row {
TextField { id: newItemField; placeholderText: "Новая задача" }
Button {
text: "Добавить"
onClicked: {
todoModel.addItem(newItemField.text);
newItemField.text = "";
}
}
}
ListView {
width: parent.width
height: 350
model: todoModel
delegate: Row {
spacing: 8
CheckBox {
checked: model.completed
onClicked: todoModel.toggleCompleted(index)
}
Text {
text: model.title
font.strikeout: model.completed
}
Button {
text: "✕"
onClicked: todoModel.removeItem(index)
}
}
}
}
}
Пояснения к коду
roleNames() сопоставляет внутренние числовые роли с именами "title"/"completed", которые становятся доступны в QML-делегате через model.title/model.completed для каждой строки. Методы addItem, toggleCompleted, removeItem помечены Q_INVOKABLE, что делает их вызываемыми напрямую из QML — каждый из них правильно оборачивает изменение данных нужными сигналами модели (beginInsertRows/endInsertRows для вставки, beginRemoveRows/endRemoveRows для удаления, dataChanged для изменения существующей строки), что заставляет ListView корректно и эффективно обновить только затронутую часть отображения, а не перерисовывать список целиком.
В QML ListView напрямую привязывается к экземпляру TodoListModel через свойство model, а каждый delegate автоматически получает контекстные свойства model (доступ к ролям текущей строки) и index (порядковый номер строки) без явной передачи.
Подводные камни
- Изменение данных без
beginInsertRows/endInsertRows(или соответствующих парных вызовов). Прямая модификация внутреннего контейнераm_itemsбез уведомления через эти методы оставляет представление в QML несинхронизированным с реальными данными — в лучшем случае список не обновится визуально, в худшем — обращение к несуществующему индексу вызовет краш. - Несовпадение диапазона индексов в
beginInsertRows(parent, first, last)с реально вставленным количеством элементов. Указание неверного диапазона (например, вставка одного элемента, но указание диапазона на два) приводит к неконсистентному состоянию модели представления, которое может проявляться как пропущенные или дублирующиеся элементы вListView. - Производительность при частых поштучных изменениях большого списка. Множественные отдельные вызовы
beginInsertRows/endInsertRowsдля каждого элемента в цикле менее эффективны, чем один вызов для целого диапазона — при массовой вставке стоит группировать изменения в один вызов с правильным диапазоном, если это возможно. - Забытое
Q_INVOKABLEили неверная сигнатура метода. Без этого макроса метод недоступен для вызова из QML — попытка вызватьtodoModel.removeItem(index)для метода безQ_INVOKABLEтихо завершится ошибкой в QML-консоли о неизвестном методе, что легко принять за опечатку в имени, а не за отсутствие экспорта метода.