I am working on an application in PyQt5 that contains a QTreeView, QStandardItemModel and QSortFilterProxyModel. The TreeView also has a QToolBar on the last column for some rows.
I have made a simplified version for an example :
And here's the source code :
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class view(QWidget):
def __init__(self):
super(view, self).__init__()
self.tree = QTreeView(self)
layout = QVBoxLayout(self)
layout.addWidget(self.tree)
self.model = QStandardItemModel()
self.model.setHorizontalHeaderLabels(["Col 0", "Col 1", "Col 2", "Toolbar"])
self.tree.header().setDefaultSectionSize(180)
self.tree.setModel(self.model)
self.importData()
self.tree.expandAll()
def importData(self, root=None):
for i in range(3):
parent1 = QStandardItem("Family {}".format(i))
for j in range(3):
self.createRow(parent1, i, j)
def createRow(self, parent, i, j):
child1 = QStandardItem("Child {}".format(i * 3 j))
child2 = QStandardItem("row: {}, col: {}".format(i, j 1))
child3 = QStandardItem("row: {}, col: {}".format(i, j 2))
child4 = QStandardItem("")
parent.appendRow([child1, child2, child3, child4])
self.model.appendRow(parent)
toolbar = QToolBar()
toolbar.addWidget(QLabel("Toolbar Btn: "))
toolbar.addWidget(QPushButton("Btn"))
self.tree.setIndexWidget(child4.index(), toolbar)
if __name__ == "__main__":
app = QApplication(sys.argv)
view = view()
view.setGeometry(300, 100, 600, 300)
view.setWindowTitle("QTreeview Example")
view.show()
sys.exit(app.exec_())
Now I would like to add a filter using a QLineEdit widget and QSortFilterProxyModel, but as you can see below the toolbar gets removed. Can someone explain why and how I could solve this issue ?
Here's my code so far :
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class view(QWidget):
def __init__(self):
super(view, self).__init__()
self.tree = QTreeView(self)
layout = QVBoxLayout(self)
self.filter = QLineEdit()
self.filter.textChanged.connect(self.onTextChanged)
layout.addWidget(self.filter)
layout.addWidget(self.tree)
self.model = QStandardItemModel()
self.model.setHorizontalHeaderLabels(["Col 0", "Col 1", "Col 2", "Toolbar"])
self.proxyModel = QSortFilterProxyModel(
self.tree, recursiveFilteringEnabled=True
)
self.proxyModel.setSourceModel(self.model)
self.tree.header().setDefaultSectionSize(180)
self.tree.setModel(self.proxyModel)
self.importData()
self.tree.expandAll()
def importData(self, root=None):
for i in range(3):
parent1 = QStandardItem("Family {}".format(i))
for j in range(3):
self.createRow(parent1, i, j)
def createRow(self, parent, i, j):
child1 = QStandardItem("Child {}".format(i * 3 j))
child2 = QStandardItem("row: {}, col: {}".format(i, j 1))
child3 = QStandardItem("row: {}, col: {}".format(i, j 2))
child4 = QStandardItem("")
parent.appendRow([child1, child2, child3, child4])
self.model.appendRow(parent)
toolbar = QToolBar()
toolbar.addWidget(QLabel("Toolbar Btn: "))
toolbar.addWidget(QPushButton("Btn"))
self.tree.setIndexWidget(child4.index(), toolbar)
def onTextChanged(self, text):
self.proxyModel.setFilterRegExp(text)
if __name__ == "__main__":
app = QApplication(sys.argv)
view = view()
view.setGeometry(300, 100, 600, 300)
view.setWindowTitle("QTreeview Example")
view.show()
sys.exit(app.exec_())
Thanks for your help !
CodePudding user response:
There are three problems in your code.
First of all, the parent shouldn't be added in the createRow()
function, but in importData()
and possibly before adding children.
Qt warns you about this in the output:
StdErr: QStandardItem::insertRows: Ignoring duplicate insertion of item ...
Then, the index used for setIndexWidget()
must be based on the model of the view (which is the proxy), while you're using the source model, so the index is invalid as it belongs to another model.
Finally, when using filtering, the filtered indexes are completely destroyed and their index widget along with them, so when you unset the filter those widget will not be restored.
The solution is to set the widgets when the child indexes are actually added to the model, and this can be done by connecting to the rowsInserted()
signal, after verifying that the parent is valid (otherwise they would be top level items).
Note that QToolBar is not intended for this kind of usage, and a standard QWidget (or QFrame) should be used instead.
class view(QWidget):
def __init__(self):
# ...
self.proxyModel.rowsInserted.connect(self.updateWidgets)
def importData(self, root=None):
for i in range(3):
parent1 = QStandardItem("Family {}".format(i))
self.model.appendRow(parent1)
for j in range(3):
self.createRow(parent1, i, j)
def createRow(self, parent, i, j):
child1 = QStandardItem("Child {}".format(i * 3 j))
child2 = QStandardItem("row: {}, col: {}".format(i, j 1))
child3 = QStandardItem("row: {}, col: {}".format(i, j 2))
child4 = QStandardItem("")
parent.appendRow([child1, child2, child3, child4])
def updateWidgets(self, parent, first, last):
if not parent.isValid():
return
for row in range(first, last 1):
toolbar = QWidget()
layout = QHBoxLayout(toolbar)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(QLabel("Toolbar Btn: "))
layout.addWidget(QPushButton("Btn"))
childIndex = self.proxyModel.index(row, 3, parent)
self.tree.setIndexWidget(childIndex, toolbar)
Note: classes and constants should always have capitalized names.
CodePudding user response:
The problem is partly to do with the order in which operations are happening. Consider the code...
def createRow(self, parent, i, j):
child1 = QStandardItem("Child {}".format(i * 3 j))
child2 = QStandardItem("row: {}, col: {}".format(i, j 1))
child3 = QStandardItem("row: {}, col: {}".format(i, j 2))
child4 = QStandardItem("")
parent.appendRow([child1, child2, child3, child4])
self.model.appendRow(parent)
toolbar = QToolBar()
toolbar.addWidget(QLabel("Toolbar Btn: "))
toolbar.addWidget(QPushButton("Btn"))
self.tree.setIndexWidget(child4.index(), toolbar)
By the time createRow
is called the tree view's source model is the proxy -- not the QStandardItemModel
. Hence the call...
self.tree.setIndexWidget(child4.index(), toolbar)
will fail since the QModelIndex
returned by child4.index()
is not recognized by the view as belonging to its source model.
Instead you need to map the value returned by child4.index()
from the QStandardItemModel
to the QSortFilterProxyModel
like so...
self.tree.setIndexWidget(self.proxyModel.mapFromSource(child4.index()), toolbar)
On a side note, the line...
self.model.appendRow(parent)
in createRow
causes warnings due to parent
being added multiple times. This line should be moved out of createRow
and into importData
. The correct importData
and createRow
implementations would then be...
def importData(self, root=None):
for i in range(3):
parent1 = QStandardItem("Family {}".format(i))
self.model.appendRow(parent1)
for j in range(3):
self.createRow(parent1, i, j)
def createRow(self, parent, i, j):
child1 = QStandardItem("Child {}".format(i * 3 j))
child2 = QStandardItem("row: {}, col: {}".format(i, j 1))
child3 = QStandardItem("row: {}, col: {}".format(i, j 2))
child4 = QStandardItem("")
parent.appendRow([child1, child2, child3, child4])
toolbar = QToolBar()
toolbar.addWidget(QLabel("Toolbar Btn: "))
toolbar.addWidget(QPushButton("Btn"))
self.tree.setIndexWidget(self.proxyModel.mapFromSource(child4.index()), toolbar)