22
22

More than 5 years have passed since last update.

UE4 便利なWidget(C++)

Last updated at Posted at 2017-12-02

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
(そのうちマーケットプレイス出品したい)

22
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
22