在UE中开发编辑器插件时,通过USTRUCT的反射信息自动创建属性面板是非常方便提供配置化的方式,但经常会有一些特殊的属性面板定制需求,比如提供特殊的面板选项、根据参数的不同展示不同类型的值等。UE中的属性面板在HotPatcher和ResScannerUE中都有应用,能够非常方便地进行插件地配置与定制化。UE的官方文档:Details Panel Customization。
本篇文章从创建独立的Details
面板入手,通过ResScannerUE中的具体案例,提供属性面板和属性条目的自定义的实现方法。
属性面板StructureDetailsView
以ResScannerUE为例,启动插件就是创建了一个Dock,其中包含了一些基础操作的按钮和一个属性面板:
属性面板中展示的的元素,都是某一个USTRUCT结构的反射属性。以上述面板为例,他就是一个FScannerConfig
的结构:
1 | USTRUCT(BlueprintType) |
只需要这样定义一个结构,UE的属性面板就会把对应的类型、子结构成员都能创建出来,完全避免针对每个属性编写创建代码。创建方式也非常简单,只需要通过PropertyEditor
模块在某个Slate类的Construct
中创建一个SStructureDetailsView
。
首先需要在build.cs
中包含PropertyEditor
模块,然后就能通过PropertyEditor
进行创建了:
1 | // .h |
创建的面板需要绑定一个UStruct,用于获取该结构的反射信息,以及绑定一个结构实例,用于获取展示值以及存储值。
同时,可以通过设置FDetailsViewArgs
与FStructureDetailsViewArgs
的参数来控制属性面板的功能,比如是否能搜索属性名、滑动条等。
创建SettingsView
之后,显示在Slate控件中的方法:
1 | void SResScannerConfigPage::Construct(const FArguments& InArgs) |
其实就是把创建的SettingsView
的Widget放入一个容器中显示,该Widget就包含了绑定UStruct的所有可编辑的反射属性,如前文ResScannerUE的面板显示。
属性面板自定义
在前面创建SettingView的代码中,有这么一行:
1 | SettingsView->GetDetailsView()->RegisterInstancedCustomPropertyLayout(FScannerConfig::StaticStruct(),FOnGetDetailCustomizationInstance::CreateStatic(&FScannerSettingsDetails::MakeInstance)); |
这里是指定一个当前SettingsView的DetailCustomization类实例,用于在创建SettingView控件的时候进行定制化的操作,该类的声明如下:
1 |
|
在这个CustomizeDetails
接口中,就可以做一些定制化的操作,比如在属性面板中创建一个新的slate控件,以ResScannerUE中的Import
按钮为例:
1 | void FScannerSettingsDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) |
可以通过接口中的DetailBuilder
获取到当前SettingView绑定的结构实例,能够从配置中获取数据、执行操作。
上述代码是获取到了属性的Caregory
,在RulesTable
下创建一个CustomRow元素,在ValueContent()
可以直接写Slate代码来创建我们自定义的控件。
因为也能够直接获取绑定的结构体实例,所以控件中的实现也能够直接调用结构的函数,实现点击Import
按钮就能从Datatable
导入到配置中的效果。
面板中的属性自定义
上节提到了在属性面板中可以创建自定义的slate控件,实现定制行为。但是有一些需求,需要在面板中修改属性展示。
如我在做ResScannerUE中属性匹配的一个需求:
- 可以选择UClass类型
- 根据选择的UClass类型,列出可选属性的列表,并能够动态创建所选属性的值类型在面板中显示
- 属性名-值是一个数组,每个元素可以指定不同的属性名,并且创建出不同的值类型
我已经实现了,效果如下:
下面从实现上做介绍,如果只是在反射数据获取上,其实没难度。但是在属性面板上显示就要复杂一些。
- 根据UClass遍历所有的属性
- 得到FProperty就得到了当前属性的类型
- 把不同的FProperty在属性面板上创建出来
因为前文也提到了,属性面板是会绑定一个USTRUCT然后创建出属性,最开始我的思路是动态修改UStruct,但是发现不太合理,UStruct其实也是固定的,没办法做到一个数组中的元素类型不同。
那么,还有什么方法呢?经过查阅引擎的代码,发现UE也可以对某种类型的属性在属性面板做自定义的操作,核心就是IPropertyTypeCustomization
。
它的所用与IDetailCustomization
接口类似,都是提供一个自定义属性面板控件的接口,但是在创建形式上略有不同:
- DetailCustomization需要在创建SettingView时指定,而
PropertyTypeCustomization
不需要 - DetailCustomization只能在自己创建的SettingView中生效,而
PropertyTypeCustomization
在所有创建该属性的地方都能生效。
PropertyTypeCustomization
更灵活一些,在写完自定义事件之后,不管是在SettingView
还是在DataTable
创建出来的结构实例,都能正确地展示。
我在ResScannerUE中创建了一个结构FPropertyMatchMapping
,来存储属性名和值:
1 | USTRUCT(BlueprintType) |
PropertyName
和MatchValue
都是字符串,使用字符串的原因是,字符串可以存储任意类型的值。后续对属性自定义的操作,就是针对于FPropertyMatchMapping
这个结构类型。
所以,属性自定义的实现思路为:
- 创建一个自定义的
PropertyTypeCustomization
- 当属性面板创建时,替换掉原本的属性创建逻辑,使用
PropertyTypeCustomization
创建
先创建FPropertyMatchMapping
类型的PropertyTypeCustomization类:
1 | class FCustomPropertyMatchMappingDetails : public IPropertyTypeCustomization |
然后需要在模块启动时把该类注册进去:
1 | void FResScannerEditorModule::StartupModule() |
该步骤,就是告诉PropertyEditor
模块在属性面板中创建FPropertyMatchMapping
类型时,会创建出一个FCustomPropertyMatchMappingDetails
的实例来执行创建操作,这样就把我们想要指定的类的控件创建流程转移到了自己的代码中。
FCustomPropertyMatchMappingDetails
的实现:CustomizeHeader
函数中是可以直接创建Property-value
的,但是我在CustomizeHeader
传递进来的参数中,没找到方法获取到整个父结构实例,也就获取不到所选择的UClass
是什么,如果在创建不依赖上下文关系的属性-值中,可以直接在这里创建。
但是我要实现的依赖于父级结构所选择的UClass值,所以我只在CustomizeHeader
中创建了属性名,而值的控件没有创建:
1 | void FCustomPropertyMatchMappingDetails::CustomizeHeader(TSharedRef<IPropertyHandle> StructPropertyHandle, |
在CustomizeHeader
中获取并存储了FPropertyMatchMapping
实例的真实元素值(PropertyName/Value都是FString),该结构的元素都被放到了CustomizeChildren
中创建,因为在CustomizeChildren
传递进来的IDetailChildrenBuilder
中可以访问到父级结构,也就能获取到所选择的UClass:
1 | void FCustomPropertyMatchMappingDetails::CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, |
获取到UClass之后,就是获取所有的反射属性,并创建出一个SComboBox
:
1 |
|
其实也是通过Slate创建出一个SWidget,然后塞到由StructBuilder.AddCustomRow
创建的ValueContent容器中。
通过创建的SComboBox
,可以在编辑器中获取到当前ComboBox
中选择的属性名,通过UClass
/属性名
就能拿到该属性的FProperty,也就是属性的反射信息。所以,当ComboBox中选择的名字不一样,拿到的FProperty也不一样,其所代表的类型也不一样。
下一步的关键就是,通过得到的FProperty在属性面板中动态地创建出来应有的类型:
首先需要在CustomizeChildren
中创建存储值控件的容器:
1 | StructBuilder.AddCustomRow(FText::FromString(TEXT("PropertyValue"))) |
PropertyContent
是一个TSharedPtr<SBox>
,后面创建出来的控件,要通过它来展示。
回来继续说到根据FProperty创建其类型的值控件,在IDetailChildrenBuilder
中有一个函数AddExternalStructureProperty
,可以将一个外部结构的某个属性添加到当前的Details中。
以UTexture2D
的CompressionSettings
属性为例:
1 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Compression, AssetRegistrySearchable) |
它是一个枚举,我想要在一个不包含它的UStruct的属性面板中创建出来,并把它放到前面创建的PropertyContent
中显示:
1 | // Class is UTexture2D |
mStructBuilder
可以在CustomizeChildren
的参数中拿到并存储起来。
其实上面的代码就是实现根据FProperty
创建出对应值类型控件的关键部分,只需要通过UClass
和属性名,就能创建出对应的SWidget
,将其放入某个容器中即可显示出来!
根据ComboxBox获得选择的属性名,再根据属性名获取FProperty,再创建出Wiget的代码碍于篇幅这里就不完整贴出了,感兴趣的可以查看ResScannerUE的DetailCustomization/CustomPropertyMatchMappingDetails.cpp代码,具有完整的实现。
通用类型的数据存储
除了需要能够在面板中显示不同的属性,还需要用一种通用的方法把他们存储起来。
可以使用字符串来存储所有的类型值信息,UE中也提供了Value Widget
读取字符串值、从字符串设置值得方式:
1 | FString Value; |
通过这两个函数就能实现,所有类型的值都能通过字符串来存储和读取。