Qt Model-View Tutorial:
QListView + QAbstractListModel 이해
Qt에서는 복잡한 데이터 구조를 UI에 효율적으로 표현하고 조작하기 위해 모델-뷰(Model-View) Architecture를 채택함.
이 문서에서는 그 개념을 정리하고, QAbstractListModel기반의 Custom Model과 QListView를 사용한 도서 관리 예제를 통해 실습해 봄.
1. Qt의 Model-View Architecture :
Qt의 Model-View Architecture 는 데이터(Model)와 사용자 인터페이스(View)를 명확히 분리하여 관리하는 구조임.
이 구조는 재사용성과 유지보수성을 크게 높여주며, 특히 동적으로 변경되는 데이터 UI에 적합합니다.
- Model:
- 데이터를 보관하고 외부에 제공하는 역할
QAbstractListModelQStandardItemModel
- 도서 관리 예제에서는 도서 데이터 목록(제목, 저자, 연도 등)에 해당.
- 데이터를 보관하고 외부에 제공하는 역할
- View:
- 데이터를 사용자에게 보여주는 역할
QListViewQTableView
- 모델의 데이터를 list 또는 table 등으로 보여주는 역할.
- 클릭, 선택 등의 이벤트를 감지.
- 데이터를 사용자에게 보여주는 역할
- Delegate (선택적):
- 데이터 표현과 편집 방식을 위임받아 사용자 정의할 수 있는 구성 요소:
QStyledItemDelegate
- 책 제목을 어떻게 보여줄지, 클릭시 어떤 동작을 할지 등등을 구현.
- 지정하지 않는 경우, View의 기본 delegate를 사용함:
- 텍스트 기반인 경우 보통
QLabel을 이용해 랜더링.
- 텍스트 기반인 경우 보통
- 데이터 표현과 편집 방식을 위임받아 사용자 정의할 수 있는 구성 요소:
View는 Model의 데이터를 읽어와 화면에 표시하며, 사용자의 입력은 시그널/슬롯을 통해 Model로 전달됨.
참고: 전통적인 MVC Architecture와 Qt Model-View Architecture의 비교 :
| 구성 요소 | 전통적인 MVC | Qt Model-View |
| Model | 데이터 및 비즈니스 로직 | 데이터 제공 |
| View | 사용자 UI | 사용자 UI |
| Controller | 입력을 받아 Model/Logic 제어 | View가 직접 시그널 처리 |
Qt의 Model-View Architecture는 Controller가 View와 통합된 구조임.
View가 시그널과 슬롯(custom method 도 slot으로 사용됨)을 통해 Model과 직접 통신함.
2. 도서 목록 예제를 통해 Model-View Architecture 이해하기 :
도서 목록(Book List) 예제에서의 Model-View Architecture는 다음과 같이 구현:
| 구성 요소 | 역할 | 관련 class |
| Model | 책 list 데이터를 보관, 관리하며 view에 제공 | BookModel |
| View | 모델의 데이터를 사용자에게 보여주고, event를 감지 | QListView |
| Delegate | 출판연도 편집을 어떻게 할지 결정 | YearSpinBoxDelegate |
즉, 사용자가 UI를 조작할 때:
- View는 입력을 감지하고
- Delegate는 그에 맞는 위젯(예:
QSpinBox)을 보여주며 - Model은 최종적으로 데이터를 갱신함.
해당 예제를 잘 이해하기 위해
우선, QAbstractListModel과 QListView 클래스들에 대한 설명으로 시작함.
3. QAbstractListModel 의 구성요소 및 동작 방식 :
QAbstractListModel은 리스트 형태의 데이터를 다루는 Qt가 제공하는 Model 클래스.
다음의 상속구조를 가짐
QObject
↑
QAbstractItemModel
↑
QAbstractListModel
list에 대응하는 사용자 정의 모델을 만들기 위해 상속해서 사용함.
다음과 같은 핵심 메서드와 시그널을 재정의하거나 호출합니다.
rowCount,data,flags메서드는 override 하여 사용 (flags는 optional)dataChanged,layoutChanged의 Signal을 통해 연결된 View에게 갱신을 요청.beginInsertRows,beginRemoveRows,beginResetModel등은 명시적으로 호출하여 갱신 요청.- Signal 을 사용(emit)하는 것이 보다 편함.
- 보다 성능을 극대화하려면 begin/end 메서드를 사용하는 것이 좋음.
이를 간단히 table로 정리하면 다음과 같음.
| Method/Signal | Desc. | override 여부 |
호출 시점 |
rowCount() |
항목 수 반환 | 필수 override |
View가 모델 크기 요청 시 |
data(index, role) |
항목 데이터 반환 | 필수 override |
View가 표시 요청 시 |
setData(index, value, role) |
항목 데이터 수정 처리 | override | View가 편집 결과를 모델에 반영할 때 |
flags(index) |
항목의 속성 반환 | override | 선택/편집 가능성 확인 시 |
beginInsertRows()endInsertRows() |
새 항목 삽입 알림 | 호출 | 항목 추가 전후 |
beginRemoveRows()endRemoveRows() |
항목 제거 알림 | 호출 | 항목 삭제 전후 |
beginResetModel()endResetModel() |
전체 교체 시 | 호출 | 모델 리셋 전후 |
dataChanged |
특정 항목 값 변경 알림 Signal | emit() | 항목 수정 후 |
layoutAboutToBeChangedlayoutChanged |
모델 구조 변경 시 사용되는 Signal | emit() | 정렬 전후 |
3-1. .rowCount() 메서드 :
Qt의 View 클래스(QListView 등) 에서 Model이 보유한 데이터의 개수를 알아내기 위해 자동으로 호출되는 메서드로 반드시 overriding 해야함.
def rowCount(
self,
parent:QModelIndex,
) -> int
- View에 Model을
view.setMode(model)로 연결할 때 및 - View를 다시 갱신( resize 이벤트 로 인해 화면에 다시 표시)할 때,
- 데이터의 숫자가 갱신된 경우 등에 View가 호출.
예제 코드에서의 구현은 다음과 같음
def rowCount(self, parent=QModelIndex()):
return len(self._books)
QListView와 연결되는 경우엔
- parent가 무조건
QModelIndex()로 지정되어 - 부모가 없다는 것을 나타내어 전체 row 수(root에서의 node수)를 구하지만,
QTreeView에 연결되는 경우엔
- parent 가 넘겨지고,
- 해당 항목 아래에 children의 수를 반환하도록 overriding.
이를 반영하여 QListView에서의 구현하며 다음과 같음:
def rowCount(self, parent=QModelIndex()):
if parent.isValid():
return 0 # 리스트 모델이므로 자식 없음
return len(self._books)
QTreeView에서의 구현은 다음과 같음:
def rowCount(self, parent):
if parent.isValid():
return <parent 노드의 자식 수>
else:
return <root 노드의 자식 수>
참고로, QModelIndex 클래스는 모델 내부의 특정 위치를 나타내는 객체의 타입으로,
.row()와.column()의 getter를 가지고,.isValid()를 통해 유효한 index인지 확인 가능함.
3-2. .data(index, role) 메서드:
Qt의 View 객체에서 데이터를 요청할 때, 적절한 결과를 반환해주는 메서드로 반드시 overriding 해야함.
def data(
self,
index: QModelIndex,
role: int,
) -> Any
- View 객체가 데이터를 표시하기 위해
- 자신의 Model 객체에게 데이터를 요청할 때 호출됨.
QAbstractItemModel을 상속한 모든 클래스에서 override해야하는 메서드임.
Model을 구현시 overriding 하여 구현 하며 핵심적인 동작을 결정함.
argument는 다음과 같음:
3-2-1. index 파라미터:
- View 객체가 요구하는 데이터의 index로
.row()와.column()메서드를 가진 객체임. list의 경우엔.row()만 처리하고.column()은 무시함.
3-2-2. role 파라미터:
- View 객체가 어떤 목적으로 데이터를 요청했는지를 나타냄.
- Qt에서 제공하는
role이 가질 수 있는 값은 다음과 같음 (enum 형으로 실제로 정수상수임)
Role (Qt.*) |
Desc. | return type | example |
Qt.DisplayRole |
화면에 표시할 기본 텍스트 | str, int, float |
"The Great Gatsby" |
Qt.EditRole |
편집 시 사용되는 원시 값 | str, int, float 등 |
1949 |
Qt.ToolTipRole |
마우스 오버 시 툴팁 텍스트 | str |
"출판년도: 1949" |
Qt.TextAlignmentRole |
셀 내 텍스트 정렬 정보 | Qt.AlignmentFlag |
`Qt.AlignRight |
Qt.FontRole |
글꼴 정보 | QFont |
QFont( |
Qt.ForegroundRole |
글자색 (텍스트 색상 강조) | QColor, QBrush |
QColor("red") |
Qt.BackgroundRole |
셀 배경색 지정 | QColor, QBrush |
QBrush(QColor("#eef")) |
Qt.DecorationRole |
아이콘, 이미지, 색상 등 셀 앞 장식 | QIcon, QPixmap, QColor, QBrush |
QIcon(":/icons/book.png") |
예제에서 사용되는 code는 다음과 같음:
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None # 유효하지 않은 요청은 무시
book = self._books[index.row()] # 해당 row의 도서 정보를 가져옴
if role == Qt.DisplayRole:
# QListView가 화면에 표시할 문자열 요청
return f"{book['title']} by {book['author']} ({book['year']})"
if role == Qt.EditRole:
# Delegate가 QSpinBox에 설정할 값 요청
return book["year"]
return None # 다른 role은 처리하지 않음python
Qt.DisplayRole은
사실Qt.ItemDataRole.DisplayRole의 alias임.
보다 짧은 표현(shortcut)이라 더 많이 애용됨.
3-3. .setData() 메서드:
View나 Delegate에서 데이터 변경을 요청할 때, 모델 내부 데이터를 갱신하는 메서드.
- View가 편집 결과를 모델에 반영하고자 할 때 자동으로 호출됨.
- 모델 클래스(
QAbstractItemModel,QAbstractListModel)를 상속할 때 편집이 가능 하도록 하려면 반드시 override 해야 함. - 읽기 기능만 필요한 경우 굳이 구현할 필요 없음: 기본으로는 False만 반환하도록 구현됨.
def setData(
self,
index: QModelIndex,
value: Any,
role: int = Qt.EditRole,
) -> bool:
보통 dataChanged signal 과 함께 사용되며 예제코드에서는 다음과 같이 작성됨
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid() or role != Qt.EditRole:
return False
# 내부 데이터 갱신
self._books[index.row()]["year"] = int(value)
# View에게 값이 바뀌었다고 알림
self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole])
return True
QSpinBox로 년도가 수정되도록 Delegate를 사용하고 있는데,- 해당 delegate에서 모델로 값을 전달할 경우 이를
setData()호출되어 - 년도가 수정됨.
3-4. .flags(index) 메서드:
Qt의 모델에서 item(항목)의 기본적인 동작 가능 여부를 결정하는 메서드.
def flags(
self,
index: QModelIndex,
) -> Qt.ItemFlags
View는 이 메서드가 반환하는 플래그(Flag) 정보를 바탕으로 해당 item 이
- 선택 가능한지,
- 편집 가능한지,
- 드래그 가능한지 등을 결정
반환하는 Qt.ItemFlags의 주요 상수는 다음과 같음:
플래그 상수 (Qt.ItemFlag) |
설명 |
Qt.ItemIsEnabled |
항목이 활성화됨 (비활성화 상태가 아님) |
Qt.ItemIsSelectable |
항목을 마우스 또는 키보드로 선택할 수 있음 |
Qt.ItemIsEditable |
항목이 편집 가능함 (예: 더블클릭 시 편집 위젯 표시됨) |
Qt.ItemIsDragEnabled |
항목을 드래그할 수 있음 |
Qt.ItemIsDropEnabled |
항목 위에 다른 항목을 드롭할 수 있음 |
Qt.ItemIsUserCheckable |
항목에 체크박스를 표시하고 체크할 수 있음 |
Qt.ItemIsTristate |
체크박스가 3가지 상태 (체크됨/안됨/부분 체크) 가능 |
Qt.ItemNeverHasChildren |
항목이 자식을 가질 수 없음 (트리뷰 전용) |
Qt.ItemIsSelectable |
항목이 선택될 수 있음 |
- bitwise or 연산자(
|) 로 조합하여 동시에 여러 속성을 부여할 수 있음
일반적인 flags 조합은 다음과 같음.
3-4-1. 기본 flags 조합
다음은 기본으로 구현된 flags 메서드임.
def flags(self, index):
if not index.isValid():
return Qt.NoItemFlasgs
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
- index의 항목은 보이고 선택은 가능하지만 편집은 안 됨.
3-4-2. 편집 가능 flags 조합
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
- View는 EditRole 에 따라 편집 위젯(예:
QSpinBox,QLineEdit) 을 띄움.
다음은 특정 행만 편집 가능하게 처리하는 경우임.
def flags(self, index):
if index.row() == 0:
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
3-4-4. 예제에서 사용된 코드.
def flags(self, index):
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
3-5. .beginInsertRows(parent, first, last) / .endInsertRows 메서드
항목이 추가될 때 View와 동기화 를 위해 사용하는 페어 메서드 (수동으로 명시적으로 알려주는 용도)
beginInsertRows: View에게 “지금부터 이 위치에 행이 삽입될 것이다” 라고 알림.endInsertRows: 실제 데이터가 추가된 후, View에게 “추가가 끝났고 이제 화면을 새로 그려도 됨"을 알림.
beginInsertRows(
parent: QModelIndex,
first: int,
last: int,
) -> None
endInsertRows() -> None
parent: 삽입될 항목의 부모 인덱스.list관련 모델에선QModelIndex()로 설정.first: 삽입이 시작될 index.last: 삽입이 끝날 index.
예제에서는 책을 추가할 때에 사용됨.
first와last가 같을 경우, 한 row만 insert.
def add_book(self, title):
pos = self.rowCount() # 새 책을 추가할 위치는 리스트의 마지막 인덱스
# View에 알림: "pos ~ pos 위치에 새 항목이 들어올 예정임"
self.beginInsertRows(QModelIndex(), pos, pos)
# 실제 데이터 추가
self._books.append({"title": title})
# View에 알림: "삽입 완료. 다시 갱신하면 됨"
self.endInsertRows()
3-6. .beginRemoveRows(parent, first, last) / .endRemoveRows 메서드
항목이 삭제될 때 View와 동기화를 위해 사용하는 페어 메서드 (수동으로 명시적으로 알려주는 용도)
beginRemoveRows: View에게 “지금부터 이 위치에 행이 삭제될 것임” 라고 알림endRemoveRows: 실제 데이터가 삭제된 후, View에게 “삭제가 끝났고 이제 화면을 새로 그려도 됨"을 알림
beginRemoveRows(
parent: QModelIndex,
first: int,
last: int,
) -> None
endRemoveRows() -> None
parent: 삭제될 항목의 부모 인덱스.list관련 모델에선QModelIndex()로 설정.first: 삭제가 시작될 index.last: 삭제가 끝날 index.
예제에서는 책을 추가할 때에 사용됨.
first와last가 같을 경우, 한 row만 insert.
def remove_book(self, row):
# 유효한 행 번호인지 확인
if 0 <= row < self.rowCount():
# View에 행 삭제가 시작된다고 알림
self.beginRemoveRows(QModelIndex(), row, row)
# 내부 데이터 리스트에서 해당 항목 삭제
del self._books[row]
# View에 행 삭제가 완료되었다고 알림
self.endRemoveRows()
3-7. .beginResetModel()/.endResetModel() 메서드
모델의 전체 데이터가 변경되거나 대규모 수정이 발생할 경우
View에 “모델 전체가 리셋된다”는 사실을 알리기 위한 페어 메서드
beginResetModel: View에게 “지금부터 모델이 완전히 바뀔 것”이라고 알림endResetModel: 데이터 교체가 완료되었고 View가 다시 표시를 갱신해도 된다고 알림
signature는 다음과 같음:
beginResetModel() -> None
endResetModel() -> None
예제에서 사용된 code는 다음과 같음.
def reset_books(self):
# View에 전체 데이터 리셋이 시작됨을 알림
self.beginResetModel()
# 내부 데이터를 초기화 또는 완전히 교체
self._books = []
# View에 리셋 완료됨을 알림 → 전체 다시 그려짐
self.endResetModel()
3-8. dataChanged(topLeft, bottomRight, roles=[]) (signal)
모델의 데이터 값 자체가 변경되었을 때,
View에게 해당 항목 범위를 다시 업데이트하라고 알리는 시그널.
모델의 shape가 바뀌는 추가나 삭제의 경우를 제외한
변경에 emit()시켜야 함.
dataChanged:- 모델 데이터가 바뀌었음을 View에게 알리고,
- View는 지정된 범위의 항목에 대해 다시
data()를 호출함
- 데이터의 item의 갯수가 변한 경우엔 사용하면 안 됨: 모델의 shape가 변경된 경우이므로.
signature는 다음과 같음:
dataChanged(
topLeft: QModelIndex,
bottomRight: QModelIndex,
roles: list[int] = []
) -> None
topLeft: 변경된 범위의 시작 idxbottomRight: 변경된 범위의 끝 idx.- 보통
topLeft와 같이 사용하여 하나만 지정.
- 보통
roles: 변경된 역할 목록[Qt.DisplayRole]등.- 복수의 role을 지정하는 방식은 bitwise or 연산으로 합친 하나의 flag 값이 아니라,
list로 여러 개의 role을 각각 명시 **
주의할 점.
topLeft == bottomRight이면 single item 또는 single row 만 변경된 경우- roles 인자를 생략하면 View는 모든 role(
DisplayRole,EditRole등등)에 대해 새로 요청함. - View는 이 범위에 해당하는 인덱스에 대해 모델의
data()를 다시 호출하여 UI를 갱신함.
다음은 예제 코드에서 사용한 부분임.
def update_book(self, row, title=None, author=None):
# 유효한 행 인덱스인지 확인
if 0 <= row < self.rowCount():
# title이 주어졌다면 해당 행의 제목을 수정
if title:
self._books[row]["title"] = title
# author가 주어졌다면 해당 행의 저자 정보를 수정
if author:
self._books[row]["author"] = author
# View에 알림: row 번째 항목의 데이터가 변경되었음을 시그널로 전달
# topLeft = bottomRight = 해당 인덱스 (단일 항목 변경)
index = self.index(row)
self.dataChanged.emit(index, index)
3-9. layoutAboutToBeChanged / layoutChanged (signal)
사실상 만능 signal로 모든 변경에 emit() 할 경우, view가 변경이 반영됨.
권장되는 경우는
- 모델의 데이터 수(row/column 수)는 그대로지만,
- 행의 순서가 바뀌거나 정렬되는 경우, View에게 레이아웃이 바뀔 것임을 알려주는 시그널(signal) 페어
즉, 추가나 삭제의 경우는 앞서의 begin/end 페어 메서드들을 사용하는 것이 성능에는 좋음.
하지만,
- 간단한 시제품 등에서는 이 signal 중 layoutChanged 만 활용해도 충분하고 편리함.
- 단, 복잡한 데이터나 어플리케이션에선 성능 저하를 보일 수 있음.
앞서의 pair methods가 반드시 짝을 이루어 호출되어야 하는 것과 달리 layoutChanged 시그널만 emit해도 충분함.
layoutAboutToBeChanged: View에게 “곧 레이아웃이 바뀔 것이다”라고 알림layoutChanged: View에게 “레이아웃 변경이 완료되었고, 다시 갱신해도 된다”라고 알림
두 시그널 모두 직접 emit() 호출 로
사용해야 함
self.layoutAboutToBeChanged.emit()
# 데이터 재정렬 등
self.layoutChanged.emit()
Qt문서의 권장에도
- item의 삭제나 추가에는 전용 함수(begin/end)를 통해 동기화하고,
- 이 signal은 재정렬에 쓰라고 하지만,
- 실제로 많은 시작품에서는 추가/삭제 의 경우에도 사용함.
layoutChanged()는 단독 사용 및 item 추가/삭제에서도 동작하는 이유는 다음과 같이 동작하기 때문임:
- View는
layoutChanged.emit()이 발생하면: - 내부 인덱스 매핑을 모두 초기화
- 모든 항목에 대해 새로 index() → data() 호출
즉, 변화의 내용( 삽입 / 삭제 등)은 View가 관심 없고,
그저 “전체가 바뀌었으니 다 무효화하고 다시 확인할게”라는 리셋 성격의 시그널임.
참고로 layoutAboutToBeChanged 는 View 또는 Delegate가 내부에 상태를 유지하거나
현재 선택 상태, 포커스, 스크롤 위치 등을 사전에 보존하고 싶을 때 사용됨.
다음은 예제에서 사용된 코드임.
def sort_books(self):
# View에 레이아웃이 곧 변경될 것임을 알림 (시그널 emit)
self.layoutAboutToBeChanged.emit()
# 내부 데이터 정렬 (row 순서 변경)
self._books.sort(key=lambda book: book["title"])
# View에 레이아웃 변경이 완료되었음을 알림 (시그널 emit)
self.layoutChanged.emit()
4. QListView의 역할과 활용
QListView는 :
QAbstractItemView를 기반으로 한 클래스이며,- 모델(Model)로부터 데이터를 받아 list 형태로 화면에 출력.
상속구조는 다음과 같음
QObject
↑
QWidget
↑
QFrame
↑
QAbstractScrollArea
↑
QAbstractItemView
↑
QListView
QListView는 override하지 않고
사용자 코드에서 인스턴스 생성 후 메서드를 호출
하여 사용하는 것이 일반적.
4-1. 주요 메서드 및 시그널
| Method/Signal | Desc. | 호출 방식 |
setModel(model) |
뷰에 표시할 모델 객체를 설정 | 호출 |
setItemDelegate(delegate) |
항목을 표현하거나 편집할 때 사용할 delegate 설정 | 호출 |
setEditTriggers(flags) |
어떤 사용자 동작이 편집을 트리거할지 설정 (DoubleClicked 등) |
호출 |
currentIndex() |
현재 선택된 항목의 QModelIndex 반환 |
호출 |
edit(index) |
지정된 인덱스를 편집 상태로 전환 | 호출 |
clicked |
항목이 클릭되었을 때 발생하는 시그널 (QModelIndex 전달됨) |
시그널 |
doubleClicked |
항목이 더블클릭되었을 때 발생하는 시그널 | 시그널 |
setSelectionMode(mode) |
항목 선택 방식을 설정 (SingleSelection, MultiSelection 등 가능) |
호출 |
setModel()로QAbstractListModel객체와 연결setItemDelegate()로QStyledItemDelegate객체와 연결setEditTriggers()를 통해 어떤 동작이 편집을 유발할지 설정 가능
4-2. code snippet
# View 생성 및 설정
self.view = QListView()
self.view.setModel(self.model) # 모델 연결
self.view.setItemDelegate(YearSpinBoxDelegate()) # delegate 설정
self.view.setEditTriggers(QAbstractItemView.DoubleClicked) # 더블클릭 시 편집 허용
위의 코드의 Double Click(더블클릭)을 통한 setEditTriggers 방식은 자동 트리거이며,
다음과 같이 signals and slots을 통해 직접 수동 호출방식으로 구현할 수도 있음
self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.view.doubleClicked.connect(self.view.edit) # 더블클릭 시 명시적 편집 시작
QListView에서 더블클릭을 통한 모델 업데이트의 내부 호출 흐름은 다음과 같음
[사용자 더블클릭]
↓
[편집 시작]
├─ (자동 트리거: setEditTriggers)
└─ (수동 호출: doubleClicked → edit(index))
↓
Delegate.createEditor() → 편집기 위젯 생성 (QSpinBox 등)
↓
Delegate.setEditorData() → 기존 데이터를 위젯에 설정
↓
[사용자 값 입력 후 포커스 아웃 등으로 편집 완료]
↓
Delegate.setModelData() → 위젯에서 값 가져옴
↓
model.setData() → 모델 데이터 갱신
↓
model.dataChanged.emit() → View에 변경 알림
↓
View가 해당 인덱스에 대해 다시 data() 호출 → 화면 갱신
QListView,QTableView,QTreeView에 대해- 기본 Delegate 객체로
QStyledItemDelegate인스턴스를 자동으로 설정. - 이 경우
QLineEdit가 문자열 입력으로 사용됨
5. Delegate의 역할과 활용
Delegate는 Qt 모델-뷰 구조에서 항목의 표현 및 편집 방식을 사용자 정의할 수 있도록 도와주는 구성 요소임.
- View는 Delegate를 통해 항목을 어떻게 표시할지, 편집할지를 위임(delegate).
- View와 Model 사이에서 사용자와의 데이터 편집을 연결함.
View객체의
setItemDelegate(custom_delegate)메서드를 통해
특정 view와 연결됨.
Qt에서는 QStyledItemDelegate가 기본 제공되며, 사용자 정의 위젯을 통한 입력이나 렌더링이 필요할 경우 이 QStyledItemDelegate를 상속하여 확장함.
- 사용자 정의 Delegate를 통해 보다 직관적이고 제약 있는 사용자 입력 인터페이스를 구성할 수 있음.
- 숫자, 날짜, 색상 등 비문자 기반 입력 처리에서 Custom Delegate는 중요한 역할을 함.
Delegate 없어도 되는가?
- Delegate를 따로 설정하지 않으면 Qt는 기본적으로
QStyledItemDelegate를 사용 - 이 경우, 편집기는
QLineEdit(문자 입력기) 로 자동 지정됨 - 복잡한 입력 제약 조건이 필요하거나 UI 컨트롤을 커스터마이즈하려면 명시적 Delegate 설정이 권장됨.
5-1. 주요 메서드 동작 흐름 (Delegate)
다음의 순서는 사용자가 항목을 편집할 때 Qt가 Delegate를 통해 호출하는 표준 동작 흐름임
| method | Desc. | 호출 시점 |
createEditor(parent, option, index) |
편집기 위젯을 생성 ( QSpinBox, QLineEdit, QComboBox 등) |
사용자가 항목을 편집 시작할 때 |
setEditorData(editor, index) |
모델 데이터를 읽어 편집기 초기화 | 편집기 생성 직후 |
setModelData(editor, model, index) |
편집기 데이터를 모델에 저장 (→ model.setData() 호출) |
사용자가 편집을 마치고 저장할 때 |
updateEditorGeometry(editor, option, index) |
편집기 위젯의 위치와 크기 설정 | View가 편집기 배치할 때 |
5-1-1. .createEditor(self, parent, option, index) 메서드
항목을 편집할 때 사용할 실제 편집기 위젯(예: QSpinBox, QLineEdit 등) 을 생성하는 메서드.
| Arguemnt | Type | Desc |
parent |
QWidget |
생성할 편집기의 부모 위젯. 보통 View (QListView, QTableView 등) |
option |
QStyleOptionViewItem |
항목의 시각적 상태 및 스타일 정보 (선택 여부, 포커스 등 포함) |
index |
QModelIndex |
편집하려는 항목의 인덱스. 어떤 항목인지 구분하는 데 사용됨 |
5-1-2. .setEditorData(self, editor, index) 메서드
모델의 데이터를 읽어와 편집기에 초기값으로 설정하는 메서드.
| Arguemnt | Type | Desc |
editor |
QWidget |
createEditor()에서 생성된 편집기 위젯 |
index |
QModelIndex |
모델에서 데이터를 가져올 인덱스 (EditRole 역할로 접근) |
5-1-3. .setModelData(self, editor, model, index) 메서드
편집기에서 입력된 데이터를 모델로 저장하는 메서드.
| Arguemnt | Type | Desc |
editor |
QWidget |
사용자 입력이 들어있는 편집기 위젯 |
model |
QAbstractItemModel |
데이터를 저장할 모델 객체 |
index |
QModelIndex |
데이터를 저장할 대상 인덱스 위치 |
5-1-4. .updateEditorGeometry(self, editor, option, index) 메서드
편집기 위젯의 위치와 크기를 설정하는 메서드.
| Arguemnt | Type | Desc |
editor |
QWidget |
위치와 크기를 지정할 편집기 위젯 |
option |
QStyleOptionViewItem |
셀의 사각형 영역 정보 포함 (option.rect) |
index |
QModelIndex |
편집 대상의 모델 인덱스 (위치 조정 시 필요한 경우 사용) |
5-2. QStyleOptionViewItem
QStyleOptionViewItem은
- PySide6에서 View (
QListView,QTableView) 항목(item)의 시각적 상태와 레이아웃, 스타일 정보를 담는 객체 임. - delegate에서 넘겨져서, View의 정보에 접근하는데 사용됨.
delegate.paint()또는delegate.updateEditorGeometry()같은 메서드에서 사용되며,- View가 그리거나 편집기를 배치할 때 시각적 힌트를 제공
5-2-1. 주요 Attribute 목록
| Attribute Name | Type | Desc. |
rect |
QRect |
항목의 표시 영역 (위치와 크기) |
state |
QStyle.State (아래 참고) |
항목의 상태 플래그 (선택, 포커스 등) |
font |
QFont |
항목 텍스트에 사용할 폰트 |
palette |
QPalette |
항목의 전경/배경 색상 정보 |
displayAlignment |
Qt.Alignment |
텍스트 정렬 방식 (ex: Qt.AlignCenter) |
features |
QStyleOptionViewItem.ViewItemFeatures |
항목의 기능 플래그 (체크박스 표시 등) |
viewItemPosition |
QStyleOptionViewItem.Position |
항목이 리스트 내 어디에 위치하는지 ( Beginning, Middle, End) |
decorationPosition |
QStyleOptionViewItem.Position |
아이콘(Decoration)의 위치 |
decorationAlignment |
Qt.Alignment |
아이콘 정렬 방식 |
checkState |
Qt.CheckState |
체크박스 상태 (예: Qt.Checked) |
index속성은 Qt 6에서만 제공되며,
PySide6 (Qt 6.x 이상)에서만 사용 가능.
5-2-2. 예제 코드
def updateEditorGeometry(self, editor, option, index):
# 항목의 rect 정보를 기반으로 편집기의 위치와 크기 설정
editor.setGeometry(option.rect)
def paint(self, painter, option, index):
# 항목이 선택되었을 경우 배경색 처리
if option.state & QStyle.State_Selected: # 선택되었는지 flag bit로 확인.
painter.fillRect(option.rect, option.palette.highlight())
# 텍스트 정렬 정보 사용
alignment = option.displayAlignment or Qt.AlignLeft #왼쪽 정렬 추가
text = index.data(Qt.DisplayRole)
painter.drawText(option.rect, alignment, text)
5-2-3. state 값들.
QStyle.State는 View 항목의 시각적 상태를 나타내는 플래그(enum) 로,QStyleOptionViewItem.state에서 bitwise OR 로 결합된 형태로 전달됨.
| 플래그 이름 | Desc. |
QStyle.State_None |
아무 상태도 없음 (기본값) |
QStyle.State_Enabled |
항목이 활성화되어 있음 (비활성 상태가 아님) |
QStyle.State_Raised |
마우스가 올라가 있는 상태 (플랫 버튼 등에서 사용) |
QStyle.State_Sunken |
눌린 상태로 표현됨 |
QStyle.State_Off |
체크박스, 라디오 버튼이 해제된 상태 |
QStyle.State_NoChange |
체크 상태 없음 (혼합 상태 등) |
QStyle.State_On |
체크된 상태 |
QStyle.State_DownArrow |
아래 방향 화살표 표시용 (콤보박스 등) |
QStyle.State_UpArrow |
위 방향 화살표 표시용 |
QStyle.State_Horizontal |
수평 방향 상태 표시 (슬라이더 등) |
QStyle.State_HasFocus |
포커스를 가지고 있음 |
QStyle.State_Top |
View 항목 중 상단 항목 (스타일 계산용) |
QStyle.State_Bottom |
View 항목 중 하단 항목 |
QStyle.State_Selected |
항목이 선택된 상태 |
QStyle.State_Active |
현재 활성화된 위젯 내 항목 |
QStyle.State_Window |
현재 표시되는 윈도우의 항목 |
QStyle.State_Open |
트리뷰 항목이 확장(open)된 상태 |
QStyle.State_MouseOver |
마우스가 항목 위에 올라간 상태 |
여러 상태는 bitwise OR 연산으로 결합됨.
예:QStyle.State_Enabled | QStyle.State_Selected | QStyle.State_HasFocus
다음은 선택 항목 강조 예제 코드임.
from PySide6.QtWidgets import QStyledItemDelegate
from PySide6.QtGui import QPainter
from PySide6.QtCore import Qt
class HighlightSelectedDelegate(QStyledItemDelegate):
def paint(self, painter, option, index):
# 선택된 항목이면 파란색 배경을 그림
if option.state & QStyle.State_Selected: # 해당 항목이 선택되어 있음을 확인
painter.fillRect(option.rect, option.palette.highlight())
# 텍스트 정렬 정보 확인하여
# option.displayAlignment에 정렬 정보가 이미 있으면 그것을 사용하고,
# 없다면 기본적으로 왼쪽 정렬(Qt.AlignLeft)을 사용.
alignment = option.displayAlignment or Qt.AlignLeft
text = index.data(Qt.DisplayRole)
# 텍스트 출력
painter.drawText(option.rect, alignment, text)
5-3. 예제 코드에서의 구현:
class YearSpinBoxDelegate(QStyledItemDelegate):
# 사용자가 항목을 편집할 때 호출되어, 실제로 사용할 편집기 위젯을 생성하는 메서드
def createEditor(self, parent, option, index):
# QSpinBox를 생성하여 연도 입력 전용으로 사용
editor = QSpinBox(parent)
# 최소 연도 설정 (예: 1000년부터)
editor.setMinimum(1000)
# 최대 연도 설정 (예: 9999년까지)
editor.setMaximum(9999)
return editor # 생성한 위젯을 View에 반환
# 모델로부터 값을 읽어와 편집기에 반영하는 메서드 (편집 시작 시 자동 호출됨)
def setEditorData(self, editor, index):
# 모델에서 현재 편집 대상 셀의 EditRole 데이터를 가져옴
year = index.model().data(index, Qt.EditRole)
# 가져온 값을 QSpinBox 위젯에 설정
editor.setValue(int(year))
# 편집기에서 수정된 데이터를 모델에 저장하는 메서드 (편집 종료 시 호출됨)
def setModelData(self, editor, model, index):
# QSpinBox에서 입력된 값을 읽어와서
value = editor.value()
# 모델에 EditRole로 설정 → 내부적으로 model.setData() 호출됨
model.setData(index, value, Qt.EditRole)
# 편집기가 화면에 정확히 위치하도록 위치/크기를 설정하는 메서드
def updateEditorGeometry(self, editor, option, index):
# View가 제공한 사각형(rect)에 맞춰 편집기의 위치와 크기를 설정
editor.setGeometry(option.rect)
QSpinBox를 사용하여 출판연도를int(정수)로만 입력하도록 강제setEditorData()와setModelData()를 통해 모델과 편집기 간의 데이터 동기화 수행
6. 예제 코드: 도서 목록 앱

# Qt의 핵심(Core) 기능과 모델/뷰 관련 클래스 임포트
from PySide6.QtCore import Qt, QAbstractListModel, QModelIndex
# 위젯 관련 클래스 임포트
from PySide6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QHBoxLayout,
QListView, QPushButton, QLineEdit, QLabel, QStyledItemDelegate,
QSpinBox, QAbstractItemView
)
import sys
# 모델 클래스 정의: 도서 리스트를 관리
class BookModel(QAbstractListModel):
def __init__(self, books=None):
super().__init__()
self._books = books or [] # 도서 정보 리스트
def rowCount(self, parent=QModelIndex()):
return len(self._books) # 전체 도서 수 반환
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
book = self._books[index.row()] # 해당 행의 도서 정보
if role == Qt.DisplayRole:
# 리스트 뷰에 표시될 문자열 구성
return f"{book['title']} by {book['author']} ({book['year']})"
if role == Qt.EditRole:
# 편집 모드에서 사용할 출판연도 값
return book["year"]
return None
def flags(self, index):
# 항목이 선택 가능하고, 활성화되며, 편집 가능하도록 설정
return Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
# 사용자가 수정한 연도 값을 모델에 반영
self._books[index.row()]["year"] = int(value)
self.dataChanged.emit(index, index) # 변경 알림
return True
return False
def add_book(self, title, author, year):
# 새로운 도서 추가
pos = self.rowCount()
self.beginInsertRows(QModelIndex(), pos, pos)
self._books.append({"title": title, "author": author, "year": year})
self.endInsertRows()
def remove_book(self, row):
# 선택된 도서 삭제
if 0 <= row < self.rowCount():
self.beginRemoveRows(QModelIndex(), row, row)
del self._books[row]
self.endRemoveRows()
def update_book(self, row, title=None, author=None):
# 제목이나 저자 정보 수정
if 0 <= row < self.rowCount():
if title:
self._books[row]["title"] = title
if author:
self._books[row]["author"] = author
index = self.index(row)
self.dataChanged.emit(index, index) # UI에 반영되도록 알림
def reset_books(self):
# 도서 리스트 초기화
self.beginResetModel()
self._books = []
self.endResetModel()
# 출판연도 항목에 대한 Delegate 정의 (편집용 위젯으로 QSpinBox 사용)
class YearSpinBoxDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QSpinBox(parent)
editor.setMinimum(1000)
editor.setMaximum(9999)
return editor
def setEditorData(self, editor, index):
year = index.model().data(index, Qt.EditRole)
editor.setValue(int(year)) # 모델의 값을 편집 위젯에 설정
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), Qt.EditRole) # 위젯 값을 모델에 반영
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect) # 편집 위젯 위치 지정
# 메인 어플리케이션 클래스 정의
class BookApp(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Qt Model-View + Delegate + CRUD + Reset")
self.resize(600, 500)
# 초기 도서 목록 세팅
self.model = BookModel([
{"title": "1984", "author": "George Orwell", "year": 1949},
{"title": "Dune", "author": "Frank Herbert", "year": 1965}
])
# QListView 생성 및 설정
self.view = QListView()
self.view.setModel(self.model) # 모델 연결
self.view.setItemDelegate(YearSpinBoxDelegate()) # 출판연도 편집을 위한 Delegate 설정
self.view.setEditTriggers(QAbstractItemView.DoubleClicked) # 더블클릭으로 편집 모드 진입
# 입력 필드: 제목, 저자, 출판연도
self.title_input = QLineEdit()
self.author_input = QLineEdit()
self.year_input = QLineEdit()
for w, p in zip(
[self.title_input, self.author_input, self.year_input],
["제목", "저자", "출판연도"]
):
w.setPlaceholderText(p)
# 버튼들 생성
self.add_btn = QPushButton("도서 추가")
self.update_btn = QPushButton("선택 항목 수정")
self.delete_btn = QPushButton("선택 항목 삭제")
self.reset_btn = QPushButton("초기화")
# 입력 필드 수평 레이아웃
input_layout = QHBoxLayout()
input_layout.addWidget(self.title_input)
input_layout.addWidget(self.author_input)
input_layout.addWidget(self.year_input)
# 버튼 수평 레이아웃
btn_layout = QHBoxLayout()
btn_layout.addWidget(self.add_btn)
btn_layout.addWidget(self.update_btn)
btn_layout.addWidget(self.delete_btn)
btn_layout.addWidget(self.reset_btn)
# 전체 수직 레이아웃
layout = QVBoxLayout(self)
layout.addWidget(QLabel("📚 도서 목록 - 출판연도는 더블클릭으로 편집"))
layout.addWidget(self.view)
layout.addLayout(input_layout)
layout.addLayout(btn_layout)
# 버튼에 기능 연결 (시그널-슬롯 연결)
self.add_btn.clicked.connect(self.add_book)
self.update_btn.clicked.connect(self.update_book)
self.delete_btn.clicked.connect(self.delete_book)
self.reset_btn.clicked.connect(self.reset_books)
# 입력 필드 값 가져오기
def get_inputs(self):
return self.title_input.text(), self.author_input.text(), self.year_input.text()
# 입력 필드 초기화
def clear_inputs(self):
self.title_input.clear()
self.author_input.clear()
self.year_input.clear()
# 도서 추가 버튼 동작
def add_book(self):
title, author, year = self.get_inputs()
if title and author and year.isdigit():
self.model.add_book(title, author, int(year))
self.clear_inputs()
# 선택 항목 수정 버튼 동작
def update_book(self):
index = self.view.currentIndex()
if index.isValid():
title, author, _ = self.get_inputs()
self.model.update_book(index.row(), title, author)
self.clear_inputs()
# 선택 항목 삭제 버튼 동작
def delete_book(self):
index = self.view.currentIndex()
if index.isValid():
self.model.remove_book(index.row())
# 전체 초기화 버튼 동작
def reset_books(self):
self.model.reset_books()
self.clear_inputs()
# 프로그램 실행
if __name__ == "__main__":
app = QApplication(sys.argv)
window = BookApp()
window.show()
sys.exit(app.exec())
- 파일에 저장하고 시작시 로드하는 기능 추가 필요.
'Python > PySide PyQt' 카테고리의 다른 글
| [PySide] QItemSelectionModel 살펴보기 - 작성중 (0) | 2025.06.04 |
|---|---|
| [PySide6] QTreeView 와 QStandardItemModel, QStandardItem (1) | 2025.06.02 |
| [PySide6] QWidget.setFocusPolicy(policy: Qt.FocusPolicy) (0) | 2025.05.13 |
| [PySide6] Installing PySide6 (and Designer) on Windows (with Conda) (0) | 2025.02.11 |
| [PySide] Ex: Img Viewer. QListWidget and Matplotlib (0) | 2024.06.04 |