[UE5] 構造体のヘッダーにカスタム文字列を表示する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/e2b3e97a-9043-d4e0-addf-a74702b5da6e.png)
UE5 のエディタ UI では、構造体のプロパティを折りたたむことができますが、その状態では中身が見えないという課題があります。そこで、プロパティ名の右側の空欄部分に内容を簡略化して表示できるよう、汎用的な `PropertyCustomization` を作成しました。また、`TStructOpsTypeTraits` で構造体を拡張することで、適切なテキスト出力をすることができます。今回はこの 2 つのエディタ拡張について紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/f46786bd-c272-0848-23c8-93b8e68fd3f9.png)
これが、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/c92a2ccd-73c5-208b-a9a8-aaf4d0e0782c.png)
こうなる。

# 構造体の追加
まずは、適当な Animation プロパティを持つ構造体を定義します。 

```c++
USTRUCT()
struct FAnimationNode
{
	GENERATED_BODY()

	UPROPERTY( EditDefaultsOnly )
	TSoftObjectPtr<class UAnimationAsset> Animation;
};

// ...どこかのPROPERTY
public:
	UPROPERTY( EditDefaultsOnly )
	struct FAnimationNode Node;
```

# PropertyCustomization に登録する
`IPropertyTypeCustomization` を継承したエディタ UI を拡張するクラスを作成し、`StartupModule` から `PropertyCustomization` を登録します。 

```c++
// プロパティ列を拡張する UI クラス
class FNamedStructureDetails final : public IPropertyTypeCustomization
{
public:
	static TSharedRef<IPropertyTypeCustomization> MakeInstance()
	{
		return MakeShared<FNamedStructureDetails>();
	}

protected:
	// ヘッダー行をカスタマイズ
	virtual void CustomizeHeader(
		TSharedRef<IPropertyHandle> PropertyHandle,
		FDetailWidgetRow& HeaderRow,
		IPropertyTypeCustomizationUtils& CustomizationUtils ) override
	{
		HeaderRow
		.NameContent()
		[
			// プロパティ名を表示
			PropertyHandle->CreatePropertyNameWidget()
		]
		.ValueContent()
		[
			SNew( SHorizontalBox )
			+ SHorizontalBox::Slot()
			.VAlign( VAlign_Center ) // 表示位置を縦方向の中央に寄せる
			[
				SNew( STextBlock )
				.Text_Lambda( [PropertyHandle]
				{
					// 構造体から文字列を取得
					FText DisplayText;
					PropertyHandle->GetValueAsDisplayText( DisplayText );

					return DisplayText;
				} )
			]
		];
	}

	// 子要素は通常のレイアウトと同様に表示
	virtual void CustomizeChildren(
		TSharedRef<IPropertyHandle> PropertyHandle,
		IDetailChildrenBuilder& ChildBuilder,
		IPropertyTypeCustomizationUtils& CustomizationUtils ) override
	{
		uint32 NumChildren;
		PropertyHandle->GetNumChildren( NumChildren );

		for ( uint32 Index = 0; Index < NumChildren; ++Index )
		{
			// 通常と同様に、構造体の子要素を追加
			ChildBuilder.AddProperty( PropertyHandle->GetChildHandle( Index ).ToSharedRef() );
		}
	}
};

class FMyProjectModule final : public FDefaultGameModuleImpl
{
	virtual void StartupModule() override
	{
		auto& PropertyModule =
			FModuleManager::LoadModuleChecked<FPropertyEditorModule>( "PropertyEditor" );

		// PropertyCustomizationに登録
		PropertyModule.RegisterCustomPropertyTypeLayout(
			"NamedStructure", // 後述のPresentAsTypeで指定する
			FOnGetPropertyTypeCustomizationInstance::CreateStatic(
				&FNamedStructureDetails::MakeInstance
			)
		);
	}
};

IMPLEMENT_PRIMARY_GAME_MODULE( FMyProjectModule, MyProject, "MyProject" );
```
（本来はエディタでのみ使う機能なので、EditorModule に追加してください）

https://zenn.dev/hakuto_gamedev/articles/a546a185dc02f6

## PresentAsType メタ指定子
構造体にメタ指定子を追加します。

```c++
// 登録したPropertyCustomization名を指定する
USTRUCT( meta = ( PresentAsType = "NamedStructure" ) )
struct FAnimationNode
```

通常、`FPropertyEditorModule::RegisterCustomPropertyTypeLayout` ではプロパティ名を登録しますが、`PresentAsType` を使用すると、どんな型でも `PropertyCustomization` を適用することが可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/752f671c-6bec-f3bc-9f8d-dd2d65bdad9b.png)
ヘッダー欄に構造体の内容が、テキストとして表示されるようになりました。

# ExportTextItem でテキスト出力を実装
`TStructOpsTypeTraits` に `WithExportTextItem` 特性を追加し、さらに構造体に `ExportTextItem` 関数を定義することで、構造体のテキスト出力フォーマットを拡張できます。今回は、`PPF_PropertyWindow` の場合、つまりエディタ UI での表示を拡張する例を示します。

```c++
struct FAnimationNode
{
	// UStruct Overrides
	bool ExportTextItem(
		FString& ValueStr, const FAnimationNode& DefaultValue,
		UObject* Parent, int32 PortFlags, UObject* ExportRootScope ) const
	{
		if ( PortFlags & PPF_PropertyWindow )
		{
			// AnimationがセットされていればAssetNameを表示
			ValueStr += !Animation.IsNull() ?
				Animation.GetAssetName() : FName( NAME_None ).ToString();
			return true;
		}

		return false;
	}

// ...略

template <>
struct TStructOpsTypeTraits<FAnimationNode> : TStructOpsTypeTraitsBase2<FAnimationNode>
{
	enum
	{
		WithExportTextItem = true,
	};
};
```

:::note info
`ValueStr +=` としているのは、呼び出し元の実装を引き継ぐことを想定するためです。配列であれば呼び出し元で `"()"` が付与されるなど。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/c92a2ccd-73c5-208b-a9a8-aaf4d0e0782c.png)
以上で、テキスト出力を自由にカスタマイズできるようになりました。

# 配列の子要素でテキストを表示する

```c++
	UPROPERTY( EditDefaultsOnly )
	TArray<FAnimationNode> Nodes;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/58335cdf-0db2-eda8-ac4f-80b5d2e6c49e.png)
配列でも `PropertyCustomization` が適用されてヘッダー欄にテキストが表示されます。`TitleProperty` メタ指定子の必要もありません。

## FInstancedStructでも！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/ede77f89-9711-19d7-2a69-5327e8defd99.png)
`FInstancedStruct` を入れ子にする場合は、`FInstancedStruct` 自身が `ExportTextItem` を実装しており、内部の構造体のテキスト出力関数を呼び出してくれます。今回のケースでは `FAnimationNode` を `FAnimationNode_Simple` や `FAnimationNode_Random` に拡張するアイデアがあったため、この手法を採用しました。

# まとめ
今回のカスタマイズにより、UE5 のエディタ UI で構造体の内容を柔軟に表示できるようになりました。特に、構造体の中身をテキスト形式で簡略表示することで、UI の使いやすさが向上しました。エディタでのプロパティ表示をカスタマイズしたい方には、今回紹介した手法が役立つでしょう。