【问题标题】:QStandardItem does not clone correctly when the items are moved移动项目时,QStandardItem 无法正确克隆
【发布时间】:2019-11-04 17:50:43
【问题描述】:

如以下代码所示,当您拖放一个项目(使用clone() 方法从QStandardItem 子类化)时,您会得到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_()

【问题讨论】:

  • 我认为您的意思是 obj = myItem() 而不是 obj = super()。很好 - 我明白 - 但是,无论“数据”和“元”是否丢失。如何避免?
  • 好的,一旦你复制了正确的对象,那么只需将数据/元数据从主要对象复制到新对象——你当然会有手动进行——但应该是你的类的一部分——复制功能注意确保当你这样做时,你正在将苹果复制到苹果,有时你必须提取数据才能使用 setData 函数
  • 在附加的代码中试图做到这一点 - 但它不起作用。在 clone 方法中保存数据(或元数据)不起作用:.data() 上出现以下错误:数据异常无法将 QVariant 转换回 Python 对象
  • 查看您的 QStandardItem 对象文档,因为不,您不能按照您的方式进行操作 - 您使用 obj.data() 获取数据,但您使用 obj.setData(value[, role=Qt.UserRole + 1]) 设置数据,我相信这就是您的方式也设置元数据 - 如果不深入研究它就不能 100% 确定 - 你现在应该能够做到
  • 这些都不起作用。我尝试使用 old_data = self.data() 获取数据(如果我使用 role=Qt.UserRole+1,它是隐含的并且不会改变行为)关键是在调用克隆之前数据(或元)丢失.尝试以类似方式保存元数据失败。

标签: python drag-and-drop pyqt5 qtreeview qstandarditemmodel


【解决方案1】:

这里有几个问题与 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 类,以便pickling is handled correctly

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_()

【讨论】:

  • 确实很有趣。使用全局数据结构(字典)来存储对“真实”数据的引用。我确信这项工作在简单的示例中提供 - 我毫不怀疑。它是否适用于我更大的应用程序将证明是有趣的。但我认为这个问题和答案可能涵盖了使用 QStandardItem 的隐藏“功能”。
  • @TimCarnahan 不,QStandardItem 完全按预期工作。唯一的困难是复制QObject,这不是隐藏功能——它是well-documented。真正的问题在于应用程序的设计,它试图做一些 Qt 明确不支持的事情。
【解决方案2】:

你不是在克隆你的班级,而是一个普通的QStandardItem

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

这实际上意味着“调用 base 类的clone() 方法”。
当从单个类继承子类时,super() 的行为与以子类实例作为第一个参数调用类方法完全相同,所以在这种情况下,它就是这样做的:

obj = QtGui.QStandardItem.clone(self)

super() 最常见的优点是简单性和可维护性(如果您更改要继承的基类,您只需在子类声明中进行);除此之外,它最重要的好处来自多重继承,即当您从 多个 基类继承时,但由于这在 PyQt 中很少见,因此情况并非如此;此外,多继承是不可能的,不止一个 Qt 类。

setItemPrototype()(强调我的)中所述:

提供自己的原型,子类 QStandardItem,重新实现 QStandardItem::clone() 并将原型设置为您的 实例 自定义类

clone() 实际上是does,实际上是使用QStandardItem(other) 构造函数,它创建other 项的副本

因此,您只需这样做即可获得正确的克隆:

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

【讨论】:

  • 对不起 - 不......该解决方案不起作用 - 在你到达 .setData 和 meta = self.meta 之前,数据丢失了 - 根据上面与丹尼斯詹森的讨论 - 我承认你的观察obj = myItem() 但根本问题是,当您在树中拖放项目时,数据(元或 data() )丢失。你如何恢复它(或适当地保存它)
  • 对不起,我的错。原因是 QStandardItemModel 使用 itemPrototype 作为克隆源,而不是项目本身。所以恐怕正确更新数据的唯一方法是重新实现视图的dropEvent。我现在无法测试或更新我的答案,但如果您在那之前无法获得答案,我会在今晚进行。
  • @TimCarnahan 请参阅 my answer 关于此主题。克隆时,仅复制项目数据和标志 - 忽略其他所有内容。此外,Qt 不允许复制QObject,因此在拖放操作期间没有直接的方法来序列化实例。此外,PyQt 使用pickle 复制您的myData 实例 - 但当然QObject 不可腌制,因此也会失败。解决此问题的一种方法是将QObject 实例保留在dict 中,然后改为克隆密钥。
  • @musicamante 这不是原因。问题是 Qt 必须使用QVariant 序列化数据,从而限制了可以复制的内容。
  • @ekhumoro,我相信你是正确的,我不知道该怎么做。我知道pickle(或序列化)不适用于QObject(因此,当我想“保存”我的会话时,我必须删除QObject - 为了生成一个cPickle文件) - 但是,为了在程序中支持拖放操作我迷路了。 (内部序列化是我无法理解的,即使我知道如何我也不知道在哪里)。你上面的答案正是我开始寻找的地方 - 但随后出现了 QObject 问题,因此我在这里。
猜你喜欢
  • 1970-01-01
  • 2021-12-31
  • 2013-11-27
  • 2020-06-30
  • 2012-06-05
  • 1970-01-01
  • 2017-05-19
  • 2018-10-18
  • 2021-10-12
相关资源
最近更新 更多