Edited at

UE4 便利なWidget(C++)

More than 1 year has passed since last update.

Unreal Engine 4 (UE4) Advent Calendar 2017の12月3日の記事です。

みんな大好きSlate/スレートのお話です。

とは言ってもスレート自体については語りませんので、ご存知ない方はコチラの概要をご確認下さいませ。楽しい機能です。

スレートの概要 | Unreal Engine


はじめに

スレートの機能はUMGに搭載されています。

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

一部は公式ドキュメントにも乗っています。

スレート ウィジェットのサンプル | Unreal Engine #アイテム表示

うん、簡潔過ぎる……

という事でサンプルコード付きで紹介していきます。

コード多くて読みにくかったらごめんなさい。


SListView

https://docs.unrealengine.com/latest/INT/API/Runtime/Slate/Widgets/Views/SListView/index.html



こんなリスト構造を簡単に作れるWidgetです。

1行のWidgetも自由に組めます。


説明

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


Slate生成

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);


    • アイテムをダブルクリックした時に、そのアイテムを引数に呼ばれるコールバック




処理

上のスクリーンショットで言うと、下記のような流れが自動で行われます。

1. ListItemsSource の 引数で渡されたTArray要素を回っていく

2. Root1 の OnGenerateRow

3. Root2 の OnGenerateRow

...


サンプルコード


ヘッダー


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;
};



ソース


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();
}



使う側


Slate生成

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とほぼ同じです。


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 OnGenerateRow(ItemType InItem, const TSharedRef& OwnerTable);


    • 1行分のWidgetを生成するコールバック。ITableRowを返すので、この中で自由なWidgetを作る事が可能。



  • void OnGetChildren(ItemType InItem, TArray& 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を返す

...


サンプルコード


ヘッダー


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;
};



ソース


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();
}



使う側


Slate生成

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を使用します


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)


    • アセットを変更した時に呼ばれるコールバック

    • ここでアセットのリファレンスを取ります




サンプルコード


ヘッダー


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;
};



ソース


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" を追加


使う側


Slate生成

SNew(SEntryBoxObjectTestWidget)



SClassPropertyEntryBox

https://docs.unrealengine.com/latest/INT/API/Editor/PropertyEditor/SClassPropertyEntryBox/index.html



SObjectPropertyEntryBoxのclass版。

クラスを選択出来ます。


説明

SClassPropertyEntryBoxを使用します


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)


    • クラスが選択された時に呼び出されます




サンプルコード


ヘッダー


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;
};



ソース


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" を追加


使う側


Slate生成

SNew(SEntryBoxClassTestWidget)



最後に

C++側での実装という事もありUMGとの連携が若干面倒かもしれません。

(Native Widget Host使えばマシになるかも……)

また、PC向けゲームでしたらTreeView辺りは使えるかもしれないですが、実際にゲーム内のWidgetとして使う事は少ないかもしれません。

使用されるのは主にデバッグ用途のWidget、開発用のエディター拡張/ツール等かと思います。

皆々様も是非たのしいツール開発を!



(そのうちマーケットプレイス出品したい)