본문 바로가기
목차
Python/PySide PyQt

[PySide] QItemSelectionModel 살펴보기 - 작성중

by ds31x 2025. 6. 4.
728x90
반응형

QItemSelectionModel은 Qt Model-View Architecture에서 selection 을 관리하는 클래스.

QItemSelectionModel 개요

from PySide6.QtCore import QItemSelectionModel, QModelIndex
from PySide6.QtWidgets import QListView, QTableView, QTreeView

# 모든 View는 자동으로 QItemSelectionModel을 가집니다
view = QListView()
selection_model = view.selectionModel()  # QItemSelectionModel 인스턴스
print(type(selection_model))  # <class 'PySide6.QtCore.QItemSelectionModel'>

 

역할:

  • 현재 선택된 항목(current item) 추적
  • 선택 영역(selection range) 관리
  • 선택 변경 시 자동으로 signal 발생: slot 연결을 통한 처리.
  • 프로그래밍을 통한 선택(selection) 조작 제공

다음 URL의 글에서 사용된 방식도 같이 확인해 볼 것.

2025.06.02 - [Python/PySide PyQt] - [PySide6] QTreeView 와 QStandardItemModel, QStandardItem

 

[PySide6] QTreeView 와 QStandardItemModel, QStandardItem

0. Pre-requisites:0-0. Model-View Architecture란?QTreeView는 Qt의 Model-View Architecture 를 따름.이는 데이터(Model)와 그 표현(View)을 분리하는 디자인 패턴.Qt에서 Model-View Architecture는 다음의 요소로 구성됨:Model:데

ds31x.tistory.com

 


1. 주요 시그널들 (Signals)

1-1. currentChanged-Signal

1-1-1. Signature:

currentChanged(current: QModelIndex, previous: QModelIndex)

1-1-2. 용도:

  • 현재 selection(선택된 items)이 바뀔 때 발생
  • QAbstractItemView.SingleSelection(단일 선택, setSelectionMode메서드로 설정)에서 가장 중요한 signal
  • 키보드 커서마우스 클릭으로 다른 항목을 선택할 때 발생

1-1-3. 매개변수:

  • current: 새로 선택된 항목의 QModelIndex
  • previous: 이전에 선택되었던 항목의 QModelIndex

QModelIndex 의 주요 메서드는 다음과 같음:

  • row(): 부모 내에서의 행 번호 반환
  • column(): 열 번호 반환
  • parent(): 부모 인덱스 반환
  • isValid(): 유효한 인덱스인지 확인

1-1-4. 예제:

def on_current_changed(self, current, previous):
    if current.isValid():
        item_text = self.model.data(current, Qt.DisplayRole)
        print(f"새 선택: {item_text} (행 {current.row()})")

    if previous.isValid():
        prev_text = self.model.data(previous, Qt.DisplayRole)
        print(f"이전 선택: {prev_text} (행 {previous.row()})")

# 연결
selection_model.currentChanged.connect(on_current_changed)

1-2. selectionChanged 시그널

1-2-1. 시그니처:

selectionChanged(selected: QItemSelection, deselected: QItemSelection)

1-2-2. 용도:

  • 현재 selection(선택된 items)이 바뀔 때 발생
    • QAbstractItemView.MultipleSelection(다중 선택)에서 주로 사용
  • Ctrl+<click>, Shift+<click>으로 여러 항목을 선택/해제할 때
  • 프로그래밍 코드 상에서 selection(선택)을 추가/제거할 때

1-2-3. 매개변수:

  • selected: 새로 선택된 영역들 (QItemSelection)
  • deselected: 선택 해제된 영역들 (QItemSelection)

참고: QItemSelection

  • select(topLeft: QModelIndex, bottomRight: QModelIndex)
    주어진 인덱스 범위를 선택 영역에 추가
  • merge(other: QItemSelection, command: QItemSelectionModel.SelectionFlags)
    다른 선택 객체와 병합 (선택 방식 지정 가능)
  • indexes(): List[QModelIndex]
    선택된 모든 인덱스를 평탄화하여 리스트로 반환
  • ranges(): List[QItemSelectionRange]
    선택된 범위(QItemSelectionRange 객체)들의 list 객체 반환
  • clear()
    선택 영역 초기화
  • isEmpty(): bool
    선택 영역이 비어 있는지 확인
  • size(): int
    포함된 선택 범위의 개수 반환
  • append(range: QItemSelectionRange)
    선택 범위 하나를 추가

1-2-4. 예제:

def on_selection_changed(self, selected, deselected):
    total_selected = len(self.selection_model.selectedIndexes())
    print(f"현재 총 선택된 항목: {total_selected}개")

    # 새로 선택된 범위 정보
    if not selected.isEmpty():
        for selection_range in selected:
            print(f"추가 선택: {selection_range.top()}-{selection_range.bottom()}행")

    # 선택 해제된 범위 정보  
    if not deselected.isEmpty():
        for selection_range in deselected:
            print(f"선택 해제: {selection_range.top()}-{selection_range.bottom()}행")

# 연결
selection_model.selectionChanged.connect(on_selection_changed)

1-3 currentRowChanged 시그널

1-3-1. 시그니처:

currentRowChanged(current: QModelIndex, previous: QModelIndex)

1-3-2. 용도:

  • 현재 선택된 행이 바뀔 때 발생
  • currentChanged와 유사하지만 행(row) 변경에만 특화
  • 테이블 뷰에서 같은 행의 다른 열로 이동할 때는 발생하지 않음

1-3-3. 예제:

def on_current_row_changed(self, current, previous):
    if current.isValid():
        print(f"현재 행: {current.row()}")
    if previous.isValid():
        print(f"이전 행: {previous.row()}")

# 연결
selection_model.currentRowChanged.connect(on_current_row_changed)

1-4. currentColumnChanged 시그널

1-4-1. 시그니처:

currentColumnChanged(current: QModelIndex, previous: QModelIndex)

1-4-2. 용도:

  • 현재 선택된 열이 바뀔 때 발생
  • 주로 테이블 뷰에서 사용
  • 리스트 뷰에서는 거의 사용하지 않음 (열이 1개뿐이므로)

1-4-3. 예제:

def on_current_column_changed(self, current, previous):
    if current.isValid():
        print(f"현재 열: {current.column()}")

# 연결 (주로 QTableView에서)
table_view.selectionModel().currentColumnChanged.connect(on_current_column_changed)

 


2. 주요 메서드들 (Methods)

2-1. 선택 상태 조회 메서드

2-1-1. currentIndex() 메서드

2-1-1-1. 시그니처:

currentIndex() -> QModelIndex

 

2-1-1-2. 용도:

  • 현재 포커스가 있는 항목의 인덱스를 반환
  • 키보드 입력을 받을 항목을 나타냄
  • 다중 선택 시에도 하나의 항목만 "current"가 됨

2-1-1-3. 예제:

current = selection_model.currentIndex()
if current.isValid():
    item_text = model.data(current, Qt.DisplayRole)
    print(f"현재 항목: {item_text} (행: {current.row()}, 열: {current.column()})")
else:
    print("현재 선택된 항목 없음")

2-1-2. selectedIndexes() 메서드

2-1-2-1. 시그니처:

selectedIndexes() -> List[QModelIndex]

 

2-1-2-2. 용도:

  • 선택된 모든 인덱스들을 리스트로 반환
  • 다중 선택에서 모든 선택된 항목을 확인할 때 사용
  • 빈 리스트면 선택된 항목이 없음

2-1-2-3. 예제:

selected = selection_model.selectedIndexes()
print(f"선택된 항목 수: {len(selected)}")

for index in selected:
    item_text = model.data(index, Qt.DisplayRole)
    print(f"선택됨: {item_text} (행 {index.row()})")

2-1-3. selectedRows() 메서드

2-1-3-1. 시그니처:

selectedRows(column: int = 0) -> List[QModelIndex]

 

2-1-3-2. 용도:

  • 선택된 행들의 인덱스를 반환
  • 테이블에서 전체 행이 선택된 경우 사용
  • column 매개변수로 어느 열의 인덱스를 반환할지 지정

2-1-3-3. 예제:

selected_rows = selection_model.selectedRows()
print(f"선택된 행 수: {len(selected_rows)}")

for row_index in selected_rows:
    print(f"선택된 행: {row_index.row()}")

2-1-4. hasSelection() 메서드

2-1-4-1. 시그니처:

hasSelection() -> bool

 

2-1-4-2. 용도:

  • 선택된 항목이 하나라도 있는지 확인
  • GUI 업데이트나 메뉴 활성화 판단에 사용

2-1-4-3. 예제:

if selection_model.hasSelection():
    print("선택된 항목이 있습니다")
    # 삭제 버튼 활성화 등
    delete_button.setEnabled(True)
else:
    print("선택된 항목이 없습니다")
    delete_button.setEnabled(False)

2-1-5. isSelected() 메서드

2-1-5-1. 시그니처:

isSelected(index: QModelIndex) -> bool

 

2-1-5-2. 용도:

  • 특정 인덱스가 선택되었는지 확인
  • 조건부 처리나 상태 확인에 사용

2-1-5-3. 예제:

test_index = model.index(5, 0)  # 5번째 행
if selection_model.isSelected(test_index):
    print("5번째 항목이 선택되어 있습니다")
else:
    print("5번째 항목이 선택되어 있지 않습니다")

2-2. 선택 상태 설정 메서드

2-2-1. setCurrentIndex() 메서드

2-2-1-1. 시그니처:

setCurrentIndex(index: QModelIndex, command: QItemSelectionModel.SelectionFlags)

 

2-2-1-2. 용도:

  • 현재 선택을 프로그래밍으로 변경
  • 두 번째 매개변수로 선택 동작을 제어

2-2-1-3. SelectionFlags:

  • NoUpdate: 아무것도 하지 않음
  • Clear: 기존 선택 모두 해제
  • Select: 해당 항목 선택
  • ClearAndSelect: 기존 해제 후 해당 항목만 선택
  • Current: 현재 항목으로 설정 (포커스만)

2-2-1-4. 예제:

# 3번째 행을 선택하고 기존 선택은 모두 해제
new_index = model.index(3, 0)
selection_model.setCurrentIndex(new_index, QItemSelectionModel.ClearAndSelect)

# 포커스만 이동 (선택 상태 변경 없음)
focus_index = model.index(5, 0)  
selection_model.setCurrentIndex(focus_index, QItemSelectionModel.Current)

2-2-2. select() 메서드

2-2-2-1. 시그니처:

select(index: QModelIndex, command: QItemSelectionModel.SelectionFlags)
select(selection: QItemSelection, command: QItemSelectionModel.SelectionFlags)

 

2-2-2-2. 용도:

  • 특정 인덱스나 선택 영역을 선택/해제
  • 다중 선택을 프로그래밍으로 조작할 때 사용

2-2-2-3. 예제:

# 기존 선택에 추가
new_index = model.index(7, 0)
selection_model.select(new_index, QItemSelectionModel.Select)

# 특정 항목의 선택을 토글
toggle_index = model.index(2, 0)
selection_model.select(toggle_index, QItemSelectionModel.Toggle)

# 특정 항목만 선택 해제
deselect_index = model.index(4, 0)
selection_model.select(deselect_index, QItemSelectionModel.Deselect)

2-2-3. selectAll() 메서드 - (View 용)

QItemSelectionModel의 메서드가 아니며, 연결된 View 객체에서 호출해야 한다.

자주 이용되는터라 여기에 넣어두었지만, 주의할 것.

 

2-2-3-1. 시그니처:

selectAll() -> None

 

2-2-3-2.용도:

  • 모든 항목을 선택
  • Ctrl+A 기능 구현에 사용

2-2-3-3.예제:

# 전체 선택
# selection_model.selectAll() # not working!
self.view.selectAll()
print(f"전체 선택됨: {len(selection_model.selectedIndexes())}개 항목")

2-2-4. clearSelection() 메서드

2-2-4-1. 시그니처:

clearSelection() -> None

 

2-2-4-2. 용도:

  • 모든 선택을 해제
  • 현재 항목(current)은 유지됨

2-2-4-3. 예제:

# 모든 선택 해제
selection_model.clearSelection()
print("모든 선택이 해제되었습니다")

# 현재 항목 확인 (여전히 유효할 수 있음)
current = selection_model.currentIndex()
if current.isValid():
    print(f"현재 항목은 여전히 유효: 행 {current.row()}")

2-2-5. clearCurrentIndex() 메서드

2-2-5-1. 시그니처:

clearCurrentIndex() -> None

 

2-2-5-2. 용도:

  • 현재 항목(current) 설정을 해제
  • 포커스를 완전히 제거

2-2-5-3. 예제:

# 현재 항목과 선택을 모두 해제
selection_model.clearSelection()
selection_model.clearCurrentIndex()

# 이제 아무것도 선택되지 않음
print(f"현재 항목 유효: {selection_model.currentIndex().isValid()}")  # False
print(f"선택 항목 있음: {selection_model.hasSelection()}")  # False

3. 종합 예제

아래 소스를 돌려보면서 설명을 보는게 좋음.

from PySide6.QtCore import (
    Qt, 
    QAbstractListModel, 
    QModelIndex, 
    QItemSelectionModel, 
    QItemSelection,
    )
from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QListView, 
    QPushButton, QLabel, QTextEdit, QSpinBox, QComboBox, QGroupBox,
    QAbstractItemView, QCheckBox,
    )
    
import sys

class NumberListModel(QAbstractListModel):
    """숫자 리스트 모델"""
    def __init__(self, numbers=None):
        super().__init__()
        self._numbers = numbers or list(range(1, 21))  # 1~20

    def rowCount(self, parent=QModelIndex()):
        return len(self._numbers)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid():
            return None
        if role == Qt.DisplayRole:
            return f"항목 {self._numbers[index.row()]}"
        return None

class SelectionModelDemo(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QItemSelectionModel 종합 데모")
        self.resize(1000, 800)

        # 모델과 뷰 생성
        self.model = NumberListModel()
        self.view = QListView()
        self.view.setModel(self.model)
        self.view.setSelectionMode(QAbstractItemView.ExtendedSelection)

        # 선택 모델 참조
        self.selection_model = self.view.selectionModel()

        # 로그 영역
        self.log = QTextEdit()
        self.log.setMaximumHeight(250)

        self.setup_ui()
        self.connect_all_signals()

    def setup_ui(self):
        main_layout = QHBoxLayout(self)

        # 왼쪽: 뷰와 시그널 정보
        left_layout = QVBoxLayout()

        # 리스트 뷰
        left_layout.addWidget(QLabel("숫자 리스트 (Ctrl/Shift로 다중 선택)"))
        left_layout.addWidget(self.view)

        # 시그널 정보 그룹
        signal_group = self.create_signal_info_group()
        left_layout.addWidget(signal_group)

        main_layout.addWidget(self.create_group("뷰 & 시그널", left_layout))

        # 오른쪽: 메서드 테스트
        right_layout = QVBoxLayout()

        # 조회 메서드 그룹
        query_group = self.create_query_methods_group()
        right_layout.addWidget(query_group)

        # 설정 메서드 그룹
        control_group = self.create_control_methods_group()
        right_layout.addWidget(control_group)

        main_layout.addWidget(self.create_group("메서드 테스트", right_layout))

        # 하단: 로그
        bottom_layout = QVBoxLayout()
        bottom_layout.addWidget(QLabel("시그널 & 메서드 실행 로그:"))
        bottom_layout.addWidget(self.log)

        main_widget = QWidget()
        main_widget.setLayout(main_layout)

        container_layout = QVBoxLayout(self)
        container_layout.addWidget(main_widget)
        container_layout.addLayout(bottom_layout)

    def create_group(self, title, layout):
        """그룹박스 생성"""
        group = QGroupBox(title)
        group.setLayout(layout)
        return group

    def create_signal_info_group(self):
        """시그널 정보 표시 그룹"""
        layout = QVBoxLayout()

        # 시그널 활성화 체크박스들
        self.current_changed_cb = QCheckBox("currentChanged 시그널")
        self.current_changed_cb.setChecked(True)
        self.current_changed_cb.toggled.connect(self.toggle_current_changed)
        layout.addWidget(self.current_changed_cb)

        self.selection_changed_cb = QCheckBox("selectionChanged 시그널")
        self.selection_changed_cb.setChecked(True)
        self.selection_changed_cb.toggled.connect(self.toggle_selection_changed)
        layout.addWidget(self.selection_changed_cb)

        self.current_row_changed_cb = QCheckBox("currentRowChanged 시그널")
        self.current_row_changed_cb.setChecked(False)
        self.current_row_changed_cb.toggled.connect(self.toggle_current_row_changed)
        layout.addWidget(self.current_row_changed_cb)

        layout.addWidget(QLabel("시그널을 체크/해제하여 로그 출력을 제어하세요"))

        return self.create_group("시그널 모니터링", layout)

    def create_query_methods_group(self):
        """조회 메서드 테스트 그룹"""
        layout = QVBoxLayout()

        # currentIndex() 테스트
        btn_current = QPushButton("currentIndex() 실행")
        btn_current.clicked.connect(self.test_current_index)
        layout.addWidget(btn_current)

        # selectedIndexes() 테스트
        btn_selected = QPushButton("selectedIndexes() 실행")
        btn_selected.clicked.connect(self.test_selected_indexes)
        layout.addWidget(btn_selected)

        # selectedRows() 테스트
        btn_rows = QPushButton("selectedRows() 실행")
        btn_rows.clicked.connect(self.test_selected_rows)
        layout.addWidget(btn_rows)

        # hasSelection() 테스트
        btn_has = QPushButton("hasSelection() 실행")
        btn_has.clicked.connect(self.test_has_selection)
        layout.addWidget(btn_has)

        # isSelected() 테스트
        test_layout = QHBoxLayout()
        self.test_index_spin = QSpinBox()
        self.test_index_spin.setRange(0, 19)
        self.test_index_spin.setValue(5)
        test_layout.addWidget(QLabel("인덱스:"))
        test_layout.addWidget(self.test_index_spin)

        btn_is_selected = QPushButton("isSelected() 실행")
        btn_is_selected.clicked.connect(self.test_is_selected)
        test_layout.addWidget(btn_is_selected)
        layout.addLayout(test_layout)

        return self.create_group("조회 메서드", layout)

    def create_control_methods_group(self):
        """제어 메서드 테스트 그룹"""
        layout = QVBoxLayout()

        # setCurrentIndex() 테스트
        current_layout = QHBoxLayout()
        self.set_index_spin = QSpinBox()
        self.set_index_spin.setRange(0, 19)
        self.set_index_spin.setValue(10)

        self.selection_flag_combo = QComboBox()
        self.selection_flag_combo.addItems([
            "ClearAndSelect", "Select", "Current", "Clear"
        ])

        current_layout.addWidget(QLabel("인덱스:"))
        current_layout.addWidget(self.set_index_spin)
        current_layout.addWidget(QLabel("플래그:"))
        current_layout.addWidget(self.selection_flag_combo)

        btn_set_current = QPushButton("setCurrentIndex() 실행")
        btn_set_current.clicked.connect(self.test_set_current_index)
        current_layout.addWidget(btn_set_current)
        layout.addLayout(current_layout)

        # select() 테스트
        select_layout = QHBoxLayout()
        self.select_index_spin = QSpinBox()
        self.select_index_spin.setRange(0, 19)
        self.select_index_spin.setValue(7)

        self.select_flag_combo = QComboBox()
        self.select_flag_combo.addItems([
            "Select", "Deselect", "Toggle"
        ])

        select_layout.addWidget(QLabel("인덱스:"))
        select_layout.addWidget(self.select_index_spin)
        select_layout.addWidget(QLabel("플래그:"))
        select_layout.addWidget(self.select_flag_combo)

        btn_select = QPushButton("select() 실행")
        btn_select.clicked.connect(self.test_select)
        select_layout.addWidget(btn_select)
        layout.addLayout(select_layout)

        # 기타 제어 메서드들
        other_layout = QHBoxLayout()

        btn_select_all = QPushButton("view.selectAll()")
        btn_select_all.clicked.connect(self.test_select_all)
        other_layout.addWidget(btn_select_all)

        btn_clear_selection = QPushButton("clearSelection()")
        btn_clear_selection.clicked.connect(self.test_clear_selection)
        other_layout.addWidget(btn_clear_selection)

        btn_clear_current = QPushButton("clearCurrentIndex()")
        btn_clear_current.clicked.connect(self.test_clear_current)
        other_layout.addWidget(btn_clear_current)

        layout.addLayout(other_layout)

        # 범위 선택 테스트
        range_layout = QHBoxLayout()
        range_layout.addWidget(QLabel("범위 선택 (5-10행):"))
        btn_select_range = QPushButton("범위 선택 실행")
        btn_select_range.clicked.connect(self.test_select_range)
        range_layout.addWidget(btn_select_range)
        layout.addLayout(range_layout)

        return self.create_group("제어 메서드", layout)

    def connect_all_signals(self):
        """모든 시그널 연결"""
        # 기본적으로 연결
        self.selection_model.currentChanged.connect(self.on_current_changed)
        self.selection_model.selectionChanged.connect(self.on_selection_changed)

    def log_message(self, message, color="black"):
        """로그 메시지 추가"""
        self.log.append(f'<span style="color: {color};">{message}</span>')

    # =============================================================================
    # 시그널 핸들러들
    # =============================================================================

    def on_current_changed(self, current, previous):
        """currentChanged 시그널 핸들러"""
        if not self.current_changed_cb.isChecked():
            return

        msg = "- currentChanged 시그널 발생"
        if current.isValid():
            item_text = self.model.data(current, Qt.DisplayRole)
            msg += f" → 새 선택: {item_text} (행 {current.row()})"
        else:
            msg += " → 새 선택: 없음"

        if previous.isValid():
            prev_text = self.model.data(previous, Qt.DisplayRole)
            msg += f", 이전: {prev_text} (행 {previous.row()})"
        else:
            msg += ", 이전: 없음"

        self.log_message(msg, "blue")

    def on_selection_changed(self, selected, deselected):
        """selectionChanged 시그널 핸들러"""
        if not self.selection_changed_cb.isChecked():
            return

        total = len(self.selection_model.selectedIndexes())
        msg = f"- selectionChanged 시그널 발생 → 총 선택: {total}개"

        if not selected.isEmpty():
            ranges = selected[0]  # 첫 번째 범위
            msg += f", 추가: {ranges.top()}-{ranges.bottom()}행"

        if not deselected.isEmpty():
            ranges = deselected[0]  # 첫 번째 범위
            msg += f", 제거: {ranges.top()}-{ranges.bottom()}행"

        self.log_message(msg, "green")

    def on_current_row_changed(self, current, previous):
        """currentRowChanged 시그널 핸들러"""
        if not self.current_row_changed_cb.isChecked():
            return

        msg = "- currentRowChanged 시그널 발생"
        if current.isValid():
            msg += f" → 새 행: {current.row()}"
        if previous.isValid():
            msg += f", 이전 행: {previous.row()}"

        self.log_message(msg, "purple")

    # =============================================================================
    # 시그널 토글 메서드들
    # =============================================================================

    def toggle_current_changed(self, enabled):
        if enabled:
            self.selection_model.currentChanged.connect(self.on_current_changed)
            self.log_message("- currentChanged 시그널 연결됨", "orange")
        else:
            self.selection_model.currentChanged.disconnect(self.on_current_changed)
            self.log_message("- currentChanged 시그널 연결 해제됨", "orange")

    def toggle_selection_changed(self, enabled):
        if enabled:
            self.selection_model.selectionChanged.connect(self.on_selection_changed)
            self.log_message("- selectionChanged 시그널 연결됨", "orange")
        else:
            self.selection_model.selectionChanged.disconnect(self.on_selection_changed)
            self.log_message("- selectionChanged 시그널 연결 해제됨", "orange")

    def toggle_current_row_changed(self, enabled):
        if enabled:
            self.selection_model.currentRowChanged.connect(self.on_current_row_changed)
            self.log_message("- currentRowChanged 시그널 연결됨", "orange")
        else:
            self.selection_model.currentRowChanged.disconnect(self.on_current_row_changed)
            self.log_message("- currentRowChanged 시그널 연결 해제됨", "orange")

    # =============================================================================
    # 조회 메서드 테스트들
    # =============================================================================

    def test_current_index(self):
        """currentIndex() 메서드 테스트"""
        current = self.selection_model.currentIndex()
        if current.isValid():
            item_text = self.model.data(current, Qt.DisplayRole)
            self.log_message(f"- currentIndex(): {item_text} (행 {current.row()}, 열 {current.column()})", "navy")
        else:
            self.log_message("- currentIndex(): 유효하지 않음 (선택 없음)", "navy")

    def test_selected_indexes(self):
        """selectedIndexes() 메서드 테스트"""
        selected = self.selection_model.selectedIndexes()
        self.log_message(f"- selectedIndexes(): {len(selected)}개 항목 선택됨", "navy")

        if selected:
            items = []
            for index in selected[:5]:  # 최대 5개만 표시
                item_text = self.model.data(index, Qt.DisplayRole)
                items.append(f"{item_text}(행{index.row()})")

            display_text = ", ".join(items)
            if len(selected) > 5:
                display_text += f" ... (총 {len(selected)}개)"

            self.log_message(f"   → {display_text}", "navy")

    def test_selected_rows(self):
        """selectedRows() 메서드 테스트"""
        selected_rows = self.selection_model.selectedRows()
        self.log_message(f"- selectedRows(): {len(selected_rows)}개 행 선택됨", "navy")

        if selected_rows:
            row_numbers = [idx.row() for idx in selected_rows[:10]]
            if len(selected_rows) > 10:
                self.log_message(f"   → 행 번호: {row_numbers} ... (총 {len(selected_rows)}개)", "navy")
            else:
                self.log_message(f"   → 행 번호: {row_numbers}", "navy")

    def test_has_selection(self):
        """hasSelection() 메서드 테스트"""
        has_selection = self.selection_model.hasSelection()
        self.log_message(f"- hasSelection(): {has_selection}", "navy")

    def test_is_selected(self):
        """isSelected() 메서드 테스트"""
        row = self.test_index_spin.value()
        test_index = self.model.index(row)
        is_selected = self.selection_model.isSelected(test_index)
        item_text = self.model.data(test_index, Qt.DisplayRole)
        self.log_message(f"- isSelected({item_text}): {is_selected}", "navy")

    # =============================================================================
    # 제어 메서드 테스트들
    # =============================================================================

    def test_set_current_index(self):
        """setCurrentIndex() 메서드 테스트"""
        row = self.set_index_spin.value()
        index = self.model.index(row)
        flag_text = self.selection_flag_combo.currentText()

        flag_map = {
            "ClearAndSelect": QItemSelectionModel.ClearAndSelect,
            "Select": QItemSelectionModel.Select,
            "Current": QItemSelectionModel.Current,
            "Clear": QItemSelectionModel.Clear
        }

        flag = flag_map[flag_text]
        self.selection_model.setCurrentIndex(index, flag)

        item_text = self.model.data(index, Qt.DisplayRole)
        self.log_message(f"- setCurrentIndex({item_text}, {flag_text}) 실행됨", "red")

    def test_select(self):
        """select() 메서드 테스트"""
        row = self.select_index_spin.value()
        index = self.model.index(row)
        flag_text = self.select_flag_combo.currentText()

        flag_map = {
            "Select": QItemSelectionModel.Select,
            "Deselect": QItemSelectionModel.Deselect,
            "Toggle": QItemSelectionModel.Toggle
        }

        flag = flag_map[flag_text]
        self.selection_model.select(index, flag)

        item_text = self.model.data(index, Qt.DisplayRole)
        self.log_message(f"- select({item_text}, {flag_text}) 실행됨", "red")

    def test_select_all(self):
        """view.selectAll() 메서드 테스트"""
        # self.selection_model.selectAll()
        self.view.selectAll() # view에 존재함.
        total = len(self.selection_model.selectedIndexes())
        self.log_message(f"- view.selectAll() 실행됨 → {total}개 항목 선택", "red")

    def test_clear_selection(self):
        """clearSelection() 메서드 테스트"""
        self.selection_model.clearSelection()
        self.log_message("- clearSelection() 실행됨 → 모든 선택 해제", "red")

    def test_clear_current(self):
        """clearCurrentIndex() 메서드 테스트"""
        self.selection_model.clearCurrentIndex()
        self.log_message("- clearCurrentIndex() 실행됨 → 현재 항목 해제", "red")

    def test_select_range(self):
        """범위 선택 테스트"""
        start_index = self.model.index(5)
        end_index = self.model.index(10)

        selection = QItemSelection(start_index, end_index)
        self.selection_model.select(selection, QItemSelectionModel.ClearAndSelect)

        self.log_message("- 범위 선택 (5-10행) 실행됨", "red")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = SelectionModelDemo()
    window.show()
    sys.exit(app.exec())

 


4. 실용적 활용 가이드

4-1. 상황별 시그널 선택

# 상황 1: 단일 선택에서 선택 변경 감지 (가장 일반적)
selection_model.currentChanged.connect(self.on_item_selected)

# 상황 2: 다중 선택에서 선택 변경 감지
selection_model.selectionChanged.connect(self.on_multi_selection_changed)

# 상황 3: 테이블에서 행 변경만 감지 (열 변경은 무시)
selection_model.currentRowChanged.connect(self.on_row_changed)

4-2. 자주 사용하는 패턴

# 패턴 1: 선택된 데이터 가져오기
def get_selected_data(self):
    selected_data = []
    for index in self.selection_model.selectedIndexes():
        data = self.model.data(index, Qt.DisplayRole)
        selected_data.append(data)
    return selected_data

# 패턴 2: 조건부 GUI 업데이트
def update_buttons(self):
    has_selection = self.selection_model.hasSelection()
    self.delete_button.setEnabled(has_selection)
    self.edit_button.setEnabled(has_selection)

    current = self.selection_model.currentIndex()
    self.details_panel.setVisible(current.isValid())

# 패턴 3: 프로그래밍으로 안전한 선택
def select_item_safely(self, row):
    if 0 <= row < self.model.rowCount():
        index = self.model.index(row)
        self.selection_model.setCurrentIndex(index, QItemSelectionModel.ClearAndSelect)
        return True
    return False

5. 정리

5-1. 가장 중요한 시그널 TOP 3:

  1. currentChanged : 단일 선택 변경 (90% 사용)
  2. selectionChanged : 다중 선택 변경
  3. currentRowChanged : 테이블에서 행 변경

5-2. 가장 중요한 메서드 TOP 5:

  1. currentIndex() - 현재 선택 조회
  2. selectedIndexes() - 모든 선택 조회
  3. setCurrentIndex() - 프로그래밍으로 선택
  4. hasSelection() - 선택 여부 확인
  5. clearSelection() - 선택 해제

5-3. 실무 팁:

  • 단일 선택 앱 : currentChanged 시그널만 사용
  • 다중 선택 앱 : selectionChanged 시그널 주로 사용
  • GUI 업데이트 : hasSelection(), currentIndex().isValid() 활용
  • 안전한 선택 : 항상 인덱스 유효성 검사 후 설정
728x90