在 python 3.6+ 和 PyQt5 中腌制一个 QPolygon

Posted

技术标签:

【中文标题】在 python 3.6+ 和 PyQt5 中腌制一个 QPolygon【英文标题】:Pickle a QPolygon in python 3.6+ and PyQt5 【发布时间】:2019-02-28 14:54:52 【问题描述】:

我尝试腌制一个 QPolygon 并在之后加载它,但出现错误。我已经在 Python2 上使用 PyQt4 完成了此操作,但现在想在 Python3 上使用 PyQt5。

我不想读取/加载使用 Python 2 生成的数据! pickle 文件仅用于临时存储 Qt 元素,例如从 Python3 到 Python3 的 QPolygons。

我已经为 pickle.dump() 测试了从 1 到 4 的不同协议选项,并尝试使用在 Python3 中不应该产生影响的“fix_imports=True”选项。

这是我的简化代码

from PyQt5.QtGui import QPolygon
from PyQt5.QtCore import QPoint
import pickle

file_name = "test_pickle.chip"

with open(file_name, 'wb') as f:
    poly = QPolygon((QPoint(1, 1), QPoint(2, 2))) 
    pickle.dump(poly, f, protocol=2)  # , fix_imports=True)

# loading the data again
with open(file_name, 'rb') as f:
    elem = pickle.load(f, encoding='bytes')  # , fix_imports=True)

我收到以下错误消息,但对此无能为力:

elem = pickle.load(f, encoding='bytes')  # , fix_imports=True)
TypeError: index 0 has type 'int' but 'QPoint' is expected

除了泡菜还有其他选择吗? 提前致谢!

【问题讨论】:

【参考方案1】:

你可以使用QDataStream来序列化/反序列化Qt对象:

from PyQt5 import QtCore
from PyQt5.QtGui import QPolygon
from PyQt5.QtCore import QPoint, QFile, QIODevice, QDataStream, QVariant

file_name = "test.dat"

output_file = QFile(file_name)
output_file.open(QIODevice.WriteOnly)
stream_out = QDataStream(output_file)
output_poly = QPolygon((QPoint(1, 6), QPoint(2, 6)))
output_str = QVariant('foo')  # Use QVariant for QString
stream_out << output_poly << output_str
output_file.close()

input_file = QFile(file_name)
input_file.open(QIODevice.ReadOnly)
stream_in = QDataStream(input_file)
input_poly = QPolygon()
input_str = QVariant()
stream_in >> input_poly >> input_str
input_file.close()

print(str(output_str.value()))
print(str(input_str.value()))

【讨论】:

看起来有一个关于在 QPolygon 中使用 pickle 的错误。我喜欢你的回答,因为它看起来是用 QDataStream 替换 pickle 的一种快速简便的方法。但我可能简化了我的例子。我还需要存储例如输出文件/流中的字符串。 QDataStream也可以吗?我的第一次测试没有成功将字符串和几个 QPolygon 存储在一个输出文件中。 嗨,是的,你可以存储你想要的任何东西,但是你需要使用 Qt 对象,我会更新我的例子给你看。 在读回 QDataStream 时是否有可能检查下一个元素的类型?我必须检查下一个是字符串还是 QPolygon。要确定类型,我必须将数据读入例如QVariant 但这会以某种方式破坏 QPolygons。 我认为默认情况下不可能,但您应该知道添加项目的顺序,对吧?也许您可以创建一个继承自 QObject 并包含类型列表的类,然后将该类传递给第一个类,并使用它来了解反序列化时需要什么类型的项目。【参考方案2】:

在文档上进行一些搜索会告诉您如何通过__reduce__ method 为自定义类实现自定义酸洗。基本上,您返回一个元组,包括取消腌制时要创建的新对象的构造函数以及将传递给构造函数的参数元组。然后,您可以选择传递一个state 对象(请参阅__getstate____setstate__)和一些迭代器来添加位置数据和键值数据。

通过子类化QPolygon,我们可以像这样添加picklability:(这可能会被清理/重组,但这是我得到的第一个实现)

from PyQt5.QtGui import QPolygon
from PyQt5.QtCore import QPoint
import pickle

class Picklable_QPolygon(QPolygon):

    def __reduce__(self):
        # constructor, (initargs), state object passed to __setstate__
        return type(self), (), self.__getstate__()

    #I'm not sure where this gets called other than manually in __reduce__
    #  but it seemed like the semantically correct way to do it...
    def __getstate__(self):
        state = [0]
        for point in self:
            state.append(point.x())
            state.append(point.y())
        return state

    #putPoints seems to omit the `nPoints` arg compared to c++ version.
    #  this may be a version dependent thing for PyQt. Definitely test that
    #  what you're getting out is the same as what you put in..
    def __setstate__(self, state):
        self.putPoints(*state)

poly = Picklable_QPolygon((QPoint(1, 1), QPoint(2, 2)))
s = pickle.dumps(poly)

elem = pickle.loads(s)

【讨论】:

【参考方案3】:

这一定是 pyqt5 中的错误,因为 the documentation states 支持酸洗 QPolygon。因此,请使用您的示例向pyqt mailing list 发布错误报告。

与此同时,迄今为止最简单的替代方法是使用QSettings:

from PyQt5.QtGui import QPolygon
from PyQt5.QtCore import QSettings, QPoint

file_name = "test_pickle.chip"

poly = QPolygon((QPoint(1, 1), QPoint(2, 2)))

# write object
s = QSettings(file_name, QSettings.IniFormat)
s.setValue('poly', poly)

del s, poly

# read object
s = QSettings(file_name, QSettings.IniFormat)
poly = s.value('poly')

print(poly)
print(poly.point(0), poly.point(1))

输出:

<PyQt5.QtGui.QPolygon object at 0x7f871f1f8828>
PyQt5.QtCore.QPoint(1, 1) PyQt5.QtCore.QPoint(2, 2)

这可以与 PyQt 支持的任何类型的酸洗一起使用,以及 PyQt 可以convert to/from a QVariant 的任何其他类型。 PyQt 还支持QSettings.value()type 参数,可用于显式声明所需类型。而且由于QSettings 是为存储应用程序配置数据而设计的,因此可以在同一个文件中存储任意数量的对象(有点像python 的shelve module)。

【讨论】:

我还在文档中找到了相关信息,但不确定我是否做错了什么。我写信给 pyqt 邮件列表,会及时更新。 @HoWil 好的。您是否有某些理由不认为 QSettings 是一个不错的选择?它似乎比任何其他解决方案(包括泡菜本身)更易于使用。在幕后,QSettings 使用数据流运算符进行序列化,因此看起来它必须非常高效(因为一切都发生在 c++ 中)。 原因是我必须命名我放入 QSettings 的每个条目。在我用来展示问题的简化示例中,只有一个 QPolygon。实际上有数百个多边形和字符串。这使得处理所有变量名有点困难。我终于接受了使用 QDataStream 的 isma 解决方案,这与 pickle 最等效,只需一个接一个地编写它并以相同的方式读取它。不过,感谢您的帮助和讨论。 这种方法的一个缺点是数据总是作为配置/设置文件存储在你的 Qt 配置文件夹中。在 linux 上,这是您的主文件夹中的 .config,但不在我的 python 文件旁边或我指定的路径中。 @HoWil 这不是真的 - 你可以specify a particular path。我将更新我的答案以使用该方法(这实际上是我最初的意图)。这将使用标准的 ini 文件格式。【参考方案4】:

受先前答案的启发,我创建了一个函数,该函数采用某种支持 QDataSteam 的 Qt 并返回一个从该类继承且可挑选的类,在以下示例中,我使用 QPoygon 和 QPainterPath 展示:

import pickle
from PyQt5 import QtCore, QtGui

def picklable_reduce(self):
    return type(self), (), self.__getstate__()

def picklable_getstate(self):
    ba = QtCore.QByteArray()
    stream = QtCore.QDataStream(ba, QtCore.QIODevice.WriteOnly)
    stream << self
    return ba

def picklable_setstate(self, ba):
    stream = QtCore.QDataStream(ba, QtCore.QIODevice.ReadOnly)
    stream >> self

def create_qt_picklable(t):
    return type("Picklable_".format(t.__name__), (t,),
        
            '__reduce__': picklable_reduce,
            '__getstate__': picklable_getstate,
            '__setstate__': picklable_setstate
        
    ) 

if __name__ == '__main__':
    # QPolygon picklable
    Picklable_QPolygon = create_qt_picklable(QtGui.QPolygon)
    old_poly = Picklable_QPolygon((QtCore.QPoint(1, 1), QtCore.QPoint(2, 2)))
    s = pickle.dumps(old_poly)
    new_poly = pickle.loads(s)
    assert(old_poly == new_poly)
    # QPainterPath picklable
    Picklable_QPainterPath = create_qt_picklable(QtGui.QPainterPath)
    old_painterpath = Picklable_QPainterPath()
    old_painterpath.addRect(20, 20, 60, 60)
    old_painterpath.moveTo(0, 0)
    old_painterpath.cubicTo(99, 0,  50, 50,  99, 99)
    old_painterpath.cubicTo(0, 99,  50, 50,  0, 0);
    s = pickle.dumps(old_painterpath)
    new_painterpath= pickle.loads(s)
    assert(old_painterpath == new_painterpath)

使用 OP 代码:

if __name__ == '__main__':
    Picklable_QPolygon = create_qt_picklable(QtGui.QPolygon)

    file_name = "test_pickle.chip"
    poly = Picklable_QPolygon((QtCore.QPoint(1, 1), QtCore.QPoint(2, 2))) 
    with open(file_name, 'wb') as f:
        pickle.dump(poly, f, protocol=2)  # , fix_imports=True)

    elem = None
    with open(file_name, 'rb') as f:
        elem = pickle.load(f, encoding='bytes')  # , fix_imports=True)

    assert(poly == elem)

【讨论】:

以上是关于在 python 3.6+ 和 PyQt5 中腌制一个 QPolygon的主要内容,如果未能解决你的问题,请参考以下文章

PyQt5 GUI Programming With Python 3.6

无法在 Windows 10 上的 python 3.6 64 位上运行 pyqt5(没有 anaconda 没有 virtualenv)

如何在 Python 3 中腌制和取消腌制到可移植字符串

由于pyqt5 Python中没有qt插件初始化错误,Qt设计器无法启动

Python:用一些不可腌制的物品腌制一个字典

如何在 python 中腌制一个动态创建的嵌套类?