كيفية التعامل مع السحب والإفلات بشكل صحيح باستخدام PYQT QAbstractItemModel

StackOverflow https://stackoverflow.com//questions/22020091

سؤال

إليكم الكود الذي انتهيت منه بعد يومين من جنون TreeView/Model.يبدو أن الموضوع أوسع بكثير مما كنت أعتقد.بالكاد أستطيع قضاء الكثير من الوقت في إنشاء عنصر واجهة مستخدم واحد.على أي حال.تم تمكين وظيفة السحب والإفلات لعناصر TreeView.ولكن بخلاف عدد قليل من المطبوعات المثيرة للاهتمام، لا يوجد الكثير.يتيح النقر المزدوج على أحد العناصر للمستخدم إدخال اسم عنصر جديد لن يتم التقاطه.

تم تحريره بعد يوم واحد باستخدام رمز منقح.

إنها الآن أداة فعالة بنسبة 90%.

يمكن للمستخدم التعامل مع عناصر TreeView عن طريق السحب والإفلات وإنشاء/تكرار/حذف وإعادة التسمية.تمثل عناصر TreeView الدلائل أو المجلدات بطريقة هرمية قبل إنشائها على محرك أقراص عن طريق الضغط على زر "طباعة" (بدلاً من os.makedirs()، لا تزال الأداة تطبع كل دليل كسلسلة).
أود أن أقول إنني سعيد جدًا بالنتيجة.شكرا ل hackyday ولكل من رد علي وساعدني في أسئلتي.

بعض الأمنيات الأخيرة..

أمنية رقم 01:

  1. أتمنى أن تستخدم طريقة PrintOut() وظيفة أكثر أناقة وأكثر ذكاءً للتنقل عبر عناصر TreeView لإنشاء قاموس يتم تمريره إلى طريقة make_dirs_from_dict().

أمنية رقم 02:

  1. أتمنى أن يكون حذف العناصر أكثر استقرارًا.لسبب غير معروف، تتعطل الأداة عند النقر على زر الحذف الثالث/الرابع.حتى الآن لم أتمكن من تتبع المشكلة.

أمنية رقم 03:3.أتمنى التوفيق للجميع وأشكركم على مساعدتكم:

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_())
هل كانت مفيدة؟

المحلول

لست متأكدًا تمامًا مما تحاول تحقيقه، ولكن يبدو أنك تريد استرداد العنصر الذي تم سحبه في عملية الإفلات، ثم انقر نقرًا مزدوجًا فوق حفظ اسم العقدة الجديد.

أولاً، عليك حفظ العنصر الذي تم سحبه في ملف mimeData.حاليًا، أنت تقوم فقط بحفظ السلسلة "mimeData"، والتي لا تخبرك بالكثير.ال نوع التمثيل الصامت السلسلة التي تم حفظها باسم (هنا استخدمت "bstream") يمكن أن تكون في الواقع أي شيء.طالما أنه يطابق ما تستخدمه لاسترداد البيانات، وهو موجود في القائمة التي تم إرجاعها بواسطة 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 خارج فئة النموذج.هذه طريقة محمية في كيو تي؛لا تفرض لغة Python متغيرات أو أساليب خاصة/محمية، ولكن عند استخدام مكتبة من لغة أخرى تقوم بذلك، أحاول احترام التنظيم المقصود للفئات والوصول إليها.

الغرض من النموذج هو توفير واجهة لبياناتك.يجب عليك الوصول إليه باستخدام index, data, parent إلخ.الوظائف العامة للنموذج.للحصول على أصل فهرس معين، استخدم هذا الفهرس (أو النموذج) parent الدالة، والتي ستقوم أيضًا بإرجاع QModelIndex.بهذه الطريقة، لن تضطر إلى الاطلاع على (أو معرفة) البنية الداخلية للبيانات.وهذا ما فعلته في deleteLevel طريقة.

من كيو تي مستندات:

ولضمان إبقاء تمثيل البيانات منفصلاً عن طريقة الوصول إليها، تم تقديم مفهوم فهرس النموذج.يتم تمثيل كل معلومة يمكن الحصول عليها عبر النموذج بواسطة فهرس النموذج...يحتاج النموذج فقط إلى معرفة كيفية الحصول على البيانات، ويمكن تحديد نوع البيانات التي يديرها النموذج بشكل عام إلى حد ما.

يمكنك أيضًا استخدام العودية لتبسيط طريقة الطباعة.

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}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top