This is a casual guide for those who know Qt and C++ and are looking to get started writing custom Editor Plugins for UnrealEngine.

Quick Disclaimer: I am not an Unreal developer and am still quite new to this myself, but given the lack of Slate training resources out there perhaps this guide will be helpful to others facing the same issues I have.

If you haven’t already, please read the Official Docs as it provides a lot of key concepts that I won’t be covering here. I wish Epic would dedicate a smidgen of time to improving their Slate UI docs as there is basically only API docs at this stage.

This guide is written for Unreal Engine 4.27, but there is a note about moving to Unreal Engine 5 at the end.

Useful Resources:

Check out these links for some slate resources:

Official Docs
Reference Docs
Slate Leaderboard tutorial
K2 Nodes
Slate in Python
My Custom Script Editor

Please share other resources in the comments and I can add to this list.

Getting set up

Before you can make a tool, you need to create an editor plugin, if you are familiar with this then you can skip this section.
I highly recommend compiling UnrealEngine from source if possible as this will give you access to all the source you will no doubt be spending a lot of time in.

Regarding which editor to use, I recommend vscode, it’s not as stable as visual studio and has trouble following the symbols, but visual studio tries to “auto-indent” and fails miserably every time it works with slate. At least that has been my personal experience.

Widget Reflector

Other than the source code, your most useful reference is going to be the Widget Reflector, this allows you to find what widget was used to create any component in Unreal.
This is found under “Window -> Developer Tools -> Widget Reflector”

Click on “Pick Hit-Testable Widgets”, hover over the widget you want to inspect and press the Escape key, you can now click on the source link to pull up the file used to create that component.

Create your plugin

In the Plugins Window (Edit -> Plugins), Click on New Plugin.
Here you will create a new “Editor Standalone Window” plugin, you can also start from a blank one but this is the easiest entry point.

First thing you need to do once the plugin is built is refresh your code:
“File -> Refresh Visual Studio Code”
This is so that vscode can find the symbols it needs for auto-completion.

Updating your plugin

Any time you add new files to your project, you need to re-run:
“File -> Refresh Visual Studio Code”

When you are ready to preview your changes you need to recompile it, this is done with the Modules panel:
“Window -> Developer Tools -> Modules”

Simply click Recompile and view the output log for any errors as normal.
Sometimes with Slate you may need to unload, reload and load to get it to refresh depending if the memory wasn’t cleared out properly.

Chances are it will crash and force you to relaunch unreal anyway.

Where to put your code

Unreal will sort out all the boilerplate logic for you, it even tells you exactly where to make the changes!

TSharedRef<SDockTab> FMyWindowModule::OnSpawnPluginTab(const FSpawnTabArgs& SpawnTabArgs)
{
	FText WidgetText = FText::Format(
		LOCTEXT("WindowWidgetText", "Add code to {0} in {1} to override this window's contents"),
		FText::FromString(TEXT("FMyWindowModule::OnSpawnPluginTab")),
		FText::FromString(TEXT("MyWindow.cpp"))
		);

	return SNew(SDockTab)
		.TabRole(ETabRole::NomadTab)
		[
			// Put your tab content here!
			SNew(SBox)
			.HAlign(HAlign_Center)
			.VAlign(VAlign_Center)
			[
				SNew(STextBlock)
				.Text(WidgetText)
			]
		];
}

Shove your custom widget or any UI code you want under the SDockTab, keep reading to see what this strange code is and how to make sense of it.

Slate concepts

Fortunately Slate is a Retained-Mode UI framework similar to Qt and much of the structure is similar so it won’t take long to get started.

Naming

Not Slate related but Unreal uses UpperCamelCase for everything, helps to stick with that for consistency.
In Unreal you will see these naming conventions:

SMyWidget: All Widgets prefixed with an S (For Slate?)
FMyClass: All non widget classes prefixed with an F
TMyTemplate<>: All template classes/methods prefixed with a T
IMyInterface: All interfaces prefixed with an I
OnMyEvent: All slate event callbacks (signals) are prefixed with On

Structure

The structure in Slate is a little different than Qt, but once you get used to it it’s not too bad, let’s take a look at this example:

void SMyClass::Construct(const FArguments& InArgs)
{
	ChildSlot
    [
        SNew(SBox)
        .VAlign(VAlign_Fill)
        .HAlign(HAlign_Fill)
        [
			SNew(SHorizontalBox)
			+ SHorizontalBox::Slot()
			[
                SNew(STextBlock)
			    .Text("Label:")
            ]
			+ SHorizontalBox::Slot()
			[
                SNew(SEditableTextBox)
            ]
		]
    ];
}

First thing to notice is the widget is not built in the class constructor like in Qt, it is built in the Construct method, while this isn’t always the case it is most of the time.
The actual widget is built inline under “ChildSlot”, Everything between the square braces will be assigned as children to this and as you can see above they can be stacked.

Some widgets will have multiple “Slots”, this is usually layout controllers. A “Slot” in Slate can be thought of as a child entry.

Creating new objects

There are two primary ways to create new objects in Slate, SNew and SAssignNew, while you can get away with using new you will also need to manage the memory yourself in that case.

SNew: Create a new object of type passed in the first parameter, you may see additional arguments passed here but it is preferred to pass arguments as slate arguments.
SAssignNew: Like SNew, but the object type is the second parameter and the first parameter is the TSharedRef<> to assign it to.

TSharedRef and TSharedPtr

Unreal has it’s own smart pointers that you will have to use, they are covered in depth here
To convert from a TSharedRef to a TSharedPtr:

TSharedPtr<FMyObjectType> MySharedPointer = MySharedReference;

To convert from a TSharedPtr to a TSharedRef:

TSharedRef<FMyObjectType> MySharedReference = MySharedPointer.ToSharedRef();

To Create a new TSharedRef or TSharedPtr:

TSharedRef<FMyObjectType> NewReference(new FMyObjectType());
TSharedPtr<FMyObjectType> NewPointer(new FMyObjectType());

You can also use TWeakRef<>::Pin() to turn a weak ref or pointer to a shared ref or pointer.

Layouts

Layouts in Slate are similar to Qt, you add your widgets either to a layout or just arbitrarily on top of each other. Slate provides a number of layout types you can use.

Slate ElementQt EquivalentDescription
SBoxQWidgetStretchable layout with one child
SVerticalBoxQVBoxLayoutVertical Layout
SHorizontalBoxQHBoxLayoutHorizontal Layout
SOverlayQWidgetStacks children on top of each other, like QStack but with positioning.
SPanelN/APanel where the children specify their arrangement
SGridPanelQGridLayoutArrange children in a grid pattern (specified)
SSplitterQSplitterArrange children with adjustable splitter
SScrollPanelQScrollAreaan SPanel with ScrollBars

There are far more options than this, best to explore the docs and engine code to find what you need.

Signals/Slots

Signals in slate are called events, there are also no specific slots, just functions.
To Connect a Slate Event to your function it is passed as an argument:

// Connect OnTextChanged to an internal method
SNew(SEditableText).OnTextChanged(this, &SMyClass::TextChanged)

Sometimes this will error due to incompatibility, don’t fret, just use “OnEventName_Raw” instead, every event has a “raw” method, this works the same but you can’t use replies.

You can also use “OnEventName_Lambda” to connect straight to a c++ lambda instead of a function, this works the same as the Raw connection where there is no replies.

FReply

Somewhat different to Qt, Slate allows event callbacks to reply to the function sending the event, really this just returns FReply::Handled on success or FReply::Unhandled on failure, this is useful when you need to update the UI on failure or just check if the function actually did anything with the data (For example OnKeyPress needs to know if it needs to perform the default action or not after the custom override)

FReply SMyWidget::DoTheThing()
{
    FTargets Targets = FindTargets();
    if (!Targets.IsValid)
        return FReply::Unhandled();
    
    // Do something with Targets
    return FReply::Handled();
}

Custom objects

Unless you are extended a specific widget or making a new primitive, you will want to be inheriting from SCompoundWidget, This is intended by Unreal to be the base for any non-primitive widget.

There are two main parts you require in your custom widget, slate args and a construct method. Here is an example widget with no arguments:

// SMyWindow.h
#pragma once

#include "CoreMinimal.h"
#include "SlateFwd.h"
#include "Widgets/SCompoundWidget.h"

class SMyWindow : public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SMyWindow) {}
	SLATE_END_ARGS()
	void Construct( const FArguments& InArgs );
};



// SMyWindow.cpp
void SMyWindow::Construct(const FArguments& InArgs )
{
    ChildSlot
    [
        SNew(SBox)
        .VAlign(VAlign_Top)
        .HAlign(HAlign_Center)
        [
            SNew(STextBlock)
            .ColorAndOpacity(FLinearColor::Red)
            .Font(FSlateFontInfo("Ariel", 24))
            .Text(LOCTEXT("Hello", "Hello World"))
        ]
    ];
}

It’s just a red text in the top center of the window with no controls.

Arguments

There are two ways of passing arguments in slate, the first is as parameters to Construct.

class SMyWindow : public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SMyWindow) {}
	SLATE_END_ARGS()
	void Construct( const FArguments& InArgs, bool firstArg, bool secondArg );
};
// To use:
ChildSlot[
    SNew(SMyWindow, true, false)
];

The second more common way is by defining slate arguments These are defined by type in the Slate args section.
These are also split into two ways:
SLATE_ARGUMENT: Defines an argument and it’s type
SLATE_ATTRIBUTE: Same as above but stores as TAttribute<type>, this is required for property bindings

The SLATE_BEGIN_ARGS macro generates a struct where every argument is stored as _Attribute.
Because it is a struct you can also put default values in here too.
Generally speaking you will use SLATE_ATTRIBUTE for primitive types (text, int, bool, etc) as this allows them to be bound and SLATE_ARGUMENT for everything else.

// SMyWindow.h
#pragma once

#include "CoreMinimal.h"
#include "SlateFwd.h"
#include "Widgets/SCompoundWidget.h"

class SMyWindow : public SCompoundWidget
{
public:
	SLATE_BEGIN_ARGS(SMyWindow)
    : _Text()
    , _FontSize(24) {}
    SLATE_ATTRIBUTE(FText, Text)
    SLATE_ARGUMENT(int, FontSize)
	SLATE_END_ARGS()
	void Construct( const FArguments& InArgs );
};



// SMyWindow.cpp
void SMyWindow::Construct(const FArguments& InArgs )
{
    int Text = InArgs._Text;
    int FontSize = InArgs._FontSize;
    ChildSlot
    [
        SNew(SBox)
        .VAlign(VAlign_Top)
        .HAlign(HAlign_Center)
        [
            SNew(STextBlock)
            .ColorAndOpacity(FLinearColor::Red)
            .Font(FSlateFontInfo("Ariel", FontSize))
            .Text(Text)
        ]
    ];
}

Events

Slate events are the equivalent of signals in Qt, I covered how to connect them above, now let’s create our own.
A Slate event is an instance of an FDelegate, while many delegates already exist and can be used, personally I prefer to only use FSimpleDelegate or define my own. Let’s take a look:

// SMyWindow.h
#pragma once

#include "CoreMinimal.h"
#include "SlateFwd.h"
#include "Widgets/SCompoundWidget.h"

class SMyWindow : public SCompoundWidget
{
public:
	DECLARE_DELEGATE_OneParam(FMyWindowDelegate, bool /*value*/);

	SLATE_BEGIN_ARGS(SMyWindow) {}
    SLATE_EVENT(FSimpleDelegate, OnButtonClicked)
    SLATE_EVENT(FMyWindowDelegate, OnMyCustomEvent)
	SLATE_END_ARGS()
	void Construct( const FArguments& InArgs );

private:
    FReply ButtonClicked();

	FSimpleDelegate OnButtonClicked;
	FMyWindowDelegate OnMyCustomEvent;
};



// SMyWindow.cpp
void SMyWindow::Construct(const FArguments& InArgs )
{
    // Assign the bound methods
	OnButtonClicked = Args._OnButtonClicked;
	OnMyCustomEvent = Args._OnMyCustomEvent;
    ChildSlot
    [
        SNew(SBox)
        .VAlign(VAlign_Top)
        .HAlign(HAlign_Center)
        [
            SNew(SButton)
            .Text(LOCTEXT("text", "Click Me!"))
            .OnClicked(this, &SMyWindow::ButtonClicked)
        ]
    ];
}
FReply SMyWindow::ButtonClicked()
{
    UE_LOG(LogTemp, Display, TEXT("They clicked the button"));
    // Check if event is connected
    if ( OnButtonClicked.IsBound() ) {
        // We don't care about reply here but you can customize it
        FReply Reply = OnButtonClicked.Execute();
    }
    // Can also do event with arguments
    if ( OnMyCustomEvent.IsBound() ) {
        OnMyCustomEvent.Execute(true);
    }
    return FReply::Handled();
}

So here we declared a custom delegate with bool as an argument (You can use TwoParams, ThreeParams, etc for more arguments)
This is then added to the args struct as a SLATE_EVENT.
We also then add these as property variables so we can grab the input from InArgs and store it. We use the same name here but it’s not mandatory.

To emit the event, first check if it is bound, if you don’t it will likely crash.

Finally you can take the FReply and return it or modify it. (Note if using OnClicked_raw you will not have access to the reply object)

Styling

Styling in Unreal is really just a static class with style properties that are referred to in your widgets.
When creating your plugin it will create an FMyPluginNameStyle class, here you just call Style->Set(“StyleName”, value) to set the property, then you use it with FMyPluginNameStyle::Get()->GetBrush(“StyleName”).

Example from my ScriptEditor plugin:

TSharedRef< FSlateStyleSet > FScriptEditorStyle::Create()
{
	TSharedRef< FSlateStyleSet > Style = MakeShareable(new FSlateStyleSet("ScriptEditorStyle"));
	Style->SetContentRoot(IPluginManager::Get().FindPlugin("ScriptEditor")->GetBaseDir() / TEXT("Resources"));

	Style->Set("ScriptEditor.OpenPluginWindow", new IMAGE_BRUSH(TEXT("ButtonIcon_40x"), Icon40x40));
	Style->Set("ScriptEditor.Execute", new IMAGE_BRUSH(TEXT("UI/Execute_40x"), Icon40x40));
	Style->Set("ScriptEditor.Save", new IMAGE_BRUSH(TEXT("UI/Save_40x"), Icon40x40));
	Style->Set("ScriptEditor.Open", new IMAGE_BRUSH(TEXT("UI/FolderOpen"), Icon40x40));

	const FSlateFontInfo Consolas10  = DEFAULT_FONT("Mono", 9);
	const FTextBlockStyle NormalText = FTextBlockStyle()
		.SetFont(Consolas10)
		.SetColorAndOpacity(FLinearColor::White)
		.SetShadowOffset(FVector2D::ZeroVector)
		.SetShadowColorAndOpacity(FLinearColor::Black)
		.SetHighlightColor(FLinearColor(0.02f, 0.3f, 0.0f))
		.SetHighlightShape(BOX_BRUSH("UI/TextBlockHighlightShape", FMargin(3.f / 8.f)))
		;

	// Text editor
	{
		Style->Set("TextEditor.NormalText", NormalText);

		Style->Set("SyntaxHighlight.PY.Normal", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xfff2f2f2))));// light grey
		Style->Set("SyntaxHighlight.PY.Operator", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xff9cb4d9)))); // blue
		Style->Set("SyntaxHighlight.PY.Keyword", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xffb35757)))); // red
		Style->Set("SyntaxHighlight.PY.String", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xffdbc47d)))); // yellow
		Style->Set("SyntaxHighlight.PY.Number", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xff8d7ddb)))); // purple
		Style->Set("SyntaxHighlight.PY.Comment", FTextBlockStyle(NormalText).SetColorAndOpacity(FLinearColor(FColor(0xff2d6b1a)))); // green

		Style->Set("TextEditor.Border", new BOX_BRUSH("UI/TextEditorBorder", FMargin(4.0f/16.0f), FLinearColor(0.02f,0.02f,0.02f,1)));

		Style->Set("TextEditor.Background", new FSlateColorBrush(FLinearColor(FColor(33, 33, 33))));
		Style->Set("Menu.Background", new FSlateColorBrush(FLinearColor(FColor(33, 33, 33))));
	}
	return Style;
}

And it’s usage:

.VAlign(VAlign_Fill)
[
	SNew(SBorder)
	.BorderImage(FScriptEditorStyle::Get().GetBrush("TextEditor.Background"))
]

Of course some primitives allow passing styles to them as an argument but this is not consistent so your milage may vary.

Context Menus

Context menus are available on certain slate components as the event “OnContextMenuOpening”
Unlike Qt however, instead of returning a QMenu, this returns an SWidget. This allows you to essentially have any widget as your context menu.
Also unlike Qt, your command callbacks are registered outside the menu building.

To create a menu, first you must register your commands, these will generally go in the FMyPluginCommands::RegisterCommands() method created for you when your plugin was created.
Here is an example from my anim toolkit:

void FAnimToolkitCommands::RegisterCommands()
{
	UI_COMMAND(PluginAction, "AnimToolkit", "Execute AnimToolkit action", EUserInterfaceActionType::Button, FInputGesture());
	UI_COMMAND(KeySelected, "KeySelected", "Key Selected", EUserInterfaceActionType::Button, FInputGesture());
	UI_COMMAND(KeyAll, "KeyAll", "Key All", EUserInterfaceActionType::Button, FInputGesture());
}

In your widgets construct, map these commands to the appropriate callback, this behaves similar to events, you may also use lambdas as required.

void SChannelBox::Construct(const FArguments& Args)
{
	const FGenericCommands& GenericCommands = FGenericCommands::Get();
	const FAnimToolkitCommands& Commands = FAnimToolkitCommands::Get();
	CommandList = MakeShareable(new FUICommandList);
	CommandList->MapAction(
		Commands.KeySelected,
		FExecuteAction::CreateSP(this, &SChannelBox::KeySelectedCB));
	CommandList->MapAction(
		Commands.KeyAll,
		FExecuteAction::CreateSP(this, &SChannelBox::KeyAllCB));
	CommandList->MapAction(
		GenericCommands.Copy,
		FExecuteAction::CreateSP(this, &SChannelBox::CopyCB));
	CommandList->MapAction(
		GenericCommands.Paste,
		FExecuteAction::CreateSP(this, &SChannelBox::PasteCB));
	...

Connect the event to your generate callback:

.OnContextMenuOpening(this, &SChannelBox::GenerateContextMenu)

And have the callback create the widget

TSharedPtr<SWidget> SChannelBox::GenerateContextMenu()
{
	const FGenericCommands& GenericCommands = FGenericCommands::Get();
	const FAnimToolkitCommands& Commands = FAnimToolkitCommands::Get();
	FMenuBuilder MenuBuilder(true, CommandList);
	FSlateIcon DummyIcon(NAME_None, NAME_None);

    MenuBuilder.AddMenuEntry(Commands.KeySelected, NAME_None, TAttribute<FText>(), TAttribute<FText>(), DummyIcon);
    MenuBuilder.AddMenuEntry(Commands.KeyAll, NAME_None, TAttribute<FText>(), TAttribute<FText>(), DummyIcon);
    MenuBuilder.AddMenuEntry(GenericCommands.Copy, NAME_None, TAttribute<FText>(), TAttribute<FText>(), DummyIcon);
    MenuBuilder.AddMenuEntry(GenericCommands.Paste, NAME_None, TAttribute<FText>(), TAttribute<FText>(), DummyIcon);

	return MenuBuilder.MakeWidget();
}

See my ChannelBox post for the context of this context menu.

Slate widget types

I can’t realistically show every type, so instead I have made a map between the Slate widget type, Qt widget type, documentation link and a link to the unreal source where it is used. This should at least provide you a starting point.
Note to view the example you will need git permissions to unreal, follow this link for more info.

Qt TypeSlate TypeExample
QLabelSTextBlock Link
QPushButtonSButton Link
QCheckBoxSCheckBox Link
QRadioButtonSCheckBox Link
QLineEditSEditableText or SEditableTextBox Link
QPlainTextEditSMultiLineEditableText Link
QComboBoxSComboBox, STextComboBox Link
QProgressBarSProgressBar Link
QSliderSSlider Link
QListViewSSlistView Link
QTreeViewSTreeView Link
QTableViewSTableViewBase and STableRow Link
QSplitterSSplitter Link
ImageSBorder with BorderImage attribute Link
FlowLayoutSWrapBox Link
QVBoxLayoutSVerticalBox Link
QHBoxLayoutSHorizontalBox Link
QGridLayoutSGridPanel Link

I have intentionally not included QTabWidget here as tabs in Slate are infuriating and take many steps, they need their own dedicated post.

Unreal 4 vs Unreal 5

Slate widgets written in Unreal 4 will still work in Unreal 5, there are however some new features. I haven’t had a chance to dive deep into these changes, there is also no Slate changelog I could fine which doesn’t help.

The two main changes are Styling and Attribute Validations. I will add style guides later as I learn more about it, but here are the attribute validation changes:

Attribute validations

Unreal 5 adds callbacks and bindings to slate attributes, this is done with a combination of macros, setup logic and a new TSlateAttribute (No doc link, behaves like TAttribute),
Slight disclaimer: I updated to Windows 11 while writing this post and can now no longer run UE5 to test the following code.

Take this example component, Text is stored in a TSlateAttribute instead of TAttribute or simply FText.
Also note the new SLATE_DECLARE_WIDGET, this is required to get the PrivateRegisterAttributes method

class SMyComponent : public SCompoundWidget
{
	SLATE_DECLARE_WIDGET(SMyComponent, SCompoundWidget)
public:
	SLATE_BEGIN_ARGS(SMyComponent)
		: _Text()
    {}
    SLATE_ATTRIBUTE( FText, Text )
	SLATE_END_ARGS()

    SMyComponent();
    ~SMyComponent();

	void Construct(const FArguments& Args);

	const FText& GetText() const;
	void SetText(TAttribute<FText> InText);
private:
	TSlateAttribute<FText> BoundText;
    bool bIsAttributeBoundTextBound;
};

In the source there is a new PrivateRegisterAttributes method, this allows further attribute definition and callbacks.
Callbacks may include layout or paint updates or even lambdas to be run on value change. This has some QML binding tones to it. While this isn’t the most intuitive attribute binding setup, I can see why it was done this way.

When getting or setting the attribute, it must be done via the bound attribute in order to properly propogate the values.


SLATE_IMPLEMENT_WIDGET(SMyComponent)
void SMyComponent::PrivateRegisterAttributes(FSlateAttributeInitializer& AttributeInitializer)
{
	SLATE_ADD_MEMBER_ATTRIBUTE_DEFINITION(AttributeInitializer, BoundText, EInvalidateWidgetReason::Layout);
}

SMyComponent::STextBlock()
	: BoundText(*this),
      bIsAttributeBoundTextBound(true)
{
}

SMyComponent::~SMyComponent()
{
	// Needed to avoid "deletion of pointer to incomplete type 'FSlateTextBlockLayout'; no destructor called" error when using TUniquePtr
}
const FText& SMyComponent::GetText() const
{
    if (bIsAttributeBoundTextBound) {
        SMyComponent& MutableSelf = const_cast<SMyComponent&>(*this);
        MutableSelf.BoundText.UpdateNow(MutableSelf);
    }
    return BoundText.Get();
}
void SMyComponent::SetText(TAttribute<FText> InText)
{
	// Cache the IsBound.
	//When the attribute is not bound, we need to go through all the other bound property to check if it is bound.
	bIsAttributeBoundTextBound = InText.IsBound();
	BoundText.Assign(*this, MoveTemp(InText));
}

This appears to be how styling is propogated through tools in UE5 which allows for fluid design style changes, though is not something I have experimented with myself.

Conclusion

Hopefully this was helpful, as I said at the start I am still new to this myself so if there is a mistake here please do correct me.
Due to my day job ramping up I haven’t had much time to work on any cool projects but if you have suggestions for more guides leave a comment and I will see what I can do.

Regards,
Mr MinimalEffort