前言
UE4的细节面板(DetailViews)指的是展示UObject对象属性的面板,如下所示:
UE4提供了一套针对UObject的细节面板生成机制,能将一个UObject的各种属性轻松的生成对应的细节面板
UObject对象细节面板(DetailsView)
UObject细节面板(DetailsView)的构成
上面是StaticMeshComponent的细节面板,其自身就是SDetailsView, 为SCompountWidget组件。
SDetailsView自身是由UObject的属性生成,类别名其实就是UObject的UPEROPRTY(Category = "xxx").
而根据UObject的变量类别的不同,最终生成的控件就不同。
具体可以在PropertyEditorHelpers.cpp看到所有的UObject属性反射生成的SWidget类型
UObject细节面板(IDetailsView)的创建
用IDetailsView来创建细节面板,IDetailsView本身也是SCompoundWidget
TSharedPtr<IDetailsView> DetailsView;
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsViewArgs(false, false, false, FDetailsViewArgs::HideNameArea);
DetailsView = PropertyEditorModule.CreateDetailView(DetailsViewArgs);
FTestDetailViewEdMode* TestEdMode = GetEditorMode();
if (nullptr != TestEdMode)
{
DetailsView->SetObject(TestEdMode->MyObject);
}
这里的MyObject对象为
UCLASS()
class TESTDETAILVIEW_API UMyObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Test")
float a;
UPROPERTY(EditAnywhere, Category = "Test")
UStaticMesh* Mesh;
UPROPERTY(EditAnywhere, Category = "Test")
int c;
};
细节面板(IDetailsView)的定制
由UObject生成的DetailsView是各种可调节值的控件的组合, 也就是SPropertyEditorXXX类。但有时候,我们为了功能需求想进一步定制DetailsView,比如在XXX类别下加个SButton之类。想要进一步定制DetailsView,得继承IDetailCustomization,并在CustomizeDetails中增加控件。
继承IDetailCustomization
class MyObjectDetailCustom : public IDetailCustomization
{
public:
static TSharedRef<IDetailCustomization> MakeInstance();
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
};
CustomizeDetails,EditCategory,AddCustomRow
在void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)函数中增加需要的控件,IDetailLayoutBuilder接口提供了编辑类别EditCategory可以返回IDetailCategoryBuilder, 而IDetailCategoryBuilder的AddCustomRow可以给类别增加自定义的一行内容
TSharedRef<IDetailCustomization> MyObjectDetailCustom::MakeInstance()
{
return MakeShareable(new MyObjectDetailCustom);
}
void MyObjectDetailCustom::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
IDetailCategoryBuilder& aaa = DetailBuilder.EditCategory(FName("aaa"));
aaa.AddCustomRow(FText::GetEmpty())
.NameContent()
[
SNew(STextBlock)
.Text(FText::FromString("sss"))
]
.ValueContent()
[
SNew(SButton)
.Text(FText::FromString("abc"))
];
}
当然要让CustomizeDetails起效果,还得在开始模块进行CustomizeDetails的注册
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditorModule.RegisterCustomClassLayout(FName("MyObject"), FOnGetDetailCustomizationInstance::CreateStatic(&MyObjectDetailCustom::MakeInstance));
退出模块记得注销CustomizeDetails
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditorModule.UnregisterCustomClassLayout(FName("MyObject"));
效果如下:
其实CustomizeDetails是SDetailsView的SetObject调用的。
细节面板(DetailsView)的刷新
SDetailsView的接口SetObject不仅仅用于生成UObject对应的细节面板,当SetObject(XXXUObject, true)指定true的时候可以重新刷新界面
细节面板(DetailsView)控制UObject的某些属性是否显示
有时候细节面板存在太多的属性,导致界面过于复杂,我们希望某些属性在某些情况下不显示或者显示,SDetailsView提供了
void SDetailsViewBase::SetIsPropertyVisibleDelegate(FIsPropertyVisible InIsPropertyVisible) 接口
/** Delegate called to see if a property should be visible */
DECLARE_DELEGATE_RetVal_OneParam( bool, FIsPropertyVisible, const FPropertyAndParent& );
例子:
UCLASS()
class TESTDETAILVIEW_API UMyObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Test", meta = (TestShow = "111"))
float a;
UPROPERTY(EditAnywhere, Category = "Test")
UStaticMesh* Mesh;
UPROPERTY(EditAnywhere, Category = "Test", meta = (TestShow = "222"))
int c;
};
这里利用属性的Meta数据作为属性显示和不显示的判断依据(实际上编辑器细节面板开发控制属性显示不显示经常用到UProperty的MetaData)
DetailsView->SetIsPropertyVisibleDelegate(FIsPropertyVisible::CreateSP(this, &SMyCompoundWidget::IsPropertyVisible));
bool SMyCompoundWidget::IsPropertyVisible(const FPropertyAndParent& PropertyAndParent)
{
const UProperty& Property = PropertyAndParent.Property;
static const FName MetaName = "TestShow";
if (Property.HasMetaData(MetaName))
{
TArray<FString> MetaStrs;
Property.GetMetaData(MetaName).ParseIntoArray(MetaStrs, TEXT(","), true);
if(MetaStrs.Contains("222"))
return true;
}
return false;
}
显示结果:
定制细节面板(CustomizeDetails)控制类别下添加的行(FDetailWidgetRow)是否显示
可以直接用FDetailWidgetRow的Visibility
aaa.AddCustomRow(FText::GetEmpty())
.Visibility(xxxxxxxx)
.ValueContent()
[
SNew(SButton)
.Text(FText::FromString("abc"))
];
细节面板添加额外External对象显示(IDetailLayoutBuilder)
CustomizeDetails支持额外添加 本UObject默认未显示的属性|额外的结构体|额外的UObject|额外结构体的某个属性 这些在UI反射机制下生成的UI都可以添加到细节面板
API在 DetailCategoryBuilder.h 的 AddProperty 和 AddExternalXXX等接口
看案例:
UObject和UStruct代码
USTRUCT()
struct FTestData
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Asset")
int32 A;
UPROPERTY(EditAnywhere, Category = "Asset")
UStaticMesh* Mesh;
};
UCLASS()
class UTestOtherObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
FFilePath Path;
};
UCLASS()
class UTestObject : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
FString Name;
UPROPERTY()
FTestData TestData;
UPROPERTY()
FTestData SecondTestData;
UPROPERTY()
float S;
};
CustomDetail代码
void FTestObjectDetailCustom::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
IDetailCategoryBuilder& Test = DetailBuilder.EditCategory(FName("Test"));
TArray<TWeakObjectPtr<UObject>> SelectObjects;
SelectObjects = DetailBuilder.GetSelectedObjects();
for(auto & Object : SelectObjects)
{
if(Object.IsValid() && Object->GetClass()->IsChildOf(UTestObject::StaticClass()))
{
TestObject = Cast<UTestObject>(Object.Get());
}
}
if(!TestObject.IsValid())
return;
OtherObject = NewObject<UTestOtherObject>();
OtherObject->AddToRoot();
TSharedRef<IPropertyHandle> SProperty = DetailBuilder.GetProperty(GET_MEMBER_NAME_CHECKED(UTestObject, S));
Test.AddProperty(SProperty);
Test.AddExternalObjects({OtherObject});
TSharedPtr<FStructOnScope> StructToDisplay = MakeShareable(new FStructOnScope(FTestData::StaticStruct(), (uint8*)&TestObject->TestData));
IDetailPropertyRow* Row = Test.AddExternalStructure(StructToDisplay);
Row->DisplayName(FText::FromString("TesStruct"));
TSharedPtr<FStructOnScope> SecondStructToDisplay = MakeShareable(new FStructOnScope(FTestData::StaticStruct(), (uint8*)&TestObject->TestData));
Test.AddExternalStructureProperty(SecondStructToDisplay, "A");
// Test.AddExternalObjectProperty() 使用与AddExternalStructureProperty类似
}
显示效果
AddProperty的Property也得打上EditAnywhere等标签才能显示,不能作为特殊技巧绕过metadata的控制。
UStruct对象的细节面板
前面说到的是针对UObject对象的细节面板定制, 实际上UStruct对象也是可以定制细节面板。一个非常经典的例子就是FColor(颜色)或者FCurve(曲线),他们在细节面板暴露的UI就比较特别,就是通过UStruct DetailPanel来定制的。
拿下面自定的UStuct为例子:
USTRUCT()
struct FTestData
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, Category = "Asset")
int32 A;
UPROPERTY(EditAnywhere, Category = "Asset")
UStaticMesh* Mesh;
};
UStruct Customization定制类声明和定义
class FTestDataCustomization : public IPropertyTypeCustomization
{
public:
static TSharedRef<IPropertyTypeCustomization> MakeInstance()
{
return MakeShareable(new FTestDataCustomization());
}
/**
* Destructor
*/
virtual ~FTestDataCustomization()
{
}
public:
// IDetailCustomization interface
virtual void CustomizeHeader(TSharedRef<IPropertyHandle> InStructPropertyHandle, FDetailWidgetRow& InHeaderRow, IPropertyTypeCustomizationUtils& InStructCustomizationUtils) override;
virtual void CustomizeChildren(TSharedRef<IPropertyHandle> InStructPropertyHandle, IDetailChildrenBuilder& InChildBuilder, IPropertyTypeCustomizationUtils& InStructCustomizationUtils) override;
private:
TSharedPtr<IPropertyHandle> APropertyHandle;
TSharedPtr<IPropertyHandle> MeshPropertyHandle;
void FTestDataCustomization::CustomizeHeader(TSharedRef<IPropertyHandle> InStructPropertyHandle, FDetailWidgetRow& InHeaderRow, IPropertyTypeCustomizationUtils& InStructCustomizationUtils)
{
// 获取本UStruct对象所在的UObject对象
const TArray<TWeakObjectPtr<UObject>>& SelectedObjects = InStructCustomizationUtils.GetPropertyUtilities()->GetSelectedObjects();
if(SelectedObjects.Num() > 0 && SelectedObjects[0].IsValid())
{
}
// 获取本Struct对象
void* ValuePtr;
InStructPropertyHandle->GetValueData(ValuePtr);
FTestData* Test = reinterpret_cast<FTestData*>(ValuePtr);
if(Test)
{
}
APropertyHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTestData, A));
MeshPropertyHandle = InStructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FTestData, Mesh));
InHeaderRow.WholeRowContent()
.HAlign(HAlign_Center)
[
SNew(SVerticalBox)
+SVerticalBox::Slot()
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
[
APropertyHandle->CreatePropertyNameWidget()
]
+SHorizontalBox::Slot()
[
APropertyHandle->CreatePropertyValueWidget()
]
]
+SVerticalBox::Slot()
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
[
MeshPropertyHandle->CreatePropertyNameWidget()
]
+SHorizontalBox::Slot()
[
MeshPropertyHandle->CreatePropertyValueWidget()
]
]
+SVerticalBox::Slot()
[
SNew(SButton)
.Text(FText::FromString("Hello World"))
]
];
}
void FTestDataCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> InStructPropertyHandle, IDetailChildrenBuilder& InChildBuilder, IPropertyTypeCustomizationUtils& InStructCustomizationUtils)
{
}
步骤和UObjectDetail极其类似。
UStruct Customization定制类注册(在模块启动)
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomPropertyTypeLayout( "TestData", FOnGetPropertyTypeCustomizationInstance::CreateStatic( &FTestDataCustomization::MakeInstance ) );
退出模块记得注销
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomPropertyTypeLayout( "TestData");
UStruct细节面板(IStructureDetailsView)的创建
FTestData TestData;
TSharedPtr<class IStructureDetailsView> StructureDetailsView;
TSharedPtr<FStructOnScope> StructToDisplay = MakeShareable(new FStructOnScope(FTestData::StaticStruct(), (uint8*)&TestData));
FStructureDetailsViewArgs StructureViewArgs;
StructureViewArgs.bShowObjects = true;
StructureViewArgs.bShowAssets = true;
StructureViewArgs.bShowClasses = true;
StructureViewArgs.bShowInterfaces = true;
FDetailsViewArgs ViewArgs;
ViewArgs.bAllowSearch = false;
ViewArgs.bHideSelectionTip = false;
ViewArgs.bShowActorLabel = false;
// Create a property view
FPropertyEditorModule& EditModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
StructureDetailsView = EditModule.CreateStructureDetailView(ViewArgs, StructureViewArgs, StructToDisplay, LOCTEXT("Struct", "Struct View"));
return SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
// Put your tab content here!
SNew(SBox)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
StructureDetailsView->GetWidget().ToSharedRef()
]
];
UStruct反射生成的UI添加到IDetailLayoutBuilder的坑
UStruct反射生成的UI不能直接通过手动生成IStructureDetailsView方式挂载到UObject 定制UI的IDetailLayoutBuilder,需要通过AddExternalStructure等接口. 参考上面的“细节面板添加额外External对象显示(IDetailLayoutBuilder)”
参考资料
[1]. UE4的地形编辑器源码 Engine\Source\Editor\LandscapeEditor文件夹下的SLandscapeEditor.cpp,SLandscapeEditor.h, LandscapeEditorObject.h
[2].https://docs.unrealengine.com/zh-CN/Programming/Slate/DetailsCustomization/index.html
[3] FBlackboardSelectorDetails
[4] SKismetInspector.h