From 3d88a3615a035808582897a57157f92bb142c991 Mon Sep 17 00:00:00 2001 From: Jesse Qu Date: Wed, 20 May 2026 11:52:54 +0800 Subject: [PATCH] Add property grouping support to PropertyEditor Introduce IPropertyGroupProvider interface that lets QObjects declare which group each property belongs to. PropertyEditor then renders collapsible group headers in the tree view, with properties sorted under their respective groups. Key changes: - New IPropertyGroupProvider interface (getPropertyGroup method) - QDetailsViewRow_Group row type with bold group header rendering - isGroup model role and delegate property for styled group rows - TapHandler-based expand/collapse toggle in delegate template - One-time C++ auto-expand for groups in setObject() to avoid re-expansion loop with delegate recreation - Grouped property children added via LayoutBuilder::addGroup() - GraphicsBusSectionItem demonstrates grouping with 5 categories Co-Authored-By: Claude Opus 4.7 --- CLAUDE.md | 320 ++++++++++++++++++ .../source/include/IPropertyGroupProvider.h | 12 + .../include/QQuickDetailsViewLayoutBuilder.h | 1 + .../source/include/QQuickDetailsViewModel.h | 1 + .../source/include/QQuickDetailsViewRow.h | 9 + ...ropertyTypeCustomization_ObjectDefault.cpp | 60 +++- .../source/src/QQuickDetailsView.cpp | 21 +- .../src/QQuickDetailsViewLayoutBuilder.cpp | 9 +- .../source/src/QQuickDetailsViewModel.cpp | 10 +- .../source/src/QQuickDetailsViewPrivate.h | 4 + .../source/src/QQuickDetailsViewRow.cpp | 59 +++- include/graphicsItem/graphicsBaseItem.h | 6 +- include/graphicsItem/graphicsBusSectionItem.h | 2 + .../graphicsItem/graphicsBusSectionItem.cpp | 31 ++ 14 files changed, 532 insertions(+), 13 deletions(-) create mode 100644 PropertyEditor/source/include/IPropertyGroupProvider.h diff --git a/CLAUDE.md b/CLAUDE.md index f558add..30f7870 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -643,3 +643,323 @@ ItemControlHandle 实现操纵器模式,将抽象的变换操作(缩放、 | 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() + │ │ └─ 遍历 metaObject->propertyCount() + │ │ └─ 每个 QMetaProperty 创建一个子 QPropertyHandle + │ │ └─ 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, 不可见) + ├── child[0]: QDetailsViewRow_Property("Int") ← QMetaProperty + ├── child[1]: QDetailsViewRow_Property("Float") + ├── child[2]: QDetailsViewRow_Property("LimitedDouble") + ├── child[3]: QDetailsViewRow_Property("String") + ├── ... + ├── child[N]: QDetailsViewRow_Property("CustomType") ← IPropertyTypeCustomization + │ ├── child[0]: QDetailsViewRow_Property("ArraySize") + │ └── child[1]: QDetailsViewRow_Property("Array") + │ ├── child[0]: QDetailsViewRow_Property("[0]") + │ ├── child[1]: QDetailsViewRow_Property("[1]") + │ └── ... + └── child[N+1]: QDetailsViewRow_Property("CustomGadget") + └── child[0]: QDetailsViewRow_Property("LimitedDouble") + └── child[1]: QDetailsViewRow_Property("Desc") +``` + +#### 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 +``` + +#### 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)` — 添加自定义行 + - `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 等) — 这些是 PropertyEditor 示例代码中的测试属性 +- `GraphicsBaseItem`、`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) ← 值被拦截时通知编辑器回滚 +``` + +### 关键文件索引 + +| 文件 | 角色 | +|------|------| +| `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/src/Customization/PropertyTypeCustomization_ObjectDefault.cpp` | 默认 QObject 属性展开(遍历 QMetaProperty)| +| `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 的图元(测试用途)| diff --git a/PropertyEditor/source/include/IPropertyGroupProvider.h b/PropertyEditor/source/include/IPropertyGroupProvider.h new file mode 100644 index 0000000..2484a36 --- /dev/null +++ b/PropertyEditor/source/include/IPropertyGroupProvider.h @@ -0,0 +1,12 @@ +#ifndef IPropertyGroupProvider_h__ +#define IPropertyGroupProvider_h__ + +#include + +class IPropertyGroupProvider { +public: + virtual ~IPropertyGroupProvider() = default; + virtual QString getPropertyGroup(const QString& propertyName) const = 0; +}; + +#endif // IPropertyGroupProvider_h__ diff --git a/PropertyEditor/source/include/QQuickDetailsViewLayoutBuilder.h b/PropertyEditor/source/include/QQuickDetailsViewLayoutBuilder.h index e7c30da..39b036b 100644 --- a/PropertyEditor/source/include/QQuickDetailsViewLayoutBuilder.h +++ b/PropertyEditor/source/include/QQuickDetailsViewLayoutBuilder.h @@ -28,6 +28,7 @@ public: void addCustomRow(std::function inCustomRowCreator, QString inOverrideName = ""); void addProperty(QPropertyHandle* inPropertyHandle, QString inOverrideName = ""); + IDetailsViewRow* addGroup(const QString& groupName); void addObject(QObject* inObject); private: IDetailsViewRow* mRootRow = nullptr; diff --git a/PropertyEditor/source/include/QQuickDetailsViewModel.h b/PropertyEditor/source/include/QQuickDetailsViewModel.h index 43bf663..b54fa7e 100644 --- a/PropertyEditor/source/include/QQuickDetailsViewModel.h +++ b/PropertyEditor/source/include/QQuickDetailsViewModel.h @@ -15,6 +15,7 @@ class QDETAILS_VIEW_API QQuickDetailsViewModel : public QAbstractItemModel { Q_OBJECT enum Roles { name = 0, + isGroup, }; friend class IDetailsViewRow; public: diff --git a/PropertyEditor/source/include/QQuickDetailsViewRow.h b/PropertyEditor/source/include/QQuickDetailsViewRow.h index 405efc9..a867aca 100644 --- a/PropertyEditor/source/include/QQuickDetailsViewRow.h +++ b/PropertyEditor/source/include/QQuickDetailsViewRow.h @@ -22,6 +22,7 @@ public: virtual void setupItem(QQuickItem* inParent){} virtual void attachChildren() {} virtual void addChild(QSharedPointer inChild); + virtual bool isGroup() const { return false; } void clear(); QQuickDetailsViewModel* model(); @@ -32,6 +33,7 @@ public: return mParent->mChildren.indexOf(const_cast(this)); } void invalidateChildren(); + void notifyChildrenInserted(); protected: QString mName; QQuickDetailsViewModel* mModel = nullptr; @@ -63,4 +65,11 @@ private: std::function mRowCreator; }; +class QDETAILS_VIEW_API QDetailsViewRow_Group : public IDetailsViewRow { +public: + QDetailsViewRow_Group(const QString& groupName); + bool isGroup() const override { return true; } + void setupItem(QQuickItem* inParent) override; +}; + #endif // QQuickDetailsViewRow_h__ diff --git a/PropertyEditor/source/src/Customization/PropertyTypeCustomization_ObjectDefault.cpp b/PropertyEditor/source/src/Customization/PropertyTypeCustomization_ObjectDefault.cpp index 57ad671..3f0a164 100644 --- a/PropertyEditor/source/src/Customization/PropertyTypeCustomization_ObjectDefault.cpp +++ b/PropertyEditor/source/src/Customization/PropertyTypeCustomization_ObjectDefault.cpp @@ -1,13 +1,67 @@ #include "PropertyTypeCustomization_ObjectDefault.h" #include "QQuickDetailsViewLayoutBuilder.h" +#include "QQuickDetailsViewRow.h" #include "QPropertyHandle.h" +#include "IPropertyGroupProvider.h" void PropertyTypeCustomization_ObjectDefault::customizeChildren(QPropertyHandle* inPropertyHandle, QQuickDetailsViewLayoutBuilder* inBuilder) { auto objectHandle = inPropertyHandle->asObject(); const QMetaObject* metaObject = objectHandle->getMetaObject(); - if (objectHandle->isGadget()) { - for (int i = objectHandle->isGadget() ? 0 : 1; i < metaObject->propertyCount(); i++) { + + int startIndex = objectHandle->isGadget() ? 0 : 1; + + // Check if the object supports property grouping + IPropertyGroupProvider* groupProvider = nullptr; + if (!objectHandle->isGadget() && objectHandle->getObject()) { + groupProvider = dynamic_cast(objectHandle->getObject()); + } + + if (groupProvider) { + // Collect properties by group + QMap> groupedProps; + QList ungroupedProps; + for (int i = startIndex; i < metaObject->propertyCount(); i++) { + QMetaProperty prop = metaObject->property(i); + QString group = groupProvider->getPropertyGroup(prop.name()); + if (group.isEmpty()) { + ungroupedProps.append(prop); + } else { + groupedProps[group].append(prop); + } + } + + auto addProp = [&](QQuickDetailsViewLayoutBuilder* builder, QMetaProperty prop) { + builder->addProperty( + inPropertyHandle->findOrCreateChild( + prop.metaType(), + prop.name(), + [this, prop, objectHandle]() { + return prop.read(objectHandle->getObject()); + }, + [this, prop, objectHandle, inPropertyHandle](QVariant var) { + prop.write(objectHandle->getObject(), var); + } + ), + prop.name() + ); + }; + + // Add grouped properties + for (auto it = groupedProps.begin(); it != groupedProps.end(); ++it) { + IDetailsViewRow* groupRow = inBuilder->addGroup(it.key()); + QQuickDetailsViewLayoutBuilder groupBuilder(groupRow); + for (const auto& prop : it.value()) { + addProp(&groupBuilder, prop); + } + } + + // Add ungrouped properties directly + for (const auto& prop : ungroupedProps) { + addProp(inBuilder, prop); + } + } else if (objectHandle->isGadget()) { + for (int i = startIndex; i < metaObject->propertyCount(); i++) { QMetaProperty prop = metaObject->property(i); QString propName = prop.name(); inBuilder->addProperty( @@ -29,7 +83,7 @@ void PropertyTypeCustomization_ObjectDefault::customizeChildren(QPropertyHandle* } else { if (objectHandle->getObject() != nullptr) { - for (int i = objectHandle->isGadget() ? 0 : 1; i < metaObject->propertyCount(); i++) { + for (int i = startIndex; i < metaObject->propertyCount(); i++) { QMetaProperty prop = metaObject->property(i); QString propName = prop.name(); inBuilder->addProperty( diff --git a/PropertyEditor/source/src/QQuickDetailsView.cpp b/PropertyEditor/source/src/QQuickDetailsView.cpp index 2e3e517..009bb45 100644 --- a/PropertyEditor/source/src/QQuickDetailsView.cpp +++ b/PropertyEditor/source/src/QQuickDetailsView.cpp @@ -1,4 +1,4 @@ -#include "QQuickDetailsView.h" +#include "QQuickDetailsView.h" #include "private/qqmldata_p.h" #include @@ -13,7 +13,7 @@ void QQuickDetailsViewPrivate::initItemCallback(int serializedModelIndex, QObjec auto item = qobject_cast(object); if (!item) return; - const QModelIndex& index = m_treeModelToTableModel.mapToModel(serializedModelIndex);; + const QModelIndex& index = m_treeModelToTableModel.mapToModel(serializedModelIndex); IDetailsViewRow* node = static_cast(index.internalPointer()); node->setupItem(item); } @@ -44,6 +44,20 @@ void QQuickDetailsView::setObject(QObject* inObject) if (inObject != d_func()->mModel->getObject()) { d_func()->mModel->setObject(inObject); Q_EMIT asObjectChanged(inObject); + + // Auto-expand group rows (once, C++ side — independent of delegate lifecycle) + Q_D(QQuickDetailsView); + QAbstractItemModel* m = d->mModel; + for (int i = 0; i < m->rowCount(); ++i) { + QModelIndex idx = m->index(i, 0); + IDetailsViewRow* node = static_cast(idx.internalPointer()); + if (node && node->isGroup()) { + int row = d->m_treeModelToTableModel.itemIndex(idx); + if (row >= 0) { + expand(row); + } + } + } } } @@ -72,9 +86,10 @@ void QQuickDetailsView::componentComplete() required property bool expanded required property int hasChildren required property int depth + required property bool isGroup implicitWidth: detailsView.width implicitHeight: heightProxy ? heightProxy.height + 5 : 30 - TapHandler { + TapHandler { onTapped: detailsView.toggleExpanded(row) } onImplicitHeightChanged: { diff --git a/PropertyEditor/source/src/QQuickDetailsViewLayoutBuilder.cpp b/PropertyEditor/source/src/QQuickDetailsViewLayoutBuilder.cpp index 56a4169..41ed1b1 100644 --- a/PropertyEditor/source/src/QQuickDetailsViewLayoutBuilder.cpp +++ b/PropertyEditor/source/src/QQuickDetailsViewLayoutBuilder.cpp @@ -51,7 +51,7 @@ QPair QQuickDetailsViewRowBuilder::makeNameValueSlot() Item{ id: nameEditorContent height: parent.height - anchors.left: parent.left + anchors.left: parent.left anchors.leftMargin: padding + (detailsDelegate.isTreeNode ? (detailsDelegate.depth + 1) * detailsDelegate.indent : 0) anchors.right: splitter.left anchors.verticalCenter: parent.verticalCenter @@ -210,6 +210,13 @@ void QQuickDetailsViewLayoutBuilder::addProperty(QPropertyHandle* inPropertyHand child->attachChildren(); } +IDetailsViewRow* QQuickDetailsViewLayoutBuilder::addGroup(const QString& groupName) +{ + auto child = QSharedPointer::create(groupName); + mRootRow->addChild(child); + return child.data(); +} + void QQuickDetailsViewLayoutBuilder::addObject(QObject* inObject) { addProperty(QPropertyHandle::FindOrCreate(inObject)); diff --git a/PropertyEditor/source/src/QQuickDetailsViewModel.cpp b/PropertyEditor/source/src/QQuickDetailsViewModel.cpp index 022c399..0b3e515 100644 --- a/PropertyEditor/source/src/QQuickDetailsViewModel.cpp +++ b/PropertyEditor/source/src/QQuickDetailsViewModel.cpp @@ -13,7 +13,14 @@ QVariant QQuickDetailsViewModel::data(const QModelIndex& index, int role) const if (!index.isValid()) return QVariant(); IDetailsViewRow* node = static_cast(index.internalPointer()); - return node->name(); + switch (role) { + case Roles::name: + return node->name(); + case Roles::isGroup: + return node->isGroup(); + default: + return QVariant(); + } } Qt::ItemFlags QQuickDetailsViewModel::flags(const QModelIndex& index) const { @@ -133,5 +140,6 @@ int QQuickDetailsViewModel::columnCount(const QModelIndex& parent) const { QHash QQuickDetailsViewModel::roleNames() const { return { { Roles::name,"name" }, + { Roles::isGroup,"isGroup" }, }; } diff --git a/PropertyEditor/source/src/QQuickDetailsViewPrivate.h b/PropertyEditor/source/src/QQuickDetailsViewPrivate.h index a0efd06..3826630 100644 --- a/PropertyEditor/source/src/QQuickDetailsViewPrivate.h +++ b/PropertyEditor/source/src/QQuickDetailsViewPrivate.h @@ -5,6 +5,7 @@ #include "QQuickTreeViewExPrivate.h" #include "QQuickDetailsView.h" #include "QQuickDetailsViewModel.h" +#include "QQuickDetailsViewRow.h" class QQuickDetailsViewPrivate : public QQuickTreeViewExPrivate { @@ -36,6 +37,9 @@ void QQuickDetailsViewPrivate::updateRequiredProperties(int serializedModelIndex setRequiredProperty("hasChildren", m_treeModelToTableModel.hasChildren(row), serializedModelIndex, object, init); setRequiredProperty("expanded", q->isExpanded(row), serializedModelIndex, object, init); setRequiredProperty("depth", m_treeModelToTableModel.depthAtRow(row), serializedModelIndex, object, init); + const QModelIndex& index = m_treeModelToTableModel.mapToModel(serializedModelIndex); + IDetailsViewRow* node = static_cast(index.internalPointer()); + setRequiredProperty("isGroup", node->isGroup(), serializedModelIndex, object, init); } #endif // QQuickDetailsViewPrivate_h__ \ No newline at end of file diff --git a/PropertyEditor/source/src/QQuickDetailsViewRow.cpp b/PropertyEditor/source/src/QQuickDetailsViewRow.cpp index a537b42..1723922 100644 --- a/PropertyEditor/source/src/QQuickDetailsViewRow.cpp +++ b/PropertyEditor/source/src/QQuickDetailsViewRow.cpp @@ -30,6 +30,24 @@ QQuickDetailsViewModel* IDetailsViewRow::model() return mModel; } +void IDetailsViewRow::notifyChildrenInserted() +{ + if (!mModel) + return; + const int count = mChildren.size(); + if (count == 0) + return; + QModelIndex parentIndex = mModel->indexForRow(this); + if (!parentIndex.isValid()) + parentIndex = QModelIndex(); + mModel->beginInsertRows(parentIndex, 0, count - 1); + for (int i = 0; i < count; ++i) { + mChildren[i]->mParent = this; + mChildren[i]->mModel = mModel; + } + mModel->endInsertRows(); +} + void IDetailsViewRow::invalidateChildren() { if (!mModel) { @@ -39,17 +57,17 @@ void IDetailsViewRow::invalidateChildren() QModelIndex parentIndex = mModel->indexForRow(this); if (!parentIndex.isValid()) { - parentIndex = QModelIndex(); + parentIndex = QModelIndex(); } const int oldChildCount = mChildren.size(); if (oldChildCount > 0) { mModel->beginRemoveRows(parentIndex, 0, oldChildCount - 1); - mChildren.clear(); - mModel->endRemoveRows(); + mChildren.clear(); + mModel->endRemoveRows(); } - attachChildren(); + attachChildren(); const int newChildCount = mChildren.size(); if (newChildCount > 0) { @@ -116,3 +134,36 @@ void QDetailsViewRow_Custom::setupItem(QQuickItem* inParent) mRowCreator(&builder); } } + +QDetailsViewRow_Group::QDetailsViewRow_Group(const QString& groupName) +{ + setName(groupName); +} + +void QDetailsViewRow_Group::setupItem(QQuickItem* inParent) +{ + QQuickDetailsViewRowBuilder builder(this, inParent); + auto slotPair = builder.makeNameValueSlot(); + QQuickItem* groupLabel = builder.setupItem(slotPair.first, R"( + import QtQuick; + import QtQuick.Controls; + import ColorPalette; + Item{ + property string lableText + implicitHeight: 25 + width: parent.width + anchors.verticalCenter: parent.verticalCenter + Text { + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + clip: true + elide: Text.ElideRight + text: lableText + font.bold: true + color: ColorPalette.theme.labelPrimary + } + } + )"); + groupLabel->setProperty("lableText", mName); + builder.setHeightProxy(groupLabel); +} diff --git a/include/graphicsItem/graphicsBaseItem.h b/include/graphicsItem/graphicsBaseItem.h index 59cd7ea..24add1d 100644 --- a/include/graphicsItem/graphicsBaseItem.h +++ b/include/graphicsItem/graphicsBaseItem.h @@ -2,6 +2,7 @@ #define GRAPHICSBASEITEM_H #include "itemControlHandle.h" +#include "IPropertyGroupProvider.h" #include #include @@ -291,7 +292,7 @@ protected: typedef AbstractShapeType AbstractShape; -class GraphicsBaseItem : public QObject, public AbstractShapeType +class GraphicsBaseItem : public QObject, public AbstractShapeType, public IPropertyGroupProvider { Q_OBJECT @@ -313,6 +314,9 @@ public: GraphicsBaseItem(QGraphicsItem *parent); virtual ~GraphicsBaseItem(); + // IPropertyGroupProvider - 默认返回空字符串(无分组),子类可重写 + QString getPropertyGroup(const QString&) const override { return {}; } + virtual void createOperationCopy(); virtual void removeOperationCopy(); virtual void moveOperationCopy(const QPointF&); diff --git a/include/graphicsItem/graphicsBusSectionItem.h b/include/graphicsItem/graphicsBusSectionItem.h index 1c16994..8c5df3b 100644 --- a/include/graphicsItem/graphicsBusSectionItem.h +++ b/include/graphicsItem/graphicsBusSectionItem.h @@ -28,6 +28,8 @@ public: void move(const QPointF&) override; void editShape(int, const QPointF&) override; + QString getPropertyGroup(const QString& propName) const override; + public: enum QCustomEnum { One, diff --git a/source/graphicsItem/graphicsBusSectionItem.cpp b/source/graphicsItem/graphicsBusSectionItem.cpp index ee79cef..cab47d1 100644 --- a/source/graphicsItem/graphicsBusSectionItem.cpp +++ b/source/graphicsItem/graphicsBusSectionItem.cpp @@ -18,6 +18,37 @@ GraphicsBusSectionItem::~GraphicsBusSectionItem() { } +QString GraphicsBusSectionItem::getPropertyGroup(const QString& propName) const +{ + // 数值类型属性 + if (propName == "Int" || propName == "Float" || propName == "LimitedDouble") { + return QString::fromWCharArray(L"数值"); + } + // 字符串和路径 + if (propName == "String" || propName == "Directory") { + return QString::fromWCharArray(L"文本"); + } + // 向量和矩阵 + if (propName == "Vec2" || propName == "Vec3" || propName == "Vec4" || propName == "Mat4") { + return QString::fromWCharArray(L"向量/矩阵"); + } + // 颜色属性 + if (propName == "Color" || propName == "ColorList" || propName == "ColorMap") { + return QString::fromWCharArray(L"颜色"); + } + // 枚举和自定义类型 + if (propName == "CustomEnum" || propName == "CustomType" || propName == "CustomGadget" + || propName == "CustomGadgetPtr" || propName == "CustomGadgetSharedPtr") { + return QString::fromWCharArray(L"自定义类型"); + } + // 内部属性不分组 + if (propName == "m_lastBoudingRectF" || propName == "m_lastBoudingPointF" + || propName == "m_dRatioX" || propName == "m_dRatioY") { + return {}; + } + return {}; +} + QPainterPath GraphicsBusSectionItem::shape() { QPainterPath path;