3강. Model/View 실습

1. QTableView 추가하기
→ 같은 Model을 세 개의 View가 함께 사용하는 구조로 확장합니다.
1.1 실습 목표
지금까지는
하나의 QFileSystemModel을
두 개의 View(QTreeView와 QListView)가 함께 사용했습니다.
이번에는 여기에 QTableView를 하나 더 추가합니다.
목표는 하나의 Model을 세 개의 View가 함께 사용하는 구조를 만드는 것입니다.
QFileSystemModel
├── QTreeView
├── QListView
└── QTableView
QTableView는 파일과 폴더 정보를 표 형태로 보여줄 수 있습니다.
파일 이름, 크기, 종류, 수정 날짜 같은 정보를 열 단위로 확인할 때 적합합니다.
실습 목표
QTableView를 추가해도 새로운 Model을 만들 필요가 없다는 점을 확인합니다.
같은 QFileSystemModel을 QTreeView, QListView, QTableView가 함께 사용합니다.
1.2 import에 QTableView 추가하기
먼저 widget.py 상단 import에 QTableView를 추가합니다.
QTableView는 PySide6.QtWidgets에서 가져올 수 있습니다.
from PySide6.QtCore import QDir, Qt
from PySide6.QtWidgets import (
QWidget,
QSplitter,
QFileSystemModel,
QTreeView,
QListView,
QTableView, # 추가
QVBoxLayout,
)
이제 widget.py 안에서 QTableView 클래스를 사용할 수 있습니다.
1.3 QTableView 생성하고 같은 Model 연결하기
기존 QTreeView와 QListView과 같은 방식으로 QTableView도 추가합니다.
# --- (3) QTableView 설정 ---
self.table_view = QTableView(splitter)
self.table_view.setModel(self.model)
self.table_view.setRootIndex(self.model.index(root_path))
구조는 앞에서 작성한 QTreeView, QListView와 같습니다.
중요한 점은 setModel(self.model)을 사용해 새로운 Model을 만들 필요가 없다는 점입니다.
| 코드 | 설명 |
| QTableView(splitter) | 스플리터 안에 표 형태의 View를 만듭니다. |
| setModel(self.model) | QTreeView, QListView와 같은 QFileSystemModel을 연결합니다. |
| setRootIndex(...) | 표에서 어느 폴더 내용을 보여줄지 정합니다. |
QTableView
↓
setModel(self.model)
↓
같은 QFileSystemModel을 표 형태로 표시
중요한 점
QTableView를 추가해도 Model은 새로 만들지 않습니다.
기존 self.model을 그대로 연결합니다.
1.4 선택 변경 함수 수정하기
이전에는 트리에서 선택이 바뀌면 QListView의 Root Index만 바꿨습니다.
이제 QTableView도 함께 바뀌도록 수정합니다.
def on_tree_current_changed(self, current, previous):
"""
트리에서 선택이 바뀔 때마다 호출됩니다.
current : 현재 선택된 QModelIndex
previous : 이전에 선택되어 있던 QModelIndex
"""
if self.model.isDir(current):
self.list_view.setRootIndex(current)
self.table_view.setRootIndex(current) # ← 추가
else:
parent_index = current.parent()
self.list_view.setRootIndex(parent_index)
self.table_view.setRootIndex(parent_index) # ← 추가
폴더를 선택하면 QListView와 QTableView가 모두 선택한 폴더 내용을 보여줍니다.
파일을 선택하면 해당 파일이 들어 있는 부모 폴더 내용을 보여줍니다.
QTreeView에서 폴더 클릭
↓
QListView의 Root Index 변경
QTableView의 Root Index 변경
QTreeView에서 파일 클릭
↓
부모 폴더 인덱스 가져오기
↓
QListView와 QTableView가 부모 폴더 내용 표시
1.5 전체 구조 정리
QTableView를 추가한 후 전체 구조는 다음과 같습니다.
실제 파일/폴더 데이터
↓
QFileSystemModel
├──> QTreeView : 트리 형태로 출력
├──> QListView : 리스트 형태로 출력
└──> QTableView : 표 형태로 출력
데이터는 하나입니다.
하지만 표현 방식은 세 개가 되었습니다.
| View | 표현 방식 |
| QTreeView | 파일과 폴더를 트리 구조로 보여줍니다. |
| QListView | 선택한 폴더 안의 항목을 리스트로 보여줍니다. |
| QTableView | 선택한 폴더 안의 항목을 표 형태로 보여줍니다. |
기억할 문장
View가 늘어나도 같은 Model을 연결하면 됩니다.
2. Model 외부에서 주입하기
→ Model을 Widget 내부에서 만들지 않고, 외부에서 만들어 전달하는 구조를 실습합니다.
2.1 실습 목표
QTableView를 추가해서 하나의 Model을 세 개의 View가 함께 사용하도록 만들었습니다.
하지만 QFileSystemModel은 여전히 Widget 안에서 만들고 있습니다.
self.model = QFileSystemModel(self)
root_path = QDir.currentPath()
self.model.setRootPath(root_path)
이 구조도 틀린 것은 아닙니다.
하지만 프로그램이 커지면 Model을 Widget 밖에서 만들고, Widget에는 주입하는 구조가 더 유용할 수 있습니다.
main.py
↓
QFileSystemModel 생성
↓
Widget(model, root_path)로 전달
↓
Widget은 전달받은 Model을 View에 연결
실습 목표
Model을 Widget 내부에서 직접 만들지 않고, 외부에서 만든 Model을 Widget에 전달하는 구조를 이해합니다.
2.2 구조 비교
첫번째 실습 구조에서는 Model 생성, View 생성, 시그널 연결이 모두 Widget 안에 들어 있습니다.
Widget
├── QFileSystemModel 생성
├── QTreeView 생성
├── QListView 생성
├── QTableView 생성 # 첫번째 실습에서 추가
├── 시그널 연결
└── 화면 배치
두번째 실습 단계에서는
Model 생성을 main.py로 옮기고,
Widget 에 주입하는 구조를 구현합니다.
main.py
└── QFileSystemModel 생성 # 추가
Widget
├── 전달받은 Model 사용 # 수정
├── QTreeView 생성
├── QListView 생성
├── QTableView 생성
├── 시그널 연결
└── 화면 배치
QTreeView, QListView, QTableView 생성 코드는 아직 Widget 안에 그대로 둡니다.
| 역할 | 이번 단계에서의 위치 |
| Model 생성 | main.py로 이동합니다. |
| View 생성 | 아직 Widget 안에 둡니다. |
| 시그널 연결 | 아직 Widget 안에 둡니다. |
중요한 점
이번 단계에서는 한 번에 모든 구조를 바꾸지 않습니다.
먼저 Model 생성 위치만 Widget 밖으로 분리합니다.
2.3 main.py에서 Model 만들기
이번에는 main.py에서 QFileSystemModel을 먼저 만듭니다.
그리고 만든 Model과 root_path를 Widget에 전달합니다.
# main.py
import sys
from PySide6.QtCore import QDir
from PySide6.QtWidgets import QApplication, QFileSystemModel
from widget import Widget
def main():
app = QApplication(sys.argv)
# [1] Model 생성
# app을 부모로 지정하면 프로그램이 실행되는 동안 Model이 안전하게 유지됩니다.
model = QFileSystemModel(app)
# [2] Model의 기준 경로 설정
root_path = QDir.currentPath()
model.setRootPath(root_path)
# [3] Model과 root_path를 Widget에 전달
window = Widget(model, root_path)
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
이제 QFileSystemModel은 Widget 안이 아니라 main.py에서 만들어집니다.
Widget은 이미 만들어진 Model을 전달받아 View에 연결하기만 합니다.
| 코드 | 의미 |
| model = QFileSystemModel(app) | 파일/폴더 정보를 제공할 Model을 main.py에서 만듭니다. app을 부모로 지정해 프로그램 실행 동안 유지되도록 합니다. |
| model.setRootPath(root_path) | Model이 읽을 기준 경로를 설정합니다. |
| Widget(model, root_path) | 생성한 Model과 기준 경로를 Widget에 주입합니다. |
참고
이 예제에서는 Model을 Widget 밖에서 만듭니다.
그래도 app을 부모로 지정했기 때문에 프로그램이 실행되는 동안 Model 객체가 안정적으로 유지됩니다.
2.4 Widget이 Model을 전달받도록 수정하기
이제 widget.py를 수정합니다.
Widget이 직접 Model을 만들지 않고, __init__()의 매개변수로 전달받도록 바꿉니다.
# widget.py
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QWidget,
QSplitter,
QTreeView,
QListView,
QTableView,
QVBoxLayout,
)
class Widget(QWidget):
"""
외부에서 전달받은 Model을 사용해
QTreeView, QListView, QTableView를 구성하는 위젯입니다.
"""
def __init__(self, model, root_path, parent=None):
super().__init__(parent)
# [1] 기본 창 설정
self.setWindowTitle("Model/View 데모 - Model 외부 주입")
self.resize(900, 400)
# [2] 외부에서 전달받은 Model과 경로 저장
self.model = model
self.root_path = root_path
# [3] 스플리터 생성
splitter = QSplitter(Qt.Horizontal, self)
# [4] TreeView 설정
self.tree_view = QTreeView(splitter)
self.tree_view.setModel(self.model)
self.tree_view.setRootIndex(self.model.index(self.root_path))
# [5] ListView 설정
self.list_view = QListView(splitter)
self.list_view.setModel(self.model)
self.list_view.setRootIndex(self.model.index(self.root_path))
# [6] TableView 설정
self.table_view = QTableView(splitter)
self.table_view.setModel(self.model)
self.table_view.setRootIndex(self.model.index(self.root_path))
# [7] 트리에서 선택이 바뀌면 다른 View의 Root Index 변경
self.tree_view.selectionModel().currentChanged.connect(
self.on_tree_current_changed
)
# [8] 전체 레이아웃 설정
layout = QVBoxLayout(self)
layout.addWidget(splitter)
def on_tree_current_changed(self, current, previous):
"""
트리에서 선택한 항목에 따라
ListView와 TableView의 표시 기준을 변경합니다.
"""
if self.model.isDir(current):
self.list_view.setRootIndex(current)
self.table_view.setRootIndex(current)
else:
parent_index = current.parent()
self.list_view.setRootIndex(parent_index)
self.table_view.setRootIndex(parent_index)
수정된 Widget은 QFileSystemModel을 직접 생성하지 않습니다.
대신 main.py에서 전달받은 model을 self.model에 저장한 뒤, 여러 View에 연결합니다.
중요한 점
Widget은 Model을 생성하지 않습니다.
Widget은 전달받은 Model을 각 View에 연결하고, 선택 변경 시 Root Index를 바꾸는 역할을 담당합니다.
2.5 전체 실행 흐름
Model을 외부에서 주입하는 구조의 전체 실행 흐름은 다음과 같습니다.
main.py 실행
↓
QApplication 생성
↓
QFileSystemModel 생성
↓
root_path 설정
↓
Widget(model, root_path) 생성, Model 주입
↓
Widget이 QTreeView / QListView / QTableView 생성, Model 주입
↓
세 View가 같은 Model 사용
↓
창 표시
이제 Model 생성 위치가 Widget 내부에서 main.py로 이동했습니다.
Widget은 화면 조립과 시그널 연결에 집중할 수 있습니다.
2.6 기존 구조와 Model 주입 구조 비교
Model을 외부에서 주입하면 역할이 더 분명해집니다.
main.py는 프로그램 실행과 Model 준비를 담당합니다.
Widget은 전달받은 Model을 View에 연결하고 화면을 구성합니다.
| 구분 | Widget 내부 생성 | Model 외부 주입 |
| Model 생성 위치 | Widget 안에서 생성 | main.py에서 생성 |
| Widget 역할 | Model 생성과 View 조립을 모두 담당 | 전달받은 Model로 View 조립 담당 |
| main.py 역할 | Widget 실행만 담당 | Model 생성 후 Widget에 전달 |
| 확장성 | Model 생성 로직이 Widget에 묶임 | 여러 Widget에 같은 Model을 전달하기 쉬움 |
추가 실습의 핵심
Model을 외부에서 만들고 Widget에 전달하면, Widget은 화면 구성에 더 집중할 수 있습니다.
이 구조는 나중에 여러 화면이 같은 Model을 공유할 때도 도움이 됩니다.
3. View 분리하기
→ QTreeView, QListView, QTableView 설정 코드를 각각의 View 클래스로 분리합니다.
3.1 실습 목표
이전 실습은 Model을 Widget 밖에서 만들고 Widget에 전달했습니다.
하지만 QTreeView, QListView, QTableView 설정 코드는 여전히 Widget 안에 들어 있습니다.
현재 구조
main.py
└── QFileSystemModel 생성
Widget
├── 전달받은 Model 사용
├── QTreeView 구현
├── QListView 구현
├── QTableView 구현
├── 시그널 연결
└── 화면 배치
이번 단계에서는 View 설정 코드를 별도의 클래스로 나눕니다.
그리고 Widget은 여러 View 모듈을 조립하는 역할에 집중하도록 바꿉니다.
변경 구조
main.py
└── QFileSystemModel 생성
FileTreeView
└── QTreeView 구현 담당
FileListView
└── QListView 구현 담당
FileTableView
└── QTableView 구현 담당
Widget
├── 전달받은 Model 사용
├── QTreeView 조합
├── QListView 조합
├── QTableView 조합
├── 시그널 연결
└── 화면 배치
실습 목표
각 View의 설정 코드를 별도 클래스로 분리하고, Widget은 여러 View를 하나의 화면에 조립하는 역할로 정리합니다.
3.2 왜 View 클래스를 분리할까?
두번째 예제 구조에서는 Model 생성을 main.py로 분리했습니다.
하지만 View 구현 코드는 아직 Widget 안에 모여 있습니다.
코드가 짧을 때는 괜찮지만, View별 설정이 늘어나면 관리가 어려워질 수 있습니다.
| 문제 | 설명 |
| Widget 코드가 길어짐 | View 생성, Model 연결, 레이아웃, 시그널 연결 코드가 한 클래스에 모입니다. |
| View별 수정이 어려움 | TreeView만 수정하고 싶어도 Widget 전체 코드를 같이 봐야 합니다. |
| 역할이 섞임 | Widget이 화면 조립뿐 아니라 각 View의 세부 설정까지 담당하게 됩니다. |
그래서 View별 설정을 각각의 클래스로 분리하면 역할이 더 명확해집니다.
문제의 핵심
하나의 Widget 클래스가 너무 많은 화면 설정을 담당하면 코드가 길어지고 유지보수가 어려워질 수 있습니다.
3.3 프로젝트 구조 변경
이번 실습에서는 파일을 다음과 같이 나눕니다.
2View1Model_Demo/
├─ main.py
├─ widget.py
└─ views.py # FileTreeView, FileListView, FileTableView 각각의 클래스 파일이라고 가정합니다.
| 파일 | 역할 |
| main.py | 프로그램 실행 진입점이며, Model을 생성합니다. |
| widget.py | 전달받은 Model을 사용해, 여러 View를 통합하여 화면에 배치합니다. |
| views.py | FileTreeView, FileListView, FileTableView 클래스를 정의합니다. |
핵심은 View 클래스를 views.py로 분리하는 것입니다.
3.4 views.py 작성하기
먼저 views.py 파일을 만듭니다.
이 파일에는 QTreeView, QListView, QTableView를 상속한 클래스를 작성합니다.
# views.py
from PySide6.QtWidgets import QTreeView, QListView, QTableView
class FileTreeView(QTreeView):
"""
파일/폴더 구조를 트리 형태로 보여주는 View입니다.
"""
def __init__(self, model, root_path, parent=None):
super().__init__(parent)
self.setModel(model)
self.setRootIndex(model.index(root_path))
class FileListView(QListView):
"""
선택된 폴더 안의 파일/폴더를 리스트 형태로 보여주는 View입니다.
"""
def __init__(self, model, root_path, parent=None):
super().__init__(parent)
self.setModel(model)
self.setRootIndex(model.index(root_path))
class FileTableView(QTableView):
"""
선택된 폴더 안의 파일/폴더를 표 형태로 보여주는 View입니다.
"""
def __init__(self, model, root_path, parent=None):
super().__init__(parent)
self.setModel(model)
self.setRootIndex(model.index(root_path))
각 클래스는 Model과 root_path를 전달받습니다.
그리고 내부에서 setModel()과 setRootIndex()를 실행합니다.
| 클래스 | 역할 |
| FileTreeView | QTreeView 설정을 담당합니다. |
| FileListView | QListView 설정을 담당합니다. |
| FileTableView | QTableView 설정을 담당합니다. |
중요한 점
View 클래스는 데이터를 직접 만들지 않습니다.
외부에서 전달받은 Model을 자기 화면 방식에 맞게 보여줄 뿐입니다.
3.5 widget.py 수정하기
이제 widget.py에서는 QTreeView, QListView, QTableView를 직접 만들지 않습니다.
views.py에서 만든 FileTreeView, FileListView, FileTableView를 가져와 사용합니다.
# widget.py
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QWidget,
QSplitter,
QVBoxLayout,
)
from views import FileTreeView, FileListView, FileTableView
class Widget(QWidget):
"""
외부에서 전달받은 Model을 사용해
여러 View 클래스를 통합해서 배치하는 위젯입니다.
"""
def __init__(self, model, root_path, parent=None):
super().__init__(parent)
# [1] 기본 창 설정
self.setWindowTitle("Model/View 데모 - View 클래스 분리 + Model 주입")
self.resize(900, 400)
# [2] 외부에서 전달받은 Model과 경로 저장
self.model = model
self.root_path = root_path
# [3] 스플리터 생성
splitter = QSplitter(Qt.Horizontal, self)
# [4] 분리된 View 클래스 사용
self.tree_view = FileTreeView(self.model, self.root_path, splitter)
self.list_view = FileListView(self.model, self.root_path, splitter)
self.table_view = FileTableView(self.model, self.root_path, splitter)
# [5] 트리에서 선택이 바뀌면 다른 View의 Root Index 변경
self.tree_view.selectionModel().currentChanged.connect(
self.on_tree_current_changed
)
# [6] 전체 레이아웃 설정
layout = QVBoxLayout(self)
layout.addWidget(splitter)
def on_tree_current_changed(self, current, previous):
"""
트리에서 선택한 항목에 따라
ListView와 TableView의 표시 기준을 변경합니다.
"""
if self.model.isDir(current):
self.list_view.setRootIndex(current)
self.table_view.setRootIndex(current)
else:
parent_index = current.parent()
self.list_view.setRootIndex(parent_index)
self.table_view.setRootIndex(parent_index)
기존 코드에서는 Widget 안에서 QTreeView, QListView, QTableView를 직접 생성했습니다.
수정 후에는 FileTreeView, FileListView, FileTableView 클래스를 사용합니다.
Widget
├── 전달받은 Model 저장
├── FileTreeView 생성
├── FileListView 생성
├── FileTableView 생성
├── 시그널 연결
└── 화면 배치
Widget은 이제 각각의 View 세부 설정을 직접 알 필요가 줄어듭니다.
대신 이미 만들어진 View 클래스를 가져와 조립하는 역할에 집중합니다.
3.6 main.py 작성하기
main.py는 9장에서 작성한 구조를 그대로 사용합니다.
Model을 만들고, root_path를 설정한 뒤, Widget에 전달합니다.
# main.py
import sys
from PySide6.QtCore import QDir
from PySide6.QtWidgets import QApplication, QFileSystemModel
from widget import Widget
def main():
app = QApplication(sys.argv)
# [1] Model 생성
model = QFileSystemModel(app)
# [2] Model의 기준 경로 설정
root_path = QDir.currentPath()
model.setRootPath(root_path)
# [3] Model과 root_path를 Widget에 전달
window = Widget(model, root_path)
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
main.py의 역할은 명확합니다.
Model을 준비하고, Widget에 전달한 뒤, 창을 실행합니다.
3.7 전체 실행 흐름
View 클래스 분리와 Model 외부 주입을 함께 적용한 실행 흐름은 다음과 같습니다.
main.py 실행
↓
QApplication 생성
↓
QFileSystemModel 생성
↓
root_path 설정
↓
Widget(model, root_path) 생성, Model 주입
↓
Widget이 FileTreeView / FileListView / FileTableView 생성, Model 주입
↓
세 View가 주입된 같은 Model 사용
↓
창 표시
이 구조에서는 Model 생성과 View 설정이 모두 분리됩니다.
main.py는 Model을 준비합니다.
views.py는 View 설정을 담당합니다.
widget.py는 여러 View를 하나의 화면에 조립합니다.
3.8 기존 구조와 분리 구조 비교
이번 변경은 프로그램 동작을 크게 바꾸는 것이 아닙니다.
같은 Model을 여러 View가 공유한다는 핵심은 그대로 유지됩니다.
달라지는 것은 코드의 역할 분리입니다.
| 구분 | 예제2 구조 | 예제3 구조 |
| Model 생성 | main.py에서 생성 | main.py에서 생성 |
| View 생성 | Widget 안에서 직접 생성 | FileTreeView, FileListView, FileTableView에서 담당 |
| Widget 역할 | 전달받은 Model로 View를 직접 만들고 배치 | 분리된 View 클래스를 조립하고 시그널 연결 |
| View별 수정 | Widget 코드 안에서 수정 | views.py의 해당 View 클래스에서 수정 |
# 두번째 예제 구조
main.py
└── Model 생성
Widget
├── QTreeView 설정
├── QListView 설정
└── QTableView 설정
# 세번째 예제 구조
main.py
└── Model 생성
views.py
├── FileTreeView
├── FileListView
└── FileTableView
widget.py
└── 세 View를 조립
추가 실습의 핵심
View 클래스를 분리하면 각 View의 설정 코드를 독립적으로 관리할 수 있습니다.
Widget은 여러 View를 모아 하나의 화면으로 구성하는 통합 역할에 집중합니다.
기억할 문장
Model은 main.py에서 준비하고, View 설정은 views.py에서 담당하며, Widget은 여러 View를 조립합니다.