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;