背景

在开发上层UMG过程中,有时候会遇到需要更底层的支持,以此为契机,研究Slate使用与底层注意事项。

基本使用

Slate是UE中提供的UI框架,它的核心理念如下:

关于UI的一些基础概念,可以了解:

在UE中,使用Slate,需要了解三个核心结构:

  • FSlateApplication :全局单例,所有UI的调度中心。
  • SWindow :顶层窗口,持有跨平台窗口的实例(FGenericWindow),提供窗口相关的配置和操作。
  • SWidget :小部件,划分窗口区域,处理自身区域内的交互和绘制事件。

在UE中,一个简单的Slate使用示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
auto Window = SNew(SWindow)                         //创建窗口
.ClientSize(FVector2D(600, 600)) //设置窗口大小
[ //填充窗口内容
SNew(SHorizontalBox) //创建水平盒子
+ SHorizontalBox::Slot() //添加子控件插槽
[
SNew(STextBlock) //创建文本框
.Text(FText::FromString("Hello")) //设置文本框内容
]
+ SHorizontalBox::Slot() //添加子控件插槽
[
SNew(STextBlock) //创建文本框
.Text_Lambda([](){ //设置文本框内容
return FText::FromString("Slate");
})
]
];
FSlateApplication::Get().AddWindow(Window, true); //注册该窗口,并立即显示

Slate的代码风格如下:

  • Slate 控件的类命名一般以 S开头
  • 可以通过以下函数来快速构建Slate控件:
    • SNew( WidgetType, ... ):通用的构造方式
    • SAssignNew( ExposeAs, WidgetType, ... ):构造完成后,把控件赋值给ExposeAs
    • SArgumentNew( InArgs, WidgetType, ... ):使用参数集进行构造
  • 在构造的代码表达式中,可以通过一些函数来设置控件的构造参数,由于这些函数都会返回控件自身的引用,因此可以进行链式调用
    • 可以通过operatpr . 调用函数来设置控件的属性和事件
  • 对于 SPanel 的子类,可以使用operator +SPanelType::Slot添加子控件的插槽
    • 对于 SCompoundWidgetSPanel::Slot ,可以使用operator []来填充子控件
  • 对于Slate的属性和事件,可以通过多种方式绑定,比如上面的.Text(...)设置的是静态值,还可以通过绑定函数来动态获取属性值:
      • Text_Lambda(...)
    • Text_Raw(...)
      • Text_Static(...)
    • Text_UObject(...)
  • Slate可以同时作为 Game UI 和 Editor UI

  • 在Editor中,可以通过如下方式来添加UI:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    auto Window = SNew(SWindow)                         //必须具有一个顶层窗口
    .Title(FText::FromString("CustomWindow")) //设置窗口标题
    .ClientSize(FVector2D(600, 600)) //设置窗口大小
    [ //填充窗口内容
    SNew(SSpacer) //自身控件,这里是一个空白填充
    ];

    //方法1:注册该窗口,并立即显示
    FSlateApplication::Get().AddWindow(Window, true);

    //方法2:注册该窗口,不显示,手动调用ShowWindow来显示
    FSlateApplication::Get().AddWindow(Window, false); //注册该窗口,并立即显示
    Window->ShowWindow();

    //----------通过DockTab的方式来添加窗口: -------------
    // 注册Tab页面的生成器
    FGlobalTabmanager::Get()->RegisterTabSpawner(FName("CustomTab"),FOnSpawnTab::CreateLambda([](const FSpawnTabArgs& Args){
    return SNew(SDockTab)
    [
    SNew(SSpacer)
    ];
    }));
    FGlobalTabmanager::Get()->TryInvokeTab(FTabId("CustomTab")); //尝试激活Tab页面

在Game中,可以通过以下方式来添加UI:

Using Slate In-Game in Unreal Engine | Unreal Engine 5.2 Documentation | Epic Developer Community

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

GEngine->GameViewport->AddViewportWidgetContent(
SNew(SSpacer)
);

//--------------------------GameViewport还有其他接口可用:

class UGameViewportClient : public UScriptViewportClient, public FExec
{
virtual void AddViewportWidgetContent( TSharedRef<class SWidget> ViewportContent, const int32 ZOrder = 0 );
virtual void RemoveViewportWidgetContent( TSharedRef<class SWidget> ViewportContent );
virtual void AddViewportWidgetForPlayer(ULocalPlayer* Player, TSharedRef<SWidget> ViewportContent, const int32 ZOrder);
virtual void RemoveViewportWidgetForPlayer(ULocalPlayer* Player, TSharedRef<SWidget> ViewportContent);
void RemoveAllViewportWidgets();
void RebuildCursors();
};

Swidget SWindow

  • Swidget 结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
TAttribute<bool> EnabledState,                          //开启状态,指定是否能够与控件交互,如果禁用,则显示为灰色
TAttribute<EVisibility> Visibility, //可见性策略
TSharedPtr<IToolTip> ToolTip, //提示框控件
TAttribute<FText> ToolTipText, //提示框文本内容
TAttribute<TOptional<EMouseCursor::Type> > Cursor, //鼠标样式
float RenderOpacity, //渲染透明度
TAttribute<TOptional<FSlateRenderTransform>> Transform, //渲染变换
TAttribute<FVector2D> TransformPivot, //渲染变换中心
FName Tag, //标签
bool ForceVolatile, //强制UI失效
EWidgetClipping Clipping, //裁剪策略
EFlowDirectionPreference FlowPreference, //UI流向
TOptional<FAccessibleWidgetData> AccessibleData, //存储数据
TArray<TSharedRef<ISlateMetaData>> MetaData //元数据
  • Swindow结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
EWindowType Type;                               //窗口类型
FWindowStyle Style; //窗口样式
FText Title; //窗口标题
float InitialOpacity; //初始透明度

FVector2D ScreenPosition //窗口坐标
FVector2D ClientSize; //窗口客户区域尺寸
TOptional<float> MinWidth; //最小宽度
TOptional<float> MinHeight; //最小高度
TOptional<float> MaxWidth; //最大宽度
TOptional<float> MaxHeight; //最大高度
FMargin LayoutBorder; //窗口内容的边距
FMargin UserResizeBorder; //调整窗口区域时的响应距离

EAutoCenter AutoCenter; //居中策略
ESizingRule SizingRule; //窗口尺寸处理策略
FWindowTransparency SupportsTransparency; //窗口透明度策略
EWindowActivationPolicy ActivationPolicy; //激活处理策略

bool IsInitiallyMaximized; //初始时显示为最大化
bool IsInitiallyMinimized; //初始时显示为最小化
bool IsPopupWindow; //是否是 Pop up 窗口(无任务栏图标)
bool IsTopmostWindow; //是否是置顶窗口
bool FocusWhenFirstShown; //首次预览时获得焦点
bool AdjustInitialSizeAndPositionForDPIScale; //根据DPI调整窗口初始坐标和尺寸
bool UseOSWindowBorder; //使用操作系统自身的窗口边框
bool HasCloseButton; //是否带有关闭按钮
bool SupportsMaximize; //是否支持最大化
bool SupportsMinimize; //是否支持最小化
bool ShouldPreserveAspectRatio; //是否锁定宽高比
bool CreateTitleBar; //是否创建标题栏
bool SaneWindowPlacement; //是否将窗口约束到屏幕内
bool bDragAnywhere; //是否可拖拽到任意位置
bool bManualManageDPI; //是否手动调整DPI

Swidget基本开发

基本类

img

Slate开发的绝大部分工作内容可以归纳为:

  • 设置控件属性
  • 绑定事件逻辑
  • 组织层次结构

SWidget 是一个抽象基类,UE根据不同的使用方式,又对其进行派生,划分为四大类:

  • SCompoundWidget :可以设置ChildSlot (SBoder/SButton/…)

    Slate 用来提供给开发者的主要扩展方式,一般继承它是为了利用已有的SWidget来组织一系列的Widget。

  • SPanel :可以看做是SWidget的容器,可以包含一个或多个ChildSlot,用于添加SWidget。 (Soverlay/SBoxPanel.SHorizontalBox/SBoxPanel.SVerticalBox/…)

    Slate已经派生了足够多的SPanel,用于组织Slate的布局等各类操作,一般情况下很少对其派生。

  • SLeafWidget :叶子控件,不包含ChildSlot (SImage/STest/…)

    派生它,主要是为了自定义一些拥有独特 渲染和尺寸处理机制 的控件

  • SWeakWidget :定义逻辑上的归属而非事件上的。

    SPanel中的SWidget在事件上具有层级关系,但有时会出现一些特殊情况,比如点击按钮打开一个菜单,菜单可以看做是隶属于这个按钮的,但本质上菜单是新开启了一个窗口,它们在事件传递上并不存在层级关系,所以就得靠SWeakWidget来解决这个问题。一般情况下很少使用它。

大多时候,我们会新增一个C++类,继承自 SCompoundWidget ,在Construct函数中填充子控件,就像是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class SCustomWidget: public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SCustomWidget) {} //定义Slate参数
SLATE_END_ARGS()
public:
void Construct(const FArguments& InArgs){ //使用SNew本质上是调用该函数
ChildSlot //填充子控件
[
SNew(STextBlock)
.Text(FText::FromString("This is body"))
];
}
};

这样我们就可以使用如下代码来创建该控件:

1
auto MyWidget = SNew(SCustomWidget);

如果想增加 参数(Argument) ,比如说让文本内容可以设置,那么可以把定义改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SCustomWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SCustomWidget)
: _Text(FText::FromString("Default")) { //初始化参数默认值,变量名为参数定义时的变量名前加下划线
}
SLATE_ARGUMENT(FText,Text) //使用宏SLATE_ARGUMENT定义参数
SLATE_END_ARGS()
public:
void Construct(const FArguments& InArgs) {
Text = InArgs._Text; //接收传递进来的参数
ChildSlot
[
SNew(STextBlock)
.Text(Text) //传递文本内容
];
}
private:
FText Text;
};

这样就可以使用如下代码设置控件内容:

1
2
auto MyWidget = SNew(SCustomWidget)
.Text(FText::FromString("Hello"));

当然,我们也可以不走FArguments的方式,直接这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SCustomWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SCustomWidget){}
SLATE_END_ARGS()
public:
void Construct(const FArguments& InArgs, FText InText) { //直接从Construct的函数参数中传入
Text = InText;
ChildSlot
[
SNew(STextBlock)
.Text(Text)
];
}
private:
FText Text;
};
auto MyWidget = SNew(SCustomWidget,FText::FromString("Hello")); //从SNew的参数列表中传入Text

如果想让上述参数能够绑定委托,就需要使用Slate的 属性(Attribute) 机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SCustomWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SCustomWidget)
: _Text(FText::FromString("Default")) {
}
SLATE_ATTRIBUTE(FText,Text) //使用宏SLATE_ATTRIBUTE定义参数
SLATE_END_ARGS()
public:
void Construct(const FArguments& InArgs) {
Text = InArgs._Text; //接收传递进来的参数
ChildSlot
[
SNew(STextBlock)
.Text(Text) //传递文本属性
];
}
private:
TAttribute<FText> Text; //使用TAttribute包裹属性
};

然后就能使用这样的代码:

1
2
3
4
auto MyWidget = SNew(SCustomWidget)
.Text_Lambda([](){
return FText::FromString("Hello");
});

如果想增加一些控件的事件处理回调,比如说当上面文本变动时的做一些处理,那么可以用 Slate 的 事件(Event) 机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class SCustomWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SCustomWidget)
: _Text(FText::FromString("Default")) {
}
SLATE_ATTRIBUTE(FText,Text)
SLATE_EVENT(FSimpleDelegate, OnTextChanged) //使用宏SLATE_EVENT声明事件
SLATE_END_ARGS()
public:
void Construct(const FArguments& InArgs) {
Text = InArgs._Text;
OnTextChanged = InArgs._OnTextChanged; //传递事件委托
ChildSlot
[
SNew(STextBlock)
.Text(Text)
];
}
void SetText(FText InText) {
Text = InText;
OnTextChanged.ExecuteIfBound(); //执行委托
}
FText GetText(){
return Text.Get();
}
private:
FSimpleDelegate OnTextChanged; //定义委托
TAttribute<FText> Text;
};
auto MyWidget = SNew(SCustomWidget)
.Text_Lambda([](){
return FText::FromString("Hello");
})
.OnTextChanged_Lambda([](){ //当文字变动时打印日志
UE_LOG(LogTemp,Warning,TEXT("Oh, Text is changed!"));
});

此外,SLATE_BEGIN_ARGS(...)SLATE_END_ARGS()之间还有其他可供使用的宏,使用频率不高,因此这里不展开描述,具体可以查看:

  • Engine\Source\Runtime\SlateCore\Public\Widgets\DeclarativeSyntaxSupport.h

综上,Slate的开发流程基本如此。

控件一览

对于UI开发,需要了解以下的基础概念(UMG UGUI 都有此结构):

  • 窗口的基本状态 :激活(Active),焦点(Focus),可见(Visible),模态(Modal),变换(Transform)
  • 布局策略及相关概念
    • 盒式布局(HBox,VBox),流式布局(Flow),网格布局(Grid),锚式布局(Anchors),重叠布局(Overlap),画布(Canvas)
    • 内边距(Padding),外边距(Margin),间距(Spacing),对齐方式(Alignment)
  • 样式管理 :图标(Icon),风格(Style),画刷(Brush)
  • 字体管理 :字体类型(Font Family),文本宽度测量(Font Measure)
  • 尺寸计算 :控件尺寸计算策略
  • 交互事件 :鼠标,键盘,拖拽,焦点事件,事件处理机制,鼠标捕获
  • 绘制事件 :绘制元素,区域裁剪
  • 基本控件 :标签(Label),按钮(Button),复选框(Check),组合框(Combo),滑动条(Slider),滚动条(Scroll Bar),文本框(Text),对话框(Dialog),颜色选取(Color),菜单栏(Menu Bar),菜单(Menu),状态栏(Status Bar),滚动面板(Scroll ),堆栈(切换)面板(Stack/Switcher),列表面板(List),树形面板(Tree)…
  • 国际化 :文本本地化翻译(Localization)

1 - Slate 开发 - Modern Graphics Engine Guide其中控件一览描述,此篇省略。

UMG2Slate

UMG怎么包装Slate?

每一个UWidget里面都对应一个Slate控件。

当你在蓝图中添加一个和Button,UE会:

1.创建一个UButton(UObject)

2.在底层生成对应的SButton(Slate)

3.通过TakeWidget()将Slate控件挂载到Viewport.

如何访问 UMG 控件的底层 Slate?

(1)在C++中获取Slate控件

1
2
3
4
5
6
UButton* MyUMGButton = ...; // 获取 UMG 按钮
TSharedPtr<SWidget> SlateWidget = MyUMGButton->TakeWidget(); // 提取 Slate
if (SButton* SlateButton = StaticCastSharedPtr<SButton>(SlateWidget).Get())
{
SlateButton->SetColorAndOpacity(FSlateColor(FLinearColor::Red)); // 直接操作 Slate
}

(2)在蓝图中控制

UMG暴露了部分Slate的属性,但是无法直接访问Slate对象;

为什么要有 UMG?

Slate 虽然灵活,但存在以下问题:

  • 不适合游戏开发:需要手动管理内存(TSharedPtr),没有蓝图支持。
  • 跨平台困难:移动端输入处理复杂。
  • 迭代效率低:修改 UI 需重新编译 C++。

UMG 通过以下方式优化:

  • 蓝图驱动:设计师可独立开发 UI。
  • 自动垃圾回收:基于 UObject 系统。
  • 跨平台抽象:统一处理触摸/手柄输入。

UMG嵌入到Slate

重点是将UMG嵌入到Slate里面,容易出现内存泄漏的问题。

UMG是继承自UObject的UE自动垃圾回收的机制,但是则Slate是使用智能指针去管理内存,要分析一些智能指针是否会影响UE的垃圾回收,从而导致出现了垃圾回收不掉的内存泄漏问题。

参考

1 - Slate 开发 - Modern Graphics Engine Guide

【UE·底层篇】Slate源码分析——点击事件的触发流程梳理 - 知乎

UE5 Slate(1):Slate语法分析 - 知乎

[UMG和Slate 源码分析和混合使用 | LTQ的BLOG](https://liutianqi0123.github.io/2025/08/12/UE UMG和Slate 源码分析和混合使用/)

Unreal Engine 5 开发 — Slate基础 - 知乎