Skip to content

Instantly share code, notes, and snippets.

@danieljfarrell
Forked from nbassler/treeview_test.py
Last active April 26, 2022 02:36
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save danieljfarrell/6e94aa6f8c3c437d901fd15b7b931afb to your computer and use it in GitHub Desktop.
Save danieljfarrell/6e94aa6f8c3c437d901fd15b7b931afb 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, QtGui
from anytree import Node
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 appendData(self, rowItems):
"""
TODO: how to insert data, and update tree.
"""
print("appendData")
model = self.tw.model()
rootIdx = model.index(0, 0, QtCore.QModelIndex())
position = 3
new = CustomNode("new")
new.addChild(CustomNode(rowItems))
model.beginInsertRows(rootIdx, position, position)
model.addChild(new, None)
model.endInsertRows()
model.layoutChanged.emit() # must call this otherwise nope!
if __name__ == "__main__":
import sys
import quamash
import asyncio
# hooking into Qt Event loop with Quamash do I don't
# have to bother adding buttons to trigger the row
# insert.
app = QtWidgets.QApplication(sys.argv)
loop = quamash.QEventLoop(app)
asyncio.set_event_loop(loop)
loop.set_debug(True) # optional
app = QtWidgets.QApplication(sys.argv)
mytree = MyTree()
def doAddData(tree):
print("doAddData")
tree.appendData([1,2,3])
loop.call_later(1.0, doAddData, mytree)
mytree.tw.show()
with loop:
loop.run_forever()
@aeschcol
Copy link

aeschcol commented May 7, 2020

Hi,
I'm trying to understand your example. I don't understand how the index method can be called in the CustomModel class. How can I get the index of an item in a dictionary?
Thanks

@danieljfarrell
Copy link
Author

I cannot check this right now. In the end I abandoned using QTreeView for my application. I simply could not find a nice reusable way of using it! It is such a horrible API. I come from a Cocoa background the NSOutlineView is the equivalent, the API is actually understandable, https://developer.apple.com/documentation/appkit/nsoutlineview.

If you are using Python and just want a quick UI I would recommend taking a look at TraitsUI. They have a wrapper around QTreeView that is it ridiculously easy to use, https://github.com/enthought/traitsui/blob/master/examples/demo/Standard_Editors/TreeEditor_demo.py

@aeschcol
Copy link

aeschcol commented May 8, 2020

Thanks for your quick answer. Can you explain what Codeline 131 to 138 is for?

@danieljfarrell
Copy link
Author

It's a first attempt and getting a new node into the model object.

Get tree model

model = self.tw.model()

Ask Qt for the an index. This should be for the node you want to new data to be appended.

rootIdx = model.index(0, 0, QtCore.QModelIndex())

We can't just insert a value. All Values must live in a node. So create a node and add the values we want to insert to that as children,

new = CustomNode("new")
new.addChild(CustomNode(rowItems))

Follow the formaisied if telling Qt we are mutating the data structure,

model.beginInsertRows(rootIdx, position, position)
model.addChild(new, None)
model.endInsertRows()
model.layoutChanged.emit()  # must call this otherwise nope!

In the above I hardcoded position = 3. This is clearly wrong. position should be the index in the parent where you want to insert the new node.

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