本稿では Unreal Engine 5 の C++ を使って複数の子 Editor Utility Widget(エディタユーティリティウィジェット、以下 EUW) を、ひとつの親 EUW にはめ込んで表示する方法について共有します。
EUW のマニアックな使い方に興味があるエンジニア向けです。
動作環境
Unreal Engine 5.4.3 および 5.4.4
Windows 11 Home
Visual Studio 2022
EUW に限らず、UI は親とは別に作った子のパーツを組み合わせて作る方が柔軟性が高くなります。
そうすることで、必要に応じて好きなタイミングで別のパーツに差し替えることができるようになるためです。

これをブループリントでやるのは簡単です。
ブループリントでは誰でも簡単にいじれてしまうので、C++ で作ろうとしたときに参考資料が見つからずに困りました。
上記画像の「Construct 子パーツ名」ノードに該当する関数が C++ 側にはありません。
細かい事は割愛しますが、あれこれ試行錯誤したところ、子の EUW を構築するときは、FSoftObjectPath などを使ってアセットとしてロードする必要があることが分かりました。
FSoftObjectPath pathA(FString(TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/Test/ChildPartsA.ChildPartsA'")));
UObject* partsA_widget = pathA.TryLoad();
ネットだと、UGameplayStatics::SpawnObject() でいけるという話も出て来るのですが、うまく行きませんでした。
この関数では引数のチェックをした後に NewObject しているだけなので、NewObject で良さそうに思ったのですが、本稿のケースでは NewObject では対応できません。
親用の EUW を新規作成します。
CanvasPanel(親用)の下に、子を入れるための別のキャンバスパネル(子用)を追加します。
キャンバスパネルを使う理由は、GetLayout / SetLayout 関数を使って EUW のフォームサイズに合わせてレイアウト調整を行えるようにするためです。

子用の EUW を新規作成します。
本稿では3つの子を用意しています。
CanvasPanel の下にボーダーを追加し、子 EUW の表示範囲が分かりやすいよう色を着けています。



EUW に設定する親クラスを C++ で作成します。
Editor Utility Widget はエディタ用の機能なので、エディタモジュールにソースファイルを作成します。
プロジェクト名は EUWTest です。
エディタモジュール用の *.Build.cs に以下のモジュールを追加します。
private でも public でも、どっちでも良いです。
“UMG”
“UMGEditor”
“Blutility”
// Copyright dokuro.moe
// EUWTestEditor.h
#pragma once
#include "CoreMinimal.h"
#include "Editor/Blutility/Classes/EditorUtilityWidget.h"
#include "Editor/UMGEditor/Public/WidgetBlueprint.h"
#include "EUWTestEditor.generated.h"
class UCanvasPanel;
class UEditorUtilityWidgetBlueprint;
// 子 A の EUW
UCLASS(BlueprintType)
class EUWTESTEDITOR_API UEUWChildPartsA
: public UEditorUtilityWidget {
GENERATED_BODY()
};
// 子 B の EUW
UCLASS(BlueprintType)
class EUWTESTEDITOR_API UEUWChildPartsB
: public UEditorUtilityWidget {
GENERATED_BODY()
};
// 子 C の EUW
UCLASS(BlueprintType)
class EUWTESTEDITOR_API UEUWChildPartsC
: public UEditorUtilityWidget {
GENERATED_BODY()
};
// 親の EUW
UCLASS(BlueprintType)
class EUWTESTEDITOR_API UEUWParentWidget final
: public UEditorUtilityWidget {
GENERATED_BODY()
public:
virtual void NativeTick(const FGeometry& Geometry, float DeltaTime) override;
private:
bool IsPassedNativeTick() const { return this->bPassedNativTick; }
bool ConstructChildParts();
void AdjustLayout();
// 親用キャンバスパネルを C++ 側で使えるようにバインド
UPROPERTY(EditAnywhere, meta = (BindWidget))
UCanvasPanel* CanvasPanel{nullptr};
UPROPERTY()
TArray<UEditorUtilityWidgetBlueprint*> ChildParts;
bool bPassedNativTick{false};
FVector2f PrevSize;
FVector2f PrevPosition;
FVector2f Size;
FVector2f Position;
};
// Copyright dokuro.moe
// EUWTestEditor.cpp
#include "EUWTestEditor.h"
#include "Components/CanvasPanel.h"
#include "Components/CanvasPanelSlot.h"
#include "Editor/Blutility/Classes/EditorUtilityWidgetBlueprint.h"
void UEUWParentWidget::NativeTick(const FGeometry& Geometry, float DeltaTime) {
this->PrevSize = this->Size;
this->PrevPosition = this->Position;
// 親ウィンドウの表示位置とサイズをキャッシュ
this->Size = Geometry.GetLocalSize();
this->Position = Geometry.AbsolutePosition;
if (this->PrevSize != this->Size) {
// ウィンドウサイズが変わったら子を配置し直す
this->AdjustLayout();
}
if (!this->bPassedNativTick) {
// 子を作成(一回だけ実行)
this->ConstructChildParts();
}
}
bool UEUWParentWidget::ConstructChildParts() {
if ( this->IsPassedNativeTick()) { return false; }
if (!IsValid(this->CanvasPanel)) { return false; }
// パスは適宜修正してください。
FSoftObjectPath pathA(FString(TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/Test/ChildPartsA.ChildPartsA'")));
UObject* partsA_widget = pathA.TryLoad();
if (!IsValid(partsA_widget)) { return false; }
FSoftObjectPath pathB(FString(TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/Test/ChildPartsB.ChildPartsB'")));
UObject* partsB_widget = pathB.TryLoad();
if (!IsValid(partsB_widget)) { return false; }
FSoftObjectPath pathC(FString(TEXT("/Script/Blutility.EditorUtilityWidgetBlueprint'/Game/Test/ChildPartsC.ChildPartsC'")));
UObject* partsC_widget = pathC.TryLoad();
if (!IsValid(partsC_widget)) { return false; }
this->ChildParts.Add(CastChecked<UEditorUtilityWidgetBlueprint>(partsA_widget));
this->ChildParts.Add(CastChecked<UEditorUtilityWidgetBlueprint>(partsB_widget));
this->ChildParts.Add(CastChecked<UEditorUtilityWidgetBlueprint>(partsC_widget));
TArray<UWidget*> all_children = this->CanvasPanel->GetAllChildren();
if (all_children.Num() != this->ChildParts.Num()) { return false; }
TArray<UCanvasPanel*> panels = {
CastChecked<UCanvasPanel>(all_children[0]),
CastChecked<UCanvasPanel>(all_children[1]),
CastChecked<UCanvasPanel>(all_children[2])
};
for (int32 i = 0; i < panels.Num(); i++) {
UEditorUtilityWidgetBlueprint* widget_bp = this->ChildParts[i];
widget_bp->CreateUtilityWidget();
UWidget * widget = widget_bp->GetCreatedWidget();
UCanvasPanel* panel = panels[i];
panel->AddChild(widget);
}
this->AdjustLayout();
this->bPassedNativTick = true;
return true;
}
void UEUWParentWidget::AdjustLayout() {
TArray<UWidget*> all_children = this->CanvasPanel->GetAllChildren();
if (all_children.Num() != this->ChildParts.Num()) { return; }
TArray<UCanvasPanel*> panels = {
CastChecked<UCanvasPanel>(all_children[0]),
CastChecked<UCanvasPanel>(all_children[1]),
CastChecked<UCanvasPanel>(all_children[2])
};
const TArray<FVector2D> positions = {
FVector2D( 0.f, 0.f),
FVector2D( 0.f, this->Size.Y * 0.4f + 4.f),
FVector2D(this->Size.X * 0.5f + 2.f, this->Size.Y * 0.4f + 4.f)
};
const TArray<FVector2D> sizes = {
FVector2D(this->Size.X , this->Size.Y * 0.4f),
FVector2D(this->Size.X * 0.5f - 2.f, this->Size.Y * 0.6f - 4.f),
FVector2D(this->Size.X * 0.5f - 2.f, this->Size.Y * 0.6f - 4.f)
};
for (int32 i = 0; i < panels.Num(); i++) {
UWidget * widget = this->ChildParts[i]->GetCreatedWidget();
UCanvasPanel * panel = panels[i];
UCanvasPanelSlot* panel_slot = CastChecked<UCanvasPanelSlot>(panel->Slot);
UCanvasPanelSlot* child_panel = CastChecked<UCanvasPanelSlot>(widget->Slot);
// 子をはめ込む親のキャンバスパネル
{
FAnchorData layout = panel_slot->GetLayout();
layout.Offsets.Left = positions[i].X;
layout.Offsets.Top = positions[i].Y;
layout.Offsets.Right = positions[i].X + sizes[i].X;
layout.Offsets.Bottom = positions[i].Y + sizes[i].Y;
panel_slot->SetLayout(layout);
}
// 子のキャンバスパネル
{
FAnchorData layout = child_panel->GetLayout();
layout.Offsets.Left = 0.f;
layout.Offsets.Top = 0.f;
layout.Offsets.Right = sizes[i].X;
layout.Offsets.Bottom = sizes[i].Y;
child_panel->SetLayout(layout);
}
}
}
コードの通りですが、子 EUW は親を表示してからロードして表示しています。
EUW を表示中に PartsC を PartsD と入れ替えたい…という要望が出て来ても、上記コードのように処理すれば実現できます。
ポインタの有効性チェックを飛ばしているところがあります。
if (pointer) とか if (IsValid(pointer)) とか
ゲームとして実行するコードや、クラッシュして作業データが失われる可能性のあるツール用のコードなら、もっと厳しくチェックするのですが、動作検証用のコードなのでいちいちチェックしません。
親と全ての子の親クラスを↑のコードで定義したクラスに変更します。


