Qt - 每次调用 QSortFilterProxyModel::invalidateFilter 时都会重置 rootIndex

Posted

技术标签:

【中文标题】Qt - 每次调用 QSortFilterProxyModel::invalidateFilter 时都会重置 rootIndex【英文标题】:Qt - rootIndex gets reset each time QSortFilterProxyModel::invalidateFilter is called 【发布时间】:2022-01-03 19:14:05 【问题描述】:

我需要一个QTabWidget,其中每个选项卡都包含一个QTreeView。每个QTreeView 显示 更大模型的一个“分支”。用户有一个QLineEdit,它可以实时过滤 视图基于他们键入的任何内容。我在下面复制了 GUI + 问题。

问题是我使用QTreeView::setRootIndex 来显示单曲, 主模型的内部分支。 QLineEdit“过滤器”是用 QSortFilterProxyModel。每当用户键入时,我都会调用 QSortFilterProxyModel::invalidateFilter 从头开始​​重新过滤。这两个 方法,QTreeView::setRootIndexQSortFilterProxyModel::invalidateFilter,不要 混合均匀。

QSortFilterProxyModel::invalidateFilter 被调用时,我拥有的索引 以前使用QTreeView::setRootIndex 设置现在无效。然后QTreeView 显示整个树,而不是我之前使用setRootIndex 选择的“分支”。 QTreeView 有效地“忘记”了我设置的根索引。

我复制了下面的问题

import functools

from Qt import QtCore, QtWidgets

_NO_ROW = -1
_NUMBER_OF_BRANCHES = 3


class _MatchProxy(QtCore.QSortFilterProxyModel):
    """Use a callable function to determine if an index should be hidden from view or not."""

    def __init__(self, matcher, parent=None):
        super(_MatchProxy, self).__init__(parent=parent)

        self._matcher = matcher

    def filterAcceptsRow(self, source_row, source_index):
        return self._matcher(source_index)


class _MultiTree(QtWidgets.QWidget):
    """A widget which makes smaller, individual QTreeViews for each section of an given index."""

    def __init__(self, source_index, parent=None):
        super(_MultiTree, self).__init__(parent=parent)

        self.setLayout(QtWidgets.QVBoxLayout())

        model = source_index.model()

        self._views = []

        for index in range(model.rowCount(source_index)):
            section = model.index(index, 0, parent=source_index)
            label = model.data(section, QtCore.Qt.DisplayRole)
            view = QtWidgets.QTreeView()
            view.setModel(model)
            view.setRootIndex(section)

            self.layout().addWidget(QtWidgets.QLabel(label))
            self.layout().addWidget(view)

            self._views.append(view)

    def iter_models(self):
        for view in self._views:
            yield view.model()

    def iter_views(self):
        for view in self._views:
            yield view


class Node(object):
    """A generic name + children + parent graph node class."""

    def __init__(self, name, parent=None):
        super(Node, self).__init__()

        self._name = name
        self._children = []
        self._parent = parent

        if self._parent:
            self._parent.add_child(self)

    def add_child(self, node):
        node._parent = self
        self._children.append(node)

    def get_child(self, row):
        return self._children[row]

    def get_children(self):
        return list(self._children)

    def get_label(self):
        return self._name

    def get_parent(self):
        return self._parent

    def get_row(self):
        parent = self.get_parent()

        if not parent:
            return _NO_ROW

        return parent.get_children().index(self)

    def __repr__(self):
        return "self.__class__.__name__(self._name!r, parent=self._parent!r)".format(
            self=self
        )


class Branch(Node):
    """Syntax sugar for debugging. This class isn't "necessary" for the reproduction."""

    pass


class Model(QtCore.QAbstractItemModel):
    """The basic Qt model which contains the entire tree, including each branch."""

    def __init__(self, roots, parent=None):
        super(Model, self).__init__(parent=parent)

        self._roots = roots

    def _get_node(self, index):
        return index.internalPointer()

    def columnCount(self, _):
        return 1

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role != QtCore.Qt.DisplayRole:
            return None

        node = self._get_node(index)

        return node.get_label()

    def index(self, row, column, parent=QtCore.QModelIndex()):
        if not parent.isValid():
            return self.createIndex(row, 0, self._roots[row])

        parent_node = self._get_node(parent)
        child_node = parent_node.get_child(row)

        return self.createIndex(row, column, child_node)

    def parent(self, index):
        if not index.isValid():
            return QtCore.QModelIndex()

        node = self._get_node(index)
        parent = node.get_parent()

        if not parent:
            return QtCore.QModelIndex()

        return self.createIndex(node.get_row(), 0, parent)

    def rowCount(self, index):
        if not index.isValid():
            return len(self._roots)

        node = self._get_node(index)

        return len(node.get_children())


class Widget(QtWidgets.QWidget):
    """The main widget / window which has the filterer + QTreeViews."""

    def __init__(self, model, parent=None):
        super(Widget, self).__init__(parent=parent)

        self.setLayout(QtWidgets.QVBoxLayout())

        self._filterer = QtWidgets.QLineEdit()
        self._tabs = QtWidgets.QTabWidget()

        self._set_model(model)

        self.layout().addWidget(self._filterer)
        self.layout().addWidget(self._tabs)

        self._filterer.textChanged.connect(self._update_current_view)

    def _replace_model_with_filterer_proxy(self, view):
        def _match(line_edit, index):
            if not index.isValid():
                return True  # Show everything, don't filter anything

            current = line_edit.text().strip()

            if not current:
                return True  # Show everything, don't filter anything.

            return current in index.data(QtCore.Qt.DisplayRole)

        model = view.model()
        current_root = view.rootIndex()
        proxy = _MatchProxy(functools.partial(_match, self._filterer))
        proxy.setSourceModel(model)
        proxy.setRecursiveFilteringEnabled(True)
        view.setModel(proxy)
        view.setRootIndex(proxy.mapFromSource(current_root))

    def _set_model(self, model):
        tabs_count = model.rowCount(QtCore.QModelIndex())

        for row in range(tabs_count):
            branch_index = model.index(row, 0)
            tab_name = model.data(branch_index, QtCore.Qt.DisplayRole)
            widget = _MultiTree(branch_index)

            for view in widget.iter_views():
                self._replace_model_with_filterer_proxy(view)

            self._tabs.addTab(widget, tab_name)

    def _update_current_view(self):
        widget = self._tabs.currentWidget()

        for proxy in widget.iter_models():
            proxy.invalidateFilter()


def _make_branch_graph():
    default = Node("default")
    optional = Node("optional")

    Node("camera", parent=default)
    Node("set", parent=default)
    light = Node("light", parent=default)
    Node("directional light", parent=light)
    spot = Node("spot light", parent=light)
    Node("light center", parent=spot)
    Node("volume light", parent=light)

    Node("model", parent=optional)
    surfacing = Node("surfacing", parent=optional)
    Node("look", parent=surfacing)
    Node("hair", parent=surfacing)
    Node("fur", parent=surfacing)
    Node("rig", parent=optional)

    return (default, optional)


def _make_full_graph():
    roots = []

    for index in range(_NUMBER_OF_BRANCHES):
        branch = Branch("branch_index".format(index=index))

        for node in _make_branch_graph():
            branch.add_child(node)

        roots.append(branch)

    return roots


def main():
    application = QtWidgets.QApplication([])

    roots = _make_full_graph()
    model = Model(roots)
    window = Widget(model)
    window.show()

    application.exec_()


if __name__ == "__main__":
    main()
运行上面的代码打开 GUI。选项卡 + 内部 QTreeView 应如下所示:
QTabWidget tab - branch_0
    QTreeView (default)
        - camera
        - set
        - light
            - directional light
            - spot light
                - light center
            - volume light
    QTreeView (optional)
        - model
        - surfacing
            - look
            - hair
            - fur
        - rig
QTabWidget tab - branch_1
    - Same as branch_0
QTabWidget tab - branch_2
    - Same as branch_0

这是正确的、预期的节点显示。现在在过滤器 QLineEdit 中,输入 在光明中”。您现在将获得:

QTabWidget tab - branch_0
    QTreeView (default)
        - light
            - directional light
            - spot light
                - light center
            - volume light
    QTreeView (optional)
        - branch_0
            - default
                - light
                    - directional light
                    - spot light
                        - light center
                    - volume light
        - branch_1
            - default
                - light
                    - directional light
                    - spot light
                        - light center
                    - volume light
        - branch_2
            - default
                - light
                    - directional light
                    - spot light
                        - light center
                    - volume light
QTabWidget tab - branch_1
    - Same as branch_0
QTabWidget tab - branch_2
    - Same as branch_0

在这里您可以看到标记为“可选”的 QTreeView 现在显示 每个分支的内容,而不仅仅是应该的一个分支。

这不是预期的行为。作为参考,这是我的观点 希望得到:

QTabWidget tab - branch_0
    QTreeView (default)
        - default
            - light
                - directional light
                - spot light
                    - light center
                - volume light
    QTreeView (optional) [EMPTY, no children]
QTabWidget tab - branch_1
    - Same as branch_0
QTabWidget tab - branch_2
    - Same as branch_0

还要注意,如果你清除过滤器 QLineEdit 的文本,“”,你不会去 回到第一张图。 “可选”QTreeView 被卡住了 一切,在每个 QTreeView 中。

现在在过滤器 QLineEdit 文本中,键入“asdf”。现在图表是

QTabWidget tab - branch_0
    QTreeView (default)
        - branch_0
        - branch_1
        - branch_2
    QTreeView (optional)
        - branch_0
        - branch_1
        - branch_2
QTabWidget tab - branch_1
    - Same as branch_0
QTabWidget tab - branch_2
    - Same as branch_0

当我的预期视图是

QTabWidget tab - branch_0
    QTreeView (default) [EMPTY]
    QTreeView (optional) [EMPTY]
QTabWidget tab - branch_1
    - Same as branch_0
QTabWidget tab - branch_2
    - Same as branch_0

如果你用“”清除过滤器文本,现在两个 QTreeView 都会显示所有分支的所有内容。

有没有一种简单的方法来获得我所描述的预期图表?可能是 有一种方法可以在不使索引无效的情况下重新运行 QSortFilterProxyModel,或者 我可以使用其他一些机制来获得相同的效果吗?此刻我在 通过“保存和恢复”每个的 rootIndex 来解决问题 查看、前和后 invalidateFilter。但是我的方法不起作用 所有情况,感觉就像一个黑客。

对此的任何建议将不胜感激。

【问题讨论】:

请不要编辑问题来提供答案,因为这会使帖子变得混乱。如果您不确定建议的答案是否有效,请等到您可以测试它并然后接受它。如果你有一个不同的解决方案,那么就用它创建一个答案,但如果你只是想展示现有答案是如何在你的代码中实现的,就不要这样做。 我必须对其进行编辑,因为我测试了您的代码,但它并不能很好地工作。就是说,我想给你接受的答案,因为你一定在这个答案上很努力。但是,如果您希望我单独回复,那么我会为您做。 FWIW 我看到其他人在他们的问题中提供了解决方案,这并不令人困惑。在那些情况下,他们的代码要短得多。 您可以在问题中看到答案这一事实仅意味着没有人负责通知它或回滚。问题字段用于问题,答案必须在答案部分,这不是我的偏好,而是应该如何使用网站。也就是说,如果某些东西在答案中不起作用,请使用 cmets 指定 what 不起作用:答案不是最终的,也可能是错误的;通知作者这对您、作者(可能会学到新东西或意识到自己的错误)以及其他读者都有用。 另外,假设你的答案更正确,你至少应该解释一下为什么。在没有任何解释的情况下发布完整代码似乎对您很有用,但通过这种方式,您会迫使其他人比较原始代码和新代码,找出差异并尝试理解它们的为什么制成。抱歉,但现在您的答案似乎没有添加任何有用的信息来证明 不同 答案的合理性,因为修改似乎是由原始问题中的问题引起的,并且与实际问题没有真正的关系(根索引的有效性)。 【参考方案1】:

问题在于,当应用过滤时,未被接受的索引会变成无效索引,而对于 Qt,无效索引与 root 索引相同。 由于您将视图的根索引设置为过滤器无效的索引,因此结果与执行setRootIndex(QModelIndex()) 相同,显示整个模型。

事实上,如果您尝试使用与“默认”分支的 any 不匹配的字符串过滤模型,也会遇到您在“可选”视图中看到的相同问题items:它将显示整个根树模型,这是因为如果索引无效,您的_match 函数会返回True,因此无论如何都会显示根(在正常情况下,它会显示一个空模型)。 关于这方面的注意事项:我不确定所需的行为,但如果过滤器不匹配任何内容,则不应返回 True,因为这将使所有根索引都有效,在您的情况下将显示root 即使它不应该是显示的索引。

问题的根源在于模型不知道视图的根索引(也不应该!)并且视图无法知道何时无效的根索引变为再次有效(而且,再次,也不应该)。见this discussion on a similar matter。

一个可能的解决方案(它不能解决接受无效根的问题)是跟踪根索引,然后通过连接到rowsRemoved 来更新视图,并且大多数情况下重要的是,rowsInserted 模型的信号,以便我们可以在它再次变为“可用”时恢复原始根索引:

class TreePersistentRootIndex(QtWidgets.QTreeView):
    _sourceRootIndex = QtCore.QModelIndex()
    def setModel(self, model):
        if self.model() and isinstance(self.model(), QtCore.QSortFilterProxyModel):
            self.model().layoutChanged.disconnect(self.checkRootIndex)
            self.model().rowsRemoved.disconnect(self.checkRootIndex)
            self.model().rowsInserted.disconnect(self.checkRootIndex)
        super().setModel(model)
        self._model = model
        if isinstance(model, QtCore.QSortFilterProxyModel):
            model.layoutChanged.connect(self.checkRootIndex)
            model.rowsRemoved.connect(self.checkRootIndex)
            model.rowsInserted.connect(self.checkRootIndex)

    def checkRootIndex(self):
        if (not self._sourceRootIndex.isValid() or 
            not isinstance(self.model(), QtCore.QSortFilterProxyModel)):
                return
        rootIndex = self.model().mapFromSource(self._sourceRootIndex)
        if rootIndex != self.rootIndex():
            super().setRootIndex(rootIndex)

    def setRootIndex(self, rootIndex):
        super().setRootIndex(rootIndex)
        if isinstance(self.model(), QtCore.QSortFilterProxyModel):
            rootIndex = self.model().mapToSource(rootIndex)
        self._sourceRootIndex = rootIndex


class _MultiTree(QtWidgets.QWidget):
    def __init__(self, source_index, parent=None):
        # ...
        for index in range(model.rowCount(source_index)):
            section = model.index(index, 0, parent=source_index)
            label = model.data(section, QtCore.Qt.DisplayRole)
            view = TreePersistentRootIndex()
            # ...

要克服接受根索引的问题,您可能会考虑更改过滤行为,或者最终(如果您完全确定如果过滤器不匹配任何子项,它应该'不显示子根索引中的任何内容),如果结果索引无效,则使用setRowHidden() 使用True(如hide)或False(如show em>) 否则。

一个可能的实现(未完全测试)可能如下:

    def checkRootIndex(self):
        # ...as above, then:
        if rootIndex.isValid() != self._sourceRootIndex.isValid():
            hide = not rootIndex.isValid()
            for row in range(self.model().rowCount(rootIndex)):
                self.setRowHidden(row, rootIndex.parent(), hide)

【讨论】:

完美了,非常感谢您的帮助!!我将使用您的代码发布完整的工作示例并接受您的代码 我已经使用这个解决方案几个星期了,并注意到一些我必须解决的问题。第一个是,如果我使用(模型)beginMoveRows(),树视图会出现分段错误。我通过将self._sourceRootIndex = rootIndex 更改为self._sourceRootIndex = QtCore.QPersistentModelIndex(rootIndex) 解决了这个问题。第二个问题是 beginRemoveRows() 也是分段错误。为了修复它,我不得不添加一个相当奇怪的解决方案。此评论部分太小,因此我将其作为单独的答案发布。您能否对它进行掠夺并谈谈它为什么起作用?【参考方案2】:

我尝试了@musicamante 的答案并遇到了一些问题。如果我删除行或移动行,Qt 会出现分段错误。我将在下面单独讨论这两个问题及其解决方案。

移动行

移动根索引的共同祖先会导致 Qt 出现分段错误。为了解决这个问题,我不得不更改 setRootIndex 来存储持久索引。

-         self._sourceRootIndex = rootIndex
+         self._sourceRootIndex = QtCore.QPersistentModelIndex(rootIndex)

删除行

这个比较棘手。我找到了一个解决方案,但我实际上并不知道为什么它有效。

_TreePersistentRootIndex.checkRootIndex 期间,如果找到的rootIndexself.rootIndex() 不匹配,则调用setRootIndex。如果删除模型中的一行并调用 _TreePersistentRootIndex.checkRootIndex,则 GUI 分段错误。为了解决这个问题,我添加了这个:

        if rootIndex != self.rootIndex():
+           rootIndex = rootIndex.model().index(
+               rootIndex.row(),
+               rootIndex.column(),
+               rootIndex.parent(),
+           )
            self.setRootIndex(rootIndex)

由于某种原因,重新获取根索引显式地防止了分段错误。

Here is a reproduction of the segmentation fault(如果您按原样加载该复制,它将按预期工作。但是如果您注释掉上面提到的行,只要按下“删除分支”按钮就会出现分段错误)

【讨论】:

以上是关于Qt - 每次调用 QSortFilterProxyModel::invalidateFilter 时都会重置 rootIndex的主要内容,如果未能解决你的问题,请参考以下文章

QT出现重复调用SLOT函数

Qt:QProcess调用终端+脚本

Qt 5.1 Windows 7 - Windows 注销时未调用 aboutToQuit()

QT读取文本文件

在全屏模式下运行 Qt 应用程序时出现“黑屏”问题

Qt利用QLocalSocket和QLocalServer实现IPC