本文档描述的是当前代码库的真实实现,而不是最初的空架构设想。 目标是帮助你快速理解项目如何启动、数据放在哪里、界面如何同步,以及主要功能从哪里进。
VectorGenEditor 是一个基于 C++17 + Qt 5.15.2 Widgets 的参数化矢量图形生成与编辑器。
当前实现采用的是 Model-View-Command 风格的 MVC 变体:
flowchart TD
A[用户操作] --> B[View: CanvasView / MainWindow]
B --> C[Controller: SelectionController / DocumentController]
C --> D[Command: QUndoCommand 子类]
D --> E[Model: Document / Layer / Shape]
E --> F[signals]
F --> B
核心原则只有一句话:Document 是唯一数据源。QGraphicsScene 负责显示和交互,但不保存业务真相。
程序入口在 src/main.cpp。启动顺序大致如下:
- 创建
Application - 创建
Document - 创建
CanvasScene - 创建
DocumentController - 把
Document和Controller注入到CanvasScene - 进入主事件循环
这意味着项目的“中枢”不是某个全局单例,而是 Document + DocumentController + CanvasScene 这组三件套。
模型层负责保存所有真实数据。
- Document 管理图层、画板范围和文档级序列化
- Layer 管理图层名称、可见性和图形顺序
- Shape 是所有图形的基类
- RectShape、EllipseShape、LineShape、PolylineShape、PolygonShape、StarShape、ArrowShape、TextShape 是具体图形实现
- Geometry 和 Workspace 提供几何计算、边界判断、工作区约束
- Artboard 代表白色画板的共享矩形范围
- ShapeFactory 负责按类型创建形状
视图层负责画布显示、工具栏、属性面板和交互反馈。
- MainWindow 负责菜单、工具栏、状态栏和整体布局
- CanvasView 负责缩放、平移、框选、线段预览、文本落点等交互
- CanvasScene 负责把模型同步成可视化 item
- ShapeGraphicsItem 表示单个图形在场景中的显示对象
- HandleItem 负责缩放/旋转手柄
- PropertyPanel、GenerationPanel、LayerPanel 负责右侧属性区
- ToolPalette 负责左侧工具选择
- UiStyles 负责统一样式
控制器层负责把用户行为翻译成模型变更。
- DocumentController 是最重要的业务控制器,负责创建、删除、复制、粘贴、移动、修改样式等操作
- SelectionController 负责选中状态、多选、框选、手柄管理和拖拽同步
命令层负责撤销 / 重做。
当前已有的命令包括:
AddShapeCommandRemoveShapeCommandMoveShapeCommandMoveShapesCommandResizeShapeCommandRotateShapeCommandModifyShapeCommandChangeStyleCommandChangeTextCommand
这些命令配合 QUndoStack 使用,保证图形编辑操作可回退。
序列化层负责项目文件和 SVG 导出。
- JsonSerializer 负责项目数据保存 / 读取
- SvgExporter 负责结构化 SVG 导出
- Application 封装 Qt 应用初始化和主入口运行
Document 是整份文档的真实数据容器。
它负责:
- 管理多个图层
- 管理当前图层索引
- 管理画板 / 工作区矩形
- 提供按 id 查找图形、增删图形、移动层级等接口
- 发出
documentChanged、shapeAdded、shapeRemoved、shapeChanged、layerChanged等信号
Layer 是图形的组织单元。
它负责:
- 图层名称
- 图层可见性
- 图形列表和 z-order
- 图层级序列化
Shape 是所有图形的抽象基类。
它包含两类核心信息:
Transform:位置、宽高、旋转Style:填充色、描边色、线宽、可见性、线型
不同图形通过继承 Shape 实现自己的几何和序列化逻辑。
选择逻辑主要由 CanvasView + SelectionController 共同完成。
常见流程是:
- 用户在
CanvasView中单击或拖拽 - 视图判断当前工具是选择、直线还是文本
- 如果是选择工具,则把命中结果交给
SelectionController SelectionController维护选中集合并更新手柄- 场景 item 的选中状态和模型选择状态保持同步
图形创建统一由 DocumentController 进入。
例如:
createRectcreateEllipsecreateTrianglecreateRegularPolygoncreatePolylinecreateLinecreateTextcreateStarcreateArrow
这些接口最终会通过命令修改 Document,而不是直接改视图。
属性面板修改内容后,不应该直接改 QGraphicsItem 作为最终真相,而应由控制器生成对应命令,例如:
- 位置变化:
updateShapePosition/updateShapesPosition - 几何变化:
updateShapeGeometry - 旋转变化:
updateShapeRotation - 样式变化:
updateShapeStyle - 文本变化:
updateShapeText
这样撤销 / 重做才是完整的。
当前项目已经把“无限画布”和“白色画板”拆开了,但它们有明确关系:
- 灰色区域是可滚动 / 可扩展的场景背景
- 白色区域是画板(Artboard / Page)
Document::workspaceRect()保存画板范围Artboard::rect()作为共享画板矩形的入口Workspace负责页面边界、场景边界和裁剪判断
这套设计的好处是:
- 画布仍然可以扩展
- 导出时可以明确以画板为边界
- 图形放在画板外不会破坏编辑,但可以按规则决定是否导出
SvgExporter 当前采用结构化导出,不依赖 scene->render()。
导出策略可以概括为:
- 以文档的画板矩形作为
viewBox - 每个图形导出为独立 SVG 元素
- 仅导出可见图层和可见图形
- 使用
clipPath限制画板外内容 - 对与画板不相交的对象直接跳过
这意味着导出的 SVG 更接近“可编辑矢量源文件”,而不是一张截图。
如果你是第一次看这个项目,建议按下面顺序学:
- 先看 src/main.cpp,理解启动链路
- 再看 src/model/Document.h,理解数据放哪
- 再看 src/model/Shape.h 和几个具体 Shape,理解图形如何表达
- 再看 src/controller/DocumentController.h,理解“操作怎么变成命令”
- 再看 src/controller/SelectionController.h,理解多选、手柄和拖拽
- 再看 src/view/CanvasView.h 与 src/view/CanvasScene.h,理解鼠标事件如何进到系统里
- 最后看 src/serialization/SvgExporter.h,理解导出逻辑
这个项目当前最重要的设计选择有三个:
- 数据和视图分离,不要把业务数据绑死在
QGraphicsItem - 所有可撤销操作都走命令,这样编辑器才像编辑器
- SVG 导出要结构化,不要把最终结果当截图处理
如果后续继续扩展,最值得保持的也是这三条。