어플리케이션을 개발하다 보면 Tree 구조 형태의 데이터를 보여주어야 하는 일들이 있다. (예 : 파일 탐색기 등)

Qt에서는 이러한 Tree 구조 데이터를 보여줄 수 있는 수단으로 QTreeWidgetQTreeView라는 클래스를 제공한다.

그럼 이 둘의 차이점은 무엇이고, 언제 어떤 클래스를 사용해야 할까?

 

QTreeWidget과 QTreeView의 차이점

가장 큰 차이는, 데이터가 어디에 저장되는가데이터와 뷰의 분리가 가능한가 이다.

QTreeWidget은,

Qt 초기 버전에서 사용되던 클래스로, 데이터가 위젯 내에 존재한다. 사용자는 위젯 내에 존재하는 데이터에 대해 검색과 편집을 수행하고, 어느 시점엔가 변경된 내용을 원본 데이터에 기록하곤 했다.

  • 장점: 이 방식은 이해와 사용이 간단하다.
  • 단점: 크기가 큰 데이터 집합은 위젯 내에 데이터를 모두 보유하기 부담스러워지기 시작했고, 동일한 데이터를 둘 이상의 서로 다른 위젯에 출력하고자 하는 상황에는 적합하지 않았다. (같은 데이터를 동시에 두 화면 - 트리 형태로 보여주는 화면과, 테이블 형태로 보여주는 화면 - 에 보여주려고 하면 힘들었다. 데이터가 위젯에 각각 저장되어 있기 때문에 어느 한쪽에서 변경 시 두 화면 사이의 싱크를 맞춰주기가 힘들었음)

QTreeView는,

이러한 QTreeWidget의 단점을 보완하기 위해 만들어진 클래스로, MVC 패턴에서 영감을 받은 모델/뷰 아키텍처의 '뷰' 부분이다. 쉽게 말하면 데이터를 다루는 모델 부분과, 데이터를 화면에 출력하는 뷰 부분을 분리하여 운영하는 것이다.

데이터는 '모델 클래스' 한 곳에서 관리하고, 이 모델에 연결된 여러개의 '뷰 클래스'를 만듦으로써 동일한 데이터를 여러 화면에서 보여주는 것을 가능하게 했다. 따라서 '모델'에서만 데이터를 변경 하면, 여러개의 '뷰'에 동시에 반영이 될 수 있게 된 것이다.

 

또한, 모델에 아무리 큰 집합의 데이터가 있어도, 각 뷰에서는 자신이 보여줄 부분의 데이터만 가져와서 보여주면 되어 뷰의 성능이 획기적으로 향상되게 된다.

 

이러한 개념은 QListWidgetQListView, QTableWidgetQTableView에도 동일하게 적용된다. (각각 보여지는 형태가 리스트 형태인지, 테이블 형태인지만 달라질 뿐이다.)

QListWidget과 QTableWidget은 데이터를 위젯 내에 저장하고 / QListView와 QTableView는 데이터가 저장된 모델에 연동되어 화면을 보여줄 수 있는 뷰 클래스이다.

 

언제 어떤 클래스를 사용해야 할까?

QTreeWidget (또는 QListWidget, QTableWidget) 을 사용하는 경우

데이터 집합이 작고, 동시에 여러군데서 보여질 필요 없이, 간단히 구현이 필요할 때 사용한다.

데이터 저장/변경/화면에 표시가 하나의 위젯에서 이루어지므로 QTreeView에 비해 상대적으로 구현이 간단하다.

QTreeView (또는 QListView, QTableView) 를 사용하는 경우

QTreeWidget과 반대로, 데이터 집합이 큰 경우, 같은 데이터를 동시에 여러 뷰에서 보여주어야 하는 경우에 적합하다.

(개발 시 데이터를 다루는 '모델' 클래스를 만들고, 뷰와 연동시켜야 한다.)

델리게이트라는 개념을 통해 데이터가 View에서 보여지는 형태를 조금 더 세밀히 조정할 수도 있다.

 

어느 쪽이든 본인이 개발하는 소프트웨어에 가장 적합한 것을 선택하는 것이 현명한 방법이다.

다음번 포스팅에서는 QTreeWidget을 사용하는 방법에 대해 알아보도록 하겠다.

 

 

참고 : Qt4를 이용한 C++GUI 프로그래밍 제2판

 

트리뷰에 콤보박스를 추가해서 옵션을 선택하도록 하는 방법입니다.

  • 일부 아이템에만 우측에 콤보박스 추가
  • 버튼을 누르면 현재 선택된 아이템의 정보를 보여줌

결과 화면 : 

 

설명 : 

  • 파일에서 데이터를 읽어와 treeView에 아이템을 추가합니다.(한 row에 아이템 2개씩 추가)
    • 콤보박스가 없는 row : [정보가 있는 item, 빈 item] 두개 추가
    • 콤보박스가 있는 row : [정보가 있는 item, 콤보박스의 초기값을 가지는 item] 두개 추가
    • 콤보 박스를 추가 하고 싶은 아이템을 구분하기 위해, 데이터 파일에서 #count가 있으면 콤보 박스를 생성하게 했습니다.(콤보박스 초기값이 count)
  • ComboBox를 보여줄 수 있는 Delegate를 만듭니다.
  • column 1에 위의 combo box용 delegate를 설정해 줍니다.
    self.treeView.setItemDelegateForColumn(1, ComboDelegate(self, ["count", "1", "2", "3"]))​

전체 소스 코드 :

TreeView.py

from PyQt5.QtWidgets import QDialog, QTreeView, QStyledItemDelegate, QComboBox, QWidget, QApplication, \
    QStyleOptionComboBox, QVBoxLayout, QPushButton, QLabel, QItemDelegate, QStyle
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtCore import Qt, QVariant

class TreeViewDialog(QDialog):
    def __init__(self):
        QDialog.__init__(self)
        self.treeView = QTreeView(self)
        self.model = TreeModel()
        self.treeView.setModel(self.model)
        self.treeView.setHeaderHidden(True)
        self.treeView.setItemDelegateForColumn(1, ComboDelegate(self, ["count", "1", "2", "3"]))
        self.treeView.setCurrentIndex(self.model.index(0, 0))
        self.treeView.setColumnWidth(0, 300)
        self.treeView.expandAll()
        self.treeView.clicked.connect(self.treeViewClicked)

        self.button = QPushButton(self)
        self.button.setText("Show selected item")
        self.button.clicked.connect(self.onButtonClicked)
        self.label = QLabel(self)
        self.label.setText("...")

        vBox = QVBoxLayout(self)
        vBox.addWidget(self.treeView)
        vBox.addWidget(self.button)
        vBox.addWidget(self.label)
        self.setLayout(vBox)
        self.resize(500, 300)

    def onButtonClicked(self):
        indexes = self.treeView.selectedIndexes()
        txt = ''
        for index in indexes:
            txt += '{},'.format(index.data())
        self.label.setText(txt)

    def treeViewClicked(self, index):
        if not index.isValid():
            return
        print('row {}, col {}'.format(index.row(), index.column()))


class TreeModel(QStandardItemModel):
    def __init__(self):
        super(TreeModel, self).__init__()
        self.readData()

    def readData(self):
        rootParentItem = self.invisibleRootItem()
        roots = set()
        item = None
        with open('data.txt', 'r', encoding='utf-8') as f:
            lines = f.readlines()
        for line in lines:
            line = line.strip()
            subs = line.split(">")
            category = subs[0]
            if category not in roots:
                item = self.appendChild(category, rootParentItem, category)
                roots.add(category)
            if len(subs) > 1:
                name = subs[1]
                self.appendChild(name, item, name)

    def appendChild(self, title, parentItem, toolTip = None):
        item2 = None
        if '#count' in title:
            item2 = TreeItem('count', "")
            title = title.replace("#count", "")
        else:
            item2 = TreeItem('', '')
            item2.setEditable(False)
        item = TreeItem(title, toolTip)
        item.setData(title)
        item.setEditable(False)
        parentItem.appendRow([item, item2])
        return item


class TreeItem(QStandardItem):
    def __init__(self, text, toolTip):
        QStandardItem.__init__(self)
        self.setText(text)
        self.setToolTip(toolTip)


class ComboDelegate(QStyledItemDelegate):
    def __init__(self, owner, items):
        super(ComboDelegate, self).__init__(owner)
        self.items = items

    def createEditor(self, parent, option, index):
        self.editor = QComboBox(parent)
        self.editor.addItems(self.items)
        return self.editor

    def paint(self, painter, option, index):
        if index.data(Qt.DisplayRole) == '':	# 빈 아이템이면 콤보박스 안보임
            QStyledItemDelegate.paint(self, painter, option, index)
        else:	# 값이 있는 아이템이면 콤보박스 보임
            value = index.data(Qt.DisplayRole)
            style = QApplication.style()
            opt = QStyleOptionComboBox()
            opt.text = str(value)
            opt.rect = option.rect
            style.drawComplexControl(QStyle.CC_ComboBox, opt, painter)
            QStyledItemDelegate.paint(self, painter, option, index)

    def setEditorData(self, editor, index):
        value = index.data(Qt.DisplayRole)
        num = self.items.index(value)
        editor.setCurrentIndex(num)
        if index.column() == 1: 
            editor.showPopup()

    def setModelData(self, editor, model, index):
        value = editor.currentText()
        model.setData(index, QVariant(value),  Qt.DisplayRole)

    def updateEditorGeometry(self, editor, option, index):
        editor.setGeometry(option.rect)

if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    treeview = TreeViewDialog()
    treeview.show()
    sys.exit(app.exec_())

 

data.txt

Fruits>Apple#count
Fruits>Banana#count
Fruits>Grape
Vegetables>Tomato#count
Vegetables>Cucumber#count​

QTreeView의 아이템에 마우스를 올리면, 각 아이템 별로 다른 말풍선(툴팁)이 보여지도록 합니다.

결과 화면 :

 

소스 코드 :

  1. 다이얼로그에 QTreeView를 생성합니다.
  2. 모델을 생성해서, treeView에 연결합니다. (아래에서는 QStandardItemModel을 사용)
  3. 모델에 아이템을 추가할 때, QStandardItem을 상속받은 TreeItem 클래스를 정의해서 사용합니다.
    • TreeItem 생성 시, 인자로 아이템에 입력할 데이터와, 툴팁 정보를 받음
    • item에 입력받은 데이터와 툴팁을 설정함
# qt/treeview/TreeView.py

from PyQt5.QtWidgets import QDialog, QTreeView
from PyQt5.QtGui import QStandardItem, QStandardItemModel

class TreeViewDialog(QDialog):
    def __init__(self):
        QDialog.__init__(self)
        self.treeView = QTreeView(self)
        self.model = QStandardItemModel()
        self.treeView.setModel(self.model)
        self.treeView.setHeaderHidden(True)
        rootParentItem = self.model.invisibleRootItem()
        # Add first item and it's children
        item = self.appendChild("category 1", rootParentItem, 'tooltip 1')
        for i in range(3):
            self.appendChild('a {}'.format(i), item, 'a tooltip {}'.format(i))

        # Add second item and it's children
        item = self.appendChild("category 2", rootParentItem, 'tooltip 1')
        for i in range(3):
            self.appendChild('b {}'.format(i), item, 'b tooltip {}'.format(i))

        self.treeView.setCurrentIndex(self.model.index(0, 0))
        self.treeView.expandAll()
        self.resize(400,200)

    def appendChild(self, title, parentItem, toolTip = None):
        item = TreeItem(title, toolTip)
        item.setData(title)
        parentItem.appendRow(item)
        return item


class TreeItem(QStandardItem):
    def __init__(self, text, toolTip):
        QStandardItem.__init__(self)
        self.setText(text)
        self.setToolTip(toolTip)

 

  4. 위 다이얼로그 실행을 위한 메인 파일

# main.py
from qt.treeview.TreeView import *
import sys

def start():
    app = QApplication(sys.argv)
    treeview = TreeViewDialog()
    treeview.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    start()

+ Recent posts