diff --git a/include/document.h b/include/document.h index 85e5cc7..6e028ca 100644 --- a/include/document.h +++ b/include/document.h @@ -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 文件路径 diff --git a/include/graphicsItem/graphicsBaseItem.h b/include/graphicsItem/graphicsBaseItem.h index 6ad0361..59cd7ea 100644 --- a/include/graphicsItem/graphicsBaseItem.h +++ b/include/graphicsItem/graphicsBaseItem.h @@ -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 Q_OBJECT public: + // 使用 using 声明暴露 AbstractShapeType 的成员函数,解决多重继承下的访问问题 + using AbstractShapeType::penColor; + using AbstractShapeType::setPenColor; + using AbstractShapeType::brushColor; + using AbstractShapeType::setBrushColor; + using AbstractShapeType::pen; + using AbstractShapeType::setPen; + using AbstractShapeType::brush; + using AbstractShapeType::setBrush; + using AbstractShapeType::width; + using AbstractShapeType::setWidth; + using AbstractShapeType::height; + using AbstractShapeType::setHeight; + GraphicsBaseItem(QGraphicsItem *parent); virtual ~GraphicsBaseItem(); diff --git a/include/graphicsItem/itemControlHandle.h b/include/graphicsItem/itemControlHandle.h index d5e77f8..9bb5659 100644 --- a/include/graphicsItem/itemControlHandle.h +++ b/include/graphicsItem/itemControlHandle.h @@ -5,7 +5,7 @@ enum HandleType { - T_resize, //调整大小 + T_resize = 50, //调整大小 T_rotate, //旋转 T_editShape //编辑形状 }; diff --git a/include/mainwindow.h b/include/mainwindow.h index fd11abb..7fa2c3d 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -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); diff --git a/source/document.cpp b/source/document.cpp index c87f024..7947256 100644 --- a/source/document.cpp +++ b/source/document.cpp @@ -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 错误返回非 null,isValidItem() 也能过滤掉(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(item->pen().style())); - obj.insert("pen", penObj); + // 样式属性(只用于非组项,GraphicsItemGroup 没有 pen/brush) + GraphicsItemGroup* group = qgraphicsitem_cast(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(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(item); if (group) { QJsonArray childrenArray; QList children = group->childItems(); for (QGraphicsItem* child : children) { - // QGraphicsItem 没有 QObject 接口,所以不能用 qobject_cast - // 使用 static_cast 并检查 nullptr - GraphicsBaseItem* childBase = static_cast(child); + // 使用 qgraphicsitem_cast 安全转换,只序列化 GraphicsBaseItem 子类的项 + GraphicsBaseItem* childBase = qgraphicsitem_cast(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(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(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(parent); - if (group) { - group->addItems(QList() << item); + GraphicsItemGroup* parentGroup = qgraphicsitem_cast(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(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(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; } diff --git a/source/mainwindow.cpp b/source/mainwindow.cpp index 55d0663..e7b1f5c 100644 --- a/source/mainwindow.cpp +++ b/source/mainwindow.cpp @@ -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 信号处理 // ================================================================= diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index 4f1863a..cbf37f3 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -49,6 +49,7 @@ + @@ -105,6 +106,20 @@ QAction::MenuRole::NoRole + + + + + + 另存为 + + + 另存为 (A) + + + QAction::MenuRole::NoRole + +