간단한 트리 모델 예시
'간단한 트리 모델' 예제는 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() ` 함수를 통해 반환됩니다. 우리는 경계를 확인하고, 경계가 위반될 경우 기본 생성된 ` QVariant `를 반환하는 편의 함수 ` QList::value()`를 사용합니다:
QVariant TreeItem::data(int column) const
{
return m_itemData.value(column);
}parent() 를 사용하여 항목의 부모를 찾습니다:
TreeItem *TreeItem::parentItem()
{
return m_parentItem;
}모델의 루트 항목에는 부모가 없으므로, 이 경우 해당 함수는 0을 반환한다는 점에 유의하십시오. ` TreeModel::parent() ` 함수를 구현할 때 모델이 이 경우를 올바르게 처리하도록 해야 합니다.
TreeModel 클래스 정의
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() ` 함수를 통해 항목으로 채워집니다. 이 함수에 대해서는 본 문서의 마지막 부분에서 별도로 살펴보겠습니다.
소멸자는 모델이 소멸될 때 루트 항목과 그 모든 하위 항목이 삭제되도록 보장합니다. 루트 항목은 unique_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 {};
}이 함수의 행(row) 및 열(column) 인수는 해당 부모 항목의 자식 항목을 가리키므로, ` 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 모듈의 `xml-ph-0000@deepl.internal` 클래스는 모델 인덱스 생성 및 부모-자식 관계와 같은 모델의 일관성을 확인합니다.
예를 들어 Qt Test 단위 테스트의 일환으로 모델 인스턴스를 클래스 생성자에 전달하기만 하면 모델을 테스트할 수 있습니다:
class TestSimpleTreeModel : public QObject
{
Q_OBJECT
private slots:
void testTreeModel();
};
void TestSimpleTreeModel::testTreeModel()
{
constexpr auto fileName = ":/default.txt"_L1;
QFile file(fileName);
QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text),
qPrintable(fileName + " cannot be opened: "_L1 + file.errorString()));
TreeModel model(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_tester"
PREFIX
"/"
FILES
${simpletreemodel_resource_files}
)
add_test(NAME simpletreemodel_tester
COMMAND simpletreemodel_tester)© 2026 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.