1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-12-23

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

前編はこちら:

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

#プロパティカスタマイズ手順
##1.プロパティカスタマイズクラスの作成
FParameter 構造体のプロパティをカスタマイズするクラスを作っていきます。
IPropertyTypeCustomization インターフェイスを継承する必要がありますが、これもまた Unreal Editor 上からは選択できないため、継承なしでソースコードを追加してから内容を改変します。
screenshot.23.png
名称は ParameterCustomization としました。
追加先は「MyPluginEditor (Editor)」です。
ソースコードを改変します。

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" 以下をコピペしてください。

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 を開いてください。

MyPluginEditor.cpp
#include "MyPluginEditor.h"
#include "MyAssetActions.h"
#include "AssetToolsModule.h"
#include "ParameterCustomization.h" // 追加
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
パッと見前回の状態と変わっていないように見えますが、Parameter というくくりが消えています。
また、FParameterCustomization::CustomizeHeader 関数内にブレークポイントを張って Visual Studio 上から実行すれば、張った場所でブレークすることが確認できます。

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

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

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

ParameterCustomization.h
public:
	TOptional<int32>	GetValue() const;
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() メンバ関数で値を取り出します。

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

次は設定関数です。

ParameterCustomization.h
public:
	bool	SetValue( int32 Value );
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 クラスの取得・設定関数を以下に記載します。

ParameterCustomization.h_追加メンバ関数
public:
	bool	GetColor( FColor & Color ) const;
	bool	SetColor( const FColor & Color );
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 クラスです。
以下に実装コードを示します。

ParameterCustomization.h_追加メンバ関数
public:
	bool	GetPoint( FIntPoint & Point ) const;
	bool	SetPoint( const FIntPoint & Point );
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 実装コードを記載します。

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;
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.カスタマイズ基礎1
###前準備
それではエディタのカスタマイズを行っていきます。
ここからは Slate を用います。

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

ParameterCustomization.h
#pragma once

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

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

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

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

		"MyPlugin"
	} );

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

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

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

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

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

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
ソースコード上で NameContent() 以降が向かって左側、ValueContent() 以降が向かって右側をカスタマイズする、といったことがなんとなく分かると思います。

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

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

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

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

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

ParameterCustomize.h
public:
	FText	GetText() const;
	void	SetText( const FText & Text, ETextCommit::Type InCommitType );
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 と記述していた箇所を以下のように変更します。

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

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

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

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

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

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

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

ParameterCustomization.h
private:
	void	OnChangedColor();
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 を取得後に以下の処理を書き足します。

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.カスタマイズ基礎2
カスタマイズ基礎では SNumericEntryBoxSEditableTextBox を用いました。
ここでは Slate クラスを自作してエディタに組み込んでみます。

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

新規でソースコードを書き起こします。
ブランクコードであればよいので、継承無しで Unreal Editor から追加しても良いですし、Visual Studio 側で追加しても構いません。
今回は Unreal Editor から追加します。
screenshot.33.png
「PointEditor」という名称にしました。
また、外部に公開するつもりはないため Private 設定を選択しています。
フォルダは Private 側に Slates フォルダを作りました。

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

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_ARGSSLATE_END_ARGS の間に SLATE_EVENT を記述すると、外部で Slate 記述を行う際にデリゲート登録できるようになります。
デリゲートクラス定義はクラス内で行っておくべきで、グローバルで定義すべきではありません。

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

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

次はソースコードです。

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 に、XY を編集するための SNumericEntryBox を記述しています。
SNumericEntryBox 自体は Value でも使用しました。
OnGetX, OnGetY, OnChangedX, OnChangedY 関数もそれぞれ定義し、デリゲート登録しています。

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

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

FParameterCustomization.cpp_追加インクルード
#include "ParameterCustomization.h"
#include "PropertyEditing.h"
#include "Widgets/Input/SNumericEntryBox.h"
#include "Slates/PointEditor.h" // 追加
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

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

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

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

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
幅がカスタマイズに応じてきちんと調整されているのがわかります。
SVerticalBoxSHorizontalBoxSGridPanel は状況に応じてどれを仕様すべきか判断しましょう。

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

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

これで実行すると、合間に少しエリアができた状態になります。
screenshot.37.png
レイアウトを調整して見やすくすることは、エディタの使いやすさに直結します。
うまく調整していきましょう。

##6.カスタマイズ応用
###Array(TMap)

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

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

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

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 クラスと同様です。

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

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

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

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

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

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 を用いて型定義し直しているのはそのためです。

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

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 を用いています。
SAssignNewSharedPtr<T> の変数を元に Slate を構築します。
メンバ変数に持った状態で構築すれば、Slate の定義が終わった後にも制御が可能になります。

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

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

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

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

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

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

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

	// 新しくメンバを追加
	TSharedPtr<SArrayEditor>	ArrayEditor;
};
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 内でも SArrayEditorSAssignNew します。
事前に作成しておいた配列の取得と設定関数をデリゲート登録します。

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

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
配列の要素である FIntPoint クラスも、インデクスごとに値が保持されます。
保存して終了した後、再度立ち上げると値が保持されていることが確認できるでしょう。

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

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

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

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?