如何使用PYQT QAbstractItemModel正确处理拖放

时间:2014-02-25 16:22:54

标签: pyqt qtreeview qtreewidget qabstractitemmodel qtreewidgetitem

这是我在TreeView / Model疯狂两天后结束的代码。这个主题看起来比我想象的要广泛得多。我几乎无法花太多时间创建一个单一的小部件。无论如何。 TreeView项目的拖放功能已启用。但除了一些有趣的打印输出之外没有那么多。双击某个项目允许用户输入一个不会被选中的新项目名称。

使用修订后的代码编辑了一天。

现在是90%的功能工具。

用户可以通过拖放,创建/复制/删除和重命名来操作TreeView项目。 TreeView项目在驱动器上创建之前以分层方式表示目录或文件夹,方法是按“打印”按钮(而不是os.makedirs(),工具仍然只是将每个目录打印为字符串。
我会说我对结果非常满意。感谢 hackyday 以及回复并帮助解决我问题的所有人。

一些遗愿......

愿望号码01:

  1. 我希望PrintOut()方法使用更优雅的智能函数来遍历TreeView项,以构建一个传递给make_dirs_from_dict()方法的字典。
  2. 愿望号码02:

    1. 我希望删除项目会更稳定。由于某种未知原因,工具在第三次/第四次删除按钮点击时崩溃。到目前为止,我无法追查问题。
    2. 愿望号码03: 我祝大家一切顺利,感谢你的帮助:

      import sys, os
      from PyQt4 import QtGui, QtCore
      from PyQt4.QtGui import *
      from PyQt4.QtCore import *
      from copy import deepcopy
      import cPickle    
      
      class TreeItem(object):
          def __init__(self, name, parent=None):
      
              self.name = QtCore.QString(name)       
              self.parent = parent
              self.children = []       
              self.setParent(parent)
      
          def setParent(self, parent):
              if parent != None:
                  self.parent = parent
                  self.parent.appendChild(self)
              else:     self.parent = None
      
          def appendChild(self, child):
              self.children.append(child)
      
          def childAtRow(self, row):
              if len(self.children)>row: 
                  return self.children[row]
      
          def rowOfChild(self, child):       
              for i, item in enumerate(self.children):
                  if item == child:  return i
              return -1
      
          def removeChild(self, row):
              value = self.children[row]
              self.children.remove(value)
              return True
      
          def __len__(self):
              return len(self.children) 
      
      class TreeModel(QtCore.QAbstractItemModel):
          def __init__(self):
      
              QtCore.QAbstractItemModel.__init__(self)
      
              self.columns = 1
              self.clickedItem=None
      
              self.root = TreeItem('root', None) 
              levelA = TreeItem('levelA', self.root)
              levelB = TreeItem('levelB', levelA)
              levelC1 = TreeItem('levelC1', levelB)
              levelC2 = TreeItem('levelC2', levelB)
              levelC3 = TreeItem('levelC3', levelB)
              levelD = TreeItem('levelD', levelC3)
      
              levelE = TreeItem('levelE', levelD)
              levelF = TreeItem('levelF', levelE)
      
          def nodeFromIndex(self, index):
              return index.internalPointer() if index.isValid() else self.root
      
          def index(self, row, column, parent):        
              node = self.nodeFromIndex(parent)
              return self.createIndex(row, column, node.childAtRow(row))
      
          def parent(self, child):
              # print '\n parent(child)', child  # PyQt4.QtCore.QModelIndex
              if not child.isValid():  return QModelIndex()
              node = self.nodeFromIndex(child)       
              if node is None:   return QModelIndex()
              parent = node.parent           
              if parent is None:      return QModelIndex()       
              grandparent = parent.parent
      
              if grandparent==None:    return QModelIndex()
      
              row = grandparent.rowOfChild(parent)    
              assert row != - 1
      
              return self.createIndex(row, 0, parent)
      
          def rowCount(self, parent):
              node = self.nodeFromIndex(parent)
              if node is None: return 0
              return len(node)
      
          def columnCount(self, parent):
              return self.columns
      
          def data(self, index, role):
              if role == Qt.DecorationRole:
                  return QVariant()               
              if role == Qt.TextAlignmentRole:
                  return QVariant(int(Qt.AlignTop | Qt.AlignLeft))       
              if role != Qt.DisplayRole:   
                  return QVariant()                   
              node = self.nodeFromIndex(index)       
              if index.column() == 0:     
                  return QVariant(node.name)       
              elif index.column() == 1:   
                  return QVariant(node.state)       
              elif index.column() == 2:   
                  return QVariant(node.description)
              else:   return QVariant()
      
          def supportedDropActions(self):
              return Qt.CopyAction | Qt.MoveAction
      
          def flags(self, index):
              defaultFlags = QAbstractItemModel.flags(self, index)       
              if index.isValid():  return Qt.ItemIsEditable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | defaultFlags           
              else:   return Qt.ItemIsDropEnabled | defaultFlags  
      
          def setData(self, index, value, role):
              if role == Qt.EditRole:
                  if value.toString() and len(value.toString())>0: 
                      self.nodeFromIndex(index).name = value.toString()
                      self.dataChanged.emit(index, index)
                  return True
      
          def mimeTypes(self):
              return ['bstream', 'text/xml']
      
          def mimeData(self, indexes):
      
              mimedata = QtCore.QMimeData()
              bstream = cPickle.dumps(self.nodeFromIndex(indexes[0]))
              mimedata.setData('bstream', bstream)
              return mimedata
      
          def dropMimeData(self, mimedata, action, row, column, parentIndex):
      
              if action == Qt.IgnoreAction: return True  
      
              droppedNode=cPickle.loads(str(mimedata.data('bstream')))
      
              droppedIndex = self.createIndex(row, column, droppedNode)
      
              parentNode = self.nodeFromIndex(parentIndex)
      
              newNode = deepcopy(droppedNode)
              newNode.setParent(parentNode)
      
              self.insertRow(len(parentNode)-1, parentIndex)
      
              self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex)
      
              return True
      
          def insertRow(self, row, parent):
              return self.insertRows(row, 1, parent)
          def insertRows(self, row, count, parent):
              self.beginInsertRows(parent, row, (row + (count - 1)))
              self.endInsertRows()
              return True
      
          def removeRow(self, row, parentIndex):
              return self.removeRows(row, 1, parentIndex)
      
          def removeRows(self, row, count, parentIndex):
              self.beginRemoveRows(parentIndex, row, row)
              node = self.nodeFromIndex(parentIndex)
              node.removeChild(row)
              self.endRemoveRows()       
              return True
      
      
      class GUI(QtGui.QDialog):
          def build(self, myWindow):
              myWindow.resize(600, 400)
              self.myWidget = QWidget(myWindow)        
              self.boxLayout = QtGui.QVBoxLayout(self.myWidget)
      
              self.treeView = QtGui.QTreeView()
      
              self.treeModel = TreeModel()
              self.treeView.setModel(self.treeModel)
              self.treeView.expandAll()
              self.treeView.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
              self.treeView.connect(self.treeView.model(), SIGNAL("dataChanged(QModelIndex,QModelIndex)"), self.onDataChanged)
              QtCore.QObject.connect(self.treeView, QtCore.SIGNAL("clicked (QModelIndex)"),  self.treeItemClicked)
              self.boxLayout.addWidget(self.treeView)
      
      
              self.PrintButton= QtGui.QPushButton("Print")  
              self.PrintButton.clicked.connect(self.PrintOut) 
              self.boxLayout.addWidget(self.PrintButton)
      
              self.DeleteButton= QtGui.QPushButton("Delete")  
              self.DeleteButton.clicked.connect(self.DeleteLevel) 
              self.boxLayout.addWidget(self.DeleteButton)
      
              self.insertButton= QtGui.QPushButton("Insert")  
              self.insertButton.clicked.connect(self.insertLevel) 
              self.boxLayout.addWidget(self.insertButton)
      
              self.duplicateButton= QtGui.QPushButton("Duplicate")  
              self.duplicateButton.clicked.connect(self.duplicateLevel) 
              self.boxLayout.addWidget(self.duplicateButton)
      
              myWindow.setCentralWidget(self.myWidget)
      
      
          def make_dirs_from_dict(self, dirDict, current_dir='/'):
              for key, val in dirDict.items():
                  #os.mkdir(os.path.join(current_dir, key))
                  print "\t\t Creating directory: ", os.path.join(current_dir, key)
                  if type(val) == dict:
                      self.make_dirs_from_dict(val, os.path.join(current_dir, key))
      
          def PrintOut(self):
              result_dict = {}
              for a1 in self.treeView.model().root.children:
                  result_dict[str(a1.name)]={}
                  for a2 in a1.children:
                      result_dict[str(a1.name)][str(a2.name)]={}
                      for a3 in a2.children:
                          result_dict[str(a1.name)][str(a2.name)][str(a3.name)]={}
                          for a4 in a3.children:
                              result_dict[ str(a1.name)][str(a2.name)][str(a3.name)][str(a4.name)]={}
                              for a5 in a4.children:
                                  result_dict[ str(a1.name)][str(a2.name)][str(a3.name)][str(a4.name)][str(a5.name)]={}
                                  for a6 in a5.children:
                                      result_dict[str(a1.name)][str(a2.name)][str(a3.name)][str(a4.name)][str(a5.name)][str(a6.name)]={}
                                      for a7 in a6.children:
                                          result_dict[str(a1.name)][str(a2.name)][str(a3.name)][str(a4.name)][str(a5.name)][str(a6.name)][str(a7.name)]={}
      
      
              self.make_dirs_from_dict(result_dict)                      
      
      
          def DeleteLevel(self):
              if len(self.treeView.selectedIndexes())==0: return
      
              currentIndex = self.treeView.selectedIndexes()[0]
              currentRow=currentIndex.row()
              currentColumn=currentIndex.column()
              currentNode = currentIndex.internalPointer()
      
              parentNode = currentNode.parent
              parentIndex = self.treeView.model().createIndex(currentRow, currentColumn, parentNode)
              print '\n\t\t\t CurrentNode:', currentNode.name, ', ParentNode:', currentNode.parent.name, ', currentColumn:', currentColumn, ', currentRow:', currentRow 
      
              # self.treeView.model().removeRow(len(parentNode)-1, parentIndex) 
      
              self.treeView.model().removeRows(currentRow, 1, parentIndex )
      
              #self.treeView.model().removeRow(len(parentNode), parentIndex)
              #self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex) 
      
          def insertLevel(self):
              if len(self.treeView.selectedIndexes())==0: return
      
              currentIndex = self.treeView.selectedIndexes()[0]
              currentNode = currentIndex.internalPointer()
              newItem = TreeItem('Brand New', currentNode)
              self.treeView.model().insertRow(len(currentNode)-1, currentIndex)
              self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), currentIndex, currentIndex)   
      
          def duplicateLevel(self):
              if len(self.treeView.selectedIndexes())==0: return
      
              currentIndex = self.treeView.selectedIndexes()[0]
              currentRow=currentIndex.row()
              currentColumn=currentIndex.column()
              currentNode=currentIndex.internalPointer()
      
              parentNode=currentNode.parent
              parentIndex=self.treeView.model().createIndex(currentRow, currentColumn, parentNode)
              parentRow=parentIndex.row()
              parentColumn=parentIndex.column()
      
              newNode = deepcopy(currentNode)
              newNode.setParent(parentNode)
      
              self.treeView.model().insertRow(len(parentNode)-1, parentIndex)
              self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex) 
      
              print '\n\t\t\t CurrentNode:', currentNode.name, ', ParentNode:', parentNode.name, ', currentColumn:', currentColumn, ', currentRow:', currentRow, ', parentColumn:', parentColumn, ', parentRow:', parentRow 
              self.treeView.update()
              self.treeView.expandAll()
      
      
          def treeItemClicked(self, index):
              print "\n clicked item ----------->", index.internalPointer().name
      
          def onDataChanged(self, indexA, indexB):
              print "\n onDataChanged NEVER TRIGGERED! ####################### \n ", index.internalPointer().name
              self.treeView.update(indexA)
              self.treeView.expandAll()
              self.treeView.expanded()
      
      
      
      if __name__ == '__main__':
      
          app = QtGui.QApplication(sys.argv)
      
          myWindow = QMainWindow()
          myGui = GUI()
          myGui.build(myWindow)
          myWindow.show()
          sys.exit(app.exec_())
      

1 个答案:

答案 0 :(得分:1)

我不完全确定您要实现的目标,但听起来您想要在放置操作中检索拖动的项目,并且双击保存新的节点名称。

首先,您需要将拖动的项目保存到mimeData。目前,您只保存字符串'mimeData',这对您来说并不多。保存为(此处我使用'bstream')的 mimeType 字符串实际上可以是任何内容。只要它与您用于检索数据的内容相匹配,并且位于模型的mimeTypes方法返回的列表中。要传递对象本身,必须先将其序列化(您可以将对象转换为xml,如果这是您计划进行的操作),因为它不是mime数据的标准类型。

为了保存您输入的数据,您必须重新实现模型的setData方法并定义EditRole的行为。

相关方法:

def setData(self, index, value, role):
    if role == Qt.EditRole:
        self.nodeFromIndex(index).name = value
        self.dataChanged.emit(index, index)
        return True

def mimeTypes(self):
    return ['bstream', 'text/xml']

def mimeData(self, indexes):
    mimedata = QtCore.QMimeData()
    # assuming single dragged item ...
    # only pass the node name
    # mimedata.setData('text/xml', str(self.nodeFromIndex(indexes[0]).name))
    # pass the entire object
    bstream = cPickle.dumps(self.nodeFromIndex(indexes[0]))
    mimedata.setData('bstream', bstream)
    return mimedata

def dropMimeData(self, mimedata, action, row, column, parentIndex):

    if action == Qt.IgnoreAction: return True        

    parentNode = self.nodeFromIndex(parentIndex)

    # data = mimedata.data('text/xml')
    data = cPickle.loads(str(mimedata.data('bstream')))
    print '\n\t incoming row number:', row, ', incoming column:', column, \
        ', action:', action, ' mimedata: ', data.name

    print "\n\t Item's name on which drop occurred: ", parentNode.name, \
        ', number of its childred:', len(parentNode.children)

    if len(parentNode.children)>0: print '\n\t zero indexed child:', parentNode.children[0].name

    return True

编辑:
这是你更新的很多代码,但我会强调你强调的要点。避免在模型类之外调用createIndex。这是Qt中受保护的方法; Python不强制实施私有/受保护的变量或方法,但是当使用其他语言的库时,我会尝试尊重类的预期组织,并访问它们。

该模型的目的是为您的数据提供一个接口。您应该使用模型的indexdataparent等公共函数来访问它。要获取给定索引的父级,请使用该索引(或模型的)parent函数,该函数还将返回QModelIndex。这样,您就不必经历(或确实了解)数据的内部结构。这就是我在deleteLevel方法中所做的。

来自qt docs

  

为了确保数据的表示与访问方式不同,引入了模型索引的概念。可以通过模型获得的每条信息由模型索引表示......只有模型需要知道如何获取数据,并且可以相当普遍地定义模型管理的数据类型。

此外,您可以使用递归来简化打印方法。

def printOut(self):
    result_dict = dictify(self.treeView.model().root)
    self.make_dirs_from_dict(result_dict)  

def deleteLevel(self):
    if len(self.treeView.selectedIndexes()) == 0: 
        return

    currentIndex = self.treeView.selectedIndexes()[0]
    self.treeView.model().removeRow(currentIndex.row(), currentIndex.parent())

我把它与班级分开了

def dictify(node):
    kids = {}
    for child in node.children:
        kids.update(dictify(child)) 
    return {str(node.name): kids}