Defining the issue
I intend to explain the process in a very simple example. Throughout this article we will deal with two data structures and a data asset that makes use of them. The two structures are:
USTRUCT() struct ARRAYCUSTOMIZATION_API FC_CustomSubStruct { GENERATED_BODY() UPROPERTY(EditDefaultsOnly) FString someName; UPROPERTY(EditDefaultsOnly) int32 someValue; }; USTRUCT() struct ARRAYCUSTOMIZATION_API FC_CustomStruct { GENERATED_BODY(); UPROPERTY(EditDefaultsOnly) TArray<FC_CustomSubStruct>
arrayProperty; UPROPERTY(EditDefaultsOnly) FC_CustomSubStruct subProperty; };
And the data asset looks like so:
UCLASS()
class ARRAYCUSTOMIZATION_API UC_CustomDataAsset : public UDataAsset
{
GENERATED_BODY()
UPROPERTY(EditDefaultsOnly)
FC_CustomSubStruct customArrayElement;
UPROPERTY(EditDefaultsOnly)
FC_CustomStruct customArrayProperty;
};
Pretty simple stuff and something we deal with quite often.
Once compiled, this code will display the properties for us, but the way it does it leaves a lot to be desired. Here’s a snip of how the data asset looks like when it’s opened for edit:
Notice how both properties are represented only as headers and need to be expanded before their properties can be edited. In an asset that contains lots of properties, this can become tedious and increases the chance of introducing bugs when editing this data. Our goal in this article is to get to this much more helpful and useful data layout:
This layout takes way less space on the screen and it has the additional benefit of exposing the data as is, limiting the amount of tree drilling that’s required.
First steps
It is highly recommended to contain all our type details customization code in an editor module that’s separate from our main game module and can directly link the necessary UnrealEd dlls. If you don’t have such a module set up yet, there are plenty of good resources explaining the process on the Internet. A good example is this article.
Once you have your module set up, we need to prepare a details customization implementation for our FC_CustomSubStruct struct. Once that’s done, the struct will no longer need to be expanded and we will be able to edit it inline, like so:
- Creating an implementation of the IPropertyTypeCustomization class (IDetailsCustomization if our type was a class)
- Registering it as a custom property type layout in the FPropertyEditorModule
Creating IPropertyTypeCustomization for FC_CustomSubStruct
The first function is the MakeInstance() factory function. This one should simply return a TSharedRef of our customization implementation class as an IPropertyTypeCustomization.
Then there are two virtual functions we need to override where the meat of the action happens. Those are void CustomizeHeader and void CustomizeChildren (or CustomizeDetails in case of classes).
This is where we’re going to put our customization code in. Below I’ve attached a full header file of what we’re going to implement:
#include "IPropertyTypeCustomization.h"
#include "DetailLayoutBuilder.h"
class FC_CustomSubStructCustomization : public IPropertyTypeCustomization
{
public:
static TSharedRefMakeInstance();
void CustomizeHeader(TSharedRefPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils);
void CustomizeChildren(TSharedRefPropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils); private:
TSharedPtrsomeNameProperty;
TSharedPtrsomeValueProperty; };
Implementing CustomizeHeader and CustomizeChildren
void FC_CustomSubStructCustomization::CustomizeHeader(TSharedRef PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
someNameProperty = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FC_CustomSubStruct, someName));
someValueProperty = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FC_CustomSubStruct, someValue));
if (!someNameProperty || !someValueProperty) {
return; }
HeaderRow.NameContent()
[
PropertyHandle->CreatePropertyNameWidget()
]
.ValueContent().MaxDesiredWidth(500.f).MinDesiredWidth(300.f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot().HAlign(HAlign_Left).VAlign(VAlign_Center).Padding(1.f, 0.f)
[
someNameProperty->CreatePropertyNameWidget()
]
+ SHorizontalBox::Slot().HAlign(HAlign_Fill).VAlign(VAlign_Center).Padding(1.f, 0.f).MaxWidth(200.f)
[
someNameProperty->CreatePropertyValueWidget()
]
+ SHorizontalBox::Slot().HAlign(HAlign_Left).VAlign(VAlign_Center).Padding(1.f, 0.f)
[
someValueProperty->CreatePropertyNameWidget()
]
+SHorizontalBox::Slot().HAlign(HAlign_Fill).VAlign(VAlign_Center).Padding(1.f, 0.f).MaxWidth(60.f)
[
someValueProperty->CreatePropertyValueWidget()
]
];
}
Registering a type customization
FPropertyEditorModule& propertyModule = FModuleManager::LoadModuleChecked("PropertyEditor");
propertyModule.RegisterCustomPropertyTypeLayout("C_CustomSubStruct", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FC_CustomSubStructCustomization::MakeInstance));
Customizing the look of FC_CustomStruct
class FC_CustomStructDetailsArrayBuilder : public FDetailArrayBuilder, public TSharedFromThis
{
public:
FC_CustomStructDetailsArrayBuilder(TSharedRef inBaseProperty);
virtual void GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) override;
private:
void GenerateEntry(TSharedRef elementProperty, int32 elementIndex, IDetailChildrenBuilder& childrenBuilder);
private:
TSharedPtr arrayPropertyHandle;
};
FC_CustomStructDetailsArrayBuilder::FC_CustomStructDetailsArrayBuilder(TSharedRef inBaseProperty)
: FDetailArrayBuilder(inBaseProperty, true, true, true)
, arrayPropertyHandle(inBaseProperty->AsArray())
{}
void FC_CustomStructDetailsArrayBuilder::GenerateChildContent(IDetailChildrenBuilder& childrenBuilder)
{
uint32 childrenCount = 0;
arrayPropertyHandle->GetNumElements(childrenCount);
for (uint32 childIndex = 0; childIndex < childrenCount; childIndex++)
{
TSharedRef elementHandle = arrayPropertyHandle->GetElement(childIndex);
GenerateEntry(elementHandle, childIndex, childrenBuilder);
}
}
void FC_CustomStructDetailsArrayBuilder::GenerateEntry(TSharedRef elementProperty, int32 elementIndex, IDetailChildrenBuilder& childrenBuilder)
{
IDetailPropertyRow& newElementRow = childrenBuilder.AddProperty(elementProperty);
newElementRow.ShowPropertyButtons(true);
}
void FC_CustomStructCustomization::CustomizeChildren(TSharedRef PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
static const FName arrayPropertyName = TEXT("arrayProperty");
TSharedPtr subStructArrayProperty = PropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FC_CustomStruct, arrayProperty));
uint32 childPropertiesCount = 0;
PropertyHandle->GetNumChildren(childPropertiesCount);
for (uint32 childIndex = 0; childIndex < childPropertiesCount; childIndex++)
{
TSharedPtr propertyAtIndex = PropertyHandle->GetChildHandle(childIndex);
if (propertyAtIndex == subStructArrayProperty)
{
subStructArrayBuilder = TSharedPtr(new FC_CustomStructDetailsArrayBuilder(subStructArrayProperty.ToSharedRef()));
ChildBuilder.AddCustomBuilder(subStructArrayBuilder.ToSharedRef());
}
else
{
ChildBuilder.AddProperty(propertyAtIndex.ToSharedRef());
}
}
}
Comments
Post a Comment