QStandardItem неправильно клонируется при перемещении элементов - PullRequest
1 голос
/ 04 ноября 2019

Как указано в следующем коде, при перетаскивании элемента (подкласс из QStandardItem с помощью метода clone()) вы получаете QStandardItem, а не подкласс. Более того - данные, хранящиеся в классе или как часть setData, теряются. Я подозреваю, что это из-за невозможности «сериализации» данных. Но я не знаю, как «сохранить» данные - или мета. Как я могу сохранить QObject? Следующий код работает нормально, но после перемещения узла ветви все узлы ветви и ветви становятся QStandardItem, а не myItem и теряют данные (если они были).

# -*- coding: utf-8 -*-
"""
Created on Mon Nov  4 09:10:16 2019

Test of Tree view with subclassed QStandardItem and Drag and Drop
enabled.  When you move a parent the parent looses the subclass and thus
the meta - however, it also looses the data:  This is likely because
the data cannot be serialized.  How to fix?

@author: tcarnaha
"""
import sys
from PyQt5 import QtGui, QtWidgets, QtCore


class myData():
    def __init__(self, title):
        self._title = title
        self._stuff = dict()
        self._obj = QtCore.QObject()

    @property
    def obj(self):
        return self._obj

    @obj.setter
    def obj(self, value):
        self._obj = value

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, value):
        self._title = value


class myItem(QtGui.QStandardItem):
    def __init__(self, parent=None):
        super(myItem, self).__init__(parent)
        self._meta = None

    @property
    def meta(self):
        return self._meta

    @meta.setter
    def meta(self, value):
        self._meta = value

    def clone(self):
        print "My cloning"
        old_data = self.data()
        print "Old data [{}]".format(old_data)
        old_meta = self.meta
        obj = myItem()
        obj.setData(old_data)
        print "New data [{}]".format(obj.data())
        obj.meta = old_meta
        print "Clone is a ", obj.__class__
        return obj

class mainWidget(QtWidgets.QMainWindow):
    def __init__(self):
        super(mainWidget, self).__init__()
        self.model = QtGui.QStandardItemModel()
        self.model.setItemPrototype(myItem())
        self.view = QtWidgets.QTreeView()
        self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.view.customContextMenuRequested.connect(self.list_click)
        self.view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
        self.view.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.view.setDragDropOverwriteMode(False)
        self.view.setAcceptDrops(True)
        self.view.setDropIndicatorShown(True)
        self.view.setDragEnabled(True)
        self.view.setModel(self.model)
        dataA = myData('A thing')
        parentA = myItem()
        parentA.setText('A')
        parentA.setDragEnabled(True)
        parentA.setDropEnabled(True)
        parentA.setData(dataA)
        parentA.meta = QtCore.QObject()
        childa = myItem()
        childa.setText('a')
        childb = myItem()
        childb.setText('b')
        childc = myItem()
        childc.setText('c')
        parentA.appendRows([childa, childb, childc])
        dataB = myData('B thing')
        parentB = myItem()
        parentB.setText('B')
        parentB.setDragEnabled(True)
        parentB.setDropEnabled(True)
        parentB.setData(dataB)
        parentB.meta = QtCore.QObject()
        childd = myItem()
        childd.setText('d')
        childe = myItem()
        childe.setText('e')
        childf = myItem()
        childf.setText('f')
        parentB.appendRows([childd, childe, childf])
        self.model.appendRow(parentA)
        self.model.appendRow(parentB)

        classAct = QtWidgets.QAction('Class', self)
        classAct.triggered.connect(self.classIs)
        dataAct = QtWidgets.QAction('Data', self)
        dataAct.triggered.connect(self.dataIs)
        metaAct = QtWidgets.QAction('Meta', self)
        metaAct.triggered.connect(self.metaIs)
        self.menu = QtWidgets.QMenu("Item info")
        self.menu.addAction(classAct)
        self.menu.addAction(dataAct)
        self.menu.addAction(metaAct)

        self.setCentralWidget(self.view)

    @QtCore.pyqtSlot(QtCore.QPoint)
    def list_click(self, position):
        self.menu.popup(self.view.viewport().mapToGlobal(position))

    def classIs(self):
        selected_indexes = self.view.selectedIndexes()
        for index in selected_indexes:
            item = self.model.itemFromIndex(index)
            print "Item {} Class {} ".format(item.text(), item.__class__())

    def dataIs(self):
        selected_indexes = self.view.selectedIndexes()
        for index in selected_indexes:
            item = self.model.itemFromIndex(index)
            try:
                print "Item {} data {} Object {}".format(item.text(),
                                                         item.data().title,
                                                         item.data().obj)
            except Exception as exc:
                print "Data exception ", exc

    def metaIs(self):
        selected_indexes = self.view.selectedIndexes()
        for index in selected_indexes:
            item = self.model.itemFromIndex(index)
            try:
                print "Item {} meta {} ".format(item.text(), item.meta)
            except Exception as exc:
                print "Meta exception ", exc


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    main = mainWidget()
    main.show()
    app.exec_()

Ответы [ 2 ]

2 голосов
/ 05 ноября 2019

Здесь есть пара проблем, связанных с тем, как объекты сериализуются Qt, а также PyQt. Во-первых, при клонировании QStandardItem копируются только флаги и данные - все остальное игнорируется (включая динамические атрибуты python). Во-вторых, нет способа напрямую скопировать QObject. Это потому, что он не может быть приведен к QVariant (который Qt использует для сериализации), и его нельзя засечь (который PyQt использует для сериализации).

Чтобы решить вторую проблему, нам нужно хранить отдельные ссылкико всем экземплярам QObject, а затем используйте косвенные ключи для последующего доступа к ним позже. Вероятно, есть много разных способов добиться этого, но вот очень простой подход, который иллюстрирует основную идею:

objects = {}

class MyObject(QtCore.QObject):
    def __init__(self, parent=None):
        super(MyObject, self).__init__(parent)
        self.setProperty('key', max(objects.keys() or [0]) + 1)
        objects[self.property('key')] = self

Таким образом, это автоматически добавляет каждый экземпляр в глобальный кеш и дает ему уникальный ключ поиска, так чтоэто может быть легко найдено позже. После этого класс myData теперь необходимо адаптировать для использования класса MyObject, чтобы обработка обрабатывалась правильно :

class myData():
    def __init__(self, title):
        self._title = title
        self._stuff = dict()
        self._obj = MyObject()

    def __setstate__(self, state):
        self._obj = objects.get(state['obj'])
        self._stuff = state['stuff']
        self._title = state['title']

    def __getstate__(self):
        return {
            'obj': self._obj.property('key'),
            'title': self._title,
            'stuff': self._stuff,
            }

Решение первой проблемы - это гораздоПроще: нам просто нужно убедиться, что любые динамические свойства Python хранят свои базовые значения в данных элемента с помощью пользовательских ролей данных. В этом конкретном случае значение должно быть ключом экземпляра MyObject элемента, чтобы его можно было извлечь после операции перетаскивания:

class myItem(QtGui.QStandardItem):
    MetaRole = QtCore.Qt.UserRole + 1000

    @property
    def meta(self):
        return objects.get(self.data(myItem.MetaRole))

    @meta.setter
    def meta(self, value):
        self.setData(value.property('key'), myItem.MetaRole)

    def clone(self):
        print "My cloning"
        obj = myItem(self)
        print "Clone is a ", obj.__class__
        return obj

Ниже приведена рабочая версия вашего исходного сценария. который реализует все вышеперечисленное. Но имейте в виду, что вам почти наверняка понадобится адаптировать это для правильной работы с вашим реальным кодом. Это просто рабочее доказательство концепции, показывающее, как бороться с двумя проблемами, описанными выше.

# -*- coding: utf-8 -*-
import sys
from PyQt5 import QtGui, QtWidgets, QtCore

objects = {}

class MyObject(QtCore.QObject):
    def __init__(self, parent=None):
        super(MyObject, self).__init__(parent)
        self.setProperty('key', max(objects.keys() or [0]) + 1)
        objects[self.property('key')] = self

class myData():
    def __init__(self, title):
        self._title = title
        self._stuff = dict()
        self._obj = MyObject()

    def __setstate__(self, state):
        self._obj = objects.get(state['obj'])
        self._stuff = state['stuff']
        self._title = state['title']

    def __getstate__(self):
        return {
            'obj': self._obj.property('key'),
            'title': self._title,
            'stuff': self._stuff,
            }

    @property
    def obj(self):
        return self._obj

    @obj.setter
    def obj(self, value):
        self._obj = value

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, value):
        self._title = value

class myItem(QtGui.QStandardItem):
    MetaRole = QtCore.Qt.UserRole + 1000

    @property
    def meta(self):
        return objects.get(self.data(myItem.MetaRole))

    @meta.setter
    def meta(self, value):
        self.setData(value.property('key'), myItem.MetaRole)

    def clone(self):
        print "My cloning"
        obj = myItem(self)
        print "Clone is a ", obj.__class__
        return obj

class mainWidget(QtWidgets.QMainWindow):
    def __init__(self):
        super(mainWidget, self).__init__()
        self.model = QtGui.QStandardItemModel()
        self.model.setItemPrototype(myItem())
        self.view = QtWidgets.QTreeView()
        self.view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        self.view.customContextMenuRequested.connect(self.list_click)
        self.view.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
        self.view.setDefaultDropAction(QtCore.Qt.MoveAction)
        self.view.setDragDropOverwriteMode(False)
        self.view.setAcceptDrops(True)
        self.view.setDropIndicatorShown(True)
        self.view.setDragEnabled(True)
        self.view.setModel(self.model)
        dataA = myData('A thing')
        parentA = myItem()
        parentA.setText('A')
        parentA.setDragEnabled(True)
        parentA.setDropEnabled(True)
        parentA.setData(dataA)
        parentA.meta = MyObject()
        childa = myItem()
        childa.setText('a')
        childb = myItem()
        childb.setText('b')
        childc = myItem()
        childc.setText('c')
        parentA.appendRows([childa, childb, childc])
        dataB = myData('B thing')
        parentB = myItem()
        parentB.setText('B')
        parentB.setDragEnabled(True)
        parentB.setDropEnabled(True)
        parentB.setData(dataB)
        parentB.meta = MyObject()
        childd = myItem()
        childd.setText('d')
        childe = myItem()
        childe.setText('e')
        childf = myItem()
        childf.setText('f')
        parentB.appendRows([childd, childe, childf])
        self.model.appendRow(parentA)
        self.model.appendRow(parentB)

        classAct = QtWidgets.QAction('Class', self)
        classAct.triggered.connect(self.classIs)
        dataAct = QtWidgets.QAction('Data', self)
        dataAct.triggered.connect(self.dataIs)
        metaAct = QtWidgets.QAction('Meta', self)
        metaAct.triggered.connect(self.metaIs)
        self.menu = QtWidgets.QMenu("Item info")
        self.menu.addAction(classAct)
        self.menu.addAction(dataAct)
        self.menu.addAction(metaAct)

        self.setCentralWidget(self.view)

    @QtCore.pyqtSlot(QtCore.QPoint)
    def list_click(self, position):
        self.menu.popup(self.view.viewport().mapToGlobal(position))

    def classIs(self):
        selected_indexes = self.view.selectedIndexes()
        for index in selected_indexes:
            item = self.model.itemFromIndex(index)
            print "Item {} Class {} ".format(item.text(), item.__class__())

    def dataIs(self):
        selected_indexes = self.view.selectedIndexes()
        for index in selected_indexes:
            item = self.model.itemFromIndex(index)
            try:
                print "Item {} data {} Object {}".format(item.text(),
                                                         item.data().title,
                                                         item.data().obj)
            except Exception as exc:
                print "Data exception ", exc

    def metaIs(self):
        selected_indexes = self.view.selectedIndexes()
        for index in selected_indexes:
            item = self.model.itemFromIndex(index)
            try:
                print "Item {} meta {} ".format(item.text(), item.meta)
            except Exception as exc:
                print "Meta exception ", exc


if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    main = mainWidget()
    main.show()
    app.exec_()
0 голосов
/ 05 ноября 2019

Вы клонируете не свой класс, а обычный QStandardItem:

obj = super(myItem, self).clone()

Это фактически означает «вызов метода clone() класса base ».
При создании подклассов из одного класса super() действует точно так же, как и вызов метода класса с экземпляром подкласса в качестве первого аргумента, поэтому в данном случае это точно так:

obj = QtGui.QStandardItem.clone(self)

Наиболее распространенное преимуществоsuper() означает простоту и удобство обслуживания (если вы изменяете базовый класс, который собираетесь наследовать, вы должны сделать это только в объявлении подкласса);кроме этого, его наиболее важным преимуществом является множественное наследование, то есть когда вы наследуете от более чем одного базового класса, но, поскольку это редкая ситуация в PyQt, это не так;также, множественное наследование невозможно с более чем одним классом Qt.

Как указано в setItemPrototype() (выделено мной):

Для обеспечениясвой собственный прототип, подкласс QStandardItem, переопределите QStandardItem :: clone () и установите для прототипа экземпляр вашего пользовательского класса .

Что clone() на самом деле действительно ли действительно использует конструктор QStandardItem(other), который создает копию другого элемента.

ИтакВы можете получить свой правильный клон, просто выполнив следующее:

def clone(self):
    obj = myItem(self)
    obj.setData(self.data())
    obj.meta = self.meta
    return obj
...