Sponsored By

UE4Cookery CPP005: Custom dictionary with IPropertyTypeCustomization

Topic: Custom dictionary with IPropertyTypeCustomization

Source for UE4.26: https://github.com/klauth86/UE4Cookery/tree/main/CPP005

Artur Kh

March 15, 2021

3 Min Read

Sometimes you need to customize visual representation of UClasses and UStructs to gain more control and comfort while working with them in Editor. Thanx to modular system this is rather simple task. We just take a look at this with custom list of values example. Imagine that you want to create a list of values, for which you can select item with ComboBox in Editor Details panel, very very similar to UEnums.

First of all, let's create some list of values and let it be a list of FName, that is rather common and popular case. So, it will be

 

MyDictionary.h


#pragma once

#include "UObject/ObjectMacros.h"
#include "UObject/NameTypes.h"
#include "MyDictionary.generated.h"

USTRUCT(BlueprintType)
struct CPP005_API FMyDictionary {

	GENERATED_USTRUCT_BODY();

	uint8 ItemIndex;
	
	static TArray<FName> Items;
};

 

and don't forget to init static things in MyDictionary.cpp


#include "MyDictionary.h"

TArray<FName> FMyDictionary::Items = { FName("Letter A"), FName("Letter B"), FName("Letter C") };

 

Now, just add Editor module for our project and create some IPropertyTypeCustomization for UMyDictionary. It will be like this

 

PropertyTypeCustomization_MyDictionary.h


#pragma once

#include "IPropertyTypeCustomization.h"

class SPinComboBox;

class CPP005EDITOR_API FPropertyTypeCustomization_MyDictionary : public IPropertyTypeCustomization {

public:

	static TSharedRef<IPropertyTypeCustomization> MakeInstance();

	void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override;

	virtual void CustomizeChildren(TSharedRef<class IPropertyHandle> StructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) override {}

protected:

	void GenerateComboBoxIndexes(TArray< TSharedPtr<int32> >& OutComboBoxIndexes);

	FString OnGetText() const;

	void ComboBoxSelectionChanged(TSharedPtr<int32> NewSelection, ESelectInfo::Type SelectInfo);

	FText OnGetFriendlyName(int32 itemIndex);

	FText OnGetTooltip(int32 itemIndex);

	template<typename T>
	T* GetPropertyAs() const {
		if (PropertyHandlePtr.IsValid()) {
			TArray<void*> RawData;
			PropertyHandlePtr->AccessRawData(RawData);
			return reinterpret_cast<T*>(RawData[0]);
		}

		return nullptr;
	}

protected:

	TSharedPtr<IPropertyHandle> PropertyHandlePtr;

	TSharedPtr<SPinComboBox> ComboBox;
};

 

PropertyTypeCustomization_MyDictionary.cpp


#include "PropertyTypeCustomization_MyDictionary.h"
#include "DetailCategoryBuilder.h"
#include "DetailWidgetRow.h"
#include "SGraphPinComboBox.h"
#include "IPropertyUtilities.h"
#include "MyDictionary.h"

#define LOCTEXT_NAMESPACE "PropertyTypeCustomization_MyDictionary"

TSharedRef<IPropertyTypeCustomization> FPropertyTypeCustomization_MyDictionary::MakeInstance() {
	return MakeShareable(new FPropertyTypeCustomization_MyDictionary);
}

void FPropertyTypeCustomization_MyDictionary::CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) {

	PropertyHandlePtr = PropertyHandle;
	TSharedPtr<IPropertyUtilities> PropertyUtils = CustomizationUtils.GetPropertyUtilities();

	// Get list of MyDictionary indexes
	TArray< TSharedPtr<int32> > ComboItems;
	GenerateComboBoxIndexes(ComboItems);

	ComboBox = SNew(SPinComboBox)
		.ComboItemList(ComboItems)
		.VisibleText(this, &FPropertyTypeCustomization_MyDictionary::OnGetText)
		.OnSelectionChanged(this, &FPropertyTypeCustomization_MyDictionary::ComboBoxSelectionChanged)
		.OnGetDisplayName(this, &FPropertyTypeCustomization_MyDictionary::OnGetFriendlyName)
		.OnGetTooltip(this, &FPropertyTypeCustomization_MyDictionary::OnGetTooltip);

	HeaderRow.NameContent()[PropertyHandle->CreatePropertyNameWidget()]
		.ValueContent()[ComboBox.ToSharedRef()].IsEnabled(MakeAttributeLambda([=] { return !PropertyHandle->IsEditConst() && PropertyUtils->IsPropertyEditingEnabled(); }));
}

void FPropertyTypeCustomization_MyDictionary::GenerateComboBoxIndexes(TArray< TSharedPtr<int32> >& OutComboBoxIndexes) {
	int32 i = 0;
	for (auto item : FMyDictionary::Items) {
		TSharedPtr<int32> EnumIdxPtr(new int32(i++));
		OutComboBoxIndexes.Add(EnumIdxPtr);
	}
}

FString FPropertyTypeCustomization_MyDictionary::OnGetText() const {

	if (auto myDictionary = GetPropertyAs<FMyDictionary>()) {

		auto itemIndex = myDictionary->ItemIndex;
		
		return (FMyDictionary::Items.Num() < itemIndex || itemIndex < 0)
			? ""
			: FMyDictionary::Items[itemIndex].ToString();
	}

	return "";
}

void FPropertyTypeCustomization_MyDictionary::ComboBoxSelectionChanged(TSharedPtr<int32> NewSelection, ESelectInfo::Type /*SelectInfo*/) {
	if (NewSelection.IsValid()) {
		if (auto myDictionary = GetPropertyAs<FMyDictionary>()) {
			myDictionary->ItemIndex = *NewSelection;
		}
	}
}

FText FPropertyTypeCustomization_MyDictionary::OnGetFriendlyName(int32 itemIndex) {
	return (FMyDictionary::Items.Num() < itemIndex || itemIndex < 0)
		? FText::GetEmpty()
		: FText::FromName(FMyDictionary::Items[itemIndex]);
}

FText FPropertyTypeCustomization_MyDictionary::OnGetTooltip(int32 itemIndex) {
	return (FMyDictionary::Items.Num() < itemIndex || itemIndex < 0)
		? FText::GetEmpty()
		: FText::FromName(FMyDictionary::Items[itemIndex]);
}

#undef LOCTEXT_NAMESPACE

 

Note, that this will require us to add PropertyEditor, SlateCore, CoreUObject and GraphEditor in our Editor.Build.cs file. After that we need only to implement some register and unregister code in our project Editor Module:

 

CPP005EditorModule.cpp


#include "CPP005EditorModule.h"
#include "PropertyEditorModule.h"
#include "MyDictionary.h"
#include "DetailCustomizations/PropertyTypeCustomization_MyDictionary.h"

DEFINE_LOG_CATEGORY(LogCPP005Editor);

#define LOCTEXT_NAMESPACE "FCPP005EditorModule"

void FCPP005EditorModule::StartupModule() {
	// Register the details customizer
	FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
	PropertyModule.RegisterCustomPropertyTypeLayout(FMyDictionary::StaticStruct()->GetFName(), 
		FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FPropertyTypeCustomization_MyDictionary::MakeInstance));
}

void FCPP005EditorModule::ShutdownModule() {
	// Unregister the details customization
	if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) {
		FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
		PropertyModule.UnregisterCustomPropertyTypeLayout(FMyDictionary::StaticStruct()->GetFName());
	}
}

#undef LOCTEXT_NAMESPACE

IMPLEMENT_MODULE(FCPP005EditorModule, CPP005Editor);

 

Main part is done, we can create some AActor class that has our FMyDictionary as UProperty

 

MyActor.h


// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "GameFramework/Actor.h"
#include "MyDictionary.h"
#include "MyActor.generated.h"

UCLASS()
class CPP005_API AMyActor : public AActor
{
	GENERATED_BODY()

protected:

	UPROPERTY(EditAnywhere, Category = "My Actor")
		FMyDictionary MyDict;
};

 

And check its view in Blueprint Editor:

 

Selection in Blueprint Editor

 

PS. You can also configure custom details for any UClass, just use RegisterCustomClassLayout method instead of RegisterCustomPropertyTypeLayout...

Read more about:

Blogs

About the Author(s)

Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like