이 페이지에서

모델/뷰 튜토리얼

모든 UI 개발자는 모델-뷰 프로그래밍에 대해 알아야 하며, 이 튜토리얼의 목표는 이 주제에 대해 쉽게 이해할 수 있는 입문 정보를 제공하는 것입니다.

테이블, 리스트, 트리 위젯은 GUI에서 자주 사용되는 구성 요소입니다. 이러한 위젯이 데이터에 접근하는 방법에는 두 가지가 있습니다. 전통적인 방식은 데이터를 저장하기 위한 내부 컨테이너를 포함하는 위젯을 사용하는 것입니다. 이 접근 방식은 매우 직관적이지만, 많은 복잡한 애플리케이션에서는 데이터 동기화 문제를 야기합니다. 두 번째 접근 방식은 모델/뷰 프로그래밍으로, 위젯이 내부 데이터 컨테이너를 유지하지 않습니다. 위젯은 표준화된 인터페이스를 통해 외부 데이터에 접근하므로 데이터 중복을 방지할 수 있습니다. 처음에는 복잡해 보일 수 있지만, 자세히 살펴보면 이해하기 쉬울 뿐만 아니라 모델/뷰 프로그래밍이 가진 많은 장점도 더 명확하게 드러납니다.

트리 뷰에 표시된 디렉터리 및 그 내용

이 과정에서 우리는 Qt가 제공하는 다음과 같은 몇 가지 기본 기술에 대해 배우게 될 것입니다:

  • 표준 위젯과 모델/뷰 위젯의 차이점
  • 폼과 모델 간의 어댑터
  • 간단한 모델/뷰 애플리케이션 개발
  • 사전 정의된 모델
  • 다음과 같은 중급 주제:
    • 트리 뷰
    • 선택
    • 델리게이트
    • 모델 테스트를 이용한 디버깅

또한 새로운 애플리케이션을 모델/뷰 프로그래밍을 통해 더 쉽게 작성할 수 있는지, 아니면 기존 위젯을 사용해도 동일한 효과를 낼 수 있는지 알아보게 될 것입니다.

이 튜토리얼에는 직접 수정하여 프로젝트에 통합할 수 있는 예제 코드가 포함되어 있습니다. 튜토리얼의 소스 코드는 Qt의 examples/widgets/tutorials/modelview 디렉터리에 있습니다.

더 자세한 정보를 원하시면 참조 문서를 살펴보시기 바랍니다.

1. 소개

모델/뷰(Model/View)는 데이터 세트를 처리하는 위젯에서 데이터와 뷰를 분리하는 데 사용되는 기술입니다. 표준 위젯은 데이터와 뷰를 분리하도록 설계되지 않았기 때문에, Qt에는 두 가지 유형의 위젯이 존재합니다. 두 유형의 위젯은 외관상 동일해 보이지만, 데이터와의 상호작용 방식은 다릅니다.

표준 위젯은 위젯 자체에 포함된 데이터를 사용합니다.

“뷰”와 “데이터”가 분리된 구조

뷰 클래스는 외부 데이터(모델)를 기반으로 작동합니다.

데이터를 처리하기 위해 “뷰”와 “모델”을 사용하는 구조

1.1 표준 위젯

표준 테이블 위젯을 좀 더 자세히 살펴보겠습니다. 테이블 위젯은 사용자가 변경할 수 있는 데이터 요소들로 구성된 2차원 배열입니다. 테이블 위젯이 제공하는 데이터 요소를 읽고 쓰면서 프로그램 흐름에 테이블 위젯을 통합할 수 있습니다. 이 방법은 매우 직관적이며 많은 애플리케이션에서 유용하지만, 표준 테이블 위젯을 사용하여 데이터베이스 테이블을 표시하고 편집하는 데는 문제가 있을 수 있습니다. 데이터의 두 복사본, 즉 위젯 외부에 있는 것과 위젯 내부에 있는 것을 서로 조율해야 하기 때문입니다. 개발자는 두 버전을 동기화할 책임이 있습니다. 게다가 표현과 데이터 간의 밀접한 결합으로 인해 단위 테스트를 작성하기가 더 어려워집니다.

1.2 모델/뷰(Model/View)의 등장

모델/뷰(Model/View)는 보다 유연한 아키텍처를 활용하는 해결책을 제시했습니다. 모델/뷰는 표준 위젯에서 발생할 수 있는 데이터 일관성 문제를 해소합니다. 또한 하나의 모델을 여러 뷰에 전달할 수 있으므로, 동일한 데이터에 대해 여러 뷰를 더 쉽게 사용할 수 있게 해줍니다. 가장 중요한 차이점은 모델/뷰 위젯이 테이블 셀 뒤에 데이터를 저장하지 않는다는 것입니다. 사실, 이 위젯들은 데이터에서 직접 작동합니다. 뷰 클래스는 데이터의 구조를 알지 못하므로, 데이터가 ` QAbstractItemModel ` 인터페이스를 따르도록 하기 위해 래퍼를 제공해야 합니다. 뷰는 이 인터페이스를 사용하여 데이터를 읽고 씁니다. QAbstractItemModel 를 구현하는 클래스의 모든 인스턴스는 모델이라고 합니다. 뷰가 모델에 대한 포인터를 수신하면, 해당 모델의 내용을 읽고 표시하며, 이를 편집하는 역할을 수행합니다.

1.3 모델/뷰 위젯 개요

다음은 모델/뷰 위젯과 이에 대응하는 표준 위젯에 대한 개요입니다.

위젯표준 위젯
(항목 기반 편의 클래스)
모델/뷰 뷰 클래스
(외부 데이터와 함께 사용하기 위한)
내용을 목록 형태로 표시하는 디렉터리QListWidgetQListView
내용을 표 형식으로 표시하는 디렉토리QTableWidgetQTableView
내용을 트리 형태로 표시하는 디렉터리QTreeWidgetQTreeView
내용을 열 형태로 표시하는 디렉터리QColumnView 트리 구조를 목록의 계층 구조로 표시합니다
모델에 있는 항목을 표시하는 콤보박스QComboBox 뷰 클래스로도, 전통적인 위젯으로도 작동할 수 있음

1.4 폼과 모델 간 어댑터 사용

폼과 모델 사이에 어댑터를 두면 유용할 수 있습니다.

테이블에 저장된 데이터는 테이블 내에서 직접 편집할 수 있지만, 텍스트 필드에서 데이터를 편집하는 것이 훨씬 더 편리합니다. 데이터셋 대신 단일 값(QLineEdit, QCheckBox 등)을 다루는 위젯의 경우, 데이터와 뷰를 분리하는 직접적인 모델/뷰 대응 관계가 없으므로, 폼을 데이터 소스에 연결하기 위해서는 어댑터가 필요합니다.

QDataWidgetMapper 는 양식 위젯을 테이블 행에 매핑하고, 데이터베이스 테이블용 양식을 매우 쉽게 구축할 수 있게 해주기 때문에 훌륭한 솔루션입니다.

개인 정보 및 기타 정보가 기재된 양식

어댑터의 또 다른 예로는 QCompleter 이 있습니다. Qt에는 QComboBox 와 같이 Qt Widgets에서 자동 완성 기능을 제공하는 QCompleter 가 있으며, 아래에서 볼 수 있듯이 QLineEdit 도 있습니다. QCompleter 는 모델을 데이터 소스로 사용합니다.

"Em"이라는 문자에 대한 제안이 포함된 완성형

2. 간단한 모델/뷰 애플리케이션

모델/뷰 애플리케이션을 개발하려면 어디서부터 시작해야 할까요? 간단한 예제부터 시작하여 단계별로 확장해 나가는 것을 권장합니다. 이렇게 하면 아키텍처를 훨씬 쉽게 이해할 수 있습니다. IDE를 실행하기 전에 모델/뷰 아키텍처를 자세히 이해하려고 시도하는 것은 많은 개발자에게 그다지 편리하지 않은 것으로 나타났습니다. 데모 데이터가 포함된 간단한 모델/뷰 애플리케이션으로 시작하는 것이 훨씬 더 쉽습니다. 한번 시도해 보세요! 아래 예제의 데이터를 자신의 데이터로 간단히 대체하기만 하면 됩니다.

아래에는 모델/뷰 프로그래밍의 다양한 측면을 보여주는, 매우 간단하고 독립적인 7개의 애플리케이션이 있습니다. 소스 코드는 examples/widgets/tutorials/modelview 디렉터리에서 찾을 수 있습니다.

2.1 읽기 전용 테이블

먼저, QTableView 를 사용하여 데이터를 표시하는 애플리케이션부터 시작하겠습니다. 편집 기능은 나중에 추가할 예정입니다.

(소스 파일: examples/widgets/tutorials/modelview/1_readonly/main.cpp)

// main.cpp
#include <QApplication>
#include <QTableView>
#include "mymodel.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QTableView tableView;
    MyModel myModel;
    tableView.setModel(&myModel);
    tableView.show();
    return a.exec();
}

일반적인 main() 함수가 있습니다:

여기서 흥미로운 부분은 다음과 같습니다. MyModel의 인스턴스를 생성하고, ` tableView.setModel(&myModel);`을 사용하여 그 포인터를 ` tableView`에 전달합니다. ` tableView `은 전달받은 포인터의 메서드를 호출하여 다음 두 가지를 확인합니다:

  • 표에 표시해야 할 행과 열의 수.
  • 각 셀에 어떤 내용을 출력해야 하는지.

모델에는 이에 대응할 수 있는 코드가 필요합니다.

테이블 데이터 세트가 있으므로, 더 일반적인 ` QAbstractItemModel`보다 사용하기 쉬운 ` QAbstractTableModel `부터 시작해 봅시다.

(파일 출처: examples/widgets/tutorials/modelview/1_readonly/mymodel.h)

// mymodel.h
#include <QAbstractTableModel>

class MyModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    explicit MyModel(QObject *parent = nullptr);

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
};

QAbstractTableModel 세 가지 추상 메서드를 구현해야 합니다.

(파일 출처: examples/widgets/tutorials/modelview/1_readonly/mymodel.cpp)

// mymodel.cpp
#include "mymodel.h"

MyModel::MyModel(QObject *parent)
    : QAbstractTableModel(parent)
{
}

int MyModel::rowCount(const QModelIndex & /*parent*/) const
{
   return 2;
}

int MyModel::columnCount(const QModelIndex & /*parent*/) const
{
    return 3;
}

QVariant MyModel::data(const QModelIndex &index, int role) const
{
    if (role == Qt::DisplayRole)
       return QString("Row%1, Column%2")
                   .arg(index.row() + 1)
                   .arg(index.column() +1);

    return QVariant();
}

행과 열의 수는 ` MyModel::rowCount()` 및 ` MyModel::columnCount()`를 통해 제공됩니다. 뷰가 셀의 텍스트를 알아야 할 때는 ` MyModel::data()` 메서드를 호출합니다. 행 및 열 정보는 ` index ` 매개변수로 지정되며, 역할은 ` Qt::DisplayRole`로 설정됩니다. 다른 역할들에 대해서는 다음 섹션에서 다룹니다. 이 예제에서는 표시해야 할 데이터가 생성됩니다. 실제 애플리케이션에서는 ` MyModel `에 ` MyData`라는 멤버가 있으며, 이 멤버는 모든 읽기 및 쓰기 작업의 대상이 됩니다.

이 간단한 예제는 모델의 수동적인 특성을 보여줍니다. 모델은 언제 사용될지, 어떤 데이터가 필요한지 알지 못합니다. 모델은 뷰가 요청할 때마다 단순히 데이터를 제공할 뿐입니다.

모델의 데이터를 변경해야 할 때는 어떻게 될까요? 뷰는 데이터가 변경되어 다시 읽어야 한다는 사실을 어떻게 파악할까요? 모델은 어떤 범위의 셀이 변경되었는지를 나타내는 신호를 보내야 합니다. 이에 대해서는 2.3절에서 설명하겠습니다.

2.2 역할(Roles)을 활용한 읽기 전용 예제 확장

모델은 뷰에 표시될 텍스트를 제어할 뿐만 아니라 텍스트의 모양도 제어합니다. 모델을 약간 변경하면 다음과 같은 결과가 나타납니다:

셀에 사용자 지정 서식이 적용된 표

사실, 글꼴, 배경색, 정렬 방식 및 체크박스를 설정하기 위해 data() 메서드 외에는 아무것도 변경할 필요가 없습니다. 아래는 위에서 보여준 결과를 생성하는 data() 메서드입니다. 차이점은 이번에는 int형 매개변수 role을 사용하여 그 값에 따라 서로 다른 정보를 반환한다는 점입니다.

(소스 파일: examples/widgets/tutorials/modelview/2_formatting/mymodel.cpp)

// mymodel.cpp
QVariant MyModel::data(const QModelIndex&index, int role) const
{
    int row = index.row();
    int col = index.column();
    // 이 메서드가 호출될 때 로그 메시지를 생성합니다
    qDebug() << QString("row %1, col%2, role %3")
           .arg(row).arg(col).arg(role);

    switch (role) {
    case Qt::DisplayRole:
        if (row== 0&& col== 1) return QString("<--left");
        if (row== 1&& col== 1) return QString("right-->");

        return QString("행%1, 열%2")
                .arg(row + 1)
                .arg(col+ 1);
    case Qt::FontRole:
        if (row== 0&& col== 0) { // (0,0) 셀에 대해서만 글꼴 변경
            QFont boldFont;
            boldFont.setBold(true);
            return boldFont;
        }
        break;
    case Qt::BackgroundRole:
        if (row== 1&& col== 2)  // (1,2) 셀에 대해서만 배경색 변경
            return QBrush(Qt::red);
        break;
    case Qt::TextAlignmentRole:
        if (row== 1&& col== 1) // (1,1) 셀에 대해서만 텍스트 정렬 변경
            return int(Qt::AlignRight | Qt::AlignVCenter);
        break;
    case Qt::CheckStateRole:
        if (row== 1&& col== 0) // 셀(1,0)에 체크박스 추가
            return Qt::Checked;
        break;
    }
    return QVariant();
}

각 서식 속성은 data() 메서드를 별도로 호출하여 모델에서 요청됩니다. role 매개변수는 모델에 어떤 속성이 요청되고 있는지 알리기 위해 사용됩니다:

enum Qt::ItemDataRole의미유형
Qt::DisplayRoletextQString
Qt::FontRolefontQFont
BackgroundRole셀 배경용 브러시QBrush
Qt::TextAlignmentRole텍스트 정렬enum Qt::AlignmentFlag
Qt::CheckStateRoleQVariant() 를 사용하여 체크박스를 숨기고,

다음과 같이 체크박스를 설정합니다. Qt::Checked

또는 Qt::Unchecked

enum Qt::ItemDataRole

Qt::ItemDataRole 열거형의 기능에 대해 자세히 알아보려면 Qt 네임스페이스 문서를 참조하십시오.

이제 분리된 모델을 사용하는 것이 애플리케이션 성능에 어떤 영향을 미치는지 파악해야 하므로, 뷰가 ` data()` 메서드를 호출하는 빈도를 추적해 봅시다. 뷰가 모델을 호출하는 빈도를 추적하기 위해, ` data()` 메서드에 디버그 문장을 삽입하여 오류 출력 스트림에 기록되도록 했습니다. 이 간단한 예제에서 ` data()`는 42번 호출됩니다. 필드 위에 커서를 올릴 때마다 ` data()`가 다시 호출되며, 각 셀마다 7번씩 호출됩니다. 따라서 ` data()`가 호출될 때 데이터가 준비되어 있어야 하고, 비용이 많이 드는 조회 작업이 캐시되어 있어야 하는 것이 중요합니다.

2.3 테이블 셀 내부의 시계

셀에 시간을 표시하는 표

여전히 읽기 전용 테이블이지만, 이번에는 현재 시간을 표시하고 있기 때문에 내용이 매초마다 변경됩니다.

(파일 출처: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)

QVariant MyModel::data(const QModelIndex &index, int role) const
{
    int row = index.row();
    int col = index.column();

    if (role == Qt::DisplayRole && row == 0 && col == 0)
        return QTime::currentTime().toString();

    return QVariant();
}

시계가 똑딱거리게 하려면 뭔가 빠진 것이 있습니다. 매초마다 시간이 변경되었으므로 뷰에서 다시 읽어야 한다는 사실을 뷰에 알려야 합니다. 이를 위해 타이머를 사용합니다. 생성자에서 타이머의 간격을 1초로 설정하고 타임아웃 신호를 연결합니다.

(파일 출처: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)

MyModel::MyModel(QObject *parent)
    : QAbstractTableModel(parent)
    , timer(new QTimer(this))
{
    timer->setInterval(1000);
    connect(timer, &QTimer::timeout , this, &MyModel::timerHit);
    timer->start();
}

다음은 이에 해당하는 슬롯입니다:

(파일 출처: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)

void MyModel::timerHit()
{
    // we identify the top left cell
    QModelIndex topLeft = createIndex(0,0);
    // emit a signal to make the view reread identified data
    emit dataChanged(topLeft, topLeft, {Qt::DisplayRole});
}

dataChanged() 신호를 발생시켜 뷰가 왼쪽 상단 셀의 데이터를 다시 읽도록 요청합니다. dataChanged() 신호를 뷰에 명시적으로 연결하지 않았다는 점에 유의하십시오. 이는 setModel()를 호출했을 때 자동으로 이루어졌습니다.

2.4 열과 행에 대한 헤더 설정

헤더는 뷰 메서드를 통해 숨길 수 있습니다: tableView->verticalHeader()->hide();

열 머리글을 나타내는 표

그러나 헤더 내용은 모델을 통해 설정되므로, ` headerData()` 메서드를 재구현합니다:

(파일 출처: examples/widgets/tutorials/modelview/4_headers/mymodel.cpp)

QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
        switch (section) {
        case 0:
            return QString("first");
        case 1:
            return QString("second");
        case 2:
            return QString("third");
        }
    }
    return QVariant();
}

headerData() 메서드에도 MyModel::data()에서와 동일한 의미를 가진 매개변수 역할이 있다는 점에 유의하십시오.

2.5 최소 편집 예제

이 예제에서는 테이블 셀에 입력된 값을 반복하여 창 제목에 내용을 자동으로 채우는 애플리케이션을 만들어 보겠습니다. 창 제목에 쉽게 접근할 수 있도록 ` QTableView `를 ` QMainWindow` 내에 배치합니다.

편집 기능의 사용 가능 여부는 모델에서 결정합니다. 사용 가능한 편집 기능을 활성화하려면 모델을 수정하기만 하면 됩니다. 이는 다음 가상 메서드, 즉 setData() 및 flags()를 재구현함으로써 수행됩니다.

(소스 파일: examples/widgets/tutorials/modelview/5_edit/mymodel.h)

// mymodel.h
#include <QAbstractTableModel>
#include <QString>

const int COLS= 3;
const int ROWS= 2;

class MyModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    MyModel(QObject *parent = nullptr);
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;
private:
    QString m_gridData[ROWS][COLS];  //holds text entered into QTableView
signals:
    void editCompleted(const QString &);
};

데이터 저장을 위해 the, QString, m_gridData 와 같은 2차원 배열을 사용합니다. 이로 인해 m_gridData 가 MyModel 의 핵심이 됩니다. MyModel 의 나머지 부분은 래퍼 역할을 하며, m_gridData 를 QAbstractItemModel 인터페이스에 맞게 조정합니다. 또한 수정된 텍스트를 창 제목으로 전송할 수 있도록 editCompleted() 신호를 도입했습니다.

(소스 파일: examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)

bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    if (role == Qt::EditRole) {
        if (!checkIndex(index))
            return false;
        //save value from editor to member m_gridData
        m_gridData[index.row()][index.column()] = value.toString();
        //for presentation purposes only: build and emit a joined string
        QString result;
        for (int row = 0; row < ROWS; row++) {
            for (int col= 0; col < COLS; col++)
                result += m_gridData[row][col] + ' ';
        }
        emit editCompleted(result);
        return true;
    }
    return false;
}

setData()는 사용자가 셀을 편집할 때마다 호출됩니다. index 매개변수는 어떤 필드가 편집되었는지 알려주며, value 는 편집 결과값을 제공합니다. 셀에는 텍스트만 포함되어 있으므로 role은 항상 Qt::EditRole 로 설정됩니다. 체크박스가 있고 사용자 권한이 체크박스 선택을 허용하도록 설정된 경우, role이 Qt::CheckStateRole 로 설정된 상태로 호출이 이루어지기도 합니다.

(파일 출처: examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)

Qt::ItemFlags MyModel::flags(const QModelIndex &index) const
{
    return Qt::ItemIsEditable | QAbstractTableModel::flags(index);
}

flags()를 사용하여 셀의 다양한 속성을 조정할 수 있습니다.

Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled 를 반환하기만 하면 편집기에 해당 셀을 선택할 수 있음을 알릴 수 있습니다.

한 셀을 편집할 때 해당 셀의 데이터보다 더 많은 데이터가 수정되는 경우, 변경된 데이터를 읽을 수 있도록 모델은 dataChanged() 신호를 발산해야 합니다.

3. 중급 주제

3.1 TreeView

위의 예제를 트리 뷰가 포함된 애플리케이션으로 변환할 수 있습니다. 단순히 QTableView 을 QTreeView 로 바꾸기만 하면 읽기/쓰기 가능한 트리가 생성됩니다. 모델에는 아무런 변경도 필요하지 않습니다. 모델 자체에 계층 구조가 없기 때문에 트리에도 계층 구조가 없습니다.

계층 구조 없이 테이블 데이터를 표시하는 트리 뷰

QListView, QTableView 및 QTreeView 은 모두 리스트, 테이블, 트리를 통합한 모델 추상화를 사용합니다. 이를 통해 동일한 모델에서 여러 가지 유형의 뷰 클래스를 사용할 수 있습니다.

리스트 모델, 테이블 모델, 트리 모델을 비교한 도표

지금까지의 예제 모델은 다음과 같습니다:

현재 모델 구조도

실제 트리를 표시하고자 합니다. 위의 예제에서는 모델을 만들기 위해 데이터를 래핑했습니다. 이번에는 QStandardItemModel 를 사용하는데, 이는 QAbstractItemModel 를 구현하는 계층적 데이터용 컨테이너입니다. 트리를 표시하려면 QStandardItemModel 에 QStandardItem를 채워야 하며, 이 는 텍스트, 글꼴, 체크박스 또는 브러시와 같은 항목의 모든 표준 속성을 포함할 수 있습니다.

모델과 뷰의 다이어그램

(소스 파일: examples/widgets/tutorials/modelview/6_treeview/mainwindow.cpp)

// modelview.cpp
#include "mainwindow.h"

#include <QTreeView>
#include <QStandardItemModel>
#include <QStandardItem>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , treeView(new QTreeView(this))
    , standardModel(new QStandardItemModel(this))
{
    setCentralWidget(treeView);

    QList<QStandardItem *> preparedRow = prepareRow("first", "second", "third");
    QStandardItem *item = standardModel->invisibleRootItem();
    // adding a row to the invisible root item produces a root element
    item->appendRow(preparedRow);

    QList<QStandardItem *> secondRow = prepareRow("111", "222", "333");
    // adding a row to an item starts a subtree
    preparedRow.first()->appendRow(secondRow);

    treeView->setModel(standardModel);
    treeView->expandAll();
}

QList<QStandardItem *> MainWindow::prepareRow(const QString &first,
                                              const QString &second,
                                              const QString &third) const
{
    return {new QStandardItem(first),
            new QStandardItem(second),
            new QStandardItem(third)};
}

QStandardItemModel 를 인스턴스화하고 생성자에 QStandardItems 를 몇 개 추가하기만 하면 됩니다. QStandardItem 는 다른 QStandardItems 를 포함할 수 있으므로 계층적 데이터 구조를 만들 수 있습니다. 노드는 뷰 내에서 접히거나 펼쳐집니다.

3.2 선택 항목 처리

선택된 항목의 콘텐츠에 접근하여, 계층 수준과 함께 창 제목에 출력하고자 합니다.

선택한 내용이 파란색으로 표시된 모델의 모습

그럼 몇 가지 항목을 만들어 보겠습니다:

(파일 출처: examples/widgets/tutorials/modelview/7_selections/mainwindow.cpp)

#include "mainwindow.h"

#include <QTreeView>
#include <QStandardItemModel>
#include <QItemSelectionModel>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , treeView(new QTreeView(this))
    , standardModel(new QStandardItemModel(this))
{
    setCentralWidget(treeView);
    auto *rootNode = standardModel->invisibleRootItem();

    // defining a couple of items
    auto *americaItem = new QStandardItem("America");
    auto *mexicoItem =  new QStandardItem("Canada");
    auto *usaItem =     new QStandardItem("USA");
    auto *bostonItem =  new QStandardItem("Boston");
    auto *europeItem =  new QStandardItem("Europe");
    auto *italyItem =   new QStandardItem("Italy");
    auto *romeItem =    new QStandardItem("Rome");
    auto *veronaItem =  new QStandardItem("Verona");

    // building up the hierarchy
    rootNode->    appendRow(americaItem);
    rootNode->    appendRow(europeItem);
    americaItem-> appendRow(mexicoItem);
    americaItem-> appendRow(usaItem);
    usaItem->     appendRow(bostonItem);
    europeItem->  appendRow(italyItem);
    italyItem->   appendRow(romeItem);
    italyItem->   appendRow(veronaItem);

    // register the model
    treeView->setModel(standardModel);
    treeView->expandAll();

    // selection changes shall trigger a slot
    QItemSelectionModel *selectionModel = treeView->selectionModel();
    connect(selectionModel, &QItemSelectionModel::selectionChanged,
            this, &MainWindow::selectionChangedSlot);
}

뷰는 별도의 선택 모델 내에서 선택 항목을 관리하며, 이 모델은 ` selectionModel()` 메서드를 통해 가져올 수 있습니다. 슬롯을 ` selectionChanged()` 신호에 연결하기 위해 선택 모델을 가져옵니다.

(소스 파일: examples/widgets/tutorials/modelview/7_selections/mainwindow.cpp)

void MainWindow::selectionChangedSlot(const QItemSelection & /*newSelection*/, const QItemSelection & /*oldSelection*/)
{
    // get the text of the selected item
    const QModelIndex index = treeView->selectionModel()->currentIndex();
    QString selectedText = index.data(Qt::DisplayRole).toString();
    // find out the hierarchy level of the selected item
    int hierarchyLevel = 1;
    QModelIndex seekRoot = index;
    while (seekRoot.parent().isValid()) {
        seekRoot = seekRoot.parent();
        hierarchyLevel++;
    }
    QString showString = QString("%1, Level %2").arg(selectedText)
                         .arg(hierarchyLevel);
    setWindowTitle(showString);
}

treeView->selectionModel()->currentIndex()를 호출하여 선택 항목에 해당하는 모델 인덱스를 얻고, 이 모델 인덱스를 사용하여 필드의 문자열을 가져옵니다. 그런 다음 항목의 ` hierarchyLevel`을 계산하기만 하면 됩니다. 최상위 항목에는 부모가 없으며, ` parent()` 메서드는 기본 생성된 ` QModelIndex()`을 반환합니다. 이것이 바로 ` parent()` 메서드를 사용하여 반복 과정에서 수행된 단계 수를 세면서 최상위까지 반복하는 이유입니다.

선택 모델(위에서 보여준 바와 같이)은 가져올 수 있을 뿐만 아니라, ` QAbstractItemView::setSelectionModel`을 사용하여 설정할 수도 있습니다. 선택 모델의 인스턴스가 단 하나만 사용되기 때문에, 이렇게 하면 3개의 뷰 클래스에서 선택 내용을 동기화할 수 있습니다. 3개의 뷰 간에 선택 모델을 공유하려면 ` selectionModel()`를 사용하고, 그 결과를 ` setSelectionModel()`를 통해 두 번째 및 세 번째 뷰 클래스에 할당합니다.

3.3 사전 정의된 모델

모델/뷰를 사용하는 일반적인 방법은 특정 데이터를 래핑하여 뷰 클래스에서 사용할 수 있도록 하는 것입니다. 그러나 Qt는 일반적인 기본 데이터 구조에 대한 사전 정의된 모델도 제공합니다. 사용 가능한 데이터 구조 중 하나가 애플리케이션에 적합하다면, 사전 정의된 모델을 사용하는 것이 좋은 선택이 될 수 있습니다.

QRangeModel기존 C++ 컨테이너 또는 범위를 적용합니다
QStringListModel문자열 목록 저장
QStandardItemModel임의의 계층적 항목을 저장합니다
QFileSystemModel로컬 파일 시스템을 캡슐화
QSqlQueryModelSQL 결과 집합을 캡슐화
QSqlTableModelSQL 테이블을 캡슐화합니다
QSqlRelationalTableModel외래 키가 있는 SQL 테이블을 캡슐화합니다
QSortFilterProxyModel다른 모델을 정렬 및/또는 필터링합니다

3.4 델리게이트

지금까지 살펴본 모든 예제에서 데이터는 셀 내의 텍스트나 체크박스 형태로 표시되며, 텍스트나 체크박스 형태로 편집됩니다. 이러한 표시 및 편집 기능을 제공하는 구성 요소를 ‘델리게이트’라고 합니다. 뷰가 기본 델리게이트를 사용하기 때문에, 우리는 델리게이트를 다루기 시작한 지 얼마 되지 않았습니다. 하지만 다른 편집기(예: 슬라이더나 드롭다운 목록)를 사용하고 싶다고 가정해 봅시다. 또는 데이터를 그래픽으로 표시하고 싶다고 가정해 봅시다. 별을 사용하여 평점을 표시하는 ‘Star Delegate’라는 예제를 살펴보겠습니다:

곡과 별점 등급을 보여주는 표

이 뷰에는 기본 델리게이트를 대체하고 사용자 정의 델리게이트를 설치하는 ` setItemDelegate()` 메서드가 있습니다. 새로운 델리게이트는 ` QStyledItemDelegate`을 상속받는 클래스를 생성하여 작성할 수 있습니다. 별을 표시하고 입력 기능이 없는 델리게이트를 작성하려면 두 가지 메서드만 재정의하면 됩니다.

class StarDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    StarDelegate(QWidget *parent = nullptr);
    void paint(QPainter *painter, const QStyleOptionViewItem &option,
               const QModelIndex &index) const;
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const;
};

paint() 메서드는 기본 데이터의 내용에 따라 별을 그립니다. 데이터는 index.data()를 호출하여 조회할 수 있습니다. 델리게이트의 sizeHint() 메서드는 각 별의 크기를 파악하는 데 사용되며, 이를 통해 셀은 별을 수용할 수 있는 충분한 높이와 너비를 확보하게 됩니다.

뷰 클래스의 그리드 내에서 데이터를 사용자 정의 그래픽 형식으로 표시하려면 사용자 정의 델리게이트를 작성하는 것이 올바른 선택입니다. 그리드 밖으로 나가고 싶다면 사용자 정의 델리게이트 대신 사용자 정의 뷰 클래스를 사용해야 합니다.

Qt 문서에서 델리게이트에 대한 기타 참조:

3.5 ModelTest를 이용한 디버깅

모델의 수동적인 특성은 프로그래머에게 새로운 과제를 안겨줍니다. 모델의 불일치는 애플리케이션 충돌을 유발할 수 있습니다. 모델은 뷰로부터 수많은 호출을 받기 때문에, 어떤 호출이 애플리케이션을 충돌시켰는지, 어떤 작업이 문제를 일으켰는지 파악하기 어렵습니다.

Qt Labs에서는 프로그래밍 실행 중에 모델을 검사하는 ModelTest라는 소프트웨어를 제공합니다. 모델이 변경될 때마다 ModelTest는 모델을 스캔하고 어서트(assert)를 통해 오류를 보고합니다. 트리 모델의 경우 계층적 특성으로 인해 미묘한 불일치가 발생할 가능성이 많기 때문에, 이 기능은 특히 중요합니다.

뷰 클래스와 달리, ModelTest는 범위를 벗어난 인덱스를 사용하여 모델을 테스트합니다. 즉, ModelTest를 사용하지 않을 때는 완벽하게 실행되더라도 ModelTest를 사용하면 애플리케이션이 충돌할 수 있습니다. 따라서 ModelTest를 사용할 때는 범위를 벗어난 모든 인덱스를 적절히 처리해야 합니다.

예제

Qt에는 모델/뷰에 대한 여러 예제가 포함되어 있습니다. Item Views Examples 페이지에서 확인할 수 있습니다.

© 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.