728x90

1. 학습 목표

더보기

1. 지난 포스트(6.11 QScrollArea)에서 문제 확인

 

6.11 QScrollArea

다양한 스크롤 UI 위젯(QScrollArea, QListWidget, QTextEdit)을 사용해 보고,이를 하나의 창에서 QTabWidget 으로 탭 구조로 통합해 보기 1. QScrollArea더보기# tab_scroll_area.pyfrom PySide6.QtWidgets import ( QWidget, QVBoxLayo

basiclike.tistory.com

1.1 QScrollArea 기반 탭
라벨을 직접 content_layout.addWidget(label) 형태로 추가
→ 데이터가 GUI 위젯 안에 저장됨
→ 다른 View와 데이터 공유 불가


1.2 QListWidget 기반 탭
self.list_widget.addItem 데이터  ("항목")
→ 아이템 데이터는 QListWidget 내부에 별도의 ListModel로 저장됨
→ 다른 View와 데이터 공유 불가

1.3 QTextEdit 기반 탭
self.log_textedit.append("로그")
→ 텍스트는 QTextEdit 내부 문서(Document)에 바로 저장됨
→ 역시 다른 View와 데이터 공유 불가능

 

1.4 공통된 근본적 문제

다양한 스크롤 UI(View)는 결국 “표시 방식만 다를 뿐, 데이터는 동일할 수 있다”

QScrollArea, QListWidget, QTextEdit은 각각 자체적으로 데이터를 내부에 저장한다
→ 즉 “뷰(View)에 데이터가 저장되는 구조”

 


 

2. Model/View 구조 구현

로그 문자열을 저장하는 LogModel 1개
이를 서로 다른 3개의 View(QScrollArea / QListWidget / QTextEdit)가 같이 공유하는 구조

 

2. 프로젝트 구조 만들기

더보기
ModelViewLogDemo/
 ├─ main.py            # 프로그램 진입점
 ├─ model_log.py       # ★ 공통 데이터 모델 (LogModel)
 ├─ view_scroll.py     # QScrollArea 기반 뷰
 ├─ view_list.py       # QListWidget 기반 뷰
 ├─ view_textedit.py   # QTextEdit 기반 뷰
 └─ widget.py          # 탭(QTabWidget)으로 3개 뷰를 묶은 메인 화면
main.py
0.00MB
model_log.py
0.00MB
view_list.py
0.00MB
view_scroll.py
0.00MB
view_textedit.py
0.00MB
widget.py
0.00MB

 

3. log_model.py

: 공통 데이터 모델 (LogModel)

더보기
# log_model.py
from PySide6.QtCore import QObject, Signal


class LogModel(QObject):
    """
    로그 문자열을 저장하는 간단한 모델 클래스.
    - 내부에 로그 리스트(_logs)를 가지고 있음
    - 로그가 변경될 때마다 logsChanged 시그널을 emit해서
      View들이 자동으로 화면을 갱신할 수 있게 해준다.
    """
    logsChanged = Signal()  # 데이터 변경 알림 시그널

    def __init__(self, parent=None):
        super().__init__(parent)
        self._logs: list[str] = []

    def add_log(self, text: str):
        """로그 한 줄 추가 후, 모든 View에 변경 사실 알림"""
        self._logs.append(text)
        self.logsChanged.emit()

    def clear(self):
        """로그 전체 삭제"""
        self._logs.clear()
        self.logsChanged.emit()

    def logs(self) -> list[str]:
        """현재 로그 리스트를 읽기 전용으로 반환"""
        return list(self._logs)  # 복사본 반환 (외부에서 직접 수정 못 하도록)
  • 로그 문자열들을 리스트로 관리
  • 로그가 추가되면 logsChanged 시그널 발행 → 모든 View에게 “다시 그려라” 알림

 

4. view_scroll.py

: QScrollArea 기반 뷰

더보기
# view_list.py
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout,
    QPushButton, QListWidget
)

from model_log import LogModel


class ListView(QWidget):
    """
    QListWidget을 이용해 로그를 보여주는 뷰(View).
    - 모델의 로그를 QListWidget 아이템으로 변환하여 표시
    """
    def __init__(self, model: LogModel, parent=None):
        super().__init__(parent)
        self.model = model

        layout = QVBoxLayout(self)

        self.list_widget = QListWidget()
        layout.addWidget(self.list_widget)

        self.add_button = QPushButton("리스트 뷰에 로그 추가")
        self.add_button.clicked.connect(self.add_log_clicked)
        layout.addWidget(self.add_button)

        # 모델이 바뀔 때마다 리스트 다시 채우기
        self.model.logsChanged.connect(self.refresh_view)

        # 초기 로딩
        self.refresh_view()

    def add_log_clicked(self):
        count = len(self.model.logs()) + 1
        self.model.add_log(f"[ListView] 추가 로그 {count}")

    def refresh_view(self):
        """모델의 로그 리스트를 QListWidget에 반영"""
        self.list_widget.clear()
        for text in self.model.logs():
            self.list_widget.addItem(text)

        # 맨 아래 항목으로 스크롤
        self.list_widget.scrollToBottom()

 

  • LogModel의 로그 리스트를 QLabel 여러 개로 생성하여 스크롤 영역에 표시
  • “로그 추가” 버튼을 누르면 모델에 로그를 추가
  • 모델이 바뀌면(logsChanged) 전체 라벨을 다시 그림

 


 

5. view_list.py 

: QListWidget 기반 뷰

더보기
# view_scroll.py
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout, QScrollArea,
    QLabel, QPushButton
)
from PySide6.QtCore import Qt

from model_log import LogModel


class ScrollView(QWidget):
    """
    QScrollArea + QLabel 조합으로 로그를 보여주는 뷰(View).
    같은 LogModel을 사용하면 다른 View와 항상 같은 로그를 공유한다.
    """
    def __init__(self, model: LogModel, parent=None):
        super().__init__(parent)
        self.model = model

        layout = QVBoxLayout(self)

        # [1] 스크롤 영역
        self.scroll_area = QScrollArea()
        self.scroll_area.setWidgetResizable(True)
        layout.addWidget(self.scroll_area)

        # [2] 로그 추가 버튼
        self.add_button = QPushButton("스크롤 뷰에 로그 추가")
        self.add_button.clicked.connect(self.add_log_clicked)
        layout.addWidget(self.add_button)

        # [3] 스크롤 안 실제 내용 위젯
        self.content_widget = QWidget()
        self.content_layout = QVBoxLayout(self.content_widget)
        self.content_layout.setAlignment(Qt.AlignTop)

        self.scroll_area.setWidget(self.content_widget)

        # [4] 모델 변경 시, 화면 갱신
        self.model.logsChanged.connect(self.refresh_view)

        # 초기 표시
        self.refresh_view()

    def add_log_clicked(self):
        """버튼 클릭 시 모델에 로그 추가 (Model만 수정, View는 자동 반영)"""
        count = len(self.model.logs()) + 1
        self.model.add_log(f"[ScrollView] 추가 로그 {count}")

    def refresh_view(self):
        """모델의 로그 리스트를 기반으로 라벨들을 다시 생성"""
        # 기존 라벨 제거
        while self.content_layout.count():
            item = self.content_layout.takeAt(0)
            w = item.widget()
            if w is not None:
                w.deleteLater()

        # 모델에서 로그 읽어와 새 라벨 생성
        for text in self.model.logs():
            label = QLabel(text)
            self.content_layout.addWidget(label)

        # 스크롤을 맨 아래로 내리기
        bar = self.scroll_area.verticalScrollBar()
        bar.setValue(bar.maximum())

 

  • LogModel의 로그들을 QListWidget 아이템으로 보여줌
  • 버튼 클릭 시 모델에 로그 추가
  • logsChanged 시그널을 받아 리스트를 다시 채움

 


 

6. view_textedit.py

:  QTextEdit 기반 뷰

더보기
# view_textedit.py
from PySide6.QtWidgets import (
    QWidget, QVBoxLayout,
    QPushButton, QTextEdit
)
from PySide6.QtGui import QTextCursor

from model_log import LogModel


class TextEditView(QWidget):
    """
    QTextEdit를 사용해 로그를 보여주는 뷰(View).
    전형적인 '로그 출력창' UI 스타일.
    """
    def __init__(self, model: LogModel, parent=None):
        super().__init__(parent)
        self.model = model

        layout = QVBoxLayout(self)

        self.log_textedit = QTextEdit()
        self.log_textedit.setReadOnly(True)
        layout.addWidget(self.log_textedit)

        self.log_button = QPushButton("텍스트 뷰에 로그 추가")
        self.log_button.clicked.connect(self.add_log_clicked)
        layout.addWidget(self.log_button)

        # 모델 변경 시 텍스트 다시 구성
        self.model.logsChanged.connect(self.refresh_view)

        self.refresh_view()

    def add_log_clicked(self):
        count = len(self.model.logs()) + 1
        self.model.add_log(f"[TextEditView] 추가 로그 {count}")

    def refresh_view(self):
        """모델 로그를 QTextEdit 전체 내용으로 반영"""
        self.log_textedit.clear()
        for text in self.model.logs():
            self.log_textedit.append(text)

        # 커서를 맨 아래로 이동
        cursor = self.log_textedit.textCursor()
        cursor.movePosition(QTextCursor.End)
        self.log_textedit.setTextCursor(cursor)
        self.log_textedit.ensureCursorVisible()

 

  • LogModel의 로그들을 한 줄씩 QTextEdit에 보여주는 로그창
  • “로그 추가” 버튼으로 모델에 로그 추가
  • 모델 변경 시 텍스트 영역 전체를 다시 그림 (append 기반 또는 전체 재구성 가능)

 

7. widget.py    

: 탭(QTabWidget)으로 3개 뷰를 묶은 메인 화면

더보기
# widget.py
from PySide6.QtWidgets import QWidget, QVBoxLayout, QTabWidget

from model_log import LogModel
from view_scroll import ScrollView
from view_list import ListView
from view_textedit import TextEditView


class ViewWidget(QWidget):
    """
    여러 View(스크롤 / 리스트 / 텍스트)를 탭으로 묶은 메인 화면.
    - 세 개의 View 모두 같은 LogModel 인스턴스를 공유한다.
    """
    def __init__(self, model: LogModel, parent=None):
        super().__init__(parent)

        self.setWindowTitle("Model / View 데모 - 여러 View, 하나의 Model")

        self.resize(600, 400)

        layout = QVBoxLayout(self)

        tab_widget = QTabWidget()

        # 같은 model을 세 개의 View에 전달
        tab_widget.addTab(ScrollView(model), "QScrollArea 뷰")
        tab_widget.addTab(ListView(model), "QListWidget 뷰")
        tab_widget.addTab(TextEditView(model), "QTextEdit 뷰")

        layout.addWidget(tab_widget)

LogModel 1개를 받아서,  세 개를 QTabWidget으로 탭 구조로 묶어 보여줌

  • ScrollView
  • ListView
  • TextEditView

 

8. main.py

: 실행 진입점 작성

더보기
# main.py
import sys
from PySide6.QtWidgets import QApplication

from model_log import LogModel
from widget import ViewWidget


def main():
    """
    프로그램의 진입점(Entry Point).
    - QApplication 생성
    - LogModel(공통 데이터 모델) 생성
    - ViewWidget(여러 View를 가진 화면)에 모델을 주입
    - 이벤트 루프 실행
    """
    # [1] QApplication 생성
    app = QApplication(sys.argv)
    app.setApplicationName("Model / View 데모")

    # [2] 공통 모델 생성 (★ 모든 View가 이 모델을 공유)
    model = LogModel()

    # 초기 테스트 로그 몇 개 추가
    model.add_log("[Init] 프로그램 시작")
    model.add_log("[Init] Model / View 데모 준비 완료")

    # [3] 메인 위젯(여러 View를 가진 화면) 생성
    window = ViewWidget(model)
    window.show()

    # [4] 이벤트 루프 실행 (GUI 프로그램 유지)
    sys.exit(app.exec())


if __name__ == "__main__":
    main()

 

9. 추가 실습 과제

: (1) QSplitter 를 사용하여 3개의 뷰를 동시에 보이도록 구현하기

더보기

6.11 QScrollArea 결과

: 구현 목표와 비교

 

 

구현 목표 

: 6.11 QScrollArea 결과와 비교

 

 

 

 

QTabWidget → QSplitter 변경으로

  • 탭을 전환할 필요 없이
  • 모든 View가 동시에 보이므로 Model–View 구조 이해가 더 쉬워짐

화면(View)이 3개여도 Model은 1개

  • 모든 View가 같은 데이터(로그)를 공유
  • 한 곳에서 데이터 변경하면 모든 화면이 자동 업데이트
# widget.py 힌트 코드
 
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QSplitter  # 상단 import에 추가
 
# __init__ 안에서
# --- Splitter 설정 ---
layout = QVBoxLayout(self)

# [1] 수평 Splitter 생성
splitter = QSplitter(Qt.Horizontal)

# [2] 같은 Model을 공유하는 3개의 View 추가
splitter.addWidget(ScrollView(model))      # 왼쪽
splitter.addWidget(ListView(model))        # 가운데
splitter.addWidget(TextEditView(model))    # 오른쪽

# [3] 초기 splitter 비율 설정 (선택)
splitter.setSizes([300, 300, 300])

layout.addWidget(splitter)
widget.py
0.00MB