Add Save As feature and fix document serialization

- Add saveAsToFile() method to Document class for "Save As" functionality
- Add actionSaveAs menu item and onAction_saveAs() handler in MainWindow
- Add isValidItem() method to GraphicsBaseItem to filter helper elements
- Add using declarations to expose AbstractShapeType members in GraphicsBaseItem
- Fix document serialization to properly handle GraphicsItemGroup pen/brush
- HandleType enum starts from 50 to avoid conflicts with future handle types
- Improve deserialization to properly restore item properties

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Jesse Qu 2026-05-18 17:55:56 +08:00
parent 78f2edf24b
commit acf56d7c83
7 changed files with 286 additions and 73 deletions

View File

@ -59,18 +59,39 @@ public:
// =================================================================
/**
* @brief
* @brief
* @param filename 使 filename()
* @return true
*
*
* 1. scene->items()
* 2.
* 3. JSON
* 4. setModified(false)
*
* 1. filename m_filename 使 m_filename
* 2. JSON
* 3.
* 4. lastSavedTime setModified(false)
* 5. saveStatusChanged
*
* m_filename
*/
bool saveToFile(const QString& filename = QString());
/**
* @brief Save As
* @param filename
* @return true
*
*
* 1. JSON
* 2.
* 3. m_filename
* 4. lastSavedTime setModified(false)
* 5. filenameChanged modifiedChanged
*
* saveToFile()
* - saveAsToFile() m_filename
* - saveAsToFile() "另存为"
*/
bool saveAsToFile(const QString& filename);
/**
* @brief
* @param filename

View File

@ -47,6 +47,10 @@ public:
public:
virtual ShapeType getType() {return m_type;}
// 显式类型检查只有有效的图元item 或 group才通过检查
// 用于区分真正的图元与辅助元素(如 handle
bool isValidItem() const { return m_type == T_item || m_type == T_group; }
QPen pen() { return m_pen; }
void setPen(const QPen &pen) { m_pen = pen; }
QColor penColor() { return m_pen.color(); }
@ -292,6 +296,20 @@ class GraphicsBaseItem : public QObject, public AbstractShapeType<QGraphicsItem>
Q_OBJECT
public:
// 使用 using 声明暴露 AbstractShapeType 的成员函数,解决多重继承下的访问问题
using AbstractShapeType<QGraphicsItem>::penColor;
using AbstractShapeType<QGraphicsItem>::setPenColor;
using AbstractShapeType<QGraphicsItem>::brushColor;
using AbstractShapeType<QGraphicsItem>::setBrushColor;
using AbstractShapeType<QGraphicsItem>::pen;
using AbstractShapeType<QGraphicsItem>::setPen;
using AbstractShapeType<QGraphicsItem>::brush;
using AbstractShapeType<QGraphicsItem>::setBrush;
using AbstractShapeType<QGraphicsItem>::width;
using AbstractShapeType<QGraphicsItem>::setWidth;
using AbstractShapeType<QGraphicsItem>::height;
using AbstractShapeType<QGraphicsItem>::setHeight;
GraphicsBaseItem(QGraphicsItem *parent);
virtual ~GraphicsBaseItem();

View File

@ -5,7 +5,7 @@
enum HandleType
{
T_resize, //调整大小
T_resize = 50, //调整大小
T_rotate, //旋转
T_editShape //编辑形状
};

View File

@ -56,6 +56,7 @@ private slots:
void onAction_new();
void onAction_open();
void onAction_save();
void onAction_saveAs();
void onDocument_modifiedChanged(bool modified);
void onDocument_filenameChanged(const QString& filename);
void onDocument_saveStatusChanged(bool success, const QString& message);

View File

@ -71,18 +71,23 @@ bool Document::saveToFile(const QString& filename)
QString targetFile = filename.isEmpty() ? m_filename : filename;
if (targetFile.isEmpty()) {
// 没有目标文件名,需要另存为对话框(这里只设置失败状态)
emit saveStatusChanged(false, tr("未指定文件路径"));
// 没有目标文件名,无法保存
emit saveStatusChanged(false, tr("未指定文件路径,请使用另存为功能"));
return false;
}
bool success = saveInternal(targetFile);
if (success) {
m_filename = targetFile;
m_lastSavedTime = QDateTime::currentDateTime();
m_modified = false;
emit filenameChanged(m_filename);
// 如果传入的文件名与当前文件名不同,更新 m_filename
if (!filename.isEmpty() && filename != m_filename) {
m_filename = filename;
emit filenameChanged(m_filename);
}
emit modifiedChanged(false);
emit saveStatusChanged(true, tr("保存成功"));
} else {
@ -92,6 +97,32 @@ bool Document::saveToFile(const QString& filename)
return success;
}
bool Document::saveAsToFile(const QString& filename)
{
if (filename.isEmpty()) {
emit saveStatusChanged(false, tr("未指定文件路径"));
return false;
}
bool success = saveInternal(filename);
if (success) {
QString oldFilename = m_filename;
m_filename = filename;
m_lastSavedTime = QDateTime::currentDateTime();
m_modified = false;
// 另存为总是会更新文件名
emit filenameChanged(m_filename);
emit modifiedChanged(false);
emit saveStatusChanged(true, tr("另存为成功"));
} else {
emit saveStatusChanged(false, tr("另存为失败"));
}
return success;
}
bool Document::loadFromFile(const QString& filename)
{
bool success = loadInternal(filename);
@ -149,6 +180,13 @@ QByteArray Document::serialize() const
continue;
}
// 双重检查:通过 isValidItem() 确保不是辅助元素(如 ItemControlHandle
// ItemControlHandle 是 QGraphicsRectItem 子类,不是 GraphicsBaseItem 子类
// 即使 qgraphicsitem_cast 错误返回非 nullisValidItem() 也能过滤掉m_type 为 T_undefined
if (!baseItem->isValidItem()) {
continue;
}
QJsonObject itemObj = serializeItem(baseItem);
itemsArray.append(itemObj);
}
@ -272,16 +310,29 @@ QJsonObject Document::serializeItem(GraphicsBaseItem* item) const
scaleObj.insert("y", item->scale());
obj.insert("scale", scaleObj);
// 样式属性
QJsonObject penObj;
penObj.insert("color", item->penColor().name());
penObj.insert("width", item->pen().widthF());
penObj.insert("style", static_cast<int>(item->pen().style()));
obj.insert("pen", penObj);
// 样式属性只用于非组项GraphicsItemGroup 没有 pen/brush
GraphicsItemGroup* group = qgraphicsitem_cast<GraphicsItemGroup*>(item);
if (!group) {
// 普通图元才有 pen/brush
QJsonObject penObj;
QColor penColor = item->penColor();
if (penColor.isValid()) {
penObj.insert("color", penColor.name());
} else {
penObj.insert("color", QString());
}
penObj.insert("width", item->pen().widthF());
penObj.insert("style", static_cast<int>(item->pen().style()));
obj.insert("pen", penObj);
QJsonObject brushObj;
brushObj.insert("color", item->brushColor().name());
obj.insert("brush", brushObj);
QJsonObject brushObj;
brushObj.insert("color", item->brushColor().name());
obj.insert("brush", brushObj);
} else {
// 组没有 pen/brush插入空对象
obj.insert("pen", QJsonObject());
obj.insert("brush", QJsonObject());
}
// 尺寸
obj.insert("width", item->width());
@ -291,17 +342,16 @@ QJsonObject Document::serializeItem(GraphicsBaseItem* item) const
obj.insert("properties", serializeItemProperties(item));
// 如果是组,递归序列化子项
GraphicsItemGroup* group = qgraphicsitem_cast<GraphicsItemGroup*>(item);
if (group) {
QJsonArray childrenArray;
QList<QGraphicsItem*> children = group->childItems();
for (QGraphicsItem* child : children) {
// QGraphicsItem 没有 QObject 接口,所以不能用 qobject_cast
// 使用 static_cast 并检查 nullptr
GraphicsBaseItem* childBase = static_cast<GraphicsBaseItem*>(child);
// 使用 qgraphicsitem_cast 安全转换,只序列化 GraphicsBaseItem 子类的项
GraphicsBaseItem* childBase = qgraphicsitem_cast<GraphicsBaseItem*>(child);
if (childBase) {
childrenArray.append(serializeItem(childBase));
}
// 跳过 ItemControlHandle 等非 GraphicsBaseItem 的子项
}
obj.insert("children", childrenArray);
}
@ -314,71 +364,54 @@ void Document::deserializeItem(const QJsonObject& obj, QGraphicsScene* scene, QG
QString typeStr = obj["type"].toString();
GraphicsBaseItem* item = nullptr;
GraphicsItemGroup* group = nullptr;
// 根据类型创建图元
// 根据类型创建图元(属性在工厂函数中已设置)
if (typeStr == "GraphicsRectItem") {
item = deserializeRectItem(obj);
} else if (typeStr == "GraphicsBusSectionItem") {
item = deserializeBusSectionItem(obj);
} else if (typeStr == "GraphicsItemGroup") {
GraphicsItemGroup* group = deserializeItemGroup(obj, scene, parent);
// 对于组,不设置单个图元的属性,只递归处理子项
group = deserializeItemGroup(obj, scene, parent);
// 先添加到 scene 或父组,再递归处理子项
if (parent) {
GraphicsItemGroup* parentGroup = qgraphicsitem_cast<GraphicsItemGroup*>(parent);
if (parentGroup) {
parentGroup->addToGroup(group);
}
} else {
scene->addItem(group);
}
// 递归处理子项(此时 group 已在 scene 中)
QJsonArray childrenArray = obj["children"].toArray();
for (auto childIt : childrenArray) {
deserializeItem(childIt.toObject(), scene, group);
}
// 添加完子项后,更新 group 的 coordinate会自动计算 boundingRect
group->updateCoordinate();
// 更新 handle 位置
group->updateHandles();
return;
}
// GraphicsPolygonItem 暂时跳过,因为没有 Q_OBJECT 宏
// GraphicsPolygonItem 暂时跳过
if (!item) {
return;
}
// 设置基本属性
QJsonObject posObj = obj["pos"].toObject();
item->setPos(posObj["x"].toDouble(), posObj["y"].toDouble());
item->setRotation(obj["rotation"].toDouble());
// Qt 的 setScale() 只接受单个参数,使用 QTransform 来设置非均匀缩放
QJsonObject scaleObj = obj["scale"].toObject();
double scaleX = scaleObj["x"].toDouble();
double scaleY = scaleObj["y"].toDouble();
QTransform transform;
transform.scale(scaleX, scaleY);
item->setTransform(transform);
QJsonObject penObj = obj["pen"].toObject();
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();
QBrush brush;
brush.setColor(QColor(brushObj["color"].toString()));
item->setBrush(brush);
item->setWidth(obj["width"].toDouble());
item->setHeight(obj["height"].toDouble());
// 添加到场景或父项
if (parent) {
// 添加到组中
GraphicsItemGroup* group = static_cast<GraphicsItemGroup*>(parent);
if (group) {
group->addItems(QList<QGraphicsItem*>() << item);
GraphicsItemGroup* parentGroup = qgraphicsitem_cast<GraphicsItemGroup*>(parent);
if (parentGroup) {
parentGroup->addToGroup(item);
}
} else {
scene->addItem(item);
}
// 普通图元没有子项
// 递归处理子项
QJsonArray childrenArray = obj["children"].toArray();
for (auto childIt : childrenArray) {
deserializeItem(childIt.toObject(), scene, item);
}
// 添加后更新 handle 位置(如果有)
item->setHandleVisible(false); // 默认不显示 handle
}
// =================================================================
@ -465,8 +498,48 @@ GraphicsBaseItem* Document::deserializeRectItem(const QJsonObject& obj)
double height = obj["height"].toDouble();
// 图元的原点在中心,因此 rect 的左上角为 (-width/2, -height/2)
// 在构造函数中已经设置了 boundingRect所以不需要再调用 setWidth/setHeight
QRect rect(-width/2, -height/2, width, height);
return new GraphicsRectItem(rect, false);
GraphicsRectItem* item = new GraphicsRectItem(rect, false);
// 从 JSON 恢复属性
QJsonObject posObj = obj["pos"].toObject();
item->setPos(posObj["x"].toDouble(), posObj["y"].toDouble());
item->setRotation(obj["rotation"].toDouble());
// 使用 setScale 而不是 setTransform避免与 item 的 transform 冲突)
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()) {
// 如果 pen 对象为空(来自组的序列化),使用默认值
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);
}
return item;
}
GraphicsBaseItem* Document::deserializeBusSectionItem(const QJsonObject& obj)
@ -474,9 +547,45 @@ GraphicsBaseItem* Document::deserializeBusSectionItem(const QJsonObject& obj)
// 总线段重建
double width = obj["width"].toDouble();
double height = obj["height"].toDouble();
// GraphicsBusSectionItem 构造函数rect, parent
QRect rect(0, 0, width, height);
return new GraphicsBusSectionItem(rect);
QRect rect(-width/2, -height/2, width, height);
GraphicsBusSectionItem* item = new GraphicsBusSectionItem(rect);
// 从 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);
}
return item;
}
GraphicsItemGroup* Document::deserializeItemGroup(const QJsonObject& obj,
@ -484,8 +593,28 @@ GraphicsItemGroup* Document::deserializeItemGroup(const QJsonObject& obj,
QGraphicsItem* parent)
{
GraphicsItemGroup* group = new GraphicsItemGroup(parent);
group->setWidth(obj["width"].toDouble());
group->setHeight(obj["height"].toDouble());
// 先设置位置、旋转等变换属性
QJsonObject posObj = obj["pos"].toObject();
group->setPos(posObj["x"].toDouble(), posObj["y"].toDouble());
group->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) {
group->setScale(qMax(scaleX, scaleY));
}
// 获取宽高,但不立即调用 setWidth/setHeight会触发 updateCoordinate
double width = obj["width"].toDouble();
double height = obj["height"].toDouble();
// 注意:对于 GraphicsItemGroup其 boundingRect 是由子项自动计算的
// 我们创建一个临时的空矩形,待子项添加后会自动更新
// 暂时不设置宽高,因为 updateCoordinate 需要 parentItem 为空才能正常工作
// 子项添加后group 的 boundingRect 会自动重新计算
return group;
}

View File

@ -156,6 +156,7 @@ void CMainWindow::initializeAction()
connect(ui->actionNew, SIGNAL(triggered()), this, SLOT(onAction_new()));
connect(ui->actionOpen, SIGNAL(triggered()), this, SLOT(onAction_open()));
connect(ui->actionSave, SIGNAL(triggered()), this, SLOT(onAction_save()));
connect(ui->actionSaveAs, SIGNAL(triggered()), this, SLOT(onAction_saveAs()));
}
void CMainWindow::onAction_zoomIn()
@ -270,6 +271,7 @@ void CMainWindow::initializeDocument()
ui->menuFile->addAction(ui->actionNew);
ui->menuFile->addAction(ui->actionOpen);
ui->menuFile->addAction(ui->actionSave);
ui->menuFile->addAction(ui->actionSaveAs);
ui->menuFile->addSeparator();
}
@ -337,13 +339,40 @@ void CMainWindow::onAction_open()
void CMainWindow::onAction_save()
{
// 如果文档没有文件名,执行另存为
if (m_pDocument->filename().isEmpty()) {
onAction_saveAs();
return;
}
if (m_pDocument->saveToFile()) {
// 保存成功,标记为未修改
m_pDocument->setModified(false);
updateWindowTitle();
}
}
void CMainWindow::onAction_saveAs()
{
QString defaultFileName = m_pDocument->filename();
if (defaultFileName.isEmpty()) {
defaultFileName = QString("未命名.bay");
}
QString fileName = QFileDialog::getSaveFileName(this, tr("另存为"), defaultFileName,
tr("BayTemplate Files (*.bay);;All Files (*)"));
if (fileName.isEmpty()) {
return;
}
// 确保文件扩展名为.bay
if (!fileName.toLower().endsWith(".bay")) {
fileName += ".bay";
}
if (!m_pDocument->saveAsToFile(fileName)) {
QMessageBox::critical(this, tr("错误"), tr("无法保存文件:%1").arg(fileName));
}
}
// =================================================================
// Document 信号处理
// =================================================================

View File

@ -49,6 +49,7 @@
<addaction name="actionNew"/>
<addaction name="actionOpen"/>
<addaction name="actionSave"/>
<addaction name="actionSaveAs"/>
<addaction name="separator"/>
<addaction name="actionCopy"/>
<addaction name="actionCut"/>
@ -105,6 +106,20 @@
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionSaveAs">
<property name="icon">
<iconset theme="document-save-as"/>
</property>
<property name="text">
<string>另存为</string>
</property>
<property name="toolTip">
<string>另存为 (A)</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionCopy">
<property name="icon">
<iconset theme="edit-copy"/>