BayTemplate/CLAUDE.md

52 KiB
Raw Blame History

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 进行配置和构建:

# 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: DesignerSceneSelectorManager::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 canvasDesignerViewDesignerSceneSelectorManager::getWorkingSelector() → selector processes event

  3. User selects itemsDesignerScene::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<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 共享):

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 类型枚举:

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模式

移动和旋转操作使用"预览副本"技术,提供视觉反馈:

// 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 模板)

// 使用模板继承,允许继承 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 实现:

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() 实现组扁平化:

// 如果选中的 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

{
    "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 子类代表不同的状态:

// 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. QDetailsViewC++ 侧的 QWidget 包装器)

// source/PropertyEditor/source/src/QDetailsView.cpp
class QDetailsView : public QWidget {
    QQuickWidget* mQuickWidget;          // 嵌入 QML 引擎
    QQuickDetailsView* mQuickDetailsView; // QML 端的 DetailsView 对象
    void setObject(QObject* inObject);   // 关键入口:设置被检查的对象
};

2. QQuickDetailsViewModelQAbstractItemModel 的树形实现)

// source/PropertyEditor/source/src/QQuickDetailsViewModel.cpp
class QQuickDetailsViewModel : public QAbstractItemModel {
    QSharedPointer<QDetailsViewRow_Property> 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属性操作的统一入口

// 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<QVariant()>;
    using Setter = std::function<void(QVariant)>;
    
    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<IPropertyHandleImpl> 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
    • 有 metaObjectQObject 或 QSharedPointer→ Object
    • 否则 → RawType基本类型
  • findOrCreateChild() — 为 Object 类型的属性创建子 Handle自动连接到父 Handle 的 asVarChanged 信号以触发子项刷新

4. IPropertyHandleImpl编辑器创建策略

IPropertyHandleImpl
  ├── QPropertyHandleImpl_RawType  → 调用 QQuickDetailsViewManager::createValueEditor() 
  │                                  查找已注册的 TypeEditor
  ├── QPropertyHandleImpl_Enum     → 创建 ComboBox 选择器
  ├── QPropertyHandleImpl_Object   → createValueEditor() 返回 nullptrObject 类型本身不显示值编辑器)
  │   ├── 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() 返回 truesetupItem() 渲染粗体分组标题 + 折叠时显示子项计数 "(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类型自定义扩展接口

class IPropertyTypeCustomization {
    virtual void customizeHeaderRow(QPropertyHandle*, QQuickDetailsViewRowBuilder*);
    virtual void customizeChildren(QPropertyHandle*, QQuickDetailsViewLayoutBuilder*);
};
  • customizeHeaderRow: 自定义该属性行本身的 UI替换默认的 name + value editor
  • customizeChildren: 自定义该属性展开后的子行结构
  • 注册方式:QQuickDetailsViewManager::Get()->registerPropertyTypeCustomization<QCustomType, MyCustomization>()

内置 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_Matrix4x44×4 矩阵编辑器

7. QQuickDetailsViewManager全局注册中心Singleton

QHash<const QMetaObject*, PropertyTypeCustomizationCreator> mClassCustomizationMap;   // QObject 类型 → 自定义
QHash<QMetaType, PropertyTypeCustomizationCreator> mMetaTypeCustomizationMap;         // 值类型 → 自定义
QHash<QMetaType, TypeEditorCreator> mTypeEditorCreatorMap;                            // 基本类型 → 编辑器 QML 组件
  • 内置注册的基本类型编辑器:int/unsigned int/size_t, float, double, QString, QVector2D/3D/4D, QColor, QDir
  • 内置注册的类型自定义:QMatrix4x4PropertyTypeCustomization_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 / QQuickDetailsViewLayoutBuilderQML 布局构建器)

  • 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 对:

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 项目封装)

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

// 1. 注册自定义类型编辑器
QQuickDetailsViewManager::Get()->registerPropertyTypeCustomization<QCustomType, PropertyTypeCustomization_CustomType>();

// 2. 创建 QDetailsView 并挂载到右侧 Dock
m_pPropertiesEditorView = new QDetailsView();
m_pPropertiesEditorView->setObject(m_pDrawingPanel->getQGraphicsScene());

选择变化处理source/mainwindow.cpp:226-235

void CMainWindow::onSignal_selectionChanged() {
    QList<QGraphicsItem*> selectedItems = scene->selectedItems();
    if (selectedItems.count() != 1) {
        m_pPropertiesEditorView->setObject(scene);   // 多选/无选 → 显示场景属性
        return;
    }
    m_pPropertiesEditorView->setObject(static_cast<QObject*>(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 接口,默认返回空字符串(无分组),子类可重写
  • GraphicsRectItemGraphicsPolygonItemGraphicsItemGroup无 Q_PROPERTY 声明 — 因此属性编辑器不会显示任何属性行

BayTemplate 自定义类型

  • QCustomType (结构体){ unsigned int ArraySize; QVector<int> 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 接口

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

// 在 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

// 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,默认返回空字符串(无分组):

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 的图元(测试用途)