Пример редактируемой древовидной модели в проекте с Qt

Добавлено 6 января 2023 в 16:59

В данном примере показано, как реализовать простую редактируемую древовидную модель на основе элементов, которую можно использовать с другими классами фреймворка модель/представление.

Пример редактируемой древовидной модели в проекте с Qt

Данная модель поддерживает редактируемые элементы, настраиваемые заголовки и возможность вставки и удаления строк и столбцов. С помощью этих функций также можно вставлять новые дочерние элементы, и это показано во вспомогательном коде примера.

Обзор

Как описано в справке по созданию подклассов моделей, модели должны предоставлять реализации для стандартного набора функций модели: flags(), data(), headerData(), columnCount() и rowCount(). Кроме того, иерархические модели, такие как эта, должны обеспечивать реализации index() и parent().

Редактируемая модель также должна предоставлять реализации setData() и setHeaderData() и должна возвращать подходящую комбинацию флагов из своей функции flags().

Поскольку данный пример позволяет изменять размеры модели, мы также должны реализовать функции insertRows(), insertColumns(), removeRows() и removeColumns().

Проектирование

Как и в примере с простой древовидной моделью, эта модель действует просто как обертка для коллекции экземпляров класса TreeItem. Каждый TreeItem предназначен для хранения данных для строки элементов в представлении в виде дерева, и поэтому он содержит список значений, соответствующих данным, отображаемым в каждом столбце.

Поскольку QTreeView обеспечивает ориентированное на строки представление модели, естественно выбрать ориентированное на строки проектирование для структур данных, которые будут предоставлять данные через модель для такого вида представления. Хотя это делает древовидную модель менее гибкой и, возможно, менее полезной для использования с более сложными представлениями, это также делает ее менее сложной в разработке и более простой в реализации.

Связи между внутренними элементами

При проектировании структуры данных для пользовательской модели полезно предоставлять родительский объект каждого элемента с помощью такой функции, как TreeItem::parent(), потому что это упростит написание собственной функции parent() в модели. Точно так же функция TreeItem::child() полезна при реализации функции index() модели. В результате каждый TreeItem хранит информацию о своих родителе и дочерних элементах, что позволяет нам перемещаться по древовидной структуре.

На диаграмме ниже показано, как экземпляры TreeItem связаны через свои функции parent() и child().

Связи между внутренними элементами

В показанном примере два элемента верхнего уровня, A и B, могут быть получены из корневого элемента путем вызова его функции child(), и каждый из этих элементов возвращает корневой узел из своей функции parent(), хотя это только показано для узла А.

Каждый TreeItem хранит данные для каждого столбца в строке, которую он представляет, в своем закрытом члене itemData (список объектов QVariant). Поскольку между каждым столбцом в представлении и каждой записью в списке существует взаимно однозначное соответствие, мы предоставляем простую функцию data() для чтения записей в списке itemData и функцию setData(), позволяющую их модифицировать. Как и в случае с другими функциями в элементе, это упрощает реализацию функций модели data() и setData().

Мы помещаем элемент в корень дерева элементов. Этот корневой элемент соответствует нулевому индексу модели, QModelIndex(), который используется для представления родителя элемента верхнего уровня при обработке индексов модели. Хотя корневой элемент не имеет видимого представления ни в одном из стандартных представлений, мы используем его внутренний список объектов QVariant для хранения списка строк, которые будут переданы представлениям для использования в качестве контента горизонтальных заголовков.

Доступ к данным через модель

В случае, показанном на диаграмме, часть информации, представленная в a, может быть получена с использованием стандартного API модель/представление:

QVariant a = model->index(0, 0, QModelIndex()).data();
Доступ к данным через модель

Поскольку каждый элемент содержит фрагменты данных для каждого столбца в заданной строке, может быть много модельных индексов, которые сопоставляются с одним и тем же объектом TreeItem. Например, информацию, представленную в b, можно получить с помощью следующего кода:

QVariant b = model->index(1, 0, QModelIndex()).data();

Доступ к тому же базовому элементу TreeItem будет осуществляться для получения информации о других индексах модели в той же строке, что и b.

В классе модели TreeModel мы связываем объекты TreeItem с индексами модели, передавая указатель для каждого элемента при создании соответствующего индекса модели с помощью QAbstractItemModel::createIndex() в наших реализациях index() и parent(). Сохраненные таким образом указатели мы можем получить, вызвав функцию internalPointer() для соответствующего индекса модели – мы создадим собственную функцию getItem(), которая сделает всю работу за нас, и вызываем ее из наших реализаций data() и parent().

Хранение указателей на элементы удобно, когда мы контролируем, как они создаются и уничтожаются, поскольку мы можем предположить, что адрес, полученный из internalPointer(), является допустимым указателем. Однако некоторым моделям необходимо обрабатывать элементы, получаемые из других компонентов системы, и во многих случаях невозможно полностью контролировать создание или уничтожение элементов. В таких ситуациях подход, основанный исключительно на указателях, должен быть дополнен мерами безопасности, чтобы гарантировать, что модель не попытается получить доступ к удаленным элементам.

Хранение информации в базовой структуре данных

Несколько фрагментов данных хранятся как объекты QVariant в члене itemData каждого экземпляра TreeItem.

На диаграмме ниже показано, как фрагменты информации, представленные метками a, b и c на предыдущей диаграммах, хранятся в элементах A, B и C базовой структуры данных. Обратите внимание, что все части информации из одной и той же строки в модели получены из одного и того же элемента. Каждый элемент в списке соответствует части информации, предоставляемой каждым столбцом в данной строке модели.

Хранение информации в базовой структуре данных

Поскольку реализация TreeModel была разработана для использования с QTreeView, мы добавили ограничение на способ использования экземпляров TreeItem: каждый элемент должен предоставлять одинаковое количество столбцов данных. Это делает просмотр модели согласованным, позволяя нам использовать корневой элемент для определения количества столбцов для любой заданной строки, и только добавляет требование, чтобы мы создавали элементы, содержащие достаточно данных для общего числа столбцов. В результате вставка и удаление столбцов являются трудоемкими операциями, поскольку нам нужно пройти все дерево, чтобы изменить каждый элемент.

Альтернативным подходом может быть разработка класса TreeModel таким образом, чтобы он усекал или расширял список данных в отдельных экземплярах TreeItem по мере изменения элементов данных. Однако этот «ленивый» подход к изменению размера позволил бы нам вставлять и удалять столбцы только в конец каждой строки и не позволял бы вставлять или удалять столбцы в произвольных позициях в каждой строке.

Связь элементов с использованием индексов модели

Как и в примере с простой древовидной моделью, TreeModel должен иметь возможность брать индекс модели, находить соответствующий TreeItem и возвращать индексы модели, соответствующие его родительскому и дочерним элементам.

На диаграмме ниже показано, как реализация parent() в модели получает индекс модели, соответствующий родителю элемента, предоставленного вызывающим, используя элементы, показанные на предыдущей диаграмме.

Связь элементов с использованием индексов модели

Указатель на элемент C получается из соответствующего индекса модели с помощью функции QModelIndex::internalPointer(). Этот указатель был сохранен внутри индекса при его создании. Поскольку дочерний элемент содержит указатель на своего родителя, мы используем его функцию parent() для получения указателя на элемент B. Индекс модели для родителя создается с помощью функции QAbstractItemModel::createIndex(), в которую передается указатель на элемент B в качестве внутреннего указателя.

Определение класса TreeItem

Класс TreeItem предоставляет простые элементы, которые содержат несколько фрагментов данных, включая информацию об их родительских и дочерних элементах:

class TreeItem
{
public:
    explicit TreeItem(const QList<QVariant> &data, TreeItem *parent = nullptr);
    ~TreeItem();

    TreeItem *child(int number);
    int childCount() const;
    int columnCount() const;
    QVariant data(int column) const;
    bool insertChildren(int position, int count, int columns);
    bool insertColumns(int position, int columns);
    TreeItem *parent();
    bool removeChildren(int position, int count);
    bool removeColumns(int position, int columns);
    int childNumber() const;
    bool setData(int column, const QVariant &value);

private:
    QList<TreeItem *> childItems;
    QList<QVariant> itemData;
    TreeItem *parentItem;
};

Мы разработали API таким образом, чтобы он был похож на предоставляемый QAbstractItemModel, предоставив каждому элементу функции для возврата количества столбцов информации, чтения и записи данных, а также вставки и удаления столбцов. Однако мы здесь делаем связи между элементами явными, предоставляя функции для работы с «дочерними элементами», а не со «строками».

Каждый элемент содержит список указателей на дочерние элементы, указатель на свой родительский элемент и список объектов QVariant, которые соответствуют информации, содержащейся в столбцах данной строки в модели.

Реализация класса TreeItem

Каждый TreeItem состоит из списка данных и необязательного родительского элемента:

TreeItem::TreeItem(const QList<QVariant> &data, TreeItem *parent)
    : itemData(data), parentItem(parent)
{}

Изначально у каждого элемента нет потомков. Они добавляются во внутренний член childItems с помощью функции insertChildren(), описанной ниже.

Деструктор гарантирует, что каждый дочерний элемент, добавленный к элементу, будет удален при удалении самого элемента:

TreeItem::~TreeItem()
{
    qDeleteAll(childItems);
}

Поскольку каждый элемент хранит указатель на своего родителя, функция parent() тривиальна:

TreeItem *TreeItem::parent()
{
    return parentItem;
}

Информацию о дочерних элементах элемента предоставляют три функции. child() возвращает конкретный дочерний элемент из внутреннего списка дочерних элементов:

TreeItem *TreeItem::child(int number)
{
    if (number < 0 || number >= childItems.size())
        return nullptr;
    return childItems.at(number);
}

Функция childCount() возвращает количество дочерних элементов:

int TreeItem::childCount() const
{
    return childItems.count();
}

Функция childNumber() используется для определения индекса дочернего элемента в родительском списке дочерних элементов. Она напрямую обращается к элементу childItems родителя, чтобы получить эту информацию:

int TreeItem::childNumber() const
{
    if (parentItem)
        return parentItem->childItems.indexOf(const_cast<TreeItem*>(this));
    return 0;
}

Корневой элемент не имеет родительского элемента; для него мы возвращаем ноль, чтобы он соответствовал другим элементам.

Функция columnCount() просто возвращает количество элементов во внутреннем списке itemData объектов QVariant:

int TreeItem::columnCount() const
{
    return itemData.count();
}

Данные извлекаются с помощью функции data(), которая обращается к соответствующему элементу в списке itemData:

QVariant TreeItem::data(int column) const
{
    if (column < 0 || column >= itemData.size())
        return QVariant();
    return itemData.at(column);
}

Данные устанавливаются с помощью функции setData(), которая сохраняет значения в списке itemData только для допустимых индексов списка, соответствующих значениям столбца в модели:

bool TreeItem::setData(int column, const QVariant &value)
{
    if (column < 0 || column >= itemData.size())
        return false;

    itemData[column] = value;
    return true;
}

Чтобы упростить реализацию модели, мы возвращаем значение true, чтобы указать, что данные были установлены успешно.

Редактируемые модели часто должны изменять размер, позволяя вставлять и удалять строки и столбцы. Вставка строк в модель под заданным индексом модели приводит к вставке новых дочерних элементов в соответствующий элемент, обрабатываемый функцией insertChildren():

bool TreeItem::insertChildren(int position, int count, int columns)
{
    if (position < 0 || position > childItems.size())
        return false;

    for (int row = 0; row < count; ++row) {
        QList<QVariant> data(columns);
        TreeItem *item = new TreeItem(data, this);
        childItems.insert(position, item);
    }

    return true;
}

Это гарантирует, что новые элементы будут созданы с необходимым количеством столбцов и вставлены в допустимую позицию во внутреннем списке дочерних элементов. Удаляются элементы с помощью функции removeChildren():

bool TreeItem::removeChildren(int position, int count)
{
    if (position < 0 || position + count > childItems.size())
        return false;

    for (int row = 0; row < count; ++row)
        delete childItems.takeAt(position);

    return true;
}

Как обсуждалось выше, функции для вставки и удаления столбцов используются иначе, чем функции для вставки и удаления дочерних элементов, поскольку ожидается, что они будут вызываться для каждого элемента в дереве. Мы делаем это, рекурсивно вызывая эту функцию для каждого потомка элемента:

bool TreeItem::insertColumns(int position, int columns)
{
    if (position < 0 || position > itemData.size())
        return false;

    for (int column = 0; column < columns; ++column)
        itemData.insert(position, QVariant());

    for (TreeItem *child : qAsConst(childItems))
        child->insertColumns(position, columns);

    return true;
}

Определение класса TreeModel

Класс TreeModel предоставляет реализацию класса QAbstractItemModel, предоставляя необходимый интерфейс для модели, которую можно редактировать и изменять размер.

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    TreeModel(const QStringList &headers, const QString &data,
              QObject *parent = nullptr);
    ~TreeModel();

Конструктор и деструктор специфичны для этой модели.

    QVariant data(const QModelIndex &index, int role) const override;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role = Qt::DisplayRole) const override;

    QModelIndex index(int row, int column,
                      const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &index) const override;

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;

Древовидные модели только для чтения должны обеспечивать только приведенные выше функции. Следующие открытые функции обеспечивают поддержку редактирования и изменения размера:

    Qt::ItemFlags flags(const QModelIndex &index) const override;
    bool setData(const QModelIndex &index, const QVariant &value,
                 int role = Qt::EditRole) override;
    bool setHeaderData(int section, Qt::Orientation orientation,
                       const QVariant &value, int role = Qt::EditRole) override;

    bool insertColumns(int position, int columns,
                       const QModelIndex &parent = QModelIndex()) override;
    bool removeColumns(int position, int columns,
                       const QModelIndex &parent = QModelIndex()) override;
    bool insertRows(int position, int rows,
                    const QModelIndex &parent = QModelIndex()) override;
    bool removeRows(int position, int rows,
                    const QModelIndex &parent = QModelIndex()) override;

private:
    void setupModelData(const QStringList &lines, TreeItem *parent);
    TreeItem *getItem(const QModelIndex &index) const;

    TreeItem *rootItem;
};

Чтобы упростить этот пример, данные, предоставляемые моделью, организованы в структуру данных с помощью функции модели setupModelData(). Многие реальные модели вообще не обрабатывают сырые данные, а просто работают с существующей структурой данных или библиотечным API.

Реализация класса TreeModel

Конструктор создает корневой элемент и инициализирует его предоставленными данными заголовков:

TreeModel::TreeModel(const QStringList &headers, const QString &data, QObject *parent)
    : QAbstractItemModel(parent)
{
    QList<QVariant> rootData;
    for (const QString &header : headers)
        rootData << header;

    rootItem = new TreeItem(rootData);
    setupModelData(data.split('\n'), rootItem);
}

Мы вызываем внутреннюю функцию setupModelData() для преобразования предоставленных текстовых данных в структуру данных, которую мы можем использовать с данной моделью. Другие модели могут быть инициализированы готовой структурой данных или использовать API из библиотеки, которая поддерживает свои собственные данные.

Деструктор должен удалять только корневой элемент, что приведет к рекурсивному удалению всех дочерних элементов.

TreeModel::~TreeModel()
{
    delete rootItem;
}

Поскольку интерфейс модели с другими компонентами архитектуры модель/представление основан на индексах модели, а внутренняя структура данных основана на элементах, многие функции, реализованные моделью, должны иметь возможность преобразовывать любой заданный индекс модели в соответствующий элемент. Для удобства и согласованности мы определили функцию getItem() для выполнения этой повторяющейся задачи:

TreeItem *TreeModel::getItem(const QModelIndex &index) const
{
    if (index.isValid()) {
        TreeItem *item = static_cast<TreeItem*>(index.internalPointer());
        if (item)
            return item;
    }
    return rootItem;
}

Каждый индекс модели, переданный этой функции, должен соответствовать действительному элементу в памяти. Если индекс недействителен или его внутренний указатель не ссылается на допустимый элемент, вместо него возвращается корневой элемент.

Реализация модели rowCount() проста: сначала используется функция getItem() для получения соответствующего элемента; а затем он возвращает количество дочерних элементов, которые содержит:

int TreeModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid() && parent.column() > 0)
        return 0;

    const TreeItem *parentItem = getItem(parent);

    return parentItem ? parentItem->childCount() : 0;
}

Для реализации columnCount(), напротив, не нужно искать конкретный элемент, потому что все элементы определены как имеющие одинаковое количество связанных с ними столбцов.

int TreeModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return rootItem->columnCount();
}

В результате количество столбцов можно получить непосредственно из корневого элемента.

Чтобы разрешить редактирование и выбор элементов, функция flags() должна быть реализована так, чтобы она возвращала комбинацию флагов, включающую флаги Qt::ItemIsEditable и Qt::ItemIsSelectable, а также Qt::ItemIsEnabled:

Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::NoItemFlags;

    return Qt::ItemIsEditable | QAbstractItemModel::flags(index);
}

Модель должна иметь возможность генерировать индексы, чтобы другие компоненты могли запрашивать данные и информацию о ее структуре. Эта задача выполняется функцией index(), которая используется для получения индексов модели, соответствующих дочерним элементам заданного родительского элемента:

QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
    if (parent.isValid() && parent.column() != 0)
        return QModelIndex();

В этой модели мы возвращаем индексы для дочерних элементов только в том случае, если родительский индекс недействителен (соответствует корневому элементу) или имеет нулевой номер столбца.

Мы используем пользовательскую функцию getItem() для получения экземпляра TreeItem, соответствующего предоставленному индексу модели, и запрашиваем его дочерний элемент, соответствующий указанной строке.

    TreeItem *parentItem = getItem(parent);
    if (!parentItem)
        return QModelIndex();

    TreeItem *childItem = parentItem->child(row);
    if (childItem)
        return createIndex(row, column, childItem);
    return QModelIndex();
}

Поскольку каждый элемент содержит информацию для всей строки данных, мы создаем индекс модели, чтобы однозначно идентифицировать его, вызывая его createIndex() с номерами строки и столбца и указателем на элемент. В функции data() мы будем использовать указатель элемента и номер столбца для доступа к данным, связанным с индексом модели; в этой модели номер строки не требуется для идентификации данных.

Функция parent() предоставляет индексы модели для родителей элементов, находя соответствующий элемент для данного индекса модели, используя функцию parent() для получения его родительского элемента, а затем создавая индекс модели для представления родителя (смотрите приведенную выше диаграмму).

QModelIndex TreeModel::parent(const QModelIndex &index) const
{
    if (!index.isValid())
        return QModelIndex();

    TreeItem *childItem = getItem(index);
    TreeItem *parentItem = childItem ? childItem->parent() : nullptr;

    if (parentItem == rootItem || !parentItem)
        return QModelIndex();

    return createIndex(parentItem->childNumber(), 0, parentItem);
}

Элементы без родителей, включая корневой элемент, обрабатываются путем возврата нулевого индекса модели. В противном случае индекс модели создается и возвращается, как в функции index(), с соответствующим номером строки, но с нулевым номером столбца, чтобы соответствовать схеме, используемой в реализации index().

Данные из модели получаются через функцию data(). Поскольку элемент сам управляет своими столбцами, то для получения данных с помощью функции TreeItem::data() нам нужно использовать номер столбца:

QVariant TreeModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    if (role != Qt::DisplayRole && role != Qt::EditRole)
        return QVariant();

    TreeItem *item = getItem(index);

    return item->data(index.column());
}

Для редактирования данных используется функции setData(). Здесь мы так же получаем элемент TreeItem, а затем вызываем его функцию setData(). И в случае успеха выдаем сигнал dataChanged().

bool TreeModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role != Qt::EditRole)
        return false;

    TreeItem *item = getItem(index);
    bool result = item->setData(index.column(), value);

    if (result)
        emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});

    return result;
}

Для получения и изменения данных заголовков функции headerData() и setHeaderData() модели используют функции data() и setData() корневого элемента.

При вставке строк мы получаем родительский элемент и добавляем указанное количество дочерних элементов в указанную позицию с помощью функции insertChildren() класса TreeItem.

bool TreeModel::insertRows(int position, int rows, const QModelIndex &parent)
{
    TreeItem *parentItem = getItem(parent);
    if (!parentItem)
        return false;

    beginInsertRows(parent, position, position + rows - 1);
    const bool success = parentItem->insertChildren(position,
                                                    rows,
                                                    rootItem->columnCount());
    endInsertRows();

    return success;
}

Для удаления строк действуем аналогично.

bool TreeModel::removeRows(int position, int rows, const QModelIndex &parent)
{
    TreeItem *parentItem = getItem(parent);
    if (!parentItem)
        return false;

    beginRemoveRows(parent, position, position + rows - 1);
    const bool success = parentItem->removeChildren(position, rows);
    endRemoveRows();

    return success;
}

Для манипуляций со столбцами вызываем соответствующие функции для корневого элемента, остальные элементы будут обработаны рекурсивно.

bool TreeModel::insertColumns(int position, int columns, const QModelIndex &parent)
{
    beginInsertColumns(parent, position, position + columns - 1);
    const bool success = rootItem->insertColumns(position, columns);
    endInsertColumns();

    return success;
}
bool TreeModel::removeColumns(int position, int columns, const QModelIndex &parent)
{
    beginRemoveColumns(parent, position, position + columns - 1);
    const bool success = rootItem->removeColumns(position, columns);
    endRemoveColumns();

    if (rootItem->columnCount() == 0)
        removeRows(0, rowCount());

    return success;
}

Теги

C++ / CppMVC / Model-View-Controller / Модель-Представление-КонтроллерQtПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.