Add Document class for file persistence

- Add Document class (include/document.h, source/document.cpp)
  - JSON serialization/deserialization of DesignerScene
  - State management: filename, modified flag, timestamps, metadata
  - File operations: New, Open, Save with .bay extension
- Integrate Document into CMainWindow
  - initializeDocument() to create and associate with DesignerScene
  - File menu actions connected to Document methods
  - Window title updates on modified/filename changes
  - Close event checks for unsaved changes
- Update CLAUDE.md with Document architecture documentation
This commit is contained in:
Jesse Qu 2026-05-13 16:24:21 +08:00
parent ff67c48a86
commit 78f2edf24b
8 changed files with 1424 additions and 2 deletions

506
CLAUDE.md
View File

@ -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<QGraphicsItem>) │
│ ├── GraphicsRectItem (rectangle, 圆角矩形) │
│ ├── GraphicPolygonItem (多点绘制,顶点编辑) │
│ ├── GraphicsBusSectionItem (总线段) │
│ └── GraphicsItemGroup (AbstractShapeType<QGroup>) │
└─────────────────────────────────────────────────────────────┘
```
### 坐标系与变换系统
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 <typename BaseType = QGraphicsItem>
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<ItemControlHandle*> m_vecHanle;
QGraphicsPathItem* m_pOperationCopy;
QPointF m_movingIniPos;
};
// 具体类型定义
typedef AbstractShapeType<QGraphicsItem> AbstractShape; // 单图元基类
// GraphicsBaseItem : QObject, AbstractShape
// GraphicsItemGroup : QObject, AbstractShapeType<QGraphicsItemGroup> // 组图元基类
```
**关键点:**
- `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<GraphicsItemGroup*>(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<QString, QVariant>`)
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<T*>(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 | 编辑多边形顶点等 |

View File

@ -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

217
include/document.h Normal file
View File

@ -0,0 +1,217 @@
#ifndef DOCUMENT_H
#define DOCUMENT_H
#include <QString>
#include <QDateTime>
#include <QMap>
#include <QVariant>
#include <QObject>
#include <QByteArray>
/**
* @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<QString, QVariant> m_metaData; // 自定义元数据
DesignerScene* m_pScene; // 关联的场景(组合关系,非拥有所有权)
};
#endif // DOCUMENT_H

View File

@ -5,6 +5,7 @@
class GraphicPolygonItem : public GraphicsBaseItem
{
Q_OBJECT
public:
GraphicPolygonItem(QGraphicsItem *parent = 0);
virtual ~GraphicPolygonItem();

View File

@ -5,6 +5,7 @@
class GraphicsRectItem : public GraphicsBaseItem
{
Q_OBJECT
public:
GraphicsRectItem(const QRect &rect, bool isRound = false, QGraphicsItem *parent = 0);
virtual ~GraphicsRectItem();

View File

@ -4,6 +4,7 @@
#include <QMainWindow>
#include <QComboBox>
#include <QWidgetAction>
#include <QFileInfo>
#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;

504
source/document.cpp Normal file
View File

@ -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 <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QFile>
#include <QDataStream>
#include <QSet>
#include <QJsonArray>
#include <QJsonDocument>
#include <QFile>
#include <QDataStream>
// 文档格式版本号
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<QGraphicsItem*> items = m_pScene->items();
for (QGraphicsItem* item : items) {
// 跳过 handle 等辅助元素
// QGraphicsItem 不是 QObject所以不能用 qobject_cast
// GraphicsBaseItem 继承 QObject 和 AbstractShape (QGraphicsItem),可以用 qgraphicsitem_cast
GraphicsBaseItem* baseItem = qgraphicsitem_cast<GraphicsBaseItem*>(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<quintptr>(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<int>(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<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);
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<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);
}
} 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<bool>()) {
return value.toBool();
} else if (value.typeId() == qMetaTypeId<int>()) {
return value.toInt();
} else if (value.typeId() == qMetaTypeId<double>()) {
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<QVariant> list;
for (const QJsonValue& v : value.toArray()) {
list.append(fromJsonValue(v));
}
return list;
} else if (value.isObject()) {
QMap<QString, QVariant> 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<GraphicsRectItem*>(item)) {
return "GraphicsRectItem";
} else if (qobject_cast<GraphicsBusSectionItem*>(item)) {
return "GraphicsBusSectionItem";
} else if (qobject_cast<GraphicsItemGroup*>(item)) {
return "GraphicsItemGroup";
}
// GraphicsPolygonItem 没有 Q_OBJECT 宏,暂时不支持序列化
return "GraphicsBaseItem";
}
QJsonObject Document::serializeItemProperties(GraphicsBaseItem* item) const
{
QJsonObject props;
// 类型特定的属性序列化
GraphicsRectItem* rectItem = qobject_cast<GraphicsRectItem*>(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);
}

View File

@ -9,6 +9,9 @@
#include <QUndoStack>
#include <QGraphicsScene>
#include <QGraphicsItem>
#include <QFileDialog>
#include <QMessageBox>
#include <QFileInfo>
#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<GraphicsBaseItem*>(selectedItems.first());
m_pPropertiesEditorView->setObject(static_cast<QObject*>(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);
}