拖放 QTreeView 中包含 QImage 的项目失败

Drag and drop in QTreeView fails with items that hold a QImage

我在 QTreeView 中有一个项目列表。每个项目都包含一个 QImage 对象。如果我尝试拖放项目,程序会冻结。但是当我注释掉行 objMod._Image = QImage(flags = Qt.AutoColor) 时,程序运行正常。

如何使用 QImage 对象拖放项目? QImage 包含渲染的图像。渲染过程需要一段时间,所以最好保留 QImage 对象。

import sys
import os

from PySide.QtCore    import *
from PySide.QtGui     import *
from PySide.QtUiTools import *

from PIL import Image, ImageCms, ImageQt

class ObjModel:
    def __init__(self):
        self._Image = None

class DragMoveTest(QMainWindow):
    def __init__(self):
        super(DragMoveTest,self).__init__()
        self.initGUI()
        self.show()

    def initGUI(self):
        self.treeView = QTreeView()
        modelTreeView = QStandardItemModel()
        self.treeView.setModel(modelTreeView)
        for i in range(0, 4):
            objMod = ObjModel()
            objMod._Image = None
            objMod._Image = QImage(flags = Qt.AutoColor)

            item = QStandardItem('Test: %s' % str(i))
            item.setData(objMod, Qt.UserRole + 1)
            modelTreeView.invisibleRootItem().appendRow(item)

        self.treeView.setDragDropMode(QAbstractItemView.InternalMove)
        self.setCentralWidget(self.treeView)

def main(args):
    app = QApplication(sys.argv)
    qt_main_wnd = DragMoveTest()
    ret = app.exec_()
    sys.exit(ret)

if __name__ == "__main__":
    main(sys.argv)

这是由 PySide 中的错误引起的。在拖放操作中,被拖项目中的数据必须是serialized。对于大多数数据类型,这将由 Qt 处理,但对于特定于 Python 的类型,需要特殊处理。这种特殊处理似乎在 PySide 中被打破了。如果您的示例转换为 PyQt,则在尝试拖动项目时会引发 TypeError,但程序不会冻结。

问题的根源在于您正在使用自定义 Python class 存储数据。 PyQt 使用 pickle 序列化自定义数据类型,但不可能同时 pickle 存储在其 __dict__ 中的 QImage,因此操作失败。我假设 PySide 必须尝试类似的事情,但出于某种原因,它在失败时不会引发错误。 Qt在拖动的同时抓取鼠标,所以如果操作异常失败,不会再松开,程序会出现卡死的现象。

解决此问题的最简单方法是避免使用自定义 class 来保存 QImage,而是将图像直接存储在项目中:

    image = QImage()
    item = QStandardItem('Test: %s' % i)
    item.setData(image, Qt.UserRole + 1)

要存储更多数据项,您可以为每个数据项使用不同的数据角色,或者使用字典来保存所有数据项:

    data = {'image': QImage(), 'title': 'foo', 'timestamp': 1756790}
    item.setData(data, Qt.UserRole + 1)

但是,如果您这样做,您必须始终使用字符串键 dict,否则你会面临和以前一样的问题。 (使用字符串键意味着 dict 可以转换为 Qt 知道如何序列化的 QMap

(注意:如果您想知道 Qt class 是否可以序列化,请查看文档以查看它是否定义了 datastream operators)。

我想出了一个不同的解决方案。拥有一个包含 io.BytesIO 的对象更容易。您将 ImageData 存储到 bytesIO 变量中。在您的图像库上,您可以从 bytesIO 变量打开图像。 在演示中,class ObjModel 可以处理来自 Pillow/PIL 的 QImage 和图像。如果您使用 set 方法,图像对象将被转换为 bytesIO。 简而言之,这里有一个工作示例:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import os
import io

from PySide.QtCore                          import *
from PySide.QtGui                           import *
from PySide.QtUiTools                       import *

from PIL                import Image, ImageCms, ImageQt

########################################################################
class ObjModel:
    """"""
    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        self._ImageByteIO = None


    #----------------------------------------------------------------------
    def getObjByte(self):
        """"""
        return self._ImageByteIO


    #----------------------------------------------------------------------
    def getQImage(self):
        """"""
        try:
            self._ImageByteIO.seek(0)
            qImg = QImage.fromData(self._ImageByteIO.getvalue())
            return qImg
        except:
            return None


    #----------------------------------------------------------------------
    def getPILImage(self):
        """"""
        try:
            self._ImageByteIO.seek(0)
            img = Image.open(tBytesIO)
            return img
        except:
            return None


    #----------------------------------------------------------------------
    def setObjByte(self, fileName):
        """"""
        try:
            tBytesIO = io.BytesIO()
            f = open (fileName, 'rb')
            tBytesIO.write(f.read())
            f.close()
            self._ImageByteIO = tBytesIO
        except:
            self._ImageByteIO = None


    #----------------------------------------------------------------------
    def setQImage(self, qImg):
        """"""
        try:
            tBytesIO = io.BytesIO()
            qByteArray = QByteArray()
            qBuf = QBuffer(qByteArray)
            qBuf.open(QIODevice.ReadWrite)
            qImg.save(qBuf, 'PNG')
            tBytesIO = io.BytesIO()
            tBytesIO.write(qByteArray.data())
            self._ImageByteIO = tBytesIO
        except:
            self._ImageByteIO = None


    #----------------------------------------------------------------------
    def setPILImage(self, pImg):
        """"""
        tBytesIO = io.BytesIO()
        pImg.save(tBytesIO, 'png')
        self._ImageByteIO = tBytesIO



#----------------------------------------------------------------------
class DragMoveTest(QMainWindow):
    def __init__(self):
        """"""
        super(DragMoveTest,self).__init__()
        self.initGUI()
        self.show()


    #----------------------------------------------------------------------
    def initGUI(self):
        """"""
        self.treeView = QTreeView()
        modelTreeView = QStandardItemModel()
        self.treeView.setModel(modelTreeView)
        for i in range(0, 4):
            objMod = ObjModel()
            objMod.setQImage(QImage(flags = Qt.AutoColor))
            item = QStandardItem('Test: %s' % str(i))
            item.setData(objMod, Qt.UserRole + 1)
            modelTreeView.invisibleRootItem().appendRow(item)

        self.treeView.setDragDropMode(QAbstractItemView.InternalMove)
        self.setCentralWidget(self.treeView)


#----------------------------------------------------------------------
def main(args):
    app = QApplication(sys.argv)
    qt_main_wnd = DragMoveTest()
    ret = app.exec_()
    sys.exit(ret)


#----------------------------------------------------------------------
if __name__ == "__main__":
    main(sys.argv)