Unreal Engine will in general display TMap
entries with the key and the value
in the same row. For example TMap
with keys that are FString
, FName
,
int32
, even UTexture
will all be displayed with the key and value together.
UPROPERTY(EditAnywhere)
TMap<FString, int32> StringTMap;
UPROPERTY(EditAnywhere)
TMap<FName, int32> NameTMap;
UPROPERTY(EditAnywhere)
TMap<int32, int32> IntTMap;
UPROPERTY(EditAnywhere)
TMap<UTexture*, int32> TextureTMap;
The Problem
However FGameplayTag
and custom structs will be displayed with the key and
value on separate lines. Yuck.
USTRUCT()
struct FExampleStruct
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere)
FString Name;
// Used so we can have a TMap of this struct
FORCEINLINE friend uint32 GetTypeHash(const FExampleStruct& Struct)
{
return ::GetTypeHash(Struct.Name);
}
};
// later...
UPROPERTY(EditAnywhere)
TMap<FGameplayTag, int32> VanillaGameplayTagTMap;
UPROPERTY(EditAnywhere)
TMap<FExampleStruct, int32> VanillaStructTMap;
Is there a way we can force them to be shown on the same line? Yes! There are two ways.
Solutions
ForceInlineRow
The meta UPROPERTY()
tag ForceInlineRow
does exactly what you expect, it forces keys and values to be displayed inline on the same row.
UPROPERTY(EditAnywhere)
TMap<FGameplayTag, int32> Stats;
UPROPERTY(EditAnywhere, meta=(ForceInlineRow))
TMap<FGameplayTag, int32> StatsInlineRow;
That's all you need to add!
Custom Inspector
This is a lot more involved, but it will let us show any sort of data structure in a key-value TMap-style way.
Imagine we want to let designers specify the contents of a store. There will be an array of items and how many of them there will be.
USTRUCT()
struct FShopEntry
{
GENERATED_BODY()
UPROPERTY(EditAnywhere)
TSoftObjectPtr<UItemDataAsset> ItemDataAsset;
UPROPERTY(EditAnywhere, meta=(UIMin=1, ClampMin=1, UIMax=20))
int32 Count = 1;
};
We want to show this in tabular form, like ForceInlineRow
, but for whatever
reason, ForceInlineRow
doesn't work for the class we're using as a Key.
Here's how you do that.
- Create an editor module if you don't have one already.
- Create a details customization for the struct mentioned above.
- In the editor module's
StartupModule()
function, register the customization.
ShopEntryCustomization.h
// Copyright Brace Yourself Games. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "IPropertyTypeCustomization.h"
class IPropertyHandle;
class FShopEntryCustomization : public IPropertyTypeCustomization
{
public:
static TSharedRef<IPropertyTypeCustomization> MakeInstance()
{
return MakeShareable( new FShopEntryCustomization );
}
/** IPropertyTypeCustomization interface */
virtual void CustomizeHeader( TSharedRef<class IPropertyHandle> InStructPropertyHandle, class FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) override;
virtual void CustomizeChildren( TSharedRef<class IPropertyHandle> InStructPropertyHandle, class IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils ) override;
private:
/** Handle to the struct property being customized */
TSharedPtr<IPropertyHandle> StructPropertyHandle;
};
ShopEntryCustomization.cpp
#include "ShopEntryCustomization.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "PropertyHandle.h"
#include "PropertyCustomizationHelpers.h"
void FShopEntryCustomization::CustomizeHeader( TSharedRef<IPropertyHandle> InStructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& StructCustomizationUtils )
{
StructPropertyHandle = InStructPropertyHandle;
// Gets ItemDataAsset
TSharedPtr<IPropertyHandle> Key = StructPropertyHandle->GetChildHandle(0);
// Gets Count
TSharedPtr<IPropertyHandle> Value = StructPropertyHandle->GetChildHandle(1);
// Setup in the header row so that we still get the TArray dropdown
HeaderRow
.NameContent()
[
Key->CreatePropertyValueWidget()
]
.ValueContent()
.MaxDesiredWidth(0.0f)
[
Value->CreatePropertyValueWidget()
];
// This avoids making duplicate reset boxes
StructPropertyHandle->MarkResetToDefaultCustomized();
}
void FShopEntryCustomization::CustomizeChildren(TSharedRef<IPropertyHandle> InStructPropertyHandle, IDetailChildrenBuilder& StructBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils)
{
}
Now here's how we register the details customization.
MyProjectEditorModule.cpp
#include "MyProjectEditorModule.h"
#include "Modules/ModuleManager.h"
#include "Modules/ModuleInterface.h"
#include "PropertyTypeCustomization/ShopEntryCustomization.h"
#define LOCTEXT_NAMESPACE "FMyProjectEditorModule"
void FMyProjectEditorModule::StartupModule()
{
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomPropertyTypeLayout("ShopEntry", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FShopEntryCustomization::MakeInstance));
}
void FMyProjectEditorModule::ShutdownModule()
{
FModuleManager::Get().OnModulesChanged().RemoveAll(this);
// Unregister customization and callback
FPropertyEditorModule* PropertyEditorModule = FModuleManager::GetModulePtr<FPropertyEditorModule>("PropertyEditor");
if (PropertyEditorModule)
{
PropertyEditorModule->UnregisterCustomPropertyTypeLayout(TEXT("ShopEntry"));
}
}
IMPLEMENT_MODULE(FMyProjectEditorModule, MyProjectEditor)
#undef LOCTEXT_NAMESPACE
Hopefully between ForceInlineRow
and the example above, you should have an
idea of how to do key-value style display