在继承QAbstractItemModel时使用列表作为QModelIndex的internalPointer,PyQt
我正在尝试在Python中把一个列表包装成QAbstractItemModel,这样我就可以利用PyQt提供的图形界面功能把这个列表以树形结构展示出来。具体来说,我使用的是PyQt5和Python 3.3。
我试着明确生成指向列表项的指针,但在识别父项时遇到了麻烦。我没有给它们分配父项,也不确定在执行createIndex时PyQt是否会自动分配。我决定重写代码,明确识别通过索引(行值)访问的项。例如,给定一个嵌套列表["A",["a",[1,2,3]],"B","C",["b",[4,5,6],"d"]],我可以通过[4,1,1]指向5,父项则可以通过去掉最后一个项的列表[4,1]来获取。
我的问题是这样的。当我使用从行值生成的列表时,CREATEINDEX方法会导致代码崩溃。我的最小可重现示例(MBE)如下。取消注释包含"return self.createIndex(row,col,ptr)"的两行代码,就能看到我描述的问题。
from PyQt5.QtGui import QFont, QColor
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt, QAbstractListModel, QAbstractTableModel, QSize, QVariant
from PyQt5.QtWidgets import QApplication, QTreeView, QTableView, QListView
class NodeTree(QAbstractItemModel) :
def __init__(self,parent,data) :
super(NodeTree,self).__init__(parent)
self.data = data
def rowCount(self, parent = QModelIndex()) :
# int rowCount (self, QModelIndex parent = QModelIndex())
if parent.isValid() :
data = self.data
for idx in parent.internalPointer() :
data = data[idx]
return len(data)
else :
return len(self.data)
def columnCount(self, parent = QModelIndex()) :
# int columnCount (self, QModelIndex parent = QModelIndex())
return 1
def data(self, index = QModelIndex(), role = None) :
# QVariant data (self, QModelIndex index, int role = Qt.DisplayRole)
if role == 0 :
if index.isValid() :
return str(index.internalPointer())
else :
return "No Data"
return None
def index(self,row,col, parent = QModelIndex()):
# QModelIndex index (self, int row, int column, QModelIndex parent = QModelIndex())
# QModelIndex createIndex (self, int arow, int acolumn, int aid)
# QModelIndex createIndex (self, int arow, int acolumn, object adata = 0)
if parent.isValid() :
ptr = [parent.internalPointer(),row]
# return self.createIndex(row,col,ptr)
return QModelIndex()
else :
ptr = [row]
# return self.createIndex(row,col,ptr)
return QModelIndex()
return QModelIndex()
def parent(self, child = QModelIndex()):
# QObject parent (self)
if child.isValid() :
print(child.internalPointer())
return QModelIndex()
if __name__ == "__main__" :
# http://blog.mathieu-leplatre.info/filesystem-watch-with-pyqt4.html
import sys
app = QApplication(sys.argv)
TreeView = QTreeView()
TreeModel = NodeTree(TreeView, [['A',['a',1]],['C','D'],['E','F']])
TreeView.setModel(TreeModel)
TreeView.show()
app.exec_()
我不明白的是,QModelIndex是如何在CREATEINDEX中创建的,为什么在INDEX方法中动态生成的列表会导致崩溃。在我看来,INTERNALPOINTER应该在调用之间保持不变,传递一个列表应该是没问题的。
另一个让我困惑的事情是,父项和索引何时以及为什么会被调用。根据我的理解,index是向下遍历树,而parent是向上遍历。所以调用INDEX时会识别在(row,col)位置的父项的子项,而PARENT则是确定一个子项的父项。这是通过内部指针来完成的吗?如果是这样,为什么QModelIndex还要维护自己的PARENT方法?看起来Qt维护了自己的内部树,根据index和parent之间的调用来建立项之间的层级关系。也就是说,总是有两棵树,一棵是我的模型,另一棵是显示的那棵。
2 个回答
我想你的帮助是基于这个输入列表得到的。
[['A',['a',1]],['C','D'],['E','F']]
下面是输出结果:
我觉得你的代码主要在以下几个方面出问题:
- 你没有把树模型和树项数据分开,这样会造成一些混淆。
- 与第一个问题相关,你是根据自己的数据来计算一些属性,而不是仅仅根据Qt的ModelIndex。
在下面的代码中,我尽量做到与Qt示例代码(C++)在TreeView组件上的一一对应关系(就像这里一样)。
from PyQt5.QtCore import (
QAbstractItemModel,
QModelIndex,
Qt,
QVariant
)
from PyQt5.QtWidgets import QApplication, QTreeView
class TreeItem(object):
def __init__(self, content, parentItem):
self.content = content
self.parentItem = parentItem
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def columnCount(self):
return 1
def data(self, column):
if self.content != None and column == 0:
return QVariant(self.content)
return QVariant()
def parent(self):
return self.parentItem
def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
class NodeTree(QAbstractItemModel):
def __init__(self,parent,data) :
super(NodeTree,self).__init__(parent)
self.rootItem = TreeItem(None, None)
self.parents = {0 : self.rootItem}
self.setupModelData(self.rootItem, data)
def setupModelData(self, root, data):
for el in data:
if isinstance(el, list):
item = TreeItem("Node", root)
self.setupModelData(item, el)
else:
item = TreeItem(el, root)
root.appendChild(item)
def rowCount(self, parent = 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 columnCount(self, parent = QModelIndex()):
return 1
def data(self, index, role):
if not index.isValid():
return QVariant()
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.data(index.column())
if role == Qt.UserRole:
if item:
return item.content
return QVariant()
def headerData(self, column, orientation, role):
if (orientation == Qt.Horizontal and
role == Qt.DisplayRole):
return QVariant("Content")
return QVariant()
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QModelIndex()
def parent(self, index):
if not index.isValid():
return QModelIndex()
childItem = index.internalPointer()
if not childItem:
return QModelIndex()
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
if __name__ == "__main__" :
# http://blog.mathieu-leplatre.info/filesystem-watch-with-pyqt4.html
import sys
app = QApplication(sys.argv)
TreeView = QTreeView()
TreeModel = NodeTree(TreeView, [['A',['a',1]],['C','D'],['E','F']])
TreeView.setModel(TreeModel)
TreeView.show()
app.exec_()
希望这能对你有所帮助。
我试过同样的事情,也发现 Python 会崩溃。问题出在 C++ 和 Python 的垃圾回收器之间的互动。当你创建 QModelIndex 时,你把一个 Python 的列表对象传进去。PySide 在 C++ 的结构里保存了这个列表的指针,但没有告诉垃圾回收器(GC)这个指针的存在。这就意味着,Python 这边看不到这个列表的引用,垃圾回收器会把它释放掉。与此同时,C++ 的 QModelIndex 还在被传来传去,直到它到达视图。视图调用 data(index)(或者其他任何函数),这时 C++ 的 QModelIndex 又回到了 Python 代码里。PySide 然后创建了一个新的 QModelIndex 对象。但现在内部的指针指向的东西已经不存在了,这就会引发各种问题。
注意:如果你把列表存储在一个持久的结构里(也就是比函数活得更久的结构),那么你的方法就能奏效。但这样就失去了使用路径列表作为内部指针的意义。