QtMiddleTechnical

Что такое классы model/view в Qt (QAbstractItemModel, QListView, QTableView)?

Model/View Qt разделяет данные (QAbstractItemModel) и отображение (QListView, QTableView). Кастомная модель реализует rowCount(), columnCount(), data() и сообщает об изменениях через beginInsertRows()/endInsertRows() и emit dataChanged().

Архитектура Model/View в Qt

Qt реализует паттерн Model/View, разделяя данные (модель), их отображение (представление) и редактирование (делегат). Это позволяет использовать одну модель с несколькими представлениями одновременно, а также легко подменять источник данных без изменения UI.

Ключевые классы

  • QAbstractItemModel — базовый класс всех моделей; определяет интерфейс данных.
  • QAbstractListModel — упрощённая база для одномерных списков.
  • QAbstractTableModel — база для двумерных таблиц.
  • QStandardItemModel — готовая модель общего назначения (дерево, таблица, список).
  • QListView — отображает список (одна колонка).
  • QTableView — отображает таблицу (строки и колонки).
  • QTreeView — отображает иерархическое дерево.
  • QSortFilterProxyModel — прокси-модель для сортировки и фильтрации без изменения исходной модели.

Реализация кастомной модели

#include <QAbstractTableModel>
#include <QColor>
#include <vector>

struct Employee {
    QString name;
    int     salary;
};

class EmployeeModel : public QAbstractTableModel {
    Q_OBJECT
public:
    explicit EmployeeModel(QObject *parent = nullptr)
        : QAbstractTableModel(parent) {}

    // Обязательные методы
    int rowCount(const QModelIndex &parent = {}) const override {
        return parent.isValid() ? 0 : static_cast<int>(m_data.size());
    }

    int columnCount(const QModelIndex &parent = {}) const override {
        return parent.isValid() ? 0 : 2;
    }

    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
        if (!index.isValid()) return {};
        const auto &emp = m_data.at(index.row());
        if (role == Qt::DisplayRole) {
            return index.column() == 0 ? emp.name : QString::number(emp.salary);
        }
        if (role == Qt::BackgroundRole && emp.salary > 200000) {
            return QColor(Qt::yellow);  // выделить высокооплачиваемых
        }
        return {};
    }

    // Заголовки столбцов
    QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
        if (role != Qt::DisplayRole || orientation != Qt::Horizontal) return {};
        return section == 0 ? tr("Name") : tr("Salary");
    }

    // Редактируемая модель
    bool setData(const QModelIndex &index, const QVariant &value, int role) override {
        if (role != Qt::EditRole || !index.isValid()) return false;
        if (index.column() == 0) m_data[index.row()].name = value.toString();
        else m_data[index.row()].salary = value.toInt();
        emit dataChanged(index, index, {role});
        return true;
    }

    Qt::ItemFlags flags(const QModelIndex &index) const override {
        return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
    }

    // Добавление строк
    void addEmployee(const Employee &emp) {
        const int row = static_cast<int>(m_data.size());
        beginInsertRows({}, row, row);  // ОБЯЗАТЕЛЬНО перед изменением данных
        m_data.push_back(emp);
        endInsertRows();
    }

private:
    std::vector<Employee> m_data;
};

Использование модели с представлениями

#include <QApplication>
#include <QTableView>
#include <QListView>
#include <QSortFilterProxyModel>
#include <QHeaderView>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);

    auto *model = new EmployeeModel;
    model->addEmployee({"Alice", 180000});
    model->addEmployee({"Bob",   250000});
    model->addEmployee({"Carol", 150000});

    // Прокси для сортировки — не трогает исходную модель
    auto *proxy = new QSortFilterProxyModel;
    proxy->setSourceModel(model);
    proxy->setFilterKeyColumn(0);          // фильтровать по колонке «Name»
    proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);

    QTableView tableView;
    tableView.setModel(proxy);
    tableView.setSortingEnabled(true);     // клик по заголовку сортирует
    tableView.horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    tableView.show();

    // Тот же model в listView (только первая колонка)
    QListView listView;
    listView.setModel(model);              // исходная модель, без прокси
    listView.show();

    return app.exec();
}

QModelIndex и иерархия

QModelIndex идентифицирует ячейку через тройку (row, column, internalPointer/internalId). Для плоских моделей parent() всегда возвращает невалидный индекс. Для деревьев internalPointer указывает на родительский узел.

Подводные камни

  • Отсутствие beginInsertRows/endInsertRows: изменение m_data без этих вызовов не уведомляет представления — данные обновятся, но UI останется пустым или с мусором.
  • dataChanged без правильного диапазона: если передать неверные индексы в emit dataChanged(), представление может не перерисоваться или перерисовать не те ячейки.
  • QStandardItemModel для больших данных: удобна, но медленна при тысячах строк — каждый QStandardItem — отдельный объект в куче. Для больших объёмов реализуйте кастомную модель.
  • rowCount с валидным parent: для плоских моделей rowCount должен возвращать 0, если parent.isValid(), иначе QTreeView воспримет каждую строку как узел с дочерними элементами.
  • Прямое удаление строк без beginRemoveRows: crash или UB, потому что представление хранит постоянные индексы.
  • Игнорирование роли в data(): реализация, которая возвращает данные только для DisplayRole, не поддерживает EditRole, ToolTipRole, SortRole — это ломает встроенные делегаты и прокси.
  • Хранение QModelIndex между операциями: после beginInsertRows/beginRemoveRows сохранённые индексы становятся невалидными; используйте QPersistentModelIndex если нужно хранить ссылку.

Common mistakes

  • Объяснять Qt model/view classes только по синтаксису, без жизненного цикла и стоимости.
  • Игнорировать ошибки, null/empty состояния, порядок инициализации или режим сборки.
  • Давать пример, который работает в демо, но ломается при изменении владельца ресурса.
  • Смешивать signals/slots с generic callbacks и забывать про thread affinity.

What the interviewer is testing

  • Кандидат формулирует точную модель для Qt model/view classes, а не только определение.
  • Пример компилируем, безопасен по lifetime и соответствует версии технологии.
  • Названы trade-off, ограничения и диагностируемые симптомы ошибки.
  • Понимает границу между C++ кодом, runtime/framework metadata и editor/UI слоем.

Sources

Related topics