0

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

  1. 去除QSortFilterProxyModel
  2. 不发出layoutChanged信号

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

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

1 回答 1

0

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

于 2021-07-27T21:16:36.247 回答