Download

UMG vs. Slate: How to Extend and Expose Widgets for Designers

Mar 9, 2021.Knowledge
When talking about user interface in Unreal Engine, you’ll often see the terms UMG and Slate used, sometimes interchangeably. In reality, the relationship between UMG and Slate is more like the relationship between Blueprint and C++, allowing programmers to create widgets and selectively expose parameters for easy use by designers within the engine. This article will describe what the two systems are, and how they work together to empower designers to build out user interfaces for their projects.

Slate: The Framework
Slate is a UI framework used both for the creation of the Unreal editor itself, and exposed for use in projects that need an interface of their own. The Slate Documentation goes into detail about the design philosophy and tradeoffs that were made in its development, as well as describing the declarative syntax used to create widgets and set parameters:
SNew(SScrollBox)
+SScrollBox::Slot() .Padding(10,5)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot() .HAlign(HAlign_Left)
[

]
+SHorizontalBox::Slot() .HAlign(HAlign_Center)
[

]
+SHorizontalBox::Slot() .HAlign(HAlign_Right)
[

]
]

Slate has a few limitations that we need to keep in mind:
Widgets are only created using the declarative syntax, meaning there’s no way to visualize changes without recompiling
Slate arguments aren’t inherited, so extended classes need to reimplement those arguments
Arguments can be set at construction time, without a simple way of limiting the available arguments for use by designers
UMG (Unreal Motion Graphics) was created to alleviate some of these issues, surfacing the important parts of widget creation for use in a WYSIWYG (What You See Is What You Get) editor that is compatible with blueprints.

UMG: The Wrapper
Instead of replacing Slate with a new UI system for use in the editor, the approach with UMG was to leave all of the logic in Slate and create a matching UMG widget that wraps the Slate widget in a proper, garbage-collected UObject, and add UProperties for each slate argument that should be available for designers to modify. The result is that widgets can be created and visualized at edit time, and Blueprints can be used to create and modify widgets and their properties. Each user-exposed Slate widget (SWidget) has a matching UMG Widget (UWidget) class, and this class is responsible for creating and managing the lifetime of it’s SWidget. UMG also introduced WidgetBlueprints, which work similar to Blueprints but create a UserWidget. UserWidgets represent complex widgets created in the editor by designers, where a UserWidget can contain a hierarchy of any number of other widgets (and can itself be used in other widgets).

Bringing it all together: Extending widget classes for designers
There are a handful of widget classes in which we provide both the SWidget class and the UWidget wrapper, but the UWidget may not always expose everything that a designer needs access to. Extending an SWidget is like extending any other C++ class, but you’ll also need a new UWidget for your extended class to expose the new variant of the widget to the editor. For example, we needed a scroll box that could automatically snap to a certain position when it was no longer being scrolled, but the built-in ScrollBox widget didn’t have any sort of event that it fired when scrolling stopped. To support this, we extended SScrollBox, reimplementing all of the arguments (since they are not inherited) and overriding only the functions that we needed to change. Here’s the header:
#pragma once

#include “CoreMinimal.h”
#include “UObject/NoExportTypes.h”
#include “SScrollBox.h”

class SCarouselScrollBox : public SScrollBox
{
SLATE_BEGIN_ARGS(SCarouselScrollBox)
: _Style(&FCoreStyle::Get().GetWidgetStyle(“ScrollBox”))
, _ScrollBarStyle(&FCoreStyle::Get().GetWidgetStyle(“ScrollBar”))
, _ExternalScrollbar()
, _Orientation(Orient_Vertical)
, _ScrollBarVisibility(EVisibility::Visible)
, _ScrollBarAlwaysVisible(false)
, _ScrollBarDragFocusCause(EFocusCause::Mouse)
, _ScrollBarThickness(FVector2D(9.0f, 9.0f))
, _ScrollBarPadding(2.0f)
, _AllowOverscroll(EAllowOverscroll::Yes)
, _AnimateWheelScrolling(false)
, _WheelScrollMultiplier(1.f)
, _NavigationDestination(EDescendantScrollDestination::IntoView)
, _NavigationScrollPadding(0.0f)
, _OnUserScrolled()
, _OnScrollStopped()
, _ConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible)
{
_Clipping = EWidgetClipping::ClipToBounds;
}

SLATE_SUPPORTS_SLOT(FSlot)

/** Style used to draw this scrollbox */
SLATE_STYLE_ARGUMENT(FScrollBoxStyle, Style)

/** Style used to draw this scrollbox's scrollbar */
SLATE_STYLE_ARGUMENT(FScrollBarStyle, ScrollBarStyle)

/** Custom scroll bar */
SLATE_ARGUMENT(TSharedPtr<SScrollBar>, ExternalScrollbar)

/** The direction that children will be stacked, and also the direction the box will scroll. */
SLATE_ARGUMENT(EOrientation, Orientation)

SLATE_ARGUMENT(EVisibility, ScrollBarVisibility)

SLATE_ARGUMENT(bool, ScrollBarAlwaysVisible)

SLATE_ARGUMENT(EFocusCause, ScrollBarDragFocusCause)

SLATE_ARGUMENT(FVector2D, ScrollBarThickness)

SLATE_ARGUMENT(FMargin, ScrollBarPadding)

SLATE_ARGUMENT(EAllowOverscroll, AllowOverscroll);

SLATE_ARGUMENT(bool, AnimateWheelScrolling);

SLATE_ARGUMENT(float, WheelScrollMultiplier);

SLATE_ARGUMENT(EDescendantScrollDestination, NavigationDestination);

/**
 * The amount of padding to ensure exists between the item being navigated to, at the edge of the
 * scrollbox.  Use this if you want to ensure there's a preview of the next item the user could scroll to.
 */
SLATE_ARGUMENT(float, NavigationScrollPadding);

/** Called when the button is clicked */
SLATE_EVENT(FOnUserScrolled, OnUserScrolled)

SLATE_EVENT(FOnUserScrolled, OnScrollStopped)

SLATE_ARGUMENT(EConsumeMouseWheel, ConsumeMouseWheel);

SLATE_END_ARGS()

void Construct(const FArguments& InArgs);

public:
	// SWidget interface
	virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
	virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override;
	// End of SWidget interface

	void SetOnScrollStopped(FOnUserScrolled InOnScrollStopped);

protected:
/** The delegate to execute when the button is clicked */
FOnUserScrolled OnScrollStopped;

private:
	FVector2D MouseDownPosition;

};
And the implementation:
#include “SCarouselScrollBox.h”
#include “SlateApplication.h”

void SCarouselScrollBox::Construct(const FArguments& InArgs)
{
OnScrollStopped = InArgs._OnScrollStopped;

SScrollBox::Construct(SScrollBox::FArguments()
	.Style(InArgs._Style)
	.ScrollBarStyle(InArgs._ScrollBarStyle)
	.OnUserScrolled(InArgs._OnUserScrolled)
	.Orientation(InArgs._Orientation)
	.ConsumeMouseWheel(InArgs._ConsumeMouseWheel)
	.AllowOverscroll(InArgs._AllowOverscroll)
	.WheelScrollMultiplier(InArgs._WheelScrollMultiplier)
	.NavigationScrollPadding(InArgs._NavigationScrollPadding)
	.NavigationDestination(InArgs._NavigationDestination));

}

FReply SCarouselScrollBox::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
MouseDownPosition = MouseEvent.GetScreenSpacePosition();
return SScrollBox::OnMouseButtonDown(MyGeometry, MouseEvent);
}

FReply SCarouselScrollBox::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
if (!FSlateApplication::Get().HasTraveledFarEnoughToTriggerDrag(MouseEvent, MouseDownPosition))
{
return FReply::Unhandled();
}
OnScrollStopped.ExecuteIfBound(GetScrollOffset());
return SScrollBox::OnMouseButtonUp(MyGeometry, MouseEvent);
}

void SCarouselScrollBox::SetOnScrollStopped(FOnUserScrolled InOnScrollStopped)
{
OnScrollStopped = InOnScrollStopped;
}

We’ve added a new OnScrollStopped delegate, and updated OnMouseButtonUp to fire the delegate if the mouse is released after travelling far enough to scroll the box. Note that we also declared all of the same arguments present in the SScrollBox widget, and then called SScrollBox::Construct and passed all of those through to preserve the original functionality. This is sufficient if we’re creating this new widget in C++, but we also want it to appear in the palette when we create widgets in the editor. To support this, we’ll need to write a new UWidget wrapper, and expose our new delegate as a property:
#pragma once

#include “CoreMinimal.h”
#include “UObject/ObjectMacros.h”
#include “Components/ScrollBox.h”
#include “SCarouselScrollBox.h”
#include “CarouselScrollBox.generated.h”

UCLASS()
class UCarouselScrollBox : public UScrollBox
{
GENERATED_BODY()

public:
/** Called when scrolling stops */
UPROPERTY(BlueprintAssignable, Category = “Button|Event”)
FOnUserScrolledEvent OnScrollStopped;

protected:
//~ Begin UWidget Interface
virtual TSharedRef RebuildWidget() override;
//~ End UWidget Interface

void SlateHandleScrollStopped(float CurrentOffset);

};
And the implementation:
#include “CarouselScrollBox.h”
#include “ScrollBoxSlot.h”

TSharedRef UCarouselScrollBox::RebuildWidget()
{
MyScrollBox = SNew(SCarouselScrollBox)
.Style(&WidgetStyle)
.ScrollBarStyle(&WidgetBarStyle)
.Orientation(Orientation)
.ConsumeMouseWheel(ConsumeMouseWheel)
.NavigationDestination(NavigationDestination)
.NavigationScrollPadding(NavigationScrollPadding)
.AnimateWheelScrolling(bAnimateWheelScrolling)
.WheelScrollMultiplier(WheelScrollMultiplier)
.OnUserScrolled(BIND_UOBJECT_DELEGATE(FOnUserScrolled, SlateHandleUserScrolled))
.OnScrollStopped(BIND_UOBJECT_DELEGATE(FOnUserScrolled, SlateHandleScrollStopped));

for (UPanelSlot* PanelSlot : Slots)
{
	if (UScrollBoxSlot* TypedSlot = Cast<UScrollBoxSlot>(PanelSlot))
	{
		TypedSlot->Parent = this;
		TypedSlot->BuildSlot(MyScrollBox.ToSharedRef());
	}
}

return MyScrollBox.ToSharedRef();

}

void UCarouselScrollBox::SlateHandleScrollStopped(float CurrentOffset)
{
OnScrollStopped.Broadcast(CurrentOffset);
}

You may notice that the UWidget side of things is almost entirely plumbing, which offers the opportunity to expose only the properties that should be available to designers and hide anything else. When working with complex widgets made up of a large number of other widget classes, you can expose properties that you pass down to those other widgets as needed, without having to provide a list of every single argument for every single widget in the hierarchy.

Widget Design for Teams
UMG seeks to eliminate as much ambiguity as possible, providing UI designers with the tools they need to experiment and create interfaces and making it easy for programmers to provide new widget classes as needed. There is a Widget Designer category in the project settings that allows you to customize the canvas resolutions available in the designer and hide specific widgets from the palette, making it easy to provide custom widgets for your project and hiding the default widgets to ensure the correct classes are used. The widgets included in the engine are primarily meant to be used as examples, so you may find the need to take steps like replacing the built-in button widget with your own project-specific button widget class, exposing exactly the parameters you need to tweak while hiding anything that you expect to be standardized across the project. By strategically exposing widgets to UMG that add the functionality needed for your project, you can empower designers to spend less time hacking together widgets and more time iterating on the look and feel of your user interface.