Issue
I have three models, a QAbstractItemModel "sourceModel", QSortFilterProxyModel "proxyModel" for filtering, and a QIdentityProxyModel "dataModel" for modifying columns and displayed data.
When I only layer QAbstractItemModel -> QIdentityProxyModel , it all works well. My source model only has one default column given, I can add multiple new columns on my QIdentifyProxyModel, and my view displays all the right data. This allows me to reuse the same model on multiple views with different data, great.
However, if I layer QAbstractItemModel -> QSortFilterProxyModel -> QIdentityProxyModel, then my view only shows data in the first column, and the selection model breaks. If I define an equal or higher amount of columns in my source model, then all the QIdentityProxyModel columns behaves correctly in my view, and shows different data than what I defined in my source model.
When proxies are nested, the QIdentityProxyModel class can still access the source item with the data, but the index passed to the QIdentityProxyModel's data-function only queries the amount columns defined in the source model.
Any idea how to go about this? Any help is much appreciated!
from PySide2 import QtCore, QtWidgets, QtGui
class Item(dict):
def __init__(self, data=None):
super(Item, self).__init__()
self._children = list()
self._parent = None
if data:
self.update(data)
def childCount(self):
return len(self._children)
def child(self, row):
if row >= len(self._children):
return
return self._children[row]
def children(self):
return self._children
def parent(self):
return self._parent
def row(self):
if self._parent is not None:
siblings = self.parent().children()
return siblings.index(self)
def add_child(self, child):
child._parent = self
self._children.append(child)
class TreeModel(QtCore.QAbstractItemModel):
Columns = list()
ItemRole = QtCore.Qt.UserRole + 1
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self._root_item = Item()
def rowCount(self, parent):
if parent.isValid():
item = parent.internalPointer()
else:
item = self._root_item
return item.childCount()
def columnCount(self, parent):
return len(self.Columns)
def setColumns(self, keys):
assert isinstance(keys, (list, tuple))
self.Columns = keys
def data(self, index, role):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
item = index.internalPointer()
column = index.column()
key = self.Columns[column]
return item.get(key, None)
if role == self.ItemRole:
return index.internalPointer()
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if section < len(self.Columns):
return self.Columns[section]
super(TreeModel, self).headerData(section, orientation, role)
def parent(self, index):
item = index.internalPointer()
parent_item = item.parent()
if parent_item == self._root_item or not parent_item:
return QtCore.QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
def index(self, row, column, parent):
if not parent.isValid():
parent_item = self._root_item
else:
parent_item = parent.internalPointer()
child_item = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
else:
return QtCore.QModelIndex()
def add_child(self, item, parent=None):
if parent is None:
parent = self._root_item
parent.add_child(item)
def column_name(self, column):
if column < len(self.Columns):
return self.Columns[column]
def clear(self):
self.beginResetModel()
self._root_item = Item()
self.endResetModel()
class CustomSortFilterProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super(CustomSortFilterProxyModel, self).__init__(parent)
def filterAcceptsRow(self, row, parent):
model = self.sourceModel()
index = model.index(row, self.filterKeyColumn(), parent)
item = index.internalPointer()
if item.get('name'):
return True
else:
return False
class IdentityProxyModel(QtCore.QIdentityProxyModel):
def __init__(self, *args, **kwargs):
super(IdentityProxyModel, self).__init__(*args, **kwargs)
self.Columns = []
self._root_item = Item()
def setColumns(self, keys):
assert isinstance(keys, (list, tuple))
self.Columns = keys
#
def columnCount(self, parent):
# return 3
return len(self.Columns)
def column_name(self, column):
if column < len(self.Columns):
return self.Columns[column]
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
if section < len(self.Columns):
return self.Columns[section]
super(IdentityProxyModel, self).headerData(section, orientation, role)
def data(self, index, role):
if not index.isValid():
return
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
item = self.mapToSource(index).data(TreeModel.ItemRole)
column = index.column()
key = self.Columns[column]
return item.get(key, None)
return super(IdentityProxyModel, self).data(index, role)
if __name__ == '__main__':
import sys
sourceModel = TreeModel()
sourceModel.setColumns(['name'])
sourceModel.add_child(Item({'name': 'itemA', 'number': '1', 'info': 'A'}))
sourceModel.add_child(Item({'name': 'itemB', 'number': '2', 'info': 'B'}))
proxyModel = CustomSortFilterProxyModel()
proxyModel.setSourceModel(sourceModel)
dataModel = IdentityProxyModel()
dataModel.setSourceModel(proxyModel)
dataModel.setColumns(['name', 'info'])
app = QtWidgets.QApplication(sys.argv)
view = QtWidgets.QTreeView()
view.setModel(dataModel)
view.show()
sys.exit(app.exec_())
Solution
Tree models are tricky. A lot.
Whenever you have to deal with them, you have to really understand recursion. And with Qt models (which are somehow complex) that becomes even harder, and often a cause of massive headaches and late nights.
I am basing the following code on a previous answer of mine which is very basic and doesn't consider the possibility of having inconsistent number of child columns.
The concept is that proxy models that are based on tree structures need to keep track of the hierarchy (which is usually based on the parent index internal pointer or id), and that must be considered whenever "ghost" columns are created.
All of the above is always very important. Item views need all of the implementation in order to properly update their contents and allow valid user interaction.
class IdentityProxyModel(QtCore.QIdentityProxyModel):
def __init__(self, *args, **kwargs):
super(IdentityProxyModel, self).__init__(*args, **kwargs)
self.Columns = []
self._parents = {}
self._root_item = Item()
# ...
def _isInvalid(self, column):
# we assume that the model always has the same column count
return column > self.sourceModel().columnCount() - 1
def mapToSource(self, index):
if self._isInvalid(index.column()):
index = index.sibling(index.row(), 0)
return super().mapToSource(index)
def index(self, row, column, parent=QtCore.QModelIndex()):
if self._isInvalid(column):
index = self.createIndex(row, column, parent.internalId())
self._parents[index] = parent
return index
return super().index(row, column, parent)
def parent(self, index):
if self._isInvalid(index.column()):
return self._parents[index]
return super().parent(index)
def flags(self, index):
if self._isInvalid(index.column()):
return self.flags(index.sibling(index.row(), 0))
return super().flags(index)
def sibling(self, row, column, idx):
if self._isInvalid(column):
return self.index(row, column, idx.parent())
elif self._isInvalid(idx.column()):
idx = self.index(idx.row(), 0, idx.parent())
return super().sibling(row, column, idx)
Note: overrides should always use the signature of their base implementation. For instance, both rowCount()
and columnCount()
must accept an invalid keyword parent argument, which is not used for "fast calls" on the top level of the model. The common practice is to use a basic QModelIndex instance, since it is fundamentally an immutable object (but None
is commonly accepted too):
def rowCount(self, parent=QtCore.QModelIndex()):
# ...
def columnCount(self, parent=QtCore.QModelIndex()):
# ...
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.