간단한 트리 모델 예제
단순 트리 모델 예제는 Qt의 표준 뷰 클래스와 함께 계층적 모델을 사용하는 방법을 보여줍니다.
Qt의 모델/뷰 아키텍처는 데이터의 추상 모델을 사용하여 뷰가 데이터 소스의 정보를 조작할 수 있는 표준 방법을 제공하여 액세스 방식을 단순화 및 표준화합니다. 단순 모델은 데이터를 항목의 테이블로 나타내며, 뷰가 인덱스 기반 시스템을 통해 이 데이터에 액세스할 수 있도록 합니다. 보다 일반적으로 모델은 각 항목이 하위 항목 테이블의 부모 역할을 하도록 하여 트리 구조의 형태로 데이터를 표현하는 데 사용할 수 있습니다.
트리 모델을 구현하기 전에 데이터가 외부 소스에서 제공되는지 아니면 모델 자체에서 유지 관리할 것인지 고려하는 것이 좋습니다. 이 예제에서는 외부 소스에서 데이터를 패키징하는 방법에 대해 설명하기보다는 데이터를 보관하는 내부 구조를 구현하겠습니다.
디자인 및 개념
데이터 구조를 표현하기 위해 사용하는 데이터 구조는 TreeItem
객체로 구축된 트리의 형태를 취합니다. 각 TreeItem
은 트리 보기의 항목을 나타내며 여러 개의 데이터 열을 포함합니다.
![]() | 간단한 트리 모델 구조 데이터는 포인터 기반 트리 구조로 서로 연결된 각 트리 보기의 각 항목에는 일반적으로 여러 열의 데이터(이 예에서는 제목과 요약)가 포함되어 있으므로 각 항목에 이 정보를 저장하는 것이 당연합니다. 간단하게 하기 위해 QVariant 개체 목록을 사용하여 항목의 각 열에 대한 데이터를 저장하겠습니다. |
포인터 기반 트리 구조를 사용하면 모델 인덱스를 뷰에 전달할 때 해당 항목의 주소를 인덱스에 기록하고( QAbstractItemModel::createIndex() 참조) 나중에 QModelIndex::internalPointer()로 검색할 수 있습니다. 이렇게 하면 모델을 더 쉽게 작성할 수 있고 동일한 항목을 참조하는 모든 모델 인덱스가 동일한 내부 데이터 포인터를 갖도록 보장할 수 있습니다.
적절한 데이터 구조를 갖추면 최소한의 추가 코드만으로 트리 모델을 생성하여 다른 컴포넌트에 모델 인덱스와 데이터를 제공할 수 있습니다.
TreeItem 클래스 정의
TreeItem
클래스는 다음과 같이 정의됩니다:
class TreeItem { public: explicit TreeItem(QVariantList data, TreeItem *parentItem = nullptr); void appendChild(std::unique_ptr<TreeItem> &&child); TreeItem *child(int row); int childCount() const; int columnCount() const; QVariant data(int column) const; int row() const; TreeItem *parentItem(); private: std::vector<std::unique_ptr<TreeItem>> m_childItems; QVariantList m_itemData; TreeItem *m_parentItem; };
이 클래스는 기본 C++ 클래스입니다. QObject 에서 상속하거나 신호 및 슬롯을 제공하지 않습니다. 이 클래스는 열 데이터와 트리 구조에서 해당 위치에 대한 정보를 포함하는 QVariant 목록을 보유하는 데 사용됩니다. 이 함수는 다음과 같은 기능을 제공합니다:
appendChildItem()
함수는 모델을 처음 구성할 때 데이터를 추가하는 데 사용되며 일반적인 사용 중에는 사용되지 않습니다.child()
및childCount()
함수를 사용하면 모델에서 하위 항목에 대한 정보를 얻을 수 있습니다.- 항목과 관련된 열 수에 대한 정보는
columnCount()
에서 제공하며, 각 열의 데이터는 data() 함수를 사용하여 얻을 수 있습니다. row()
및parent()
함수는 항목의 행 번호와 상위 항목을 가져오는 데 사용됩니다.
상위 항목과 열 데이터는 parentItem
및 itemData
비공개 멤버 변수에 저장됩니다. childItems
변수에는 항목의 하위 항목에 대한 포인터 목록이 포함되어 있습니다.
TreeItem 클래스 구현
생성자는 항목의 부모 항목과 각 열에 연결된 데이터를 기록하는 데만 사용됩니다.
TreeItem::TreeItem(QVariantList data, TreeItem *parent) : m_itemData(std::move(data)), m_parentItem(parent) {}
이 항목에 속하는 각 하위 항목에 대한 포인터는 childItems
비공개 멤버 변수에 std::unique_ptr로 저장됩니다. 클래스의 소멸자가 호출되면 자식 항목은 메모리를 재사용할 수 있도록 자동으로 삭제됩니다:
각 자식 항목은 모델이 처음에 데이터로 채워질 때 생성되므로 자식 항목을 추가하는 함수는 간단합니다:
void TreeItem::appendChild(std::unique_ptr<TreeItem> &&child) { m_childItems.push_back(std::move(child)); }
각 항목은 적절한 행 번호가 주어지면 자식 항목을 반환할 수 있습니다. 예를 들어, 위 다이어그램에서 문자 "A"로 표시된 항목은 row = 0
로 루트 항목의 하위 항목에 해당하고, "B" 항목은 row = 1
로 "A" 항목의 하위 항목이며, "C" 항목은 row = 1
로 루트 항목의 하위 항목에 해당합니다.
child()
함수는 항목의 하위 항목 목록에서 지정된 행 번호에 해당하는 하위 항목을 반환합니다:
TreeItem *TreeItem::child(int row) { return row >= 0 && row < childCount() ? m_childItems.at(row).get() : nullptr; }
보유한 하위 항목의 수는 childCount()
로 확인할 수 있습니다:
int TreeItem::childCount() const { return int(m_childItems.size()); }
TreeModel
는 이 함수를 사용하여 주어진 상위 항목에 대해 존재하는 행 수를 확인합니다.
row()
함수는 부모 항목 목록 내에서 항목의 위치를 보고합니다:
int TreeItem::row() const { if (m_parentItem == nullptr) 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()
함수에 의해 간단하게 반환됩니다.
int TreeItem::columnCount() const { return int(m_itemData.count()); }
열 데이터는 data()
함수에 의해 반환됩니다. QList::value () 편의 함수를 사용하여 경계를 확인하고 경계를 위반하는 경우 기본으로 구성된 QVariant 을 반환합니다:
QVariant TreeItem::data(int column) const { return m_itemData.value(column); }
항목의 부모는 parent()
로 찾습니다:
TreeItem *TreeItem::parentItem() { return m_parentItem; }
모델의 루트 항목에는 부모가 없으므로 이 함수는 이 경우 0을 반환합니다. TreeModel::parent()
함수를 구현할 때 모델이 이 경우를 올바르게 처리하는지 확인해야 합니다.
트리모델 클래스 정의
TreeModel
클래스는 다음과 같이 정의됩니다:
class TreeModel : public QAbstractItemModel { Q_OBJECT public: Q_DISABLE_COPY_MOVE(TreeModel) explicit TreeModel(const QString &data, QObject *parent = nullptr); ~TreeModel() override; 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 = {}) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = {}) const override; int columnCount(const QModelIndex &parent = {}) const override; private: static void setupModelData(const QList<QStringView> &lines, TreeItem *parent); std::unique_ptr<TreeItem> rootItem; };
이 클래스는 읽기 전용 모델을 제공하는 QAbstractItemModel 의 다른 대부분의 서브클래스와 유사합니다. 생성자의 형식과 setupModelData()
함수만 이 모델에 고유합니다. 또한 모델이 소멸될 때 정리하는 소멸자를 제공합니다.
TreeModel 클래스 구현
단순화를 위해 이 모델은 데이터를 편집할 수 없습니다. 따라서 생성자는 모델이 뷰 및 델리게이트와 공유할 데이터가 포함된 인수를 받습니다:
TreeModel::TreeModel(const QString &data, QObject *parent) : QAbstractItemModel(parent) , rootItem(std::make_unique<TreeItem>(QVariantList{tr("Title"), tr("Summary")})) { setupModelData(QStringView{data}.split(u'\n'), rootItem.get()); }
모델의 루트 항목을 만드는 것은 생성자의 몫입니다. 이 항목에는 편의상 세로 헤더 데이터만 포함됩니다. 또한 모델 데이터를 포함하는 내부 데이터 구조를 참조하는 데 사용되며, 모델에서 최상위 항목의 가상의 부모를 나타내는 데 사용됩니다. 루트 항목은 모델이 삭제될 때 항목의 전체 트리가 삭제되도록 하기 위해 std::unique_ptr로 관리됩니다.
모델의 내부 데이터 구조는 setupModelData()
함수에 의해 항목으로 채워집니다. 이 함수는 이 문서의 마지막 부분에서 별도로 살펴보겠습니다.
소멸자는 모델이 삭제될 때 루트 항목과 그 하위 항목이 모두 삭제되도록 합니다. 루트 항목은 고유_ptr에 저장되므로 이 작업은 자동으로 수행됩니다.
TreeModel::~TreeModel() = default;
모델이 생성되고 설정된 후에는 모델에 데이터를 추가할 수 없으므로 항목의 내부 트리가 관리되는 방식이 간소화됩니다.
모델은 index()
함수를 구현하여 뷰 및 델리게이트가 데이터에 액세스할 때 사용할 인덱스를 제공해야 합니다. 인덱스는 행 및 열 번호와 상위 모델 인덱스로 참조될 때 다른 컴포넌트에 대해 생성됩니다. 잘못된 모델 인덱스가 부모로 지정된 경우 모델의 최상위 항목에 해당하는 인덱스를 반환하는 것은 모델의 몫입니다.
모델 인덱스가 제공되면 먼저 유효한지 여부를 확인합니다. 그렇지 않은 경우 최상위 항목이 참조되고 있다고 가정하고, 그렇지 않은 경우 internalPointer() 함수를 사용하여 모델 인덱스에서 데이터 포인터를 가져와 TreeItem
객체를 참조하는 데 사용합니다. 우리가 구성하는 모든 모델 인덱스에는 기존 TreeItem
에 대한 포인터가 포함되므로 수신하는 모든 유효한 모델 인덱스에는 유효한 데이터 포인터가 포함된다는 것을 보장할 수 있습니다.
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) return {}; TreeItem *parentItem = parent.isValid() ? static_cast<TreeItem*>(parent.internalPointer()) : rootItem.get(); if (auto *childItem = parentItem->child(row)) return createIndex(row, column, childItem); return {}; }
이 함수의 행과 열 인수는 해당 상위 항목의 하위 항목을 참조하므로 TreeItem::child()
함수를 사용하여 항목을 가져옵니다. createIndex () 함수는 반환할 모델 인덱스를 생성하는 데 사용됩니다. 행 및 열 번호와 항목 자체에 대한 포인터를 지정합니다. 모델 인덱스는 나중에 항목의 데이터를 가져오는 데 사용할 수 있습니다.
TreeItem
객체가 정의되는 방식 덕분에 parent()
함수를 쉽게 작성할 수 있습니다:
QModelIndex TreeModel::parent(const QModelIndex &index) const { if (!index.isValid()) return {}; auto *childItem = static_cast<TreeItem*>(index.internalPointer()); TreeItem *parentItem = childItem->parentItem(); return parentItem != rootItem.get() ? createIndex(parentItem->row(), 0, parentItem) : QModelIndex{}; }
루트 항목에 해당하는 모델 인덱스를 반환하지 않도록 하기만 하면 됩니다. index()
함수가 구현되는 방식과 일관성을 유지하기 위해 모델에서 최상위 항목의 부모에 대해 잘못된 모델 인덱스를 반환합니다.
반환할 모델 인덱스를 만들 때는 해당 상위 항목의 행 번호와 열 번호를 지정해야 합니다. 행 번호는 TreeItem::row()
함수를 사용하여 쉽게 찾을 수 있지만, 상위 항목의 열 번호는 0을 지정하는 규칙을 따릅니다. 모델 인덱스는 index()
함수와 동일한 방식으로 createIndex()로 생성됩니다.
rowCount()
함수는 지정된 모델 인덱스에 해당하는 TreeItem
의 하위 항목 수 또는 잘못된 인덱스가 지정된 경우 최상위 항목 수를 반환합니다:
int TreeModel::rowCount(const QModelIndex &parent) const { if (parent.column() > 0) return 0; const TreeItem *parentItem = parent.isValid() ? static_cast<const TreeItem*>(parent.internalPointer()) : rootItem.get(); 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() || role != Qt::DisplayRole) return {}; const auto *item = static_cast<const TreeItem*>(index.internalPointer()); return item->data(index.column()); }
이 구현에서는 DisplayRole 만 지원하며, 잘못된 모델 인덱스에 대해서는 잘못된 QVariant 객체도 반환한다는 점에 유의하세요.
flags()
함수를 사용하여 뷰가 모델이 읽기 전용임을 알 수 있도록 합니다:
Qt::ItemFlags TreeModel::flags(const QModelIndex &index) const { return index.isValid() ? QAbstractItemModel::flags(index) : Qt::ItemFlags(Qt::NoItemFlags); }
headerData()
함수는 루트 항목에 편리하게 저장한 데이터를 반환합니다:
QVariant TreeModel::headerData(int section, Qt::Orientation orientation, int role) const { return orientation == Qt::Horizontal && role == Qt::DisplayRole ? rootItem->data(section) : QVariant{}; }
이 정보는 생성자에서 지정하거나 headerData()
함수에 하드 코딩하는 등 다른 방식으로 제공될 수 있었습니다.
모델에서 데이터 설정
setupModelData()
함수를 사용하여 모델의 초기 데이터를 설정합니다. 이 함수는 텍스트 파일을 구문 분석하여 모델에서 사용할 텍스트 문자열을 추출하고 데이터와 전체 모델 구조를 모두 기록하는 항목 개체를 만듭니다. 당연히 이 함수는 이 모델에 매우 특정한 방식으로 작동합니다. 이 함수의 동작에 대한 설명은 다음과 같으며, 자세한 내용은 예제 코드를 참조하시기 바랍니다.
먼저 다음 형식의 텍스트 파일로 시작합니다:
Getting Started How to familiarize yourself with Qt Widgets Designer Launching Designer Running the Qt Widgets Designer application The User Interface How to interact with Qt Widgets Designer ...
Connection Editing Mode Connecting widgets together with signals and slots Connecting Objects Making connections in Qt Widgets Designer Editing Connections Changing existing connections
다음 두 가지 규칙으로 텍스트 파일을 처리합니다:
- 각 줄의 각 문자열 쌍에 대해 트리 구조로 항목(또는 노드)을 만들고 각 문자열을 항목의 데이터 열에 배치합니다.
- 한 줄의 첫 번째 문자열이 이전 줄의 첫 번째 문자열에 대해 들여쓰기되면 해당 항목을 이전에 생성된 항목의 하위 항목으로 만듭니다.
모델이 올바르게 작동하려면 올바른 데이터와 상위 항목을 사용하여 TreeItem
인스턴스만 생성하면 됩니다.
모델 테스트하기
아이템 모델을 올바르게 구현하는 것은 어려울 수 있습니다. QAbstractItemModelTester 클래스는 Qt Test 모듈의 클래스는 모델 인덱스 생성 및 부모-자식 관계와 같은 모델 일관성을 검사합니다.
예를 들어 Qt 유닛 테스트의 일부로 클래스 생성자에 모델 인스턴스를 전달하여 모델을 테스트할 수 있습니다:
TestSimpleTreeModel 클래스: 공용 QObject { Q_OBJECTprivate slots: void testTreeModel(); };void TestSimpleTreeModel::testTreeModel() { constexpr auto fileName = ":/default.txt"_L1; QFile file(fileName); QVERIFY2(file.open(QIODevice::읽기 전용 | QIODevice::Text), qPrintable(fileName + " cannot be opened: "_L1 + file.errorString())); 트리모델 모델(QString::fromUtf8(file.readAll())); QAbstractItemModelTester tester(&model); } QTEST_APPLESS_MAIN(TestSimpleTreeModel)#include "test.moc"
ctest
실행 파일을 사용하여 실행할 수 있는 테스트를 만들려면 add_test()
을 사용합니다:
# Unit Test include(CTest) qt_add_executable(simpletreemodel_tester test.cpp treeitem.cpp treeitem.h treemodel.cpp treemodel.h) target_link_libraries(simpletreemodel_tester PRIVATE Qt6::Core Qt6::Test ) if(ANDROID) target_link_libraries(simpletreemodel_tester PRIVATE Qt6::Gui ) endif() qt_add_resources(simpletreemodel_tester "simpletreemodel" PREFIX "/" FILES ${simpletreemodel_resource_files} ) add_test(NAME simpletreemodel_tester COMMAND simpletreemodel_tester)
© 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.