问题描述
我正在尝试在QTreeView中构建条目的小列表,并基于on the example posted here,我通过添加的右键单击上下文菜单删除了所有子项。但是当我删除它时,我的父树就崩溃了。在某些情况下,如果我按特定顺序删除某个项目,则会崩溃
我的理解是,这是因为删除后项目的索引会更改,并且为了防止QtCore.QPersistentModelIndex()
可以根据以下线程使用:How to delete multiple rows in a QTableView widget?
尽管该示例使用了QStandardItemmodel()
,但是由于我的使用了qabstractitemmodel()
,我如何实现类似的概念,并防止崩溃……?
import sys
from functools import partial
from PyQt4 import QtGui,QtCore
HORIZONTAL_HEADERS = ("Asset Name","Date Added")
class AssetClass(object):
'''
a trivial custom data object
'''
def __init__(self,**kwargs):
if not kwargs.get('name') or not kwargs.get('type'):
return
self.name = kwargs.get('name')
self.date_added = kwargs.get('date_added')
self.type = kwargs.get('type')
def __repr__(self):
return "%s - %s %s" % (self.type,self.name,self.date_added)
class TreeItem(object):
'''
a python object used to return row/column data,and keep note of
it's parents and/or children
'''
def __init__(self,asset,header,parent_item):
self.asset = asset
self.parent_item = parent_item
self.header = header
self.child_items = []
def appendChild(self,item):
self.child_items.append(item)
def removeChild(self,item):
print 'removeChild: item is %s' % item
print 'removeChild: self.child_items is %s' % self.child_items
self.child_items.remove(item)
def child(self,row):
return self.child_items[row]
def childCount(self):
return len(self.child_items)
def columnCount(self):
return 2
def data(self,column):
if self.asset == None:
if column == 0:
return QtCore.QVariant(self.header)
if column == 1:
return QtCore.QVariant("")
else:
if column == 0:
return QtCore.QVariant(self.asset.name)
if column == 1:
return QtCore.QVariant(self.asset.date_added)
return QtCore.QVariant()
def parent(self):
return self.parent_item
def row(self):
if self.parent_item:
return self.parent_item.child_items.index(self)
return 0
class TreeModel(QtCore.qabstractitemmodel):
'''
a model to display a few names,ordered by sex
'''
def __init__(self,parent=None):
super(TreeModel,self).__init__(parent)
self.assets = []
model_data = (("VEHICLE","Truck",'May 27th,2020'),("VEHICLE","Car",'May 25th,("CHaraCTER","Peter","Rachel",'May 29th,("PROP","Chair","Axe",'May 17th,2020'))
for asset_type,name,date in model_data:
asset = AssetClass(type=asset_type,name=name,date_added=date)
self.assets.append(asset)
self.rootItem = TreeItem(None,"ALL",None)
self.parents = {0: self.rootItem}
self.setupModelData()
def columnCount(self,parent=None):
if parent and parent.isValid():
return parent.internalPointer().columnCount()
else:
return len(HORIZONTAL_HEADERS)
def data(self,index,role):
if not index.isValid():
return QtCore.QVariant()
item = index.internalPointer()
if role == QtCore.Qt.displayRole:
return item.data(index.column())
if role == QtCore.Qt.UserRole:
if item:
return item.asset
return QtCore.QVariant()
def headerData(self,column,orientation,role):
if (orientation == QtCore.Qt.Horizontal and
role == QtCore.Qt.displayRole):
try:
return QtCore.QVariant(HORIZONTAL_HEADERS[column])
except IndexError:
pass
return QtCore.QVariant()
def index(self,row,parent):
if not self.hasIndex(row,parent):
return QtCore.QModelIndex()
if not parent.isValid():
parent_item = self.rootItem
else:
parent_item = parent.internalPointer()
childItem = parent_item.child(row)
if childItem:
return self.createIndex(row,childItem)
else:
return QtCore.QModelIndex()
def parent(self,index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
if not childItem:
return QtCore.QModelIndex()
parent_item = childItem.parent()
if parent_item == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parent_item.row(),parent_item)
def rowCount(self,parent=QtCore.QModelIndex()):
if parent.column() > 0:
return 0
if not parent.isValid():
p_Item = self.rootItem
else:
p_Item = parent.internalPointer()
return p_Item.childCount()
def setupModelData(self):
for asset in self.assets:
asset_type = asset.type
if not self.parents.has_key(asset_type):
new_parent = TreeItem(None,asset_type,self.rootItem)
self.rootItem.appendChild(new_parent)
self.parents[asset_type] = new_parent
print 'self.parents: ',self.parents
parent_item = self.parents[asset_type]
new_item = TreeItem(asset,"",parent_item)
parent_item.appendChild(new_item)
def addSubrow(self,new_asset):
asset_type,date = new_asset
asset = AssetClass(type=asset_type,date_added=date)
parent_item = self.parents[asset_type]
already_exists = False
for child in parent_item.child_items:
if child.asset.name == name and child.asset.type == asset_type:
already_exists = True
if already_exists:
print 'this asset already exists'
return
new_item = TreeItem(asset,parent_item)
parent_item.appendChild(new_item)
def removeRow(self,rowIndexes):
child_tree_item = rowIndexes[0].internalPointer()
asset_type = rowIndexes[0].parent().data().toString()
parent_item = self.parents[str(asset_type)]
# hint to keep the tree open after deleting: https://stackoverflow.com/questions/48121393/how-to-delete-multiple-rows-in-a-qtableview-widget
self.beginRemoveRows(QtCore.QModelIndex(),rowIndexes[0].row(),rowIndexes[0].row() + 1)
parent_item.removeChild(child_tree_item)
self.endRemoveRows()
def searchModel(self,asset):
'''
get the modelIndex for a given appointment
'''
def searchNode(node):
'''
a function called recursively,looking at all nodes beneath node
'''
for child in node.child_items:
print child.childCount()
if asset == child.asset:
index = self.createIndex(child.row(),child)
return index
if child.childCount() > 0:
result = searchNode(child)
if result:
return result
retarg = searchNode(self.parents[0])
print retarg
return retarg
def findAssetName(self,name):
app = None
for asset in self.assets:
print asset.name
if asset.name == name:
app = asset
break
if app != None:
index = self.searchModel(app)
return (True,index)
return (False,None)
class TreeView(QtGui.QTreeView):
right_button_clicked = QtCore.pyqtSignal(list,int)
def __init__(self,parent=None):
super(TreeView,self).__init__(parent)
# self.clicked.connect(self.on_treeView_clicked)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.openMenu)
def selectedRows(self):
rows = []
for index in self.selectedindexes():
if index.column() == 0:
rows.append(index.row())
print type(rows)
return rows
def openMenu(self,position):
indexes = self.selectedindexes()
if len(indexes) > 0:
level = 0
index = indexes[0]
while index.parent().isValid():
index = index.parent()
level += 1
menu = QtGui.QMenu()
editMenu = None
if level == 0:
editMenu = QtGui.QAction("Edit person",self)
menu.addAction(editMenu)
elif level == 1:
editMenu = QtGui.QAction("Delete",self)
menu.addAction(editMenu)
elif level == 2:
editMenu = QtGui.QAction("Edit object",self)
menu.addAction(editMenu)
if editMenu:
editMenu.triggered.connect(partial(self._on_right_click,indexes,level))
menu.exec_(self.viewport().mapToGlobal(position))
def _on_right_click(self,level):
self.right_button_clicked.emit(indexes,level)
def delete_test(self):
print 'addButton clicked'
new_asset = ("CHaraCTER","Smith",'May 28th,2020')
asset_type,date_added=date)
parent_item = self.tree_view.model().parents[asset_type]
already_exists = False
for child in parent_item.child_items:
if child.asset.name == name and child.asset.type == asset_type:
already_exists = True
if already_exists:
print 'this asset already exists'
return
new_item = TreeItem(asset,parent_item)
parent_item.appendChild(new_item)
self.tree_view.model().layoutChanged.emit()
class Dialog(QtGui.QDialog):
add_signal = QtCore.pyqtSignal(int)
def __init__(self,parent=None):
super(Dialog,self).__init__(parent)
self.setMinimumSize(300,150)
self.model = TreeModel()
layout = QtGui.QVBoxLayout(self)
self.tree_view = TreeView(self)
self.tree_view.setModel(self.model)
self.tree_view.setAlternatingRowColors(True)
self.tree_view.right_button_clicked.connect(self.deleteButtonClicked)
layout.addWidget(self.tree_view)
label = QtGui.QLabel("Search for the following person")
layout.addWidget(label)
buts = []
frame = QtGui.qframe(self)
layout2 = QtGui.QHBoxLayout(frame)
for asset in self.model.assets:
but = QtGui.QPushButton(asset.name,frame)
buts.append(but)
layout2.addWidget(but)
QtCore.QObject.connect(but,QtCore.SIGNAL("clicked()"),self.but_clicked)
layout.addWidget(frame)
self.add_button = QtGui.QPushButton("Add \"Character - Smith\"")
layout.addWidget(self.add_button)
QtCore.QObject.connect(self.add_button,self.addButtonClicked)
self.delete_button = QtGui.QPushButton("Delete Selected")
layout.addWidget(self.delete_button)
QtCore.QObject.connect(self.delete_button,self.tree_view.clearSelection)
self.but = QtGui.QPushButton("Clear Selection")
layout.addWidget(self.but)
QtCore.QObject.connect(self.but,self.tree_view.clearSelection)
QtCore.QObject.connect(self.tree_view,QtCore.SIGNAL("clicked (QModelIndex)"),self.row_clicked)
def row_clicked(self,index):
'''
when a row is clicked... show the name
'''
print 'row_clicked index type: %s' % index
print self.tree_view.model().data(index,QtCore.Qt.UserRole)
def but_clicked(self):
'''
when a name button is clicked,I iterate over the model,find the person with this name,and set the treeviews current item
'''
name = self.sender().text()
print "BUTTON CLICKED:",name
result,index = self.model.findAssetName(name)
if result:
if index:
self.tree_view.setCurrentIndex(index)
return
self.tree_view.clearSelection()
def addButtonClicked(self):
print 'addButton clicked'
new_asset = ("CHaraCTER",2020')
self.tree_view.model().addSubrow(new_asset)
self.tree_view.model().layoutChanged.emit()
@QtCore.pyqtSlot(list,int)
def deleteButtonClicked(self,level):
print 'deleteButton clicked'
self.tree_view.model().removeRow(indexes)
self.tree_view.model().layoutChanged.emit()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
dialog = Dialog()
dialog.show()
sys.exit(app.exec_())
解决方法
beginRemoveRows()
期望将要删除的QModelIndex的父对象QModelIndex作为第一个参数。对于您在表类型模型的代码注释中指示的示例,索引没有父级,因此传递了无效的QModelIndex。
def removeRow(self,rowIndexes):
child_tree_item = rowIndexes[0].internalPointer()
parent_item = child_tree_item.parent()
self.beginRemoveRows(
rowIndexes[0].parent(),rowIndexes[0].row(),rowIndexes[0].row() + 1
)
parent_item.removeChild(child_tree_item)
self.endRemoveRows()