refactor: distribute serialization/deserialization from Document into item classes

Move per-type serialization logic out of Document's monolithic
  serializeItem()/deserializeItem() into each item class:

  - Add virtual serialize() to AbstractShapeType template, overridden by
    GraphicsRectItem, GraphicPolygonItem, and GraphicsItemGroup
  - Add static createFromJson() factory + self-registration to each item
  - Extract shared pen/brush/common helpers into serializationUtils
  - Add type registry (serializationRegistry) for factory-based deserialization
  - Document::saveAs()/load() now delegate to item->serialize() and
    createItemFromJson() — no longer owns per-type dispatch
  - Adding a new shape type no longer requires touching Document
This commit is contained in:
Jesse Qu 2026-07-09 11:40:14 +08:00
parent c8f71cb9e3
commit 3ad8b211e6
14 changed files with 298 additions and 226 deletions

View File

@ -58,6 +58,9 @@ set(H_HEADER_FILES
include/util/editingSelector.h include/util/editingSelector.h
include/util/selectorManager.h include/util/selectorManager.h
include/util/serializationUtils.h
include/util/serializationRegistry.h
include/graphicsItem/itemControlHandle.h include/graphicsItem/itemControlHandle.h
include/graphicsItem/graphicsBaseItem.h include/graphicsItem/graphicsBaseItem.h
include/graphicsItem/graphicsRectItem.h include/graphicsItem/graphicsRectItem.h
@ -83,6 +86,8 @@ set(CPP_SOURCE_FILES
source/util/scalingSelector.cpp source/util/scalingSelector.cpp
source/util/editingSelector.cpp source/util/editingSelector.cpp
source/util/selectorManager.cpp source/util/selectorManager.cpp
source/util/serializationUtils.cpp
source/util/serializationRegistry.cpp
source/graphicsItem/itemControlHandle.cpp source/graphicsItem/itemControlHandle.cpp
source/graphicsItem/graphicsBaseItem.cpp source/graphicsItem/graphicsBaseItem.cpp

View File

@ -51,10 +51,6 @@ private:
void setModified(bool modified); void setModified(bool modified);
void setupNewScene(); void setupNewScene();
// Serialization helpers
QJsonObject serializeItem(QGraphicsItem *item) const;
QGraphicsItem *deserializeItem(const QJsonObject &obj);
DesignerScene *m_pScene; DesignerScene *m_pScene;
QUndoStack *m_pUndoStack; QUndoStack *m_pUndoStack;
bool m_bModified; bool m_bModified;

View File

@ -8,6 +8,7 @@
#include <QGraphicsItem> #include <QGraphicsItem>
#include <QGraphicsSceneMouseEvent> #include <QGraphicsSceneMouseEvent>
#include <QPen> #include <QPen>
#include <QJsonObject>
enum ShapeType enum ShapeType
{ {
@ -95,6 +96,8 @@ public:
virtual void updateCoordinate() {} virtual void updateCoordinate() {}
virtual QJsonObject serialize() const = 0;
//handle相关 //handle相关
virtual int handleCount() { return m_vecHanle.count(); } virtual int handleCount() { return m_vecHanle.count(); }
virtual ItemControlHandle* getHandle(int nHandle) virtual ItemControlHandle* getHandle(int nHandle)

View File

@ -18,6 +18,8 @@ public:
void removeOperationCopy(); void removeOperationCopy();
void moveOperationCopy(const QPointF&); void moveOperationCopy(const QPointF&);
void rotateOperationCopy(const double&); void rotateOperationCopy(const double&);
QJsonObject serialize() const override;
static QGraphicsItem *createFromJson(const QJsonObject &obj);
void addItems(const QList<QGraphicsItem*>&); void addItems(const QList<QGraphicsItem*>&);
//QList<QGraphicsItem*> getItems() {return m_listItem;} //QList<QGraphicsItem*> getItems() {return m_listItem;}

View File

@ -14,6 +14,8 @@ public:
void resize(int,double, double, const QPointF&); void resize(int,double, double, const QPointF&);
void updateCoordinate(); void updateCoordinate();
void move(const QPointF&); void move(const QPointF&);
QJsonObject serialize() const override;
static QGraphicsItem *createFromJson(const QJsonObject &obj);
void editShape(int, const QPointF&); void editShape(int, const QPointF&);
void addPoint(const QPointF&); void addPoint(const QPointF&);

View File

@ -19,6 +19,8 @@ public:
void setRatioX(double rx) { m_dRatioX = rx; } void setRatioX(double rx) { m_dRatioX = rx; }
void setRatioY(double ry) { m_dRatioY = ry; } void setRatioY(double ry) { m_dRatioY = ry; }
void move(const QPointF&); void move(const QPointF&);
QJsonObject serialize() const override;
static QGraphicsItem *createFromJson(const QJsonObject &obj);
void editShape(int, const QPointF&); void editShape(int, const QPointF&);
protected: protected:

View File

@ -0,0 +1,13 @@
#ifndef SERIALIZATIONREGISTRY_H
#define SERIALIZATIONREGISTRY_H
#include <QGraphicsItem>
#include <QJsonObject>
#include <functional>
using ItemFactory = std::function<QGraphicsItem*(const QJsonObject&)>;
void registerItemFactory(const QString &type, ItemFactory factory);
QGraphicsItem *createItemFromJson(const QJsonObject &obj);
#endif // SERIALIZATIONREGISTRY_H

View File

@ -0,0 +1,19 @@
#ifndef SERIALIZATIONUTILS_H
#define SERIALIZATIONUTILS_H
#include <QJsonObject>
#include <QPen>
#include <QBrush>
#include <QGraphicsItem>
QString penStyleToString(Qt::PenStyle style);
Qt::PenStyle penStyleFromString(const QString &str);
QJsonObject serializePen(const QPen &pen);
QPen deserializePen(const QJsonObject &obj);
QJsonObject serializeBrush(const QBrush &brush);
QJsonObject serializeCommon(const QGraphicsItem *item, const QString &type);
void applyCommon(QGraphicsItem *item, const QJsonObject &obj);
#endif // SERIALIZATIONUTILS_H

View File

@ -2,10 +2,8 @@
#include "designerScene.h" #include "designerScene.h"
#include "graphicsItem/graphicsBaseItem.h" #include "graphicsItem/graphicsBaseItem.h"
#include "graphicsItem/graphicsRectItem.h"
#include "graphicsItem/graphicsPolygonItem.h"
#include "graphicsItem/graphicsItemGroup.h" #include "graphicsItem/graphicsItemGroup.h"
#include "graphicsItem/itemControlHandle.h" #include "util/serializationRegistry.h"
#include "global.h" #include "global.h"
#include <QFile> #include <QFile>
@ -38,8 +36,6 @@ Document::~Document()
void Document::setupNewScene() void Document::setupNewScene()
{ {
if (m_pScene) { if (m_pScene) {
// Clear items only — do NOT delete the scene. The view in DrawingPanel
// still references it, and deleting would leave a dangling pointer.
m_pScene->clear(); m_pScene->clear();
} else { } else {
m_pScene = new DesignerScene(this); m_pScene = new DesignerScene(this);
@ -94,219 +90,6 @@ void Document::clear()
emit filePathChanged(m_sFilePath); emit filePathChanged(m_sFilePath);
} }
// -- Pen style string conversion ------------------------------------------
static QString penStyleToString(Qt::PenStyle style)
{
switch (style) {
case Qt::NoPen: return "none";
case Qt::SolidLine: return "solid";
case Qt::DashLine: return "dash";
case Qt::DotLine: return "dot";
case Qt::DashDotLine: return "dashDot";
case Qt::DashDotDotLine: return "dashDotDot";
default: return "solid";
}
}
static Qt::PenStyle penStyleFromString(const QString &str)
{
static const QHash<QString, Qt::PenStyle> map = {
{"none", Qt::NoPen},
{"solid", Qt::SolidLine},
{"dash", Qt::DashLine},
{"dot", Qt::DotLine},
{"dashDot", Qt::DashDotLine},
{"dashDotDot", Qt::DashDotDotLine}
};
return map.value(str, Qt::SolidLine);
}
// -- Write helpers ---------------------------------------------------------
static QJsonObject serializePen(const QPen &pen)
{
QJsonObject obj;
obj["color"] = pen.color().name(QColor::HexArgb);
obj["width"] = pen.widthF();
obj["style"] = penStyleToString(pen.style());
return obj;
}
static QJsonObject serializeBrush(const QBrush &brush)
{
QJsonObject obj;
obj["color"] = brush.color().name(QColor::HexArgb);
return obj;
}
static QJsonObject serializeCommon(QGraphicsItem *item, const QString &type)
{
QJsonObject obj;
obj["type"] = type;
obj["pos"] = QJsonObject{{"x", item->pos().x()}, {"y", item->pos().y()}};
obj["rotation"] = item->rotation();
obj["zValue"] = item->zValue();
return obj;
}
// -- Item serialization ----------------------------------------------------
QJsonObject Document::serializeItem(QGraphicsItem *item) const
{
// Groups use a different template instantiation (AbstractShapeType<QGraphicsItemGroup>),
// so qgraphicsitem_cast<AbstractShape*> will not match them.
// Check for groups first.
GraphicsItemGroup *group = dynamic_cast<GraphicsItemGroup *>(item);
if (group) {
QJsonObject obj = serializeCommon(item, "group");
obj["pen"] = serializePen(group->pen());
obj["brush"] = serializeBrush(group->brush());
QJsonArray children;
const auto kids = group->childItems();
for (QGraphicsItem *child : kids) {
if (qgraphicsitem_cast<ItemControlHandle *>(child))
continue;
QJsonObject childObj = serializeItem(child);
if (!childObj.isEmpty())
children.append(childObj);
}
obj["children"] = children;
return obj;
}
AbstractShape *shape = qgraphicsitem_cast<AbstractShape *>(item);
if (!shape)
return {};
// Use dynamic_cast for type dispatch — GraphicsBaseItem subclasses do not
// override QGraphicsItem::type(), so the GIT_* enum values are never returned.
if (GraphicsRectItem *rectItem = dynamic_cast<GraphicsRectItem *>(item)) {
bool isRound = rectItem->isRound();
QJsonObject obj = serializeCommon(item, isRound ? "roundRect" : "rect");
obj["width"] = shape->width();
obj["height"] = shape->height();
if (isRound) {
obj["ratioX"] = rectItem->ratioX();
obj["ratioY"] = rectItem->ratioY();
}
obj["pen"] = serializePen(shape->pen());
obj["brush"] = serializeBrush(shape->brush());
return obj;
}
if (GraphicPolygonItem *poly = dynamic_cast<GraphicPolygonItem *>(item)) {
QJsonObject obj = serializeCommon(item, "polygon");
QJsonArray pts;
for (const QPointF &pt : poly->getPoints()) {
pts.append(QJsonObject{{"x", pt.x()}, {"y", pt.y()}});
}
obj["points"] = pts;
obj["pen"] = serializePen(shape->pen());
obj["brush"] = serializeBrush(shape->brush());
return obj;
}
qWarning("Document::serializeItem: unrecognized item");
return {};
}
// -- Item deserialization --------------------------------------------------
static void applyCommon(QGraphicsItem *item, const QJsonObject &obj)
{
QJsonObject pos = obj["pos"].toObject();
item->setPos(pos["x"].toDouble(), pos["y"].toDouble());
item->setRotation(obj["rotation"].toDouble());
item->setZValue(obj["zValue"].toDouble());
}
template<typename T>
static void applyPen(AbstractShapeType<T> *shape, const QJsonObject &penObj)
{
QPen pen;
pen.setColor(QColor(penObj["color"].toString()));
pen.setWidthF(penObj["width"].toDouble());
pen.setStyle(penStyleFromString(penObj["style"].toString()));
shape->setPen(pen);
}
template<typename T>
static void applyBrush(AbstractShapeType<T> *shape, const QJsonObject &brushObj)
{
shape->setBrushColor(QColor(brushObj["color"].toString()));
}
QGraphicsItem *Document::deserializeItem(const QJsonObject &obj)
{
// Note: deserializeItem creates the item but does NOT add it to the scene.
// The caller (load() or a parent group) is responsible for scene addition.
QString type = obj["type"].toString();
if (type == "group") {
GraphicsItemGroup *group = new GraphicsItemGroup();
applyCommon(group, obj);
QJsonArray children = obj["children"].toArray();
QList<QGraphicsItem *> childItems;
for (const QJsonValue &val : children) {
QGraphicsItem *child = deserializeItem(val.toObject());
if (child)
childItems.append(child);
}
// Child positions in JSON are group-relative, but addToGroup()
// treats child pos() as scene-absolute. Convert before grouping.
for (QGraphicsItem *child : childItems)
child->setPos(group->mapToScene(child->pos()));
group->addItems(childItems);
applyPen(group, obj["pen"].toObject());
applyBrush(group, obj["brush"].toObject());
group->setHandleVisible(false);
return group;
}
if (type == "rect" || type == "roundRect") {
double w = obj["width"].toDouble();
double h = obj["height"].toDouble();
bool isRound = (type == "roundRect");
GraphicsRectItem *item = new GraphicsRectItem(
QRectF(-w / 2, -h / 2, w, h).toRect(), isRound);
applyCommon(item, obj);
applyPen(item, obj["pen"].toObject());
applyBrush(item, obj["brush"].toObject());
if (isRound) {
item->setRatioX(obj["ratioX"].toDouble(0.1));
item->setRatioY(obj["ratioY"].toDouble(0.1));
}
item->updateCoordinate();
item->setHandleVisible(false);
return item;
}
if (type == "polygon") {
GraphicPolygonItem *item = new GraphicPolygonItem();
QJsonArray pts = obj["points"].toArray();
for (const QJsonValue &val : pts) {
QJsonObject pt = val.toObject();
item->addPoint(QPointF(pt["x"].toDouble(), pt["y"].toDouble()));
}
item->endDrawing();
applyCommon(item, obj);
applyPen(item, obj["pen"].toObject());
applyBrush(item, obj["brush"].toObject());
item->updateCoordinate();
item->setHandleVisible(false);
return item;
}
return nullptr;
}
// -- Save / Load -----------------------------------------------------------
bool Document::save() bool Document::save()
{ {
if (m_sFilePath.isEmpty()) if (m_sFilePath.isEmpty())
@ -319,10 +102,13 @@ bool Document::saveAs(const QString &filePath)
QJsonArray items; QJsonArray items;
const auto sceneItems = m_pScene->items(); const auto sceneItems = m_pScene->items();
for (QGraphicsItem *item : sceneItems) { for (QGraphicsItem *item : sceneItems) {
// Only serialize top-level items (children of groups are serialized recursively)
if (item->parentItem()) if (item->parentItem())
continue; continue;
QJsonObject obj = serializeItem(item); QJsonObject obj;
if (auto *group = dynamic_cast<GraphicsItemGroup *>(item))
obj = group->serialize();
else if (auto *shape = qgraphicsitem_cast<AbstractShape *>(item))
obj = shape->serialize();
if (!obj.isEmpty()) if (!obj.isEmpty())
items.append(obj); items.append(obj);
} }
@ -362,14 +148,13 @@ bool Document::load(const QString &filePath)
QJsonObject root = doc.object(); QJsonObject root = doc.object();
// Clear current state without triggering undo
m_nSavedIndex = 0; m_nSavedIndex = 0;
m_pUndoStack->clear(); m_pUndoStack->clear();
setupNewScene(); setupNewScene();
QJsonArray items = root["items"].toArray(); QJsonArray items = root["items"].toArray();
for (const QJsonValue &val : items) { for (const QJsonValue &val : items) {
QGraphicsItem *item = deserializeItem(val.toObject()); QGraphicsItem *item = createItemFromJson(val.toObject());
if (item) if (item)
m_pScene->addItem(item); m_pScene->addItem(item);
} }

View File

@ -3,6 +3,7 @@
#include <QPainter> #include <QPainter>
#include <QStyleOption> #include <QStyleOption>
#include <QTimer> #include <QTimer>
#include <QJsonArray>
GraphicsItemGroup::GraphicsItemGroup(QGraphicsItem *parent) GraphicsItemGroup::GraphicsItemGroup(QGraphicsItem *parent)
: AbstractShapeType<QGraphicsItemGroup>(parent) : AbstractShapeType<QGraphicsItemGroup>(parent)
@ -286,3 +287,63 @@ void GraphicsItemGroup::addItems(const QList<QGraphicsItem*>& items)
updateCoordinate(); updateCoordinate();
} }
#include "util/serializationUtils.h"
#include "graphicsItem/graphicsBaseItem.h"
#include "graphicsItem/itemControlHandle.h"
QJsonObject GraphicsItemGroup::serialize() const
{
QJsonObject obj = serializeCommon(this, "group");
obj["pen"] = serializePen(m_pen);
obj["brush"] = serializeBrush(m_brush);
QJsonArray children;
const auto kids = childItems();
for (QGraphicsItem *child : kids) {
if (qgraphicsitem_cast<ItemControlHandle *>(child))
continue;
QJsonObject childObj;
if (auto *group = dynamic_cast<GraphicsItemGroup *>(child))
childObj = group->serialize();
else if (auto *shape = qgraphicsitem_cast<AbstractShape *>(child))
childObj = shape->serialize();
if (!childObj.isEmpty())
children.append(childObj);
}
obj["children"] = children;
return obj;
}
#include "util/serializationRegistry.h"
QGraphicsItem *GraphicsItemGroup::createFromJson(const QJsonObject &obj)
{
GraphicsItemGroup *group = new GraphicsItemGroup();
applyCommon(group, obj);
QJsonArray children = obj["children"].toArray();
QList<QGraphicsItem *> childItems;
for (const QJsonValue &val : children) {
QGraphicsItem *child = createItemFromJson(val.toObject());
if (child)
childItems.append(child);
}
// Child positions in JSON are group-relative, but addToGroup()
// treats child pos() as scene-absolute. Convert before grouping.
for (QGraphicsItem *child : childItems)
child->setPos(group->mapToScene(child->pos()));
group->addItems(childItems);
group->setPen(deserializePen(obj["pen"].toObject()));
group->setBrushColor(QColor(obj["brush"].toObject()["color"].toString()));
group->setHandleVisible(false);
return group;
}
static struct GroupItemRegistrar {
GroupItemRegistrar() {
registerItemFactory("group", &GraphicsItemGroup::createFromJson);
}
} _groupItemRegistrar;

View File

@ -3,6 +3,7 @@
#include <QPainter> #include <QPainter>
#include <QStyleOption> #include <QStyleOption>
#include <QJsonArray>
GraphicPolygonItem::GraphicPolygonItem(QGraphicsItem *parent) GraphicPolygonItem::GraphicPolygonItem(QGraphicsItem *parent)
: GraphicsBaseItem(parent) : GraphicsBaseItem(parent)
@ -150,3 +151,43 @@ bool GraphicPolygonItem::endDrawing()
return bSuccess; return bSuccess;
} }
#include "util/serializationUtils.h"
QJsonObject GraphicPolygonItem::serialize() const
{
QJsonObject obj = serializeCommon(this, "polygon");
QJsonArray pts;
for (const QPointF &pt : m_points) {
pts.append(QJsonObject{{"x", pt.x()}, {"y", pt.y()}});
}
obj["points"] = pts;
obj["pen"] = serializePen(m_pen);
obj["brush"] = serializeBrush(m_brush);
return obj;
}
#include "util/serializationRegistry.h"
QGraphicsItem *GraphicPolygonItem::createFromJson(const QJsonObject &obj)
{
GraphicPolygonItem *item = new GraphicPolygonItem();
QJsonArray pts = obj["points"].toArray();
for (const QJsonValue &val : pts) {
QJsonObject pt = val.toObject();
item->addPoint(QPointF(pt["x"].toDouble(), pt["y"].toDouble()));
}
item->endDrawing();
applyCommon(item, obj);
item->setPen(deserializePen(obj["pen"].toObject()));
item->setBrushColor(QColor(obj["brush"].toObject()["color"].toString()));
item->updateCoordinate();
item->setHandleVisible(false);
return item;
}
static struct PolygonItemRegistrar {
PolygonItemRegistrar() {
registerItemFactory("polygon", &GraphicPolygonItem::createFromJson);
}
} _polygonItemRegistrar;

View File

@ -196,3 +196,48 @@ void GraphicsRectItem::editShape(int nHandle,const QPointF& ptMouse)
prepareGeometryChange(); prepareGeometryChange();
updateHandles(); updateHandles();
} }
#include "util/serializationUtils.h"
QJsonObject GraphicsRectItem::serialize() const
{
QJsonObject obj = serializeCommon(this, m_bIsRound ? "roundRect" : "rect");
obj["width"] = m_dWidth;
obj["height"] = m_dHeight;
if (m_bIsRound) {
obj["ratioX"] = m_dRatioX;
obj["ratioY"] = m_dRatioY;
}
obj["pen"] = serializePen(m_pen);
obj["brush"] = serializeBrush(m_brush);
return obj;
}
#include "util/serializationRegistry.h"
QGraphicsItem *GraphicsRectItem::createFromJson(const QJsonObject &obj)
{
QString type = obj["type"].toString();
bool isRound = (type == "roundRect");
double w = obj["width"].toDouble();
double h = obj["height"].toDouble();
GraphicsRectItem *item = new GraphicsRectItem(
QRectF(-w / 2, -h / 2, w, h).toRect(), isRound);
applyCommon(item, obj);
item->setPen(deserializePen(obj["pen"].toObject()));
item->setBrushColor(QColor(obj["brush"].toObject()["color"].toString()));
if (isRound) {
item->setRatioX(obj["ratioX"].toDouble(0.1));
item->setRatioY(obj["ratioY"].toDouble(0.1));
}
item->updateCoordinate();
item->setHandleVisible(false);
return item;
}
static struct RectItemRegistrar {
RectItemRegistrar() {
registerItemFactory("rect", &GraphicsRectItem::createFromJson);
registerItemFactory("roundRect", &GraphicsRectItem::createFromJson);
}
} _rectItemRegistrar;

View File

@ -0,0 +1,25 @@
#include "util/serializationRegistry.h"
#include <QHash>
#include <QJsonObject>
static QHash<QString, ItemFactory> &registry()
{
static QHash<QString, ItemFactory> map;
return map;
}
void registerItemFactory(const QString &type, ItemFactory factory)
{
registry().insert(type, factory);
}
QGraphicsItem *createItemFromJson(const QJsonObject &obj)
{
QString type = obj["type"].toString();
ItemFactory factory = registry().value(type);
if (factory)
return factory(obj);
qWarning("createItemFromJson: unknown type '%s'", qPrintable(type));
return nullptr;
}

View File

@ -0,0 +1,73 @@
#include "util/serializationUtils.h"
#include <QJsonObject>
#include <QHash>
QString penStyleToString(Qt::PenStyle style)
{
switch (style) {
case Qt::NoPen: return "none";
case Qt::SolidLine: return "solid";
case Qt::DashLine: return "dash";
case Qt::DotLine: return "dot";
case Qt::DashDotLine: return "dashDot";
case Qt::DashDotDotLine: return "dashDotDot";
default: return "solid";
}
}
Qt::PenStyle penStyleFromString(const QString &str)
{
static const QHash<QString, Qt::PenStyle> map = {
{"none", Qt::NoPen},
{"solid", Qt::SolidLine},
{"dash", Qt::DashLine},
{"dot", Qt::DotLine},
{"dashDot", Qt::DashDotLine},
{"dashDotDot", Qt::DashDotDotLine}
};
return map.value(str, Qt::SolidLine);
}
QJsonObject serializePen(const QPen &pen)
{
QJsonObject obj;
obj["color"] = pen.color().name(QColor::HexArgb);
obj["width"] = pen.widthF();
obj["style"] = penStyleToString(pen.style());
return obj;
}
QPen deserializePen(const QJsonObject &obj)
{
QPen pen;
pen.setColor(QColor(obj["color"].toString()));
pen.setWidthF(obj["width"].toDouble());
pen.setStyle(penStyleFromString(obj["style"].toString()));
return pen;
}
QJsonObject serializeBrush(const QBrush &brush)
{
QJsonObject obj;
obj["color"] = brush.color().name(QColor::HexArgb);
return obj;
}
QJsonObject serializeCommon(const QGraphicsItem *item, const QString &type)
{
QJsonObject obj;
obj["type"] = type;
obj["pos"] = QJsonObject{{"x", item->pos().x()}, {"y", item->pos().y()}};
obj["rotation"] = item->rotation();
obj["zValue"] = item->zValue();
return obj;
}
void applyCommon(QGraphicsItem *item, const QJsonObject &obj)
{
QJsonObject pos = obj["pos"].toObject();
item->setPos(pos["x"].toDouble(), pos["y"].toDouble());
item->setRotation(obj["rotation"].toDouble());
item->setZValue(obj["zValue"].toDouble());
}