I'm trying to just delete the QTreeView item "Node 6 (delete me)" from within the model.
I can't get it to work, as nothing is deleted. What am I doing wrong?
See the MainWindow.init method for the removal code.
import sys
try:
from PySide2 import QtWidgets
except ImportError:
from PyQt5 import QtWidgets
try:
from PySide2 import QtCore
except ImportError:
from PyQt5 import QtCore
class Node(object):
def __init__(self, name, parent=None):
self._name = name
self._children = []
self._parent = parent
if parent is not None:
parent.addChild(self)
def addChild(self, child):
self._children.append(child)
child._parent = self
def name(self):
return self._name
def child(self, row):
return self._children[row]
def insertChild(self, position, child):
if position < 0 or position > len(self._children):
return False
self._children.insert(position, child)
child._parent = self
return True
def childCount(self):
return len(self._children)
def parent(self):
return self._parent
def setParent(self, new_parent):
self._parent._children.remove(self)
self._parent = new_parent
new_parent._children.append(self)
def row(self):
if self._parent is not None:
return self._parent._children.index(self)
def removeChild(self, position):
if position < 0 or position > len(self._children):
return False
child = self._children.pop(position)
child._parent = None
return True
def __repr__(self):
return self._name
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, root, parent=None):
super(TreeModel, self).__init__(parent)
self._rootNode = root
def rowCount(self, parent=QtCore.QModelIndex()):
if not parent.isValid():
parentNode = self._rootNode
else:
parentNode = parent.internalPointer()
return parentNode.childCount()
def columnCount(self, parent):
return 1
def data(self, index, role):
"""Return whatever the view should display"""
if not index.isValid():
return None
node = index.internalPointer()
if role == QtCore.Qt.DisplayRole:
if index.column() == 0:
return node.name()
def index(self, row, column, parent):
if not parent.isValid():
# parent is not valid when it is the root node, since the "parent"
# method returns an empty QModelIndex
parentNode = self._rootNode
else:
parentNode = parent.internalPointer() # the node
childItem = parentNode.child(row)
return self.createIndex(row, column, childItem)
def parent(self, index):
node = index.internalPointer()
parentNode = node.parent()
if parentNode == self._rootNode:
return QtCore.QModelIndex()
return self.createIndex(parentNode.row(), 0, parentNode)
def flags(self, index):
# Original, inherited flags:
original_flags = super(TreeModel, self).flags(index)
return (original_flags | QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled
| QtCore.Qt.ItemIsDropEnabled)
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if section == 0:
return 'Node name'
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.tree = QtWidgets.QTreeView()
self.layout = QtWidgets.QGridLayout()
self.main_widget = QtWidgets.QWidget()
self.layout.addWidget(self.tree, 0, 0)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
# Note how these are added in a non-sorted fashion
root_node = Node('Hidden root')
Node(name='Node 3', parent=root_node) # add using parent param
n2 = Node(name='Node 2', parent=root_node) # add using parent param
Node(name='Node 1', parent=root_node) # add using parent param
n2.addChild(Node(name='Node 5')) # add using addChild
n2.addChild(Node(name='Node 6 (delete me)')) # add using addChild
n2.addChild(Node(name='Node 4')) # add using addChild
model = TreeModel(root=root_node)
self.tree.setModel(model)
self.tree.expandAll()
# Attempt to remove a node from within the model
node2 = model.index(1, 0, QtCore.QModelIndex()) # "Node 2" index
index = model.index(1, 0, parent=node2)
index_node = index.internalPointer()
print('index represents node "%s" (its parent node is "%s")' %
(index_node.name(), index.parent().internalPointer().name()))
print('Removing %s on row %s' % (index_node.name(), index.row()))
model.beginRemoveRows(index.parent(), index.row(), index.row())
success = model.removeRow(index.row(), parent=index.parent())
print('Removal was a succes?:', success)
model.endRemoveRows()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
#Jaa-c if you like, take this and copy-paste it into an answer from yourself and I'll mark it as the accepted answer.
import sys
try:
from PySide2 import QtWidgets
except ImportError:
from PyQt5 import QtWidgets
try:
from PySide2 import QtCore
except ImportError:
from PyQt5 import QtCore
class Node(object):
def __init__(self, name, parent=None):
self._name = name
self._children = []
self._parent = parent
if parent is not None:
parent.addChild(self)
def addChild(self, child):
self._children.append(child)
child._parent = self
def name(self):
return self._name
def child(self, row):
return self._children[row]
def insertChild(self, position, child):
if position < 0 or position > len(self._children):
return False
self._children.insert(position, child)
child._parent = self
return True
def childCount(self):
return len(self._children)
def parent(self):
return self._parent
def setParent(self, new_parent):
self._parent._children.remove(self)
self._parent = new_parent
new_parent._children.append(self)
def row(self):
if self._parent is not None:
return self._parent._children.index(self)
def removeChild(self, position):
if position < 0 or position > len(self._children):
return False
child = self._children.pop(position)
child._parent = None
return True
def __repr__(self):
return self._name
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, root, parent=None):
super(TreeModel, self).__init__(parent)
self._rootNode = root
def rowCount(self, parent=QtCore.QModelIndex()):
if not parent.isValid():
parentNode = self._rootNode
else:
parentNode = parent.internalPointer()
return parentNode.childCount()
def columnCount(self, parent):
return 1
def data(self, index, role):
"""Return whatever the view should display"""
if not index.isValid():
return None
node = index.internalPointer()
if role == QtCore.Qt.DisplayRole:
if index.column() == 0:
return node.name()
def index(self, row, column, parent):
if not parent.isValid():
# parent is not valid when it is the root node, since the "parent"
# method returns an empty QModelIndex
parentNode = self._rootNode
else:
parentNode = parent.internalPointer() # the node
childItem = parentNode.child(row)
return self.createIndex(row, column, childItem)
def parent(self, index):
node = index.internalPointer()
parentNode = node.parent()
if parentNode == self._rootNode:
return QtCore.QModelIndex()
return self.createIndex(parentNode.row(), 0, parentNode)
def flags(self, index):
# Original, inherited flags:
original_flags = super(TreeModel, self).flags(index)
return (original_flags | QtCore.Qt.ItemIsEnabled
| QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled
| QtCore.Qt.ItemIsDropEnabled)
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if section == 0:
return 'Node name'
def removeRow(self, row, parent):
if not parent.isValid():
# parent is not valid when it is the root node, since the "parent"
# method returns an empty QModelIndex
parentNode = self._rootNode
else:
parentNode = parent.internalPointer() # the node
parentNode.removeChild(row)
return True
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.tree = QtWidgets.QTreeView()
self.layout = QtWidgets.QGridLayout()
self.main_widget = QtWidgets.QWidget()
self.layout.addWidget(self.tree, 0, 0)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
# Note how these are added in a non-sorted fashion
root_node = Node('Hidden root')
Node(name='Node 3', parent=root_node) # add using parent param
n2 = Node(name='Node 2', parent=root_node) # add using parent param
Node(name='Node 1', parent=root_node) # add using parent param
n2.addChild(Node(name='Node 5')) # add using addChild
n2.addChild(Node(name='Node 6 (delete me)')) # add using addChild
n2.addChild(Node(name='Node 4')) # add using addChild
model = TreeModel(root=root_node)
self.tree.setModel(model)
self.tree.expandAll()
# Attempt to remove a node from within the model
node2 = model.index(1, 0, QtCore.QModelIndex()) # "Node 2" index
index = model.index(1, 0, parent=node2)
index_node = index.internalPointer()
print('index represents node "%s" (its parent node is "%s")' %
(index_node.name(), index.parent().internalPointer().name()))
print('Removing %s on row %s' % (index_node.name(), index.row()))
model.beginRemoveRows(index.parent(), index.row(), index.row())
success = model.removeRow(index.row(), parent=index.parent())
print('Removal was a succes?:', success)
model.endRemoveRows()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
Related
I'm currently struggling with QTreeView. Even though I called header().setVisible(True) the header is still not visible. header().isVisible() returns False. headerHidden is already set to False in Qt Designer.
I'm using Python 3.10 and latest PySide2 5.15.2.1 on Windows 10.
Any ideas what is wrong?
An example for a performant implementation of a sortable QTreeView usage in Python would be very appreciated.
class Node(object):
def __init__(self, path, parent=None):
self._parent = parent
self._path = path
self._children = []
def children(self):
return self._children
def hasChildren(self):
return bool(self.children())
def columnCount(self):
return 3
def child_count(self):
return len(self._children)
def add_child(self, child) -> 'Node':
self._children.append(child)
child._parent = self
return self
def child(self, row):
if 0 <= row < self.child_count():
return self._children[row]
def row(self):
if self._parent is not None:
return self._parent._children.index(self)
return -1
class MyModel(QAbstractItemModel):
def __init__(self, parent=None):
super().__init__(parent)
self._rootNode: Node = Node("Root", None)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if orientation == Qt.Horizontal:
if role == Qt.DisplayRole:
return ("File", "Line", "Text")[section]
def index(self, row, column, parent=QModelIndex()):
if not self.hasIndex(row, column, parent):
return QModelIndex()
node = parent.internalPointer() if parent.isValid() else self._rootNode
if node.children:
return self.createIndex(row, column, node.child(row))
return QModelIndex()
def parent(self, child):
if not child.isValid():
return QModelIndex()
node: Node = child.internalPointer()
if node.row() >= 0:
return self.createIndex(node.row(), 0, node.parent())
return QModelIndex()
def rowCount(self, parent=QModelIndex()):
node = parent.internalPointer() if parent.isValid() else self._rootNode
return node.child_count()
def columnCount(self, parent=QModelIndex()):
return 3
def hasChildren(self, parent=QModelIndex()):
node = parent.internalPointer() if parent.isValid() else self._rootNode
return node.hasChildren()
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return None
if role == Qt.DisplayRole:
node: Node = index.internalPointer()
if index.column() == 0:
return node.path
return "column"
def setData(self, index, value, role=Qt.EditRole):
return False
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
def appendRow(self, item, parent=None):
pos = self.rowCount(self.indexFromItem(parent))
self.beginInsertRows(self.indexFromItem(parent), pos, pos)
if parent is None:
parent = self._rootNode.add_child(item)
else:
parent.add_child(item)
self.endInsertRows()
return parent
class MyTreeView(QTreeView):
def __init__(self, _model, parent=None):
super().__init__(parent)
self._model = MyModel(self)
self.setModel(self._model)
parent = None
for i in range(1000):
parent = self._model.appendRow(Node(f"Path{i}", parent)
if i % 5 == 0:
parent = None
self.header().setVisible(True)
print(self.header().isVisible()) # <-- Outputs False
I'm trying to add custom widget (image below) as item delegate in QTreeview. I do see the size of the treeItem getting bigger but my widget does not show up. Not sure what am I missing here. Thanks in advance for all your suggestions.
Custom Widget
Here is the code
ItemNode (node for QAbstractTeeModel)
class BaseNode:
def __init__(self, data, parent=None):
self._data = data
self._children = list()
self._parent = None
if parent:
parent.addChild(self)
def addChild(self, node):
self._children.append(node)
node._parent = self
def insertChildren(self, position, children):
if position < 0:
return False
for child in children:
child._parent = self
self._children.insert(position, child)
return True
def removeChildren(self, position, count):
if position < 0 or position > len(self._children):
return False
for i in range(count):
child = self._children.pop(position)
child._parent = None
return True
def child(self, row):
if row < self.rowCount():
return self._children[row]
def rowCount(self):
return len(self._children)
def columnCount(self):
return len(self._data)
def parent(self):
return self._parent
def index(self):
if self._parent:
return self._parent._children.index(self)
return 0
def data(self, column):
return self._data[column]
QAbstractItemModel
class NodeModel(QtCore.QAbstractItemModel):
def __init__(self, parent=None):
super().__init__(parent)
self.headers = ["Node"]
self.root_node = BaseNode(["root"])
def getNode(self, index):
if index.isValid():
node = index.internalPointer()
if node:
return node
return self.root_node
def rowCount(self, parent=QtCore.QModelIndex()):
return self.root_node.rowCount()
def columnCount(self, parent=QtCore.QModelIndex()):
return self.root_node.columnCount()
def data(self, index, role):
if not index.isValid():
return
node = self.getNode(index)
if role == QtCore.Qt.EditRole or role == QtCore.Qt.EditRole:
return node
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.headers[section]
def flags(self, index):
flgs = 0
if index.isValid():
flgs = QtCore.Qt.ItemIsEditable
flgs |= QtCore.Qt.ItemIsEnabled
flgs |= QtCore.Qt.DisplayRole
return flgs
def parent(self, child):
node = self.getNode(child)
parent = node.parent()
if parent == self.root_node:
return QtCore.QModelIndex()
return self.createIndex(parent.index(), 0, parent)
def index(self, row, column, parent):
parent = self.getNode(parent)
child = parent.child(row)
if child:
return self.createIndex(row, column, child)
return QtCore.QModelIndex()
def insertRows(self, row, count, parent=QtCore.QModelIndex(), children=None):
parent_node = self.getNode(parent)
self.beginInsertRows(parent, row, row + count - 1)
result = parent_node.insertChildren(row, children)
self.endInsertRows()
return result
def removeRows(self, row, count, parent=QtCore.QModelIndex()):
parent_node = self.getNode(parent)
self.beginRemoveRows(parent, row, row + count - 1)
success = parent_node.removeChildren(row, count)
self.endRemoveRows()
return success
ItemEditor (QWidget for Item Delegate
class ItemEditor(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_Form() # this is a UI containing
self.ui.setupUi(self)
self.setAutoFillBackground(True)
Item Delegate
class NodeDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def createEditor(self, parent, option, index):
if index.column() == 0:
editor = ItemEditor()
return editor
else:
return super().createEditor(self, parent, option, index)
def setEditorData(self, editor, index):
if index.column() == 0:
pass
else:
return super().setEditorData(editor, index)
def setModelData(self, editor, model, index):
if index.column() == 0:
pass
else:
super().setModelData(editor, model, index)
def sizeHint(self, option, index):
editor = ItemEditor()
return editor.sizeHint()
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
Implementation in TreeView
app = QtWidgets.QApplication([])
tw = QtWidgets.QTreeView()
model = NodeModel()
tw.setModel(model)
for i in "abcd":
print(i)
node = BaseNode(data=[i])
index = model.index(model.root_node.rowCount(), 0, QtCore.QModelIndex())
model.insertRows(1, 1, index, children=[node])
item_delegate = NodeDelegate()
tw.setItemDelegateForRow(0, item_delegate)
tw.show()
app.exec_()
In the code example below I populate an item model with a set of top level items which contain key-value properties that I can view and edit with a QTreeView.
Looking at the Qt Documentation for QAbstractItemModel::columnCount it says that this should return the number of columns for the children of the given parent, meaning that this should be a hierarchical dependent property.
However, using the code below, if I return the column count as a hierarchical dependent property (in this case root->children have 1 column, root->child->children have 2 columns) then the view will only display 1 column.
Printing node.columnCount() (see the code) will actually show that the Item class nodes do in fact return columnCount = 2 after you expand one of the items.
If I just always return 2 for the model.columnCount function, then the view will properly display both columns.
Is this required to always return the desired number of columns in a view no matter the hierarchy or am I just doing something wrong and if so what? Returning a number of columns for a parent whose children have a different number of columns just to make the view work properly feels like it must be wrong.
import sys
import typing
from PyQt5 import QtCore, QtWidgets
class Node:
def __init__(self, parent=None):
self.parent = parent # type: Node
self.name : str
def children(self) -> list:
return None
def hasChildren(self):
return bool(self.children())
def getData(self, index: QtCore.QModelIndex):
if index.column() == 0:
return self.name
def setData(self, val, index: QtCore.QModelIndex):
if index.column() == 0:
self.name = val
def columnCount(self):
return 1
def rowCount(self):
children = self.children()
return 0 if not children else len(children)
def flags(self, index: QtCore.QModelIndex):
if index.column() == 0:
return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
else:
return QtCore.Qt.NoItemFlags
class Property(Node):
def __init__(self, parent, label, value):
super().__init__(parent)
self.label = label
self.value = value
def getData(self, index: QtCore.QModelIndex):
col = index.column()
if col == 0:
return self.label
elif col == 1:
return self.value
def setData(self, val, index: QtCore.QModelIndex):
if index.column() == 1:
self.value = val
def flags(self, index: QtCore.QModelIndex):
col = index.column()
if col == 0:
return QtCore.Qt.ItemIsEnabled
elif col == 1:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEditable
class Item(Node):
def __init__(self, parent):
super().__init__(parent)
self.name = 'Item'
self.p1 = Property(self, 'string', 'text')
self.p2 = Property(self, 'float', 1.2)
def children(self):
return [self.p1, self.p2]
def columnCount(self):
return 2
class Root(Node):
def __init__(self):
super().__init__(parent=None)
self._children = list()
def children(self):
return self._children
class Model(QtCore.QAbstractItemModel):
def __init__(self):
super().__init__()
self.root = Root()
def index(self, row: int, column: int, parent: QtCore.QModelIndex = ...) -> QtCore.QModelIndex:
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
node = parent.internalPointer() if parent.isValid() else self.root
if node.children:
return self.createIndex(row, column, node.children()[row])
else:
return QtCore.QModelIndex()
def parent(self, child: QtCore.QModelIndex) -> QtCore.QModelIndex:
if not child.isValid():
return QtCore.QModelIndex()
node = child.internalPointer() # type: Node
if node.parent and node.parent.parent:
row = node.parent.parent.children().index(node.parent)
return self.createIndex(row, 0, node.parent)
else:
return QtCore.QModelIndex()
def rowCount(self, parent: QtCore.QModelIndex = ...) -> int:
node = parent.internalPointer() if parent.isValid() else self.root
children = node.children()
return len(children) if children else 0
def columnCount(self, parent: QtCore.QModelIndex = ...) -> int:
node = parent.internalPointer() if parent.isValid() else self.root
print(f'{node.__class__.__name__} column count: ', node.columnCount()) # shows that column count 2 is returned, when items are expanded
# return 2 # 2nd column only shows up if I just always return 2
return node.columnCount() # view only shows 1 columns
def hasChildren(self, parent: QtCore.QModelIndex = ...) -> bool:
node = parent.internalPointer() if parent.isValid() else self.root
return node.hasChildren()
def data(self, index: QtCore.QModelIndex, role: int = ...):
if index.isValid() and role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
node = index.internalPointer() # type: Node
return node.getData(index)
else:
return None
def setData(self, index: QtCore.QModelIndex, value: typing.Any, role: int = ...) -> bool:
if role in (QtCore.Qt.EditRole,):
node = index.internalPointer() # type: Node
node.setData(value, index)
self.dataChanged.emit(index, index)
return True
else:
return False
def flags(self, index: QtCore.QModelIndex):
node = index.internalPointer() if index.isValid() else self.root
return node.flags(index)
def appendRow(self, item):
row = len(self.root.children())
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.root.children().append(item)
self.endInsertRows()
class TreeView(QtWidgets.QTreeView):
def __init__(self, parent=None):
super(TreeView, self).__init__(parent)
self._model = Model()
self.setModel(self._model)
self.setSelectionMode(self.ExtendedSelection)
# self.setDropIndicatorShown(False)
self.setEditTriggers(self.DoubleClicked | self.SelectedClicked | self.EditKeyPressed)
def model(self) -> Model:
return self._model
sys.excepthook = sys.__excepthook__
app = QtWidgets.QApplication(sys.argv)
widget = TreeView()
model = widget.model()
for i in range(2):
model.appendRow(Item(model.root))
widget.show()
widget.setAttribute(QtCore.Qt.WA_DeleteOnClose)
sys.exit(app.exec_())
It seems that the docs is not clear and does not exactly match the implementation, in the implementation the number of columns in the view is dependent on the horizontal QHeaderView, and the horizontal QHeaderView uses the number of columns of the root that is the invisible item, ie the number of columns should be given by Root(), and since Root() does not overwrite the columnCount() it will have the value of 1 by default (although for me the columnCount() of Node must be 0 and children() must return a empty list), so the solution is set to 2 in Root columnCount().
import sys
import typing
from PyQt5 import QtCore, QtWidgets
class Node:
def __init__(self, parent=None):
self.parent = parent # type: Node
self.name : str
def children(self) -> list:
return list()
def hasChildren(self):
return bool(self.children())
def getData(self, index: QtCore.QModelIndex):
if index.column() == 0:
return self.name
def setData(self, val, index: QtCore.QModelIndex):
if index.column() == 0:
self.name = val
def columnCount(self):
return 0
def rowCount(self):
children = self.children()
return len(children)
def flags(self, index: QtCore.QModelIndex):
if index.column() == 0:
return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable)
else:
return QtCore.Qt.NoItemFlags
class Property(Node):
def __init__(self, parent, label, value):
super().__init__(parent)
self.label = label
self.value = value
def getData(self, index: QtCore.QModelIndex):
col = index.column()
if col == 0:
return self.label
elif col == 1:
return self.value
def setData(self, val, index: QtCore.QModelIndex):
if index.column() == 1:
self.value = val
def flags(self, index: QtCore.QModelIndex):
col = index.column()
if col == 0:
return QtCore.Qt.ItemIsEnabled
elif col == 1:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEditable
def columnCount(self):
return 1
class Item(Node):
def __init__(self, parent):
super().__init__(parent)
self.name = 'Item'
self.p1 = Property(self, 'string', 'text')
self.p2 = Property(self, 'float', 1.2)
def children(self):
return [self.p1, self.p2]
def columnCount(self):
return 2
class Root(Node):
def __init__(self):
super().__init__(parent=None)
self._children = list()
def children(self):
return self._children
def columnCount(self):
return 2
class Model(QtCore.QAbstractItemModel):
def __init__(self):
super().__init__()
self.root = Root()
def index(self, row: int, column: int, parent: QtCore.QModelIndex = ...) -> QtCore.QModelIndex:
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
node = parent.internalPointer() if parent.isValid() else self.root
if node.children:
return self.createIndex(row, column, node.children()[row])
else:
return QtCore.QModelIndex()
def parent(self, child: QtCore.QModelIndex) -> QtCore.QModelIndex:
if not child.isValid():
return QtCore.QModelIndex()
node = child.internalPointer() # type: Node
if node.parent and node.parent.parent:
row = node.parent.parent.children().index(node.parent)
return self.createIndex(row, 0, node.parent)
else:
return QtCore.QModelIndex()
def rowCount(self, parent: QtCore.QModelIndex = ...) -> int:
node = parent.internalPointer() if parent.isValid() else self.root
children = node.children()
return len(children) if children else 0
def columnCount(self, parent: QtCore.QModelIndex = ...) -> int:
node = parent.internalPointer() if parent.isValid() else self.root
print(f'{node.__class__.__name__} column count: ', node.columnCount()) # shows that column count 2 is returned, when items are expanded
# return 2 # 2nd column only shows up if I just always return 2
return node.columnCount() # view only shows 1 columns
def hasChildren(self, parent: QtCore.QModelIndex = ...) -> bool:
node = parent.internalPointer() if parent.isValid() else self.root
return node.hasChildren()
def data(self, index: QtCore.QModelIndex, role: int = ...):
if index.isValid() and role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
node = index.internalPointer() # type: Node
return node.getData(index)
else:
return None
def setData(self, index: QtCore.QModelIndex, value: typing.Any, role: int = ...) -> bool:
if role in (QtCore.Qt.EditRole,):
node = index.internalPointer() # type: Node
node.setData(value, index)
self.dataChanged.emit(index, index)
return True
else:
return False
def flags(self, index: QtCore.QModelIndex):
node = index.internalPointer() if index.isValid() else self.root
return node.flags(index)
def appendRow(self, item):
row = len(self.root.children())
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.root.children().append(item)
self.endInsertRows()
class TreeView(QtWidgets.QTreeView):
def __init__(self, parent=None):
super(TreeView, self).__init__(parent)
self._model = Model()
self.setModel(self._model)
self.setSelectionMode(self.ExtendedSelection)
# self.setDropIndicatorShown(False)
self.setEditTriggers(self.DoubleClicked | self.SelectedClicked | self.EditKeyPressed)
def model(self) -> Model:
return self._model
sys.excepthook = sys.__excepthook__
app = QtWidgets.QApplication(sys.argv)
widget = TreeView()
model = widget.model()
for i in range(2):
model.appendRow(Item(model.root))
widget.show()
widget.setAttribute(QtCore.Qt.WA_DeleteOnClose)
sys.exit(app.exec_())
As #eyllanesc answered, the number of columns in the View is only affected by the root item. However, columnCount() will have an effect on rows that give a number that is less than this number, in that columns less than this number will not be populated.
Taking the above code:
class Root(Node):
...
def columnCount():
return 2
class Property(Node):
...
def columnCount():
return 1
In this case even though two columns are shown, the Property nodes will not show data for the second column because the view is being told by columnCount() that there is only 1 column
I am working on a simple tree directory explorer based on a qtreeview with a model view/controller/implementation. I need to use some threads that recursively search the sub-folders and feed the model/datas of the qtreeview. All of this works fine.
But my issue is that the view doesnt refresh when the datas change ...
I have tried a few different things, but iam not happy with any of the solutions:
QtGui.QStandardItemModel.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
Emitting data change from setData() of the model should update the view, but it doesnt work for me. Also i havnt found an elegant way of finding the qmodelIndex. Iam just recusively loop all the datas to find the right Index.
from PyQt4 import QtCore, QtGui
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import time
import traceback, sys, os
from glob import glob
from random import randrange
import traceback
DEPTH = 0
threadpool = QThreadPool()
##########################################
###### Example thread function #####
##########################################
def listFolders( parent ):
global DEPTH
time.sleep(2)
if DEPTH>4:
return {'fileList':[], 'parent':parent}
else:
DEPTH+=1
fileList = []
for item in range(randrange(1,5)):
fileList.append('item_'+str(item))
return {'fileList':fileList, 'parent':parent}
##########################################
###### simple threading #####
##########################################
class WorkerSignals(QObject):
finished = pyqtSignal()
error = pyqtSignal(tuple)
result = pyqtSignal(object)
progress = pyqtSignal(int)
class Worker(QRunnable):
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
#pyqtSlot()
def run(self):
try:
result = self.fn(*self.args, **self.kwargs)
except:
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
self.signals.result.emit(result) # Return the result of the processing
finally:
self.signals.finished.emit() # Done
##########################################
###### Model for qtreeview #####
##########################################
class SceneGraphModel(QtCore.QAbstractItemModel):
def __init__(self, root ,parent=None):
super(SceneGraphModel, self).__init__(parent)
self._rootNode = root
def rowCount(self, parent):
if not parent.isValid():
parentNode = self._rootNode
else:
parentNode = parent.internalPointer()
return parentNode.childCount()
def columnCount(self, parent):
return 1
def data(self, index, role):
if not index.isValid():
return None
node = index.internalPointer()
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if index.column() == 0:
return node.name()
def setData(self, index, value, role=QtCore.Qt.EditRole):
if index.isValid():
if role == QtCore.Qt.EditRole:
node = index.internalPointer()
node.setName(value)
return True
return False
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if section == 0:
return "Scenegraph"
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable
def parent(self, index):
node = self.getNode(index)
parentNode = node.parent()
if parentNode == self._rootNode:
return QtCore.QModelIndex()
if parentNode == None:
row = 0
else:
row = parentNode.row()
return self.createIndex(row, 0, parentNode)
def index(self, row, column, parent):
parentNode = self.getNode(parent)
childItem = parentNode.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def getNode(self, index):
if index.isValid():
node = index.internalPointer()
if node:
return node
return self._rootNode
##########################################
###### Node class that contain the qtreeview datas #####
##########################################
class Node(object):
def __init__(self, name, parent=None):
self._name = name
self._children = []
self._parent = parent
if parent is not None:
parent.addChild(self)
def typeInfo(self):
return "folder"
def addChild(self, child):
self._children.append(child)
def name(self):
return self._name
def child(self, row):
return self._children[row]
def childCount(self):
return len(self._children)
def parent(self):
return self._parent
def row(self):
if self._parent is not None:
return self._parent._children.index(self)
def __repr__(self):
return 'NODE_'+self.name()
##########################################
###### qtreeview containing the threading #####
##########################################
class DirectoryTree(QTreeView):
def __init__(self):
super(DirectoryTree, self).__init__()
#create root node
self.rootNode = Node('root')
#add model to treeview
self._model = SceneGraphModel(self.rootNode)
self.setModel(self._model)
#recurive loop with thread to add more datas
self.loop( self.rootNode )
def thread(self, path):
return listFolders(path)
def threadResult(self, result ):
for item in result['fileList']:
newNode = Node(item,result['parent'])
self.loop(newNode)
def loop(self, parent ):
worker = Worker( self.thread, parent )
worker.signals.result.connect( self.threadResult )
threadpool.start(worker)
##########################################
###### window with countdown #####
##########################################
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.counter = 0
self.layout = QVBoxLayout()
self.l = QLabel("Start")
self.layout.addWidget(self.l)
w = QWidget()
w.setLayout(self.layout)
self.setCentralWidget(w)
self.treeView = DirectoryTree()
self.layout.addWidget(self.treeView)
self.show()
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
self.setGeometry(0, 0, 650, 550)
self.setWindowTitle("shot tree")
self.centerOnScreen()
def centerOnScreen (self):
resolution = QtGui.QDesktopWidget().screenGeometry()
self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
(resolution.height() / 2) - (self.frameSize().height() / 2))
def recurring_timer(self):
self.counter +=1
self.l.setText("Counter: %d" % self.counter)
##### This is a hack to refresh the view
##### i want to remove this line
##### and properly emit the changes from the node class to refresh the qtreeview
self.treeView.expandAll()
app = QApplication([])
window = MainWindow()
app.exec_()
This is my code example. there is a count down in the main window that would execute : self.treeView.expandAll()
every second to force the view to update, i want to find a better solution ...
Related topics i found:
Refresh view when model data has not changed (Qt/PySide/PyQt)?
PyQt and MVC-pattern
The problem has nothing to do with threads. For the view to be notified the model must emit the signal layoutAboutToBeChanged before the change and layoutChanged after the change, but for this the node must access the model, so the model must be made as a Node attribute. With that change you no longer need a QTimer to update the view.
class SceneGraphModel(QtCore.QAbstractItemModel):
def __init__(self, root, parent=None):
super(SceneGraphModel, self).__init__(parent)
self._rootNode = root
self._rootNode._model = self
def rowCount(self, parent=QtCore.QModelIndex()):
if not parent.isValid():
parentNode = self._rootNode
else:
parentNode = parent.internalPointer()
return parentNode.childCount()
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
node = index.internalPointer()
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
if index.column() == 0:
return node.name()
def setData(self, index, value, role=QtCore.Qt.EditRole):
if index.isValid():
if role == QtCore.Qt.EditRole:
node = index.internalPointer()
node.setName(value)
return True
return False
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if section == 0:
return "Scenegraph"
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable
def parent(self, index):
node = self.getNode(index)
parentNode = node.parent()
if parentNode == self._rootNode:
return QtCore.QModelIndex()
if parentNode is None:
row = 0
else:
row = parentNode.row()
return self.createIndex(row, 0, parentNode)
def index(self, row, column, parent=QtCore.QModelIndex()):
parentNode = self.getNode(parent)
childItem = parentNode.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def getNode(self, index):
if index.isValid():
node = index.internalPointer()
if node:
return node
print("node", node)
return self._rootNode
class Node(object):
def __init__(self, name, parent=None):
self._name = name
self._children = []
self._parent = parent
self._model = None
if parent is not None:
parent.addChild(self)
def typeInfo(self):
return "folder"
def addChild(self, child):
self._model.layoutAboutToBeChanged.emit()
self._children.append(child)
child._model = self._model
self._model.layoutChanged.emit()
def name(self):
return self._name
def setName(self, name):
self._name = name
def child(self, row):
return self._children[row] if row < len(self._children) else None
def childCount(self):
return len(self._children)
def parent(self):
return self._parent
def row(self):
return 0 if self.parent() is None else self._parent._children.index(self)
def __repr__(self):
return 'NODE_' + self.name()
I implemented a cutom model for treeview for having checkboxes inside the treeview. If I check a parent node all child nodes should be chekced automatically. This basically works but der is a time lack between checking the parent node and updating the subnodes.
from PyQt4 import QtCore, QtGui
import sys
class Node(object):
def __init__(self, name, parent=None, checked=False):
self._name = name
self._children = []
self._parent = parent
self._checked = checked
if parent is not None:
parent.addChild(self)
def addChild(self, child):
self._children.append(child)
def insertChild(self, position, child):
if position < 0 or position > len(self._children):
return False
self._children.insert(position, child)
child._parent = self
return True
def name(self):
return self._name
def checked(self):
return self._checked
def setChecked(self, state):
self._checked = state
for c in self._children:
c.setChecked(state)
def child(self, row):
return self._children[row]
def childCount(self):
return len(self._children)
def parent(self):
return self._parent
def row(self):
if self._parent is not None:
return self._parent._children.index(self)
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, root, parent=None):
super().__init__(parent)
self._rootNode = root
def rowCount(self, parent):
if not parent.isValid():
parentNode = self._rootNode
else:
parentNode = parent.internalPointer()
return parentNode.childCount()
def columnCount(self, parent):
return 1
def data(self, index, role):
if not index.isValid():
return None
node = index.internalPointer()
if role == QtCore.Qt.DisplayRole:
if index.column() == 0:
return node.name()
if role == QtCore.Qt.CheckStateRole:
if node.checked():
return QtCore.Qt.Checked
else:
return QtCore.Qt.Unchecked
def setData(self, index, value, role=QtCore.Qt.EditRole):
if index.isValid():
if role == QtCore.Qt.CheckStateRole:
node = index.internalPointer()
node.setChecked(not node.checked())
return True
return False
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
return "Nodes"
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable| QtCore.Qt.ItemIsUserCheckable
def parent(self, index):
node = self.getNode(index)
parentNode = node.parent()
if parentNode == self._rootNode:
return QtCore.QModelIndex()
return self.createIndex(parentNode.row(), 0, parentNode)
def index(self, row, column, parent):
parentNode = self.getNode(parent)
childItem = parentNode.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def getNode(self, index):
if index.isValid():
node = index.internalPointer()
if node:
return node
return self._rootNode
def removeRows(self, position, rows, parent=QtCore.QModelIndex()):
parentNode = self.getNode(parent)
self.beginRemoveRows(parent, position, position + rows - 1)
for row in range(rows):
success = parentNode.removeChild(position)
self.endRemoveRows()
return success
def main_simple():
app = QtGui.QApplication(sys.argv)
rootNode = Node("Root")
n1 = Node("Node1", rootNode)
n2 = Node("Node2", rootNode)
n3 = Node("Node3", rootNode)
n1_1 = Node("Node1 1", n1)
n1_2 = Node("Node1 2", n1)
n1_3 = Node("Node1 3", n1)
n2_1 = Node("Node2 1", n2)
n2_2 = Node("Node2 2", n2)
n2_3 = Node("Node2 3", n2)
n3_1 = Node("Node3 1", n3)
n3_2 = Node("Node3 2", n3)
n3_3 = Node("Node3 3", n3)
model = TreeModel(rootNode)
treeView = QtGui.QTreeView()
treeView.show()
treeView.setModel(model)
sys.exit(app.exec_())
if __name__ == '__main__':
main_simple()
How can I avoid this lack so that the userinterface is more smoothly?
The model should emit the dataChanged() signal for the children when they are toggled, so that the view can update their checkboxes instantly. According to the documentation, setData should also emit dataChanged for the item that was explicitly changed.
This should work:
def setData(self, index, value, role=QtCore.Qt.EditRole):
if index.isValid():
if role == QtCore.Qt.CheckStateRole:
node = index.internalPointer()
node.setChecked(not node.checked())
self.dataChanged.emit(index, index)
self.emitDataChangedForChildren(index)
return True
return False
def emitDataChangedForChildren(self, index):
count = self.rowCount(index)
if count:
self.dataChanged.emit(index.child(0, 0), index.child(count-1, 0))
for child in range(count):
self.emitDataChangedForChildren(index.child(child, 0))
As per flonks answer above, a non-iterative solution that worked for me is as follows:
C++: emit dataChanged(QModelIndex(), QModelIndex());
Python: self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())
This should update all items in the tree.