UE4
UnrealEngine

UE4 便利なWidget(C++)

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

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

はじめに

スレートの機能はUMGに搭載されています。
2017-12-02_16h35_10.png

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

一部は公式ドキュメントにも乗っています。
スレート ウィジェットのサンプル | Unreal Engine #アイテム表示
うん、簡潔過ぎる……
という事でサンプルコード付きで紹介していきます。
コード多くて読みにくかったらごめんなさい。

SListView

https://docs.unrealengine.com/latest/INT/API/Runtime/Slate/Widgets/Views/SListView/index.html
2017-12-02_16h28_00.png
こんなリスト構造を簡単に作れる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
2017-12-02_15h48_40.png
こんなツリー構造を簡単に作れる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
2017-12-02_17h32_12.png
ツール開発で良く必要になるアセット選択の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
2017-12-02_18h16_59.png
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、開発用のエディター拡張/ツール等かと思います。

皆々様も是非たのしいツール開発を!
2017-12-02_18h57_24.png
(そのうちマーケットプレイス出品したい)