#本記事について
本記事は、筆者が UE4 を用いて独自のアセット及びそのエディタを組んでいた際のメモを、マニュアルとしてまとめ直したものです。
前編・後編の 2 つに分かれており、本記事は後編になります。
前編はこちら:
前編で用いた .uproject をそのまま使用していきます。
#プロパティカスタマイズ手順
##1.プロパティカスタマイズクラスの作成
FParameter
構造体のプロパティをカスタマイズするクラスを作っていきます。
IPropertyTypeCustomization
インターフェイスを継承する必要がありますが、これもまた Unreal Editor 上からは選択できないため、継承なしでソースコードを追加してから内容を改変します。
名称は ParameterCustomization
としました。
追加先は「MyPluginEditor (Editor)」です。
ソースコードを改変します。
#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"
以下をコピペしてください。
#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 を開いてください。
#include "MyPluginEditor.h"
#include "MyAssetActions.h"
#include "AssetToolsModule.h"
#include "ParameterCustomization.h" // 追加
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 クラスアセットをダブルクリックし、エディタを起動してみます。
パッと見前回の状態と変わっていないように見えますが、Parameter というくくりが消えています。
また、FParameterCustomization::CustomizeHeader
関数内にブレークポイントを張って Visual Studio 上から実行すれば、張った場所でブレークすることが確認できます。
##3.プロパティハンドルからの値取得・設定方法について
さっそくエディタのカスタマイズを……と行きたいところですが、その前にプロパティハンドル越しに値の取得と設定を行う部分を説明します。
エディタ上から編集する値を参照することはよくありますし、特定の値が設定された際に別の項目も一緒に設定する、といったこともよくあるためです。
FParameter
構造体のメンバをプロパティハンドル越しに取得・設定していきます。
###プリミティヴ型
もともとプログラム自体に定義されており、「基本データ型」や「組み込み型」、「ビルトイン型」とも呼ばれるものです。
ValueHandle
から Value
プロパティの値を取得する関数を追加してみます。
Value
メンバは int32
型、つまりプリミティヴ型なので素直に記述できます。
public:
TOptional<int32> GetValue() const;
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()
メンバ関数で値を取り出します。
取得できなかった場合は一応エラーを表示しています。
次は設定関数です。
public:
bool SetValue( int32 Value );
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
クラスの取得・設定関数を以下に記載します。
public:
bool GetColor( FColor & Color ) const;
bool SetColor( const FColor & Color );
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
クラスです。
以下に実装コードを示します。
public:
bool GetPoint( FIntPoint & Point ) const;
bool SetPoint( const FIntPoint & Point );
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
実装コードを記載します。
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;
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 に以下のインクルードを追加します。
#pragma once
#include "CoreMinimal.h"
#include "IPropertyTypeCustomization.h"
#include "PropertyHandle.h"
#include "SlateBasics.h" // 追加
MyPluginEditor.build.cs を開き、追加で参照するモジュールを記述します。
PublicDependencyModuleNames.AddRange(new string[] {
"Core",
"CoreUObject",
"Engine",
"UnrealEd",
"InputCore", // 追加
"Slate", // 追加
"SlateCore", // 追加
"MyPlugin"
} );
追加がわかりやすいよう少し整形しています。
この状態でビルドが通ることを確認してください。
###基本事項
FParameterCustomization::CustomizeChildren
関数内でカスタマイズを行っていきます。
引数である StructBuilder
に対して、項目を追加していきます。
###Value
それでは Value
の編集箇所を組んでみましょう。
まずはソースに SNumericEntryBox
をインクルードします。
#include "ParameterCustomization.h"
#include "PropertyEditing.h"
#include "Widgets/Input/SNumericEntryBox.h" // 追加
次に、StructBuilder.AddProperty( ValueHandle.ToSharedRef() )
と記載していた箇所を次のように変更します。
// この一文をコメントアウト
// 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
は前セクションにて作成した取得・設定関数になります。
ビルドが通ったら実行してみます。
ソースコード上で NameContent()
以降が向かって左側、ValueContent()
以降が向かって右側をカスタマイズする、といったことがなんとなく分かると思います。
エディタを開くと、数値部分がキーボード入力だけでなく、スライダーで変更できるようになっています。
AllowSpin
で true
を設定すると、このようにスライドで値を変更できるようになります。
その際、最大値と最小値は Max/MinSliderValue
で設定可能です。
こういった拡張によるメリットとしては
- 最大最小値が存在することで入力ミスが減らせる
- スライダーなのでマウスのみのオペレーションが可能
といったことが挙げられます。
###Text
文字列を編集するテキストボックスを作ります。
その前に、Text
の値をプロパティハンドル越しに取得・設定する関数を作ります。
これはテキストボックスに対してデリゲート登録する関数になります。
ヘッダとソースファイルに以下の関数を追加しましょう。
public:
FText GetText() const;
void SetText( const FText & Text, ETextCommit::Type InCommitType );
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
と記述していた箇所を以下のように変更します。
// この一文をコメントアウト
// 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 )
];
実行してみます。
ヒントテキストとしてうっすら「Please input here」と書かれています。
テキストを入力すると、Enter を押した際に反映されます。
また、SelectAllTextWhenFocused
で true
を指定しているので、フォーカスを入れた際に文字列を全選択状態にします。
こういった拡張によるメリットとしては
- ヒントテキストで何を入力するのか先に伝えることができる
- 入力してはいけない文字などが入っていた場合、
SetText
関数内で対処できる - 頻繁に再入力することが想定される場合、
SelectAllTextWhenFocused
を用いて文字列を削除する手間を省ける - その他、テキスト変更時の処理を
ETextCommit
で切り分けることができる
といったことが挙げられます。
###Color
実は FColor
の場合、デフォルトのエディタを使用するのが最も効率的です。
(プロパティ取得・設定関数を作らせておいて怒られそうですが。)
しかし、デフォルトのエディタを Slate で記述するにはなかなか大変です。
そのため、FColor
に関しては「プロパティ値が変更された際のデリゲート」を組んでみます。
では「プロパティ値が変更された」ことを検知する関数を作ります。
private:
void OnChangedColor();
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
を取得後に以下の処理を書き足します。
ColorHandle = StructPropertyHandle->GetChildHandle( GET_MEMBER_NAME_CHECKED( FParameter, Color ) );
if( ColorHandle.IsValid() ) {
ColorHandle->SetOnPropertyValueChanged(
FSimpleDelegate::CreateRaw( this, &FParameterCustomization::OnChangedColor )
);
}
これで間接的に色が変更されたことを検知した処理を組むことが可能になります。
カラーパレット上で値を少しでも変更すれば、検知して呼ばれますが、アルファ値がゼロだと RGB 値もゼロで返されるので注意してください。
もちろん、自作した SetColor
関数を呼んで手動で色を設定してもこちらのデリゲート処理が呼ばれます。
カラー値を他の要素に対して反映させたい場合、OnChangedColor
関数内で対応することができます。
##5.カスタマイズ基礎2
カスタマイズ基礎では SNumericEntryBox
や SEditableTextBox
を用いました。
ここでは Slate クラスを自作してエディタに組み込んでみます。
###FIntPoint
FParameterCustomization
クラスでデリゲート登録できるパーツ的なクラスを作っていきます。
新規でソースコードを書き起こします。
ブランクコードであればよいので、継承無しで Unreal Editor から追加しても良いですし、Visual Studio 側で追加しても構いません。
今回は Unreal Editor から追加します。
「PointEditor」という名称にしました。
また、外部に公開するつもりはないため Private 設定を選択しています。
フォルダは Private 側に Slates フォルダを作りました。
それではヘッダファイルとソースファイルを書き換えます。
まずはヘッダファイルからです。
#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
を取得・設定する関数の定義をしています。
次はソースコードです。
#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
クラスを組み込んでみましょう。
#include "ParameterCustomization.h"
#include "PropertyEditing.h"
#include "Widgets/Input/SNumericEntryBox.h"
#include "Slates/PointEditor.h" // 追加
// この一文をコメントアウト
// 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 の記述が膨れ上がりそうであれば、素直に別クラス化したほうが取り回しがしやすいです。
ここまで順調にできていれば、エディタ起動時に以下のようになっているはずです。
Point
の X
と Y
がそれぞれ -2 から 2 の間でスライドできるようになっています。
しかし、これではどちらが X
で Y
なのか分かりません。
テキスト表示を追加してみましょう。
PointEditor.cpp の 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 です。
これでビルドして実行すると、以下のようになります。
エリアも決まっているため、むりやり押し込んだ形になっています。
この状態でもよいかもしれませんが、格子状に Widget を配置する場合は SGridPanel
を用いると良い場合があります。
SGridPanel
に差し替えてみましょう。
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
クラスを用いて幅を調整しています。
この状態で実行すると以下のようになります。
幅がカスタマイズに応じてきちんと調整されているのがわかります。
SVerticalBox
、SHorizontalBox
、SGridPanel
は状況に応じてどれを仕様すべきか判断しましょう。
要素同士がくっついてしまっているのを離したい、といった場合は、.Padding
を設定するとマージンを取ることができます。
試しに SNumericEntryBox
が入っている SBox
の HAlign
の記述の下に追記してみましょう。
.HAlign( EHorizontalAlignment::HAlign_Fill )
.Padding( FMargin( 1.0f, 1.0f ) )
これで実行すると、合間に少しエリアができた状態になります。
レイアウトを調整して見やすくすることは、エディタの使いやすさに直結します。
うまく調整していきましょう。
##6.カスタマイズ応用
###Array(TMap)
最終セクションとして、すでに操作できる Value
値に応じて Array
メンバの配列サイズを変更したり、入力した Point
メンバの値を一括で入力してみます。
前準備として、PointEditor2
クラスを作成します。
PointEditor.cpp/h をコピー&ペースト、リネームして Visual Studio プロジェクトに追加しましょう。
配列のインデクスを識別する機能を追加したものになります。
まずはヘッダファイルです。
#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
クラスと同様です。
次はソースファイルです。
#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 と同フォルダ内に追加します。
まずはヘッダファイルです。
#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
を用いて型定義し直しているのはそのためです。
次はソースファイルです。
#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
クラスに組み込んでみます。
// 新しくインクルードを追加
#include "Slates/ArrayEditor.h"
class FParameterCustomization : public IPropertyTypeCustomization {
public:
FParameterCustomization();
/***** 中略 *****/
// 新しくメンバを追加
TSharedPtr<SArrayEditor> ArrayEditor;
};
// この一文をコメントアウト
// 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
の制御を記述します。
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
の最大数が動的に変化します。
配列の要素である FIntPoint
クラスも、インデクスごとに値が保持されます。
保存して終了した後、再度立ち上げると値が保持されていることが確認できるでしょう。
#おわりに
できる限り具体的なコードを掲載したのでかなりの長編になってしまいました。
前編・後編ともに全セクションをこなせば、手元に少しだけ有用なサンプルが出来上がっていると思います。
ここから Slate についての理解を深めていっていただけるとありがたいです。
まだ SComboBox
や SButton
など説明しきれていないものもありますので、後日別記事として上げル予定です。
以上、読んでいただきありがとうございました。
#参考リンク集
※前編にも同じリンクを載せています。