【UE5】詳細で良く見るアセットの種類にクラスでフィルターをかけてコンボボックスから候補一覧を選択するとそのサムネイルも表示してくれるウィジェットを EditorUtilityWidget で使う方法【C++】

Unreal Engine のエディタでスケルタルメッシュやマテリアルなどを選択するときに使うウィジェットがあります。
アイキャッチ画像で示した通りですが、マテリアルやスケルトンなんかを選択するときも、このウィジェットが使われています。

これ、よく見るウィジェットなので、Editor Utility Widget(以下、EUW)でも簡単に使えるだろう…と思い、EUW のデザイナー画面で「パレット」にないか探してみるのですが見つかりません。

実はこれ、ウィジェット(UWidget)ではなく、スレート(Slate)です。
スレートについて細かい説明は割愛しますが、このままでは、Editor Utility Button ウィジェットなどのように、デザイナー画面でキャンバスパネルなどに配置することができません。

本稿では、このスレートをウィジェットとして使えるようにする方法を共有します。
C++ を使う必要があるため、EUW の実装に詳しいテクニカルアーティストやエンジニアが対象となります。

動作環境
Unreal Engine 5.4.4, 5.5 Preview
Windows 11 Home
Visual Studio 2022

もくじ

そもそも、このスレート何て言うの?

クラス名
SObjectPropertyEntryBox

親クラス
SCompoundWidget

ソースファイルの場所
/Engine/Source/Editor/PropertyEditor/Public/PropertyCustomizationHelpers.h
/Engine/Source/Editor/PropertyEditor/Private/PropertyCustomizationHelpers.cpp

インクルード
#include “PropertyCustomizationHelpers.h”

モジュール
PropertyEditor

クラス名の接頭辞が S なので、Slate(スレート)です。

スレートは UMG やウィジェットのコア機能です。
UMG なんかは最終的にスレートとして処理されています。
スレート単体で使っているのは、エディタの UI です。
ゲームでスレートを直接使うことはまずありません。

要するに、このウィジェットは、スレートとしては存在しているのですが、ウィジェットにはないので、パレットで選択できないということになります。
また、コンボボックスの部分は、このスレートにまとまっていますが、選択したアセットを表示してくれるサムネイル部分は別です。
サムネイルだけなら、AssetThumbnailWidget が既にパレットにあるので、これと組み合わせて使うことになります。

何故、サムネイル表示用のウィジェットだけ存在しているのかはナゾです。
以下の記事では AssetThumbnailWidget の活用例が紹介されています。
参考にした記事


この記事のゴール

やりたいことをまとめると以下のようになります。

  1. EUW のデザイナー画面の「パレット」で選べるようにする。
  2. EUW のデザイナー画面で編集できるようにする。
  3. EUW のブループリントで選択したアセットにアクセスできるようにする。
  4. 選択しているアセットが変わったときに EUW のブループリントのイベントが呼ばれるようにする。


1. EUW のデザイナー画面の「パレット」で選べるようにする。

親を UWidget にすることで、パレットで選べるようになります。
UUserWidget や UEditorUtilityWidget は UWidget の子クラスなので、エディタで Widget Blueprint や Editor Utility Widget を新規作成しても構いません。

本稿では Editor Utility Widget を新規作成して作ります。
※ルートウィジェットはキャンバスパネルで構いません。

EUW には、サムネイルと SObjectPropertyEntryBox のふたつのウィジェットを持たせます。
SObjectPropertyEntryBox を持ったウィジェットを C++ で作ることで、パレットから選べるようにします。

EUW はエディタ専用の機能なので、プロジェクトに Editor モジュールがない場合、先に追加しておく必要があります。
従って、後述のコードは Editor モジュール内に追加します。

リンクを通すのに必要なモジュールは以下です。

PropertyEditor
SlateCore
UMG

// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.h

#pragma once

#include "Components/ContentWidget.h"
#include "AssetSelectorWidget.generated.h"

class SObjectPropertyEntryBox;

UCLASS(BlueprintType)
class プロジェクト名EDITOR_API UAssetSelectorWidget
  : public UContentWidget
{
  GENERATED_BODY()
protected:
  virtual TSharedRef<SWidget> RebuildWidget() override;
  TSharedPtr<SObjectPropertyEntryBox> ObjectPropertyEntryBox;
};
// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.cpp

#include "AssetSelectorWidget.h"
#include "PropertyCustomizationHelpers.h"

TSharedRef<SWidget> UAssetSelectorWidget::RebuildWidget() {
  this->ObjectPropertyEntryBox = SNew(SObjectPropertyEntryBox);
  return this->ObjectPropertyEntryBox.ToSharedRef();
}

以下は、EUW のデザイナー画面で「パレット」から AssetThumbnailWidget と、↑ のソースコードで追加した AssetSelectorWidget を配置した画面のスクリーンショットです。

※タップまたはクリックすると大きい画像で表示します。

これで「パレット」から選べるようになりました。

↑ のソースコードでは、RebuildWidget 関数をオーバーライドして、SObjectPropertyEntryBox を生成し、ポインタを返しているだけです。
最低限必要なコードはこれだけです。
後は必要に応じて処理を追加していきます。

モジュールを追加するのって、地味に面倒ですよね。
私は以下のプラグインで追加しています。

New C++ Module tool

※モジュールを追加したら、Public と Private フォルダの中身をモジュールフォルダのルートに全て移動させて、それらのフォルダは削除します。
※Logging.h/.cpp のファイル名を変更して、これをインクルードしている箇所を修正し、プラグインの指示通りに *.Build.cs を編集しています。
※今ならモジュールの読み込みは .cpp ファイルにマクロをひとつ書くだけでできますが、このプラグインが出力する冗長な書き方でも問題ないです(モジュールの読み込みと解放を細かく制御したい場合に必要になります)。


2. EUW のデザイナー画面で編集できるようにする。

レイアウトを調節するくらいはできますが、デザイナー画面で編集したいのは以下の2点です。

これらの機能があれば、アセットを選択して、選択したアセットのサムネイルを表示することができるようになります。

このウィジェットで選択したアセットを外部に公開する機能があれば必要な機能は全て揃いますが、それはデザイナー画面でやることではないので、後の項で説明します。


A. 選択可能なアセットのクラスを指定できるようにする(クラスでフィルターをかけられるようにする)。

前述したコードを以下のように変更します。

// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.h

#pragma once

#include "Components/ContentWidget.h"
#include "AssetSelectorWidget.generated.h"

class SObjectPropertyEntryBox;

UCLASS(BlueprintType)
class プロジェクト名EDITOR_API UAssetSelectorWidget
  : public UContentWidget
{
  GENERATED_BODY()
// 追加
public:
  UPROPERTY(EditAnywhere)
  TObjectPtr<UClass> AllowedClass;
// ここまで
protected:
  virtual TSharedRef<SWidget> RebuildWidget() override;
  TSharedPtr<SObjectPropertyEntryBox> ObjectPropertyEntryBox;
};
// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.cpp

#include "AssetSelectorWidget.h"
#include "PropertyCustomizationHelpers.h"

TSharedRef<SWidget> UAssetSelectorWidget::RebuildWidget() {
  this->ObjectPropertyEntryBox = SNew(SObjectPropertyEntryBox) // ; を削除
    .AllowedClass(this->AllowedClass); //追加
  return this->ObjectPropertyEntryBox.ToSharedRef();
}
コードの説明
this->ObjectPropertyEntryBox = SNew(SObjectPropertyEntryBox)
  .AllowedClass(this->AllowedClass);

これ、スレート特有の書き方ですが、ただのプロパティーチェインです。
SNew(SObjectPropertyEntryBox).AllowedClass(this->AllowedClass);

SNew で生成したスレートのインスタンスが持っているプロパティー(メンバ変数)にアクセスしています。
ここでは、SObjectPropertyEntryBox のメンバ変数 AllowedClass に値をセットしているだけです。

this->AllowedClass は ↑ のヘッダファイルに追加した UPROPERTY です。
EUW のデザイナー画面にある「階層(Hierarchy)」でキャンバスパネルに配置した AssetSelectorWidget を選択すると、「詳細」からアクセスできるようになります。


「詳細」の AllowedClass に指定したクラスでフィルターがかかるようになります。
SkeletalMesh だけ選択できるようにしたいなら、SkeletalMesh を指定します。

B. 選択しているアセットが変更されたことを通知するイベントを指定する。

イベントはブループリントからバインドできるようにします。
その場合、UE のデリゲートの仕組みが厄介なので、少し面倒です。

// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.h

#pragma once

#include "Components/ContentWidget.h"
#include "PropertyCustomizationHelpers.h" //追加
#include "AssetSelectorWidget.generated.h"

//class SObjectPropertyEntryBox; 削除

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnObjectChanged, const FAssetData&, InAssetData); //追加

UCLASS(BlueprintType)
class プロジェクト名EDITOR_API UAssetSelectorWidget
  : public UContentWidget
{
  GENERATED_BODY()
public:
  UPROPERTY(EditAnywhere)
  TObjectPtr<UClass> AllowedClass;
//追加
  UPROPERTY(BlueprintAssignable)
  FOnObjectChanged OnObjectChanged;
//ここまで
protected:
  virtual TSharedRef<SWidget> RebuildWidget() override;
  TSharedPtr<SObjectPropertyEntryBox> ObjectPropertyEntryBox;
//追加
  FOnSetObject OnSetObject;
  virtual void OnObjectChangedInternal(const FAssetData& InAssetData);
//ここまで
};
// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.cpp

#include "AssetSelectorWidget.h"
//#include "PropertyCustomizationHelpers.h" 削除

TSharedRef<SWidget> UAssetSelectorWidget::RebuildWidget() {
  this->OnSetObject.BindUObject(this, &UAssetSelectorWidget::OnObjectChangedInternal); // 追加
  this->ObjectPropertyEntryBox = SNew(SObjectPropertyEntryBox)
    .AllowedClass(this->AllowedClass) // ; を削除
    .OnObjectChanged(this->OnSetObject); // 追加
  return this->ObjectPropertyEntryBox.ToSharedRef();
}

//追加
void UAssetSelectorWidget::OnObjectChangedInternal(const FAssetData& InAssetData) {
  if (!this->OnObjectChanged.IsBound()) { return; }
  // バインドされているブループリントのイベント、または C++ の関数を呼び出す
  this->OnObjectChanged.Broadcast(InAssetData);
}
//ここまで
コードの説明

ブループリントのイベントをバインドするには、ダイナミック・マルチキャスト・デリゲートにする必要があるのですが、SObjectPropertyEntryBox に指定できるのは、ただのデリゲートなので、両者を仲介する仕組みが必要になります。


【問題発生】メモリリーク発生

サンプルコードをそのまま使用すると、RebuildWidget() で生成したスレートが解放されず、メモリリークします。
なので、生成したスレートを解放する処理を追加します。

// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.h

#pragma once

#include "Components/ContentWidget.h"
#include "PropertyCustomizationHelpers.h"
#include "AssetSelectorWidget.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnObjectChanged, const FAssetData&, InAssetData);

UCLASS(BlueprintType)
class プロジェクト名EDITOR_API UAssetSelectorWidget
  : public UContentWidget
{
  GENERATED_BODY()
public:
  UPROPERTY(EditAnywhere)
  TObjectPtr<UClass> AllowedClass;

  UPROPERTY(BlueprintAssignable)
  FOnObjectChanged OnObjectChanged;
protected:
  virtual TSharedRef<SWidget> RebuildWidget() override;
//追加
  virtual void ReleaseSlateResources(bool bReleaseChildren) override;
//ここまで
  virtual void OnObjectChangedInternal(const FAssetData& InAssetData);

  TSharedPtr<SObjectPropertyEntryBox> ObjectPropertyEntryBox;
  FOnSetObject OnSetObject;
};
// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.cpp

#include "AssetSelectorWidget.h"

TSharedRef<SWidget> UAssetSelectorWidget::RebuildWidget() {
  this->OnSetObject.BindUObject(this, &UAssetSelectorWidget::OnObjectChangedInternal);
  this->ObjectPropertyEntryBox = SNew(SObjectPropertyEntryBox)
    .AllowedClass(this->AllowedClass)
    .OnObjectChanged(this->OnSetObject);
  return this->ObjectPropertyEntryBox.ToSharedRef();
}
void UAssetSelectorWidget::OnObjectChangedInternal(const FAssetData& InAssetData) {
  if (!this->OnObjectChanged.IsBound()) { return; }
  // バインドされているブループリントのイベント、または C++ の関数を呼び出す
  this->OnObjectChanged.Broadcast(InAssetData);
}

//追加
void UAssetSelectorWidget::ReleaseSlateResources(bool bReleaseChildren) {
  Super::ReleaseSlateResources(bReleaseChildren);
  this->ObjectPropertyEntryBox.Reset();
  this->OnSetObject.Unbind();
}
//ここまで
コードの説明

RebuildWidget() で生成したスレートを解放するためのメンバ関数があるので、それをオーバーライドして解放する処理を追加するだけです。


【問題発生】アセットを選択してもコンボボックスに反映されない

SObjectPropertyEntryBox が汎用化されない理由が分かりました。
まず、ここまでのコードを実行して動作を確認すると以下の問題があると分かります。

※タップまたはクリックすると大きい画像で表示します。
コンボボックスでアセットを選択すると…。


コンボボックスに選択したアセットが表示されるはずですが、選択する前と変わりません。

詳細は割愛しますが、この状態を修正するためのコードを以下に示します。

// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.h

#pragma once

#include "Components/ContentWidget.h"
#include "PropertyCustomizationHelpers.h"
#include "AssetSelectorWidget.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnObjectChanged, const FAssetData&, InAssetData);

class IPropertyHandle; // 追加

UCLASS(BlueprintType)
class プロジェクト名EDITOR_API UAssetSelectorWidget
  : public UContentWidget
{
  GENERATED_BODY()
public:
  UPROPERTY(EditAnywhere)
  TObjectPtr<UClass> AllowedClass;

  UPROPERTY(BlueprintAssignable)
  FOnObjectChanged OnObjectChanged;
protected:
  virtual TSharedRef<SWidget> RebuildWidget() override;
  virtual void ReleaseSlateResources(bool bReleaseChildren) override;
  virtual void OnObjectChangedInternal(const FAssetData& InAssetData);

  TSharedPtr<SObjectPropertyEntryBox> ObjectPropertyEntryBox;
  FOnSetObject OnSetObject;
//追加
  TSharedPtr<IPropertyHandle> PropertyHandle;
//ここまで
};
// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.cpp

#include "AssetSelectorWidget.h"
#include "PropertyHandle.h" //追加

//追加
namespace { namespace DokuroMoe {
class FPropertyHandle
	: public IPropertyHandle
{
  FString Value;
  FName Name;
  FFieldVariant FieldVariant;
  TSharedPtr<FStrProperty> StrProperty;
public:
  FPropertyHandle()
    : Name("Test")
    , StrProperty(::MakeShareable<FStrProperty>(new FStrProperty(FieldVariant, Name, EObjectFlags::RF_NoFlags)))
  {
    StrProperty->SetMetaData(Name, TEXT(""));
  }
  virtual FPropertyAccess::Result SetValue(const FAssetData& InAssetData, EPropertyValueSetFlags::Type) override {
    Value = InAssetData.GetSoftObjectPath().ToString();
    return FPropertyAccess::Result::Success;
  }
  virtual FPropertyAccess::Result GetValueAsFormattedString(FString& OutValue, EPropertyPortFlags) const override {
    OutValue = Value;
    return FPropertyAccess::Result::Success;
  }
  //以下はビルドを通すためだけに必要な無駄なコード
  virtual bool IsValidHandle() const override { return true; }
  virtual bool IsSamePropertyNode(TSharedPtr<IPropertyHandle>) const override { return false; }
  virtual bool IsEditConst() const override { return false; }
  virtual bool IsEditable() const override { return true; }
  virtual const FFieldClass* GetPropertyClass() const override { return nullptr; }
  virtual FProperty* GetProperty() const override { return StrProperty.Get(); }
  virtual FStringView GetPropertyPath() const override { return FStringView(); }
  virtual TSharedPtr<FPropertyPath> CreateFPropertyPath() const override { return TSharedPtr<FPropertyPath>(); }
  virtual int32 GetArrayIndex() const override { return -1; }
  virtual void RequestRebuildChildren() override {}
  virtual FProperty* GetMetaDataProperty() const override { return nullptr; }
  virtual bool HasMetaData(const FName&) const override { return false; }
  virtual const FString& GetMetaData(const FName&) const override {
    static FString s;
    return s;
  }
  virtual bool GetBoolMetaData(const FName&) const override { return false; }
  virtual int32 GetIntMetaData(const FName&) const override { return -1; }
  virtual float GetFloatMetaData(const FName&) const override { return -1.f; }
  virtual double GetDoubleMetaData(const FName&) const override { return -1.; }
  virtual UClass* GetClassMetaData(const FName&) const override { return nullptr; }
  virtual void SetInstanceMetaData(const FName&, const FString&) override {}
  virtual const FString* GetInstanceMetaData(const FName&) const override { return nullptr; }
  virtual const TMap<FName, FString>* GetInstanceMetaDataMap() const override { return nullptr; }
  virtual FText GetToolTipText() const override { return FText(); }
  virtual void SetToolTipText(const FText& ToolTip) override {}
  virtual bool HasDocumentation() override { return false; }
  virtual FString GetDocumentationLink() override { return FString(); }
  virtual FString GetDocumentationExcerptName() override { return FString(); }
  virtual uint8* GetValueBaseAddress(uint8*) const override { return nullptr; }
  virtual FPropertyAccess::Result GetValueAsDisplayString(FString&, EPropertyPortFlags) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValueAsFormattedText(FText&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValueAsDisplayText(FText&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValueFromFormattedString(const FString&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual void SetOnPropertyValueChanged(const FSimpleDelegate&) override {}
  virtual void SetOnPropertyValueChangedWithData(const TDelegate<void(const FPropertyChangedEvent&)>&) override {}
  virtual void SetOnChildPropertyValueChanged(const FSimpleDelegate&) override {}
  virtual void SetOnChildPropertyValueChangedWithData(const TDelegate<void(const FPropertyChangedEvent&)>&) override {}
  virtual void SetOnPropertyValuePreChange(const FSimpleDelegate&) override {}
  virtual void SetOnChildPropertyValuePreChange(const FSimpleDelegate&) override {}
  virtual void SetOnPropertyResetToDefault(const FSimpleDelegate&) override {}
  virtual FPropertyAccess::Result GetValue(FString&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(float&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(double&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(bool&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(int8&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(int16&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(int32&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(int64&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(uint8&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(uint16&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(uint32&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(uint64&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(FText&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(FName&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(FVector&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(FVector2D&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(FVector4&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(FQuat&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(FRotator&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(UObject*&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(const UObject*&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(FAssetData&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValueData(void*&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(FProperty*&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetValue(const FProperty*&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const FString&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const float&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const double&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const bool&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const int8&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const int16&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const int32&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const int64&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const uint8&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const uint16&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const uint32&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const uint64&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const FText&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const FName&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const FVector&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const FVector2D&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const FVector4&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const FQuat&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const FRotator&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(UObject* const&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const UObject* const&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const TCHAR*, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(FProperty* const&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetValue(const FProperty* const&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual void NotifyPreChange() override {}
  virtual void NotifyPostChange(EPropertyChangeType::Type) override {}
  virtual void NotifyFinishedChangingProperties() override {}
  virtual FPropertyAccess::Result SetObjectValueFromSelection() override { return FPropertyAccess::Result::Fail; }
  virtual int32 GetNumPerObjectValues() const override { return -1; }
  virtual FPropertyAccess::Result SetPerObjectValues(const TArray<FString>&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetPerObjectValues(TArray<FString>&) const override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result SetPerObjectValue(const int32, const FString&, EPropertyValueSetFlags::Type) override { return FPropertyAccess::Result::Fail; }
  virtual FPropertyAccess::Result GetPerObjectValue(const int32, FString&) const override { return FPropertyAccess::Result::Fail; }
  virtual int32 GetIndexInArray() const override { return -1; }
  virtual TSharedPtr<IPropertyHandle> GetChildHandle(FName, bool) const override { return TSharedPtr<IPropertyHandle>(); }
  virtual TSharedPtr<IPropertyHandle> GetChildHandle(uint32) const override { return TSharedPtr<IPropertyHandle>(); }
  virtual TSharedPtr<IPropertyHandle> GetParentHandle() const override { return TSharedPtr<IPropertyHandle>(); }
  virtual TSharedPtr<IPropertyHandle> GetKeyHandle() const override { return TSharedPtr<IPropertyHandle>(); }
  virtual FPropertyAccess::Result GetNumChildren(uint32&) const override { return FPropertyAccess::Result::Fail; }
  virtual uint32 GetNumOuterObjects() const override { return -1; }
  virtual void GetOuterObjects(TArray<UObject*>&) const override {}
  virtual void GetOuterStructs(TArray<TSharedPtr<FStructOnScope>>&) const override {}
  virtual const UClass* GetOuterBaseClass() const override { return nullptr; }
  virtual void ReplaceOuterObjects(const TArray<UObject*>&) override {}
  virtual void GetOuterPackages(TArray<UPackage*>&) const override {}
  virtual void EnumerateRawData(const EnumerateRawDataFuncRef&) override {}
  virtual void EnumerateConstRawData(const EnumerateConstRawDataFuncRef&) const override {}
  virtual void AccessRawData(TArray<void*>&) override {}
  virtual void AccessRawData(TArray<const void*>&) const override {}
  virtual TSharedPtr<IPropertyHandleArray> AsArray() override { return TSharedPtr<IPropertyHandleArray>(); }
  virtual TSharedPtr<IPropertyHandleSet> AsSet() override { return TSharedPtr<IPropertyHandleSet>(); }
  virtual TSharedPtr<IPropertyHandleMap> AsMap() override { return TSharedPtr<IPropertyHandleMap>(); }
  virtual TSharedPtr<IPropertyHandleOptional> AsOptional() override { return TSharedPtr<IPropertyHandleOptional>(); }
  virtual TSharedPtr<IPropertyHandleStruct> AsStruct() override { return TSharedPtr<IPropertyHandleStruct>(); }
  virtual FText GetPropertyDisplayName() const override { return FText(); }
  virtual void SetPropertyDisplayName(FText) override {}
  virtual void ResetToDefault() override {}
  virtual bool DiffersFromDefault() const override { return false; }
  virtual FText GetResetToDefaultLabel() const override { return FText(); }
  virtual bool GeneratePossibleValues(TArray<TSharedPtr<FString>>&, TArray<FText>&, TArray<bool>&) override { return false; }
  virtual void MarkHiddenByCustomization() override {}
  virtual void MarkResetToDefaultCustomized(bool) override {}
  virtual void ClearResetToDefaultCustomized() override {}
  virtual bool IsFavorite() const override { return false; }
  virtual bool IsCustomized() const override { return false; }
  virtual bool IsResetToDefaultCustomized() const override { return false; }
  virtual FString GeneratePathToProperty() const override { return FString(); }
  virtual TSharedRef<SWidget> CreatePropertyNameWidget(const FText&, const FText&) const override { return SNullWidget::NullWidget; }
  virtual TSharedRef<SWidget> CreatePropertyNameWidget(const FText&, const FText&, bool, bool, bool) const { return SNullWidget::NullWidget; } // deprecated
  virtual TSharedRef<SWidget> CreatePropertyValueWidget(bool) const override { return SNullWidget::NullWidget; }
  virtual TSharedRef<SWidget> CreatePropertyValueWidgetWithCustomization(const IDetailsView*) override { return SNullWidget::NullWidget; }
  virtual TSharedRef<SWidget> CreateDefaultPropertyButtonWidgets() const override { return SNullWidget::NullWidget; }
  virtual void CreateDefaultPropertyCopyPasteActions(FUIAction&, FUIAction&) const override {}
  virtual void AddRestriction(TSharedRef<const FPropertyRestriction>) override {}
  virtual bool IsRestricted(const FString&) const override { return false; }
  virtual bool IsRestricted(const FString&, TArray<FText>&) const override { return false; }
  virtual bool GenerateRestrictionToolTip(const FString&, FText&) const override { return false; }
  virtual bool IsDisabled(const FString&) const override { return false; }
  virtual bool IsDisabled(const FString&, TArray<FText>&) const override { return false; }
  virtual bool IsHidden(const FString&) const override { return false; }
  virtual bool IsHidden(const FString&, TArray<FText>&) const override { return false; }
  virtual void SetIgnoreValidation(bool bInIgnore) override {}
  virtual TArray<TSharedPtr<IPropertyHandle>> AddChildStructure(TSharedRef<FStructOnScope>) override { return TArray<TSharedPtr<IPropertyHandle>>(); }
  virtual TArray<TSharedPtr<IPropertyHandle>> AddChildStructure(TSharedRef<IStructureDataProvider>) override { return TArray<TSharedPtr<IPropertyHandle>>(); }
  virtual bool CanResetToDefault() const override { return false; }
  virtual void ExecuteCustomResetToDefault(const FResetToDefaultOverride&) override {}
  virtual FName GetDefaultCategoryName() const override { return FName(); }
  virtual FText GetDefaultCategoryText() const override { return FText(); }
  virtual bool IsCategoryHandle() const override { return false; }
  //ここまで
#if 0 //5.5 では更に以下のオーバーライドが必要になるので 5.5 でビルドする場合は #if 1 に変更してください。
  virtual bool IsExpanded() const override { return false; }
  virtual void SetExpanded(bool) override {}
  virtual bool GeneratePossibleValues(TArray<FString>&, TArray<FText>&, TArray<bool>&, TArray<FText>*) override { return false; }
#endif
};
}}
//コードの追加はここまで(↓にも追加コードがあるので注意)

TSharedRef<SWidget> UAssetSelectorWidget::RebuildWidget() {
  this->PropertyHandle = ::MakeShared<::DokuroMoe::FPropertyHandle>(); //追加
  this->OnSetObject.BindUObject(this, &UAssetSelectorWidget::OnObjectChangedInternal);
  this->ObjectPropertyEntryBox = SNew(SObjectPropertyEntryBox)
    .PropertyHandle(this->PropertyHandle) //追加
    .AllowedClass(this->AllowedClass)
    .OnObjectChanged(this->OnSetObject);
  return this->ObjectPropertyEntryBox.ToSharedRef();
}
void UAssetSelectorWidget::ReleaseSlateResources(bool bReleaseChildren) {
  Super::ReleaseSlateResources(bReleaseChildren);
  this->ObjectPropertyEntryBox.Reset();
  this->OnSetObject.Unbind();
  this->PropertyHandle.Reset(); //追加
}
void UAssetSelectorWidget::OnObjectChangedInternal(const FAssetData& InAssetData) {
  if (!this->OnObjectChanged.IsBound()) { return; }
  this->OnObjectChanged.Broadcast(InAssetData);
}
コードの説明

コンボボックスを機能させるために200行あまりのコードが必要になります。
しかも、ほとんどは使われない無駄なコードです。
SObjectPropertyEntryBox が汎用化されない理由が分かりました。(二度目)

SObjectPropertyEntryBox は ObjectPath という変数を持っています。
いかにもコンボボックスで選択したアセットのパスが入るような名前なので、これを SObjectPropertyEntryBox に与えるだけで動作して欲しかったのですが、うまく行きませんでした。

↑のコードよりも簡単な方法があるかも知れないので、興味があれば調べてみてください。
もっと簡単な方法があったら教えて欲しいです。


3. EUW のブループリントから選択しているアセットにアクセスできるようにする。

ブループリントでアセットのパスをゲットする処理を追加します。

// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.h

#pragma once

#include "Components/ContentWidget.h"
#include "PropertyCustomizationHelpers.h"
#include "AssetSelectorWidget.generated.h"

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnObjectChanged, const FAssetData&, InAssetData);

class IPropertyHandle;

UCLASS(BlueprintType)
class プロジェクト名EDITOR_API UAssetSelectorWidget
  : public UContentWidget
{
  GENERATED_BODY()
public:
  UPROPERTY(EditAnywhere)
  TObjectPtr<UClass> AllowedClass;

  UPROPERTY(BlueprintAssignable)
  FOnObjectChanged OnObjectChanged;

//追加
  UPROPERTY(BlueprintReadOnly)
  FString SelectedAssetPath;
//ここまで
protected:
  virtual TSharedRef<SWidget> RebuildWidget() override;
  virtual void ReleaseSlateResources(bool bReleaseChildren) override;
  virtual void OnObjectChangedInternal(const FAssetData& InAssetData);

  TSharedPtr<SObjectPropertyEntryBox> ObjectPropertyEntryBox;
  FOnSetObject OnSetObject;
  TSharedPtr<IPropertyHandle> PropertyHandle;
};
// Copyright(c) dokuro.moe All Rights Reserved.

// AssetSelectorWidget.cpp

#include "AssetSelectorWidget.h"
#include "PropertyHandle.h"

/* 長いので省略 */

TSharedRef<SWidget> UAssetSelectorWidget::RebuildWidget() {
  /* 略 */
}
void UAssetSelectorWidget::ReleaseSlateResources(bool bReleaseChildren) {
  /* 略 */
}
void UAssetSelectorWidget::OnObjectChangedInternal(const FAssetData& InAssetData) {
//追加
  this->SelectedAssetPath = InAssetData.PackageName.ToString();
//ここまで
  if (!this->OnObjectChanged.IsBound()) { return; }
  this->OnObjectChanged.Broadcast(InAssetData);
}


ブループリントでアクセスできるようになりました。


4. 選択しているアセットが変わったときに EUW のブループリントのイベントが呼ばれるようにする。

これは既に「B. 選択しているアセットが変更されたことを通知するイベントを指定する。」で対応しています。

ここでは実際にブループリントを使って動作確認します。
作成したブループリントは以下のスクリーンショットをご覧ください。

※タップまたはクリックすると大きい画像で表示します。

このイベントを呼ぶために、デザイナーでイベントを設定します。
※タップまたはクリックすると大きい画像で表示します。


【仕上げ】アセットを選択したらそのサムネイルが表示されるようにする

ブループリントで必要な情報(コンボボックスで選択したアセットのパス)を取得できるようになったので、サムネイルを変更する処理もブループリントで作ります。

※タップまたはクリックすると大きい画像で表示します。

ブループリントの説明

Event Construct では、何も選択していないときのサムネイルを設定しています。
この
On Object Changed イベントが呼ばれたら、選択したオブジェクトのパスをサムネイルに設定します。
コンボボックスで「クリア」を選択すると、無効なパスになるため、その場合は何も選択していないときのサムネイルに戻します。

この EUW を別の EUW に含めたときに、選択したアセットのパスを外部で受け取れるようにするため、ブループリントの変数 AssetPath に選択したアセットのパスをセットしています。

動作確認

※タップまたはクリックすると大きい画像で表示します。
コンボボックスでアセットを選択すると…。


選択したアセットのサムネイルが表示され、コンボボックスにアセット名が表示されるようになる。

やってみた感想

UE の学習を始めてから何年も経ちますが、何ができるのかが分かると、「ドキュメントには書いてないし、検索しても出て来ないけど、こういうことはできるのかな?」と思いつくようになります。
余計なことはしないで、そのままにしておけばいいのですが、試さずにはいられない性分なので試すじゃないですか。
そしたら、予想の1億倍面倒なことが分かる…というのを繰り返しています。

やりごたえはあるのですが、解決方法を見つけ出して、その方法を共有するために記事にまとめると、膨大な時間がかかってしまいます…。
面倒なものほど、それによって得られる知見が多くなる傾向があるためです。

そんな私なので、「やぶ蛇に自ら突っ込むおりが」(『おりが』は筆者のハンドルネーム)という二つ名を賜りたい。

プラグイン化しました。

UE5 AssetSelectorWidget Plugin
[Download at GitHub]

この記事で共有した EUW を使いまわせるようプラグインにしました。MIT ライセンスです。

プロジェクト直下の Plugins フォルダに AssetSelectorWidget フォルダを放り込めば使えると思います。
※README.md と LICENSE ファイルは削除して構いません。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です