편집 가능한 트리 모델 예제

이 예에서는 모델/뷰 프레임워크의 다른 클래스와 함께 사용할 수 있는 간단한 항목 기반 트리 모델을 구현하는 방법을 보여 줍니다.

이 모델은 편집 가능한 항목, 사용자 지정 헤더, 행과 열을 삽입 및 제거하는 기능을 지원합니다. 이러한 기능을 사용하면 새로운 하위 항목을 삽입할 수도 있으며, 이는 지원 예제 코드에 나와 있습니다.

개요

모델 서브클래싱 참조에 설명된 대로 모델은 표준 모델 함수 집합인 flags(), data(), headerData(), columnCount() 및 rowCount()에 대한 구현을 제공해야 합니다. 또한 이 모델과 같은 계층적 모델은 index() 및 parent()의 구현을 제공해야 합니다.

편집 가능한 모델은 setData() 및 setHeaderData()의 구현을 제공해야 하며 flags() 함수에서 적절한 플래그 조합을 반환해야 합니다.

이 예에서는 모델의 차원을 변경할 수 있으므로 insertRows(), insertColumns(), removeRows() 및 removeColumns()도 구현해야 합니다.

디자인

단순 트리 모델 예제와 마찬가지로 이 모델은 TreeItem 클래스의 인스턴스 컬렉션을 감싸는 래퍼 역할을 합니다. 각 TreeItem 은 트리 보기의 항목 행에 대한 데이터를 보유하도록 설계되었으므로 각 열에 표시된 데이터에 해당하는 값의 목록을 포함합니다.

QTreeView 은 모델에 행 중심의 보기를 제공하므로 이러한 종류의 보기에 모델을 통해 데이터를 제공하는 데이터 구조에 대해 행 중심의 디자인을 선택하는 것은 당연합니다. 이렇게 하면 트리 모델의 유연성이 떨어지고 보다 정교한 보기에 사용하기에는 유용성이 떨어질 수 있지만, 설계가 덜 복잡하고 구현하기가 더 쉬워집니다.

내부 항목 간의 관계

사용자 정의 모델에 사용할 데이터 구조를 디자인할 때 각 항목의 부모를 TreeItem::parent() 같은 함수를 통해 노출하면 모델 자체의 parent() 함수를 더 쉽게 작성할 수 있으므로 유용합니다. 마찬가지로 모델의 index() 함수를 구현할 때 TreeItem::child() 같은 함수를 사용하면 유용합니다. 결과적으로 각 TreeItem 은 부모와 자식에 대한 정보를 유지하므로 트리 구조를 횡단할 수 있습니다.

이 다이어그램은 TreeItem 인스턴스가 부모() 및 자식 () 함수를 통해 어떻게 연결되는지 보여줍니다.

표시된 예에서는 루트 항목에서 child() 함수를 호출하여 두 개의 최상위 항목인 A와 B를 얻을 수 있으며, 이러한 각 항목은 부모() 함수에서 루트 노드를 반환하지만 이는 항목 A에 대해서만 표시되어 있습니다.

TreeItem 은 해당 행의 각 열에 대한 데이터를 itemData 비공개 멤버( QVariant 객체 목록)에 저장합니다. 뷰의 각 열과 목록의 각 항목 사이에는 일대일 매핑이 있으므로 itemData 목록의 항목을 읽는 간단한 data() 함수와 수정할 수 있는 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 에서는 index()parent( ) 구현에서 QAbstractItemModel::createIndex()로 해당 모델 인덱스를 만들 때 각 항목에 대한 포인터를 전달하여 TreeItem 객체를 모델 인덱스와 연관시킵니다. 이렇게 저장된 포인터는 관련 모델 인덱스에서 internalPointer() 함수를 호출하여 검색할 수 있습니다. 이 작업을 수행할 자체 getItem() 함수를 생성하고 데이터()부모( ) 구현에서 호출합니다.

아이템에 대한 포인터를 저장하면 internalPointer()에서 얻은 주소가 유효한 포인터라고 가정할 수 있으므로 아이템이 생성되고 소멸되는 방식을 제어할 때 편리합니다. 그러나 일부 모델은 시스템의 다른 컴포넌트에서 가져온 항목을 처리해야 하며, 많은 경우 항목이 생성되거나 소멸되는 방식을 완전히 제어할 수 없습니다. 이러한 상황에서는 순수 포인터 기반 접근 방식에 모델이 삭제된 항목에 접근을 시도하지 못하도록 하는 안전 장치를 보완해야 합니다.

기본 데이터 구조에 정보 저장

여러 데이터 조각이 각 TreeItem 인스턴스의 itemData 멤버에 QVariant 객체로 저장됩니다.

이 다이어그램은 앞의 두 다이어그램에서 레이블 a, bc로 표시된 정보 조각이 기본 데이터 구조의 항목 A, BC에 저장되는 방식을 보여줍니다. 모델에서 같은 행의 정보 조각은 모두 같은 항목에서 가져온 것입니다. 목록의 각 요소는 모델에서 주어진 행의 각 열이 노출하는 정보 조각에 해당합니다.

TreeModel 구현은 QTreeView 과 함께 사용하도록 설계되었으므로 TreeItem 인스턴스를 사용하는 방식에 제한을 추가했습니다. 각 항목은 동일한 수의 데이터 열을 노출해야 합니다. 이렇게 하면 모델을 일관성 있게 볼 수 있으므로 루트 항목을 사용하여 주어진 행의 열 수를 결정할 수 있으며, 총 열 수에 맞는 충분한 데이터가 포함된 항목을 생성해야 한다는 요구 사항만 추가됩니다. 결과적으로 모든 항목을 수정하기 위해 전체 트리를 탐색해야 하므로 열을 삽입하고 제거하는 작업은 시간이 많이 걸리는 작업입니다.

다른 접근 방식은 데이터 항목이 수정될 때 개별 TreeItem 인스턴스의 데이터 목록을 잘라내거나 확장하도록 TreeModel 클래스를 설계하는 것입니다. 그러나 이러한 "지연" 크기 조정 방식은 각 행의 끝에서만 열을 삽입하고 제거할 수 있으며 각 행의 임의 위치에서 열을 삽입하거나 제거할 수 없습니다.

모델 인덱스를 사용하여 항목 연관시키기

단순 트리 모델 예제와 마찬가지로 TreeModel 에서 모델 인덱스를 가져와 해당하는 TreeItem 을 찾은 다음 부모 및 자식에 해당하는 모델 인덱스를 반환할 수 있어야 합니다.

이 다이어그램에서는 모델의 parent() 구현이 이전 다이어그램에 표시된 항목을 사용하여 호출자가 제공한 항목의 부모에 해당하는 모델 인덱스를 가져오는 방법을 보여 줍니다.

QModelIndex::internalPointer() 함수를 사용하여 해당 모델 인덱스에서 항목 C에 대한 포인터를 얻습니다. 이 포인터는 인덱스가 생성될 때 내부적으로 인덱스에 저장되었습니다. 자식에는 부모에 대한 포인터가 포함되어 있으므로 부모() 함수를 사용하여 항목 B에 대한 포인터를 얻습니다. 부모 모델 인덱스는 QAbstractItemModel::createIndex() 함수를 사용하여 생성되며, 항목 B에 대한 포인터를 내부 포인터로 전달합니다.

TreeItem 클래스 정의

TreeItem 클래스는 부모 및 자식 항목에 대한 정보를 포함하여 여러 데이터를 포함하는 간단한 항목을 제공합니다:

class TreeItem
{
public:
    explicit TreeItem(QVariantList data, TreeItem *parent = nullptr);

    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 row() const;
    bool setData(int column, const QVariant &value);

private:
    std::vector<std::unique_ptr<TreeItem>> m_childItems;
    QVariantList itemData;
    TreeItem *m_parentItem;
};

각 항목에 정보 열 수를 반환하고, 데이터를 읽고 쓰고, 열을 삽입하고 제거하는 함수를 제공하여 QAbstractItemModel 에서 제공하는 것과 유사하게 API를 설계했습니다. 그러나 '행'이 아닌 '하위 항목'을 처리하는 함수를 제공하여 항목 간의 관계를 명확히 했습니다.

각 항목에는 자식 항목에 대한 포인터 목록, 부모 항목에 대한 포인터, 모델에서 지정된 행의 열에 있는 정보에 해당하는 QVariant 객체 목록이 포함됩니다.

TreeItem 클래스 구현

TreeItem 은 데이터 목록과 선택적 상위 항목으로 구성됩니다:

TreeItem::TreeItem(QVariantList data, TreeItem *parent)
    : itemData(std::move(data)), m_parentItem(parent)
{}

처음에는 각 항목에 자식이 없습니다. 이러한 항목은 나중에 설명하는 insertChildren() 함수를 사용하여 항목의 내부 childItems 멤버에 추가됩니다.

항목 자체 삭제 시 항목에 추가된 각 하위 항목이 삭제되도록 하기 위해 하위 항목은 std::unique_ptr에 저장됩니다.

각 항목은 부모에 대한 포인터를 저장하므로 parent() 함수는 사소합니다:

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

child() 함수는 내부 자식 목록에서 특정 자식을 반환합니다. 함수는 항목의 자식에 대한 정보를 제공합니다:

TreeItem *TreeItem::child(int number)
{
    return (number >= 0 && number < childCount())
        ? m_childItems.at(number).get() : nullptr;
}

childCount() 함수는 총 자식 수를 반환합니다:

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

childNumber() 함수는 부모의 자식 목록에서 자식의 인덱스를 확인하는 데 사용됩니다. 이 정보를 얻기 위해 부모의 childItems 멤버에 직접 액세스합니다:

int TreeItem::row() const
{
    if (!m_parentItem)
        return 0;
    const auto it = std::find_if(m_parentItem->m_childItems.cbegin(), m_parentItem->m_childItems.cend(),
                                 [this](const std::unique_ptr<TreeItem> &treeItem) {
        return treeItem.get() == this;
    });

    if (it != m_parentItem->m_childItems.cend())
        return std::distance(m_parentItem->m_childItems.cbegin(), it);
    Q_ASSERT(false); // should not happen
    return -1;
}

루트 항목에는 상위 항목이 없으므로 이 항목의 경우 다른 항목과 일관성을 유지하기 위해 0을 반환합니다.

columnCount() 함수는 단순히 QVariant 객체의 내부 itemData 목록에 있는 요소의 수를 반환합니다:

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

데이터는 itemData 목록에서 해당 요소에 액세스하는 data() 함수를 사용하여 검색합니다:

QVariant TreeItem::data(int column) const
{
    return itemData.value(column);
}

데이터는 모델의 열 값에 해당하는 유효한 목록 인덱스에 대해서만 itemData 목록에 값을 저장하는 setData() 함수를 사용하여 설정합니다:

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 > qsizetype(m_childItems.size()))
        return false;

    for (int row = 0; row < count; ++row) {
        QVariantList data(columns);
        m_childItems.insert(m_childItems.cbegin() + position,
                std::make_unique<TreeItem>(data, this));
    }

    return true;
}

이렇게 하면 필요한 수의 열로 새 항목이 생성되고 내부 childItems 목록의 유효한 위치에 삽입됩니다. 항목은 removeChildren() 함수로 제거됩니다:

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

    for (int row = 0; row < count; ++row)
        m_childItems.erase(m_childItems.cbegin() + 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 (auto &child : std::as_const(m_childItems))
        child->insertColumns(position, columns);

    return true;
}

트리모델 클래스 정의

TreeModel 클래스는 QAbstractItemModel 클래스의 구현을 제공하여 편집 및 크기 조정이 가능한 모델에 필요한 인터페이스를 노출합니다.

class TreeModel : public QAbstractItemModel
{
    Q_OBJECT

public:
    Q_DISABLE_COPY_MOVE(TreeModel)

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

생성자와 소멸자는 이 모델에만 해당됩니다.

    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 = {}) const override;
    QModelIndex parent(const QModelIndex &index) const override;

    int rowCount(const QModelIndex &parent = {}) const override;
    int columnCount(const QModelIndex &parent = {}) 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 = {}) override;
    bool removeColumns(int position, int columns,
                       const QModelIndex &parent = {}) override;
    bool insertRows(int position, int rows,
                    const QModelIndex &parent = {}) override;
    bool removeRows(int position, int rows,
                    const QModelIndex &parent = {}) override;

private:
    void setupModelData(const QList<QStringView> &lines);
    TreeItem *getItem(const QModelIndex &index) const;

    std::unique_ptr<TreeItem> rootItem;
};

이 예시를 단순화하기 위해 모델에 의해 노출된 데이터는 모델의 setupModelData() 함수에 의해 데이터 구조로 구성됩니다. 실제 많은 모델은 원시 데이터를 전혀 처리하지 않고 기존 데이터 구조 또는 라이브러리 API를 사용하여 작업합니다.

TreeModel 클래스 구현

생성자는 루트 항목을 생성하고 제공된 헤더 데이터로 초기화합니다:

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

    rootItem = std::make_unique<TreeItem>(rootData);
    setupModelData(QStringView{data}.split(u'\n'));
}

제공된 텍스트 데이터를 모델에 사용할 수 있는 데이터 구조로 변환하기 위해 내부 setupModelData() 함수를 호출합니다. 다른 모델은 기성 데이터 구조로 초기화하거나 자체 데이터를 유지하는 라이브러리의 API를 사용할 수 있습니다.

TreeModel::~TreeModel() = default;

소멸자는 루트 항목만 삭제하면 모든 하위 항목이 재귀적으로 삭제됩니다. 이 작업은 루트 항목이 고유_ptr 안에 저장되므로 기본 소멸자에 의해 자동으로 수행됩니다.

다른 모델/뷰 컴포넌트에 대한 모델의 인터페이스는 모델 인덱스를 기반으로 하고 내부 데이터 구조는 항목 기반이므로 모델에서 구현하는 많은 함수는 주어진 모델 인덱스를 해당 항목으로 변환할 수 있어야 합니다. 편의성과 일관성을 위해 이 반복적인 작업을 수행하는 getItem() 함수를 정의했습니다:

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

이 함수에 전달되는 각 모델 인덱스는 메모리에서 유효한 항목에 대응해야 합니다. 인덱스가 유효하지 않거나 내부 포인터가 유효한 항목을 가리키지 않으면 루트 항목이 대신 반환됩니다.

모델의 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();
}

따라서 열의 수는 루트 항목에서 직접 얻을 수 있습니다.

항목을 편집하고 선택할 수 있도록 하려면 Qt::ItemIsEditableQt::ItemIsSelectable 플래그와 Qt::ItemIsEnabled 플래그가 포함된 플래그 조합을 반환하도록 flags() 함수를 구현해야 합니다:

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 {};

이 모델에서는 부모 인덱스가 유효하지 않거나(루트 항목에 해당) 열 번호가 0인 경우에만 자식 항목에 대한 모델 인덱스를 반환합니다.

사용자 지정 getItem() 함수를 사용하여 제공된 모델 인덱스에 해당하는 TreeItem 인스턴스를 가져와서 지정된 행에 해당하는 하위 항목을 요청합니다.

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

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

각 항목에는 전체 데이터 행에 대한 정보가 포함되어 있으므로 행 및 열 번호와 항목에 대한 포인터를 사용하여 createIndex()를 호출하여 고유하게 식별할 수 있는 모델 인덱스를 생성합니다. data() 함수에서는 항목 포인터와 열 번호를 사용하여 모델 인덱스와 연결된 데이터에 액세스하지만, 이 모델에서는 데이터를 식별하는 데 행 번호가 필요하지 않습니다.

parent() 함수는 주어진 모델 인덱스에 해당하는 항목을 찾고, parent() 함수를 사용하여 해당 항목의 부모 항목을 찾은 다음, 부모를 나타내는 모델 인덱스를 생성하여 항목의 부모에 대한 모델 인덱스를 제공합니다. ( 위 다이어그램 참조).

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

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

    return (parentItem != rootItem.get() && parentItem != nullptr)
        ? createIndex(parentItem->row(), 0, parentItem) : QModelIndex{};
}

루트 항목을 포함하여 부모가 없는 항목은 null 모델 인덱스를 반환하여 처리합니다. 그렇지 않으면 index () 함수에서와 같이 모델 인덱스가 생성되어 반환되며, 적절한 행 번호가 있지만 index() 구현에 사용된 체계와 일치하도록 열 번호는 0으로 지정됩니다.

모델 테스트하기

항목 모델을 올바르게 구현하는 것은 어려울 수 있습니다. QAbstractItemModelTester 클래스는 Qt Test 모듈의 클래스는 모델 인덱스 생성 및 부모-자식 관계와 같은 모델 일관성을 검사합니다.

예를 들어 Qt 유닛 테스트의 일부로 클래스 생성자에 모델 인스턴스를 전달하여 모델을 테스트할 수 있습니다:

TestEditableTreeModel 클래스: 공용 QObject
{ Q_OBJECTprivate slots: void testTreeModel(); };void TestEditableTreeModel::testTreeModel() { constexpr auto fileName = ":/default.txt"_L1;    QFile file(fileName); QVERIFY2(file.open(QIODevice::읽기 전용 | QIODevice::Text),             qPrintable(fileName + " cannot be opened: "_L1 + file.errorString()));

   const QStringList headers{"column1"_L1, "column2"_L1}; TreeModel model(headers, QString::fromUtf8(file.readAll()));    QAbstractItemModelTester tester(&model); } QTEST_APPLESS_MAIN(TestEditableTreeModel)#include "test.moc"

ctest 실행 파일을 사용하여 실행할 수 있는 테스트를 만들려면 add_test() 을 사용합니다:

# Unit Test

include(CTest)

qt_add_executable(editabletreemodel_tester
    test.cpp
    treeitem.cpp treeitem.h
    treemodel.cpp treemodel.h)

target_link_libraries(editabletreemodel_tester PRIVATE
    Qt6::Core
    Qt6::Test
)

if(ANDROID)
    target_link_libraries(editabletreemodel_tester PRIVATE
        Qt6::Gui
    )
endif()

qt_add_resources(editabletreemodel_tester "editabletreemodel_tester"
    PREFIX
        "/"
    FILES
        ${editabletreemodel_resource_files}
)

add_test(NAME editabletreemodel_tester
         COMMAND editabletreemodel_tester)

예제 프로젝트 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.