Search code examples
pythonoffsetqgraphicsitempyside6movable

How to access the position offsets of movable QGraphicsItems?


I recently got started with Python Qt and I'm trying to make a click-and-drag map editor that reads from and writes to a .json file. So far my progress is getting the QGraphicsView to accurately display the entire room layout with rectangles dynamically. But now I have no idea how to access the x and y offsets of each rectangle given that they can all be moved individually with the setFlag(QGraphicsItem.ItemIsMovable) property.

Here is a short version of my code:

import json
import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *

TILEWIDTH = 25
TILEHEIGHT = 15
OUTLINE = 3

class Room:
    def __init__(self, name, width, height, offset_x, offset_z):
        self.name = name
        self.width = width
        self.height = height
        self.offset_x = offset_x
        self.offset_z = offset_z

class Main(QMainWindow):

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

    def initUI(self):
        #Setting up viewport
        self.scene = QGraphicsScene(self)
        self.view = QGraphicsView(self.scene, self)
        
        self.view.scale(1, -1)
        self.view.setStyleSheet("background:transparent; border: 0px")
        self.setCentralWidget(self.view)
        
        self.setGeometry(360, 190, 1200, 700)
        self.showMaximized()
        
        #Reading json and converting entries to rooms
        self.room_list = []
        with open("Data\Content\PB_DT_RoomMaster.json", "r") as file_reader:
            self.content = json.load(file_reader)
        for i in self.content:
            self.room_list.append(self.convert_json_to_room(i))
        self.draw_map()
    
    def convert_json_to_room(self, json):
        name = json["Key"]
        width = json["Value"]["AreaWidthSize"] * TILEWIDTH
        height = json["Value"]["AreaHeightSize"] * TILEHEIGHT
        offset_x = round(json["Value"]["OffsetX"]/12.6) * TILEWIDTH
        offset_z = round(json["Value"]["OffsetZ"]/7.2) * TILEHEIGHT 
        
        room = Room(name, width, height, offset_x, offset_z)
        return room
  
    def draw_map(self):
        for i in self.room_list:
            fill = QColor("#000000")
            outline = QPen("#ffffff")
            outline.setWidth(OUTLINE)
            outline.setJoinStyle(Qt.MiterJoin)
            #Drawing rooms
            rect = self.scene.addRect(i.offset_x, i.offset_z, i.width, i.height, outline, fill)
            rect.setFlag(QGraphicsItem.ItemIsMovable)

def main():
    app = QApplication(sys.argv)
    main = Main()
    main.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

This is the json:

[
  {
    "Key": "m01SIP_000",
    "Value": {
      "LevelName": "m01SIP_000",
      "EnemyPatternSuffix": "",
      "AreaID": "EAreaID::m01SIP",
      "SameRoom": "None",
      "AdjacentRoomName": [
        "m01SIP_001",
        "m01SIP_024",
        "m01SIP_023"
      ],
      "OutOfMap": false,
      "EventFlagNameForShowEventIfNotSeen": "None",
      "EventFlagNameForMarkEventAsSeen": "None",
      "WarpPositionX": 0.0,
      "WarpPositionY": 0.0,
      "WarpPositionZ": 0.0,
      "RoomType": "ERoomType::Normal",
      "RoomPath": "ERoomPath::Both",
      "ConsiderLeft": true,
      "ConsiderRight": true,
      "ConsiderTop": true,
      "ConsiderBottom": true,
      "AreaWidthSize": 2,
      "AreaHeightSize": 1,
      "OffsetX": 25.2,
      "OffsetZ": 0.0,
      "DoorFlag": [
        2,
        32
      ],
      "HiddenFlag": [],
      "RoomCollisionFromSplineOnly": false,
      "RoomCollisionFromGimmick": false,
      "NoRoomOutBlinder": false,
      "Collision2DProjectionDistance": -1.0,
      "FlyMaterialDistance": 10.0,
      "NoTraverse": [],
      "MagCameraFovScale": 0.0,
      "MagCameraVolumeScale": 1.5,
      "DemagCameraFovScale": 0.77,
      "DemagCameraVolumeScale": 0.0,
      "BgmID": "BGM_m01SIP",
      "BgmType": "ERoomBgmType::PlayNormal",
      "Amb1": "AMB_01SIP_Ship_Roll01",
      "AmbVol1": 70,
      "Amb2": "",
      "AmbVol2": 0,
      "Amb3": "",
      "AmbVol3": 0,
      "Amb4": "",
      "AmbVol4": 0,
      "Decay_Near": 1260.0,
      "Decay_Far": 2520.0,
      "Decay_Far_Volume": 0.5,
      "UseLava": false,
      "FrameType": "EFramePlateType::FPT_Full",
      "PerfLevel": 1
    }
  },
  {
    "Key": "m01SIP_001",
    "Value": {
      "LevelName": "m01SIP_001",
      "EnemyPatternSuffix": "",
      "AreaID": "EAreaID::m01SIP",
      "SameRoom": "None",
      "AdjacentRoomName": [
        "m01SIP_000",
        "m01SIP_023",
        "m01SIP_002"
      ],
      "OutOfMap": false,
      "EventFlagNameForShowEventIfNotSeen": "None",
      "EventFlagNameForMarkEventAsSeen": "None",
      "WarpPositionX": 0.0,
      "WarpPositionY": 0.0,
      "WarpPositionZ": 0.0,
      "RoomType": "ERoomType::Normal",
      "RoomPath": "ERoomPath::Both",
      "ConsiderLeft": true,
      "ConsiderRight": true,
      "ConsiderTop": true,
      "ConsiderBottom": true,
      "AreaWidthSize": 3,
      "AreaHeightSize": 1,
      "OffsetX": 50.4,
      "OffsetZ": 0.0,
      "DoorFlag": [
        1,
        24,
        3,
        8
      ],
      "HiddenFlag": [],
      "RoomCollisionFromSplineOnly": false,
      "RoomCollisionFromGimmick": false,
      "NoRoomOutBlinder": false,
      "Collision2DProjectionDistance": -1.0,
      "FlyMaterialDistance": 10.0,
      "NoTraverse": [],
      "MagCameraFovScale": 0.0,
      "MagCameraVolumeScale": 1.5,
      "DemagCameraFovScale": 0.77,
      "DemagCameraVolumeScale": 0.0,
      "BgmID": "BGM_m01SIP",
      "BgmType": "ERoomBgmType::PlayNormal",
      "Amb1": "AMB_01SIP_Ship_Roll01",
      "AmbVol1": 70,
      "Amb2": "AMB_01SIP_Wind02_LP",
      "AmbVol2": 70,
      "Amb3": "",
      "AmbVol3": 0,
      "Amb4": "",
      "AmbVol4": 0,
      "Decay_Near": 1260.0,
      "Decay_Far": 2520.0,
      "Decay_Far_Volume": 0.5,
      "UseLava": false,
      "FrameType": "EFramePlateType::FPT_Full",
      "PerfLevel": 1
    }
  },
...

Basically I need to update the offsets of each room in the json after it's been moved on the map editor. What is the best way to approach this ?


Solution

  • You did not set the offset as a parameter of the rectangle since later you will have to do a conversion since it is in local coordinates of the item, instead use the method pos() that is in coordinates of the scene:

    rect = self.scene.addRect(0, 0, i.width, i.height, outline, fill)
    rect.setPos(i.offset_x, i.offset_z)
    rect.setFlag(QGraphicsItem.ItemIsMovable)
    

    Then you can get the position after doing:

    i.offset_x = rect.pos().x()
    i.offset_z = rect.pos().y()
    

    I don't see the need to separate the information as you can create an item that stores the information so it can be retrieved later.

    import json
    import sys
    from functools import cached_property
    
    from PySide6.QtCore import Qt
    from PySide6.QtGui import QColor, QPen
    from PySide6.QtWidgets import (
        QApplication,
        QGraphicsRectItem,
        QGraphicsScene,
        QGraphicsView,
        QMainWindow,
        QGraphicsItem,
    )
    
    TILEWIDTH = 25
    TILEHEIGHT = 15
    OUTLINE = 3
    
    KEY_METADATA = 1
    
    
    class RoomItem(QGraphicsRectItem):
        def __init__(self, x, y, width, height, metadata=None, parent=None):
            super().__init__(0, 0, width, height, parent)
            self.setPos(x, y)
            self.setFlag(QGraphicsItem.ItemIsMovable)
            self.setData(KEY_METADATA, metadata)
    
            fill = QColor("#000000")
            outline = QPen("#ffffff")
            outline.setWidth(OUTLINE)
            outline.setJoinStyle(Qt.MiterJoin)
            self.setPen(outline)
            self.setBrush(fill)
    
        @classmethod
        def from_json(cls, d):
            x = d["Value"]["OffsetX"] / 12.6 * TILEWIDTH
            y = d["Value"]["OffsetZ"] / 7.2 * TILEHEIGHT
            width = d["Value"]["AreaWidthSize"] * TILEWIDTH
            height = d["Value"]["AreaHeightSize"] * TILEHEIGHT
            return cls(x, y, width, height, d)
    
        def to_json(self):
            d = self.data(KEY_METADATA) or dict()
            d["Value"] = dict(
                OffsetX=self.pos().x() * 12.6 / TILEWIDTH,
                OffsetZ=self.pos().y() * 7.2 / TILEHEIGHT,
                AreaWidthSize=self.rect().width() / TILEWIDTH,
                AreaHeightSize=self.rect().height() / TILEHEIGHT,
            )
            return d
    
    
    class Main(QMainWindow):
        def __init__(self):
            super().__init__()
            self.initUI()
    
        @cached_property
        def items(self):
            return list()
    
        def initUI(self):
            # Setting up viewport
            self.scene = QGraphicsScene(self)
            self.view = QGraphicsView(self.scene, self)
    
            self.view.scale(1, -1)
            self.view.setStyleSheet("background:transparent; border: 0px")
            self.setCentralWidget(self.view)
    
            self.setGeometry(360, 190, 1200, 700)
            self.showMaximized()
    
        def load_from_json(self, filename):
            with open(filename, "r") as f:
                for e in json.load(f):
                    item = RoomItem.from_json(e)
                    self.scene.addItem(item)
                    self.items.append(item)
    
        def save_to_json(self, filename):
            with open(filename, "w") as f:
                l = []
                for item in self.items:
                    l.append(item.to_json())
                json.dump(l, f)
    
    
    def main():
        app = QApplication(sys.argv)
        main = Main()
        main.show()
        main.load_from_json("data.json")
        ret = app.exec_()
    
        main.save_to_json("data.json")
    
        sys.exit(ret)
    
    
    if __name__ == "__main__":
        main()