Fix serialization/deserialization for rounded rects, polygons, and groups

- Rounded rects: serialize/restore isRound, ratioX, ratioY; pass isRound
  to constructor so rounded-corner control handles are created
- Polygons: implement full serialization/deserialization including vertex
  points; add updateBoundingRectFromPoints() to fix hit-test and handles
  after deserialization
- Groups: add updateBoundingRectFromChildren() to correctly compute
  bounding rect after child items are deserialized

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Jesse Qu 2026-05-19 10:27:47 +08:00
parent acf56d7c83
commit f6d6e71e32
7 changed files with 127 additions and 21 deletions

View File

@ -221,6 +221,7 @@ private:
GraphicsBaseItem* deserializeRectItem(const QJsonObject& obj);
GraphicsBaseItem* deserializeBusSectionItem(const QJsonObject& obj);
GraphicsBaseItem* deserializePolygonItem(const QJsonObject& obj);
GraphicsItemGroup* deserializeItemGroup(const QJsonObject& obj, QGraphicsScene* scene, QGraphicsItem* parent);
QString m_filename; // 文档文件名

View File

@ -20,6 +20,7 @@ public:
void rotateOperationCopy(const double&);
void addItems(const QList<QGraphicsItem*>&);
void updateBoundingRectFromChildren();
//QList<QGraphicsItem*> getItems() {return m_listItem;}
protected:

View File

@ -18,6 +18,7 @@ public:
void addPoint(const QPointF&);
bool endDrawing();
QPolygonF getPoints(void) { return m_points; }
void updateBoundingRectFromPoints();
protected:
virtual QPainterPath shape();

View File

@ -15,6 +15,13 @@ public:
void move(const QPointF&);
void editShape(int, const QPointF&);
bool isRound() const { return m_bIsRound; }
void setIsRound(bool round) { m_bIsRound = round; }
double ratioX() const { return m_dRatioX; }
double ratioY() const { return m_dRatioY; }
void setRatioX(double rx) { m_dRatioX = rx; }
void setRatioY(double ry) { m_dRatioY = ry; }
protected:
virtual QPainterPath shape();
virtual void paint(QPainter*, const QStyleOptionGraphicsItem*, QWidget*);

View File

@ -4,8 +4,7 @@
#include "graphicsItem/graphicsItemGroup.h"
#include "graphicsItem/graphicsRectItem.h"
#include "graphicsItem/graphicsBusSectionItem.h"
class GraphicsPolygonItem; // 前向声明,暂不使用多边形
#include "graphicsItem/graphicsPolygonItem.h"
#include <QJsonObject>
#include <QJsonArray>
@ -371,6 +370,8 @@ void Document::deserializeItem(const QJsonObject& obj, QGraphicsScene* scene, QG
item = deserializeRectItem(obj);
} else if (typeStr == "GraphicsBusSectionItem") {
item = deserializeBusSectionItem(obj);
} else if (typeStr == "GraphicPolygonItem") {
item = deserializePolygonItem(obj);
} else if (typeStr == "GraphicsItemGroup") {
group = deserializeItemGroup(obj, scene, parent);
// 先添加到 scene 或父组,再递归处理子项
@ -387,13 +388,13 @@ void Document::deserializeItem(const QJsonObject& obj, QGraphicsScene* scene, QG
for (auto childIt : childrenArray) {
deserializeItem(childIt.toObject(), scene, group);
}
// 添加完子项后,更新 group 的 coordinate会自动计算 boundingRect
group->updateCoordinate();
// 更新 handle 位置
group->updateHandles();
// 添加完子项后,更新 group 的 m_boundingRect
// 使用 Qt 的 QGraphicsItemGroup::boundingRect() 计算实际包围盒,
// 然后更新 m_boundingRect、m_boundingRect_selected 和 handle 位置
// 这适用于顶层和嵌套 groupupdateCoordinate 对嵌套 group 不更新 m_boundingRect
group->updateBoundingRectFromChildren();
return;
}
// GraphicsPolygonItem 暂时跳过
if (!item) {
return;
@ -410,7 +411,9 @@ void Document::deserializeItem(const QJsonObject& obj, QGraphicsScene* scene, QG
}
// 普通图元没有子项
// 添加后更新 handle 位置(如果有)
// 添加后更新 handles 位置和 m_boundingRect_selected
// 这一步至关重要deserialization 后需要重新计算选中框和 handle 位置
item->updateHandles();
item->setHandleVisible(false); // 默认不显示 handle
}
@ -470,8 +473,9 @@ QString Document::getTypeString(GraphicsBaseItem* item) const
return "GraphicsBusSectionItem";
} else if (qobject_cast<GraphicsItemGroup*>(item)) {
return "GraphicsItemGroup";
} else if (qobject_cast<GraphicPolygonItem*>(item)) {
return "GraphicPolygonItem";
}
// GraphicsPolygonItem 没有 Q_OBJECT 宏,暂时不支持序列化
return "GraphicsBaseItem";
}
@ -482,11 +486,26 @@ QJsonObject Document::serializeItemProperties(GraphicsBaseItem* item) const
// 类型特定的属性序列化
GraphicsRectItem* rectItem = qobject_cast<GraphicsRectItem*>(item);
if (rectItem) {
// 这里可以添加圆角矩形等特殊属性
// props.insert("isRound", rectItem->m_bIsRound);
props.insert("isRound", rectItem->isRound());
if (rectItem->isRound()) {
props.insert("ratioX", rectItem->ratioX());
props.insert("ratioY", rectItem->ratioY());
}
}
// 多边形等特殊属性可以在这里扩展
// 多边形顶点序列化
GraphicPolygonItem* polygonItem = qobject_cast<GraphicPolygonItem*>(item);
if (polygonItem) {
QPolygonF points = polygonItem->getPoints();
QJsonArray pointsArray;
for (const QPointF& pt : points) {
QJsonObject ptObj;
ptObj.insert("x", pt.x());
ptObj.insert("y", pt.y());
pointsArray.append(ptObj);
}
props.insert("points", pointsArray);
}
return props;
}
@ -497,10 +516,14 @@ GraphicsBaseItem* Document::deserializeRectItem(const QJsonObject& obj)
double width = obj["width"].toDouble();
double height = obj["height"].toDouble();
// 先读取 properties获取 isRound 以在构造时创建正确的 control handles
QJsonObject props = obj["properties"].toObject();
bool isRound = props["isRound"].toBool(false);
// 图元的原点在中心,因此 rect 的左上角为 (-width/2, -height/2)
// 在构造函数中已经设置了 boundingRect所以不需要再调用 setWidth/setHeight
QRect rect(-width/2, -height/2, width, height);
GraphicsRectItem* item = new GraphicsRectItem(rect, false);
GraphicsRectItem* item = new GraphicsRectItem(rect, isRound);
// 从 JSON 恢复属性
QJsonObject posObj = obj["pos"].toObject();
@ -539,6 +562,12 @@ GraphicsBaseItem* Document::deserializeRectItem(const QJsonObject& obj)
item->setBrush(brush);
}
// 恢复圆角比例
if (isRound) {
item->setRatioX(props["ratioX"].toDouble(0.1));
item->setRatioY(props["ratioY"].toDouble(0.1));
}
return item;
}
@ -588,6 +617,61 @@ GraphicsBaseItem* Document::deserializeBusSectionItem(const QJsonObject& obj)
return item;
}
GraphicsBaseItem* Document::deserializePolygonItem(const QJsonObject& obj)
{
GraphicPolygonItem* item = new GraphicPolygonItem();
// 恢复顶点数据(必须在设置变换之前,因为 addPoint 内部使用 mapFromScene 转换,
// 此时 item 无变换mapToScene/mapFromScene 为恒等变换,顶点坐标不变)
QJsonObject props = obj["properties"].toObject();
QJsonArray pointsArray = props["points"].toArray();
for (const QJsonValue& val : pointsArray) {
QJsonObject ptObj = val.toObject();
QPointF localPt(ptObj["x"].toDouble(), ptObj["y"].toDouble());
item->addPoint(item->mapToScene(localPt));
}
// 从 JSON 恢复变换属性
QJsonObject posObj = obj["pos"].toObject();
item->setPos(posObj["x"].toDouble(), posObj["y"].toDouble());
item->setRotation(obj["rotation"].toDouble());
QJsonObject scaleObj = obj["scale"].toObject();
double scaleX = scaleObj["x"].toDouble();
double scaleY = scaleObj["y"].toDouble();
if (scaleX != 1.0 || scaleY != 1.0) {
item->setScale(qMax(scaleX, scaleY));
}
QJsonObject penObj = obj["pen"].toObject();
if (penObj.isEmpty()) {
QPen pen(Qt::black);
pen.setWidthF(1.0);
pen.setStyle(Qt::SolidLine);
item->setPen(pen);
} else {
QPen pen;
pen.setColor(QColor(penObj["color"].toString()));
pen.setWidthF(penObj["width"].toDouble());
pen.setStyle(static_cast<Qt::PenStyle>(penObj["style"].toInt()));
item->setPen(pen);
}
QJsonObject brushObj = obj["brush"].toObject();
if (brushObj.isEmpty()) {
item->setBrush(QBrush(Qt::NoBrush));
} else {
QBrush brush;
brush.setColor(QColor(brushObj["color"].toString()));
item->setBrush(brush);
}
// 从顶点数据更新 boundingRect否则 hit-test 和 handles 都会失效
item->updateBoundingRectFromPoints();
return item;
}
GraphicsItemGroup* Document::deserializeItemGroup(const QJsonObject& obj,
QGraphicsScene* scene,
QGraphicsItem* parent)
@ -606,14 +690,10 @@ GraphicsItemGroup* Document::deserializeItemGroup(const QJsonObject& obj,
group->setScale(qMax(scaleX, scaleY));
}
// 获取宽高,但不立即调用 setWidth/setHeight会触发 updateCoordinate
double width = obj["width"].toDouble();
double height = obj["height"].toDouble();
// 注意:对于 GraphicsItemGroup其 boundingRect 是由子项自动计算的
// 我们创建一个临时的空矩形,待子项添加后会自动更新
// 暂时不设置宽高,因为 updateCoordinate 需要 parentItem 为空才能正常工作
// 子项添加后group 的 boundingRect 会自动重新计算
// 注意:不设置宽高,因为 GraphicsItemGroup 的 m_boundingRect 需要在添加子项后才能正确计算
// 在 deserializeItem() 中,添加子项后会调用 updateCoordinate() 和 updateHandles()
// 如果 group 没有 parent顶层 groupupdateCoordinate() 会自动更新 m_boundingRect
// 如果 group 有 parent我们需要在 addChildren 后手动调用 updateHandles() 来更新 handle 位置
return group;
}

View File

@ -275,6 +275,14 @@ void GraphicsItemGroup::rotateOperationCopy(const double& dAngle)
}
void GraphicsItemGroup::updateBoundingRectFromChildren()
{
m_boundingRect = QGraphicsItemGroup::boundingRect();
m_dWidth = m_boundingRect.width();
m_dHeight = m_boundingRect.height();
updateHandles();
}
void GraphicsItemGroup::addItems(const QList<QGraphicsItem*>& items)
{
foreach (QGraphicsItem *item, items)

View File

@ -150,3 +150,11 @@ bool GraphicPolygonItem::endDrawing()
return bSuccess;
}
void GraphicPolygonItem::updateBoundingRectFromPoints()
{
m_boundingRect = m_points.boundingRect();
m_dWidth = m_boundingRect.width();
m_dHeight = m_boundingRect.height();
m_lastPoints = m_points;
}