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