PyQt5 QTreeViewのサンプル

PyQt5でツリーを表示するときに使えるQTableViewを使ってみました。



実装

QTreeViewでは親子関係のあるデータを表示できます。モデルで扱うデータはItemというクラスで、プロパティに単一の親parent_itemと子のリストchildrenを持たせて親子関係を実現しました。

class Item(object):
    def __init__(self, _parent=None):
        self._dict = {}
        self.parent_item = _parent
        self.children = []

そのほか変わったところといえば、アイテムを追加したときにIDのデータに親の行数を再帰で取得するという処理を入れて、Insertボタンをクリックしてアイテムを追加したときの見た目がわかりやすくなるようにしています。

        def recursion(item, _ids):
            _ids.append( str(item.row()) )
            if item == self.root_item:
                return
            return recursion(item.parent(), _ids)
        
        ids = []
        recursion(item, ids)
        item.setData( 'ID', '-'.join(ids[::-1][1:]) )

所感

QTableViewと比べると、モデルのindexやparentメソッドなどに親がいる場合や子がいる場合の処理などを加えなければいけないのでQTableView用に作成したモデルをそのまま使えない場合があるかもしれません。反対に、QTreeView用に作成したモデルはそのままQListViewやQTableViewでも使えると思います。したがって、QListView、QTableView、QTreeView に同じモデルを渡して同じ内容を表示したりもできます。

例えば部品表などの親子関係があるデータはエクセルで表を作ろうとすると見づらくなったりするので、QTreeViewを使うと見やすい表が作れるかもしれませんね。



ソースコード

main.py

import sys
from mainwindow import Ui_MainWindow
from treeview import Model, Delegate, Item
from PyQt5 import QtWidgets, QtCore
 
class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, app):
        super().__init__()
 
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
 
        self.model = Model(self)
        self.model.addColumns(['ID', 'Name'])
 
        self.ui.treeView.setModel(self.model)
        self.ui.treeView.setItemDelegate(Delegate())
        self.ui.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.ui.treeView.customContextMenuRequested.connect(self.contextMenu)
        self.ui.pushButton.clicked.connect(self.insertRow)
        self.ui.pushButton_2.clicked.connect(self.delItem)
 
    def contextMenu(self, point):
        self.menu = QtWidgets.QMenu(self)
        self.menu.addAction('Add', self.insertRow)
        self.menu.addAction('Delete', self.delItem)
        self.menu.exec_( self.focusWidget().mapToGlobal(point) )
 
    def insertRow(self):
        indexes = self.ui.treeView.selectedIndexes()
        
        if len(indexes) == 0:
            self.model.addItem()
            return
        
        indexes2 = []
        for index in indexes:
            if not index.row() in [ index2.row() for index2 in indexes2 if index2.parent() == index.parent() ]:
                indexes2.append(index)
        
        for index in indexes2:
            self.model.addItem(index)

    def delItem(self):
        indexes = self.ui.treeView.selectedIndexes()
        if len(indexes) == 0:
            return

        indexes2 = []
        for index in indexes:
            if not index.row() in [ index2.row() for index2 in indexes2 if index2.parent() == index.parent() ]:
                indexes2.append(index)
        
        for index in indexes2:
            self.model.removeItem(index)
 
def main():
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow(app)
    window.show()
    app.exec_()
 
if __name__ == '__main__':
    main()

treeview.py

# -*- coding: utf-8 -*-
from PyQt5 import QtWidgets, QtCore
 
class Item(object):
    def __init__(self, _parent=None):
        self._dict = {}
        self.parent_item = _parent
        self.children = []
    
    def appendChild(self, item):
        self.children.append(item)

    def data(self, column):
        if column in self._dict.keys():
            return self._dict[column]
        return ''
 
    def setData(self, column, data):
        self._dict[column] = data
    
    def child(self, row):
        return self.children[row]

    def childrenCount(self):
        return len(self.children)

    def parent(self):
        return self.parent_item

    def removeChild(self, row):
        del self.children[row]

    def row(self):
        if self.parent_item:
            return self.parent_item.children.index(self)
        return 0

class Model(QtCore.QAbstractItemModel):
    def __init__(self, parent_=None):
        super(Model, self).__init__(parent_)
        self.root_item = Item()
        self.root_item.setData('ID', 'root item')
        self.columns = []
 
    def addColumns(self, columns, parent=QtCore.QModelIndex()):
        self.beginInsertColumns(parent, self.columnCount(), self.columnCount() + len(columns) - 1)
        self.columns.extend(columns)
        self.endInsertColumns()
 
    def addItem(self, parent=QtCore.QModelIndex()):
        if parent == QtCore.QModelIndex():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()
        item = Item(parent_item)
        row = parent_item.childrenCount()
        self.beginInsertRows(parent, row, row)
        parent_item.children.insert( row, item )
        self.endInsertRows()
        
        def recursion(item, _ids):
            _ids.append( str(item.row()) )
            if item == self.root_item:
                return
            return recursion(item.parent(), _ids)
        
        ids = []
        recursion(item, ids)
        item.setData( 'ID', '-'.join(ids[::-1][1:]) )
        
    def column(self, key):
        return self.columns[key]
 
    def columnCount(self, parent=QtCore.QModelIndex()):
        return len(self.columns)
 
    def data(self, index, role):
        if not index.isValid():
            return QtCore.QVariant()
        if role == QtCore.Qt.EditRole or role == QtCore.Qt.DisplayRole:
            return index.internalPointer().data( self.column(index.column()) )
        return QtCore.QVariant()
 
    def flags(self, index):
        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
 
    def headerData(self, i, orientation, role):
        if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
            return self.columns[i]
        if orientation == QtCore.Qt.Vertical and role == QtCore.Qt.DisplayRole:
            return i + 1
 
    def index(self, row, column, parent):
        if not parent.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()

        if parent_item.childrenCount() > 0:
            return self.createIndex(row, column, parent_item.child(row))
        return QtCore.QModelIndex()

    def parent(self, index):
        if not index.isValid():
            return QtCore.QModelIndex()
        child_item = index.internalPointer()
        if not child_item:
            return QtCore.QModelIndex()
        parent_item = child_item.parent()
        if parent_item == self.root_item:
            return QtCore.QModelIndex()
        return self.createIndex(parent_item.row(), 0, parent_item)
        
    def removeItem(self, index):
        parent = index.parent()
        if parent == QtCore.QModelIndex():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()
        self.beginRemoveRows(parent, index.row(), index.row())
        parent_item.removeChild(index.row())
        self.endRemoveRows()
 
    def rowCount(self, parent=QtCore.QModelIndex()):
        if not parent.isValid():
            return self.root_item.childrenCount()
        return parent.internalPointer().childrenCount()

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        if role == QtCore.Qt.EditRole:
            index.internalPointer().setData( self.column(index.column()), value )
            return True
        return False
 
class Delegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, parent=None, setModelDataEvent=None):
        super(Delegate, self).__init__(parent)
        self.setModelDataEvent = setModelDataEvent
 
    def createEditor(self, parent, option, index):
        return QtWidgets.QLineEdit(parent)
 
    def setEditorData(self, editor, index):
        value = index.model().data(index, QtCore.Qt.DisplayRole)
        editor.setText(str(value))
 
    def setModelData(self, editor, model, index):
        model.setData(index, editor.text())
        if not self.setModelDataEvent is None:
            self.setModelDataEvent()

mainwindow.py

# -*- coding: utf-8 -*-

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(400, 500)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setObjectName("pushButton")
        self.horizontalLayout.addWidget(self.pushButton)
        self.pushButton_2 = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton_2.setObjectName("pushButton_2")
        self.horizontalLayout.addWidget(self.pushButton_2)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.treeView = QtWidgets.QTreeView(self.centralwidget)
        self.treeView.setAlternatingRowColors(True)
        self.treeView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.treeView.setObjectName("treeView")
        self.verticalLayout.addWidget(self.treeView)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 400, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.pushButton.setText(_translate("MainWindow", "Insert"))
        self.pushButton_2.setText(_translate("MainWindow", "Delete"))

mainwindow.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>500</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
       <widget class="QPushButton" name="pushButton">
        <property name="text">
         <string>Insert</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QPushButton" name="pushButton_2">
        <property name="text">
         <string>Delete</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
    <item>
     <widget class="QTreeView" name="treeView">
      <property name="alternatingRowColors">
       <bool>true</bool>
      </property>
      <property name="selectionMode">
       <enum>QAbstractItemView::ExtendedSelection</enum>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>400</width>
     <height>21</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

コメント

タイトルとURLをコピーしました