In the last post, we built a learning environment for the Slate standalone program. In this post, we will learn to write a simple Slate standalone program.

1. Import the necessary modules

There is a question: What is a module? Actually, this is not a simple question, we will discuss it in a post, now, you just need to know it works like a advanced dll.

First of all, we need to import the slate module and slate core module, as well as miscellaneous modules such ass AppFramework, but here, the most worth mentioning is the Standalone module, we do not need to render complex scenes with a lot of 3D models and material, we only need to import a simple renderer for UI drawn by slate framework.

public class SlateLearning : ModuleRules
{
public SlateLearning(ReadOnlyTargetRules Target) : base(Target)
 {
PublicIncludePaths.Add("Runtime/Launch/Public");

PrivateIncludePaths.Add("Runtime/Launch/Private"); // For LaunchEngineLoop.cpp include

PrivateDependencyModuleNames.AddRange(
new string[] {
"AppFramework",
"Core",
"ApplicationCore",
"Projects",
"Slate",
"SlateCore",
"StandaloneRenderer",
 }
 );
 }
}

After import these modules, go to the Target.cs file and set bCompileAgainstApplicationCore to true.

2. Draw Standalone Window

Go to the main function and fill in these content first, and then we will analyze what we have wrote:

INT32_MAIN_INT32_ARGC_TCHAR_ARGV()
{
	GEngineLoop.PreInit(ArgC, ArgV);
	FSlateApplication::InitializeAsStandaloneApplication(GetStandardStandaloneRenderer());
	FSlateApplication::InitHighDPI(true);
	const TSharedPtr<SWindow> MainWindow = SNew(SWindow).ClientSize(FVector2D(800, 600));
	FSlateApplication::Get().AddWindow(MainWindow.ToSharedRef());
	while (!IsEngineExitRequested())
	{
		BeginExitIfRequested();
		FSlateApplication::Get().PumpMessages();
		FSlateApplication::Get().Tick();
	}
	FEngineLoop::AppExit();
	return 0;
}

Now run the program, you will see

First of all, let’s look at this sentence:

const TSharedPtr<SWindow> MainWindow = SNew(SWindow).ClientSize(FVector2D(800,600));

There are a few interesting points, First is the TSharedPtr, Which is a smart pointer that uses reference counting to manage memory. It is not like UObject, the GC method of UObject is mark sweeping. And it is “S” class, not “U” class, so it is not UObject, means it has no CDO, reflection, or any else high-level featured provided by UObject. It is worth mentioning that UObject cannot use TSharedPtr, for these two GC methods are conflict.

Next one is “SNew”, we don’t use “new”  to construct slate object, Just like we use NewObject to create UObject and SpawnActor for AActor. SNew is the method used to construct objects derived from slate, and it returns a TSharedRef object.

Just like TSharedPtr, TSharedRef will increase the reference count of the object, the difference is that the content pointed by TSharedRef cannot be null, so we cannot do like:

TSharedRef<SLeafWidget> Leaf; // Wrong!

So, why here we can use TSharedPtr to run normally? This is because TSharedRef can be converted to TSharedPtr implicitly. But not vice versa. Convert TSharedPtr to TSharedRef using AsShared() method.

In addition to SNew, you can also use SAssignNew() method. The difference with SNew and SAssignNew is that SAssignNew returns TSharedPtr.

TSharedPtr<SWindow> MainWindow;
SAssignNew(MainWindow, SWindow).ClientSize(FVector2D(800, 600));

Next, We add a few buttons to this window to get familiar with the basic operations of Slate, but first, we need to understand the design concept of “Slot”.

3. Understanding Slot

First of all, the “Slot” is designed based on the idea that for a widget(SWidget), it may have child widgets. If it is a UI framework designed by yourself, you can hold them by a TArray<SWidget*>. But UE chose to use Slot – that means, the widget itself doesn’t store widgets, but slot.

Based on this idea, widgets can be divided into three categories: widgets without slots, widgets with at most one slot,  and those with multiple and variable slots.

  • SLeafWidget: As the name suggests, this type of widget is a leaf node and has no slots, so it cannot have child widgets. Like this dude:
  • class SLATE_API STextBlock : public SLeafWidget
  • SCompoundWidget: This type of widget has only one slot, so it can have a child widget, such as SWindow:
  • class SLATECORE_API SWindow : public SCompoundWidget, public FSlateInvalidationRoot
  • SPanel: This type of widget can use a variable number of slots, such as SVerticalBox, and their parent class SBoxPanel inherits from SPanel.

4. Using SCompoundWidget

OK, after the above study, we know that SWindow is a SCompoundWidget, but this is not in line with most people’s habits – in most people’s perspective, a window should have multiple components. So we have to do something with it. In fact, the processing here is flexible. You can directly start to divide the Window into rectangles with SHorizontalBox and SVerticalBox, and add Widgets to it hierarchically. You can also add an Overlay, which is a SPanel, which gives you flexibility directly.

For simplicity, here we use the method of directly adding SHorizontalBox.

const TSharedPtr<SWindow> MainWindow = SNew(SWindow).ClientSize(FVector2D(800, 600))
[
	SNew(SHorizontalBox)
];

Here we meet the first confusing syntax of Slate, which is the square brackets. In fact, this is a syntactic sugar, and the implementation method of “[]” is operator overloading of C++. This method essentially adds content to SLATE_DEFAULT_SLOT. This also illustrates a phenomenon for us: only SCompoundWidget can use square brackets like this. For SPanel, you need to add Slot first and then use square brackets for slot. The reason is simple: the number of Slots of SPanel is variable, and there is no DefaultWidget.

The second one is the .ClientSize after New. You may simply think of it as a chain call function. After future standing of my posts, you will have a deeper understanding of it. But here, you can ignore it for now.

Now, we can add a button:

const TSharedPtr<SWindow> MainWindow = SNew(SWindow).ClientSize(FVector2D(800, 600))
[
	SNew(SHorizontalBox) + SHorizontalBox::Slot()
	[
		SNew(SButton).Text(NSLOCTEXT("L10N", "Key", "Button Content"))
	]
];

OK, here comes the second confusing syntax, which is “+”, which is also a syntactic sugar brought by operator overloading, which is used to add a Slot. Why does the syntax change here? Because SHorizontalBox is a SPanel, it can have multiple Slots, and its Slots need to be added manually.

Now run the program, you can see:

Button is too big, because the default strategy of Slot is HAlign::Fill, VAlign::Fill. We modify it to:

const TSharedPtr<SWindow> MainWindow = SNew(SWindow).ClientSize(FVector2D(800, 600))
[
	SNew(SHorizontalBox) + SHorizontalBox::Slot().HAlign(HAlign_Left).VAlign(VAlign_Top)
	[
		SNew(SButton).Text(NSLOCTEXT("L10N", "Key", "Button Content"))
	]
];

Now run agian:

Much better!

5. Slot Pointer of SPanel

OK, let’s think about a problem next. I want to add five more Buttons through a for-loop. I don’t know what the APi is. It’s not important. The IDE will tell you what APi we should use. The core problem here is——I can’t get it A pointer to this horizontal group control, because it is in a slot of SOverlay.

For a CompoundWidget like SWindow, you may think that this is not a problem. It only has one Slot, and UE will inevitably provide APIs for direct acquisition, but what if I do this?

const TSharedPtr<SWindow> MainWindow = SNew(SWindow).ClientSize(FVector2D(800, 600))
[
	SNew(SOverlay) + SOverlay::Slot().HAlign(HAlign_Left).VAlign(VAlign_Top)
	[
		SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth()
		[
			SNew(SButton).Text(NSLOCTEXT("L10N", "ButtonContent", "Button"))
		]
	]
];

Without a pointer points to that slot, you lose control over it. Now we need the slot pointer. UE doesn’t give you such APi.

Here we will use a special method, Expose(), which is a method of Slot, which can help you get the reference of Slot. Let’s modify the code:

SOverlay::FOverlaySlot* Slot;

const TSharedPtr<SWindow> MainWindow = SNew(SWindow).ClientSize(FVector2D(800, 600))
[
	SNew(SOverlay) + SOverlay::Slot().HAlign(HAlign_Left).VAlign(VAlign_Top).Expose(Slot)
	[
		SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth()
		[
			SNew(SButton).Text(NSLOCTEXT("L10N", "ButtonContent", "Button"))
		]
	]
];

SWidget& SlotWidget = Slot->GetWidget().Get();
SHorizontalBox& HorizontalBox = static_cast<SHorizontalBox&>(SlotWidget);

The Expose() method returns a slot pointer. After that, we can get a reference to a SWidget through API. References can be converted to references of the classes you need at compile time. After getting the pointer, it is easy to write a loop.

for (int i = 0; i < 5; ++i)
{
	HorizontalBox.AddSlot()
	[
		SNew(SButton).Text(NSLOCTEXT("L10N", "ButtonContent", "Button"))
	];
}

Now run the program:

Going one step further, we can add some methods to each Button, through methods like OnClicked_Lambda, which is left as an exercise for the reader.

By JiahaoLi

Hypergryph - Game Programmer 2023 - Now Shandong University - Bachelor 2019-2023

Leave a Reply

Your email address will not be published. Required fields are marked *