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:
parent
ff67c48a86
commit
78f2edf24b
506
CLAUDE.md
506
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<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 | 编辑多边形顶点等 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
class GraphicPolygonItem : public GraphicsBaseItem
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
GraphicPolygonItem(QGraphicsItem *parent = 0);
|
||||
virtual ~GraphicPolygonItem();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
class GraphicsRectItem : public GraphicsBaseItem
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
GraphicsRectItem(const QRect &rect, bool isRound = false, QGraphicsItem *parent = 0);
|
||||
virtual ~GraphicsRectItem();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue