informa
/
3 MIN READ
Blogs

UE4Cookery CPP005: Custom dictionary with IPropertyTypeCustomization

Topic: Custom dictionary with IPropertyTypeCustomization Source for UE4.26: https://github.com/klauth86/UE4Cookery/tree/main/CPP005

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...

Latest Jobs

Cryptic Studios

Remote
1.19.23
Senior Producer

Anne Arundel Community College

Arnold, MD, USA
1.30.23
Instructor/Assistant Professor, Game Art

Night School Studio

Los Angeles, CA, USA
1.09.23
Level Designer / Scripter, Games Studio
More Jobs   

CONNECT WITH US

Explore the
Subscribe to
Follow us

Game Developer Job Board

Game Developer Newsletter

@gamedevdotcom

Explore the

Game Developer Job Board

Browse open positions across the game industry or recruit new talent for your studio

Browse
Subscribe to

Game Developer Newsletter

Get daily Game Developer top stories every morning straight into your inbox

Subscribe
Follow us

@gamedevdotcom

Follow us @gamedevdotcom to stay up-to-date with the latest news & insider information about events & more