QTreeView, QAbstractItemModel. 在展开节点时应用程序退出。

huangapple go评论112阅读模式
英文:

QTreeView, QAbstractItemModel. App exits while expanding node

问题

I need to display the hierarchical model. Child nodes must be created when the user expands the node. The number of child nodes is not known in advance. Some child nodes can be created immediately after expanding the parent node. And some child nodes need time to get data by sending a request and only then can be created.

So I create QTreeView + QSortFilterProxyModel + Qt model (QAbstractItemModel inheritor) + data model. The code below works fine with the proxy model. But without a proxy model + for nodes that are created immediately I have Process finished with exit code -1073741819 (0xC0000005) while expanding any node. I'm afraid that this error will also appear in the presence of a proxy model sooner or later.

UPD 1: I added the _populate_request signal with QueuedConnection to 'separate' fetchMore call stack from adding/removing nodes from the model (thanks to @musicamante - it's the same idea as a single-shot timer). That helped. But this step is not obvious for me, and I'm still open to ideas why direct calls lead to a crash.

UPD 2: Added "Reload" context menu to reload children. To check old child nodes removing for a crash.

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

# ... (The code continues)

Note: The code you provided seems to be in Python and includes various classes and signals for managing a hierarchical model in PyQt. If you have any specific questions or need further assistance with this code, please let me know.

英文:

I need to display the hierarchical model. Child nodes must be created when user expands the node. The number of child nodes is not known in advance. Some child nodes can be created immediately after expanding parent node. And some child nodes need time to get data by sending request and only then can be created.

So I create QTreeView + QSortFilterProxyModel + Qt model (QAbstractItemModel inheritor) + data model. The code below works fine with the proxy model. But without proxy model + for nodes that created immediately I have Process finished with exit code -1073741819 (0xC0000005) while expanding any node. I'm afraid that this error will also appear in the presence of a proxy model sooner or later.

UPD 1: I added the _populate_request signal with QueuedConnection to 'separate' fetchMore call stack from adding/removing nodes from model (thanks to @musicamante - it's the same idea as a single shot timer). That helped. But this step is not obvious for me and I still open for ideas why direct calls leads to crash.

UPD 2: Added "Reload" context menu to reload children. To check old child nodes removing for crash.

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

答案1

得分: 0

问题出在TreeNode._populate方法中。我们只需要在count > 0 时才发出 before_children_removed 信号。像这样。

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 信号和其他的变通方法。

解释

old_children_count == 0 时发送 before_children_removed 信号是一个错误。这种情况出现在节点的第一次 populate 调用期间。

在这种情况下,我有一个 TreeModel._before_children_removed 调用,pos=0count=0TreeModel.beginRemoveRows 调用内部的 first=0last=-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);
}

有趣的是,当使用发布版的Qt DLLs时,这个 Q_ASSERT 并不会导致应用程序崩溃。带有调试符号的发布DLLs只会显示另一个后续错误和后果。只有在使用调试版Qt DLLs时才能看到真正的原因。

英文:

The problem was in TreeNode._populate method. We need to emit before_children_removed signal only when count &gt; 0. Like this.

if old_children_count &gt; 0:
self.before_children_removed.emit(self, 0, old_children_count)

Same for children_removed signal.

if old_children_count &gt; 0:
self.children_removed.emit(self, 0, old_children_count)

So no more need for _populate_request signal and other workarounds.

Explanation

It was a mistake to send before_children_removed signal when old_children_count == 0. This situation arose during the first populate call for the node.

In that case I had a TreeModel._before_children_removed call with pos=0 and count=0, TreeModel.beginRemoveRows with first=0 and last=-1 inside. These values triggers Q_ASSERT(last &gt;= first); in Qt source code and caused another subsequent errors and finally crash.

void QAbstractItemModel::beginRemoveRows(const QModelIndex &amp;parent, int first, int last)
{
Q_ASSERT(first &gt;= 0);
Q_ASSERT(last &gt;= first);
Q_ASSERT(last &lt; rowCount(parent));
Q_D(QAbstractItemModel);
d-&gt;changes.push(QAbstractItemModelPrivate::Change(parent, first, last));
emit rowsAboutToBeRemoved(parent, first, last, QPrivateSignal());
d-&gt;rowsAboutToBeRemoved(parent, first, last);
}

Curious that this Q_ASSERT does not lead to app crash when using release Qt DLLs. Release DLLs with debug symbols shows only another subsequent errors, consequences. The real reason we can see only with debug Qt DLLs.

huangapple
  • 本文由 发表于 2023年2月9日 02:08:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/75390016.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定