2025년 12월 19일

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 : 키-값 쌍을 저장하는 해시 테이블이다.
Roles.TimestampRole → 256 Roles.MessageRole → 257 를 의미하므로, 역할 번호를 문자열 이름에 매핑하는 과정이다.enum Roles { TimestampRole = Qt::UserRole + 1, MessageRole, };
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를 만드는 과정이 꽤나 복잡하고 어려움을 느낄 수 있었다. 다만 기본적으로 솔루션을 제공하는 것이니 익숙해진다면 어렵지 않게 만들 수 있을 것이다.