如何过滤包含小部件的 TreeView

How to filter TreeView that contains widgets

我正在 PyQt5 中开发一个包含 QTreeView、QStandardItemModel 和 QSortFilterProxyModel 的应用程序。 TreeView 在最后一列的某些行上也有一个 QToolBar。

我做了一个简化版的例子:

这是源代码:

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_())

现在我想使用 QLineEdit 小部件和 QSortFilterProxyModel 添加过滤器,但是正如您在下面看到的那样,工具栏被删除了。有人可以解释为什么以及如何解决这个问题吗?

到目前为止,这是我的代码:

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_())

感谢您的帮助!

问题部分与操作发生的顺序有关。考虑代码...

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)

createRow 调用树视图的源模型时是代理 -- 不是 QStandardItemModel。因此呼吁...

self.tree.setIndexWidget(child4.index(), toolbar)

将失败,因为 child4.index() 返回的 QModelIndex 未被视图识别为属于其源模型。

相反,您需要像这样将 child4.index() 返回的值从 QStandardItemModel 映射到 QSortFilterProxyModel...

self.tree.setIndexWidget(self.proxyModel.mapFromSource(child4.index()), toolbar)

在旁注中,行...

self.model.appendRow(parent)

in createRow 由于多次添加 parent 导致警告。此行应移出 createRow 并移入 importData。正确的 importDatacreateRow 实现将是...

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)

你的代码存在三个问题。

首先,不应该在 createRow() 函数中添加父项,而是在 importData() 中,并且可能 添加子项之前。
Qt 在输出中警告您:

StdErr: QStandardItem::insertRows: Ignoring duplicate insertion of item ...

然后,用于 setIndexWidget() 的索引必须 基于视图的模型(即代理),而您正在使用源模型, 因此索引无效,因为它属于另一个模型。

最后,当使用过滤时,过滤后的索引及其索引小部件将被完全销毁,因此当您取消设置过滤器时,这些小部件将不会恢复。

解决方案是在子索引实际添加到模型时设置小部件,这可以通过连接到 rowsInserted() 信号来完成,在验证父索引有效后(否则它们会是顶级项目)。

请注意,QToolBar 不适用于这种用途,应该使用标准的 QWidget(或 QFrame)。

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)

注意:类 和常量的名称应始终大写。