Unreal Engine 4 (UE4) Advent Calendar 2017の12月3日の記事です。
みんな大好きSlate/スレートのお話です。
とは言ってもスレート自体については語りませんので、ご存知ない方はコチラの概要をご確認下さいませ。楽しい機能です。
スレートの概要 | Unreal Engine
はじめに
しかし、C++のみに存在する機能もあります。
一部は公式ドキュメントにも乗っています。
スレート ウィジェットのサンプル | Unreal Engine #アイテム表示
うん、簡潔過ぎる……
という事でサンプルコード付きで紹介していきます。
コード多くて読みにくかったらごめんなさい。
SListView
https://docs.unrealengine.com/latest/INT/API/Runtime/Slate/Widgets/Views/SListView/index.html
こんなリスト構造を簡単に作れるWidgetです。
1行のWidgetも自由に組めます。
説明
テンプレートクラスのSListViewを使用します。
typedef SListView<1行要素のクラス> SListViewType;
SAssignNew(ListView, SListViewType)
.ListItemsSource( &ItemList )
.OnGenerateRow( this, &SListViewTestWidget::OnGenerateRow )
.OnMouseButtonDoubleClick( this, &SListViewTestWidget::OnListSelectionDoubleClicked)
- ListItemsSource
- TArray型を渡す。要素のリスト
- TSharedRef OnGenerateRow(ItemType InItem, const TSharedRef& OwnerTable)
- 1行分のWidgetを生成するコールバック。ITableRowを返すので、この中で自由なWidgetを作る事が可能。
- void OnListSelectionDoubleClicked(ItemType Item);
- アイテムをダブルクリックした時に、そのアイテムを引数に呼ばれるコールバック
処理
上のスクリーンショットで言うと、下記のような流れが自動で行われます。
- ListItemsSource の 引数で渡されたTArray要素を回っていく
- Root1 の OnGenerateRow
- Root2 の OnGenerateRow
...
サンプルコード
ヘッダー
#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;
};
ソース
#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();
}
使う側
SAssignNew(ListViewTest, SListViewTestWidget)
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
こんなツリー構造を簡単に作れるWidgetです。
折り畳みの「▶」や頭のインデントなんかも自動でやってくれます。
1行のWidgetも自由に組めます。
説明
テンプレートクラスのSTreeViewを使用します。
使い方は、SListViewとほぼ同じです。
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 OnGenerateRow(ItemType InItem, const TSharedRef& OwnerTable);
- 1行分のWidgetを生成するコールバック。ITableRowを返すので、この中で自由なWidgetを作る事が可能。
- void OnGetChildren(ItemType InItem, TArray& OutChildren);
- 引数のInItemの子供要素を取得するコールバック。ココでOutChildrenに子供を返す事によって親子階層が作れる
- void OnTreeSelectionDoubleClicked(ItemType Item);
- アイテムをダブルクリックした時に、そのアイテムを引数に呼ばれるコールバック
処理
上のスクリーンショットで言うと、下記のような流れが自動で行われます。
- TreeItemsSource の 引数で渡されたTArray要素を回っていく
- Root1 の OnGenerateRow
- Root1 の OnGetChildren -> Child1を返す
- Child1 の OnGenerateRow
- Child1 の OnGetChildren -> 何も返さないので終わり
- Root2 の OnGenerateRow
- Root2 の OnGetChildren -> Child2を返す
...
サンプルコード
ヘッダー
#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;
};
ソース
#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();
}
使う側
SAssignNew(TreeViewTest, STreeViewTestWidget)
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
ツール開発で良く必要になるアセット選択のWidgetです。
指定のクラスや条件式でのフィルタリングもできます。
説明
SObjectPropertyEntryBoxを使用します
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)
- アセットを変更した時に呼ばれるコールバック
- ここでアセットのリファレンスを取ります
サンプルコード
ヘッダー
#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;
};
ソース
#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" を追加
使う側
SNew(SEntryBoxObjectTestWidget)
SClassPropertyEntryBox
https://docs.unrealengine.com/latest/INT/API/Editor/PropertyEditor/SClassPropertyEntryBox/index.html
SObjectPropertyEntryBoxのclass版。
クラスを選択出来ます。
説明
SClassPropertyEntryBoxを使用します
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)
- クラスが選択された時に呼び出されます
サンプルコード
ヘッダー
/** 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;
};
ソース
#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" を追加
使う側
SNew(SEntryBoxClassTestWidget)
最後に
C++側での実装という事もありUMGとの連携が若干面倒かもしれません。
(Native Widget Host使えばマシになるかも……)
また、PC向けゲームでしたらTreeView辺りは使えるかもしれないですが、実際にゲーム内のWidgetとして使う事は少ないかもしれません。
使用されるのは主にデバッグ用途のWidget、開発用のエディター拡張/ツール等かと思います。