Introduction

What is the best way to set the size of a Blueprint UUserWidget when dynamically adding it to a UCanvasPanel?

Correctly Sized Dice

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:

An image showing wrongly sized dice images.

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:

Editor Size Property

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:

  1. Setting the widget to custom size, then setting the size.
  2. 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:

Editor Custom 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.