※この記事にサンプルプロジェクトはありません。
プラグイン等に関しては記事内のリンクからダウンロードしてください。
→Github:kinnajichan

この記事は、Unreal Engine Meetup Connect – Vol.2 – UETipsLT編にて講演した、「UE5版 EditorUtilityWidgetについてのあれこれ」のスライドの内容を記事にしたものです。

スライドだと検索性が低くなるため、記事としても投稿いたします。

EUWについての記事

※これらの記事の内容をある程度読むことでより理解が深まるかと思います。

•公式ドキュメント:
エディタ ユーティリティ ウィジェット

•わかりやすく説明してる記事:
UE5:Editor Utilityを活用したツール制作術 第1回
(CGWorld:とんこつ様&キンアジより)

•基礎は大体書いてある(古いけど):
【UE4】Editor Utility Widgetについてのあれこれ(アンナプルナ様より)

•EUWに関する記事一覧:
Unreal Engine Editor Utility Widget | エディタ拡張(UE5攻略リンク様より)

•過去に公開したEUWTips:
EditorUtilityWidgetPetitDeepdive(Docswell:キンアジより)

UE5のEUWアップデートTips

ObjectMixer(UE5.2~)

UE5.2から、EUWにてObjectMixerというWidgetが使えるようになりました。

独自のクラスフィルタリングやアクターが持つプロパティをObjectMixerWidget上に直接表示させるようなカスタマイズされたOutlinerを独自に作成することができます。

※UE5.2だと意図通りに動かない機能があるため、UE5.3~使うのがオススメです。

詳しい説明:【UE5】BPだけで独自のOutlinerを作っちゃおう!【★★☆~★★★☆】(キンアジより)
ObjectMixerを使用したプラグイン:KAJiraUtility(Github:キンアジより)

EUWの複数起動(UE5.2~)

UE5.1までは、EUWのウィンドウは同じクラスで複数立ち上げることをC++を用いなければできませんでしたが、UE5.2からはUEditorUtilitySubsystem::SpawnAndRegisterTabWithIDにて複数立ち上げることができます。

ちなみに、UE5.2から追加されたEUWのEditorにあるRunUtilityWidgetボタンと右クリメニューのRunEditorUtilityWidgetSpawnAndRegisterTabノードは別枠(Asset名から自動的に作られるID)なので、IDを指定した起動方法でなくても2つまでは同じクラスのWindowを起動できます。

変数表示の自動化(UE5.2~)

UE5.2から、ProjectSettingsEngineUserInterfaceにあるAuthorizeAutomaticWidgetVariableCreationというプロパティが追加されています。

UE5.1までは、ButtonなどのWidgetは新しくDesignerウィンドウ上で追加した場合に自動的にIsVariableのチェックが有効になっていましたが、UE5.2からはAuthorizeAutomaticWidgetVariableCreationを有効にしないと自動的にチェックが入らないようになっています。
※通常のWidgetBlueprintと共通の設定です。

EUWのデバッグ(UE5.2~)

UE5.2から、EUWがプレイ中でなくてもブレークポイントによるブループリントのデバッグができるようになりました。これに伴い、以下2つのプロパティがEditorUtilityWidgetBlueprintに追加されています。
参考:[UE5] UE5.2以降、EditorUtilityWidgetがエディタ実行中に動作しなくなった時に確認すべきこと ( Is Enabled in PIE プロパティ )
(おかず様より)

IsEnabledinPIE

IsEnabledinPIEを有効にすることで、PIE中でもEUWが動作するようになります。(デフォルトで無効)

※UE5.1まではデフォルトでPIEでも使用できたので、バージョンアップに伴いPIEで使用ができなくなった場合はこの設定を確認してください。

IsEnabledinDebugging

ブレークポイントで動作が停止している場合にデフォルトだとEUWは動作しなくなります。
IsEnabledinDebuggingを有効にすることで、ブレークポイントで動作が停止している場合でもEUWが動作するようになります。

※プレイ中、非プレイ中のどちらにも適用されます。

EUW用のWidget(UE5.3~)

UE5.3から、EUW用にEditorUtilityButtonやEditorUtilityEditableText等の汎用的なWidgetクラスが用意されました。

機能としてのアップデートではなく、Runtime用のWidgetクラスと分けることで、UE5.2まではRuntime用のWidgetクラスにEUW用のStyle(見た目)が記載されていたのがなくなり内部的な実装の切り分けが行いやすいような設計になりました。

TemplateSelector(UE5.3~)

UE5.2までは、デフォルトで新規にEUWを作成する際はRootとなるパネルは必ずCanvasPanelとなっていましたが(WidgetBlueprintと共通の設定が使われていた)UE5.3からデフォルトでRootとなるWidgetを選択できるTemplateSelectorEditorUtilityWidgetBlueprint用にも実装されています。

なお、TemplateSelectorで表示されるWidgetClassはProjectSettingsEditorEditorUtilityWidgets(Team)WidgetClassesToHideによる表示しないWidgetClassの影響を受けません。

特に問題があるわけではないのですが、意図せずRuntime用のWidgetを使用してしまう可能性がある点に注意しましょう。

Templateを選ばせずにUE5.2以前と同じようにする方法
→以下のようなコードをProject等のStartupModule()に記載し、UseWidgetTemplateSelectorFalseにします。

//追加記述が必要なコードのみ記載
//※build.csのPrivateDependencyModuleNamesに"Blutility"を追加が必要
#include "EditorUtilityWidgetProjectSettings.h"
#include "PropertyEditorModule.h"
void FKAMyProjectModule::StartupModule()
{
	FPropertyEditorModule& PropertyModule =
		FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
	PropertyModule.UnregisterCustomClassLayout(
		UEditorUtilityWidgetProjectSettings::StaticClass()->GetFName());
}

WidgetBlueprintからの変換(UE5.3~)

UE5.3から、通常のWidgetBlueprintEditorUtilityWidgetBlueprintに変換することができるようになりました。(WidgetBlueprintを右クリック→AssetActionsConverttoEditorUtilityWidgetから変換可能)

ちなみに、このようなコードをプロジェクトのソース等に関数として書いて実行すれば、EditorUtilityWidgetBlueprintから通常のWidgetBlueprintへ逆変換できます。(UEditorUtilityLibrary::ConvertToEditorUtilityWidgetを少し改良しただけ)

//必要なコードのみ記載
//※build.csのPrivateDependencyModuleNamesに"Blutility"を追加が必要
#include "EditorUtilityWidgetProjectSettings.h"
#include "PropertyEditorModule.h"

void UKAEditorUtilityBlueprintLibrary::ConvertToEUWBPToWBP(UEditorUtilityWidgetBlueprint* EUWBP)
{
if (!EUWBP || !EUWBP->IsA<UEditorUtilityWidgetBlueprint>()){
	return;
}
FName BPName = EUWBP->GetFName();
UObject* Outer = EUWBP->GetOuter();
EObjectFlags Flags = EUWBP->GetFlags();

TArray<struct FEditedDocumentInfo> OriginalEditedDocuments = EUWBP->LastEditedDocuments;
EUWBP->Rename(nullptr, GetTransientPackage(), REN_DontCreateRedirectors | REN_SkipGeneratedClasses | REN_ForceNoResetLoaders);
TArray<UObject*> Children;
GetObjectsWithOuter(EUWBP, Children, false);

UWidgetBlueprint* WBP = NewObject<UWidgetBlueprint>(Outer, BPName, Flags);
if (WBP->WidgetTree)
{
WBP->WidgetTree->Rename(nullptr, GetTransientPackage(), REN_ForceNoResetLoaders | REN_DontCreateRedirectors);
WBP->WidgetTree = EUWBP->WidgetTree;
}
for (UObject* Child : Children)
{
Child->Rename(nullptr, WBP, REN_DontCreateRedirectors | REN_SkipGeneratedClasses | REN_ForceNoResetLoaders);
}
UEngine::FCopyPropertiesForUnrelatedObjectsParams Params;
Params.bPerformDuplication = true;
Params.bNotifyObjectReplacement = false;
Params.bPreserveRootComponent = false;
UEngine::CopyPropertiesForUnrelatedObjects(EUWBP, WBP);
WBP->LastEditedDocuments = OriginalEditedDocuments;

WBP->GeneratedClass->ClassGeneratedBy = WBP;
check(WBP->GeneratedClass == nullptr || WBP->GeneratedClass->GetOuter() == WBP->GetOuter());

TMap<UObject*, UObject*> OldToNew;
OldToNew.Add(EUWBP, WBP);

TArray<UObject*> Targets = { EUWBP, GetTransientPackage() };
FArchiveReplaceObjectRef<UObject> ReplaceReferencesInRoot(WBP, OldToNew);
FFindReferencersArchive Archive(WBP, Targets);
check(Archive.GetReferenceCount(EUWBP) == 0);

Children.Reset();
GetObjectsWithOuter(WBP->GetOuter(), Children);

for (UObject* Child : Children)
{
FArchiveReplaceObjectRef<UObject> ReplaceReferences(Child, OldToNew);
FFindReferencersArchive Archive2(Child, Targets);
check(Archive2.GetReferenceCount(EUWBP) == 0);
}
}

Assetのサムネ表示用Widget(UE5.3~)

UE5.3から、アセットのサムネイルをEUW上で表示できるAssetThumbnailWidgetが追加されました。
使用例:UE5:Editor Utilityを活用したツール制作術 第2回:アセットのプロパティを設定・取得するツール(CGWorld:とんこつ様より)

ThumbnailLabelの色はHintColorAndOpacityで設定されており、デフォルトでOpacityが0になっていて見えないので、Asset名等を表示したい場合はHintColorAndOpacityの設定を見直してみてください。

その他EUWTips

簡単に実装できるキーイベント

EUWで機能するショートカットキーのようなものを、FUICommandInfo等のC++実装をする必要なく簡単に実装できます。

//※一度EUW内の何かしらのWidgetをクリックしないと有効化されない。
//自動的に有効にしたい場合は、EventConstruct等で以下のようなコードをBPに公開し実行することで可能。
//※build.csのPrivateDependencyModuleNamesに"Slate","SlateCore","Blutility"を追加が必要
#include "Framework/Application/SlateApplication.h"
#include "EditorUtilityWidget.h"void UKAEditorUtilityBlueprintLibrary::SetEUWFocus(UEditorUtilityWidget* FocusWidget)
{
if (FocusWidget)
{FSlateApplication::Get().SetAllUserFocus(FocusWidget->TakeWidget(), EFocusCause::SetDirectly);}
}

各種設定の保存

EUWを使用していると、DetailViews等で使用したプロパティを保存したくなる場合が多いですが、
プレイ中でなくてもSaveGameObjectは使用できるため、エディタ用のデータを保存する際に有効です。

または、変数のConfigVariableを有効にして、SavedのEditor.iniに保存するのも手っ取り早いです。

//※Configの場合はEUWインスタンス等の保存するインスタンスに対してSaveConfigをする必要がある↓はSaveConfigをBPに公開するためのコード例

void UKAEditorUtilityBlueprintLibrary::SaveConfig(UObject* InObject)
{
if (InObject) { InObject->SaveConfig(); }
}

不必要に編集されるPersistentLevel

EUWが起動する事によって生成されるEUWインスタンスは「Editor上で開いているWorld」に所属します(これによって、GetAllActorsOfClass等でエディタ上のアクター等へのアクセスが容易にできるようになっています)
→詳しくは【UE4】EditorUtility上のWorldContextについて【★★☆】(キンアジより)

通常の起動方法で起動したEUWのインスタンスはTransient(一時的)フラグが自動的に付与されるため、内部の変数をDetailViews等で変更しても他に影響を与えませんが、EUWをOuterとして生成したObjectやCreateWidget等で生成したサブウィジェットとしてのEUW等には明示的にTransientフラグをつけないと、そのオブジェクトに変更が加わった場合に
PersistentLevelが変更された扱いになってしまいます。

//UEditorUtilityWidgetBlueprint::CreateUtilityWidgetから抜粋(EUWのOuter(親)はEditor上のWorld)
if (UWorld* World = GEditor->GetEditorWorldContext().World())
{
CreatedUMGWidget = CreateWidget<UEditorUtilityWidget>(World, WidgetClass);
…

解決方法として、WidgetやObject等のインスタンスを生成する場合は、他に影響を与えないTransientなObjectをOuterにするか、Transientフラグを追加するようにしましょう。

→TransientなOuterにする場合、ブループリントではConstructObjectFromClassノードでOuterにEditorUtilitySubsystemあたりを接続しておくと無難です。

→インスタンスをTransientにするにはC++定義のクラスであればUCLASSの指定子
Transientを追加すればよいですが、Blueprintクラスの場合はSetFlags関数を
ブループリントに公開し実行することで、Transientなフラグを設定してあげると解決できます。

//Transient指定子
UCLASS(Transient)
class UEditorUtilityTestObject : public UEditorUtilityObject
//SetFlagsでTransientフラグの追加(BP関数への公開例)

void UEditorUtilityTestLibrary::AddTransientFlag(UObject* InObject)
{
if (InObject)
{
InObject->SetFlags(RF_Transient);
}
}

レベルを開くと再生成されるEUW

ULevelEditorSubsystem::LoadLevel関数等で別レベルを開くようなツールをEUWで作成することがある場合、前ページで説明した所属ワールドの都合によりレベルが開かれたタイミングで新しいEUWのインスタンスが生成され、開いているEUWのウィンドウで表示されているものは自動的に置き換えられてしまうので注意が必要です。
また、レベル移動前の古いEUWインスタンスは、どのWorldにも所属しなくなるため、WorldContextを使用したLatentノード(例えばDelayノード)のCallbackが返ってこなくなります。レベルを開いた後はWorldContextに依存しないノードで処理をする等の工夫が必要です。

コンテンツブラウザで実行されるPreConstructイベント

通常のUserWidgetでもそうですが、EventPreConstructノードはWidgetインスタンスを生成する前からDesignerウィンドウ上等でも実行されるため、非常に便利なノードとなっています。
しかし、 EventPreConstructノードはコンテンツブラウザ上でWidgetClassにマウスカーソルをあわせたタイミングでも毎フレーム実行されてまいます。
また、 EventPreConstructで値のセットやレベルの移動等を行ってしまうと意図せず実行されてしまうので、そのような処理はEventConstructノードに書くようにしましょう。

//CustomWidgetMeny.h//ModuleはToolMenus,Blutility,UMG,Slate,SlateCoreを追加

#pragma once
#include "CoreMinimal.h"
#include "EditorUtilityObject.h"
#include "EditorUtilityWidget.h"
#include "CustomWidgetMeny.generated.h"
UCLASS()
class KAEDITORUTILITYPLUGIN_API UCustomWidgetMeny : public UEditorUtilityObject
{
GENERATED_BODY()
public:
UPROPERTY()
TSubclassOf<UEditorUtilityWidget> WidgetClass;
//Widget用MenuEntryをToolMenusに追加
UFUNCTION(BlueprintCallable, Category = "KAEditorUtility")
void AddEUWMenyEntry(FName InOwner, FName InName, TSubclassOf<UEditorUtilityWidget> InWidgetClass);

TSharedRef<SWidget> GetCustomMenuWidget(
const FToolMenuContext& ToolMenuContext, 
const FToolMenuCustomWidgetContext& CustomWidgetContext, 
FName MenuName);
};
//CustomWidgetMeny.cpp
#include "CustomWidgetMeny.h"
#include "Subsystems/UnrealEditorSubsystem.h"
void UCustomWidgetMeny::AddEUWMenyEntry(
     FName InOwner, 
     FName InName, 
     TSubclassOf<UEditorUtilityWidget> InWidgetClass)
{
UToolMenus* ToolMenus = UToolMenus::Get();
FToolMenuEntry Entry(InOwner, InName, EMultiBlockType::MenuEntry);
UToolMenu* OwnerMenu = ToolMenus->FindMenu(InOwner);
Entry.MakeCustomWidget.BindUObject(this, &UCustomWidgetMeny::GetCustomMenuWidget, InName);
Entry.Type = EMultiBlockType::Widget;
Entry.WidgetData.bNoIndent = true;
Entry.WidgetData.bNoPadding = true;
WidgetClass = InWidgetClass;
OwnerMenu->AddMenuEntry(NAME_None, Entry);
ToolMenus->RefreshAllWidgets();
}
TSharedRef<SWidget> UCustomWidgetMeny::GetCustomMenuWidget(
     const FToolMenuContext& ToolMenuContext, 
     const FToolMenuCustomWidgetContext& CustomWidgetContext, 
     FName MenuName)
{
if (!WidgetClass)
{
return SNew(STextBlock).Text(FText::FromString("Invalid Menu Class"));
}
UUserWidget* MakeWidget = CreateWidget<UUserWidget>(
GEditor->GetEditorSubsystem<UUnrealEditorSubsystem>()->GetEditorWorld(), WidgetClass);
if (!IsValid(MakeWidget))
{
return SNew(STextBlock).Text(FText::FromString("Menu Widget Creation Failed"));
}
return MakeWidget->TakeWidget();
}

メニューに直接EUWを追加する

エディター上部のToolbarや右クリックメニューに対してEUWを直接表示することができます。
※EUWである必要はないですが、Editor拡張用としてEUWが一番適しているため。

簡単にエディター上にメニューを登録するプラグイン(試作版)を公開しておりますので、よければ御覧ください。
(プラグイン内にEUWのメニュー化機能もあり)
KAEditorUtilityPlugin(Github:キンアジより)

EUWを閉じる際の確認処理

EUWのタブを閉じる際に、閉じるかどうかを制御する汎用的な仕組みの実装を行うことができます。
たとえば「とじますか?」のようなポップアップメッセージを出して、キャンセルできるような実装を行いたいときにとても便利です。

※以下のソースはコアな実装しか記載していないため、これだけでは再現できません(複数クラスにまたがったりして収まらないので)
詳しくはKAEditorUtilityPlugin(Github:キンアジより)にプラグイン化されていますので御覧ください。

bool UKA_EditorUtilityAssistSubsystem::BindCanCloseEUWTab(
class UEditorUtilityWidgetBlueprint* EUWBP, 
FName TabID, 
FOnGetCanCloseEUWTab CanCloseTabDelegate)
{
FName GeneratedTabID = GenerateEUWTabID(EUWBP, TabID);
if (TSharedPtr<SDockTab> DockTab = GetEUWDockTab(EUWBP, TabID))
{
UKA_EUWTabManagerProxy* TabManagerProxy = nullptr;
if (TabManagerProxyList.Contains(GeneratedTabID))
{
TabManagerProxy = TabManagerProxyList[GeneratedTabID];
}
if(!TabManagerProxy)
{
TabManagerProxy = NewObject<UKA_EUWTabManagerProxy>(this);
TabManagerProxyList.Add(GeneratedTabID, TabManagerProxy);
}
if (FAssetRegistryModule* AssetRegistryModule = 
FModuleManager::GetModulePtr<FAssetRegistryModule>(TEXT("AssetRegistry")))
{
AssetRegistryModule->Get().OnAssetRemoved().AddUObject(
TabManagerProxy, 
&UKA_EUWTabManagerProxy::OnAssetRemoved);
}
TabManagerProxy->GetCanCloseEUWTabDelegate = CanCloseTabDelegate;
TabManagerProxy->TabID = GeneratedTabID;
TabManagerProxy->EUWBPPath = FSoftObjectPath(EUWBP);
DockTab.Get()->SetCanCloseTab(
SDockTab::FCanCloseTab::CreateUObject(TabManagerProxy, 
&UKA_EUWTabManagerProxy::GetCanCloseScriptEditorTab));
return true;
}
return false;
}

以上です!