Customizing Details & Property Type panel - tutorial
The details panel is used all over the editor. It is used to display property of UObject, Blueprint default, behavior tree node's settings, project settings,... The editor gives default layouts for all of these, but sometimes you need to make things easier and more intuitive for game designer. It's customizing time!
Overview
The intent of this page is to gives you the maximum basics details about "How to customizing your own objects" and gives you abilities to start customizing and dig in UE4 source code. Keep in mind that is not a full API documentation. Hope we can write it later...
Inspired from
-
the legacy wiki page
-
the UE4 Doc
-
the well documented and explained tutorial from Kantan website
-
a lot of digging in
UnrealEngine\Engine\Source\Editor\DetailCustomizations\
andUnrealEngine\Engine\Source\Editor\PropertyEditor
modules.
Prerequisites
You need to master these elements first (or at least well understand their key principles):
-
Create an Editor Module (but I show you quickly how to make it below)
Source codes
Every lines of codes has been written and testing on this repo: https://github.com/NansPellicari/UE4-HowTo-DetailsPanelCustomization so you can test all these next features in a glimpse.
Look at each commit! I separate each commit trying to get a step by step guide following the instructions bellow. This way you can check what lines of codes has been added at each step.
If you want to contribute on the repo you are very welcome!
Introduction
There are 2 kinds of cutomizations:
-
Property Type Customization: it gives the ability of changing the layout and validate data of the
UPROPERTY
(ies) of anUSTRUCT
.>>> This works exclusively with an
USTRUCT
. -
Detail Customization: gives full control over the rendering of all parts of the pane (customizing the category box, creating new categories, do whatever you like in the categories etc.).
>>> This works with any
UObject
orUStruct
.
Setup
First we need to create a dedicated Editor Module.
Because it is a bit out of scope, I will try to go straight here, so if you need more details, please referer to this Wiki Link.
In our *.uproject
files, add these lines:
It is exaclty the same process when you work with plugins, use *.uplugin
instead.
{
"FileVersion": 3,
"EngineAssociation": "YourEngineVersion",
"Category": "",
"Description": "",
"Modules": [
{
"Name": "MyGame",
"Type": "Runtime",
"LoadingPhase": "Default"
},
{
"Name": "MyGameEditor",
"Type": "Editor",
"LoadingPhase": "PostEngineInit"
}
]
}
Create necessary folder Source\MyGameEditor\Public
and Source\MyGameEditor\Private
then create in Source\MyGameEditor\
the file MyGameEditor.build.cs
with this content:
using UnrealBuildTool;
public class MyGameEditor : ModuleRules
{
public MyGameEditor(ReadOnlyTargetRules Target) : base(Target)
{
PublicDependencyModuleNames.AddRange(
new string[] {
"Core",
"CoreUObject",
"Engine",
"InputCore",
"Slate",
"SlateCore",
"UnrealEd",
"PropertyEditor",
"MyGame",
});
PublicIncludePaths.AddRange(
new string[]
{
"MyGameEditor/Public"
});
PrivateIncludePaths.AddRange(
new string[]
{
"MyGameEditor/Private"
});
}
}
Note the specific requirements for customization: "Slate", "SlateCore", "UnrealEd" and "PropertyEditor"
Add our new module on our MyGameEditor.Target.cs
file:
// ...
ExtraModuleNames.AddRange(
new string[] {
// ...
"MyGameEditor"
});
}
// ...
Then create the Module definition files:
// Source\MyGameEditor\Public\MyGameEditor.h
#pragma once
#include "Engine.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
#include "UnrealEd.h"
class FMyGameEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
// Source\MyGameEditor\Private\MyGameEditor.cpp
#include "MyGameEditor.h"
#include "Modules/ModuleInterface.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_GAME_MODULE(FMyGameEditorModule, MyGameEditor);
#define LOCTEXT_NAMESPACE "MyGameEditor"
void FMyGameEditorModule::StartupModule() {}
void FMyGameEditorModule::ShutdownModule() {}
#undef LOCTEXT_NAMESPACE
Now we can regenerate our project files then build.
Create a basic USTRUCT, UENUM and a BP Actor
> USTRUCT
To create a Property Type Customization we need a USTRUCT
to work with (as we've talking about above):
In case you are confused: this struct is located in MyGame
source folder. The struct is used in the game, this is only its appearence in the editor we need to customize in our new Editor Module.
// Source\MyGame\MyStruct.h
USTRUCT(BlueprintType)
struct FMyStruct
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Amount")
float Amount;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Range")
float RangeMin;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Range")
float RangeMax;
};
> UENUM
Why an enum? This enum will be used to change the customization's display in accordance with its value.
// Source\MyGame\MyStruct.h
UENUM(BlueprintType)
enum class EValueType : uint8
{
AMOUNT UMETA(DisplayName = "Amount"),
RANGE UMETA(DisplayName = "Range")
};
// Then add a field in the struct
struct FMyStruct
{
//...
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Choice")
EValueType Type = EValueType::AMOUNT;
//...
}
> Create a basic BP Actor
Create a Basic Blueprint Actor and add a field SwitchingValue
...
of type FMyStruct
...
then compile and we'll see this new setting in our details panels of the Actor:
>>> End of step 1 in the repo
Customization
Now everything will be done in our Editor module.
1. Property Type
> What can we customize?
(1) The Header
For now it is represented by our property's name (defined in the actor). It should be great to add a bit more details here to distinguished quickly what type of value has been selected.
(2) The Children
This part is dedicated to the list of properties of our Struct, this is where the most of our customization fit.
For the both parts (Children & Header), we can:
-
add events when values changes to customize behaviors
-
change or add slate widget to customize the style
> Create the customization class
Our class name shoulds represent the struct's name the customization was made for, add Customization
appendix to the FMyStruct
= FMyStructCustomization
Header file:
// Source\MyGameEditor\Public\Customization\MyStructCustomization.h
#pragma once
#include "PropertyEditor/Public/IPropertyTypeCustomization.h"
class FMyStructCustomization : public IPropertyTypeCustomization
{
public:
/**
* It is just a convenient helpers which will be used
* to register our customization. When the propertyEditor module
* find our FMyStruct property, it will use this static method
* to instanciate our customization object.
*/
static TSharedRef<IPropertyTypeCustomization> MakeInstance();
// BEGIN IPropertyTypeCustomization interface
virtual void CustomizeHeader(TSharedRef<IPropertyHandle> StructPropertyHandle,
class FDetailWidgetRow& HeaderRow,
IPropertyTypeCustomizationUtils& StructCustomizationUtils) override;
virtual void CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle,
class IDetailChildrenBuilder& StructBuilder,
IPropertyTypeCustomizationUtils& StructCustomizationUtils) override;
// END IPropertyTypeCustomization interface
};
Cpp file:
// Source\MyGameEditor\Private\Customization\MyStructCustomization.cpp
#include "Customization/MyStructCustomization.h"
TSharedRef<IPropertyTypeCustomization> FMyStructCustomization::MakeInstance()
{
// Create the instance and returned a SharedRef
return MakeShareable(new FMyStructCustomization());
}
void FMyStructCustomization::CustomizeHeader(TSharedRef<IPropertyHandle> StructPropertyHandle,
class FDetailWidgetRow& HeaderRow,
IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
UE_LOG(LogTemp, Warning, TEXT("%s - The header customization is called"), ANSI_TO_TCHAR(__FUNCTION__));
// Should customize here soon
}
void FMyStructCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle,
class IDetailChildrenBuilder& StructBuilder,
IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
// Should customize here soon
}
> Register the customization for the editor
Now the customization exists, we can register (and unregister) it thanks to our module definition:
// Source\MyGameEditor\Private\MyGameEditor.cpp
// ...
// Add these lines in the header
#include "MyGame/MyStruct.h"
#include "Customization/MyStructCustomization.h"
#include "PropertyEditor/Public/PropertyEditorModule.h"
// ...
void FMyGameEditorModule::StartupModule()
{
// import the PropertyEditor module...
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
// to register our custom property
PropertyModule.RegisterCustomPropertyTypeLayout(
// This is the name of the Struct (we can also use "MyStruct" instead)
// this tells the property editor which is the struct property our customization will applied on.
FMyStruct::StaticStruct()->GetFName(),
// this is where our MakeInstance() method is usefull
FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FMyStructCustomization::MakeInstance));
PropertyModule.NotifyCustomizationModuleChanged();
}
void FMyGameEditorModule::ShutdownModule()
{
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
// unregister properties when the module is shutdown
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.UnregisterCustomPropertyTypeLayout("MyStruct");
PropertyModule.NotifyCustomizationModuleChanged();
}
}
Now we can regenerated our project files then build and restart the project
Unfortunatly, everytime you change your customizations you should close and reopen the unreal editor...😦 BTW Unreal Doc says:
"If this is an engine class, you should add your customization class (if it does not already exist) to the DetailCustomizations
module. This module can be recompiled and reloaded without restarting the editor, making it useful for fast tweaking of properties."
We will not do that here (it is not an engine class), but keep it in mind to ease your further developments if you need so.
Now in your Actor BP, there is no more details shown for our SwitchingValue
property. This because the customization is applied but we didn't implement anything yet:
But you can see in the logs panel:
>>> End of step 2 in the repo
> Implements the header customizations
We can be tempted to make all our changes here, but it can bring some troubles as:
-
weird style results in an array
-
can't display a nested customized struct
UPROPERTY
.
First, lets retrieve the default display of our header:
// Source\MyGameEditor\Private\Customization\MyStructCustomization.cpp
// Now we used them, we needs their headers
#include "PropertyEditor/Public/DetailLayoutBuilder.h"
#include "PropertyEditor/Public/DetailWidgetRow.h"
#include "PropertyEditor/Public/PropertyHandle.h"
// ...
void FMyStructCustomization::CustomizeHeader(TSharedRef<IPropertyHandle> StructPropertyHandle,
class FDetailWidgetRow& HeaderRow,
IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
HeaderRow.NameContent()[StructPropertyHandle->CreatePropertyNameWidget()];
}
// ...
Now we'll need to add more usefull details.
First we need to get some configured style for the editor, so we have to add a dependency on the EditorStyle
module:
// Source\MyGameEditor\Public\MyGameEditor.build.cs
PublicDependencyModuleNames.AddRange(new string[] {
// ...
"EditorStyle",
"MyGame",
});
then we get the Type
of the FMyStruct
to display it :
// Source\MyGameEditor\Private\Customization\MyStructCustomization.cpp
// We need our struct and basics slate widgets now
#include "SlateBasics.h"
#include "MyGame/MyStruct.h"
// We will add new text data here, prepare them for i18n
#define LOCTEXT_NAMESPACE "MyGameEditor"
// ...
// in CustomizeHeader()
{
// Get the property handler of the type property:
TSharedPtr<IPropertyHandle> TypePropertyHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMyStruct, Type));
check(TypePropertyHandle.IsValid());
// retrieve its value as a text to display
FText Type;
TypePropertyHandle->GetValueAsDisplayText(Type);
// then change the HeaderRow to add some Slate widget
// clang-format off
HeaderRow.NameContent()[StructPropertyHandle->CreatePropertyNameWidget()]
.ValueContent()[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(STextBlock)
.Font(FEditorStyle::GetFontStyle("PropertyWindow.NormalFont"))
.Text(FText::Format(LOCTEXT("ValueType", "The value type is \"{0}\""), Type))
]
];
// clang-format on
}
// ...
#undef LOCTEXT_NAMESPACE
But it is not sufficient for now, if the property value change, this widget will not be notified (so the display not updated). Although we still don't managed the display for the value, we can already attached an event to the property to prevent this.
First upgrade our Type
to a protected property and give it a more explicit name: ChosenType
. At the same time we can write the definition of the bound function we want to attached to our event.
// Source\MyGameEditor\Public\Customization\MyStructCustomization.h
protected:
FText ChosenTypeText;
/**
* This method is bind to the SetOnPropertyValueChanged on the "Type" property.
* It retrieves the Type's value and store it to the "ChosenTypeText" property here.
*/
void OnTypeChanged(TSharedPtr<IPropertyHandle> TypePropertyHandle);
Next, add these changes on the cpp file:
// Source\MyGameEditor\Private\Customization\MyStructCustomization.cpp
// in CustomizeHeader()
{
// ...
// retrieve its value as a text to display
OnTypeChanged(TypePropertyHandle);
// attached an event when the property value changed
TypePropertyHandle->SetOnPropertyValueChanged(
FSimpleDelegate::CreateSP(this, &FMyStructCustomization::OnTypeChanged, TypePropertyHandle));
}
void FMyStructCustomization::OnTypeChanged(TSharedPtr<IPropertyHandle> TypePropertyHandle)
{
if (TypePropertyHandle.IsValid() && TypePropertyHandle->IsValidHandle())
{
TypePropertyHandle->GetValueAsDisplayText(ChosenTypeText);
}
}
// ...
Ok that seems good but something missing. The value is passed to the widget but we have to bind a function to the desired attributes (as we need to do in the editor for a blueprint Widget to have a refreshed data to display)...
with the Slate API we do:
// Source\MyGameEditor\Private\Customization\MyStructCustomization.cpp
// replace our Textblock with:
SNew(STextBlock)
.Font(FEditorStyle::GetFontStyle("PropertyWindow.NormalFont"))
// The MakeAttributeLambda do the trick.
.Text(MakeAttributeLambda([=] { return FText::Format(LOCTEXT("ValueType", "The value type is \"{0}\""), ChosenTypeText); }))
Ok compile and restart the editor, header customizations are done!
>>> End of step 3 in the repo
> Implements the children customizations
Ok the most interesting parts are coming here. First we need to test a changing value of the Type
to see if our header customization is working:
// Source\MyGameEditor\Private\Customization\MyStructCustomization.cpp
// add this line a the top
#include "IDetailChildrenBuilder.h"
// ...
void FMyStructCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle,
class IDetailChildrenBuilder& StructBuilder,
IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
TSharedPtr<IPropertyHandle> TypePropertyHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMyStruct, Type));
StructBuilder.AddCustomRow(LOCTEXT("MyStructRow", "MyStruct"))
[
TypePropertyHandle->CreatePropertyValueWidget()
];
}
// ...
After compiling we can change the Type
and see the text changing:
A. Style
Now add some style for our children properties:
// Source\MyGameEditor\Private\Customization\MyStructCustomization.cpp
// ...
// in CustomizeChildren()
{
// First we need to retrieve every Property handles
TSharedPtr<IPropertyHandle> AmountPropertyHandle =
StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMyStruct, Amount));
TSharedPtr<IPropertyHandle> LhRangePropertyHandle =
StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMyStruct, RangeMin));
TSharedPtr<IPropertyHandle> RhRangePropertyHandle =
StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMyStruct, RangeMax));
TSharedPtr<IPropertyHandle> TypePropertyHandle = StructPropertyHandle->GetChildHandle(GET_MEMBER_NAME_CHECKED(FMyStruct, Type));
// and check them before using them
check(AmountPropertyHandle.IsValid() && LhRangePropertyHandle.IsValid() && RhRangePropertyHandle.IsValid() &&
TypePropertyHandle.IsValid());
StructBuilder.AddCustomRow(LOCTEXT("MyStructRow", "MyStruct"))
[
SNew(SBorder)
.BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder"))
.BorderBackgroundColor(FLinearColor(255.f, 152.f, 0))
.Content()
[
SNew(SWrapBox)
.UseAllottedWidth(true)
+SWrapBox::Slot()
.Padding(5.f, 0.f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
TypePropertyHandle->CreatePropertyNameWidget()
]
+ SVerticalBox::Slot()
.AutoHeight()
[
TypePropertyHandle->CreatePropertyValueWidget()
]
]
+SWrapBox::Slot()
.Padding(5.f, 0.f)
[
SNew(SBox)
.MinDesiredWidth(70.f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
AmountPropertyHandle->CreatePropertyNameWidget()
]
+ SVerticalBox::Slot()
.AutoHeight()
[
AmountPropertyHandle->CreatePropertyValueWidget()
]
]
]
+SWrapBox::Slot()
.Padding(5.f, 0.f)
[
SNew(SBox)
.MinDesiredWidth(70.f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
LhRangePropertyHandle->CreatePropertyNameWidget()
]
+ SVerticalBox::Slot()
.AutoHeight()
[
LhRangePropertyHandle->CreatePropertyValueWidget()
]
]
]
+SWrapBox::Slot()
.Padding(5.f, 0.f)
[
SNew(SBox)
.MinDesiredWidth(70.f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
RhRangePropertyHandle->CreatePropertyNameWidget()
]
+ SVerticalBox::Slot()
.AutoHeight()
[
RhRangePropertyHandle->CreatePropertyValueWidget()
]
]
]
]
];
}
Ok ok style is still a subjective taste... But now you probably better understand how far you can go == Sky is the limit 😵!!
>>> End of step 4 in the repo
B. Behaviors
As we planned when we created the UENUM
, we want the properties edition depending on the Type
chosen.
Thanks to the previously header's customization, we already listen an event when Type
's value changes. We just have to push a bit further the implementation:
// Source\MyGameEditor\Public\Customization\MyStructCustomization.h
// move MyStruct inclusion here to have the "EValueType" available for the compiler
#include "MyGame/MyStruct.h"
// ...
protected:
EValueType ChosenType;
// ...
// Source\MyGameEditor\Private\Customization\MyStructCustomization.cpp
// ...
// in OnTypeChanged()
// ...
TypePropertyHandle->GetValueAsDisplayText(ChosenTypeText);
uint8 ValueAsByte;
TypePropertyHandle->GetValue(ValueAsByte);
ChosenType = (EValueType) ValueAsByte;
// ...
Then add attribute IsEnabled
for SBoxes
wrapping the desired field:
// Source\MyGameEditor\Private\Customization\MyStructCustomization.cpp
// in CustomizeChildren()
// ...
// For the SBox wrapping AmountPropertyHandle
SNew(SBox)
.IsEnabled(MakeAttributeLambda([=] { return ChosenType == EValueType::AMOUNT; }))
// ...
// For the SBoxes wrapping LhRangePropertyHandle and RhRangePropertyHandle
SNew(SBox)
.IsEnabled(MakeAttributeLambda([=] { return ChosenType == EValueType::RANGE; }))
// ...
Compile & restart, children customizations are done!
>>> End of step 5 in the repo
> BONUS: Nested Customization
If you want to call a customization inside your new customization without changing its style and behavior, you should call StructBuilder.GenerateStructValueWidget(MyPropertyCustomizedHandle.ToSharedRef())
(which return a slate widget) in your CustomizeChildren
method.
This can't be done in CustomizeHeader
(For now I don't know the reason why...)