Introduction
What is the best way to set the size of a Blueprint UUserWidget when dynamically adding it to a UCanvasPanel?
While porting Dice and Sorcery over to Unreal 5, I came across this question while trying to implement the dice used for the player’s abilities.
These dice offer the following challenges:
- Each Die has its own look and each die can have different numbers of pips on each face.
- There is an unknown amount of dice.
- Dice can be grabbed in any order, and can be placed back.
- Upon entering, dice will show a random texture based on distance moved.
- Look and Function of the dice can change per die.
To facilitate these restraints, a UCanvasPanel
was used to be flexible. However attempting this, the following issue occured:
Thats… not very square is it? A commonly suggested way to fix this, is to add a top level SizeBox
or ScaleBox
to the widget.
This is actually a good way to handle sizing the widgets for most cases. However, during runtime, a UCanvasPanel can ignore the desired size completely.
The Simple Solution
The second solution was to set the size on the UCanvasPanelSlot
, right after adding it to the UCanvasPanel
.
C++
auto* createdWidget = CreateWidget<UDiceWidget>(MyCanvasPanel, DieWidgetBlueprint);
auto* canvasPanelSlot = MyCanvasPanel->AddChildToCanvas(createdWidget);
canvasPanelSlot->SetSize(FVector2D(100, 100));
This results in the right size! It does currently require recompiling, but that can be solved by placing it within a variable.
Making the Solution Automatic
Lets take that last bit, about setting the widgetSize
to be a variable, and take it a step further.
First, lets subclass the UUserWidget
to provide a widget that stores it own size.
SizeAdjustableUserWidget.h (C++)
#pragma once
#include "CoreMinimal.h
#include "Blueprint/UserWidget.h"
#include "SizeAdjustableUserWidget.generated.h"
class UCanvasPanelSlot;
UCLASS(Abstract)
class USizeAdjustableUserWidget : public UUserWidget
{
GENERATED_BODY()
protected:
UPROPERTY(EditAnywhere)
FVector2D DefaultSize = FVector2D(100, 100);
}
Which should result in this property showing up:
Then lets subclass UCanvasPanel
to automatically set the size of the widget, if it is of the type USizeAdjustableUserWidget
.
SizeAdjustableCanvasPanel.h (C++)
#pragma once
#include "CoreMinimal.h"
#include "Components/CanvasPanel.h"
#include "SizeAdjustableCanvasPanel.generated.h"
UCLASS()
class USizeAdjustableCanvasPanel : public UCanvasPanel
{
GENERATED_BODY()
public:
virtual void OnSlotAdded(UPanelSlot* inSlot) override;
};
SizeAdjustableCanvasPanel.cpp (C++)
#include "SizeAdjustableCanvasPanel.h"
#include "SizeAdjustableUserWidget.h"
#include "Components/CanvasPanelSlot.h"
void USizeAdjustableCanvasPanel::OnSlotAdded(UPanelSlot* inSlot)
{
Super::OnSlotAdded(inSlot)
auto sizeAdjustableWidget = Cast<USizeAdjustableUserWidget>(inSlot->Content);
if (!sizeAdjustableWidget)
return;
auto canvasSlot = Cast<UCanvasPanelSlot>(inSlot);
canvasSlot->SetSize(sizeAdjustableWidget->DefaultSize);
}
And now you can use USizeAdjustableCanvasPanel
and USizeAdjustableUserWidget
to automatically have things sized to how you want it.
Adding some QoL
Now a property is good and all… but it requires the following to get the most out of it:
- Setting the widget to custom size, then setting the size.
- Then finding the property and setting the same size there.
How about we automate that second part? So that we can use to change the size:
Well we can, if we use some logic with the editor.
SizeAdjustableUserWidget.h (C++)
#pragma once
#include "CoreMinimal.h
#include "Blueprint/UserWidget.h"
#include "SizeAdjustableUserWidget.generated.h"
class UCanvasPanelSlot;
UCLASS(Abstract)
class USizeAdjustableUserWidget : public UUserWidget
{
GENERATED_BODY()
protected:
UPROPERTY(EditAnywhere)
FVector2D DefaultSize = FVector2D(100, 100);
#if WITH_EDITOR
public:
virtual bool CanEditChange(const FProperty* inProperty) const override;
protected:
virtual void SynchronizeProperties() override;
#endif
SizeAdjustableUserWidget.cpp (C++)
#include "SizeAdjustableUserWidget.h"
#if WITH_EDITOR
bool USizeAdjustableUserWidget::CanEditChange(const FProperty* inProperty) const
{
if (!Super::CanEditChange(inProperty))
return false;
//Lock the DefaultSize Property, when we don't have a parent so its not going to conflict with the custom size
if (InProperty->GetFName() == GET_MEMBER_NAME_CHECKED(USizeAdjustableUserWidget, DefaultSize))
return !GetParent();
return true;
}
void USizeAdjustableUserWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
//We do not want to auto update if we have a parent
if (GetParent())
return;
//If the mode changes while in the editor, this will not change it back. But will the next time the widget is opened.
DesignSizeMode = EDesignPreviewSizeMode::Custom;
DefaultSize = DesignTimeSize;
//Hack: Sets the default size on the blueprint itself, as it doesn't get set by the above.
auto betterPanel = Cast<USizeAdjustableUserWidget>(GetClass()->GetDefaultObject());
if (betterPanel != nullptr)
betterPanel->DefaultSize = DesignTimeSize;
}
#endif
Now this logic ties the Size of the widget, to the Custom Size set for the blueprint. It will also attempt to set the UI back to Custom sizing when the widget editor is opened.