如何将 qgraphicsitem 的位置保存和加载到 qgraphicsscene 或(正确绘制项目)

How to save and load position of qraphicsitems to qraphicscene or (correctly drow item)

原则上来说,这不是一件难事,但事实证明,一切都不像看起来那么容易。很难理解坐标。因为它们一共有三对:模型、项目、视图。我保留了 x 和 y 位置以及 class 属性的其余部分。然后我尝试使用当前位置将它们添加回来。这就是魔法开始的地方。元素占据了以前明显不存在的位置。这也显然与移动物品功能的实现有关。但是,这不是我的,我不明白如何在开始和结束时正确设置坐标。还有放大缩小场景的功能,是可以的,也会影响后面的加载,不过我已经没有想法了。

加载前

加载后

我的 MRE 工具

import sys
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from  math import pi
import os
from pickle import load,dump

class MyQGraphicsView(QGraphicsView):

    def __init__ (self, parent=None):
        super(MyQGraphicsView, self).__init__ (parent)


    def wheelEvent(self, event):

        if QApplication.keyboardModifiers() == Qt.ControlModifier:
            print('Control+Click')
        # Zoom Factor
            zoomInFactor = 1.25
            zoomOutFactor = 1 / zoomInFactor

            # Set Anchors
            self.setTransformationAnchor(QGraphicsView.NoAnchor)
            self.setResizeAnchor(QGraphicsView.NoAnchor)

            # Save the scene pos
            oldPos = self.mapToScene(event.pos())

            # Zoom
            if event.delta() > 0:
                zoomFactor = zoomInFactor
            else:
                zoomFactor = zoomOutFactor
            self.scale(zoomFactor, zoomFactor)

            # Get the new position
            newPos = self.mapToScene(event.pos())

            # Move scene to old position
            delta = newPos - oldPos
            self.translate(delta.x(), delta.y())

class Communicate(QObject):
    closeApp = Signal()
    add_delete_row = Signal(QModelIndex, str)

class Drow_equipent(QGraphicsItem):
    def __init__(self, x, y, w, h,name,brush=Qt.blue,type='эллипс'):
        super().__init__()
        self.setPos(x, y)
        self.penWidth = 1
        self.name=name
        self.x,self.y,self.h,self.w=x,y,h,w
        self.setAcceptHoverEvents(True)
        self.signal=Communicate()
        self.setFlags(QGraphicsItem.ItemSendsGeometryChanges|QGraphicsItem.ItemIsSelectable)
        self.rotation_=False
        self._brush=QBrush(brush)
        self.pen_color=Qt.black
        self.type_obj=type
        self.types = {'эллипс': lambda x: x.drawEllipse(self.x, self.y, self.w, self.h),'прямоугольник':lambda x: x.drawRect(self.x, self.y, self.w, self.h)}
                                            # x.drawLine(QPoint(self.x+self.x*2,self.y+self.y*2),QPoint(self.w_eq-self.w_eq/8,self.h_eq-self.h_eq/8)),
                                            # x.drawLine(QPoint(self.x+self.x*8,self.y+self.y*8),QPoint(self.x+self.x*4,self.y+self.y*4+self.h_eq/8)),
                                            # x.drawLine(QPoint(self.x, self.y), QPoint(5, self.y)),
                                            # x.drawLine(QPoint(self.w_eq,self.h_eq), QPoint(self.w_eq, self.h_eq-5)),
                                            # x.drawLine(QPoint(self.w_eq, self.h_eq), QPoint(self.w_eq-5, self.h_eq)),





    def get_file_settings(self):

        c=self.scenePos()


        return {'x':c.x(),'y':c.y(),'h':self.h,'w':self.w,'name':self.name,'rotation':self.rotation(),
                'brush_color':self._brush.color().toTuple(),'type_obj':self.type_obj}


    def createDefaultContextMenu(self):
        menu = QMenu()
        menu.addAction('Повернуть').triggered.connect(lambda: self.setMode('scale'))
        return menu

    def hoverEnterEvent(self, event):
        QApplication.instance().setOverrideCursor(Qt.OpenHandCursor)

    def hoverLeaveEvent(self, event):
        QApplication.instance().restoreOverrideCursor()

    def mouseMoveEvent(self, event):

        if self.rotation_:
            self.my_rotation(self.rotation()+1)
            return True


        orig_cursor_position = event.lastScenePos()
        updated_cursor_position = event.scenePos()
        orig_position = self.scenePos()
        updated_cursor_x = updated_cursor_position.x() - orig_cursor_position.x() + orig_position.x()
        updated_cursor_y = updated_cursor_position.y() - orig_cursor_position.y() + orig_position.y()



        self.setPos(QPointF(updated_cursor_x, updated_cursor_y))




    def createDefaultContextMenu(self):
        menu = QMenu()

        menu.addAction('Повернуть').triggered.connect(lambda: self.setMode(True))

        return menu



    def contextMenuEvent(self, event):
            menu = self.createDefaultContextMenu()

            menu.exec_(event.screenPos())

    def setMode(self, mode):
        self.rotation_ = mode

    def mouseReleaseEvent(self, event):
        print('x: {0}, y: {1}'.format(self.pos().x(), self.pos().y()))
        if self.rotation_:
            self.rotation_=False

    def my_rotation(self,angle):
        # self.prepareGeometryChange()
        c=self.mapToScene(self.boundingRect().center())
        self.setRotation(angle)
        cNew = self.mapToScene(self.boundingRect().center())
        offset = c - cNew
        self.moveBy(offset.x(), offset.y())


    def mousePressEvent(self, event):

        print(event.pos())


    def boundingRect(self):
        # return QRectF(-10 - penWidth / 2, -10 - penWidth / 2,
        #               20 + penWidth, 20 + penWidth)

        return QRectF(self.x,self.y,self.w,self.h)
        # return QRectF(self.x,self.y,self.w_eq,self.h_eq)

    def paint(self, painter, option, widget, PySide2_QtWidgets_QWidget=None, *args, **kwargs):
        # painter.drawRoundedRect(-10, -10, 20, 20, 5, 5)
        # painter.setBrush()

        painter.setRenderHints(QPainter.Antialiasing|QPainter.TextAntialiasing,True)

        painter.setPen(self.pen_color)

        painter.setBrush(self._brush)


        self.types[self.type_obj](painter)
        self._brush = painter.brush()



    def setBrush(self,brush):
        self._brush.setColor(brush)

    def get_square(self):
        type={'эллипс': round ((((pi*self.w*self.h)/4)/10000),4),
              'прямоугольник':(self.h*self.w)/10000}
        return  type[self.type_obj]

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.centralwidget = QWidget(self)
        self.centralwidget.setGeometry(0, 0, 600, 700)
        self.centralwidget.setObjectName(u"centralwidget")
        self.gridLayout = QGridLayout(self.centralwidget)
        self.gridLayout.setObjectName(u"gridLayout")
        self.verticalLayout = QVBoxLayout()
        self.verticalLayout.setObjectName(u"verticalLayout")
        self.graphicsView = MyQGraphicsView(self.centralwidget)
        self.graphicsView.setObjectName(u"graphicsView")
        self.scene = QGraphicsScene(self.graphicsView)
        self.scene.setObjectName('scene')
        self.scene.setSceneRect(0, 0, 500, 500)
        self.graphicsView.setScene(self.scene)
        self.graphicsView.setMouseTracking(True)
        self.verticalLayout.addWidget(self.graphicsView)

        self.horizontalLayout = QHBoxLayout()
        self.horizontalLayout.setObjectName(u"horizontalLayout")
        self.add_item = QPushButton(self.centralwidget)
        self.add_item.setObjectName(u"add_item")

        self.horizontalLayout.addWidget(self.add_item)

        self.save_items = QPushButton(self.centralwidget)
        self.save_items.setObjectName(u"save_items")

        self.horizontalLayout.addWidget(self.save_items)

        self.load_items = QPushButton(self.centralwidget)
        self.load_items.setObjectName(u"load_items")

        self.horizontalLayout.addWidget(self.load_items)

        self.verticalLayout.addLayout(self.horizontalLayout)

        self.gridLayout.addLayout(self.verticalLayout, 0, 0, 1, 1)

        self.setGeometry(0, 0, 600, 700)

        self.add_item.pressed.connect(self.add_equipment)
        self.save_items.pressed.connect(self.save_items_)
        self.load_items.pressed.connect(self.load_items_)

        self.retranslateUi()


    # setupUi


    def retranslateUi(self):

        self.add_item.setText(QCoreApplication.translate("MainWindow", u"add item", None))
        self.save_items.setText(QCoreApplication.translate("MainWindow", u"save items", None))
        self.load_items.setText(QCoreApplication.translate("MainWindow", u"load items", None))
    # retranslateUi


    def add_equipment(self):
        self.eq_obj = Drow_equipent(30, 40, 20,
                                    20, 'block', Qt.blue,'прямоугольник')

        self.scene.addItem(self.eq_obj)
        self.last_selected_item = self.eq_obj


    def load_items_(self):
        with open(os.path.join(os.getcwd(), 'config.ini'), 'rb') as f:
            settings = load(f)
        print(settings)

        items_keys = [key for key in settings.keys() if key != 'grid']
        print(items_keys)
        for item in items_keys:
            self.eq_obj = Drow_equipent(settings[item]['x'], settings[item]['y'],
                                        settings[item]['w'], settings[item]['h'],
                                        settings[item]['name'],
                                        QBrush(QColor().fromRgb(*settings[item]['brush_color'])),
                                        settings[item]['type_obj'])
            
            self.scene.addItem(self.eq_obj)


    def save_items_(self):
        settings = {}
        number_item = 0
        for item in self.scene.items():
            settings[f'item_№:{number_item}'] = item.get_file_settings()
            number_item += 1
        with open(os.path.join(os.getcwd(), 'config.ini'), 'wb') as f:
            dump(settings, f)


if __name__ == '__main__':

        app = QApplication(sys.argv)
        translator = QTranslator()
        if len(sys.argv) > 1:
            locale = sys.argv[1]
        else:
            locale = QLocale.system().name()
        translator.load('qt_%s' % locale,
                        QLibraryInfo.location(QLibraryInfo.TranslationsPath))
        app.installTranslator(translator)

        window = MainWindow()
        window.show()
        app.exec_()

要始终考虑的非常重要的事情是 QGraphicsItem 的 position 不必与其内容匹配。

考虑两个矩形项目:

rect1 = self.scene.addRect(5, 10, 20, 10)
rect2 = self.scene.addRect(0, 0, 20, 10)
rect2.setPos(5, 10)

虽然它们都显示在相同的位置,但它们不同。这些参数实际上将成为这些项目的 boundingRect,但第一个的 pos 仍在 (0, 0),第二个已被移动,这是因为图形项目 always 的起始位置为坐标 (0, 0),边界矩形在项目坐标中为 always

创建新项目时,通常最好将其边界矩形设置为 (0, 0),除非需要特殊要求(例如,始终显示为 centered[=57 的项目) =] 在其位置上)。

在展示如何更正您的代码之前,还有其他重要的问题需要解决。

  • 如果你需要允许移动项目,通常不需要自己实现移动,因为 QGraphicsItem 标志 ItemIsMovable 就足够了;如果您需要对鼠标事件进行特殊控制,只需确保在需要移动时调用默认实现即可;
  • 这也意味着 super().mousePressEvent() 必须 被调用,因为 mouseMoveEvents 仅在按下鼠标按钮后接收;
  • x() e y() 是所有 QGraphicsItems 的现有功能,虽然您仍然可以通过 self.pos().x()self.pos().y() 访问它们,但覆盖这些属性确实没有任何好处;
  • 鼠标事件不应该return一个bool;
class Drow_equipent(QGraphicsItem):
    def __init__(self, x, y, w, h, name, brush=Qt.blue, type='эллипс'):
        super().__init__()
        self.setPos(x, y)
        self.penWidth = 1
        self.name = name
        <b>self.h, self.w = h, w</b>
        self.setAcceptHoverEvents(True)
        self.signal = Communicate()
        <b>self.setFlags(
            QGraphicsItem.ItemSendsGeometryChanges|
            QGraphicsItem.ItemIsSelectable|
            QGraphicsItem.ItemIsMovable)</b>
        # ...
        self.types = {
            'эллипс': lambda x: x.drawEllipse(<b>0, 0</b>, self.w, self.h),
            'прямоугольник':lambda x: x.drawRect(<b>0, 0</b>, self.w, self.h)
        }

    # ...
    def mouseMoveEvent(self, event):
        if self.rotation_:
            self.my_rotation(self.rotation() + 1)
        else:
            super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        if self.rotation_:
            self.rotation_ = False
        super().mouseReleaseEvent(event)

    def mousePressEvent(self, event):
        print(event.pos())
        super().mousePressEvent(event)

    def boundingRect(self):
        return QRectF(0, 0, self.w, self.h)

最后,将小部件创建为 QMainWindow 的子项是不够的,而且不受支持。您必须将其设置为中央小部件:

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.centralwidget = QWidget(self)
        self.setCentralWidget(self.centralwidget)

另请注意,虽然对于 运行 不是必需的,但始终建议使用良好的代码风格;应该 总是 逗号后有 space 和等号周围的 space ,否则阅读会更不舒服,因为能够立即区分对象是非常重要:看到self.x,self.y=x,y)与self.x, self.y = x, y([=39]相同=]很好)。阅读官方Style Guide for Python Code.