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

Добавлено 4 января 2023 в 14:45

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

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

Архитектура модель/представление в Qt обеспечивает стандартный способ для представлений манипулировать информацией в источнике данных, используя абстрактную модель данных для упрощения и стандартизации способа доступа к ним. Простые модели представляют данные в виде таблицы элементов и позволяют представлениям получать доступ к этим данным через систему на основе индексов. В более общем плане модели можно использовать для представления данных в виде древовидной структуры, позволяя каждому элементу выступать в качестве родителя для таблицы дочерних элементов.

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

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

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

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

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

Древовидная структура данных
Древовидная структура данных

Каждый TreeItem содержит информацию о своем месте в древовидной структуре; он может вернуть свой родительский элемент и номер строки. Доступность этой информации упрощает внедрение модели.

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

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

Имея соответствующую структуру данных, мы можем создать древовидную модель с минимальным объемом дополнительного кода для предоставления индексов модели и данных другим компонентам.

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

Класс TreeItem определяется следующим образом:

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

    void appendChild(TreeItem *child);

    TreeItem *child(int row);
    int childCount() const;
    int columnCount() const;
    QVariant data(int column) const;
    int row() const;
    TreeItem *parentItem();

private:
    QList<TreeItem *> m_childItems;
    QList<QVariant> m_itemData;
    TreeItem *m_parentItem;
};

Этот класс является базовым. Он не наследуется от QObject и не предоставляет сигналы и слоты. Он используется для хранения списка QVariant, содержащего данные столбцов, и информацию о его положении в древовидной структуре. Он содержит функции со следующими возможностями:

  • appendChildItem() используется для добавления данных при первоначальном создании модели и не используется при обычном использовании.
  • Функции child() и childCount() позволяют модели получать информацию о любых дочерних элементах.
  • Информация о количестве столбцов, связанных с элементом, предоставляется функцией columnCount(), а данные в каждом столбце можно получить с помощью функции data().
  • Функции row() и parent() используются для получения номера строки элемента и родительского элемента.

Данные родительского элемента и столбца хранятся в закрытых переменных-членах parentItem и itemData. Переменная childItems содержит список указателей на собственные дочерние элементы элемента.

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

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

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

Указатель на каждый из дочерних элементов, принадлежащих данному элементу, будет храниться в закрытой переменной-члене childItems. Когда вызывается деструктор класса, он должен удалить каждый из них, чтобы обеспечить повторное использование их памяти:

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

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

void TreeItem::appendChild(TreeItem *item)
{
    m_childItems.append(item);
}

Каждый элемент может вернуть любой из своих дочерних элементов, если ему присвоен подходящий номер строки. Например, на приведенной выше диаграмме элемент, отмеченный буквой «А», соответствует дочернему элементу корневого элемента со row = 0, элемент «В» является дочерним элементом элемента «А» с row = 1 и элемент "C" является дочерним элементом корневого элемента с row = 1.

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

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

Количество содержащихся дочерних элементов можно определить с помощью childCount():

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

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

Функция row() сообщает о расположении элемента в списке элементов родителя:

int TreeItem::row() const
{
    if (m_parentItem)
        return m_parentItem->m_childItems.indexOf(const_cast<TreeItem*>(this));

    return 0;
}

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

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

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

Данные столбца возвращаются функцией data(). Перед доступом к контейнеру с данными проверяются границы:

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

Родитель элемента определяется с помощью parent():

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

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

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

Класс TreeModel определяется следующим образом:

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

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

    QVariant data(const QModelIndex &index, int role) const override;
    Qt::ItemFlags flags(const QModelIndex &index) 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;

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

    TreeItem *rootItem;
};

Этот класс подобен большинству других подклассов QAbstractItemModel, которые предоставляют модели только для чтения. Для этой модели специфичны только конструктор и функция setupModelData(). Кроме того, мы предоставляем деструктор для очистки при уничтожении модели.

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

Для простоты данная модель не позволяет редактировать свои данные. В результате конструктор принимает аргумент, содержащий данные, которыми модель будет делиться с представлениями и делегатами:

TreeModel::TreeModel(const QString &data, QObject *parent)
    : QAbstractItemModel(parent)
{
    rootItem = new TreeItem({tr("Title"), tr("Summary")});
    setupModelData(data.split('\n'), rootItem);
}

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

Внутренняя структура данных модели заполняется элементами с помощью функции setupModelData(). Мы рассмотрим эту функцию отдельно в конце статьи.

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

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

Поскольку мы не можем добавлять данные в модель после ее создания и настройки, это упрощает способ управления внутренним деревом элементов.

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

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

QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const
{
    if (!hasIndex(row, column, parent))
        return QModelIndex();

    TreeItem *parentItem;

    if (!parent.isValid())
        parentItem = rootItem;
    else
        parentItem = static_cast<TreeItem*>(parent.internalPointer());

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

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

То, как определены объекты TreeItem, упрощает написание функции parent():

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

    TreeItem *childItem = static_cast<TreeItem*>(index.internalPointer());
    TreeItem *parentItem = childItem->parentItem();

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

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

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

При создании индекса модели для возврата мы должны указать номера строки и столбца родительского элемента в его собственном родительском элементе. Номер строки мы можем легко определить с помощью функции TreeItem::row(); а при указании номера столбца родителя мы следуем соглашению, указывая значение 0. Так же, как и в функции index(), индекс модели создается с помощью createIndex().

Функция rowCount() просто возвращает количество дочерних элементов для TreeItem, которое соответствует заданному индексу модели, или количество элементов верхнего уровня, если указан недопустимый индекс:

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

    if (!parent.isValid())
        parentItem = rootItem;
    else
        parentItem = static_cast<TreeItem*>(parent.internalPointer());

    return parentItem->childCount();
}

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

int TreeModel::columnCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return static_cast<TreeItem*>(parent.internalPointer())->columnCount();
    return rootItem->columnCount();
}

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

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

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

    TreeItem *item = static_cast<TreeItem*>(index.internalPointer());

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

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

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

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

    return QAbstractItemModel::flags(index);
}

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

QVariant TreeModel::headerData(int section, Qt::Orientation orientation,
                               int role) const
{
    if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
        return rootItem->data(section);

    return QVariant();
}

Эта информация могла быть предоставлена другим способом: либо указана в конструкторе, либо жестко закодирована в функции headerData().

Заполнение модели данными

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

Начнем с текстового файла в следующем формате:

Getting Started                         How to familiarize yourself with Qt Designer
    Launching Designer                  Running the Qt Designer application
    The User Interface                  How to interact with Qt Designer
    ...
Connection Editing Mode                 Connecting widgets together with signals and slots
    Connecting Objects                  Making connections in Qt Designer
    Editing Connections                 Changing existing connections

Мы обрабатываем текстовый файл со следующими двумя правилами:

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

Чтобы убедиться, что модель работает правильно, необходимо только создать экземпляры TreeItem с правильными данными и родительским элементом.

Теги

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

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

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