我需要显示分层模型。用户展开节点时必须创建子节点。子节点的数量是事先不知道的。有些子节点可以在展开父节点后立即创建。而有些子节点需要时间通过发送请求获取数据,然后才能创建。
所以我创建了QTreeView + QSortFilterProxyModel + Qt模型(QAbstractItemModel继承者)+数据模型。下面的代码适用于代理模型。但是没有代理模型 + 对于立即创建的节点,我在扩展任何节点时都有
Process finished with exit code -1073741819 (0xC0000005)
。恐怕在有代理模式的情况下迟早也会出现这个错误
UPD 1:我添加了带有 QueuedConnection 的 _populate_request 信号,以将 fetchMore 调用堆栈与从模型中添加/删除节点“分开”(感谢@musicamante - 这与单发计时器的想法相同)。那有帮助。但这一步对我来说并不明显,我仍然想知道为什么直接调用会导致崩溃。
更新 2:添加了“重新加载”上下文菜单以重新加载子项。检查旧的子节点删除是否崩溃。
import random
import sys
import weakref
from enum import Enum, auto
from typing import Optional, List
from PyQt5 import QtCore, QtTest
from PyQt5.QtWidgets import QMainWindow, QTreeView, QVBoxLayout, QApplication, QMenu
from PyQt5.QtCore import QModelIndex, Qt
class TreeNodeStatus(Enum):
NotPopulated = auto()
Populating = auto()
Populated = auto()
Error = auto()
class TreeNode(QtCore.QObject):
""" Node of objects tree; root node is essentially a data model """
status_changed = QtCore.pyqtSignal(object) # node
before_children_added = QtCore.pyqtSignal(object, int, int) # parent_node, pos, count
children_added = QtCore.pyqtSignal(object, int, int) # parent_node, pos, count
before_children_removed = QtCore.pyqtSignal(object, int, int) # parent_node, pos, count
children_removed = QtCore.pyqtSignal(object, int, int) # parent_node, pos, count
_populate_request = QtCore.pyqtSignal()
def __init__(self, name: str, parent: Optional['TreeNode']):
super().__init__()
self._name = name
self._parent_ref = weakref.ref(parent) if parent is not None else lambda: None
self._status: TreeNodeStatus = TreeNodeStatus.NotPopulated
self._children: List[TreeNode] = []
# to listen root node signals only
if parent is not None:
self.status_changed.connect(parent.status_changed)
self.before_children_added.connect(parent.before_children_added)
self.children_added.connect(parent.children_added)
self.before_children_removed.connect(parent.before_children_removed)
self.children_removed.connect(parent.children_removed)
# to imitate minimal delay between fetchMore > populate call stack and adding/removing nodes;
# for nodes that can be created immediately in populate direct calls
# fetchMore > populate > _on_children_received causes crash;
# using of this signal prevents crash
self._populate_request.connect(self._populate, Qt.ConnectionType.QueuedConnection)
# for nodes that can not be created immediately in populate;
# to imitate delay due to getting response to a request
self._timer = QtCore.QTimer()
self._timer.setSingleShot(True)
self._timer.setInterval(2 * 1000) # 2s
self._timer.timeout.connect(self._on_children_received)
def parent(self) -> Optional['TreeNode']:
return self._parent_ref()
@property
def status(self) -> TreeNodeStatus:
return self._status
def _set_status(self, status: TreeNodeStatus):
self._status = status
self.status_changed.emit(self)
def populate(self):
# # signal with QueuedConnection - works good
# self._populate_request.emit()
# direct call causes app crash for nodes that can be created immediately and if there is no proxy model
self._populate()
def _populate(self):
# loading was started for this node already, exit
if self.status == TreeNodeStatus.Populating:
return
# start loading
self._set_status(TreeNodeStatus.Populating)
# forget old children
old_children_count = len(self._children)
self.before_children_removed.emit(self, 0, old_children_count)
# disconnect signals
for child in self._children:
child.status_changed.disconnect(self.status_changed)
child.before_children_added.disconnect(self.before_children_added)
child.children_added.disconnect(self.children_added)
child.before_children_removed.disconnect(self.before_children_removed)
child.children_removed.disconnect(self.children_removed)
self._children.clear()
self.children_removed.emit(self, 0, old_children_count)
# request data about children nodes
# # timer - for nodes that can not be created immediately
# self._timer.start()
# direct call - for nodes that can be created immediately
self._on_children_received()
def children(self) -> List['TreeNode']:
return self._children
@property
def name(self) -> str:
return self._name
def _on_children_received(self):
print('!_on_children_received', self.name)
# create children nodes
new_children_count = random.randint(0, 4)
self.before_children_added.emit(self, 0, new_children_count)
self._children = [TreeNode(self.name + ' ' + str(i), self) for i in range(new_children_count)]
self.children_added.emit(self, 0, new_children_count)
# update status
self._set_status(TreeNodeStatus.Populated)
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, root_node: TreeNode):
super().__init__()
# root node == data model
self._root_node = root_node
self._root_node.status_changed.connect(self._on_node_status_changed)
self._root_node.before_children_added.connect(self._before_children_added)
self._root_node.children_added.connect(self._on_children_added)
self._root_node.before_children_removed.connect(self._before_children_removed)
self._root_node.children_removed.connect(self._on_children_removed)
def index(self, row: int, column: int, parent=QModelIndex(), *args, **kwargs) -> QModelIndex:
# discard non-existent indices: check for row/column for given parent inside
if not self.hasIndex(row, column, parent):
return QModelIndex()
# get parent node by index
if parent is None or not parent.isValid():
parent_node: TreeNode = self._root_node
else:
parent_node: TreeNode = parent.internalPointer()
# if has child with given row
if row < len(parent_node.children()):
# create index with node as internalPointer
return self.createIndex(row, column, parent_node.children()[row])
return QModelIndex()
def parent(self, index: QModelIndex = None) -> QModelIndex:
# invalid index => root node
if not index.isValid():
return QModelIndex()
node: TreeNode = index.internalPointer()
parent_node: TreeNode = node.parent()
# if parent is root node, return invalid index
if parent_node is self._root_node:
return QModelIndex()
# get row of parent node; parent_node is not root, must have it's own parent
grandparent_node = parent_node.parent()
parent_row = grandparent_node.children().index(parent_node)
# create index with node as internalPointer
return self.createIndex(parent_row, 0, parent_node)
def hasChildren(self, parent=QModelIndex(), *args, **kwargs) -> bool:
# can we expand node? if we can we have a triangle to the left of the node
parent_node = self._node_from_index(parent)
# children loaded - look at the number
if parent_node.status == TreeNodeStatus.Populated:
return len(parent_node.children()) > 0
# error - no children, can't expand
elif parent_node.status == TreeNodeStatus.Error:
return False
# not loaded/loading - assume they are
else:
return True
def canFetchMore(self, parent: QModelIndex) -> bool:
# can we get more data (child nodes) for parent?
# print('canFetchMore!', self._node_from_index(parent).name)
return self._can_fetch_more(parent)
def _can_fetch_more(self, parent: QModelIndex) -> bool:
parent_node = self._node_from_index(parent)
# children are not loaded/loading - assume they are
if parent_node.status == TreeNodeStatus.NotPopulated:
return True
# in other cases - can not get more child nodes
elif parent_node.status in [TreeNodeStatus.Populating,
TreeNodeStatus.Populated,
TreeNodeStatus.Error]:
return False
assert False
def fetchMore(self, parent: QModelIndex) -> None:
# get more data (child nodes) for parent
print('!FetchMore', self._node_from_index(parent).name)
if not self._can_fetch_more(parent):
return
parent_node = self._node_from_index(parent)
if parent_node.status != TreeNodeStatus.Populating:
parent_node.populate()
def rowCount(self, parent=QModelIndex(), *args, **kwargs):
parent_node = self._node_from_index(parent)
return len(parent_node.children())
def columnCount(self, parent=None, *args, **kwargs):
return 1
def _node_from_index(self, index: Optional[QModelIndex]) -> TreeNode:
# invalid index - root node
if index is None or not index.isValid():
return self._root_node
else:
return index.internalPointer()
def _index_from_node(self, node: TreeNode) -> Optional[QModelIndex]:
# root node - invalid index
if node is self._root_node:
return QModelIndex()
# according to the principle from index method
parent_node = node.parent()
row = parent_node.children().index(node)
return self.createIndex(row, 0, node)
def data(self, index, role=None):
node = self._node_from_index(index)
if role == Qt.DisplayRole:
return node.name
# get nodes by UserRole
elif role == Qt.UserRole:
return node
elif role == Qt.DecorationRole:
pass
def _on_node_status_changed(self, node: TreeNode):
index = self._index_from_node(node)
if index is not None:
# notify about changes - icon, tooltip
self.dataChanged.emit(index, index, [Qt.DecorationRole, Qt.ToolTipRole])
def _before_children_removed(self, parent_node: TreeNode, pos: int, count: int):
parent_index = self._index_from_node(parent_node)
if parent_index is not None:
self.beginRemoveRows(parent_index, pos, pos + count - 1)
def _on_children_removed(self, parent_node: TreeNode, pos: int, count: int):
self.endRemoveRows()
def _before_children_added(self, parent_node: TreeNode, pos: int, count: int):
parent_index = self._index_from_node(parent_node)
if parent_index is not None:
self.beginInsertRows(parent_index, pos, pos + count - 1)
print('!beginInsertRows', parent_node.name)
def _on_children_added(self, parent_node: TreeNode, pos: int, count: int):
self.endInsertRows()
print('!endInsertRows', parent_node.name)
class TreeView(QTreeView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._menu = QMenu(self)
# reload child nodes
self._reload_act = self._menu.addAction('Reload')
self._reload_act.triggered.connect(self._on_reload)
def mouseReleaseEvent(self, event):
""" Call context menu on right click button release """
super().mouseReleaseEvent(event)
if event.button() == Qt.MouseButton.RightButton:
index = self.indexAt(event.pos())
# above nodes only
if index.isValid():
self._menu.popup(self.viewport().mapToGlobal(event.pos()))
def _on_reload(self):
index = self.currentIndex()
node = index.data(role=Qt.UserRole)
if node.status != TreeNodeStatus.Populating:
node.populate()
class ClientWindow(QMainWindow):
def __init__(self):
super().__init__()
self._setup_ui()
root_node = TreeNode('root', None)
model = TreeModel(root_node)
# proxy = QtCore.QSortFilterProxyModel()
# proxy.setSourceModel(model)
# FixMe crash on expanding any node if we put source model here
self._view.setModel(model)
def _setup_ui(self):
self._view = TreeView()
self._view.setSortingEnabled(True)
central_wdg = self._view
central_vlt = QVBoxLayout()
central_wdg.setLayout(central_vlt)
self.setCentralWidget(central_wdg)
if __name__ == '__main__':
app = QApplication([])
main_window = ClientWindow()
main_window.show()
sys.exit(app.exec())
问题出在 TreeNode._populate 方法中。我们只需要在
before_children_removed
时发出count > 0
信号。像这样
if old_children_count > 0:
self.before_children_removed.emit(self, 0, old_children_count)
与
children_removed
信号相同。
if old_children_count > 0:
self.children_removed.emit(self, 0, old_children_count)
所以不再需要
_populate_request
信号和其他解决方法。
before_children_removed
时发送old_children_count == 0
信号是错误的。这种情况出现在第一次 populate
调用节点时。
在那种情况下,我与
TreeModel._before_children_removed
和pos=0
进行了count=0
通话,TreeModel.beginRemoveRows
与first=0
和last=-1
在里面。这些值在 Qt 源代码中触发Q_ASSERT(last >= first);
并导致另一个后续错误并最终崩溃。
void QAbstractItemModel::beginRemoveRows(const QModelIndex &parent, int first, int last)
{
Q_ASSERT(first >= 0);
Q_ASSERT(last >= first);
Q_ASSERT(last < rowCount(parent));
Q_D(QAbstractItemModel);
d->changes.push(QAbstractItemModelPrivate::Change(parent, first, last));
emit rowsAboutToBeRemoved(parent, first, last, QPrivateSignal());
d->rowsAboutToBeRemoved(parent, first, last);
}
好奇这个
Q_ASSERT
在使用发布Qt DLL时不会导致应用程序崩溃。 Release DLLs with debug symbols 仅显示另一个后续错误,后果。我们只能通过调试 Qt DLL 才能看到真正的原因。