UE4 extended details panel

Before I dig any further into the code, here's the final result:
Insert image description here

In order to achieve the above results, we need to perform the following steps:
Create a custom module
Create specific assets
Create our own details panel (will extend the default panel)

test function

This post won't cover the first step of the process, as I've already written a tutorial about it here .

Adding the required dependencies
For this post, I created a custom module called "BlogpostModule". In its .build.cs file I added the following dependencies:

Public dependent module name. AddRange ( new string [ ] { "Core" , "CoreUObject" , "Engine" , "PropertyEditor" , "Slate" , "SlateCore" } ) ;
The reason we need these dependencies is because we will be using Slate to extend the details panel.

Before proceeding, make sure to compile your code.

Create a test actor

In order for our custom details panel to work properly, we need to tell our module that we want to bind a specific details panel so that it appears when we modify a specific class. That's why we're going to add a test class to demonstrate functionality. I named my class "FancyCube" and put it into BlogpostModule as well. This is its code:

#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 AddKeyValue();
 
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;
 
	
	
};
#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::AddKeyValue()
{
    
    
	if (CubeSM)
	{
    
    
		//Do Some Thing
	}
	
}
 
// 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 our custom details panel, we will tell our module to assign it to the "AFancyCube" class above. Now, create a Blueprint based on the class above and assign the following materials to its mesh:

Extended details panel

In order to extend the details panel, you must add a class that inherits the object class. Keep in mind that this class will not be marked with the typical UCLASS macros, we will replace the default constructors and destructors later. After creating the class, enter the following code in its header file:


#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:

#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("Add Key-Value"))
		.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("Add Key-Value"))
		]
		];
	
}

FReply FCustomDetailsPanel::ClickedOnButton()
{
    
    	
	if (GEngine)
	{
    
    
		for (const TWeakObjectPtr<UObject>& Object : SelectedObjects)
		{
    
    
			AFancyCube* FancyCube = Cast<AFancyCube>(Object.Get());
			if (FancyCube)
			{
    
    
				FancyCube->AddKeyValue();
			}
		}
		GLog->Log("Add a Key-Value!");
	}
	return FReply::Handled();
}

As you can see in the CustomizeDetails function, we use the "[ ]" operator to enter "unusual" code. Essentially, in slate, these operators create a new Slate Widget, in which we provide properties that describe its functionality (such as its appearance and/or content). If you dig into the engine's code, such as DetailWidgetRow.h on line 113 , you'll notice that the logic behind this operator is very simple. (i.e. you must provide a new Slate Widget every time you use this operator). If you think about it, this logic is similar to how UMG widgets work.

Bind the details panel to the Actor

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

#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)

At this point, compile your module and you should see the custom details panel when you select any "FancyCube" Actor. Keep in mind that you may have to restart the editor to see the changes.

But there is a huge problem with this method . When

PropertyModule.RegisterCustomClassLayout(AFancyCube::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FCustomDetailsPanel::MakeInstance));

Change to

PropertyModule.RegisterCustomClassLayout(AActor::StaticClass()->GetFName(), FOnGetDetailCustomizationInstance::CreateStatic(&FCustomDetailsPanel::MakeInstance));

It is completely ineffective because this method only applies to custom classes inherited from UObject, not the classes that come with the engine. Therefore, if you want to add to the original Actor base class, you need to modify the engine source code .

Guess you like

Origin blog.csdn.net/zx1091515459/article/details/127785361