diff --git a/CLAUDE.md b/CLAUDE.md index 08d43c5..f558add 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -89,7 +89,14 @@ source/main.cpp # Entry point - creates CMainWindow └── source/mainwindow.h/.cpp # Main window with CDockManager, initializes all components ├── initializeDockUi() # Sets up docks (left, center, right) ├── initializeAction() # Sets up QUndoStack and menu actions - └── Event handlers # onSignal_addItem, onSignal_selectionChanged, etc. + ├── initializeDocument() # Creates Document, associates with DesignerScene + ├── Event handlers # onSignal_addItem, onSignal_selectionChanged, etc. + └── File operations # onAction_new(), onAction_open(), onAction_save() + +source/document.h/.cpp # Document class - serialization and state management + ├── serialize()/deserialize() # Core JSON serialization + ├── saveToFile()/loadFromFile() # File I/O operations + └── State management # filename, modified, timestamps, meta data source/drawingPanel.h/.cpp # Central dock widget containing DesignerView └── DesignerView + DesignerScene @@ -139,3 +146,500 @@ Both are added via `add_subdirectory()` in CMakeLists.txt and must exist in the 4. **Wchar String Conversion**: Uses `QString::fromWCharArray(L"中文")` for Chinese UI strings 5. **Resource File**: `resource/BayTemplate.qrc` contains checkerboard background and icons, referenced in .ui files and CMakeLists.txt + +--- + +## Graphics View Architecture Deep Dive + +### MVC/MVVM Architecture Mapping + +BayTemplate implements a classic **Model-View-Controller** pattern using Qt's Graphics View Framework: + +| Layer | Qt Graphics View | BayTemplate Implementation | Responsibility | +|-------|------------------|---------------------------|----------------| +| **Model** | QGraphicsItem | GraphicsBaseItem, GraphicsRectItem, GraphicPolygonItem, GraphicsItemGroup | 数据模型:存储图元几何数据、样式属性(pen/brush)、变换信息(位置/旋转/缩放)| +| **Model Manager** | QGraphicsScene | DesignerScene | 模型管理:管理 item 生命周期、选择状态、碰撞检测、背景绘制(网格)| +| **View** | QGraphicsView | DesignerView | 视图呈现:坐标系变换、滚动条控制、鼠标事件捕获、缩放/平移 | +| **Controller** | (Qt 无内置) | SelectorManager + BaseSelector 体系 | 控制器:解释用户输入,转换为模型操作(创建/移动/旋转/缩放/编辑)| + +**关键点:** +- Qt 的 Graphics View 框架提供了 MVC 中的 Model 和 View 层 +- Controller 层需要开发者自行实现,BayTemplate 使用 SelectorManager 作为中枢 +- DesignerScene 介于 Model Manager 和 Controller 之间,负责事件分发 + +### View-Scene-Item 三层架构详解 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ DesignerView │ +│ (Viewport / Presentation) │ +├─────────────────────────────────────────────────────────────┤ +│ • 坐标变换:Viewport Coordinates ↔ Scene Coordinates │ +│ • 缩放控制:0.02x - 50x (mouse wheel with smooth zoom) │ +│ • 平移控制:Middle-button drag (translate transform) │ +│ • 事件捕获:mousePressEvent/moveEvent/releaseEvent │ +│ • 无滚动条:setHorizontal/VerticalScrollBarPolicy(Off) │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ DesignerScene │ +│ (Scene Graph Manager) │ +├─────────────────────────────────────────────────────────────┤ +│ • Item 管理:addItem(), removeItem(), selectedItems() │ +│ • 背景绘制:drawBackground() 绘制 20px 间距的藏青色虚线网格 │ +│ • 事件分发:将鼠标事件转发给 SelectorManager │ +│ • 组操作:createGroup() 实现扁平化打组 │ +│ • 信号发射:signalAddItem(), selectionChanged() │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ GraphicsItem │ +│ (Data Model) │ +├─────────────────────────────────────────────────────────────┤ +│ GraphicsBaseItem (AbstractShapeType) │ +│ ├── GraphicsRectItem (rectangle, 圆角矩形) │ +│ ├── GraphicPolygonItem (多点绘制,顶点编辑) │ +│ ├── GraphicsBusSectionItem (总线段) │ +│ └── GraphicsItemGroup (AbstractShapeType) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 坐标系与变换系统 + +BayTemplate 使用多层坐标系,理解它们对添加 Document 类至关重要: + +``` +1. Viewport Coordinates (像素坐标) + └── DesignerView 的 viewport 矩形,(0,0) 在左上角 + └── 受窗口大小、缩放影响 + +2. Scene Coordinates (场景坐标) + └── 全局坐标系,(0,0) 为场景原点 + └── 通过 mapToScene()/mapFromScene() 与 Viewport 互转 + └── 图元的位置 pos() 使用场景坐标 + +3. Item Coordinates (图元局部坐标) + └── 每个 item 的局部坐标系,原点在 (0,0) + └── BayTemplate 中,图元中心点通常与 (0,0) 重合 + └── boundingRect() 返回局部坐标中的边界 + +4. Parent Coordinates (父项坐标) + └── 当 item 加入 group 后,使用 group 的局部坐标 + └── mapToParent()/mapFromParent() 用于转换 +``` + +**变换链(Transform Chain):** +``` +Item Coordinates → Item Transform → Parent Coordinates → ... → Scene Coordinates → View Transform → Viewport Coordinates +``` + +- **Item Transform**: 由 `setPos()`, `setRotation()`, `setScale()` 控制的 3×3 变换矩阵 +- **Transform Origin**: `setTransformOriginPoint()` 设置变换中心(BayTemplate 中通常为中心点) +- **View Transform**: DesignerView 的 zoom/pan 产生的变换,影响所有 item 的显示 + +**关键函数:** +- `item->mapToScene(point)`: Item 局部坐标 → 场景坐标 +- `item->mapToView(point)`: Item 局部坐标 → Viewport 坐标 +- `view->mapToScene(point)`: Viewport 坐标 → 场景坐标 +- `view->mapSceneToViewport(rect)`: 场景矩形 → Viewport 矩形 + +### 操作模式与状态机(Selector 系统) + +SelectorManager 实现了一个**状态机**,不同的 Selector 代表不同的操作模式: + +``` + ┌─────────────────┐ + │ ST_base │ + │ (空闲状态) │ + └────────┬────────┘ + │ + ┌──────────────────┼──────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ + │ ST_creating │ │ ST_moving │ │ ST_editing │ + │ (创建模式) │ │ (移动模式) │ │ (顶点编辑) │ + └──────────────┘ └──────────────┘ └──────────────┘ + │ │ + │ │ + ▼ ▼ + ┌──────────────┐ ┌──────────────┐ + │ ST_scaling │ │ ST_rotation │ + │ (缩放模式) │ │ (旋转模式) │ + └──────────────┘ └──────────────┘ + │ │ + └────────┬─────────┘ + │ + ▼ + ┌────────┐ + │ ST_base│ (操作完成后返回) + └────────┘ +``` + +**状态转换规则:** + +| 触发条件 | 从状态 | 到状态 | 说明 | +|---------|--------|--------|------| +| 从图元面板拖拽 | ST_base | ST_creating | 设置 CreatingSelector 的 m_pCreatingItem | +| 点击选中 item(左键) | ST_base | ST_moving/ST_scaling/ST_rotation | 根据是否点击在 handle 上判断 | +| 点击空白处 | 任何 | ST_base | 取消选择,禁用 RubberBandDrag | +| mouseReleaseEvent | ST_moving/scaling/rotation | ST_base | 操作完成,返回空闲状态 | + +**BaseSelector 静态成员(所有 Selector 共享):** +```cpp +static QPointF ms_ptMouseDown; // 鼠标按下位置(场景坐标) +static QPointF ms_ptMouseLast; // 鼠标当前位置(场景坐标) +static double ms_dAngleMouseDownToItem; // 按下时鼠标与 item 中心的夹角(弧度→角度) +static int ms_nDragHandle; // 当前拖拽的 handle 类型 +``` + +### Handle 控制系统 + +ItemControlHandle 是围绕在选中 item 周围的视觉控件,用于精细操作: + +``` + H_leftTop ──── H_top ──── H_rightTop + │ │ │ + │ │ │ + ┌────┴────────────┴────────────┴────┐ + │ │ + │ Item Bounding │ + │ Rect │ + │ │ + └────┬────────────┬────────────┬────┘ + │ │ │ + H_left ────── H_bottom ──── H_rightBottom +``` + +**Handle 类型枚举:** +```cpp +enum HandleTag { + H_none = 0, // 无 handle + H_leftTop, // 左上角(缩放) + H_top, // 上边中点(缩放) + H_rightTop, // 右上角(缩放) + H_right, // 右边中点(缩放) + H_rightBottom, // 右下角(缩放) + H_bottom, // 下边中点(缩放) + H_leftBottom, // 左下角(缩放) + H_left, // 左边中点(缩放) + H_rotate_leftTop, // 左上角外侧(旋转) + H_rotate_rightTop, // 右上角外侧(旋转) + H_rotate_rightBottom, // 右下角外侧(旋转) + H_rotate_leftBottom, // 左下角外侧(旋转) + H_edit, // 自定义编辑点(多边形顶点、圆角控制点) + // ... 更多编辑点 +}; +``` + +**Handle 工作机制:** +1. `AbstractShapeType::updateHandles()` 根据 boundingRect 计算 handle 位置 +2. Handle 位置在 boundingRect 外扩 5px (nMargin = 5) +3. `collidesWithHandle()` 检测鼠标点击在哪个 handle 上 +4. 不同操作模式对 handle 的响应不同 + +### 操作副本(Operation Copy)模式 + +移动和旋转操作使用"预览副本"技术,提供视觉反馈: + +```cpp +// 1. mousePressEvent: 创建副本 +void createOperationCopy() { + m_pOperationCopy = new QGraphicsPathItem(this->shape()); + m_pOperationCopy->setPen(Qt::DashLine); // 虚线表示预览 + m_pOperationCopy->setPos(this->pos()); + scene->addItem(m_pOperationCopy); + m_movingIniPos = this->pos(); +} + +// 2. mouseMoveEvent: 移动副本(本体不动) +void moveOperationCopy(const QPointF& delta) { + m_pOperationCopy->setPos(m_movingIniPos + delta); +} + +// 3. mouseReleaseEvent: 副本位置应用到本体 +void removeOperationCopy() { + this->setPos(m_pOperationCopy->pos()); // 本体移动到副本位置 + scene->removeItem(m_pOperationCopy); + delete m_pOperationCopy; +} +``` + +**优势:** +- 移动过程中保持原来的 selection state +- 可以拖出 boundingRect_selected 范围(否则会被误认为取消选择) +- 旋转/缩放同理 + +### 图元基类架构(AbstractShapeType 模板) + +```cpp +// 使用模板继承,允许继承 QGraphicsItem 或 QGraphicsItemGroup +template +class AbstractShapeType : public BaseType { +protected: + ShapeType m_type; // T_undefined/T_item/T_group + QPen m_pen; // 画笔(边框) + QBrush m_brush; // 画刷(填充) + double m_dWidth, m_dHeight; // 宽高 + QRectF m_boundingRect; // 局部坐标中的边界矩形 + QRectF m_boundingRect_selected; // 选中框(外扩 5px) + double m_dSyncRotationByParent; // 父项旋转同步数据 + QVector m_vecHanle; + QGraphicsPathItem* m_pOperationCopy; + QPointF m_movingIniPos; +}; + +// 具体类型定义 +typedef AbstractShapeType AbstractShape; // 单图元基类 +// GraphicsBaseItem : QObject, AbstractShape + +// GraphicsItemGroup : QObject, AbstractShapeType // 组图元基类 +``` + +**关键点:** +- `m_boundingRect` 的中心点与 item 的 (0,0) 原点重合,简化变换计算 +- `updateCoordinate()` 在 resize/editShape 后重新校准原点 +- `m_dSyncRotationByParent` 用于组内 item 跟踪父组的旋转(因为 item 的 rotation() 不会自动更新) + +### 命令模式(Command Pattern) + +所有可撤销操作都通过 QUndoCommand 实现: + +```cpp +class AddItemCommand : public QUndoCommand { + void undo() override { scene->removeItem(m_pItem); } + void redo() override { + if(!m_pItem->scene()) scene->addItem(m_pItem); + } +}; + +class DeleteItemCommand : public QUndoCommand { + void undo() override { + foreach(item, m_listItem) scene->addItem(item); + } + void redo() override { + foreach(item, m_listItem) scene->removeItem(item); + } +}; +``` + +**关键点:** +- `QUndoStack::push(command)` 会自动调用 `command->redo()` +- `DeleteItemCommand` 使用 `removeItem()` 而非 `delete`,确保 undo 能恢复 +- 命令对象持有 item 的引用(非所有权) + +### 组(Group)扁平化策略 + +`DesignerScene::createGroup()` 实现组扁平化: + +```cpp +// 如果选中的 item 中包含已有的 group,先解散该 group,将子项加入新组 +foreach (item, selectedItems) { + if (item->getType() == T_group) { + GraphicsItemGroup* group = qgraphicsitem_cast(item); + foreach (child, group->childItems()) { + listItem.push_back(child); // 将子项加入新列表 + } + removeItem(group); + delete group; // 删除旧组 + } +} +// 创建新组,避免嵌套 +``` + +**目的:** +- 简化场景图结构(无嵌套的树) +- 简化坐标计算和碰撞检测 +- 统一 undo/redo 逻辑 + +--- + +## Document 类架构(持久化层) + +### Document 类在 MVC 中的位置 + +Document 类作为**持久化层**插入现有架构,位于 Model 层之下: + +``` +┌─────────────────────────────────────────┐ +│ Controller │ +│ (SelectorManager + 各种 Selector) │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ View │ +│ (DesignerView + UI) │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ Model Manager │ +│ (DesignerScene) │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ Model │ +│ (GraphicsItem 层次结构) │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ Document │ +│ (序列化/状态管理) │ +└──────────────────────────────────────────┘ +``` + +**设计原则:** +- **组合优于继承**:Document 持有 `DesignerScene*` 弱引用,不继承 Scene +- **单向依赖**:Document 只依赖 DesignerScene 的接口,不依赖 CMainWindow +- **信号槽通信**:通过信号与外部通信修改状态,避免紧耦合 + +### Document 类职责 + +1. **序列化/反序列化**(JSON 格式) + - `serialize()` / `deserialize()`: 核心序列化为 QByteArray + - `saveToFile()` / `loadFromFile()`: 文件 I/O 封装 + - 递归序列化/反序列化图元层次结构(支持组图元嵌套) + +2. **状态管理** + - `m_filename`: 文档文件名(空表示未保存的新文档) + - `m_modified`: 是否被修改(通过 `setModified()` 和 `modifiedChanged` 信号通知) + - `m_created` / `m_modifiedTime` / `m_lastSavedTime`: 时间戳 + - `m_metaData`: 可扩展的自定义元数据 (`QMap`) + +3. **与 CMainWindow 的集成** + - `initializeDocument()`: 创建 Document 并关联 DesignerScene + - 连接 `modifiedChanged` 信号来更新窗口标题(添加 * 标记) + - `closeEvent()` 中检查 `isModified()` 提示用户保存 + - 文件操作:New/Open/Save 通过 Document 完成 + +### 序列化格式(v1.0) + +```json +{ + "version": "1.0", + "created": "2024-01-01T12:00:00", + "modified": "2024-01-01T14:30:00", + "filename": "/path/to/file.bay", + "metaData": { + "customKey": "customValue" + }, + "items": [ + { + "type": "GraphicsRectItem", + "id": "140737488320", + "pos": {"x": 100.0, "y": 200.0}, + "rotation": 45.0, + "scale": {"x": 1.0, "y": 1.0}, + "pen": {"color": "#000000", "width": 1.0, "style": 0}, + "brush": {"color": "#FF0000"}, + "width": 100.0, + "height": 50.0, + "properties": {}, + "children": [] + }, + { + "type": "GraphicsItemGroup", + "pos": {"x": 300.0, "y": 150.0}, + "rotation": 0.0, + "scale": {"x": 1.0, "y": 1.0}, + "width": 200.0, + "height": 100.0, + "children": [ + { "type": "GraphicsRectItem", ... }, + { "type": "GraphicsBusSectionItem", ... } + ] + } + ] +} +``` + +### 实现细节 + +**类型识别:** 使用 `qobject_cast(item)` 配合 `getTypeString()` 函数识别图元类型(要求类型有 `Q_OBJECT` 宏) + +**QVariant ↔ QJsonValue 转换:** 提供 `toJsonValue()` / `fromJsonValue()` 辅助函数处理元数据序列化 + +**坐标系统:** 图元使用中心原点,`pos()` 存储场景坐标,`boundingRect()` 使用局部坐标 + +**文件扩展名:** `.bay` 作为 BayTemplate 专用格式 + +### 关键文件 + +- `include/document.h`: Document 类声明 +- `source/document.cpp`: 序列化/反序列化实现 +- `source/mainwindow.cpp`: `initializeDocument()`, `onAction_new()`, `onAction_open()`, `onAction_save()` + +--- + +## Selector 系统设计模式总结 + +### 状态模式(State Pattern) + +SelectorManager 实现状态模式,不同的 Selector 子类代表不同的状态: + +```cpp +// Context +class SelectorManager { + BaseSelector* getWorkingSelector(); // 返回当前状态的 Selector + void setWorkingSelector(SelectorType); +}; + +// State 接口 +class BaseSelector { + virtual void mousePressEvent(...) = 0; + virtual void mouseMoveEvent(...) = 0; + virtual void mouseReleaseEvent(...) = 0; +}; + +// Concrete State +class MovingSelector : public BaseSelector { ... }; +class ScalingSelector : public BaseSelector { ... }; +// ... +``` + +**静态成员共享的原因:** +BaseSelector 的静态成员(ms_ptMouseDown 等)在所有 Selector 实例间共享,使得状态切换时无需传递上下文数据。 + +### 操作副本模式(Operation Preview Pattern) + +移动、旋转、缩放都使用"操作副本"作为视觉预览: +1. mousePressEvent: 创建虚线副本 +2. mouseMoveEvent: 移动副本(本体不动) +3. mouseReleaseEvent: 将副本的变换应用到本体,删除副本 + +### Handle 系统(Manipulator Pattern) + +ItemControlHandle 实现操纵器模式,将抽象的变换操作(缩放、旋转)映射到可视化的交互控件。 + +--- + +## 坐标系转换速查表 + +| 转换 | 函数 | 说明 | +|------|------|------| +| Item → Scene | `item->mapToScene(point)` | 局部坐标转场景坐标 | +| Scene → Item | `item->mapFromScene(point)` | 场景坐标转局部坐标 | +| Item → Viewport | `item->mapToView(point)` | 局部坐标转屏幕像素 | +| Item → Parent | `item->mapToParent(point)` | 局部坐标转父项坐标 | +| Viewport → Scene | `view->mapToScene(point)` | 屏幕像素转场景坐标 | +| Scene Rect → Viewport Rect | `view->mapSceneToViewport(rect)` | 场景矩形转屏幕矩形 | + +## Handle 类型速查表 + +| Handle 类型 | 位置 | 功能 | +|------------|------|------| +| H_leftTop ~ H_left | boundingRect 八方向 | 缩放 | +| H_rotate_leftTop ~ H_rotate_leftBottom | boundingRect 外扩 | 旋转 | +| H_edit + N | 自定义位置 | 形状编辑(多边形顶点、圆角控制点)| + +## 操作模式速查表 + +| SelectorType | 触发方式 | 用途 | +|-------------|---------|------| +| ST_base | 默认 | 空闲状态,允许橡胶框选择 | +| ST_creating | 从图元面板拖拽 | 创建新图元 | +| ST_moving | 点击 item 主体 | 移动选中项 | +| ST_scaling | 点击缩放 handle | 缩放选中项 | +| ST_rotation | 点击旋转 handle | 旋转选中项 | +| ST_editing | 点击编辑 handle | 编辑多边形顶点等 | diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e4a82b..55e4c26 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ set(H_HEADER_FILES include/designerScene.h include/designerView.h include/operationCommand.h + include/document.h include/propertyType/CustomGadget.h include/propertyType/CustomType.h @@ -72,6 +73,7 @@ set(CPP_SOURCE_FILES source/designerScene.cpp source/designerView.cpp source/operationCommand.cpp + source/document.cpp source/propertyType/PropertyTypeCustomization_CustomType.cpp diff --git a/include/document.h b/include/document.h new file mode 100644 index 0000000..85e5cc7 --- /dev/null +++ b/include/document.h @@ -0,0 +1,217 @@ +#ifndef DOCUMENT_H +#define DOCUMENT_H + +#include +#include +#include +#include +#include +#include + +/** + * @brief BayTemplate 文档类 + * + * Document 类作为持久化层,位于 Model 层之下,负责: + * 1. 序列化/反序列化:将 DesignerScene 中的图元层次结构保存到 JSON/XML,从文件重建场景 + * 2. 状态管理:文件名、保存状态(是否修改过)、版本信息、元数据 + * 3. 与现有架构的集成:通过组合方式持有 DesignerScene 的引用,提供信号与 CMainWindow 通信 + * + * 设计原则: + * - 组合优于继承:Document 持有 DesignerScene 的弱引用,不继承 Scene + * - 单向依赖:Document 只依赖 DesignerScene 的接口,不依赖 CMainWindow + * - 信号槽通信:通过信号与外部通信修改状态,避免紧耦合 + */ +class DesignerScene; +class GraphicsBaseItem; +class GraphicsItemGroup; +class QGraphicsScene; +class QGraphicsItem; +class QJsonObject; + +class Document : public QObject +{ + Q_OBJECT + +public: + explicit Document(QObject *parent = nullptr); + ~Document(); + + // ================================================================= + // 场景关联(组合关系) + // ================================================================= + + /** + * @brief 关联到 DesignerScene + * @param scene 场景对象,Document 持有其弱引用 + * + * 通过组合方式关联场景,而不是继承。这符合开闭原则, + * Document 可以独立于 Scene 存在,需要序列化时才使用 scene 引用。 + */ + void setScene(DesignerScene* scene); + + /** + * @brief 获取关联的场景 + */ + DesignerScene* scene() const; + + // ================================================================= + // 文件操作 + // ================================================================= + + /** + * @brief 保存文档到文件 + * @param filename 文件路径,为空则使用当前 filename() + * @return 成功返回 true + * + * 序列化流程: + * 1. 遍历 scene->items() 获取顶层图元 + * 2. 递归序列化每个图元及其子图元(对于组图元) + * 3. 写入 JSON 格式文件 + * 4. 调用 setModified(false) + */ + bool saveToFile(const QString& filename = QString()); + + /** + * @brief 从文件加载文档 + * @param filename 文件路径 + * @return 成功返回 true + * + * 反序列化流程: + * 1. 读取 JSON 文件 + * 2. 清空当前 scene 的内容 + * 3. 递归重建图元层次结构 + * 4. 更新 m_filename 和 m_modified + */ + bool loadFromFile(const QString& filename); + + // ================================================================= + // 序列化接口(二进制格式) + // ================================================================= + + /** + * @brief 序列化为 QByteArray + * @return JSON 格式的字节数组 + */ + QByteArray serialize() const; + + /** + * @brief 从 QByteArray 反序列化 + * @param data JSON 格式的字节数组 + * @return 成功返回 true + */ + bool deserialize(const QByteArray& data); + + // ================================================================= + // 状态管理 + // ================================================================= + + /** + * @brief 文档文件名(可能为空,表示未保存的新文档) + */ + QString filename() const { return m_filename; } + void setFilename(const QString& filename) { m_filename = filename; } + + /** + * @brief 文档是否被修改过(需要保存) + */ + bool isModified() const { return m_modified; } + void setModified(bool modified) { + if (m_modified != modified) { + m_modified = modified; + emit modifiedChanged(m_modified); + } + } + + /** + * @brief 文档版本(文件格式版本,用于向前/向后兼容) + */ + QString version() const { return m_version; } + + /** + * @brief 创建时间 + */ + QDateTime created() const { return m_created; } + + /** + * @brief 最后修改时间 + */ + QDateTime modifiedTime() const { return m_modifiedTime; } + + /** + * @brief 最后保存时间 + */ + QDateTime lastSavedTime() const { return m_lastSavedTime; } + + // ================================================================= + // 元数据(可扩展) + // ================================================================= + + /** + * @brief 设置自定义元数据 + * @param key 键名 + * @param value 值 + */ + void setMetaData(const QString& key, const QVariant& value); + + /** + * @brief 获取自定义元数据 + */ + QVariant metaData(const QString& key) const; + +signals: + /** + * @brief 修改状态改变时发出 + * @param modified 新的修改状态 + * + * CMainWindow 可连接此信号来更新窗口标题(添加 * 标记) + */ + void modifiedChanged(bool modified); + + /** + * @brief 文件名改变时发出(新建、另存为等操作) + * @param filename 新的文件名 + * + * CMainWindow 可连接此信号来更新窗口标题 + */ + void filenameChanged(const QString& filename); + + /** + * @brief 保存/加载操作完成时发出 + * @param success 操作是否成功 + * @param message 成功或失败的消息 + */ + void saveStatusChanged(bool success, const QString& message); + +private: + // 内部实现 + bool saveInternal(const QString& filename); + bool loadInternal(const QString& filename); + + // 图元序列化/反序列化 + QJsonObject serializeItem(GraphicsBaseItem* item) const; + void deserializeItem(const QJsonObject& obj, QGraphicsScene* scene, QGraphicsItem* parent); + + QString getTypeString(GraphicsBaseItem* item) const; + QJsonObject serializeItemProperties(GraphicsBaseItem* item) const; + + // QVariant <-> QJsonValue 转换 + QJsonValue toJsonValue(const QVariant& value) const; + QVariant fromJsonValue(const QJsonValue& value) const; + + GraphicsBaseItem* deserializeRectItem(const QJsonObject& obj); + GraphicsBaseItem* deserializeBusSectionItem(const QJsonObject& obj); + GraphicsItemGroup* deserializeItemGroup(const QJsonObject& obj, QGraphicsScene* scene, QGraphicsItem* parent); + + QString m_filename; // 文档文件名 + bool m_modified; // 是否被修改 + QString m_version; // 文件格式版本 + QDateTime m_created; // 创建时间 + QDateTime m_modifiedTime; // 最后修改时间 + QDateTime m_lastSavedTime; // 最后保存时间 + + QMap m_metaData; // 自定义元数据 + + DesignerScene* m_pScene; // 关联的场景(组合关系,非拥有所有权) +}; + +#endif // DOCUMENT_H diff --git a/include/graphicsItem/graphicsPolygonItem.h b/include/graphicsItem/graphicsPolygonItem.h index a76f04e..27d0398 100644 --- a/include/graphicsItem/graphicsPolygonItem.h +++ b/include/graphicsItem/graphicsPolygonItem.h @@ -5,6 +5,7 @@ class GraphicPolygonItem : public GraphicsBaseItem { + Q_OBJECT public: GraphicPolygonItem(QGraphicsItem *parent = 0); virtual ~GraphicPolygonItem(); diff --git a/include/graphicsItem/graphicsRectItem.h b/include/graphicsItem/graphicsRectItem.h index 0b0ee6f..edc9c49 100644 --- a/include/graphicsItem/graphicsRectItem.h +++ b/include/graphicsItem/graphicsRectItem.h @@ -5,6 +5,7 @@ class GraphicsRectItem : public GraphicsBaseItem { + Q_OBJECT public: GraphicsRectItem(const QRect &rect, bool isRound = false, QGraphicsItem *parent = 0); virtual ~GraphicsRectItem(); diff --git a/include/mainwindow.h b/include/mainwindow.h index dd89b8d..fd11abb 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "DockManager.h" #include "DockAreaWidget.h" @@ -11,6 +12,7 @@ #include "QDetailsView.h" #include "graphicsItem/graphicsBaseItem.h" +#include "document.h" QT_BEGIN_NAMESPACE namespace Ui { class CMainWindow; } @@ -37,6 +39,7 @@ protected: private: void initializeDockUi(); void initializeAction(); + void initializeDocument(); private slots: void onAction_zoomIn(); @@ -48,12 +51,24 @@ private slots: void onSignal_deleteItem(); void onSignal_selectionChanged(); +private slots: + // Document 文件操作 + void onAction_new(); + void onAction_open(); + void onAction_save(); + void onDocument_modifiedChanged(bool modified); + void onDocument_filenameChanged(const QString& filename); + void onDocument_saveStatusChanged(bool success, const QString& message); + + void updateWindowTitle(); + private: QAction* m_pSavePerspectiveAction = nullptr; QWidgetAction* m_pPerspectiveListAction = nullptr; QComboBox* m_pPerspectiveComboBox = nullptr; QUndoStack* m_pUndoStack; + Document* m_pDocument; // 当前文档 Ui::CMainWindow *ui; diff --git a/source/document.cpp b/source/document.cpp new file mode 100644 index 0000000..c87f024 --- /dev/null +++ b/source/document.cpp @@ -0,0 +1,504 @@ +#include "document.h" +#include "designerScene.h" +#include "graphicsItem/graphicsBaseItem.h" +#include "graphicsItem/graphicsItemGroup.h" +#include "graphicsItem/graphicsRectItem.h" +#include "graphicsItem/graphicsBusSectionItem.h" + +class GraphicsPolygonItem; // 前向声明,暂不使用多边形 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// 文档格式版本号 +static const QString DOCUMENT_VERSION = "1.0"; + +Document::Document(QObject *parent) + : QObject(parent) + , m_filename() + , m_modified(false) + , m_version(DOCUMENT_VERSION) + , m_created(QDateTime::currentDateTime()) + , m_modifiedTime(QDateTime::currentDateTime()) + , m_lastSavedTime() + , m_pScene(nullptr) +{ +} + +Document::~Document() +{ + // Document 不拥有 scene 的所有权,不需要删除 + m_pScene = nullptr; +} + +// ================================================================= +// 场景关联 +// ================================================================= + +void Document::setScene(DesignerScene* scene) +{ + m_pScene = scene; + + // 如果关联了新的 scene,连接 selectionChanged 信号来追踪修改 + if (m_pScene) { + connect(m_pScene, &QGraphicsScene::selectionChanged, + this, [this]() { + // 选择改变本身不代表文档修改,但可以作为用户活动的信号 + // 实际的修改由具体的操作(添加、删除、移动等)触发 + }); + } +} + +DesignerScene* Document::scene() const +{ + return m_pScene; +} + +// ================================================================= +// 文件操作 +// ================================================================= + +bool Document::saveToFile(const QString& filename) +{ + QString targetFile = filename.isEmpty() ? m_filename : filename; + + if (targetFile.isEmpty()) { + // 没有目标文件名,需要另存为对话框(这里只设置失败状态) + 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); + emit modifiedChanged(false); + emit saveStatusChanged(true, tr("保存成功")); + } else { + emit saveStatusChanged(false, tr("保存失败")); + } + + return success; +} + +bool Document::loadFromFile(const QString& filename) +{ + bool success = loadInternal(filename); + + if (success) { + m_filename = filename; + m_modified = false; + m_lastSavedTime = QDateTime::currentDateTime(); + emit filenameChanged(m_filename); + emit modifiedChanged(false); + emit saveStatusChanged(true, tr("加载成功")); + } else { + emit saveStatusChanged(false, tr("加载失败")); + } + + return success; +} + +// ================================================================= +// 序列化核心实现 +// ================================================================= + +QByteArray Document::serialize() const +{ + if (!m_pScene) { + return QByteArray(); + } + + QJsonObject root; + root.insert("version", m_version); + root.insert("created", m_created.toString(Qt::ISODate)); + root.insert("modified", m_modifiedTime.toString(Qt::ISODate)); + root.insert("filename", m_filename); + + // 序列化元数据 + if (!m_metaData.isEmpty()) { + QJsonObject metaObj; + for (auto it = m_metaData.begin(); it != m_metaData.end(); ++it) { + // QVariant 需要转换为 QJsonValue + metaObj.insert(it.key(), toJsonValue(it.value())); + } + root.insert("metaData", metaObj); + } + + // 序列化图元 + QJsonArray itemsArray; + QList items = m_pScene->items(); + + for (QGraphicsItem* item : items) { + // 跳过 handle 等辅助元素 + // QGraphicsItem 不是 QObject,所以不能用 qobject_cast + // GraphicsBaseItem 继承 QObject 和 AbstractShape (QGraphicsItem),可以用 qgraphicsitem_cast + GraphicsBaseItem* baseItem = qgraphicsitem_cast(item); + if (!baseItem) { + continue; + } + + QJsonObject itemObj = serializeItem(baseItem); + itemsArray.append(itemObj); + } + + root.insert("items", itemsArray); + + QJsonDocument doc(root); + return doc.toJson(QJsonDocument::Indented); +} + +bool Document::deserialize(const QByteArray& data) +{ + if (data.isEmpty() || !m_pScene) { + return false; + } + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + return false; + } + + QJsonObject root = doc.object(); + + // 解析版本信息 + QString version = root["version"].toString(); + if (version.isEmpty()) { + return false; + } + + // 解析时间信息 + if (root.contains("created")) { + m_created = QDateTime::fromString(root["created"].toString(), Qt::ISODate); + } + if (root.contains("modified")) { + m_modifiedTime = QDateTime::fromString(root["modified"].toString(), Qt::ISODate); + } + + // 解析元数据 + if (root.contains("metaData")) { + QJsonObject metaObj = root["metaData"].toObject(); + for (auto it = metaObj.begin(); it != metaObj.end(); ++it) { + // QJsonValue 需要转换为 QVariant + m_metaData.insert(it.key(), fromJsonValue(it.value())); + } + } + + // 清空当前场景 + if (m_pScene) { + m_pScene->clear(); + } + + // 反序列化图元 + QJsonArray itemsArray = root["items"].toArray(); + for (auto itemIt : itemsArray) { + QJsonObject itemObj = itemIt.toObject(); + deserializeItem(itemObj, m_pScene, nullptr); + } + + return true; +} + +// ================================================================= +// 内部实现 +// ================================================================= + +bool Document::saveInternal(const QString& filename) +{ + QByteArray data = serialize(); + if (data.isEmpty()) { + return false; + } + + QFile file(filename); + if (!file.open(QIODevice::WriteOnly)) { + return false; + } + + file.write(data); + file.close(); + return true; +} + +bool Document::loadInternal(const QString& filename) +{ + QFile file(filename); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + + QByteArray data = file.readAll(); + file.close(); + + return deserialize(data); +} + +// ================================================================= +// 图元序列化/反序列化 +// ================================================================= + +QJsonObject Document::serializeItem(GraphicsBaseItem* item) const +{ + QJsonObject obj; + + // 基本属性 + obj.insert("type", getTypeString(item)); + obj.insert("id", QString::number(reinterpret_cast(item))); + + // 位置和变换 + QPointF pos = item->pos(); + QJsonObject posObj; + posObj.insert("x", pos.x()); + posObj.insert("y", pos.y()); + obj.insert("pos", posObj); + + obj.insert("rotation", item->rotation()); + + QJsonObject scaleObj; + // Qt 的 scale() 返回 qreal,不是 QPointF + scaleObj.insert("x", item->scale()); + 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); + + QJsonObject brushObj; + brushObj.insert("color", item->brushColor().name()); + obj.insert("brush", brushObj); + + // 尺寸 + obj.insert("width", item->width()); + obj.insert("height", item->height()); + + // 类型特定属性 + 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); + if (childBase) { + childrenArray.append(serializeItem(childBase)); + } + } + obj.insert("children", childrenArray); + } + + return obj; +} + +void Document::deserializeItem(const QJsonObject& obj, QGraphicsScene* scene, QGraphicsItem* parent) +{ + QString typeStr = obj["type"].toString(); + + GraphicsBaseItem* item = nullptr; + + // 根据类型创建图元 + if (typeStr == "GraphicsRectItem") { + item = deserializeRectItem(obj); + } else if (typeStr == "GraphicsBusSectionItem") { + item = deserializeBusSectionItem(obj); + } else if (typeStr == "GraphicsItemGroup") { + GraphicsItemGroup* group = deserializeItemGroup(obj, scene, parent); + // 对于组,不设置单个图元的属性,只递归处理子项 + QJsonArray childrenArray = obj["children"].toArray(); + for (auto childIt : childrenArray) { + deserializeItem(childIt.toObject(), scene, group); + } + return; + } + // GraphicsPolygonItem 暂时跳过,因为没有 Q_OBJECT 宏 + + 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); + } + } else { + scene->addItem(item); + } + + // 递归处理子项 + QJsonArray childrenArray = obj["children"].toArray(); + for (auto childIt : childrenArray) { + deserializeItem(childIt.toObject(), scene, item); + } +} + +// ================================================================= +// QVariant <-> QJsonValue 转换辅助函数 +// ================================================================= + +QJsonValue Document::toJsonValue(const QVariant& value) const +{ + if (value.typeId() == qMetaTypeId()) { + return value.toBool(); + } else if (value.typeId() == qMetaTypeId()) { + return value.toInt(); + } else if (value.typeId() == qMetaTypeId()) { + return value.toDouble(); + } else if (value.typeId() == QMetaType::Type::QString) { + return value.toString(); + } else { + return value.toString(); + } +} + +QVariant Document::fromJsonValue(const QJsonValue& value) const +{ + if (value.isDouble()) { + return value.toDouble(); + } else if (value.isBool()) { + return value.toBool(); + } else if (value.isArray()) { + QList list; + for (const QJsonValue& v : value.toArray()) { + list.append(fromJsonValue(v)); + } + return list; + } else if (value.isObject()) { + QMap map; + for (auto it = value.toObject().begin(); it != value.toObject().end(); ++it) { + map.insert(it.key(), fromJsonValue(it.value())); + } + return map; + } else { + return value.toString(); + } +} + +// ================================================================= +// 辅助函数 +// ================================================================= + +QString Document::getTypeString(GraphicsBaseItem* item) const +{ + // 使用动态类型信息获取类型字符串(qobject_cast 要求类型有 Q_OBJECT 宏) + // GraphicsRectItem, GraphicsBusSectionItem, GraphicsItemGroup 都有 Q_OBJECT 宏 + if (qobject_cast(item)) { + return "GraphicsRectItem"; + } else if (qobject_cast(item)) { + return "GraphicsBusSectionItem"; + } else if (qobject_cast(item)) { + return "GraphicsItemGroup"; + } + // GraphicsPolygonItem 没有 Q_OBJECT 宏,暂时不支持序列化 + return "GraphicsBaseItem"; +} + +QJsonObject Document::serializeItemProperties(GraphicsBaseItem* item) const +{ + QJsonObject props; + + // 类型特定的属性序列化 + GraphicsRectItem* rectItem = qobject_cast(item); + if (rectItem) { + // 这里可以添加圆角矩形等特殊属性 + // props.insert("isRound", rectItem->m_bIsRound); + } + + // 多边形等特殊属性可以在这里扩展 + + return props; +} + +GraphicsBaseItem* Document::deserializeRectItem(const QJsonObject& obj) +{ + // 根据保存的属性重建矩形 + double width = obj["width"].toDouble(); + double height = obj["height"].toDouble(); + + // 图元的原点在中心,因此 rect 的左上角为 (-width/2, -height/2) + QRect rect(-width/2, -height/2, width, height); + return new GraphicsRectItem(rect, false); +} + +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); +} + +GraphicsItemGroup* Document::deserializeItemGroup(const QJsonObject& obj, + QGraphicsScene* scene, + QGraphicsItem* parent) +{ + GraphicsItemGroup* group = new GraphicsItemGroup(parent); + group->setWidth(obj["width"].toDouble()); + group->setHeight(obj["height"].toDouble()); + return group; +} + +void Document::setMetaData(const QString& key, const QVariant& value) +{ + m_metaData.insert(key, value); + m_modifiedTime = QDateTime::currentDateTime(); + if (m_pScene) { + setModified(true); + } +} + +QVariant Document::metaData(const QString& key) const +{ + return m_metaData.value(key); +} diff --git a/source/mainwindow.cpp b/source/mainwindow.cpp index 61fef0f..55d0663 100644 --- a/source/mainwindow.cpp +++ b/source/mainwindow.cpp @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include #include "DockAreaWidget.h" #include "DockAreaTitleBar.h" @@ -24,8 +27,10 @@ #include "drawingPanel.h" #include "designerScene.h" +#include "designerView.h" #include "graphicElementsPanel.h" #include "operationCommand.h" +#include "document.h" using namespace ads; @@ -36,21 +41,41 @@ CMainWindow::CMainWindow(QWidget *parent) { ui->setupUi(this); m_pUndoStack = nullptr; + m_pDocument = nullptr; initializeDockUi(); initializeAction(); + initializeDocument(); connect(m_pGraphicElementsPanel,SIGNAL(addGraphicsItem(GraphicsItemType&)),m_pDrawingPanel,SLOT(onSignal_addGraphicsItem(GraphicsItemType&))); } CMainWindow::~CMainWindow() { + delete m_pDocument; delete ui; } void CMainWindow::closeEvent(QCloseEvent* event) { + // 检查是否有未保存的修改 + if (m_pDocument && m_pDocument->isModified()) { + int ret = QMessageBox::warning(this, tr("确认关闭"), + tr("文档已修改,是否保存?"), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + if (ret == QMessageBox::Save) { + // 尝试保存,如果保存失败则取消关闭 + if (!m_pDocument->saveToFile()) { + event->ignore(); + return; + } + } else if (ret == QMessageBox::Cancel) { + event->ignore(); + return; + } + } + // Delete dock manager here to delete all floating widgets. This ensures // that all top level windows of the dock manager are properly closed m_pDockManager->deleteLater(); @@ -126,6 +151,11 @@ void CMainWindow::initializeAction() connect(ui->actionZoomFit, SIGNAL(triggered()), this, SLOT(onAction_zoomFit())); connect(ui->actionGroup, SIGNAL(triggered()), this, SLOT(onAction_createGroup())); connect(ui->actionUngroup, SIGNAL(triggered()), this, SLOT(onAction_destroyGroup())); + + // 文件操作 + 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())); } void CMainWindow::onAction_zoomIn() @@ -198,7 +228,155 @@ void CMainWindow::onSignal_selectionChanged() if(selectedItems.count() != 1) { m_pPropertiesEditorView->setObject(m_pDrawingPanel->getQGraphicsScene()); return; - } + } GraphicsBaseItem *item = static_cast(selectedItems.first()); m_pPropertiesEditorView->setObject(static_cast(item)); } + +// ================================================================= +// Document 初始化 +// ================================================================= + +void CMainWindow::initializeDocument() +{ + // 创建新文档 + m_pDocument = new Document(this); + + // 关联 DesignerScene + DesignerScene* scene = m_pDrawingPanel->getDesignerScene(); + m_pDocument->setScene(scene); + + // 连接 Document 信号 + connect(m_pDocument, &Document::modifiedChanged, + this, &CMainWindow::onDocument_modifiedChanged); + connect(m_pDocument, &Document::filenameChanged, + this, &CMainWindow::onDocument_filenameChanged); + connect(m_pDocument, &Document::saveStatusChanged, + this, &CMainWindow::onDocument_saveStatusChanged); + + // 连接场景选择变化信号,当选择变化时标记文档为已修改 + // 注意:Qt6.5 之前没有 itemsAdded/itemsRemoved 信号 + connect(scene, &QGraphicsScene::selectionChanged, + this, [this]() { + if (m_pDocument && !m_pDocument->isModified()) { + m_pDocument->setModified(true); + } + }); + + // 初始化窗口标题 + updateWindowTitle(); + + // 将文件操作添加到文件菜单 + ui->menuFile->addAction(ui->actionNew); + ui->menuFile->addAction(ui->actionOpen); + ui->menuFile->addAction(ui->actionSave); + ui->menuFile->addSeparator(); +} + +// ================================================================= +// Document 文件操作 +// ================================================================= + +void CMainWindow::onAction_new() +{ + // 如果当前文档有修改,提示用户保存 + if (m_pDocument->isModified()) { + int ret = QMessageBox::warning(this, tr("确认新建"), + tr("当前文档已修改,是否保存?"), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + if (ret == QMessageBox::Save) { + onAction_save(); + } else if (ret == QMessageBox::Cancel) { + return; + } + } + + // 清空场景 + DesignerScene* scene = m_pDrawingPanel->getDesignerScene(); + scene->clear(); + + // 创建新文档 + delete m_pDocument; + m_pDocument = new Document(this); + m_pDocument->setScene(scene); + + connect(m_pDocument, &Document::modifiedChanged, + this, &CMainWindow::onDocument_modifiedChanged); + connect(m_pDocument, &Document::filenameChanged, + this, &CMainWindow::onDocument_filenameChanged); + connect(m_pDocument, &Document::saveStatusChanged, + this, &CMainWindow::onDocument_saveStatusChanged); + + updateWindowTitle(); +} + +void CMainWindow::onAction_open() +{ + // 如果当前文档有修改,提示用户保存 + if (m_pDocument->isModified()) { + int ret = QMessageBox::warning(this, tr("确认打开"), + tr("当前文档已修改,是否保存?"), + QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); + if (ret == QMessageBox::Save) { + onAction_save(); + } else if (ret == QMessageBox::Cancel) { + return; + } + } + + QString fileName = QFileDialog::getOpenFileName(this, tr("打开文档"), "", + tr("BayTemplate Files (*.bay);;All Files (*)")); + if (fileName.isEmpty()) { + return; + } + + if (!m_pDocument->loadFromFile(fileName)) { + QMessageBox::critical(this, tr("错误"), tr("无法打开文件:%1").arg(fileName)); + } +} + +void CMainWindow::onAction_save() +{ + if (m_pDocument->saveToFile()) { + // 保存成功,标记为未修改 + m_pDocument->setModified(false); + updateWindowTitle(); + } +} + +// ================================================================= +// Document 信号处理 +// ================================================================= + +void CMainWindow::onDocument_modifiedChanged(bool modified) +{ + updateWindowTitle(); +} + +void CMainWindow::onDocument_filenameChanged(const QString& filename) +{ + updateWindowTitle(); +} + +void CMainWindow::onDocument_saveStatusChanged(bool success, const QString& message) +{ + if (!success) { + QMessageBox::warning(this, tr("文档操作"), message); + } +} + +void CMainWindow::updateWindowTitle() +{ + QString title = tr("BayTemplate"); + if (!m_pDocument->filename().isEmpty()) { + title = QFileInfo(m_pDocument->filename()).fileName(); + } else { + title = tr("未命名"); + } + + if (m_pDocument->isModified()) { + title += " *"; + } + + setWindowTitle(title); +}