# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview BayTemplate is a **Bay Template Designer of Grid Framework DesignTime** tool - a Qt-based application for designing electrical power grid structures. It features a dockable UI with a graphics canvas for placing and manipulating grid elements. ## Build Commands ### Build the Project 使用 CMake presets 进行配置和构建: ```bash # Configure using preset cmake --preset default # Build with Ninja ninja -C build/Debug ``` ### Output Location - Executable: `build/Debug/BayTemplate.app` (macOS) ## Architecture ### Core Components #### 1. Docking System (Qt Advanced Docking System 4.3.1) - **CDockManager** manages dockable panels - **Left Dock**: 图元面板 (GraphicElementsPanel) - library of draggable grid elements - **Center**: DrawingPanel - main canvas area with DesignerScene - **Right Dock**: 属性编辑器 (PropertyEditor) - QDetailsView for editing selected item properties #### 2. Graphics View Framework - **DesignerView** (`QGraphicsView` subclass): Custom view with zoom (0.02-50x), pan (middle-button drag), checkerboard background - **DesignerScene** (`QGraphicsScene` subclass): Custom scene with grid overlay (20px spacing), routes mouse events through SelectorManager #### 3. Selector System Selector hierarchy in `include/util/`: - **BaseSelector**: Abstract base for all selectors - **CreatingSelector**: Mode for creating new items from template - **MovingSelector**: Mode for moving items - **RotationSelector**: Mode for rotating items around origin - **ScalingSelector**: Mode for scaling items - **EditingSelector**: Mode for editing polygon vertex positions - **SelectorManager**: Singleton that manages the active working selector Mouse events flow: `DesignerScene` → `SelectorManager::getWorkingSelector()` → specific selector implementation #### 4. Graphics Items (`include/graphicsItem/`) - **GraphicsBaseItem**: Abstract base class for all grid elements - **GraphicsRectItem**: Rectangle element - **GraphicsPolygonItem**: Polygon element (editable vertices) - **GraphicsItemGroup**: Container for grouped items (supports flattening to prevent nesting) - **GraphicsBusSectionItem**: Bus section element - **ItemControlHandle**: Visual handles for manipulation (rotation, scaling, editing) #### 5. Property Editor (Qt PropertyEditor) - **QDetailsView**: Qt Quick-based property editor in right dock - **QCustomType**: Custom property type for graphics item properties - **PropertyTypeCustomization_CustomType**: Custom property editor integration - Property editor observes `QGraphicsScene` and displays properties of selected items #### 6. Command Pattern (Undo/Redo) - **QUndoStack** (`m_pUndoStack` in CMainWindow) manages undo/redo - **OperationCommand**: Base command class - **AddItemCommand**: Add item to scene - **DeleteItemCommand**: Remove item from scene - **CreateItemGoupCommand**: Group selected items (with flattening) - **DestroyItemGoupCommand**: Ungroup items ### Signal Flow 1. **User drags from 图元面板** → `GraphicElementsPanel` emits signal → `DesignerScene::signalAddItem()` → creates item with `AddItemCommand` 2. **User clicks on canvas** → `DesignerView` → `DesignerScene` → `SelectorManager::getWorkingSelector()` → selector processes event 3. **User selects items** → `DesignerScene::selectionChanged()` → `CMainWindow::onSignal_selectionChanged()` → updates property editor 4. **User modifies property in 属性编辑器** → `QDetailsView` updates item property → scene updates ### Key Files and Relationships ``` 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 ├── 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 source/designerView.h/.cpp # QGraphicsView subclass - zoom, pan, middle-button navigation source/designerScene.h/.cpp # QGraphicsScene subclass - grid background, group operations └── Delegates to SelectorManager for mouse events source/util/selectorManager.h/.cpp # Singleton managing active selector └── Selector::mousePressEvent/ReleaseEvent/MoveEvent() source/graphicsItem/ # Graphics item implementations ├── GraphicsBaseItem # Base class with common functionality ├── GraphicsRectItem # Rectangle ├── GraphicsPolygonItem # Editable polygon ├── GraphicsItemGroup # Item grouping └── GraphicsBusSectionItem source/propertyType/ # Custom property editor integration ├── CustomType.h ├── CustomGadget.h └── PropertyTypeCustomization_CustomType.cpp ``` ### Third-Party Dependencies 1. **QtADS** (`QtADS/` subdirectory, v4.3.1): Advanced docking system - must be present as subdirectory 2. **PropertyEditor** (`PropertyEditor/` subdirectory): Qt Quick-based property editing system with QDetailsView Both are added via `add_subdirectory()` in CMakeLists.txt and must exist in the repository root. ### Platform Support - **Qt Version**: Qt5 or Qt6 (auto-detected, `find_package(QT NAMES Qt6 Qt5 ...)`) - **Architectures**: x86, x64, arm64, aarch64 (auto-detected in CMakeLists.txt) - **Platforms**: Windows, macOS, Linux, Android (Android uses shared library) ### Important Implementation Details 1. **CMAKE_AUTOUIC_SEARCH_PATHS**: Set to `"ui"` in CMakeLists.txt because .ui files are in separate directory from header files 2. **Group Flattening**: When creating a group that contains existing groups, the scene flattens nested groups first to prevent group nesting (see `DesignerScene::createGroup()`) 3. **Zoom Implementation**: Uses `QGraphicsView::zoom()` with smooth factor `qPow(1.0015, angleDelta.y())` per wheel event 4. **Wchar String Conversion**: Uses `QString::fromWCharArray(L"中文")` for Chinese UI strings 5. **Resource File**: `resource/BayTemplate.qrc` contains checkerboard background and icons, referenced in .ui files and CMakeLists.txt --- ## Graphics View Architecture Deep Dive ### MVC/MVVM Architecture Mapping BayTemplate implements a classic **Model-View-Controller** pattern using Qt's Graphics View Framework: | Layer | Qt Graphics View | BayTemplate Implementation | Responsibility | |-------|------------------|---------------------------|----------------| | **Model** | QGraphicsItem | GraphicsBaseItem, GraphicsRectItem, GraphicPolygonItem, GraphicsItemGroup | 数据模型:存储图元几何数据、样式属性(pen/brush)、变换信息(位置/旋转/缩放)| | **Model Manager** | QGraphicsScene | DesignerScene | 模型管理:管理 item 生命周期、选择状态、碰撞检测、背景绘制(网格)| | **View** | QGraphicsView | DesignerView | 视图呈现:坐标系变换、滚动条控制、鼠标事件捕获、缩放/平移 | | **Controller** | (Qt 无内置) | SelectorManager + BaseSelector 体系 | 控制器:解释用户输入,转换为模型操作(创建/移动/旋转/缩放/编辑)| **关键点:** - Qt 的 Graphics View 框架提供了 MVC 中的 Model 和 View 层 - Controller 层需要开发者自行实现,BayTemplate 使用 SelectorManager 作为中枢 - DesignerScene 介于 Model Manager 和 Controller 之间,负责事件分发 ### View-Scene-Item 三层架构详解 ``` ┌─────────────────────────────────────────────────────────────┐ │ DesignerView │ │ (Viewport / Presentation) │ ├─────────────────────────────────────────────────────────────┤ │ • 坐标变换:Viewport Coordinates ↔ Scene Coordinates │ │ • 缩放控制:0.02x - 50x (mouse wheel with smooth zoom) │ │ • 平移控制:Middle-button drag (translate transform) │ │ • 事件捕获:mousePressEvent/moveEvent/releaseEvent │ │ • 无滚动条:setHorizontal/VerticalScrollBarPolicy(Off) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ DesignerScene │ │ (Scene Graph Manager) │ ├─────────────────────────────────────────────────────────────┤ │ • Item 管理:addItem(), removeItem(), selectedItems() │ │ • 背景绘制:drawBackground() 绘制 20px 间距的藏青色虚线网格 │ │ • 事件分发:将鼠标事件转发给 SelectorManager │ │ • 组操作:createGroup() 实现扁平化打组 │ │ • 信号发射:signalAddItem(), selectionChanged() │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ GraphicsItem │ │ (Data Model) │ ├─────────────────────────────────────────────────────────────┤ │ GraphicsBaseItem (AbstractShapeType) │ │ ├── GraphicsRectItem (rectangle, 圆角矩形) │ │ ├── GraphicPolygonItem (多点绘制,顶点编辑) │ │ ├── GraphicsBusSectionItem (总线段) │ │ └── GraphicsItemGroup (AbstractShapeType) │ └─────────────────────────────────────────────────────────────┘ ``` ### 坐标系与变换系统 BayTemplate 使用多层坐标系,理解它们对添加 Document 类至关重要: ``` 1. Viewport Coordinates (像素坐标) └── DesignerView 的 viewport 矩形,(0,0) 在左上角 └── 受窗口大小、缩放影响 2. Scene Coordinates (场景坐标) └── 全局坐标系,(0,0) 为场景原点 └── 通过 mapToScene()/mapFromScene() 与 Viewport 互转 └── 图元的位置 pos() 使用场景坐标 3. Item Coordinates (图元局部坐标) └── 每个 item 的局部坐标系,原点在 (0,0) └── BayTemplate 中,图元中心点通常与 (0,0) 重合 └── boundingRect() 返回局部坐标中的边界 4. Parent Coordinates (父项坐标) └── 当 item 加入 group 后,使用 group 的局部坐标 └── mapToParent()/mapFromParent() 用于转换 ``` **变换链(Transform Chain):** ``` Item Coordinates → Item Transform → Parent Coordinates → ... → Scene Coordinates → View Transform → Viewport Coordinates ``` - **Item Transform**: 由 `setPos()`, `setRotation()`, `setScale()` 控制的 3×3 变换矩阵 - **Transform Origin**: `setTransformOriginPoint()` 设置变换中心(BayTemplate 中通常为中心点) - **View Transform**: DesignerView 的 zoom/pan 产生的变换,影响所有 item 的显示 **关键函数:** - `item->mapToScene(point)`: Item 局部坐标 → 场景坐标 - `item->mapToView(point)`: Item 局部坐标 → Viewport 坐标 - `view->mapToScene(point)`: Viewport 坐标 → 场景坐标 - `view->mapSceneToViewport(rect)`: 场景矩形 → Viewport 矩形 ### 操作模式与状态机(Selector 系统) SelectorManager 实现了一个**状态机**,不同的 Selector 代表不同的操作模式: ``` ┌─────────────────┐ │ ST_base │ │ (空闲状态) │ └────────┬────────┘ │ ┌──────────────────┼──────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ ST_creating │ │ ST_moving │ │ ST_editing │ │ (创建模式) │ │ (移动模式) │ │ (顶点编辑) │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ │ │ ▼ ▼ ┌──────────────┐ ┌──────────────┐ │ ST_scaling │ │ ST_rotation │ │ (缩放模式) │ │ (旋转模式) │ └──────────────┘ └──────────────┘ │ │ └────────┬─────────┘ │ ▼ ┌────────┐ │ ST_base│ (操作完成后返回) └────────┘ ``` **状态转换规则:** | 触发条件 | 从状态 | 到状态 | 说明 | |---------|--------|--------|------| | 从图元面板拖拽 | ST_base | ST_creating | 设置 CreatingSelector 的 m_pCreatingItem | | 点击选中 item(左键) | ST_base | ST_moving/ST_scaling/ST_rotation | 根据是否点击在 handle 上判断 | | 点击空白处 | 任何 | ST_base | 取消选择,禁用 RubberBandDrag | | mouseReleaseEvent | ST_moving/scaling/rotation | ST_base | 操作完成,返回空闲状态 | **BaseSelector 静态成员(所有 Selector 共享):** ```cpp static QPointF ms_ptMouseDown; // 鼠标按下位置(场景坐标) static QPointF ms_ptMouseLast; // 鼠标当前位置(场景坐标) static double ms_dAngleMouseDownToItem; // 按下时鼠标与 item 中心的夹角(弧度→角度) static int ms_nDragHandle; // 当前拖拽的 handle 类型 ``` ### Handle 控制系统 ItemControlHandle 是围绕在选中 item 周围的视觉控件,用于精细操作: ``` H_leftTop ──── H_top ──── H_rightTop │ │ │ │ │ │ ┌────┴────────────┴────────────┴────┐ │ │ │ Item Bounding │ │ Rect │ │ │ └────┬────────────┬────────────┬────┘ │ │ │ H_left ────── H_bottom ──── H_rightBottom ``` **Handle 类型枚举:** ```cpp enum HandleTag { H_none = 0, // 无 handle H_leftTop, // 左上角(缩放) H_top, // 上边中点(缩放) H_rightTop, // 右上角(缩放) H_right, // 右边中点(缩放) H_rightBottom, // 右下角(缩放) H_bottom, // 下边中点(缩放) H_leftBottom, // 左下角(缩放) H_left, // 左边中点(缩放) H_rotate_leftTop, // 左上角外侧(旋转) H_rotate_rightTop, // 右上角外侧(旋转) H_rotate_rightBottom, // 右下角外侧(旋转) H_rotate_leftBottom, // 左下角外侧(旋转) H_edit, // 自定义编辑点(多边形顶点、圆角控制点) // ... 更多编辑点 }; ``` **Handle 工作机制:** 1. `AbstractShapeType::updateHandles()` 根据 boundingRect 计算 handle 位置 2. Handle 位置在 boundingRect 外扩 5px (nMargin = 5) 3. `collidesWithHandle()` 检测鼠标点击在哪个 handle 上 4. 不同操作模式对 handle 的响应不同 ### 操作副本(Operation Copy)模式 移动和旋转操作使用"预览副本"技术,提供视觉反馈: ```cpp // 1. mousePressEvent: 创建副本 void createOperationCopy() { m_pOperationCopy = new QGraphicsPathItem(this->shape()); m_pOperationCopy->setPen(Qt::DashLine); // 虚线表示预览 m_pOperationCopy->setPos(this->pos()); scene->addItem(m_pOperationCopy); m_movingIniPos = this->pos(); } // 2. mouseMoveEvent: 移动副本(本体不动) void moveOperationCopy(const QPointF& delta) { m_pOperationCopy->setPos(m_movingIniPos + delta); } // 3. mouseReleaseEvent: 副本位置应用到本体 void removeOperationCopy() { this->setPos(m_pOperationCopy->pos()); // 本体移动到副本位置 scene->removeItem(m_pOperationCopy); delete m_pOperationCopy; } ``` **优势:** - 移动过程中保持原来的 selection state - 可以拖出 boundingRect_selected 范围(否则会被误认为取消选择) - 旋转/缩放同理 ### 图元基类架构(AbstractShapeType 模板) ```cpp // 使用模板继承,允许继承 QGraphicsItem 或 QGraphicsItemGroup template class AbstractShapeType : public BaseType { protected: ShapeType m_type; // T_undefined/T_item/T_group QPen m_pen; // 画笔(边框) QBrush m_brush; // 画刷(填充) double m_dWidth, m_dHeight; // 宽高 QRectF m_boundingRect; // 局部坐标中的边界矩形 QRectF m_boundingRect_selected; // 选中框(外扩 5px) double m_dSyncRotationByParent; // 父项旋转同步数据 QVector m_vecHanle; QGraphicsPathItem* m_pOperationCopy; QPointF m_movingIniPos; }; // 具体类型定义 typedef AbstractShapeType AbstractShape; // 单图元基类 // GraphicsBaseItem : QObject, AbstractShape // GraphicsItemGroup : QObject, AbstractShapeType // 组图元基类 ``` **关键点:** - `m_boundingRect` 的中心点与 item 的 (0,0) 原点重合,简化变换计算 - `updateCoordinate()` 在 resize/editShape 后重新校准原点 - `m_dSyncRotationByParent` 用于组内 item 跟踪父组的旋转(因为 item 的 rotation() 不会自动更新) ### 命令模式(Command Pattern) 所有可撤销操作都通过 QUndoCommand 实现: ```cpp class AddItemCommand : public QUndoCommand { void undo() override { scene->removeItem(m_pItem); } void redo() override { if(!m_pItem->scene()) scene->addItem(m_pItem); } }; class DeleteItemCommand : public QUndoCommand { void undo() override { foreach(item, m_listItem) scene->addItem(item); } void redo() override { foreach(item, m_listItem) scene->removeItem(item); } }; ``` **关键点:** - `QUndoStack::push(command)` 会自动调用 `command->redo()` - `DeleteItemCommand` 使用 `removeItem()` 而非 `delete`,确保 undo 能恢复 - 命令对象持有 item 的引用(非所有权) ### 组(Group)扁平化策略 `DesignerScene::createGroup()` 实现组扁平化: ```cpp // 如果选中的 item 中包含已有的 group,先解散该 group,将子项加入新组 foreach (item, selectedItems) { if (item->getType() == T_group) { GraphicsItemGroup* group = qgraphicsitem_cast(item); foreach (child, group->childItems()) { listItem.push_back(child); // 将子项加入新列表 } removeItem(group); delete group; // 删除旧组 } } // 创建新组,避免嵌套 ``` **目的:** - 简化场景图结构(无嵌套的树) - 简化坐标计算和碰撞检测 - 统一 undo/redo 逻辑 --- ## Document 类架构(持久化层) ### Document 类在 MVC 中的位置 Document 类作为**持久化层**插入现有架构,位于 Model 层之下: ``` ┌─────────────────────────────────────────┐ │ Controller │ │ (SelectorManager + 各种 Selector) │ └──────────────┬──────────────────────────┘ │ ┌──────────────▼──────────────────────────┐ │ View │ │ (DesignerView + UI) │ └──────────────┬──────────────────────────┘ │ ┌──────────────▼──────────────────────────┐ │ Model Manager │ │ (DesignerScene) │ └──────────────┬──────────────────────────┘ │ ┌──────────────▼──────────────────────────┐ │ Model │ │ (GraphicsItem 层次结构) │ └──────────────┬──────────────────────────┘ │ ┌──────────────▼──────────────────────────┐ │ Document │ │ (序列化/状态管理) │ └──────────────────────────────────────────┘ ``` **设计原则:** - **组合优于继承**:Document 持有 `DesignerScene*` 弱引用,不继承 Scene - **单向依赖**:Document 只依赖 DesignerScene 的接口,不依赖 CMainWindow - **信号槽通信**:通过信号与外部通信修改状态,避免紧耦合 ### Document 类职责 1. **序列化/反序列化**(JSON 格式) - `serialize()` / `deserialize()`: 核心序列化为 QByteArray - `saveToFile()` / `loadFromFile()`: 文件 I/O 封装 - 递归序列化/反序列化图元层次结构(支持组图元嵌套) 2. **状态管理** - `m_filename`: 文档文件名(空表示未保存的新文档) - `m_modified`: 是否被修改(通过 `setModified()` 和 `modifiedChanged` 信号通知) - `m_created` / `m_modifiedTime` / `m_lastSavedTime`: 时间戳 - `m_metaData`: 可扩展的自定义元数据 (`QMap`) 3. **与 CMainWindow 的集成** - `initializeDocument()`: 创建 Document 并关联 DesignerScene - 连接 `modifiedChanged` 信号来更新窗口标题(添加 * 标记) - `closeEvent()` 中检查 `isModified()` 提示用户保存 - 文件操作:New/Open/Save 通过 Document 完成 ### 序列化格式(v1.0) ```json { "version": "1.0", "created": "2024-01-01T12:00:00", "modified": "2024-01-01T14:30:00", "filename": "/path/to/file.bay", "metaData": { "customKey": "customValue" }, "items": [ { "type": "GraphicsRectItem", "id": "140737488320", "pos": {"x": 100.0, "y": 200.0}, "rotation": 45.0, "scale": {"x": 1.0, "y": 1.0}, "pen": {"color": "#000000", "width": 1.0, "style": 0}, "brush": {"color": "#FF0000"}, "width": 100.0, "height": 50.0, "properties": {}, "children": [] }, { "type": "GraphicsItemGroup", "pos": {"x": 300.0, "y": 150.0}, "rotation": 0.0, "scale": {"x": 1.0, "y": 1.0}, "width": 200.0, "height": 100.0, "children": [ { "type": "GraphicsRectItem", ... }, { "type": "GraphicsBusSectionItem", ... } ] } ] } ``` ### 实现细节 **类型识别:** 使用 `qobject_cast(item)` 配合 `getTypeString()` 函数识别图元类型(要求类型有 `Q_OBJECT` 宏) **QVariant ↔ QJsonValue 转换:** 提供 `toJsonValue()` / `fromJsonValue()` 辅助函数处理元数据序列化 **坐标系统:** 图元使用中心原点,`pos()` 存储场景坐标,`boundingRect()` 使用局部坐标 **文件扩展名:** `.bay` 作为 BayTemplate 专用格式 ### 关键文件 - `include/document.h`: Document 类声明 - `source/document.cpp`: 序列化/反序列化实现 - `source/mainwindow.cpp`: `initializeDocument()`, `onAction_new()`, `onAction_open()`, `onAction_save()` --- ## Selector 系统设计模式总结 ### 状态模式(State Pattern) SelectorManager 实现状态模式,不同的 Selector 子类代表不同的状态: ```cpp // Context class SelectorManager { BaseSelector* getWorkingSelector(); // 返回当前状态的 Selector void setWorkingSelector(SelectorType); }; // State 接口 class BaseSelector { virtual void mousePressEvent(...) = 0; virtual void mouseMoveEvent(...) = 0; virtual void mouseReleaseEvent(...) = 0; }; // Concrete State class MovingSelector : public BaseSelector { ... }; class ScalingSelector : public BaseSelector { ... }; // ... ``` **静态成员共享的原因:** BaseSelector 的静态成员(ms_ptMouseDown 等)在所有 Selector 实例间共享,使得状态切换时无需传递上下文数据。 ### 操作副本模式(Operation Preview Pattern) 移动、旋转、缩放都使用"操作副本"作为视觉预览: 1. mousePressEvent: 创建虚线副本 2. mouseMoveEvent: 移动副本(本体不动) 3. mouseReleaseEvent: 将副本的变换应用到本体,删除副本 ### Handle 系统(Manipulator Pattern) ItemControlHandle 实现操纵器模式,将抽象的变换操作(缩放、旋转)映射到可视化的交互控件。 --- ## 坐标系转换速查表 | 转换 | 函数 | 说明 | |------|------|------| | Item → Scene | `item->mapToScene(point)` | 局部坐标转场景坐标 | | Scene → Item | `item->mapFromScene(point)` | 场景坐标转局部坐标 | | Item → Viewport | `item->mapToView(point)` | 局部坐标转屏幕像素 | | Item → Parent | `item->mapToParent(point)` | 局部坐标转父项坐标 | | Viewport → Scene | `view->mapToScene(point)` | 屏幕像素转场景坐标 | | Scene Rect → Viewport Rect | `view->mapSceneToViewport(rect)` | 场景矩形转屏幕矩形 | ## Handle 类型速查表 | Handle 类型 | 位置 | 功能 | |------------|------|------| | H_leftTop ~ H_left | boundingRect 八方向 | 缩放 | | H_rotate_leftTop ~ H_rotate_leftBottom | boundingRect 外扩 | 旋转 | | H_edit + N | 自定义位置 | 形状编辑(多边形顶点、圆角控制点)| ## 操作模式速查表 | SelectorType | 触发方式 | 用途 | |-------------|---------|------| | ST_base | 默认 | 空闲状态,允许橡胶框选择 | | ST_creating | 从图元面板拖拽 | 创建新图元 | | ST_moving | 点击 item 主体 | 移动选中项 | | ST_scaling | 点击缩放 handle | 缩放选中项 | | ST_rotation | 点击旋转 handle | 旋转选中项 | | ST_editing | 点击编辑 handle | 编辑多边形顶点等 | --- ## PropertyEditor 系统架构深度剖析 ### 概述 PropertyEditor 是一个第三方属性编辑器库(位于 `PropertyEditor/` 子目录),受虚幻引擎属性面板启发,借助 Qt 反射系统自动构建 QObject 的属性编辑 UI。核心亮点: - **基于 QML GPU 渲染**:使用 `QQuickWidget` 嵌套 QML TreeView + Delegate 实现 - **自动反射**:通过 `Q_PROPERTY` 宏声明的属性自动生成编辑器行 - **类型驱动编辑器选择**:根据 `QMetaType` 自动匹配对应的 ValueEditor QML 组件 - **可扩展定制**:通过 `IPropertyTypeCustomization` 接口为特定类型自定义整行布局和子项结构 ### 完整数据流 ``` CMainWindow::onSignal_selectionChanged() │ ▼ QDetailsView::setObject(QObject*) ← 设置被检查的 QObject │ ▼ QQuickDetailsView::setObject(QObject*) ← QML 端的入口 │ ▼ QQuickDetailsViewModel::setObject(QObject*) ← QAbstractItemModel │ └─ 创建根 QPropertyHandle (FindOrCreate) │ └─ 创建根 QDetailsViewRow_Property(mRoot) │ └─ mRoot->invalidateChildren() │ │ │ ▼ │ QDetailsViewRow_Property::attachChildren() │ │ │ ├── 有 IPropertyTypeCustomization? │ │ YES → customizeChildren(handle, layoutBuilder) │ │ NO → PropertyTypeCustomization_ObjectDefault::customizeChildren() │ │ ├─ 检查 objectHandle 是否实现 IPropertyGroupProvider │ │ │ YES → 按 group 名称收集属性 → addGroup() + 子 LayoutBuilder │ │ │ NO → 遍历 metaObject->propertyCount() 平铺属性 │ │ └─ layoutBuilder->addProperty(childHandle) │ │ │ ▼ │ QQuickTreeViewEx (QML TableView) 为每行创建 Delegate │ │ │ ▼ │ QQuickDetailsViewPrivate::initItemCallback() │ │ │ ▼ │ QDetailsViewRow_Property::setupItem(QQuickItem* inParent) │ │ │ ├── 有 IPropertyTypeCustomization? │ │ YES → customizeHeaderRow(handle, rowBuilder) │ │ NO → rowBuilder->makePropertyRow(handle) │ │ │ │ │ ├── makeNameValueSlot() │ │ │ └─ 创建 QML Row: [展开箭头] [名称区] [分隔条] [值编辑区] │ │ │ │ │ ├── handle->setupNameEditor(nameSlot) │ │ │ └─ IPropertyHandleImpl::createNameEditor() │ │ │ └─ 创建 Text { text: model.name } QML 组件 │ │ │ │ │ └── handle->steupValueEditor(valueSlot) │ │ └─ IPropertyHandleImpl::createValueEditor() │ │ └─ 根据类型查找注册的编辑器 QML 组件 │ │ │ ▼ │ [最终渲染为带颜色的行,左侧名称 + 右侧编辑器控件] ``` ### 核心类详解 #### 1. QDetailsView(C++ 侧的 QWidget 包装器) ```cpp // source/PropertyEditor/source/src/QDetailsView.cpp class QDetailsView : public QWidget { QQuickWidget* mQuickWidget; // 嵌入 QML 引擎 QQuickDetailsView* mQuickDetailsView; // QML 端的 DetailsView 对象 void setObject(QObject* inObject); // 关键入口:设置被检查的对象 }; ``` #### 2. QQuickDetailsViewModel(QAbstractItemModel 的树形实现) ```cpp // source/PropertyEditor/source/src/QQuickDetailsViewModel.cpp class QQuickDetailsViewModel : public QAbstractItemModel { QSharedPointer mRoot; // 根行(不可见) QObject* mObject; // 被检查的对象 void setObject(QObject* inObject) { mObject = inObject; mRoot->setHandle(QPropertyHandle::FindOrCreate(mObject)); // 创建根 Handle mRoot->invalidateChildren(); // 触发递归构建属性树 } }; ``` **模型的树结构**(带分组示例): ``` mRoot (QDetailsViewRow_Property, 不可见) ├── QDetailsViewRow_Group("数值") ← 分组标题(可折叠) │ ├── QDetailsViewRow_Property("Int") ← QMetaProperty │ ├── QDetailsViewRow_Property("Float") │ └── QDetailsViewRow_Property("LimitedDouble") ├── QDetailsViewRow_Group("文本") ← 分组标题(可折叠) │ ├── QDetailsViewRow_Property("String") │ └── QDetailsViewRow_Property("Directory") ├── QDetailsViewRow_Group("自定义类型") ← 分组标题(可折叠) │ ├── QDetailsViewRow_Property("CustomEnum") │ ├── QDetailsViewRow_Property("CustomType") ← IPropertyTypeCustomization │ │ ├── QDetailsViewRow_Property("ArraySize") │ │ └── QDetailsViewRow_Property("Array") │ │ ├── QDetailsViewRow_Property("[0]") │ │ └── QDetailsViewRow_Property("[1]") │ └── QDetailsViewRow_Property("CustomGadget") │ ├── QDetailsViewRow_Property("LimitedDouble") │ └── QDetailsViewRow_Property("Desc") └── QDetailsViewRow_Property("m_lastBoudingRectF") ← 未分组属性(直接挂根) ``` #### 3. QPropertyHandle(属性操作的统一入口) ```cpp // source/PropertyEditor/source/include/QPropertyHandle.h // source/PropertyEditor/source/src/QPropertyHandle.cpp class QPropertyHandle : public QObject { Q_OBJECT Q_PROPERTY(QVariant Var READ getVar WRITE setVar NOTIFY asVarChanged) using Getter = std::function; using Setter = std::function; QMetaType mType; // 属性的 QMetaType PropertyType mPropertyType; // RawType | Enum | Sequential | Associative | Object QString mPropertyPath; // 属性路径(如 "SubObject.LimitedDouble") Getter mGetter; // 获取属性值的 lambda Setter mSetter; // 设置属性值的 lambda QVariantHash mMetaData; // 从 Q_CLASSINFO 解析的元数据(Min, Max, Step, Precision) QSharedPointer mImpl; // 根据 PropertyType 选择的实现策略 }; ``` **关键接口**: - `static FindOrCreate(parent, type, path, getter, setter)` — 在父对象中查找已有 Handle,找不到则创建新的。Handle 作为 `parent` 的子 QObject 存储,生命周期跟随父对象 - `getVar()` / `setVar()` — 通过 Getter/Setter lambda 读写实际属性值 - `setVar()` 的 rollback 机制:设置后重新读取,如果值不匹配则发射 `asRequestRollback` 信号通知编辑器回滚显示值 - `parserType(QMetaType)` — 根据 QMetaType 自动分类: - 可转为 QVariantList 且不可转为 QString → Sequential(列表) - 可转为 QVariantMap → Associative(映射) - IsEnumeration flag → Enum - 有 metaObject(QObject 或 QSharedPointer)→ Object - 否则 → RawType(基本类型) - `findOrCreateChild()` — 为 Object 类型的属性创建子 Handle,自动连接到父 Handle 的 `asVarChanged` 信号以触发子项刷新 #### 4. IPropertyHandleImpl(编辑器创建策略) ``` IPropertyHandleImpl ├── QPropertyHandleImpl_RawType → 调用 QQuickDetailsViewManager::createValueEditor() │ 查找已注册的 TypeEditor ├── QPropertyHandleImpl_Enum → 创建 ComboBox 选择器 ├── QPropertyHandleImpl_Object → createValueEditor() 返回 nullptr(Object 类型本身不显示值编辑器) │ ├── getObject() / getGadget() — 区分 QObject* 和 Q_GADGET 值类型 │ ├── refreshObjectPtr() — 从 Handle 重新读取对象指针 │ └── 核心作用:作为属性树的内部节点,展开显示子属性 ├── QPropertyHandleImpl_Sequential → 展开为 index-based 子行([0], [1], ...) └── QPropertyHandleImpl_Associative → 展开为 key-value 子行 ``` #### 5. IDetailsViewRow(模型树节点) ``` IDetailsViewRow (抽象基类) ├── QDetailsViewRow_Property ← 包装一个 QPropertyHandle │ setupItem(): 如果有 IPropertyTypeCustomization → customizeHeaderRow() │ 否则 → makePropertyRow(handle)(默认的名称+值编辑器布局) │ attachChildren(): 如果有 IPropertyTypeCustomization → customizeChildren() │ 否则 → PropertyTypeCustomization_ObjectDefault(遍历 metaObject->propertyCount()) ├── QDetailsViewRow_Custom ← 支持通过 lambda 创建自定义行 │ setupItem(): 调用 mRowCreator(&builder) 直接构造 QML └── QDetailsViewRow_Group ← 可折叠的属性分组标题行 isGroup() 返回 true,setupItem() 渲染粗体分组标题 + 折叠时显示子项计数 "(N)" ``` **QDetailsViewRow_Group**: - `isGroup()` 返回 `true`,通过 model 的 `isGroup` role 暴露给 QML delegate - `setupItem()` 使用 `makeNameValueSlot()` 构建行布局: - **名称区**:粗体 (`font.bold: true`) 文本显示分组名(如"数值"、"颜色") - **值区**:折叠时显示 `"(N)"` 表示子属性数量(通过 `groupChildCount` 属性绑定) - 高度固定 25px,颜色使用 `ColorPalette.theme.labelPrimary` - 展开/折叠由 delegate 模板中的 `TapHandler` 触发 `detailsView.toggleExpanded(row)` - `setObject()` 时 C++ 侧自动展开所有 group 行(一次性,避免 delegate 重建时的重复展开循环) #### 6. IPropertyTypeCustomization(类型自定义扩展接口) ```cpp class IPropertyTypeCustomization { virtual void customizeHeaderRow(QPropertyHandle*, QQuickDetailsViewRowBuilder*); virtual void customizeChildren(QPropertyHandle*, QQuickDetailsViewLayoutBuilder*); }; ``` - `customizeHeaderRow`: 自定义该属性行本身的 UI(替换默认的 name + value editor) - `customizeChildren`: 自定义该属性展开后的子行结构 - 注册方式:`QQuickDetailsViewManager::Get()->registerPropertyTypeCustomization()` **内置 Customization**: - `PropertyTypeCustomization_ObjectDefault`:默认的 QObject 处理,遍历 `metaObject->propertyCount()`,为每个 QMetaProperty 创建子 Handle(使用 `prop.read()`/`prop.write()`)。Gadget 类型从索引 0 开始,QObject 类型从索引 1 开始(跳过 `objectName`) - `PropertyTypeCustomization_Sequential`:展开 QList/QVector 等顺序容器 - `PropertyTypeCustomization_Associative`:展开 QMap 等关联容器 - `PropertyTypeCustomization_Matrix4x4`:4×4 矩阵编辑器 #### 7. QQuickDetailsViewManager(全局注册中心,Singleton) ```cpp QHash mClassCustomizationMap; // QObject 类型 → 自定义 QHash mMetaTypeCustomizationMap; // 值类型 → 自定义 QHash mTypeEditorCreatorMap; // 基本类型 → 编辑器 QML 组件 ``` - 内置注册的基本类型编辑器:`int/unsigned int/size_t`, `float`, `double`, `QString`, `QVector2D/3D/4D`, `QColor`, `QDir` - 内置注册的类型自定义:`QMatrix4x4` → `PropertyTypeCustomization_Matrix4x4` - QML 模块注册:`DetailsView` (1.0), `ColorPalette` (单例) - `getCustomPropertyType()` 查找逻辑: 1. Sequential → PropertyTypeCustomization_Sequential 2. Associative → PropertyTypeCustomization_Associative 3. Object → 查找 mClassCustomizationMap(精确匹配 + 继承匹配),fallback 到 ObjectDefault 4. RawType → 查找 mMetaTypeCustomizationMap(精确匹配 + 继承匹配) #### 8. QQuickDetailsViewRowBuilder / QQuickDetailsViewLayoutBuilder(QML 布局构建器) - **RowBuilder**:构建单行 UI - `makeNameValueSlot()` — 创建标准的 [名称区域 | 分隔条 | 值区域] QML 布局 - `setupItem(parent, qmlCode)` — 在父 QML Item 中创建子 QML 组件 - `setupLabel(parent, text)` — 创建文本标签 - `makePropertyRow(handle)` — 默认属性行:name slot + value slot - **LayoutBuilder**:管理子行结构 - `addProperty(handle, overrideName)` — 添加属性子行 - `addCustomRow(lambda, overrideName)` — 添加自定义行 - `addGroup(groupName)` — 创建 `QDetailsViewRow_Group` 分组标题行,返回其指针供后续向组内添加子行 - `addObject(QObject*)` — 添加对象作为属性行 ### Q_CLASSINFO 元数据系统 通过 `Q_CLASSINFO` 宏向属性编辑器传递校验约束,格式为逗号分隔的 `key=value` 对: ```cpp Q_CLASSINFO("LimitedDouble", "Min=0,Max=10,Step=0.5,Precision=2") ``` `QPropertyHandle::resloveMetaData()` 解析 `metaObject->classInfo()` 中与属性名匹配的项,将键值对存入 `mMetaData`。ValueEditor 从 `mMetaData` 中读取约束(`Min`, `Max`, `Step`, `Precision`)来配置输入控件。 ### Q_PROPERTY_VAR 宏(BayTemplate 项目封装) ```cpp // include/CommonInclude.h #define Q_PROPERTY_VAR(Type, Name)\ Q_PROPERTY(Type Name READ get##Name WRITE set##Name)\ Type get##Name() { return Name; }\ void set##Name(Type var) {\ Name = var;\ qDebug() << "Set" << #Name << ": " << var;\ }\ Type Name ``` 这个宏将属性声明、getter/setter 实现、成员变量声明合并为一行,大大简化 Q_PROPERTY 的使用。 ### 当前 BayTemplate 集成状态 **初始化流程**(`source/mainwindow.cpp:118-128`): ```cpp // 1. 注册自定义类型编辑器 QQuickDetailsViewManager::Get()->registerPropertyTypeCustomization(); // 2. 创建 QDetailsView 并挂载到右侧 Dock m_pPropertiesEditorView = new QDetailsView(); m_pPropertiesEditorView->setObject(m_pDrawingPanel->getQGraphicsScene()); ``` **选择变化处理**(`source/mainwindow.cpp:226-235`): ```cpp void CMainWindow::onSignal_selectionChanged() { QList selectedItems = scene->selectedItems(); if (selectedItems.count() != 1) { m_pPropertiesEditorView->setObject(scene); // 多选/无选 → 显示场景属性 return; } m_pPropertiesEditorView->setObject(static_cast(selectedItems.first())); } ``` **当前图元的 Q_PROPERTY 状态**: - `GraphicsBusSectionItem`:唯一完整声明了 Q_PROPERTY 的图元(Int, Float, LimitedDouble, String, Directory, Vec2-4, Mat4, Color, ColorList, ColorMap, CustomEnum, CustomType, CustomGadget, CustomGadgetPtr, CustomGadgetSharedPtr 等) — 同时重写 `getPropertyGroup()` 将属性分入 5 个可折叠分组(数值、文本、向量/矩阵、颜色、自定义类型) - `GraphicsBaseItem` 实现了 `IPropertyGroupProvider` 接口,默认返回空字符串(无分组),子类可重写 - `GraphicsRectItem`、`GraphicsPolygonItem`、`GraphicsItemGroup`:**无 Q_PROPERTY 声明** — 因此属性编辑器不会显示任何属性行 **BayTemplate 自定义类型**: - `QCustomType` (结构体):`{ unsigned int ArraySize; QVector Array; }` — 通过 `PropertyTypeCustomization_CustomType` 定制,header 显示 Sort 按钮,children 展开为 ArraySize + Array - `QCustomGadget` (Q_GADGET):`{ double LimitedDouble; QString Desc; }` — 会被属性编辑器递归展开子属性 ### 属性编辑器的值读写循环 ``` 用户修改 QML 编辑器中的值 │ ▼ QML 编辑器 emit valueChanged(QVariant) │ (通过 connect 连接) ▼ QPropertyHandle::setVar(var) ├── mSetter(var) ← 写入实际对象属性 ├── emit asVarChanged(var) ← 通知其他监听者 ├── QVariant currVar = mGetter() ← 重新读取验证 └── if (currVar != var) emit asRequestRollback(currVar) ← 值被拦截时通知编辑器回滚 ``` ### 属性分组系统 (Property Grouping) 属性分组允许 QObject 子类将 Q_PROPERTY 属性归类到可折叠的分组标题下,使属性编辑器在有大量属性时更易浏览。 #### IPropertyGroupProvider 接口 ```cpp // PropertyEditor/source/include/IPropertyGroupProvider.h class IPropertyGroupProvider { public: virtual ~IPropertyGroupProvider() = default; virtual QString getPropertyGroup(const QString& propertyName) const = 0; }; ``` - 返回空字符串表示属性不分组(默认行为) - 返回非空字符串表示属性属于该名称的分组 - 同名分组的属性会被收集到同一个 group 下,按分组调用顺序排列 - **不属于 PropertyEditor 库本身**,而是由使用方(如 BayTemplate 的 GraphicsBaseItem)实现 #### 分组构建流程 ``` PropertyTypeCustomization_ObjectDefault::customizeChildren(handle, builder) │ ├── 检查 objectHandle->getObject() 是否实现 IPropertyGroupProvider? │ NO → 原有逻辑:遍历 metaObject->propertyCount() 平铺所有属性 │ │ YES → 新分组逻辑: │ 1. 遍历所有 QMetaProperty │ 2. 调用 groupProvider->getPropertyGroup(prop.name()) 获取分组名 │ 3. 空字符串 → 加入 ungroupedProps 列表 │ 4. 非空 → 加入对应 groupedProps[groupName] 列表 │ 5. 遍历 groupedProps: │ ├── builder->addGroup(groupName) → 创建 QDetailsViewRow_Group │ └── 在 group 内用子 LayoutBuilder 添加该组的属性 │ 6. 遍历 ungroupedProps: │ └── builder->addProperty() 直接添加到根(不分组) ``` #### QDetailsViewRow_Group 渲染 在 QML delegate 中通过 `isGroup` role 区分 group 行和普通属性行: - **已展开**:显示粗体分组标题(如"颜色"),右侧无内容 - **已折叠**:显示粗体分组标题 + 右侧 `"(N)"` 显示子属性数量 - 展开/折叠箭头由 `makeNameValueSlot()` 中的图标控制(根据 `detailsDelegate.expanded` 选择 expand.png / unexpand.png) - 点击行触发 `TapHandler { onTapped: detailsView.toggleExpanded(row) }` **折叠计数实现**(`QQuickDetailsViewRow.cpp`): ```qml // 在 value slot 区域创建 countLabel Item { property int groupChildCount: 0 visible: !detailsDelegate.expanded // 仅折叠时可见 Text { anchors.right: parent.right text: "(" + groupChildCount + ")" color: ColorPalette.theme.textPrimary } } ``` `groupChildCount` 在 C++ 侧通过 `countLabel->setProperty("groupChildCount", childCount)` 设置。 #### Model 角色扩展 ViewModel 新增 `isGroup` role: ```cpp // QQuickDetailsViewModel enum Roles { name = 0, isGroup }; // roleNames() 返回 { { Roles::isGroup, "isGroup" } } // data() 分发 case Roles::isGroup: return node->isGroup(); ``` `QQuickDetailsViewPrivate::updateRequiredProperties()` 在每个 delegate 初始化时通过 `setRequiredProperty("isGroup", ...)` 将值推送到 QML delegate。 #### setObject() 自动展开 `QQuickDetailsView::setObject()` 在切换检查对象后,遍历顶层 model 行,对所有 `isGroup()` 为 true 的行调用 `expand(row)`。这是一次性的 C++ 侧操作,在 delegate 创建之前完成,避免 delegate 重建时的重复展开循环。 #### BayTemplate 集成 **GraphicsBaseItem** 实现 `IPropertyGroupProvider`,默认返回空字符串(无分组): ```cpp class GraphicsBaseItem : public QObject, public AbstractShape, public IPropertyGroupProvider { QString getPropertyGroup(const QString&) const override { return {}; } }; ``` **GraphicsBusSectionItem** 重写 `getPropertyGroup()` 作为使用示例(`source/graphicsItem/graphicsBusSectionItem.cpp`): | 分组名 | 包含属性 | |--------|---------| | 数值 | Int, Float, LimitedDouble | | 文本 | String, Directory | | 向量/矩阵 | Vec2, Vec3, Vec4, Mat4 | | 颜色 | Color, ColorList, ColorMap | | 自定义类型 | CustomEnum, CustomType, CustomGadget, CustomGadgetPtr, CustomGadgetSharedPtr | 不分组(返回空)的属性:`m_lastBoudingRectF`, `m_lastBoudingPointF`, `m_dRatioX`, `m_dRatioY`(内部状态属性)。 **扩展方式**:任意 QObject 子类只需实现 `IPropertyGroupProvider` 接口并重写 `getPropertyGroup()`,PropertyEditor 的 `PropertyTypeCustomization_ObjectDefault` 会自动检测并使用分组布局。 ### 关键文件索引 | 文件 | 角色 | |------|------| | `PropertyEditor/source/include/QDetailsView.h` | C++ QWidget 包装器 | | `PropertyEditor/source/src/QDetailsView.cpp` | 创建 QQuickWidget,加载 QML DetailsView | | `PropertyEditor/source/include/QQuickDetailsView.h` | QML 类型定义(Q_PROPERTY Object, SpliterPencent)| | `PropertyEditor/source/src/QQuickDetailsView.cpp` | 设置 Delegate 模板 | | `PropertyEditor/source/include/QQuickDetailsViewModel.h` | QAbstractItemModel 实现 | | `PropertyEditor/source/src/QQuickDetailsViewModel.cpp` | setObject() 入口,构建根 Handle | | `PropertyEditor/source/include/QPropertyHandle.h` | 属性操作统一入口(核心抽象)| | `PropertyEditor/source/src/QPropertyHandle.cpp` | getter/setter 绑定,类型解析,子 Handle 创建 | | `PropertyEditor/source/include/PropertyHandleImpl/IPropertyHandleImpl.h` | Impl 策略基类 | | `PropertyEditor/source/src/PropertyHandleImpl/QPropertyHandleImpl_Object.cpp` | QObject/QGadget 的属性展开逻辑 | | `PropertyEditor/source/src/PropertyHandleImpl/QPropertyHandleImpl_RawType.cpp` | 基本类型的值编辑器委托 | | `PropertyEditor/source/src/QQuickDetailsViewRow.cpp` | 模型行节点(setupItem + attachChildren)| | `PropertyEditor/source/src/QQuickDetailsViewLayoutBuilder.cpp` | QML 布局构建器 | | `PropertyEditor/source/include/IPropertyTypeCustomization.h` | 类型定制接口 | | `PropertyEditor/source/include/IPropertyGroupProvider.h` | 属性分组接口(QObject 声明属性所属分组)| | `PropertyEditor/source/src/Customization/PropertyTypeCustomization_ObjectDefault.cpp` | 默认 QObject 属性展开(含分组检测逻辑)| | `PropertyEditor/source/include/QQuickDetailsViewMananger.h` | 全局注册中心单例 | | `PropertyEditor/source/src/QQuickDetailsViewMananger.cpp` | 类型编辑器注册、自定义类型查找 | | `PropertyEditor/source/src/QQuickDetailsViewBasicTypeEditor.cpp` | 基本类型编辑器注册(int, float, QString, QColor 等)| | `PropertyEditor/source/src/QQuickDetailsViewPrivate.h` | 内部的 Model + Delegate 回调 | | `PropertyEditor/source/src/QQuickFunctionLibrary.cpp` | QML 辅助函数库 | | `include/propertyType/CustomType.h` | BayTemplate 自定义测试类型 | | `include/propertyType/CustomGadget.h` | BayTemplate 自定义测试 Gadget | | `include/CommonInclude.h` | Q_PROPERTY_VAR 宏 | | `source/propertyType/PropertyTypeCustomization_CustomType.cpp` | BayTemplate 对 QCustomType 的编辑器定制 | | `include/graphicsItem/graphicsBusSectionItem.h` | 唯一声明了 Q_PROPERTY 的图元(测试用途)|