52 KiB
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 (
QGraphicsViewsubclass): Custom view with zoom (0.02-50x), pan (middle-button drag), checkerboard background - DesignerScene (
QGraphicsScenesubclass): 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
QGraphicsSceneand displays properties of selected items
6. Command Pattern (Undo/Redo)
- QUndoStack (
m_pUndoStackin 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
-
User drags from 图元面板 →
GraphicElementsPanelemits signal →DesignerScene::signalAddItem()→ creates item withAddItemCommand -
User clicks on canvas →
DesignerView→DesignerScene→SelectorManager::getWorkingSelector()→ selector processes event -
User selects items →
DesignerScene::selectionChanged()→CMainWindow::onSignal_selectionChanged()→ updates property editor -
User modifies property in 属性编辑器 →
QDetailsViewupdates 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
- QtADS (
QtADS/subdirectory, v4.3.1): Advanced docking system - must be present as subdirectory - 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
-
CMAKE_AUTOUIC_SEARCH_PATHS: Set to
"ui"in CMakeLists.txt because .ui files are in separate directory from header files -
Group Flattening: When creating a group that contains existing groups, the scene flattens nested groups first to prevent group nesting (see
DesignerScene::createGroup()) -
Zoom Implementation: Uses
QGraphicsView::zoom()with smooth factorqPow(1.0015, angleDelta.y())per wheel event -
Wchar String Conversion: Uses
QString::fromWCharArray(L"中文")for Chinese UI strings -
Resource File:
resource/BayTemplate.qrccontains 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 工作机制:
AbstractShapeType::updateHandles()根据 boundingRect 计算 handle 位置- Handle 位置在 boundingRect 外扩 5px (nMargin = 5)
collidesWithHandle()检测鼠标点击在哪个 handle 上- 不同操作模式对 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 类职责
-
序列化/反序列化(JSON 格式)
serialize()/deserialize(): 核心序列化为 QByteArraysaveToFile()/loadFromFile(): 文件 I/O 封装- 递归序列化/反序列化图元层次结构(支持组图元嵌套)
-
状态管理
m_filename: 文档文件名(空表示未保存的新文档)m_modified: 是否被修改(通过setModified()和modifiedChanged信号通知)m_created/m_modifiedTime/m_lastSavedTime: 时间戳m_metaData: 可扩展的自定义元数据 (QMap<QString, QVariant>)
-
与 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)
移动、旋转、缩放都使用"操作副本"作为视觉预览:
- mousePressEvent: 创建虚线副本
- mouseMoveEvent: 移动副本(本体不动)
- 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 包装器)
// source/PropertyEditor/source/src/QDetailsView.cpp
class QDetailsView : public QWidget {
QQuickWidget* mQuickWidget; // 嵌入 QML 引擎
QQuickDetailsView* mQuickDetailsView; // QML 端的 DetailsView 对象
void setObject(QObject* inObject); // 关键入口:设置被检查的对象
};
2. QQuickDetailsViewModel(QAbstractItemModel 的树形实现)
// 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
- 有 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 的isGrouprole 暴露给 QML delegatesetupItem()使用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_Matrix4x4:4×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 - 内置注册的类型自定义:
QMatrix4x4→PropertyTypeCustomization_Matrix4x4 - QML 模块注册:
DetailsView(1.0),ColorPalette(单例) getCustomPropertyType()查找逻辑:- Sequential → PropertyTypeCustomization_Sequential
- Associative → PropertyTypeCustomization_Associative
- Object → 查找 mClassCustomizationMap(精确匹配 + 继承匹配),fallback 到 ObjectDefault
- 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 对:
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接口,默认返回空字符串(无分组),子类可重写GraphicsRectItem、GraphicsPolygonItem、GraphicsItemGroup:无 Q_PROPERTY 声明 — 因此属性编辑器不会显示任何属性行
BayTemplate 自定义类型:
QCustomType(结构体):{ unsigned int ArraySize; QVector<int> Array; }— 通过PropertyTypeCustomization_CustomType定制,header 显示 Sort 按钮,children 展开为 ArraySize + ArrayQCustomGadget(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 的图元(测试用途) |