PyQt:在QModelIndex的内部指针中存储复杂的元数据



这可能更适合作为错误报告,但也许我只是误解了什么。

我想实现我自己的分层QAbstractItemModel,以在QTreeView中显示对象树。我从可编辑的树模型示例 [1] 开始,它工作正常。

但是,我的索引中需要更多的元数据,因为我有不同类型的节点(或者更确切地说,节点具有属性)和一个简单的(行、列、对象指针)元组是不够的。另请参阅在 QModelIndex 中存储两种不同类型的另一个示例。

所以我创建了另一个保存这些数据的数据结构:

# this is just an example
class TempContainer:
def __init__(self, name, obj):
self.name = name
self.obj = obj

我创建了一个这样的索引:

# in TreeModel.index():
o = TempContainer("child", childItem)
self.tmpcache.append(o) # just to keep a reference alive
return self.createIndex(row, column, o)

现在可以按如下方式实现访问对象:

def getItem(self, index):
if index.isValid():
item = index.internalPointer()
if item:
return item.obj
return self.rootItem

但是,如果我添加此层元数据,则选择和编辑项目将不再起作用。我仍然可以选择根项目第一列中的单元格并编辑第一列中的所有项目。但是例如。选择子项目或第二列中的项目不再有效!

我注意到如果我将我的 TempContainer 存储在相应的 TreeItem 中,它可以工作,但这不是一个真正的选择,因为我的完整代码中有多个容器指向同一个项目。

我在Linux(Fedora 24)下运行python 3.5.3,使用PyQt 5.6.2(我也尝试过5.8.2)和Qt 5.6.2。


[1] 不幸的是,我无法附加完整源代码的 ZIP,但代码可在 https://www.riverbankcomputing.com/software/pyqt/download5 下获得,/examples/itemviews/editabletreemodel


修改后 editabletreemodel.py 的完整源代码: (请注意,我只修改了几行,您仍然需要示例中的其他文件来运行它。

#!/usr/bin/env python

#############################################################################
##
## Copyright (C) 2013 Riverbank Computing Limited.
## Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
## All rights reserved.
##
## This file is part of the examples of PyQt.
##
## $QT_BEGIN_LICENSE:BSD$
## You may use this file under the terms of the BSD license as follows:
##
## "Redistribution and use in source and binary forms, with or without
## modification, are permitted provided that the following conditions are
## met:
##   * Redistributions of source code must retain the above copyright
##     notice, this list of conditions and the following disclaimer.
##   * Redistributions in binary form must reproduce the above copyright
##     notice, this list of conditions and the following disclaimer in
##     the documentation and/or other materials provided with the
##     distribution.
##   * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
##     the names of its contributors may be used to endorse or promote
##     products derived from this software without specific prior written
##     permission.
##
## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
## $QT_END_LICENSE$
##
#############################################################################

from PyQt5.QtCore import (QAbstractItemModel, QFile, QIODevice,
QItemSelectionModel, QModelIndex, Qt)
from PyQt5.QtWidgets import QApplication, QMainWindow
import editabletreemodel_rc
from ui_mainwindow import Ui_MainWindow

class TreeItem(object):
def __init__(self, data, parent=None):
self.parentItem = parent
self.itemData = data
self.childItems = []
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def childNumber(self):
if self.parentItem != None:
return self.parentItem.childItems.index(self)
return 0
def columnCount(self):
return len(self.itemData)
def data(self, column):
return self.itemData[column]
def insertChildren(self, position, count, columns):
if position < 0 or position > len(self.childItems):
return False
for row in range(count):
data = [None for v in range(columns)]
item = TreeItem(data, self)
self.childItems.insert(position, item)
return True
def insertColumns(self, position, columns):
if position < 0 or position > len(self.itemData):
return False
for column in range(columns):
self.itemData.insert(position, None)
for child in self.childItems:
child.insertColumns(position, columns)
return True
def parent(self):
return self.parentItem
def removeChildren(self, position, count):
if position < 0 or position + count > len(self.childItems):
return False
for row in range(count):
self.childItems.pop(position)
return True
def removeColumns(self, position, columns):
if position < 0 or position + columns > len(self.itemData):
return False
for column in range(columns):
self.itemData.pop(position)
for child in self.childItems:
child.removeColumns(position, columns)
return True
def setData(self, column, value):
if column < 0 or column >= len(self.itemData):
return False
self.itemData[column] = value
return True

class TempContainer:
def __init__(self, name, obj):
self.name = name
self.obj = obj
class TreeModel(QAbstractItemModel):
def __init__(self, headers, data, parent=None):
super(TreeModel, self).__init__(parent)
rootData = [header for header in headers]
self.rootItem = TreeItem(rootData)
self.setupModelData(data.split("n"), self.rootItem)
self.tmpcache = []
def columnCount(self, parent=QModelIndex()):
return self.rootItem.columnCount()
def data(self, index, role):
if not index.isValid():
return None
if role != Qt.DisplayRole and role != Qt.EditRole:
return None
item = self.getItem(index)
return item.data(index.column())
def flags(self, index):
if not index.isValid():
return 0
return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
def getItem(self, index):
if index.isValid():
item = index.internalPointer()
if item:
return item.obj
return self.rootItem
def headerData(self, section, orientation, role=Qt.DisplayRole):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.rootItem.data(section)
return None
def index(self, row, column, parent=QModelIndex()):
if parent.isValid() and parent.column() != 0:
return QModelIndex()
parentItem = self.getItem(parent)
childItem = parentItem.child(row)
if childItem:
o = TempContainer("child", childItem)
self.tmpcache.append(o)
return self.createIndex(row, column, o)
else:
return QModelIndex()
def insertColumns(self, position, columns, parent=QModelIndex()):
self.beginInsertColumns(parent, position, position + columns - 1)
success = self.rootItem.insertColumns(position, columns)
self.endInsertColumns()
return success
def insertRows(self, position, rows, parent=QModelIndex()):
parentItem = self.getItem(parent)
self.beginInsertRows(parent, position, position + rows - 1)
success = parentItem.insertChildren(position, rows,
self.rootItem.columnCount())
self.endInsertRows()
return success
def parent(self, index):
if not index.isValid():
return QModelIndex()
childItem = self.getItem(index)
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QModelIndex()
o = TempContainer("parent", parentItem)
self.tmpcache.append(o)
return self.createIndex(parentItem.childNumber(), 0, o)
def removeColumns(self, position, columns, parent=QModelIndex()):
self.beginRemoveColumns(parent, position, position + columns - 1)
success = self.rootItem.removeColumns(position, columns)
self.endRemoveColumns()
if self.rootItem.columnCount() == 0:
self.removeRows(0, self.rowCount())
return success
def removeRows(self, position, rows, parent=QModelIndex()):
parentItem = self.getItem(parent)
self.beginRemoveRows(parent, position, position + rows - 1)
success = parentItem.removeChildren(position, rows)
self.endRemoveRows()
return success
def rowCount(self, parent=QModelIndex()):
parentItem = self.getItem(parent)
return parentItem.childCount()
def setData(self, index, value, role=Qt.EditRole):
if role != Qt.EditRole:
return False
item = self.getItem(index)
result = item.setData(index.column(), value)
if result:
self.dataChanged.emit(index, index)
return result
def setHeaderData(self, section, orientation, value, role=Qt.EditRole):
if role != Qt.EditRole or orientation != Qt.Horizontal:
return False
result = self.rootItem.setData(section, value)
if result:
self.headerDataChanged.emit(orientation, section, section)
return result
def setupModelData(self, lines, parent):
parents = [parent]
indentations = [0]
number = 0
while number < len(lines):
position = 0
while position < len(lines[number]):
if lines[number][position] != " ":
break
position += 1
lineData = lines[number][position:].trimmed()
if lineData:
# Read the column data from the rest of the line.
columnData = [s for s in lineData.split('t') if s]
if position > indentations[-1]:
# The last child of the current parent is now the new
# parent unless the current parent has no children.
if parents[-1].childCount() > 0:
parents.append(parents[-1].child(parents[-1].childCount() - 1))
indentations.append(position)
else:
while position < indentations[-1] and len(parents) > 0:
parents.pop()
indentations.pop()
# Append a new item to the current parent's list of children.
parent = parents[-1]
parent.insertChildren(parent.childCount(), 1,
self.rootItem.columnCount())
for column in range(len(columnData)):
parent.child(parent.childCount() -1).setData(column, columnData[column])
number += 1

class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
headers = ("Title", "Description")
file = QFile(':/default.txt')
file.open(QIODevice.ReadOnly)
model = TreeModel(headers, file.readAll())
file.close()
self.view.setModel(model)
for column in range(model.columnCount()):
self.view.resizeColumnToContents(column)
self.exitAction.triggered.connect(QApplication.instance().quit)
self.view.selectionModel().selectionChanged.connect(self.updateActions)
self.actionsMenu.aboutToShow.connect(self.updateActions)
self.insertRowAction.triggered.connect(self.insertRow)
self.insertColumnAction.triggered.connect(self.insertColumn)
self.removeRowAction.triggered.connect(self.removeRow)
self.removeColumnAction.triggered.connect(self.removeColumn)
self.insertChildAction.triggered.connect(self.insertChild)
self.updateActions()
def insertChild(self):
index = self.view.selectionModel().currentIndex()
model = self.view.model()
if model.columnCount(index) == 0:
if not model.insertColumn(0, index):
return
if not model.insertRow(0, index):
return
for column in range(model.columnCount(index)):
child = model.index(0, column, index)
model.setData(child, "[No data]", Qt.EditRole)
if model.headerData(column, Qt.Horizontal) is None:
model.setHeaderData(column, Qt.Horizontal, "[No header]",
Qt.EditRole)
self.view.selectionModel().setCurrentIndex(model.index(0, 0, index),
QItemSelectionModel.ClearAndSelect)
self.updateActions()
def insertColumn(self):
model = self.view.model()
column = self.view.selectionModel().currentIndex().column()
changed = model.insertColumn(column + 1)
if changed:
model.setHeaderData(column + 1, Qt.Horizontal, "[No header]",
Qt.EditRole)
self.updateActions()
return changed
def insertRow(self):
index = self.view.selectionModel().currentIndex()
model = self.view.model()
if not model.insertRow(index.row()+1, index.parent()):
return
self.updateActions()
for column in range(model.columnCount(index.parent())):
child = model.index(index.row()+1, column, index.parent())
model.setData(child, "[No data]", Qt.EditRole)
def removeColumn(self):
model = self.view.model()
column = self.view.selectionModel().currentIndex().column()
changed = model.removeColumn(column)
if changed:
self.updateActions()
return changed
def removeRow(self):
index = self.view.selectionModel().currentIndex()
model = self.view.model()
if (model.removeRow(index.row(), index.parent())):
self.updateActions()
def updateActions(self):
hasSelection = not self.view.selectionModel().selection().isEmpty()
self.removeRowAction.setEnabled(hasSelection)
self.removeColumnAction.setEnabled(hasSelection)
hasCurrent = self.view.selectionModel().currentIndex().isValid()
self.insertRowAction.setEnabled(hasCurrent)
self.insertColumnAction.setEnabled(hasCurrent)
if hasCurrent:
self.view.closePersistentEditor(self.view.selectionModel().currentIndex())
row = self.view.selectionModel().currentIndex().row()
column = self.view.selectionModel().currentIndex().column()
if self.view.selectionModel().currentIndex().parent().isValid():
self.statusBar().showMessage("Position: (%d,%d)" % (row, column))
else:
self.statusBar().showMessage("Position: (%d,%d) in top level" % (row, column))

if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

编辑:

我意识到我举的例子毫无意义,TempContainer没有真正的目的!这只是一个最小的例子,显示了我遇到的问题。至于为什么需要TempContainer,假设您的实际TreeItems如下所示:

class TreeItem:
def __init__(self,
name:str, description:str,
data:List[TreeItem], metadata:Dict[str,str],
parent:TreeItem=None):
...

这应该在表格视图中呈现为:

itemA                somedescription here
itemB                this is another item
--metadata
|    --created     yesterday
|    --color       green
--data
--itemC       this is a sub-item of itemB
|    --metadata
|    --data
--itemD       and another item

然后,您需要一个索引,例如"itemB 的第二个元数据项的名称"。您可以创建某种反映此结构但由正常TreeItem组成的影子树,但这可能会变得麻烦,尤其是在编辑时。我的想法是不仅在索引中存储指向TreeItem的指针,而且还使用子索引对其进行注释,指示我们指向TreeItem的哪个部分。

我认为您正在创建的索引几乎没问题,但它们必须履行更多的合同。 具体来说,他们需要支持有效的 equals 方法。 在模型index方法中,在索引内创建一个新TempContainer。 这意味着,当视图将该索引与之前为树中的同一位置请求的另一个索引进行比较时,它们将不会相等。 那是因为它在QModelIndex类上使用默认的等于,比较rowcolumninternalPointer的相等性。 文档说:

模型索引中的所有值在与另一个值进行比较时使用 模型索引

虽然你的TempContainer想法最初似乎很有吸引力,作为使你的树适应视图的一种方式,但这并不容易做到,正如你所发现的那样。 Qt本质上希望模型具有与视图中匹配的清晰树结构,并且很难避免。 在这种情况下,我总是创建一个 Python 对象的树层,我将其引用用作内部指针。

最新更新