QStandardItem неправильно клонируется при перемещении элементов

Как показано в следующем коде, когда вы перетаскиваете элемент (подкласс 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_()

person Tim Carnahan    schedule 04.11.2019    source источник
comment
Я думаю, вы имеете в виду obj = myItem() вместо obj = super(). Хорошо - я это понимаю - однако, несмотря на то, что «данные» и «мета» теряются. Как этого избежать?   -  person Tim Carnahan    schedule 04.11.2019
comment
Хорошо, когда вы сделали копию правильного объекта, вам остается только скопировать данные/метаданные из основного объекта в новый объект, что вам, конечно, придется сделать вручную, но это должно быть часть ваших классов -- функция копирования. Обратите внимание, что когда вы делаете это, вы копируете яблоки в яблоки, иногда вам нужно извлечь данные, чтобы использовать функцию setData.   -  person Dennis Jensen    schedule 04.11.2019
comment
В приложенном коде есть попытка сделать именно это, но не получается. Сохранение данных (или метаданных) в методе клонирования не работает: в .data() возникает следующая ошибка: исключение данных не может преобразовать QVariant обратно в объект Python.   -  person Tim Carnahan    schedule 04.11.2019
comment
Посмотрите на свою документацию по объекту QStandardItem, потому что нет, вы не можете сделать это так, как вы это делаете - вы получаете данные, используя obj.data(), но вы устанавливаете данные, используя obj.setData(value[, role=Qt.UserRole + 1]), что, как я полагаю, также является тем, как вы устанавливаете метаданные - не уверен на 100% без копания более глубоко в это - что вы должны быть в состоянии сделать сейчас   -  person Dennis Jensen    schedule 04.11.2019
comment
Ни один из них не работает. Я пытаюсь получить данные с помощью old_data = self.data() (это подразумевается и не меняет поведение, если я использую role=Qt.UserRole+1). Дело в том, что перед вызовом клона данные (или мета) теряются . Попытка сохранить мету подобным образом не увенчалась успехом.   -  person Tim Carnahan    schedule 04.11.2019
comment
Хорошо, но я думаю, что это более общая проблема с QStandardItem (или моделью/представлением?), в которой клонирование объекта не может «сериализовать» данные внутри. Таким образом, для любой сложной (QObject...) структуры данных вы будете не иметь возможности использовать структуру Model/View для сохранения данных (если вы хотите поддерживать операции перетаскивания)   -  person Tim Carnahan    schedule 04.11.2019
comment
Может быть, но опять же может и не быть -- мне нужно было бы поиграть с этим, сослаться на документацию и выяснить, как это должно быть сделано должным образом, прежде чем я сделаю это заявление -- мне это все еще кажется довольно простым с моей точки зрения. понимание объекта QStandardItem и методологии модели PyQt   -  person Dennis Jensen    schedule 05.11.2019


Ответы (2)


Здесь есть пара проблем, связанных с сериализацией объектов в 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 and 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_()
person ekhumoro    schedule 05.11.2019
comment
Действительно интересно. Использование глобальной структуры данных (словарь) для хранения ссылок на «настоящие» данные. Я уверен, что эта работа на простом примере обеспечит - я не сомневаюсь. Будет интересно, работает ли это в моем более крупном приложении. Но я думаю, что этот вопрос и ответ могут охватывать скрытую «особенность» использования QStandardItem. - person Tim Carnahan; 05.11.2019
comment
@TimCarnahan Нет, QStandardItem работает именно так, как ожидалось. Единственная сложность заключается в копировании QObject, и это не скрытая функция — это хорошо задокументирован. Настоящая проблема заключается в дизайне вашего приложения, которое пытается сделать что-то, что Qt явно не поддерживает. - person ekhumoro; 06.11.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), который создает < em>копировать другой элемент.

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

def clone(self):
    obj = myItem(self)
    obj.setData(self.data())
    obj.meta = self.meta
    return obj
person musicamante    schedule 05.11.2019
comment
Извините - нет ... Это решение не работает - до того, как вы перейдете к .setData и meta = self.meta, данные будут потеряны - в соответствии с обсуждением с Деннисом Дженсеном выше - я подтверждаю ваше наблюдение по поводу obj = myItem(), но основная проблема заключается в том, что когда вы перетаскиваете элемент в дереве, данные (мета или данные()) теряются. Как вы его восстанавливаете (или сохраняете соответствующим образом) - person Tim Carnahan; 05.11.2019
comment
Извините, это моя ошибка. Причина в том, что QStandardItemModel использует itemPrototype в качестве источника для клона, а не сам элемент. Поэтому я боюсь, что единственный способ правильно обновить данные — это переопределить dropEvent представления. Я не могу проверить или обновить свой ответ прямо сейчас, но я сделаю это сегодня вечером, если вы не сможете получить его раньше. - person musicamante; 05.11.2019
comment
@TimCarnahan См. мой ответ по этой теме. При клонировании копируются только данные элемента и флаги — все остальное игнорируется. Кроме того, Qt не позволяет копировать QObject, поэтому нет прямого способа сериализовать экземпляр во время операции перетаскивания. Кроме того, PyQt копирует ваш экземпляр myData с помощью pickle, но, конечно же, QObject не поддается обработке, поэтому это также не удастся. Один из способов обойти это — сохранить экземпляры QObject в экземпляре dict, а затем вместо этого клонировать ключи. - person ekhumoro; 05.11.2019
comment
@musicamante Причина не в этом. Проблема в том, что Qt должен сериализовать данные, используя QVariant, что ограничивает возможности копирования. - person ekhumoro; 05.11.2019
comment
@ekhumoro, я считаю, что ты прав, и как это сделать, ускользает от меня. Я понял, что pickle (или сериализация) не работает с QObject (поэтому, когда я хочу «сохранить» свою сессию, я должен удалить QObject — для создания файла cPickle) — однако для внутри программы, чтобы поддерживать действия перетаскивания Я потерялся. (внутренняя сериализация - это то, что ускользает от меня, и даже если бы я знал, КАК я не знаю ГДЕ). Ваш ответ выше был именно там, где я начал искать, но затем возникла проблема с QObject, и поэтому я здесь. - person Tim Carnahan; 05.11.2019
comment
@TimCarnahan Это не так сложно обойти, но вы абсолютно уверены, что вам нужно использовать QObject таким образом? Все стало бы намного проще, если бы вы могли исключить QObject. - person ekhumoro; 05.11.2019
comment
@ekhumoro - Честно говоря, я понимаю, что в этом примере QObject не имеет большого смысла. Однако в моем большом проекте гораздо труднее исключить подклассы QObject, которые поддерживает myItem. Я также признаю, что документация поддерживает идею о том, что из-за сериализации возникает проблема с хранением данных. Я использовал ваш ответ, чтобы начать свое решение, но все еще не понимаю, как поддерживать данные типа QObject. - person Tim Carnahan; 05.11.2019
comment
@TimCarnahan Я добавил ответ, который дает рабочее решение. - person ekhumoro; 05.11.2019
comment
@ekhumoro я имел в виду, что в случае itemPrototype clone() не предназначен для копирования содержимого удаленного элемента, поскольку он используется для клонирования прототипа, и представление элемента должно установить данные клонированного элемента в соответствии с действие перетаскивания. - person musicamante; 05.11.2019