【UE4】独自アセット実装マニュアル（後編）

#本記事について
本記事は、筆者が UE4 を用いて独自のアセット及びそのエディタを組んでいた際のメモを、マニュアルとしてまとめ直したものです。
前編・後編の 2 つに分かれており、本記事は後編になります。

前編はこちら：

https://qiita.com/arblic/private/581d251558a412f65fec

前編で用いた .uproject をそのまま使用していきます。

#プロパティカスタマイズ手順
##1.プロパティカスタマイズクラスの作成
`FParameter` 構造体のプロパティをカスタマイズするクラスを作っていきます。
`IPropertyTypeCustomization` インターフェイスを継承する必要がありますが、これもまた Unreal Editor 上からは選択できないため、継承なしでソースコードを追加してから内容を改変します。
![screenshot.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/c4885445-6e72-187f-d312-6435d73d314d.png)
名称は `ParameterCustomization` としました。
追加先は「MyPluginEditor (Editor)」です。
ソースコードを改変します。

~~~cpp:ParameterCustomization.h
#pragma once

#include "CoreMinimal.h"
#include "IPropertyTypeCustomization.h"
#include "PropertyHandle.h"

class FParameterCustomization : public IPropertyTypeCustomization {
public:
	FParameterCustomization();

	static TSharedRef<IPropertyTypeCustomization> MakeInstance() {
		return MakeShareable( new FParameterCustomization );
	}

	virtual void CustomizeHeader( TSharedRef<IPropertyHandle> StructPropertyHandle,
								  class FDetailWidgetRow & HeaderRow,
								  IPropertyTypeCustomizationUtils & StructCustomizationUtils ) override;
	virtual void CustomizeChildren( TSharedRef<class IPropertyHandle> StructPropertyHandle,
									class IDetailChildrenBuilder & StructBuilder,
									IPropertyTypeCustomizationUtils & StructCustomizationUtils ) override;

private:
	TSharedPtr<IPropertyHandle>	ValueHandle;
	TSharedPtr<IPropertyHandle>	TextHandle;
	TSharedPtr<IPropertyHandle>	ColorHandle;
	TSharedPtr<IPropertyHandle>	PointHandle;
	TSharedPtr<IPropertyHandle>	ArrayHandle;
};
~~~

ヘッダファイルは `#include "CoreMinimal.h"` 以下をコピペしてください。

~~~cpp:ParameterCustomization.cpp
#include "ParameterCustomization.h"
#include "PropertyEditing.h"

#define LOCTEXT_NAMESPACE "FMyPluginEditor"

FParameterCustomization::FParameterCustomization()
{
}

void FParameterCustomization::CustomizeHeader( TSharedRef<IPropertyHandle> StructPropertyHandle,
											   class FDetailWidgetRow & HeaderRow,
											   IPropertyTypeCustomizationUtils & StructCustomizationUtils )
{
	ValueHandle = StructPropertyHandle->GetChildHandle( GET_MEMBER_NAME_CHECKED( FParameter, Value ) );
	TextHandle  = StructPropertyHandle->GetChildHandle( GET_MEMBER_NAME_CHECKED( FParameter, Text ) );
	ColorHandle = StructPropertyHandle->GetChildHandle( GET_MEMBER_NAME_CHECKED( FParameter, Color ) );
	PointHandle = StructPropertyHandle->GetChildHandle( GET_MEMBER_NAME_CHECKED( FParameter, Point ) );
	ArrayHandle = StructPropertyHandle->GetChildHandle( GET_MEMBER_NAME_CHECKED( FParameter, Array ) );
}

void FParameterCustomization::CustomizeChildren( TSharedRef<class IPropertyHandle> StructPropertyHandle,
												 class IDetailChildrenBuilder & StructBuilder,
												 IPropertyTypeCustomizationUtils & StructCustomizationUtils )
{
	StructBuilder.AddProperty( ValueHandle.ToSharedRef() );
	StructBuilder.AddProperty( TextHandle.ToSharedRef() );
	StructBuilder.AddProperty( ColorHandle.ToSharedRef() );
	StructBuilder.AddProperty( PointHandle.ToSharedRef() );
	StructBuilder.AddProperty( ArrayHandle.ToSharedRef() );
}

#undef LOCTEXT_NAMESPACE
~~~

ソースファイルは `#include "ParamterCustomization.h"` 以下をコピペしてください。

`CustomizeHeader` 関数内では構造体のプロパティハンドルを取得しています。
`CustomizeChildren` 関数内では取得したプロパティハンドルを編集できるように記述しています。

これが最も簡単なエディタカスタマイズの形になります。

##2.プロパティカスタマイズクラスの登録
作成した `FParameterCustomization` クラスを登録します。
`FMyAssetActions` クラスを `AssetTools` モジュールに登録したのと同様の方法で、`PropertyEditor` モジュールに対して登録します。
MyPluginEditor.cpp を開いてください。

~~~cpp:MyPluginEditor.cpp
#include "MyPluginEditor.h"
#include "MyAssetActions.h"
#include "AssetToolsModule.h"
#include "ParameterCustomization.h" // 追加
~~~

~~~cpp:MyPluginEditor.cpp
void FMyPluginEditor::StartupModule()
{
	auto & moduleMgr = FModuleManager::Get();

	if( moduleMgr.IsModuleLoaded( "AssetTools" ) ) {

		auto & assetTools = moduleMgr.LoadModuleChecked<FAssetToolsModule>( "AssetTools" ).Get();

		auto assetCategoryBit = assetTools.RegisterAdvancedAssetCategory( FName( TEXT( "MyPlugin" ) ), LOCTEXT( "NewAssetCategory", "My Plugin" ) );

		auto actions = MakeShareable( new FMyAssetActions( assetCategoryBit ) );

		assetTools.RegisterAssetTypeActions( actions );
	}

	// ここから ↓↓
	if( moduleMgr.IsModuleLoaded( "PropertyEditor" ) ) {

		auto & propertyEditorModule = moduleMgr.LoadModuleChecked<FPropertyEditorModule>( "PropertyEditor" );

		propertyEditorModule.RegisterCustomPropertyTypeLayout(
			("Parameter"),
			FOnGetPropertyTypeCustomizationInstance::CreateStatic( &FParameterCustomization::MakeInstance )
		);

		propertyEditorModule.NotifyCustomizationModuleChanged();
	}
	// ここまでを追加 ↑↑
}
~~~

だいぶ長くなってきたので、要所だけの記載にとどめます。
インクルード処理を追加し、`FParameter` 構造体と `FParameterCUstomization` とを紐付ける形で `PropertyEditor` モジュールに登録します。

ビルドが通ったら、Unreal Editor を起動します。
MyAsset クラスアセットをダブルクリックし、エディタを起動してみます。
![screenshot.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/45d1def5-f2c2-927e-d274-e1bf94d163bb.png)
パッと見前回の状態と変わっていないように見えますが、Parameter というくくりが消えています。
また、`FParameterCustomization::CustomizeHeader` 関数内にブレークポイントを張って Visual Studio 上から実行すれば、張った場所でブレークすることが確認できます。

##3.プロパティハンドルからの値取得・設定方法について
さっそくエディタのカスタマイズを……と行きたいところですが、その前にプロパティハンドル越しに値の取得と設定を行う部分を説明します。
エディタ上から編集する値を参照することはよくありますし、特定の値が設定された際に別の項目も一緒に設定する、といったこともよくあるためです。
`FParameter` 構造体のメンバをプロパティハンドル越しに取得・設定していきます。

###プリミティヴ型
もともとプログラム自体に定義されており、「基本データ型」や「組み込み型」、「ビルトイン型」とも呼ばれるものです。

`ValueHandle` から `Value` プロパティの値を取得する関数を追加してみます。
`Value` メンバは `int32` 型、つまりプリミティヴ型なので素直に記述できます。

~~~cpp:ParameterCustomization.h
public:
	TOptional<int32>	GetValue() const;
~~~

~~~cpp:ParameterCustomization.cpp
TOptional<int32> FParameterCustomization::GetValue() const
{
	int32	retval = 0;
	if( FPropertyAccess::Success != ValueHandle->GetValue( retval ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't get %s's property value." ), *(ValueHandle->GetPropertyDisplayName().ToString()) );
	}

	return retval;
}
~~~

返り値は `TOptional<int32>` 型にしています。
これは後述するデリゲート関数として登録する際、`TOptional<T>` を指定しなければならないためです。
使用する際は返り値に対して `TOptional<int32>::GetValue()` メンバ関数で値を取り出します。

取得できなかった場合は一応エラーを表示しています。

次は設定関数です。

~~~cpp:ParameterCustomization.h
public:
	bool	SetValue( int32 Value );
~~~

~~~cpp:ParameterCustomization.cpp
bool FParameterCustomization::SetValue( int32 Value )
{
	if( FPropertyAccess::Success != ValueHandle->SetValue( Value ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't set %s's property value." ), *(ValueHandle->GetPropertyDisplayName().ToString()) );
		return false;
	}

	return true;
}
~~~

こちらもさして難しくはないと思います。
プロパティハンドルクラスの `GetValue`, `SetValue` 関数はいくつかの型がオーバーライドされており、以下のリストにある型であれば `int32` 型と同様の形で取り出すことができます。

- bool
- int8
- int16
- int32
- int64
- uint8
- uint16
- uint32
- uint64
- float
- double

また、以下のクラスはプリミティヴ型ではありませんが、それぞれかっこ内の形で `GetValue`, `SetValue` 関数が提供されています。

- FString（参照）
- FText（参照）
- FName（参照）
- FVector（参照）
- FVector2D（参照）
- FVector4（参照）
- FQuat（参照）
- FRotator（参照）
- UObject（ポインタ、const ポインタ）
- FAssetData（参照）
- FProperty（ポインタ、const ポインタ）

`Text` メンバは `FText` であるため上記リストに含まれますが、デリゲート関数の形が若干違うためここでは説明を省きます。
次セクションの `Text` のエディタカスタマイズにてコードを提示します。

###複合型
いくつかのプリミティヴ型を取りまとめた構造体またはクラスは一般的に「複合型」、または「コンポジット型」とも呼ばれます。
複合型は基本的に文字列化してやり取りを行います。

####1.FColor
`FParameter` 構造体の持つ `Color` メンバである `FColor` クラスは、プリミティヴ型で示したリストにはありません。
その場合は `GetValueAsFormattedString` 関数を用いて文字列で取得したあと、文字列を `FColor` に変換します。
FColor には `FColor::InitFromString` メンバ関数があるため、それを利用します。
また、設定する際も同様に文字列化し、`SetValueFromFormattedString` 関数で設定します。
それでは `FColor` クラスの取得・設定関数を以下に記載します。

~~~cpp:ParameterCustomization.h_追加メンバ関数
public:
	bool	GetColor( FColor & Color ) const;
	bool	SetColor( const FColor & Color );
~~~

~~~cpp:ParameterCustomization.cpp_追加メンバ関数
bool FParameterCustomization::GetColor( FColor & Color ) const
{
	FString	str;

	if( FPropertyAccess::Success != ColorHandle->GetValueAsFormattedString( str ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't get %s property." ), *(ColorHandle->GetPropertyDisplayName().ToString()) );
		return false;
	}

	Color.InitFromString( str );
	return true;
}

bool FParameterCustomization::SetColor( const FColor & Color )
{
	FString	str = FString::Printf( TEXT( "(B=%d,G=%d,R=%d,A=%d)" ), Color.B, Color.G, Color.R, Color.A );
	
	if( FPropertyAccess::Success != ColorHandle->SetValueFromFormattedString( str ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't set %s property." ), *(ColorHandle->GetPropertyDisplayName().ToString()) );
		return false;
	}

	return true;
}
~~~

掲載コードでは文字列にする際に `(B=%d,G=%d,R=%d,A=%d)` としていますが、内部では `B=` 等を一つのキーとして読み取るため、順序自体はこのとおりでなくても構いません。
`(R=%d,G=%d,B=%d,A=%d)` としても問題なく動きます。
ただし、`R = %d`のようにスペースを空けて記述すると内部で読み取ることができず、設定に失敗してしまいます。

プリミティヴ形ではないので、そもそもデリゲート登録ができません。
構造体やクラスは参照型引数にし、値を詰めます。
成功か失敗かを `bool` 値で返すようにします。

####2.FIntPoint
`FIntPoint` クラスは、`FColor` クラスが持つ `InitFromString` メンバ関数のような文字列から初期化する機能はありません。
その場合、自前で文字列を解析して値を取り出す必要があります。
ここで役に立つのが `FParse` クラスです。
以下に実装コードを示します。

~~~cpp:ParameterCustomization.h_追加メンバ関数
public:
	bool	GetPoint( FIntPoint & Point ) const;
	bool	SetPoint( const FIntPoint & Point );
~~~

~~~cpp:ParameterCustomization.cpp_追加メンバ関数
bool FParameterCustomization::GetPoint( FIntPoint & Point ) const
{
	FString	str;

	if( FPropertyAccess::Success != PointHandle->GetValueAsFormattedString( str ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't get %s property." ), *(PointHandle->GetPropertyDisplayName().ToString()) );
		return false;
	}

	FParse::Value( *str, TEXT( "X=" ), Point.X );
	FParse::Value( *str, TEXT( "Y=" ), Point.Y );

	return true;
}

bool FParameterCustomization::SetPoint( const FIntPoint & Point )
{
	FString	str = FString::Printf( TEXT( "(X=%d,Y=%d)" ), Point.X, Point.Y );

	if( FPropertyAccess::Success != PointHandle->SetValueFromFormattedString( str ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't set %s property." ), *(PointHandle->GetPropertyDisplayName().ToString()) );
		return false;
	}

	return true;
}
~~~

`FParse::Value` 関数は、実は前述の `FColor::InitFromString` 関数内でも使用されており、特定の文字列をキーとして紐付いた値を取得する機能を持ちます。
`R=` のようなスペースを含まない文字列でなければ読み取れない、という理由はこのためです。
しかし、自前で文字列解析を記述するよりはるかに楽です。

また、構造体やクラスは開始時に `(`、終了時に `)` とで閉じる必要があります。

####3.配列型
`TMap<T, T>` などの配列クラスもプロパティ化した場合、文字列化してやり取りをします。
もちろんですが、文字列を読み取る部分を自作する必要があります。
こうなると「特定型⇔文字列」と「文字列⇔プロパティハンドル」の処理を分離したほうがよいでしょう。

以下に `ArrayHandle` からの `Get`, `Set` 実装コードを記載します。

~~~cpp:ParameterCustomization.h_追加メンバ関数
public:
	bool	GetArray( TMap<int32, FIntPoint> & Array ) const;
	bool	SetArray( const TMap<int32, FIntPoint> & Array );

private:
	bool	StringToArray( TMap<int32, FIntPoint> & Array, const FString & str ) const;
	bool	ArrayToString( FString & str, const TMap<int32, FIntPoint> & Array ) const;
~~~

~~~cpp:ParameterCustomization.cpp_追加メンバ関数
bool FParameterCustomization::GetArray( TMap<int32, FIntPoint> & Array ) const
{
	FString	str;

	if( FPropertyAccess::Success != ArrayHandle->GetValueAsFormattedString( str ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't get %s property." ), *(ArrayHandle->GetPropertyDisplayName().ToString()) );
		return false;
	}

	return StringToArray( Array, str );
}

bool FParameterCustomization::SetArray( const TMap<int32, FIntPoint> & Array )
{
	FString	str;

	if( !ArrayToString( str, Array ) ) {
		return false;
	}

	if( FPropertyAccess::Success != ArrayHandle->SetValueFromFormattedString( str ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't set %s property." ), *(ArrayHandle->GetPropertyDisplayName().ToString()) );
		return false;
	}

	return true;
}

bool FParameterCustomization::StringToArray( TMap<int32, FIntPoint> & Array, const FString & str ) const
{
	int32 startPos = 1;
	int32 bracL, bracR, bracInL, bracInR;
	int32 key;
	FIntPoint value;
	bool invalid = false;
	FString pairStr, keyStr, ValueStr;

	Array.Reset();

	do {

		bracL = str.Find( _T( "(" ), ESearchCase::IgnoreCase, ESearchDir::FromStart, startPos );
		if( INDEX_NONE == bracL ) {
			break;
		}
		bracInL = str.Find( _T( "(" ), ESearchCase::IgnoreCase, ESearchDir::FromStart, bracL + 1 );
		if( INDEX_NONE == bracInL ) {
			break;
		}
		bracInR = str.Find( _T( ")" ), ESearchCase::IgnoreCase, ESearchDir::FromStart, bracInL + 1 );
		if( INDEX_NONE == bracInR ) {
			break;
		}
		bracR = str.Find( _T( ")" ), ESearchCase::IgnoreCase, ESearchDir::FromStart, bracInR + 1 );
		if( INDEX_NONE == bracL ) {
			break;
		}

		invalid = ((bracL < bracInL) && (bracInL < bracInR) && (bracInR < bracR));
		if( !invalid ) {
			break;
		}

		pairStr = str.Mid( bracL + 1, bracR - bracL - 1 );
		pairStr.Split( TEXT( "," ), &keyStr, &ValueStr );

		key = FCString::Atoi( *keyStr );
		FParse::Value( *ValueStr, TEXT( "X=" ), value.X );
		FParse::Value( *ValueStr, TEXT( "Y=" ), value.Y );

		Array.Add( key, value );

		startPos = bracR + 1;

	} while( 1 );

	return true;
}

bool FParameterCustomization::ArrayToString( FString & str, const TMap<int32, FIntPoint> & Array ) const
{
	str.Reset();

	str += TEXT( "(" );
	int count = 0, length = Array.Num();
	for( auto factor : Array ) {
		str += FString::Printf( TEXT( "(%d, (X=%d,Y=%d))" ), factor.Key, factor.Value.X, factor.Value.Y );
		++count;
		if( count != length ) {
			str += TEXT( "," );
		}
	}
	str += TEXT( ")" );

	return true;
}
~~~

他の型のプロパティハンドルから値取得・設定についても、上記の組み合わせで概ね解決できるはずです。

##4.カスタマイズ基礎１
###前準備
それではエディタのカスタマイズを行っていきます。
ここからは Slate を用います。

まず ParameterCustomization.h に以下のインクルードを追加します。

~~~cpp:ParameterCustomization.h
#pragma once

#include "CoreMinimal.h"
#include "IPropertyTypeCustomization.h"
#include "PropertyHandle.h"
#include "SlateBasics.h" // 追加
~~~

MyPluginEditor.build.cs を開き、追加で参照するモジュールを記述します。

~~~cs:MyPluginEditor.build.cs
	PublicDependencyModuleNames.AddRange(new string[] {
		"Core",
		"CoreUObject",
		"Engine",
		"UnrealEd",

		"InputCore",	// 追加
		"Slate",		// 追加
		"SlateCore",	// 追加

		"MyPlugin"
	} );
~~~

追加がわかりやすいよう少し整形しています。
この状態でビルドが通ることを確認してください。

###基本事項
`FParameterCustomization::CustomizeChildren` 関数内でカスタマイズを行っていきます。
引数である `StructBuilder` に対して、項目を追加していきます。

###Value
それでは `Value` の編集箇所を組んでみましょう。
まずはソースに `SNumericEntryBox` をインクルードします。

~~~cpp:FParameterCustomization.cpp_追加インクルード
#include "ParameterCustomization.h"
#include "PropertyEditing.h"
#include "Widgets/Input/SNumericEntryBox.h" // 追加
~~~

次に、`StructBuilder.AddProperty( ValueHandle.ToSharedRef() )`と記載していた箇所を次のように変更します。

~~~cpp:FParameterCustomization.cpp_CustomizeChildren_関数内
// この一文をコメントアウト
//	StructBuilder.AddProperty( ValueHandle.ToSharedRef() );

	StructBuilder.AddCustomRow( LOCTEXT( "ValueRow", "ValueRow" ) )
	.NameContent() [
		SNew( STextBlock )
		.Text( LOCTEXT( "Value", "Value Slidable" ) )
	]
	.ValueContent() [
		SNew( SNumericEntryBox<int32> )
		.AllowSpin( true )
		.MinValue( 0 )
		.MaxValue( 10 )
		.MinSliderValue( 0 )
		.MaxSliderValue( 10 )
		.Value_Raw( this, &FParameterCustomization::GetValue )
		.OnValueChanged( this, &FParameterCustomization::SetValue )
	];
~~~

`GetValue`, `SetValue` は前セクションにて作成した取得・設定関数になります。
ビルドが通ったら実行してみます。
![screenshot.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/693bf971-1db5-40f5-0cc1-34dc9372476a.png)
ソースコード上で `NameContent()` 以降が向かって左側、`ValueContent()` 以降が向かって右側をカスタマイズする、といったことがなんとなく分かると思います。

エディタを開くと、数値部分がキーボード入力だけでなく、スライダーで変更できるようになっています。
`AllowSpin` で `true` を設定すると、このようにスライドで値を変更できるようになります。
その際、最大値と最小値は `Max/MinSliderValue` で設定可能です。

こういった拡張によるメリットとしては

- 最大最小値が存在することで入力ミスが減らせる
- スライダーなのでマウスのみのオペレーションが可能

といったことが挙げられます。

###Text
文字列を編集するテキストボックスを作ります。
その前に、`Text` の値をプロパティハンドル越しに取得・設定する関数を作ります。
これはテキストボックスに対してデリゲート登録する関数になります。
ヘッダとソースファイルに以下の関数を追加しましょう。

~~~cpp:ParameterCustomize.h
public:
	FText	GetText() const;
	void	SetText( const FText & Text, ETextCommit::Type InCommitType );
~~~

~~~cpp:ParameterCustomize.cpp
FText FParameterCustomization::GetText() const
{
	FText	retval;

	if( FPropertyAccess::Success != TextHandle->GetValue( retval ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't get %s property." ), *(TextHandle->GetPropertyDisplayName().ToString()) );
	}

	return retval;
}

void FParameterCustomization::SetText( const FText & Text, ETextCommit::Type InCommitType )
{
	switch( InCommitType ) {
	case ETextCommit::Default:
		break;
	case ETextCommit::OnEnter:
		if( FPropertyAccess::Success != TextHandle->SetValue( Text ) ) {
			UE_LOG( LogTemp, Error, TEXT( "Couldn't set %s property." ), *(TextHandle->GetPropertyDisplayName().ToString()) );
		}
		break;
	case ETextCommit::OnUserMovedFocus:
		break;
	case ETextCommit::OnCleared:
		break;
	}
}
~~~

`SetText` 関数の引数には `FText` だけでなく、設定された際の状態を表す列挙体があります。
状態に応じて処理を書き分けることが可能ですが、今回は Enter を押して決定された際にプロパティハンドルに設定する処理だけで良いでしょう。

では `Value` と同様に、`AddProperty` と記述していた箇所を以下のように変更します。

~~~cpp:ParameterCustomization.cpp_CustomizeChildren_関数内

// この一文をコメントアウト
//	StructBuilder.AddProperty( TextHandle.ToSharedRef() );

	StructBuilder.AddCustomRow( LOCTEXT( "TextRow", "TextRow" ) )
	.NameContent() [
		SNew( STextBlock )
		.Text( LOCTEXT( "Text", "Text Input" ) )
	]
	.ValueContent() [
		SNew( SEditableTextBox )
		.HintText( LOCTEXT( "HintText", "Please input here." ) )
		.IsReadOnly( false )
		.SelectAllTextWhenFocused( true )
		.Text_Raw( this, &FParameterCustomization::GetText )
		.OnTextCommitted_Raw( this, &FParameterCustomization::SetText )
	];
~~~

実行してみます。

![screenshot.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/cb304f54-a245-e43a-0d39-6876fa6642a8.png)

ヒントテキストとしてうっすら「Please input here」と書かれています。
テキストを入力すると、Enter を押した際に反映されます。
また、`SelectAllTextWhenFocused` で `true` を指定しているので、フォーカスを入れた際に文字列を全選択状態にします。

こういった拡張によるメリットとしては

- ヒントテキストで何を入力するのか先に伝えることができる
- 入力してはいけない文字などが入っていた場合、`SetText` 関数内で対処できる
- 頻繁に再入力することが想定される場合、`SelectAllTextWhenFocused` を用いて文字列を削除する手間を省ける
- その他、テキスト変更時の処理を `ETextCommit` で切り分けることができる

といったことが挙げられます。

###Color
実は `FColor` の場合、デフォルトのエディタを使用するのが最も効率的です。
（プロパティ取得・設定関数を作らせておいて怒られそうですが。）
しかし、デフォルトのエディタを Slate で記述するにはなかなか大変です。
そのため、`FColor` に関しては「プロパティ値が変更された際のデリゲート」を組んでみます。

では「プロパティ値が変更された」ことを検知する関数を作ります。

~~~cpp:ParameterCustomization.h
private:
	void	OnChangedColor();
~~~

~~~cpp:ParameterCustomization.cpp
void FParameterCustomization::OnChangedColor()
{
	FColor color;

	GetColor( color );

	UE_LOG( LogTemp, Log, TEXT( "R=%d, G=%d, B=%d, A=%d" ), color.R, color.G, color.B, color.A );
}
~~~

現在のカラー値を取得してログ出力します。
それでは `CustomizeHeader` 関数内で `ColorHandle` を取得後に以下の処理を書き足します。

~~~cpp:ParameterCustomization.cpp_CustomizeHeader関数内
	ColorHandle = StructPropertyHandle->GetChildHandle( GET_MEMBER_NAME_CHECKED( FParameter, Color ) );

	if( ColorHandle.IsValid() ) {
		ColorHandle->SetOnPropertyValueChanged(
			FSimpleDelegate::CreateRaw( this, &FParameterCustomization::OnChangedColor )
		);
	}
~~~

これで間接的に色が変更されたことを検知した処理を組むことが可能になります。
カラーパレット上で値を少しでも変更すれば、検知して呼ばれますが、アルファ値がゼロだと RGB 値もゼロで返されるので注意してください。
もちろん、自作した `SetColor` 関数を呼んで手動で色を設定してもこちらのデリゲート処理が呼ばれます。

カラー値を他の要素に対して反映させたい場合、`OnChangedColor` 関数内で対応することができます。

##5.カスタマイズ基礎２
カスタマイズ基礎では `SNumericEntryBox` や `SEditableTextBox` を用いました。
ここでは Slate クラスを自作してエディタに組み込んでみます。

###FIntPoint
`FParameterCustomization` クラスでデリゲート登録できるパーツ的なクラスを作っていきます。

新規でソースコードを書き起こします。
ブランクコードであればよいので、継承無しで Unreal Editor から追加しても良いですし、Visual Studio 側で追加しても構いません。
今回は Unreal Editor から追加します。
![screenshot.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/db518876-167c-f539-92fb-f395484f4e07.png)
「PointEditor」という名称にしました。
また、外部に公開するつもりはないため Private 設定を選択しています。
フォルダは Private 側に Slates フォルダを作りました。

それではヘッダファイルとソースファイルを書き換えます。
まずはヘッダファイルからです。

~~~cpp:PointEditor.h
#pragma once

#include "CoreMinimal.h"
#include "SlateBasics.h"

class SPointEditor : public SCompoundWidget {
public:
	DECLARE_DELEGATE_RetVal_OneParam( bool, FGetIntPoint, FIntPoint & );
	DECLARE_DELEGATE_RetVal_OneParam( bool, FOnChangedIntPoint, const FIntPoint & );

	SLATE_BEGIN_ARGS( SPointEditor ) {}

	SLATE_EVENT( FGetIntPoint, GetIntPoint )
	SLATE_EVENT( FOnChangedIntPoint, OnChangedIntPoint )

	SLATE_END_ARGS()

	void Construct( const FArguments & InArgs );

private:
	TOptional<int32>	OnGetX() const;
	TOptional<int32>	OnGetY() const;
	void				OnChangedX( int32 value );
	void				OnChangedY( int32 value );

private:
	FGetIntPoint		GetPoint;
	FOnChangedIntPoint	OnChangedPoint;
};
~~~

`SCompoundWidget` クラスを継承すると、自前の Slate クラスを構築できます。
Slate クラスは `SLATE_` で始まるマクロを用いてコンストラクタを乗っ取るため、`void Construct( const FArguments & InArgs )` 関数を別途定義しておきます。
インスタンス作成時に必ず呼ばれますので、コンストラクタで行うような処理があればこちらの関数内で記述してください。

また、デストラクタは定義してもしなくてもよいです。
なにかメモリ管理を行うなら、デストラクタを定義してその中で破棄すると良いでしょう。

冒頭で `DECRARE_DELEGATE_RetVal_OneParam` マクロで定義しているのは、自前のデリゲート処理です。
このように定義した上で、`SLATE_BEGIN_ARGS` と `SLATE_END_ARGS` の間に `SLATE_EVENT` を記述すると、外部で Slate 記述を行う際にデリゲート登録できるようになります。
デリゲートクラス定義はクラス内で行っておくべきで、グローバルで定義すべきではありません。

デリゲートクラス定義マクロについては、返り値なし引数の個数に応じたものがあります。
用途に応じて使用してください。

あとは `Point` のメンバである `X` と `Y` を取得・設定する関数の定義をしています。

次はソースコードです。

~~~cpp:PointEditor.cpp
#include "Slates/PointEditor.h"
#include "Widgets/Input/SNumericEntryBox.h"

#define LOCTEXT_NAMESPACE "FMyPluginEditor"

void SPointEditor::Construct( const FArguments & InArgs )
{
	GetPoint = InArgs._GetIntPoint;
	OnChangedPoint = InArgs._OnChangedIntPoint;

	ChildSlot[
		SNew( SVerticalBox )
		+ SVerticalBox::Slot() [

			SNew( SNumericEntryBox<int32> )
			.AllowSpin( true )
			.MinValue( -2 )
			.MaxValue( 2 )
			.MinSliderValue( -2 )
			.MaxSliderValue( 2 )
			.Value_Raw( this, &SPointEditor::OnGetX )
			.OnValueChanged( this, &SPointEditor::OnChangedX )
		]
		.Padding( FMargin( 1.0f, 1.0f ) )
		.VAlign( EVerticalAlignment::VAlign_Center )
		+ SVerticalBox::Slot() [

			SNew( SNumericEntryBox<int32> )
			.AllowSpin( true )
			.MinValue( -2 )
			.MaxValue( 2 )
			.MinSliderValue( -2 )
			.MaxSliderValue( 2 )
			.Value_Raw( this, &SPointEditor::OnGetY )
			.OnValueChanged( this, &SPointEditor::OnChangedY )
		]
		.Padding( FMargin( 1.0f, 1.0f ) )
		.VAlign( EVerticalAlignment::VAlign_Center )
	];
}

TOptional<int32> SPointEditor::OnGetX() const
{
	FIntPoint point( 0, 0 );

	GetPoint.Execute( point );

	return point.X;
}

TOptional<int32> SPointEditor::OnGetY() const
{
	FIntPoint point( 0, 0 );

	GetPoint.Execute( point );

	return point.Y;
}

void SPointEditor::OnChangedX( int32 value )
{
	FIntPoint point( 0, 0 );

	GetPoint.Execute( point );

	point.X = value;

	OnChangedPoint.Execute( point );
}

void SPointEditor::OnChangedY( int32 value )
{
	FIntPoint point( 0, 0 );

	GetPoint.Execute( point );

	point.Y = value;

	OnChangedPoint.Execute( point );
}

#undef LOCTEXT_NAMESPACE
~~~

`SCompoundWidget` メンバである `ChildSlot` に、`X` と `Y` を編集するための `SNumericEntryBox` を記述しています。
`SNumericEntryBox` 自体は `Value` でも使用しました。
`OnGetX`, `OnGetY`, `OnChangedX`, `OnChangedY` 関数もそれぞれ定義し、デリゲート登録しています。

また、それぞれの関数内で、`GetPoint`、`OnChangePoint` デリゲートクラスを実行しています。
本来であればデリゲートクラスの持つ `IsBound()` 関数などでチェックしたほうがよいですが、今回は省きます。

ビルドが通るのを確認したら、今度は `FParameterCustomization` クラスに `SPointEditor` クラスを組み込んでみましょう。

~~~cpp:FParameterCustomization.cpp_追加インクルード
#include "ParameterCustomization.h"
#include "PropertyEditing.h"
#include "Widgets/Input/SNumericEntryBox.h"
#include "Slates/PointEditor.h" // 追加
~~~

~~~cpp:ParameterCustomization.cpp_CustomizeChildren_関数内
// この一文をコメントアウト
//	StructBuilder.AddProperty( PointHandle.ToSharedRef() );

	StructBuilder.AddCustomRow( LOCTEXT( "TextRow", "TextRow" ) )
	.NameContent() [
		SNew( STextBlock )
		.Text( LOCTEXT( "Point", "Point Customize" ) )
	]
	.ValueContent() [
		SNew( SPointEditor )
		.GetIntPoint_Raw( this, &FParameterCustomization::GetPoint )
		.OnChangedIntPoint_Raw( this, &FParameterCustomization::SetPoint )
	];
~~~

`FIntPoint` を取得・設定する関数を組み込んでいます。
ParameterCustomization.cpp に `Point` のエディタを作り込むのもいいのですが、Slate の記述が膨れ上がりそうであれば、素直に別クラス化したほうが取り回しがしやすいです。

ここまで順調にできていれば、エディタ起動時に以下のようになっているはずです。

![screenshot.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/2362790b-0a37-824e-b172-7f9dd0eeb603.png)

`Point` の `X` と `Y` がそれぞれ -2 から 2 の間でスライドできるようになっています。
しかし、これではどちらが `X` で `Y` なのか分かりません。
テキスト表示を追加してみましょう。
PointEditor.cpp の `ChildSlot` 周りを書き換えます。

~~~cpp:PointEditor.cpp_Construct関数ChildSlot部
	ChildSlot[
		SNew( SVerticalBox )
		+ SVerticalBox::Slot() [

			SNew( SHorizontalBox )
			+ SHorizontalBox::Slot()[

				SNew( STextBlock )
				.Text( LOCTEXT( "PointX", "X" ) )
			]
			.Padding( FMargin( 1.0f, 1.0f ) )
			.HAlign( EHorizontalAlignment::HAlign_Left )
			+ SHorizontalBox::Slot()[

				SNew( SNumericEntryBox<int32> )
				.AllowSpin( true )
				.MinValue( -2 )
				.MaxValue( 2 )
				.MinSliderValue( -2 )
				.MaxSliderValue( 2 )
				.Value_Raw( this, &SPointEditor::OnGetX )
				.OnValueChanged( this, &SPointEditor::OnChangedX )
			]
			.Padding( FMargin( 1.0f, 1.0f ) )
			.HAlign( EHorizontalAlignment::HAlign_Fill )
		]
		.Padding( FMargin( 1.0f, 1.0f ) )
		.VAlign( EVerticalAlignment::VAlign_Center )
		+ SVerticalBox::Slot() [

			SNew( SHorizontalBox )
			+ SHorizontalBox::Slot()[

				SNew( STextBlock )
				.Text( LOCTEXT( "PointY", "Y" ) )
			]
			.Padding( FMargin( 1.0f, 1.0f ) )
			.HAlign( EHorizontalAlignment::HAlign_Left )
			+ SHorizontalBox::Slot()[

				SNew( SNumericEntryBox<int32> )
				.AllowSpin( true )
				.MinValue( -2 )
				.MaxValue( 2 )
				.MinSliderValue( -2 )
				.MaxSliderValue( 2 )
				.Value_Raw( this, &SPointEditor::OnGetY )
				.OnValueChanged( this, &SPointEditor::OnChangedY )
			]
			.Padding( FMargin( 1.0f, 1.0f ) )
			.HAlign( EHorizontalAlignment::HAlign_Fill )
		]
		.Padding( FMargin( 1.0f, 1.0f ) )
		.VAlign( EVerticalAlignment::VAlign_Center )
	];
~~~

テキストを追加するだけでかなり長くなってしまっています。
Slate は 1 Slot に対して 1 Widget クラス、という取り決めがあるため、Slot を追加しなければ要素を追加できません。
`SVerticalBox` は Slot を縦方向に、`SHorizontalBox` は Slot を横方向に追加していく Widget です。

これでビルドして実行すると、以下のようになります。

![screenshot.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/261e8cf9-9688-fa47-05ff-a53d47211e38.png)

エリアも決まっているため、むりやり押し込んだ形になっています。
この状態でもよいかもしれませんが、格子状に Widget を配置する場合は `SGridPanel` を用いると良い場合があります。
`SGridPanel` に差し替えてみましょう。

~~~cpp:PointEditor.cpp_Construct関数ChildSlot部
	ChildSlot[
		SNew( SGridPanel )
		+ SGridPanel::Slot( 0, 0 ) [

			SNew( SBox )
			.WidthOverride( 30.0f ) [

				SNew( STextBlock )
				.Text( LOCTEXT( "PointX", "X" ) )
			]
			.HAlign( EHorizontalAlignment::HAlign_Center )
		]
		+ SGridPanel::Slot( 0, 1 ) [

			SNew( SBox )
			.WidthOverride( 30.0f ) [

				SNew( STextBlock )
				.Text( LOCTEXT( "PointY", "Y" ) )
			]
			.HAlign( EHorizontalAlignment::HAlign_Center )
		]
		+ SGridPanel::Slot( 1, 0 ) [

			SNew( SBox )
			.WidthOverride( 120.0f ) [

				SNew( SNumericEntryBox<int32> )
				.AllowSpin( true )
				.MinValue( -2 )
				.MaxValue( 2 )
				.MinSliderValue( -2 )
				.MaxSliderValue( 2 )
				.Value_Raw( this, &SPointEditor::OnGetX )
				.OnValueChanged( this, &SPointEditor::OnChangedX )
			]
			.HAlign( EHorizontalAlignment::HAlign_Fill )
		]
		+ SGridPanel::Slot( 1, 1 ) [

			SNew( SBox )
			.WidthOverride( 120.0f )[

				SNew( SNumericEntryBox<int32> )
				.AllowSpin( true )
				.MinValue( -2 )
				.MaxValue( 2 )
				.MinSliderValue( -2 )
				.MaxSliderValue( 2 )
				.Value_Raw( this, &SPointEditor::OnGetY )
				.OnValueChanged( this, &SPointEditor::OnChangedY )
			]
			.HAlign( EHorizontalAlignment::HAlign_Fill )
		]
	];
~~~

`SGridPanel` クラスで Slot を指定箇所に追加しています。
さらに `SBox` クラスを用いて幅を調整しています。
この状態で実行すると以下のようになります。
![screenshot.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/67d23aa8-8caf-3c9d-5efa-094f058800ed.png)
幅がカスタマイズに応じてきちんと調整されているのがわかります。
`SVerticalBox`、`SHorizontalBox`、`SGridPanel` は状況に応じてどれを仕様すべきか判断しましょう。

要素同士がくっついてしまっているのを離したい、といった場合は、`.Padding` を設定するとマージンを取ることができます。
試しに `SNumericEntryBox` が入っている `SBox` の `HAlign` の記述の下に追記してみましょう。

~~~cpp:PointEditor.cpp_Construct関数ChildSlot部
	.HAlign( EHorizontalAlignment::HAlign_Fill )
	.Padding( FMargin( 1.0f, 1.0f ) )
~~~

これで実行すると、合間に少しエリアができた状態になります。
![screenshot.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/77f0ba8d-76d8-5376-d1e7-edd85d869e62.png)
レイアウトを調整して見やすくすることは、エディタの使いやすさに直結します。
うまく調整していきましょう。

##6.カスタマイズ応用
###Array(TMap<int32, FIntPoint>)

最終セクションとして、すでに操作できる `Value` 値に応じて `Array` メンバの配列サイズを変更したり、入力した `Point` メンバの値を一括で入力してみます。

前準備として、`PointEditor2` クラスを作成します。
PointEditor.cpp/h をコピー＆ペースト、リネームして Visual Studio プロジェクトに追加しましょう。
配列のインデクスを識別する機能を追加したものになります。

まずはヘッダファイルです。

~~~cpp:PointEditor2.h
#pragma once

#include "CoreMinimal.h"
#include "SlateBasics.h"

class SPointEditor2 : public SCompoundWidget {
public:
	DECLARE_DELEGATE_RetVal_TwoParams( bool, FGetIntPoint, FIntPoint &, int32 Index );
	DECLARE_DELEGATE_RetVal_TwoParams( bool, FOnChangedIntPoint, const FIntPoint &, int32 Index );

	SLATE_BEGIN_ARGS( SPointEditor2 ) {}

	SLATE_ATTRIBUTE( int32, Index )

	SLATE_EVENT( FGetIntPoint, GetIntPoint )
	SLATE_EVENT( FOnChangedIntPoint, OnChangedIntPoint )

	SLATE_END_ARGS()

	void Construct( const FArguments & InArgs );

private:
	TOptional<int32>	OnGetX() const;
	TOptional<int32>	OnGetY() const;
	void				OnChangedX( int32 value );
	void				OnChangedY( int32 value );

protected:
	TAttribute<int32>	Index;
	FGetIntPoint		GetPoint;
	FOnChangedIntPoint	OnChangedPoint;
};
~~~

前回作成した　PointEditor.h と比較すると、デリゲートの引数が一つ増えています。
そしてその引数に渡すための `Index` を新たに定義し、`SLATE_ATTRIBUTE` として追加しています。
それ以外は `PointEditor` クラスと同様です。

次はソースファイルです。

~~~cpp:PointEditor2.cpp
#include "Slates/PointEditor2.h"
#include "Widgets/Input/SNumericEntryBox.h"

#define LOCTEXT_NAMESPACE "FMyPluginEditor"

void SPointEditor2::Construct( const FArguments & InArgs )
{
	Index = InArgs._Index;
	GetPoint = InArgs._GetIntPoint;
	OnChangedPoint = InArgs._OnChangedIntPoint;

	ChildSlot[
		SNew( SGridPanel )
		+ SGridPanel::Slot( 0, 0 ) [

			SNew( SBox )
			.WidthOverride( 30.0f ) [

				SNew( STextBlock )
				.Text( LOCTEXT( "PointX", "X" ) )
			]
			.HAlign( EHorizontalAlignment::HAlign_Center )
		]
		+ SGridPanel::Slot( 0, 1 ) [

			SNew( SBox )
			.WidthOverride( 30.0f ) [

				SNew( STextBlock )
				.Text( LOCTEXT( "PointY", "Y" ) )
			]
			.HAlign( EHorizontalAlignment::HAlign_Center )
		]
		+ SGridPanel::Slot( 1, 0 ) [

			SNew( SBox )
			.WidthOverride( 120.0f ) [

				SNew( SNumericEntryBox<int32> )
				.AllowSpin( true )
				.MinValue( -2 )
				.MaxValue( 2 )
				.MinSliderValue( -2 )
				.MaxSliderValue( 2 )
				.Value_Raw( this, &SPointEditor2::OnGetX )
				.OnValueChanged( this, &SPointEditor2::OnChangedX )
			]
			.HAlign( EHorizontalAlignment::HAlign_Fill )
			.Padding( FMargin( 1.0f, 1.0f ) )
		]
		+ SGridPanel::Slot( 1, 1 ) [

			SNew( SBox )
			.WidthOverride( 120.0f )[

				SNew( SNumericEntryBox<int32> )
				.AllowSpin( true )
				.MinValue( -2 )
				.MaxValue( 2 )
				.MinSliderValue( -2 )
				.MaxSliderValue( 2 )
				.Value_Raw( this, &SPointEditor2::OnGetY )
				.OnValueChanged( this, &SPointEditor2::OnChangedY )
			]
			.HAlign( EHorizontalAlignment::HAlign_Fill )
			.Padding( FMargin( 1.0f, 1.0f ) )
		]
	];
}

TOptional<int32> SPointEditor2::OnGetX() const
{
	FIntPoint point( 0, 0 );

	GetPoint.Execute( point, Index.Get( 0 ) );

	return point.X;
}

TOptional<int32> SPointEditor2::OnGetY() const
{
	FIntPoint point( 0, 0 );

	GetPoint.Execute( point, Index.Get( 0 ) );

	return point.Y;
}

void SPointEditor2::OnChangedX( int32 value )
{
	FIntPoint point( 0, 0 );

	GetPoint.Execute( point, Index.Get( 0 ) );

	point.X = value;

	OnChangedPoint.Execute( point, Index.Get( 0 ) );
}

void SPointEditor2::OnChangedY( int32 value )
{
	FIntPoint point( 0, 0 );

	GetPoint.Execute( point, Index.Get( 0 ) );

	point.Y = value;

	OnChangedPoint.Execute( point, Index.Get( 0 ) );
}

#undef LOCTEXT_NAMESPACE
~~~

`Index` は `TAttribute<int32>` 型になりますので、`Get` 関数で取得します。
`Get` 関数の引数はデフォルト値です。

それでは配列を編集するクラスを作っていきましょう。
名称は「ArrayEditor」とし、PointEditor2.cpp/h と同フォルダ内に追加します。

まずはヘッダファイルです。

~~~cpp:ArrayEditor.h
#include "CoreMinimal.h"
#include "SlateBasics.h"

class SArrayEditor : public SCompoundWidget {
public:
	using ArrayType = TMap<int32, FIntPoint>;
	DECLARE_DELEGATE_RetVal_OneParam( bool, FGetArray, ArrayType & );
	DECLARE_DELEGATE_RetVal_OneParam( bool, FOnChangedArray, const ArrayType & );

	SLATE_BEGIN_ARGS( SArrayEditor ) {}

	SLATE_EVENT( FGetArray, GetArray )
	SLATE_EVENT( FOnChangedArray, OnChangedArray )

	SLATE_END_ARGS()

	void Construct( const FArguments & InArgs );

	void SetArraySize( int32 InSize );

	void UpdateView();

private:
	bool	GetPoint( FIntPoint & Point, int32 Index ) const;
	bool	OnChangedPoint( const FIntPoint & Point, int32 Index );

protected:
	TSharedPtr<SGridPanel>	GridPanel;
	FGetArray				GetArray;
	FOnChangedArray			OnChangedArray;
};
~~~

`TMap<int32, FIntPoint>` 型を操作するクラスになっています。
デリゲートもそれに応じた形を用意しています。

注意点としては、`TMap<int32, FIntPoint>` のままデリゲート定義マクロに書き込むと `,` を区切りと判定されてしまいビルド時に失敗します。
直前で `using` を用いて型定義し直しているのはそのためです。

次はソースファイルです。

~~~cpp:ArrayEditor.cpp

#define LOCTEXT_NAMESPACE "FMyPluginEditor"

void SArrayEditor::Construct( const FArguments & InArgs )
{
	GetArray = InArgs._GetArray;
	OnChangedArray = InArgs._OnChangedArray;

	ChildSlot[
		SAssignNew( GridPanel, SGridPanel )
	];

	UpdateView();
}

void SArrayEditor::SetArraySize( int32 InSize )
{
	TMap<int32, FIntPoint> newMap, oldMap;
	TSet<int32> Keys;
	int32 nowKey;
	FIntPoint Val;

	GetArray.Execute( oldMap );

	oldMap.GetKeys( Keys );

	for( int32 i = 0; i < InSize; ++i ) {

		nowKey = i;
		Val = FIntPoint( 0, 0 );

		if( 0 < Keys.Num() ) {
			if( const auto * pValue = oldMap.Find( nowKey ) ) {
				Val = *pValue;
			}
		}

		newMap.Add( nowKey, Val );
	}

	OnChangedArray.Execute( newMap );
}

void SArrayEditor::UpdateView()
{
	TMap<int32, FIntPoint> arrayMap;
	int32 arraySize = 0;

	GetArray.Execute( arrayMap );

	arraySize = arrayMap.Num();

	GridPanel->ClearChildren();

	for( int32 i = 0; i < arraySize; ++i ) {

		FString label = FString::Printf( TEXT( "%d" ), i );

		GridPanel->AddSlot( 0, i )[
			SNew( STextBlock )
			.Text( FText::FromString( label ) )
		]
		.VAlign( EVerticalAlignment::VAlign_Center );

		GridPanel->AddSlot( 1, i )[
			SNew( SPointEditor2 )
			.Index( i )
			.GetIntPoint_Raw( this, &SArrayEditor::GetPoint )
			.OnChangedIntPoint_Raw( this, &SArrayEditor::OnChangedPoint )
		]
		.Padding( 1.0f, 1.0f );
	}

	Invalidate( EInvalidateWidget::Paint );
}

bool SArrayEditor::GetPoint( FIntPoint & Point, int32 Index ) const
{
	TMap<int32, FIntPoint> arrayMap;

	GetArray.Execute( arrayMap );

	if( const auto * pValue = arrayMap.Find( Index ) ) {
		Point = *pValue;
	}

	return true;
}

bool SArrayEditor::OnChangedPoint( const FIntPoint & Point, int32 Index )
{
	TMap<int32, FIntPoint> arrayMap;

	GetArray.Execute( arrayMap );

	if( auto * pValue = arrayMap.Find( Index ) ) {
		*pValue = Point;
	}

	OnChangedArray.Execute( arrayMap );

	return true;
}

#undef LOCTEXT_NAMESPACE
~~~

`SGridPanel` に対して `SNew` ではなく `SAssignNew` を用いています。
`SAssignNew` は `SharedPtr<T>` の変数を元に Slate を構築します。
メンバ変数に持った状態で構築すれば、Slate の定義が終わった後にも制御が可能になります。

実際、`UpdateView()` 関数の内部では `GridPanel` メンバを一度クリアし、引数に応じて Slot を追加しています。
このようにすることで、エディタ状の表示を動的に切り替えることができます。

`SPointEditor2` クラスには `Index` が定義してあり、これに応じて配列のインデクスを `PointEditor2` クラスごとに持たせます。
`Index` の判別処理自体は `SArrayEditor` 側で行います。

最後に `Invalidate( EInvalidateWidget::Paint )` を呼ぶことで強制的に表示を更新しています。

それでは `SArrayEditor` クラスを `FParameterCustomization` クラスに組み込んでみます。

~~~cpp:FParameterCustomize.h
// 新しくインクルードを追加
#include "Slates/ArrayEditor.h"

class FParameterCustomization : public IPropertyTypeCustomization {
public:
	FParameterCustomization();

/***** 中略 *****/

	// 新しくメンバを追加
	TSharedPtr<SArrayEditor>	ArrayEditor;
};
~~~

~~~cpp:FParameterCustomize.cpp_ChildSlot内
// この一文をコメントアウト
//	StructBuilder.AddProperty( ArrayHandle.ToSharedRef() );

	StructBuilder.AddCustomRow( LOCTEXT( "TextRow", "TextRow" ) )
	.NameContent()[
		SNew( STextBlock )
			.Text( LOCTEXT( "Array", "Array Customize" ) )
	]
	.ValueContent()[
		SAssignNew( ArrayEditor, SArrayEditor )
		.GetArray_Raw( this, &FParameterCustomization::GetArray )
		.OnChangedArray_Raw( this, &FParameterCustomization::SetArray )
	];
~~~

`FParameterCustomization` 内でも `SArrayEditor` を `SAssignNew` します。
事前に作成しておいた配列の取得と設定関数をデリゲート登録します。

最後に `SetValue` 関数内に、`SArrayEditor` の制御を記述します。

~~~cpp:FParameterCustomize.cpp
void FParameterCustomization::SetValue( int32 Value )
{
	if( FPropertyAccess::Success != ValueHandle->SetValue( Value ) ) {
		UE_LOG( LogTemp, Error, TEXT( "Couldn't set %s property." ), *(ValueHandle->GetPropertyDisplayName().ToString()) );
	}

	ArrayEditor->SetArraySize( Value );	// 追加
	ArrayEditor->UpdateView();			// 追加
}
~~~

ビルドしてエディタを立ち上げてみます。
`Value` をスライドさせると、`Array` の最大数が動的に変化します。
![ArrayEditor2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70392/af5c547c-4ed4-39d8-1655-f98eefc15b6b.gif)
配列の要素である `FIntPoint` クラスも、インデクスごとに値が保持されます。
保存して終了した後、再度立ち上げると値が保持されていることが確認できるでしょう。

#おわりに
できる限り具体的なコードを掲載したのでかなりの長編になってしまいました。
前編・後編ともに全セクションをこなせば、手元に少しだけ有用なサンプルが出来上がっていると思います。
ここから Slate についての理解を深めていっていただけるとありがたいです。

まだ `SComboBox` や `SButton` など説明しきれていないものもありますので、後日別記事として上げル予定です。
以上、読んでいただきありがとうございました。

#参考リンク集
※前編にも同じリンクを載せています。

https://qiita.com/ayumu_nagamatsu/items/c384e1d7f7ec671388ed

https://qiita.com/negimochi/items/bf67d7b1f9009c961178

https://kazuhiro77unreal.hatenablog.com/entry/2018/04/08/130131

https://tech.spark-creative.co.jp/entry/2021/04/02/194352

https://easycomplex-tech.com/blog/Unreal-AssetEditor/

https://www.orfeasel.com/creating-custom-editor-assets/

https://gmpreussner.com/reference/adding-new-asset-types-to-ue4#top

https://dokuro.moe/ue4-cpp-how-to-use-delegate/