In this post I’m going to show you how to extend the details panel inside the engine to expose more customised behavior for the systems that you might have built for your designers. Before I dive any further into the code, here’s the end result:

To achieve the result above we need to perform the following steps:

Create a custom module Create a specific asset Create our own details panel (which is going to extend the default one) Test the functionality

This post will not cover the 1st step of the process since I’ve already written a tutorial about it here.

Adding the required dependencies

For this post I have created a custom module named “BlogpostModule”. Inside it’s .build.cs file I’ve added the following dependencies:

PublicDependencyModuleNames . AddRange ( new string [ ] { "Core" , "CoreUObject" , "Engine" , "PropertyEditor" , "Slate" , "SlateCore" } ) ;

The reason we need these dependencies is because we’re going to use Slate in order to extend the details panel.

Before going any further make sure to compile your code.

Creating a test Actor

In order for our custom details panel to work, we need to tell our module that we want to bind a specific details panel to appear whenever we modify a specific class. This is the reason we’re going to add a test class just to showcase the functionality. I have named my class as “FancyCube” and placed it into the BlogpostModule as well. Here’s the code for it:

Fancy cube header file #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "FancyCube.generated.h" UCLASS() class BLOGPOSTMODULE_API AFancyCube : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties AFancyCube(); void AssignColor(FLinearColor NewColor); protected: // Called when the game starts or when spawned virtual void BeginPlay() override; UPROPERTY(VisibleAnywhere) UStaticMeshComponent* CubeSM; public: // Called every frame virtual void Tick(float DeltaTime) override; }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "FancyCube.generated.h" UCLASS ( ) class BLOGPOSTMODULE_API AFancyCube : public AActor { GENERATED_BODY ( ) public : // Sets default values for this actor's properties AFancyCube ( ) ; void AssignColor ( FLinearColor NewColor ) ; protected : // Called when the game starts or when spawned virtual void BeginPlay ( ) override ; UPROPERTY ( VisibleAnywhere ) UStaticMeshComponent * CubeSM ; public : // Called every frame virtual void Tick ( float DeltaTime ) override ; } ;

Fancy cube source file #include "FancyCube.h" // Sets default values AFancyCube::AFancyCube() { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick.bCanEverTick = true; CubeSM = CreateDefaultSubobject<UStaticMeshComponent>("CubeSM"); } void AFancyCube::AssignColor(FLinearColor NewColor) { if (CubeSM) { UMaterialInstanceDynamic* MID = CubeSM->CreateAndSetMaterialInstanceDynamic(0); MID->SetVectorParameterValue("BaseColor", NewColor); } } // Called when the game starts or when spawned void AFancyCube::BeginPlay() { Super::BeginPlay(); } // Called every frame void AFancyCube::Tick(float DeltaTime) { Super::Tick(DeltaTime); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include "FancyCube.h" // Sets default values AFancyCube :: AFancyCube ( ) { // Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it. PrimaryActorTick . bCanEverTick = true ; CubeSM = CreateDefaultSubobject < UStaticMeshComponent > ( "CubeSM" ) ; } void AFancyCube :: AssignColor ( FLinearColor NewColor ) { if ( CubeSM ) { UMaterialInstanceDynamic * MID = CubeSM -> CreateAndSetMaterialInstanceDynamic ( 0 ) ; MID -> SetVectorParameterValue ( "BaseColor" , NewColor ) ; } } // Called when the game starts or when spawned void AFancyCube :: BeginPlay ( ) { Super :: BeginPlay ( ) ; } // Called every frame void AFancyCube :: Tick ( float DeltaTime ) { Super :: Tick ( DeltaTime ) ; }

Once we have created the custom details panel we’re going to get tell our module to assign it to the “AFancyCube” class above. For now, create a Blueprint based on the class above and assign the following material to its mesh:

Extending the details panel

In order to extend the details panel you have to add a class that inherits the object class. Keep in mind that this class will not be marked with the typical UCLASS macro and we’re going to replace the default constructors and destructors later on. Once you have created your class, type in the following code in its header file:

Details Panel header #pragma once #include "CoreMinimal.h" #include "Input/Reply.h" #include "IDetailCustomization.h" class FCustomDetailsPanel : public IDetailCustomization { private: /* Contains references to all selected objects inside in the viewport */ TArray<TWeakObjectPtr<UObject>> SelectedObjects; public: /* Makes a new instance of this detail layout class for a specific detail view requesting it */ static TSharedRef<IDetailCustomization> MakeInstance(); /* IDetalCustomization interface */ virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; /* The code that fires when we click the "ChangeColor" button */ FReply ClickedOnButton(); }; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #pragma once #include "CoreMinimal.h" #include "Input/Reply.h" #include "IDetailCustomization.h" class FCustomDetailsPanel : public IDetailCustomization { private : /* Contains references to all selected objects inside in the viewport */ TArray < TWeakObjectPtr < UObject >> SelectedObjects ; public : /* Makes a new instance of this detail layout class for a specific detail view requesting it */ static TSharedRef < IDetailCustomization > MakeInstance ( ) ; /* IDetalCustomization interface */ virtual void CustomizeDetails ( IDetailLayoutBuilder & DetailBuilder ) override ; /* The code that fires when we click the "ChangeColor" button */ FReply ClickedOnButton ( ) ; } ;

Then, type the following code in the source file:

Details Panel source file #include "CustomDetailsPanel.h" //make sure to replace this include to match your class name #include "IDetailsView.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "DetailCategoryBuilder.h" #include "Widgets/SNullWidget.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SButton.h" #include "Widgets/SBoxPanel.h" #include "Text.h" #include "FancyCube.h" #include "UObject/Class.h" TSharedRef<IDetailCustomization> FCustomDetailsPanel::MakeInstance() { return MakeShareable(new FCustomDetailsPanel); } void FCustomDetailsPanel::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) { //Edits a category. If it doesn't exist it creates a new one IDetailCategoryBuilder& CustomCategory = DetailBuilder.EditCategory("CustomCategory"); //Store the currently selected objects from the viewport to the SelectedObjects array. DetailBuilder.GetObjectsBeingCustomized(SelectedObjects); //Adding a custom row CustomCategory.AddCustomRow(FText::FromString("Outline Color Changing Category")) .ValueContent() .VAlign(VAlign_Center) // set vertical alignment to center .MaxDesiredWidth(250) [ //With this operator we declare a new slate object inside our widget row //In this case the slate object is a button SNew(SButton) .VAlign(VAlign_Center) .OnClicked(this, &FCustomDetailsPanel::ClickedOnButton) //Binding the OnClick function we want to execute when this object is clicked .Content() [ //We create a new slate object inside our button. In this case a text block with the content of "Change Color" //If you ever coded a UMG button with a text on top of it you will notice that the process is quite the same //Meaning, you first declare a button which has various events and properties and then you place a Text Block widget //inside the button's widget to display text SNew(STextBlock).Text(FText::FromString("Change Color!")) ] ]; } FReply FCustomDetailsPanel::ClickedOnButton() { if (GEngine) { for (const TWeakObjectPtr<UObject>& Object : SelectedObjects) { AFancyCube* FancyCube = Cast<AFancyCube>(Object.Get()); if (FancyCube) { FancyCube->AssignColor(FLinearColor::MakeRandomColor()); } } GLog->Log("fancy cubes got a nice random color!"); } return FReply::Handled(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include "CustomDetailsPanel.h" //make sure to replace this include to match your class name #include "IDetailsView.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "DetailCategoryBuilder.h" #include "Widgets/SNullWidget.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SButton.h" #include "Widgets/SBoxPanel.h" #include "Text.h" #include "FancyCube.h" #include "UObject/Class.h" TSharedRef < IDetailCustomization > FCustomDetailsPanel :: MakeInstance ( ) { return MakeShareable ( new FCustomDetailsPanel ) ; } void FCustomDetailsPanel :: CustomizeDetails ( IDetailLayoutBuilder & DetailBuilder ) { //Edits a category. If it doesn't exist it creates a new one IDetailCategoryBuilder & CustomCategory = DetailBuilder . EditCategory ( "CustomCategory" ) ; //Store the currently selected objects from the viewport to the SelectedObjects array. DetailBuilder . GetObjectsBeingCustomized ( SelectedObjects ) ; //Adding a custom row CustomCategory . AddCustomRow ( FText :: FromString ( "Outline Color Changing Category" ) ) . ValueContent ( ) . VAlign ( VAlign_Center ) // set vertical alignment to center . MaxDesiredWidth ( 250 ) [ //With this operator we declare a new slate object inside our widget row //In this case the slate object is a button SNew ( SButton ) . VAlign ( VAlign_Center ) . OnClicked ( this , &FCustomDetailsPanel :: ClickedOnButton ) //Binding the OnClick function we want to execute when this object is clicked . Content ( ) [ //We create a new slate object inside our button. In this case a text block with the content of "Change Color" //If you ever coded a UMG button with a text on top of it you will notice that the process is quite the same //Meaning, you first declare a button which has various events and properties and then you place a Text Block widget //inside the button's widget to display text SNew ( STextBlock ) . Text ( FText :: FromString ( "Change Color!" ) ) ] ] ; } FReply FCustomDetailsPanel :: ClickedOnButton ( ) { if ( GEngine ) { for ( const TWeakObjectPtr < UObject > & Object : SelectedObjects ) { AFancyCube * FancyCube = Cast < AFancyCube > ( Object . Get ( ) ) ; if ( FancyCube ) { FancyCube -> AssignColor ( FLinearColor :: MakeRandomColor ( ) ) ; } } GLog -> Log ( "fancy cubes got a nice random color!" ) ; } return FReply :: Handled ( ) ; }

As you can see inside the CustomizeDetails function we used the “[ ]” operators to type “unusual” code. Essentially, in slate, these operators create a new Slate Widget in which we provide the properties that describe its functionality (such as its appearance and/or its contents). If you dive into the engine’s code for example DetailWidgetRow.h at line 113 you will notice the logic behind this operator is pretty straightfoward. (ie every time you use this operator you have to provide a new Slate Widget). If you think about it, this logic is similar to how UMG widgets work.

Binding the details panel to the actor

At this point, the last thing we need is to tie everything together. Go into your module’s startup function and type the following code:



Module source file #include "BlogpostModule.h" #include "FancyCube.h" #include "CustomDetailsPanel.h" #include "PropertyEditorModule.h" DEFINE_LOG_CATEGORY(BlogpostModule); #define LOCTEXT_NAMESPACE "FBlogpostModule" void FBlogpostModule::StartupModule() { UE_LOG(BlogpostModule, Warning, TEXT("BlogpostModule module has started!")); //Get the property module FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor"); //Register the custom details panel we have created PropertyModule.RegisterCustomClassLayout(AFancyCube::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FCustomDetailsPanel::MakeInstance)); } void FBlogpostModule::ShutdownModule() { UE_LOG(BlogpostModule, Warning, TEXT("BlogpostModule module has shut down")); } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE(FBlogpostModule,BlogpostModule) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include "BlogpostModule.h" #include "FancyCube.h" #include "CustomDetailsPanel.h" #include "PropertyEditorModule.h" DEFINE_LOG_CATEGORY ( BlogpostModule ) ; #define LOCTEXT_NAMESPACE "FBlogpostModule" void FBlogpostModule :: StartupModule ( ) { UE_LOG ( BlogpostModule , Warning , TEXT ( "BlogpostModule module has started!" ) ) ; //Get the property module FPropertyEditorModule & PropertyModule = FModuleManager :: LoadModuleChecked < FPropertyEditorModule > ( "PropertyEditor" ) ; //Register the custom details panel we have created PropertyModule . RegisterCustomClassLayout ( AFancyCube :: StaticClass ( ) -> GetFName ( ) , FOnGetDetailCustomizationInstance :: CreateStatic ( &FCustomDetailsPanel :: MakeInstance ) ) ; } void FBlogpostModule :: ShutdownModule ( ) { UE_LOG ( BlogpostModule , Warning , TEXT ( "BlogpostModule module has shut down" ) ) ; } #undef LOCTEXT_NAMESPACE IMPLEMENT_MODULE ( FBlogpostModule , BlogpostModule )

you may have to restart the Editor in order to see the changes .

Related

At this point, compile your module and you should see the custom details panel whenever you select any “FancyCube” actors. Keep in mind that