Have you ever wanted to build the contents of a UUserWidget
in the editor
using C++? For example imagine you want to create a gallery widget and you need
20 buttons with correct names. You could create them by hand, but it's slow and
error-prone.
The example below shows how to create widgets inside a User Widget blueprint, from C++, and correctly update the widget tree so the new widgets can be selected.
Huge thanks to @Sharundaar for sharing how to do this in my Discord, and allowing me to share it here.
Edit: It's worth mentioning that if you're trying to fill out a list, tree or grid in the editor, to give designers an idea of how it will look, you should consider using ListView.
WBP_EditorTestWidget
The final result, notice the tree contains the text widgets created, as well as the "Fill Text Blocks" button in the details panel.
Solution
First, we'll need to add "UMGEditor"
and "UnrealEd"
to your dependencies in
your Build.cs
file.
We're doing something a little weird here, we're going to be adding some
editor-specific code to a non-editor class. In order to use the editor
functions, we need to add UnrealEd
and UMGEditor
to our list of modules in
MyProject.Build.cs
, but only when building the editor.
MyProject.Build.cs snippet
if (Target.bBuildEditor)
{
PrivateDependencyModuleNames.AddRange(new string[]
{
"UMGEditor",
"UnrealEd"
});
}
BUIEditorTestWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "BUIEditorTestWidget.generated.h"
UCLASS()
class UBUIEditorTestWidget : public UUserWidget
{
GENERATED_BODY()
protected:
// Marking a parameterless function with CallInEditor makes a button
// show up in editor with that name (see screenshot)
UFUNCTION(CallInEditor, Category="Editor Fill Test")
void FillTextBlocks();
#if WITH_EDITORONLY_DATA
UPROPERTY(EditAnywhere, Category="Editor Fill Test", meta=(UIMin=1, UIMax=10, ClampMin=1, ClampMax=10))
int32 Width = 2;
UPROPERTY(EditAnywhere, Category="Editor Fill Test", meta=(UIMin=1, UIMax=10, ClampMin=1, ClampMax=10))
int32 Height = 2;
#endif
// We don't use GridPanel directly in this example, but it being BindWidget means Unreal
// will display an error if there is no correctly-named variable.
UPROPERTY(meta=(BindWidget))
class UGridPanel* GridPanel;
};
BUIEditorTestWidget.cpp
#include "BUIEditorTestWidget.h"
#if WITH_EDITOR
#include "WidgetBlueprint.h"
#include "Blueprint/WidgetBlueprintGeneratedClass.h"
#include "Blueprint/WidgetTree.h"
#include "Components/GridPanel.h"
#include "Components/TextBlock.h"
#include "Kismet2/BlueprintEditorUtils.h"
#endif
void UBUIEditorTestWidget::FillTextBlocks()
{
#if WITH_EDITOR
if (!GridPanel)
return;
UWidgetBlueprintGeneratedClass* WidgetBlueprintGeneratedClass = Cast<UWidgetBlueprintGeneratedClass>(GetClass());
UPackage* Package = WidgetBlueprintGeneratedClass->GetPackage();
UWidgetBlueprint* MainAsset = Cast<UWidgetBlueprint>(Package->FindAssetInPackage());
// We *cannot* use the BindWidget-marked GridPanel, instead we need to get the widget in the asset's widget tree.
// However thanks to the BindWidget, we can be relatively sure that FindWidget will be successful.
UGridPanel* AssetGridPanel = Cast<UGridPanel>(MainAsset->WidgetTree->FindWidget("GridPanel"));
AssetGridPanel->ClearChildren();
for (int32 Y = 0; Y < Height; ++Y)
{
for (int32 X = 0; X < Width; ++X)
{
const FString WidgetName = FString::Printf(TEXT("Text_X%d_Y%x"), X, Y);
// Create widget with ConstructWidget, not NewObject
UTextBlock* Text = MainAsset->WidgetTree->ConstructWidget<UTextBlock>(UTextBlock::StaticClass(), FName(WidgetName));
Text->SetText(FText::FromString(WidgetName));
AssetGridPanel->AddChildToGrid(Text, X, Y);
}
}
AssetGridPanel->Modify();
MainAsset->Modify();
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(MainAsset);
#endif
}