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 слоем.