Skip to content

Instantly share code, notes, and snippets.

@nbassler
Created April 29, 2018 15:03
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save nbassler/342fc56c42df27239fa5276b79fca8e6 to your computer and use it in GitHub Desktop.
Save nbassler/342fc56c42df27239fa5276b79fca8e6 to your computer and use it in GitHub Desktop.
PyQt5 TreeView with QAbstractItemModel
"""
Reworked code based on
http://trevorius.com/scrapbook/uncategorized/pyqt-custom-abstractitemmodel/
Adapted to Qt5 and fixed column/row bug.
TODO: handle changing data.
"""
import sys
from PyQt5 import QtCore, QtWidgets
class CustomNode(object):
def __init__(self, data):
self._data = data
if type(data) == tuple:
self._data = list(data)
if type(data) is str or not hasattr(data, '__getitem__'):
self._data = [data]
self._columncount = len(self._data)
self._children = []
self._parent = None
self._row = 0
def data(self, column):
if column >= 0 and column < len(self._data):
return self._data[column]
def columnCount(self):
return self._columncount
def childCount(self):
return len(self._children)
def child(self, row):
if row >= 0 and row < self.childCount():
return self._children[row]
def parent(self):
return self._parent
def row(self):
return self._row
def addChild(self, child):
child._parent = self
child._row = len(self._children)
self._children.append(child)
self._columncount = max(child.columnCount(), self._columncount)
class CustomModel(QtCore.QAbstractItemModel):
def __init__(self, nodes):
QtCore.QAbstractItemModel.__init__(self)
self._root = CustomNode(None)
for node in nodes:
self._root.addChild(node)
def rowCount(self, index):
if index.isValid():
return index.internalPointer().childCount()
return self._root.childCount()
def addChild(self, node, _parent):
if not _parent or not _parent.isValid():
parent = self._root
else:
parent = _parent.internalPointer()
parent.addChild(node)
def index(self, row, column, _parent=None):
if not _parent or not _parent.isValid():
parent = self._root
else:
parent = _parent.internalPointer()
if not QtCore.QAbstractItemModel.hasIndex(self, row, column, _parent):
return QtCore.QModelIndex()
child = parent.child(row)
if child:
return QtCore.QAbstractItemModel.createIndex(self, row, column, child)
else:
return QtCore.QModelIndex()
def parent(self, index):
if index.isValid():
p = index.internalPointer().parent()
if p:
return QtCore.QAbstractItemModel.createIndex(self, p.row(), 0, p)
return QtCore.QModelIndex()
def columnCount(self, index):
if index.isValid():
return index.internalPointer().columnCount()
return self._root.columnCount()
def data(self, index, role):
if not index.isValid():
return None
node = index.internalPointer()
if role == QtCore.Qt.DisplayRole:
return node.data(index.column())
return None
class MyTree():
"""
"""
def __init__(self):
self.items = []
# Set some random data:
for i in 'abc':
self.items.append(CustomNode(i))
self.items[-1].addChild(CustomNode(['d', 'e', 'f']))
self.items[-1].addChild(CustomNode(['g', 'h', 'i']))
self.tw = QtWidgets.QTreeView()
self.tw.setModel(CustomModel(self.items))
def add_data(self, data):
"""
TODO: how to insert data, and update tree.
"""
# self.items[-1].addChild(CustomNode(['1', '2', '3']))
# self.tw.setModel(CustomModel(self.items))
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
mytree = MyTree()
mytree.tw.show()
sys.exit(app.exec_())
@danieljfarrell
Copy link

I had a go at implementing the insert data method but ran into a bit of trouble. I'm doing exactly what the docs say but the QTreeView does not update after the insert. See here is you are interested, https://gist.github.com/danieljfarrell/6e94aa6f8c3c437d901fd15b7b931afb

@danieljfarrell
Copy link

# Get the model backing the tree widget
model = self.tw.model()
# Ask the model to generate an index for us. This is the index of the root item
rootIdx = model.index(0, 0, QtCore.QModelIndex())
# Pick an insert location for a new node. This should be based on some input. For example
# the location of a mouse click or the last element if we are appending. In this example
# I have hard coded a value.
position = 3
# Create a new node called and give it a name
new = CustomNode("new")
# Add a child node to the node which contains the value we want to display.
new.addChild(CustomNode(rowItems))

So far the we have created this:
# new
# |-- 1
# |---2
# |---3

# Now we need to insert this node into the model's tree.
# Don't quite understand all this Qt boilerplate...
model.beginInsertRows(rootIdx, position, position)
model.addChild(new, None)
model.endInsertRows()

# Now we have:
# + a
# + b
# + c
# + new
#   |-- 1
#   |--2
#   |--3

# This tells the view to redraw
model.layoutChanged.emit()
``

@hesmith1029
Copy link

Does this implementation handle drag and drop?

@Jojain
Copy link

Jojain commented Sep 11, 2021

Hello,
I have spent the whole afternoon studying your code and I still understand this :
How does the QModelIndex keep a reference to the underlying data.

For example if I want the retrieve the row = 2, col = 3 index, I would go for :

cm = CustomModel()
theQModelIndex = cm.index(2,3)
myData = cm.data(theQModelIndex, QtCore.Qt.DisplayRole)

And what I don't understand is here at the index.internalPointer()

    def data(self, index, role):
        if not index.isValid():
            return None
        node = index.internalPointer() # How does index.internalPointer() reference my node, I don't see anylink created before ???
        if role == QtCore.Qt.DisplayRole:
            return node.data(index.column())
        return None

It just seems like magic to me !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment