UE4 便利なWidget(C++)

[Unreal Engine 4 (UE4) Advent Calendar 2017](https://qiita.com/advent-calendar/2017/ue4)の12月3日の記事です。

みんな大好きSlate/スレートのお話です。
とは言ってもスレート自体については語りませんので、ご存知ない方はコチラの概要をご確認下さいませ。楽しい機能です。
[スレートの概要 | Unreal Engine](https://docs.unrealengine.com/latest/JPN/Programming/Slate/Overview/index.html)




# はじめに
スレートの機能はUMGに搭載されています。
![2017-12-02_16h35_10.png](https://qiita-image-store.s3.amazonaws.com/0/58518/dd37d209-bef9-413e-1a64-0ce12db8e8dd.png)

しかし、C++のみに存在する機能もあります。

一部は公式ドキュメントにも乗っています。
[スレート ウィジェットのサンプル | Unreal Engine #アイテム表示](https://docs.unrealengine.com/latest/JPN/Programming/Slate/Widgets/index.html#アイテム表示)
うん、簡潔過ぎる……
という事でサンプルコード付きで紹介していきます。
コード多くて読みにくかったらごめんなさい。


# SListView
https://docs.unrealengine.com/latest/INT/API/Runtime/Slate/Widgets/Views/SListView/index.html
![2017-12-02_16h28_00.png](https://qiita-image-store.s3.amazonaws.com/0/58518/c7edb328-cc37-f561-7c55-2b99868bf4a7.png)
こんなリスト構造を簡単に作れるWidgetです。
1行のWidgetも自由に組めます。

## 説明
テンプレートクラスのSListViewを使用します。

```cpp:Slate生成
typedef SListView<1行要素のクラス>  SListViewType;

SAssignNew(ListView, SListViewType)
.ListItemsSource( &ItemList )
.OnGenerateRow( this, &SListViewTestWidget::OnGenerateRow )
.OnMouseButtonDoubleClick( this, &SListViewTestWidget::OnListSelectionDoubleClicked)
```
- ListItemsSource
    - TArray型を渡す。要素のリスト
- TSharedRef<ITableRow> OnGenerateRow(ItemType InItem, const TSharedRef<STableViewBase>& OwnerTable)
    - 1行分のWidgetを生成するコールバック。ITableRowを返すので、この中で自由なWidgetを作る事が可能。
- void OnListSelectionDoubleClicked(ItemType Item);  
    - アイテムをダブルクリックした時に、そのアイテムを引数に呼ばれるコールバック

### 処理
上のスクリーンショットで言うと、下記のような流れが自動で行われます。
1. ListItemsSource の 引数で渡されたTArray要素を回っていく
2. Root1 の OnGenerateRow
3. Root2 の OnGenerateRow
...


## サンプルコード
### ヘッダー
```cpp:ListWidget.h
#pragma once

#include "CoreMinimal.h"

typedef TSharedPtr<class FListViewResult> FListViewResultShare;
typedef SListView<FListViewResultShare>  SListViewType;

/** 要素一個 */
class FListViewResult : public TSharedFromThis<FListViewResult>
{
public: 
	FListViewResult(const FText& InDisplayText);
	virtual ~FListViewResult() {}
	
	const FText& GetDisplayText() const { return DisplayText; }

private:
	/** result display */
	FText DisplayText;
};

/** ListWidgetを隠蔽したWidget */
class SListViewTestWidget
	: public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SListViewTestWidget) { }
	SLATE_END_ARGS()
	
public:
	/**
	 * Constructs the application.
	 *
	 * @param InArgs The Slate argument list.
	 */
	void Construct( const FArguments& InArgs );
	
	/** Destructor. */
	~SListViewTestWidget();
	
	// List --- Begin
	
	/** row generate */
	TSharedRef<ITableRow> OnGenerateRow(FListViewResultShare InItem, const TSharedRef<STableViewBase>& OwnerTable);
	/** item double click event */
	void OnListSelectionDoubleClicked(FListViewResultShare Item);
	
	// List --- End
	
	void SetItemList(TArray<FListViewResultShare> InItemList);
private:
	/** List widget instance*/
	TSharedPtr<SListViewType> ListView;
	
	/** list */
	TArray<FListViewResultShare> ItemList;
};
```

### ソース

```cpp:ListWidget.cpp
#include "ListView.h"
#include "Test.h"

/* root */
FListViewResult::FListViewResult(const FText& InDisplayText)
: DisplayText(InDisplayText)
{
}

/**
 * Constructs the application.
 *
 * @param InArgs The Slate argument list.
 */
void SListViewTestWidget::Construct( const FArguments& InArgs )
{
	ChildSlot
	[
		SAssignNew(ListView, SListViewType)
		.ItemHeight(24)
		.ListItemsSource( &ItemList )
		.OnGenerateRow( this, &SListViewTestWidget::OnGenerateRow )
		.OnMouseButtonDoubleClick( this, &SListViewTestWidget::OnListSelectionDoubleClicked )
		.SelectionMode( ESelectionMode::Multi )
	];
}

/** Destructor. */
SListViewTestWidget::~SListViewTestWidget()
{
}

/** row generate */
TSharedRef<ITableRow> SListViewTestWidget::OnGenerateRow(FListViewResultShare InItem, const TSharedRef<STableViewBase>& OwnerTable)
{
	return SNew( STableRow< TSharedPtr<FListViewResultShare> >, OwnerTable )
		[
			SNew(STextBlock)
			.Text(InItem->GetDisplayText())
		];
}
/** item double click event */
void SListViewTestWidget::OnListSelectionDoubleClicked(FListViewResultShare Item)
{
	// Double Click!!
}

void SListViewTestWidget::SetItemList(TArray<FListViewResultShare> InItemList)
{
	ItemList = InItemList;
	ListView->RequestListRefresh();
}
```

### 使う側
```cpp:Slate生成
SAssignNew(ListViewTest, SListViewTestWidget)
```
```cpp:要素登録サンプル
TArray<FListViewResultShare> Item;
FListViewResultShare Root1(new FListViewResult(FText::FromString(TEXT("Root1"))));
FListViewResultShare Root2(new FListViewResult(FText::FromString(TEXT("Root2"))));
Item.Add(Root1);
Item.Add(Root2);
ListViewTest->SetItemList(Item);
```

# STreeView
https://docs.unrealengine.com/latest/INT/API/Runtime/Slate/Widgets/Views/STreeView/index.html
![2017-12-02_15h48_40.png](https://qiita-image-store.s3.amazonaws.com/0/58518/f0a87950-bdf1-6216-94df-dc30c82c4762.png)
こんなツリー構造を簡単に作れるWidgetです。
折り畳みの「▶」や頭のインデントなんかも自動でやってくれます。
1行のWidgetも自由に組めます。

## 説明
テンプレートクラスのSTreeViewを使用します。
使い方は、SListViewとほぼ同じです。

```cpp:Slate生成
typedef STreeView<1行要素のクラス>  STreeViewType;

SAssignNew(TreeView, STreeViewType)
.TreeItemsSource( &ItemList )
.OnGenerateRow( this, &STreeViewTestWidget::OnGenerateRow )
.OnGetChildren( this, &STreeViewTestWidget::OnGetChildren )
.OnMouseButtonDoubleClick( this, &STreeViewTestWidget::OnTreeSelectionDoubleClicked )
```
- TreeItemsSource  
    - TArray型を渡す。1行要素のリスト。この変数が階層のトップになる。
- TSharedRef<ITableRow> OnGenerateRow(ItemType InItem, const TSharedRef<STableViewBase>& OwnerTable);  
    - 1行分のWidgetを生成するコールバック。ITableRowを返すので、この中で自由なWidgetを作る事が可能。
- void OnGetChildren(ItemType InItem, TArray<ItemType>& OutChildren);  
    - 引数のInItemの子供要素を取得するコールバック。ココでOutChildrenに子供を返す事によって親子階層が作れる
- void OnTreeSelectionDoubleClicked(ItemType Item);  
    - アイテムをダブルクリックした時に、そのアイテムを引数に呼ばれるコールバック

### 処理
上のスクリーンショットで言うと、下記のような流れが自動で行われます。
1. TreeItemsSource の 引数で渡されたTArray要素を回っていく
2. Root1 の OnGenerateRow
3. Root1 の OnGetChildren -> Child1を返す
4. Child1 の OnGenerateRow
5. Child1 の OnGetChildren -> 何も返さないので終わり
6. Root2 の OnGenerateRow
7. Root2 の OnGetChildren -> Child2を返す
...


## サンプルコード
### ヘッダー


```cpp:TreeWidget.h

#pragma once

#include "CoreMinimal.h"

typedef TSharedPtr<class FTreeViewResult> FTreeViewResultShare;
typedef STreeView<FTreeViewResultShare>  STreeViewType;

/** 要素一個 */
class FTreeViewResult : public TSharedFromThis<FTreeViewResult>
{
public: 
	FTreeViewResult(const FText& InDisplayText);

	virtual ~FTreeViewResult() {}
	
	const FText& GetDisplayText() const { return DisplayText; }

	/** Add tree child */
	void AddChild(TSharedPtr<FTreeViewResult> Child);
	const TArray<TSharedPtr<FTreeViewResult>>& GetChildren() const { return Children; }

private:
	/** result tree child */
	TArray<TSharedPtr<FTreeViewResult>> Children;
	
	/** result display */
	FText DisplayText;
};


/** TreeWidgetを隠蔽したWidget */
class STreeViewTestWidget
	: public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(STreeViewTestWidget) { }
	SLATE_END_ARGS()
public:
	/**
	 * Constructs the application.
	 *
	 * @param InArgs The Slate argument list.
	 */
	void Construct( const FArguments& InArgs );
	
	/** Destructor. */
	~STreeViewTestWidget();
		
	// Tree --- Begin
	
	/** row generate */
	TSharedRef<ITableRow> OnGenerateRow(FTreeViewResultShare InItem, const TSharedRef<STableViewBase>& OwnerTable);
	/** tree child */
	void OnGetChildren(FTreeViewResultShare InItem, TArray<FTreeViewResultShare>& OutChildren);
	/** item double click event */
	void OnTreeSelectionDoubleClicked(FTreeViewResultShare Item);
	
	// Tree --- End
	
	void SetItemList(TArray<FTreeViewResultShare> InItemList);
	
private:
	/** tree widget instance*/
	TSharedPtr<STreeViewType> TreeView;
	
	/** list */
	TArray<FTreeViewResultShare> ItemList;
};
```

### ソース

```cpp:TreeWidget.cpp
#include "TreeView.h"
#include "Test.h"

FTreeViewResult::FTreeViewResult(const FText& InDisplayText)
: Children(),
  DisplayText(InDisplayText)
{
}

void FTreeViewResult::AddChild(TSharedPtr<FTreeViewResult> Child)
{
	Children.Add(Child);
}

/**
 * Constructs the application.
 *
 * @param InArgs The Slate argument list.
 */
void STreeViewTestWidget::Construct( const FArguments& InArgs )
{
	ChildSlot
	[
		SAssignNew(TreeView, STreeViewType)
		.ItemHeight(24)
		.TreeItemsSource( &ItemList )
		.OnGenerateRow( this, &STreeViewTestWidget::OnGenerateRow )
		.OnGetChildren( this, &STreeViewTestWidget::OnGetChildren )
		.OnMouseButtonDoubleClick( this, &STreeViewTestWidget::OnTreeSelectionDoubleClicked )
		.SelectionMode( ESelectionMode::Multi )
	];
}

/** Destructor. */
STreeViewTestWidget::~STreeViewTestWidget()
{
}

/** row generate */
TSharedRef<ITableRow> STreeViewTestWidget::OnGenerateRow(FTreeViewResultShare InItem, const TSharedRef<STableViewBase>& OwnerTable)
{
	return SNew( STableRow< TSharedPtr<FTreeViewResultShare> >, OwnerTable )
		[
			SNew(STextBlock)
			.Text(InItem->GetDisplayText())
		];
}
/** tree child */
void STreeViewTestWidget::OnGetChildren(FTreeViewResultShare InItem, TArray<FTreeViewResultShare>& OutChildren)
{
	OutChildren += InItem->GetChildren();
}
/** item double click event */
void STreeViewTestWidget::OnTreeSelectionDoubleClicked(FTreeViewResultShare Item)
{
	// Double Click!!
}

void STreeViewTestWidget::SetItemList(TArray<FTreeViewResultShare> InItemList)
{
	ItemList = InItemList;
	TreeView->RequestTreeRefresh();
}
```

### 使う側
```cpp:Slate生成
SAssignNew(TreeViewTest, STreeViewTestWidget)
```
```cpp:要素登録サンプル
TArray<FTreeViewResultShare> Item;
FTreeViewResultShare Root1(new FTreeViewResult(FText::FromString(TEXT("Root1"))));
FTreeViewResultShare Root2(new FTreeViewResult(FText::FromString(TEXT("Root2"))));
Item.Add(Root1);
Item.Add(Root2);

{
	FTreeViewResultShare Child1(new FTreeViewResult(FText::FromString(TEXT("Child1"))));
	Root1->AddChild(Child1);
}
{
	FTreeViewResultShare Child2(new FTreeViewResult(FText::FromString(TEXT("Child2"))));
	FTreeViewResultShare Child3(new FTreeViewResult(FText::FromString(TEXT("Child3"))));
	Root2->AddChild(Child2);
	Child2->AddChild(Child3);
}
TreeViewTest->SetItemList(Item);
```

# SObjectPropertyEntryBox
https://docs.unrealengine.com/latest/INT/API/Editor/PropertyEditor/SObjectPropertyEntryBox/index.html
![2017-12-02_17h32_12.png](https://qiita-image-store.s3.amazonaws.com/0/58518/ef35821c-05f2-a6f5-56d5-776fb06606d9.png)
ツール開発で良く必要になるアセット選択のWidgetです。
指定のクラスや条件式でのフィルタリングもできます。

## 説明
SObjectPropertyEntryBoxを使用します

```cpp:Slate生成
SAssignNew(EntryBox, SObjectPropertyEntryBox)
.AllowedClass(UStaticMesh::StaticClass())
.OnShouldFilterAsset(this, &SEntryBoxTestWidget::ShouldFilterAsset)
.ObjectPath(this, &SEntryBoxTestWidget::GetObjectPath)
.OnObjectChanged(this, &SEntryBoxTestWidget::OnObjectChanged)
```
- AllowedClass  
    - ここで指定したクラスのみをリストアップします（継承先含む）
- bool ShouldFilterAsset(const FAssetData& AssetData)
    - フィルタリングの条件を自分で指定します
        - 同じAllowedClassだけど、こっちのアセットはリストアップしなくないとか。SkeletalMeshのスケルトンを判別して除外するとか
    - trueを返すとそのアセットはリストアップされません
- FString GetObjectPath() const
    - Widgetに表示する文字列を返します。
- void OnObjectChanged(const FAssetData& AssetData)
    - アセットを変更した時に呼ばれるコールバック
    - ここでアセットのリファレンスを取ります


## サンプルコード
### ヘッダー

```cpp:EntryBoxObject.h
#pragma once
#include "CoreMinimal.h"

/** SObjectPropertyEntryBoxを隠蔽したWidget */
class SEntryBoxObjectTestWidget
	: public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SEntryBoxObjectTestWidget) { }
	SLATE_END_ARGS()
	
public:
	/**
	 * Constructs the application.
	 *
	 * @param InArgs The Slate argument list.
	 */
	void Construct( const FArguments& InArgs );
	
	/** Destructor. */
	~SEntryBoxObjectTestWidget();
	
	/** Entry asset filtering */
	bool ShouldFilterAsset(const FAssetData& AssetData);
	
	FString GetObjectPath() const;
	void OnObjectChanged(const FAssetData& AssetData);
	
private:
	/** EntryBox widget instance*/
	TSharedPtr<class SObjectPropertyEntryBox> EntryBox;
	
	UObject* SelectedAsset;
};
```

### ソース
```cpp:EntryBoxObject.cpp
#include "EntryBoxObject.h"
#include "Test.h"

#include "PropertyCustomizationHelpers.h"
#include "PropertyHandle.h"

/**
 * Constructs the application.
 *
 * @param InArgs The Slate argument list.
 */
void SEntryBoxObjectTestWidget::Construct( const FArguments& InArgs )
{
	SelectedAsset = nullptr;
	
	ChildSlot
	[
		SAssignNew(EntryBox, SObjectPropertyEntryBox)
		.AllowedClass(UStaticMesh::StaticClass())  /* StaticMeshだけ */
		.OnShouldFilterAsset(this, &SEntryBoxObjectTestWidget::ShouldFilterAsset)
		.ObjectPath(this, &SEntryBoxObjectTestWidget::GetObjectPath)
		.OnObjectChanged(this, &SEntryBoxObjectTestWidget::OnObjectChanged)
	];
}

/** Destructor. */
SEntryBoxObjectTestWidget::~SEntryBoxObjectTestWidget()
{
}

/** Entry asset filtering */
bool SEntryBoxObjectTestWidget::ShouldFilterAsset(const FAssetData& AssetData)
{
	/* 全部OK */
	return false;
}

FString SEntryBoxObjectTestWidget::GetObjectPath() const
{
	return SelectedAsset ? SelectedAsset->GetPathName() : FString("");
}
void SEntryBoxObjectTestWidget::OnObjectChanged(const FAssetData& AssetData)
{
	/* ここでObjectのリファレンス取る */
	SelectedAsset = Cast<UObject>(AssetData.GetAsset());
}
```

### Build.cs
PublicDependencyModuleNames に "PropertyEditor" を追加

### 使う側
```cpp:Slate生成
SNew(SEntryBoxObjectTestWidget)
```


# SClassPropertyEntryBox
https://docs.unrealengine.com/latest/INT/API/Editor/PropertyEditor/SClassPropertyEntryBox/index.html
![2017-12-02_18h16_59.png](https://qiita-image-store.s3.amazonaws.com/0/58518/71e2e9bd-b991-0674-db2a-bac015615be9.png)
SObjectPropertyEntryBoxのclass版。
クラスを選択出来ます。

## 説明
SClassPropertyEntryBoxを使用します

```cpp:Slate生成
SAssignNew(EntryBox, SClassPropertyEntryBox)
.MetaClass(AActor::StaticClass())
.RequiredInterface(UStaticMesh::StaticClass())
.IsBlueprintBaseOnly(false)
.ShowTreeView(true)
.SelectedClass(this, &SEntryBoxClassTestWidget::OnGetClass)
.OnSetClass(this, &SEntryBoxClassTestWidget::OnSetClass)
```
- MetaClass  
    - ここで指定したクラスのみをリストアップします（継承先含む）
- RequiredInterface
    - ここで指定したInterfaceを実装しているクラスのみをリストアップします
- IsBlueprintBaseOnly
    - Blueprintで書かれたクラスのみが対象となるか。falseでC++のクラスもリストアップされます
- ShowTreeView
    - trueでTreeView形式で表示します
- SelectedClass
    - 選択されたクラスを指定します
- void OnSetClass(const UClass* SetClass)
    - クラスが選択された時に呼び出されます


## サンプルコード
### ヘッダー

```cpp:EntryBoxClass.h
/** SClassPropertyEntryBoxを隠蔽したWidget */
class SEntryBoxClassTestWidget
	: public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SEntryBoxClassTestWidget) { }
	SLATE_END_ARGS()
	
public:
	/**
	 * Constructs the application.
	 *
	 * @param InArgs The Slate argument list.
	 */
	void Construct( const FArguments& InArgs );
	
	/** Destructor. */
	~SEntryBoxClassTestWidget();
	
	const UClass* OnGetClass() const;
	void OnSetClass(const UClass* SetClass);
	
private:
	/** EntryBox widget instance*/
	TSharedPtr<class SClassPropertyEntryBox> EntryBox;
	
	const UClass* SelectedClass;
};
```

### ソース
```cpp:EntryBoxClass.cpp
#include "EntryBoxClass.h"
#include "Test.h"

#include "PropertyCustomizationHelpers.h"
#include "PropertyHandle.h"

/**
 * Constructs the application.
 *
 * @param InArgs The Slate argument list.
 */
void SEntryBoxClassTestWidget::Construct( const FArguments& InArgs )
{
	SelectedClass = nullptr;
	
	ChildSlot
	[
		SAssignNew(EntryBox, SClassPropertyEntryBox)
		.MetaClass(AActor::StaticClass()) /* AActorを継承しているClassのみ */
		// .RequiredInterface(UDestructibleInterface::StaticClass()) /* UDestructibleInterfaceを実装しているClassのみ */
		.IsBlueprintBaseOnly(false)
		.ShowTreeView(true)
		.SelectedClass(this, &SEntryBoxClassTestWidget::OnGetClass)
		.OnSetClass(this, &SEntryBoxClassTestWidget::OnSetClass)
	];
}

/** Destructor. */
SEntryBoxClassTestWidget::~SEntryBoxClassTestWidget()
{
}

const UClass* SEntryBoxClassTestWidget::OnGetClass() const
{
	return SelectedClass;
}
void SEntryBoxClassTestWidget::OnSetClass(const UClass* SetClass)
{
	SelectedClass = SetClass;
}
```

### Build.cs
PublicDependencyModuleNames に "PropertyEditor" を追加

### 使う側
```cpp:Slate生成
SNew(SEntryBoxClassTestWidget)
```

# 最後に
C++側での実装という事もありUMGとの連携が若干面倒かもしれません。
（Native Widget Host使えばマシになるかも……）

また、PC向けゲームでしたらTreeView辺りは使えるかもしれないですが、実際にゲーム内のWidgetとして使う事は少ないかもしれません。
使用されるのは主にデバッグ用途のWidget、開発用のエディター拡張/ツール等かと思います。
  
  
皆々様も是非たのしいツール開発を！
![2017-12-02_18h57_24.png](https://qiita-image-store.s3.amazonaws.com/0/58518/0e6f3d46-3f40-ac03-3adb-701ff6bd2dee.png)
（そのうちマーケットプレイス出品したい）