QT - QML ListView (with. C++ Items)

w99hyun·2025년 12월 19일

QT Framework

목록 보기
4/4

QT QML의 ListView에서는 QML 내에서 자체적으로 생성한 모델을 출력할 수도 있지만, C++에서 생성한 List Model을 불러와 동적으로 생성하는 방법도 존재한다. 프로그램을 제작하다보면 보통 전자보다는 후자를 많이 사용하게 될 것이다.

Create List Item

C++에서 데이터 모델을 생성하고 QML에 표시하는 방법을 알아보겠다.
우선, QML의 ListView에서 출력할 C++ 데이터 모델이 필요하다. 이 모델은 ListView에 연동할 모델이므로, QAbstractListModel을 상속받아야 한다.

여기서는 예를들어 QML에서 timestamp, message 두 가지를 표시해보려고 한다.

struct ItemEntry
{
    QString timestamp;
    QString message;
};


class EntryModel : public QAbstractListModel
{
private:
    QList<ItemEntry> m_entries;
    
public:
	enum Roles {
        TimestampRole = Qt::UserRole + 1,
        MessageRole,
    };


    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
    QHash<int, QByteArray> roleNames() const override;
}

QAbstractListModel을 상속받은 class를 생성하고, m_entries에 데이터들이 저장된다.
rowCount(), data()은 QAbstractListModel의 가상함수이므로 반드시 override해야하며, QML에서 사용하려면 enum과 함께 roleNames()도 override해주는 것이 좋다. 편의성의 이유이며, 자세한 내용은 아래에서 확인할 수 있다.

rowCount()

int EntryModel::rowCount(const QModelIndex &parent) const
{
	Q_UNUSED(parent);// 함수 인자를 쓰지 않을 때 경고 무시를 위한 구문
    return m_entries.count();
}

m_entries의 개수를 반환해서 ListView에서 구현해야 할 Item 개수를 인식시켜주도록 한다.

data()

QVariant EntryModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || index.row() >= m_entries.size())
        return QVariant();

    const ItemEntry& entry = m_entries.at(index.row());

    switch (role) {
      case TimestampRole:
          return entry.timestamp;
      case MessageRole:
          return entry.message;
      default:
          return QVariant();
    }
}

m_entries의 행에 맞는 데이터를 가져와서 role에 맞는 데이터를 반환한다.

roleNames()

QHash<int, QByteArray> EntryModel::roleNames() const
{
    QHash<int, QByteArray> roles;
    roles[TimestampRole] = "timestamp";
    roles[MessageRole] = "message";
    return roles;
}
  • QHash : 키-값 쌍을 저장하는 해시 테이블이다.
enum Roles {
    TimestampRole = Qt::UserRole + 1,
    MessageRole,
};

Roles.TimestampRole → 256
Roles.MessageRole → 257
를 의미하므로, 역할 번호를 문자열 이름에 매핑하는 과정이다.

QQmlEngine* engine = m_qmlView->engine();
    if (engine) {
        engine->rootContext()->setContextProperty("itemModel", m_logModel);
    }

QML에서 itemModel 키워드로 C++ 객체에 접근할 수 있도록 등록한다.

ListView {
	id: ListView
    anchors.fill: parent
    model: typeof itemModel
    clip: true
    spacing: 0
                
    delegate: Rectangle {
        width: parent.width
        height: 25

        Row {
            anchors.left: parent.left
            anchors.leftMargin: 12
            anchors.verticalCenter: parent.verticalCenter
            spacing: 8

            Text {
                text: "[" + (model.timestamp || "") + "]"
                color: "#D9D9D9"
                font.pixelSize: 11
            }

            Text {
                text: model.message || ""
                color: "#D9D9D9"
                font.pixelSize: 11
            }
        }
    }
}

QML에서 각 아이템에 대한 Rectangle이다. ListView 내부에서 rowCount()를 호출해서 표시할 항목 수를 확인한 뒤, rowCount()의 반환값만큼 delegate 항목이 생성된다.
m_entries 개수에 따라 delegate 항목의 개수가 결정되는 것이다.

QML 내부에서는 C++ 로직에서 등록을 해줬기 때문에, model.timestamp, model.message와 같이 해당 m_entries index에 맞는 값을 가져올 수 있다.

예를 들어 model.timestamp는 내부적으로 data(index, TimestampRole)를 호출한다. 그 후, roleNames()에서 매핑값을 확인해서 해당 role의 값을 반환한다.

⚠️ 만약, C++에서 roleNames()를 구현하지 않았다면 QML에서 model.timestamp로는 접근이 불가능하고 model[256]과 같은 방식으로만 접근이 가능하다. model을 QML에 등록할 때 QT가 내부적으로 roleNames()를 호출해서 매핑 테이블을 생성하는 것이다.


Update List Item

위에서는 C++ 모델을 QML에서 표시하는 방법을 확인했고, 만약 C++ 모델에서 새로운 아이템이 추가된다면 QML에서도 업데이트 될 수 있도록 업데이트 로직이 필요하다.

void EntryModel::addItem(const QString& message, const QString& level) {
    //1. ListView에 "데이터 추가 시작" 알림
    beginInsertRows(QModelIndex(), m_entries.size(), m_entries.size());
    
    //2. 실제 데이터 추가
    ItemEntry entry;
    entry.timestamp = getCurrentTimestamp();
    entry.message = message;
    m_entries.append(entry);
    
    //3. ListView에 "데이터 추가 완료" 알림
    endInsertRows();
    
    //4. countChanged 시그널 발생 (QML에서 count 속성 업데이트)
    emit countChanged();
}

외부에서 아이템을 추가할 때 위의 함수를 호출하는 것이다.
여기서 핵심은 beginInsertRows()endInsertRows()이다. 아래에서 동작 순서를 확인할 수 있다.

beginInsertRows() 호출
  ↓
ListView가 새 항목이 추가되려고 한다는 것을 감지
  ↓
endInsertRows() 호출
  ↓
ListView가 rowCount() 다시 호출 → 아이템 증가 확인
  ↓
새로운 delegate 자동 생성 (새로운 아이템 index)
  ↓
새 delegate에서 model.timestamp, model.message 접근
  ↓
화면에 새 아이템 항목 표시

beginInsertRows()

bool beginInsertRows(const QModelIndex &parent, int first, int last)

이 함수의 인자에 대한 설명이다.
(QModelIndex) parent

  • 부모 인덱스를 의미한다.
  • 계층 구조가 없으면 QModelIndex()를 사용한다.

(int) first

  • 삽입할 첫 번째 행의 index이다. 0부터 시작한다.

(int) last

  • 삽입할 마지막 행의 index이다.
  • first <= last여야 하고, 마지막 하나만 추가한다면 first == last이다.

예시의 코드에서는 리스트 끝에 하나를 추가하는 방식이므로 first == last이다.


지금까지 다뤘던 프레임워크에 비해 ListView를 만드는 과정이 꽤나 복잡하고 어려움을 느낄 수 있었다. 다만 기본적으로 솔루션을 제공하는 것이니 익숙해진다면 어렵지 않게 만들 수 있을 것이다.

0개의 댓글