【问题标题】:model for QTreeView crashes when layoutChanged is emitted发出 layoutChanged 时 QTreeView 的模型崩溃
【发布时间】:2021-10-03 16:14:04
【问题描述】:

我正在尝试编写一个模型(QAbstractItemModel 子类)来将数据填充到 QTreeView 中。我需要这些项目是可检查的,并希望稍后过滤掉所有未检查的内容。为了在过滤处于活动状态时正确隐藏未选中的项目,我需要在我的setData 方法中发出layoutChanged。然而,这一信号(与layoutAboutToBeChanged 结合使用)会在检查项目时导致不可预知的崩溃。我将其缩小到以下代码示例,我知道在检查项目时可以防止崩溃的两件事:

  1. 删除QSortFilterProxyModel
  2. 不要发出layoutChanged 信号

尽管如此,两者对我的过滤都至关重要(我从示例代码中取出了,因为它太多了)。要重现,您只需选择一个项目并按空格键 - 很快应用程序就会崩溃。有人看到我的模型有什么问题吗?我已经用 pytest-qt 中的QtModelTester 检查了它——没关系。

from __future__ import annotations

import string
import sys
from enum import IntEnum
from random import choices
from typing import List, Optional, Tuple, Union, cast

from PyQt6.QtCore import (
    QAbstractItemModel,
    QModelIndex,
    QObject,
    QPersistentModelIndex,
    QSortFilterProxyModel,
    Qt,
    pyqtSignal,
)
from PyQt6.QtWidgets import QApplication, QTreeView


class Node:
    """Node inside the tree model."""

    def __init__(self, name: Optional[str]) -> None:
        """Create a new Node."""
        self._name = name
        self._children: List[Node] = []
        self._parent: Optional[Node] = None
        self._check_state: Qt.CheckState = Qt.CheckState.Unchecked

    @property
    def name(self) -> Optional[str]:
        """Return the name of the Node."""
        return self._name

    @property
    def child_count(self) -> int:
        """Return the number of children for the Node."""
        return len(self._children)

    def child(self, row: int) -> Node:
        """Return the child for the given row."""
        return self._children[row]

    @property
    def children(self) -> List[Node]:
        """Return the list of children."""
        return self._children

    @property
    def parent(self) -> Optional[Node]:
        """Return the Node's parent element."""
        return self._parent

    @parent.setter
    def parent(self, parent: Node) -> None:
        """Set the Node's parent element."""
        self._parent = parent

    @property
    def row(self) -> int:
        """Return the row number of the Node."""
        if not self.parent:
            return -1
        return self.parent.children.index(self)

    def add_child(self, child: Node) -> None:
        """Add the given child to the Node's children."""
        child.parent = self
        self._children.append(child)

    def remove_child(self, child: Node) -> None:
        """Remove the given child to the Node's children."""
        self._children.remove(child)
        child.parent = None

    @property
    def check_state(self) -> Qt.CheckState:
        """Return the check state of the Node."""
        return self._check_state

    @check_state.setter
    def check_state(self, check_state: Qt.CheckState) -> None:
        """Set the check state of the Node."""
        self._check_state = check_state


class Model(QAbstractItemModel):
    """Model for data in a QTreeView."""

    add_filter = pyqtSignal(tuple, name="add_filter")
    remove_filter = pyqtSignal(tuple, name="remove_filter")

    class Header(IntEnum):
        """Header definitions."""

        NAME = 0

    def __init__(self, parent: Optional[QObject] = None) -> None:
        """Create a new Model to show data in a QTreeView."""
        super().__init__(parent)
        self._header_labels = {
            Model.Header.NAME: self.tr("Name"),
        }
        self._root = Node(None)
        self._path_cache: List[Tuple[str, ...]] = []
        self._blocked = False

    def flags(self, index: QModelIndex) -> Qt.ItemFlag:
        """Return the flags for the given index and model."""
        if index.isValid():
            return super().flags(index) | Qt.ItemFlag.ItemIsUserCheckable
        return super().flags(index)

    def columnCount(  # pylint: disable=invalid-name, no-self-use
        self, _: QModelIndex = QModelIndex()
    ) -> int:
        """Return the column count."""
        return len(self._header_labels)

    def rowCount(  # pylint: disable=invalid-name
        self, parent: QModelIndex = QModelIndex()
    ) -> int:
        """Return the row count."""
        if parent.isValid():
            return cast(Node, parent.internalPointer()).child_count
        count = self._root.child_count
        return count

    def headerData(  # pylint: disable=invalid-name
        self,
        section: int,
        orientation: Qt.Orientation,
        role: int = Qt.ItemDataRole.DisplayRole,
    ) -> Optional[str]:
        """Return the header data for the given section."""
        if (
            orientation == Qt.Orientation.Horizontal
            and role == Qt.ItemDataRole.DisplayRole
        ):
            return self._header_labels[Model.Header(section)]
        return None

    def on_add_path(self, path: Tuple[str, ...]) -> None:
        """Add a path to the model if it doesn't exist yet."""
        if path not in self._path_cache:
            self._add_path_to_node(path, QModelIndex())
            self._path_cache.append(path)

    def _add_path_to_node(
        self, path: Tuple[str, ...], index: QModelIndex
    ) -> None:
        """Add the path to the given node if it doesn't exist yet."""
        if index.isValid():
            node: Node = index.internalPointer()
        else:
            node = self._root
        for child in node.children:
            if child.name == path[0]:
                self._add_path_to_node(
                    path[1:], self.index(child.row, 0, index)
                )
                return
        new_node = Node(path[0])
        self.beginInsertRows(index, node.child_count, node.child_count)
        node.add_child(new_node)
        self.endInsertRows()
        if path[1:]:
            self._add_path_to_node(
                path[1:], self.index(new_node.row, 0, index)
            )

    def parent(self, child: QModelIndex) -> QModelIndex:  # type: ignore
        """Return the parent index for a given index."""
        if not child.isValid():
            return QModelIndex()

        child_item = child.internalPointer()
        parent_item = child_item.parent

        if parent_item is self._root:
            return QModelIndex()

        return self.createIndex(parent_item.row, 0, parent_item)

    def hasChildren(  # pylint: disable=invalid-name
        self, index: QModelIndex = QModelIndex()
    ) -> bool:
        """Evaluate if children exist for the given index."""
        if index.isValid():
            return bool(index.internalPointer().child_count)
        return bool(self._root.child_count)

    def index(
        self, row: int, col: int, parent: QModelIndex = QModelIndex()
    ) -> QModelIndex:
        """Return an index for the given row and column and parent."""
        # a not existent index should never be requested
        assert self.hasIndex(row, col, parent)

        if not parent.isValid():
            parent_item = self._root
        else:
            parent_item = parent.internalPointer()

        return self.createIndex(row, col, parent_item.child(row))

    def data(  # pylint: disable=no-self-use
        self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole
    ) -> Union[str, Qt.CheckState, None]:
        """Return data for the given index and role."""
        if role == Qt.ItemDataRole.DisplayRole:
            item = cast(Node, index.internalPointer())
            return item.name
        if role == Qt.ItemDataRole.CheckStateRole:
            item = cast(Node, index.internalPointer())
            return item.check_state
        return None

    def setData(  # pylint: disable=invalid-name
        self,
        index: QModelIndex,
        value: Qt.CheckState,
        role: int = Qt.ItemDataRole.EditRole,
    ) -> bool:
        """Set the data for the given index and role."""
        if role == Qt.ItemDataRole.CheckStateRole:
            persistent_idx = QPersistentModelIndex(index)
            self.layoutAboutToBeChanged.emit([persistent_idx])
            item = cast(Node, index.internalPointer())
            item.check_state = Qt.CheckState(value)
            self.changePersistentIndex(index, index)
            self.dataChanged.emit(
                index, index, [Qt.ItemDataRole.CheckStateRole]
            )
            self.layoutChanged.emit([persistent_idx])
            return True
        return False


if __name__ == "__main__":
    APP = QApplication(sys.argv)
    model = Model()
    rand_words = [
        "".join(choices(string.ascii_uppercase + string.digits, k=6))
        for _ in range(20)
    ]
    for char in "ABCDEFG":
        for number in range(100000, 100010):
            for word in rand_words:
                model.on_add_path((char, str(number), word))
    filter_model = QSortFilterProxyModel()
    filter_model.setSourceModel(model)
    view = QTreeView()
    view.setModel(filter_model)
    view.show()
    sys.exit(APP.exec())

【问题讨论】:

    标签: python pyqt pyqt6


    【解决方案1】:

    问题是您不必要地调用了 changePersistentIndex 函数,因为这将使 persistent_idx 为空(或至少关联的 C++ 对象)导致分段错误,因为例如 layoutChanged 发出的对象具有已删除的内存。解决方法:删除self.changePersistentIndex(index, index)

    【讨论】:

    • 感谢您的意见。 QAbstractItemModel::changePersistentIndex 的源代码实现与您解释的内容不同。它从持久索引列表中删除索引(据我所知),但它不会删除底层 C++ 对象。如果是这种情况,PyQt 会引发 TypeError,但它不会(它会因 Qt 实现直接导致的内存冲突而崩溃)。它尝试了你建议的解决方案,结果是一样的,应用程序仍然崩溃。
    • @TilmanK 1) 只有当函数接收到与预期类型不同的对象时,才应该抛出 TypeError,在这种情况下不是。 2)删除 QPersintentModelIndex 对象还意味着删除指针(例如内部数据),因此如果您稍后访问该对象,则会产生分段错误。 3)删除该行我没有看到该错误。 4) 我推荐使用 gdb 来调试你的代码。
    • 你是对的,这是一个 RuntimeError:“RuntimeError: QWidget 类型的包装 C/C++ 对象已被删除”。不过,如果我删除代码,它会崩溃。如果我可以问一下,您使用的是什么 Python 版本、PyQt 版本和操作系统?
    • @TilmanK 我在 Linux 上使用 python 3.9.6 和 PyQt6 6.1.1
    • 好的,这里的区别是,我在 Windows 上。 PyQt5 和 PySide2/6 也会出现这个问题。在 VSC 中运行时,我收到一条错误消息: QSortFilterProxyModel: index from wrong model passing to mapFromSource
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多