メインコンテンツまでスキップ

QtCore.QAbstractItemModel を使用したカスタムモデルの作成

今回は、AbstractItemModel を使用してカスタム Model を作成してから
TreeView を構成するやり方について。

とりあえず長いですが全コードから。

import os.path
import sys

from PySide2.QtCore import QAbstractItemModel, QModelIndex, Qt
from PySide2.QtWidgets import QApplication, QDialog, QTreeView, QVBoxLayout, QMenu


class BaseItem(object):
def __init__(self, data=None, parent=None):

self.parentItem = parent
self.itemData = data
self.childItems = []

def appendChild(self, item):

self.childItems.append(item)

def removeChild(self, row):
self.childItems.pop(row)

def child(self, row):
if len(self.childItems) > row:
return self.childItems[row]
else:
return None

def childCount(self):
return len(self.childItems)

def columnCount(self):
return 1

def data(self, column):
if self.itemData is None:
return ""
return self.itemData['key']

def parent(self):
return self.parentItem

def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0

def clear(self):
self.childItems = []


class TreeItem(BaseItem):

def __init__(self, data, parent=None):
super(TreeItem, self).__init__(data=data, parent=parent)


class TreeModel(QAbstractItemModel):
def __init__(self, items=[], parent=None):
super(TreeModel, self).__init__(parent)

self.__items = items
self.rootItem = BaseItem()
# 現在のページ
self.setItems(items)

def setItems(self, items):

self.__items = items
self.setupModelData()

def addItem(self, parent, text):

item = parent.internalPointer()
self.beginInsertRows(parent, item.childCount(), item.childCount())
i = TreeItem(data={"key": text}, parent=item)
item.appendChild(i)
self.endInsertRows()

def removeItem(self, item):

parent = self.parent(item)
if parent.isValid():
pItem = parent.internalPointer()
self.beginRemoveRows(parent, item.row(), item.row())
pItem.removeChild(item.row())
self.endRemoveRows()

def columnCount(self, parent):

if parent.isValid():
return parent.internalPointer().columnCount()
else:
return self.rootItem.columnCount()

def data(self, index, role):

if not index.isValid():
return None
if role != Qt.DisplayRole:
return None
item = index.internalPointer()
return item.data(index.column())

def flags(self, index):

if not index.isValid():
return Qt.NoItemFlags
return Qt.ItemIsEnabled | Qt.ItemIsSelectable

def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.rootItem.data(section)
return None

def index(self, row, column, parent):

if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QModelIndex()

def parent(self, index):

if not index.isValid():
return QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)

def rowCount(self, parent):

if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return parentItem.childCount()

def setupModelData(self):
"""
表示用のItemを再構築する
"""
self.rootItem.clear()
parents = {}
self.beginResetModel()
for item in self.__items:
if item['parent'] in parents:
p = parents[item['parent']]
else:
p = TreeItem(item, self.rootItem)
self.rootItem.appendChild(p)
parents[item['parent']] = p
treeItem = TreeItem(item, p)
p.appendChild(treeItem)
self.endResetModel()


class UISample(QDialog):

def __init__(self, parent=None):
super(UISample, self).__init__(parent)

layout = QVBoxLayout()
# カスタムUIを作成
self.view = QTreeView()
layout.addWidget(self.view)

# てきとうにListに表示するItemの配列を作る
data = []
for i in range(5):
data.append({'parent': 'hogehoge', 'key': 'homuhomu_' + str(i).zfill(3)})
for i in range(5):
data.append({'parent': 'fugafuga', 'key': 'homuhomu_' + str(i).zfill(3)})

self.model = TreeModel(data)
self.view.setModel(self.model)

self.setLayout(layout)

self.view.setContextMenuPolicy(Qt.CustomContextMenu)
self.view.customContextMenuRequested.connect(self.listContext)

def listContext(self, pos):

menu = QMenu(self.view)
add = menu.addAction("Add")
remove = menu.addAction("Remove")
add.triggered.connect(self.addItem)
remove.triggered.connect(self.removeItem)
menu.exec_(self.view.mapToGlobal(pos))

def addItem(self):

index = self.view.currentIndex()
self.model.addItem(index, "HOGEHOGE")

def removeItem(self):

index = self.view.currentIndex()
self.model.removeItem(index)


if __name__ == '__main__':
app = QApplication(sys.argv)
a = UISample()
a.show()
sys.exit(app.exec_())

長いですか、いくつか要点をまとめ。

データ構造を作る

まず、Model を作成する場合は、指定の構造になるように Item のツリーを
クラスオブジェクトで構成します。
指定の構造は ↑ の図の通り。
今回は TreeModel で説明しますが、List でも Model でも基本は同じです。
(むしろ Tree が一番面倒くさい)

TreeView の場合はこうなります。
今回のサンプル TreeItem ですが、オブジェクトに childItemsparentItem
変数を持ちます。

    def setupModelData(self):
"""
表示用のItemを再構築する
"""
self.rootItem.clear()
parents = {}
for item in self.__items:
if item['parent'] in parents:
p = parents[item['parent']]
else:
p = TreeItem(item, self.rootItem)
self.rootItem.appendChild(p)
parents[item['parent']] = p
treeItem = TreeItem(item, p)
p.appendChild(treeItem)
self.layoutChanged.emit()

そのツリー構造を作成しているのが「setupModelData」関数です。
Item を作成するときに親ノードにあたる Item を指定し、
オブジェクトを作成したら appendChild で親ノードの子に Object をセットします。

トップには RootItem を作成し、Root は親が「None」になるようにします。
構造ができたら、この Item ツリーをパースする機能をクラスに実装していきます。

関数の実装

必須な Virtual 関数

QtCore.QAbstractItemModel を使用して Model を作成する場合、最低限実装する必要がある
関数があります。
それが、

  1. columnCount
  2. rowCount
  3. parent
  4. index

この 4 つになります。

column/rowCount について

まず、Count について。
これは名前の通り現在の親 Index(引数で受け取る)の子がいくつあるかを return するようにします。
TreeView の場合は、Tree ごとにこの Count が呼ばれ
その Count 分 Item を表示します。

parent について

    def parent(self, index):

if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)

parent 関数は、引数の index の親にあたる ModelIndex を return するようにします。

ここで重要になるのが ModelIndex について。
PySide の Model は実体を持たずに、あくまでも Index で親子関係を持ちます。
実際に表示する Item は「ModelIndex.internalPointer()」に書いてある通り
実体までのポインタ を、ModelIndex が保持します。

この ModelIndex を作成しているのが self.createIndex 関数で
createIndex(row,column,実体のオブジェクト) を渡すことで ModelIndex を生成してくれます。

なので、この parent 関数がなにをしているかというと

  1. 親を取得したい Index から、Item の実体を取得
  2. 実体の TreeItem の parent を使用して親の実体を取得
  3. 親がない= Root の場合は空の Index を返す
  4. 親がある場合は createIndex を使用して親の ModelIndex を返す

このような挙動をしています。

return QtCore.QModelIndex()

こうしている部分は index.isValud() の判定で無効扱いになります。

index について

基本は parent と同様ですが、こちらは parent から指定の row/column の ModelIndex を取得します。

    def index(self, row, column, parent):

if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()

parentItem の実体の TreeItem オブジェクトを取得し、
child を取得、そしてその Child の実体と row,column から
createIndex を使用して ModelIndex を作成して返します。

実際の表示をどうするかを指定する

index と parent を実装することで、最初に作成した Item のツリーをパースする構造はできました。
パースはできましたが、じゃあ実際に「なにを表示するのか」は data 関数を実装することで
指定をします。

    def data(self, index, role):

if not index.isValid():
return None
if role != QtCore.Qt.DisplayRole:
return None
item = index.internalPointer()
return item.data(index.column())

data は、表示する場所の index と role とよばれる「ふるまい方」を受け取って処理をします。
この role は、View に関係するいろいろな情報の受け取り口になっています。
例えば「背景の色」であったり、「文字の色」であったり
表示する文字であったり、あるいは自分で指定したり。
その受け取り口の「今回はなにがほしいのか」が入るのが role になります。

今回のように文字を取得したい場合は、QtCore.Qt.DisplayRole が role に入るので
それ以外は None で終了、DisplayRole の場合は Item の data で指定した文字列が返ります。

あとは必要に応じて flags や Header を指定したりすれば OK です。
(この2つは見ての通りなので今回は説明スキップ)

重要なのは ModelIndex のふるまいと構造について。
Tree 構造をオブジェクトで作成し、internalPointer で実体にアクセスしながら
親の ModelIndex、子の ModelIndex を作って
data で最終的な出力形式を指定する...
これを押さえておけば、TreeView で ListView でも、TableView でも
同じ考え方で Model を作成することができます。

参考