3. Slate UI基础:Slate架构、SWidget、SCompoundWidget、布局控件、文本控件
好,咱们今天来聊聊Slate。说实话,很多Unreal开发者一听到Slate就头大,觉得这东西又复杂又绕。我刚开始接触的时候也差不多,看着那一堆SNew、SWidget,心里直犯嘀咕——这跟UMG到底啥关系?
嗯,咱们先把这个问题说清楚。UMG其实就是在Slate上面包了一层可视化编辑器。你拖拽的那些按钮、文本框,底层全是Slate控件。说白了,Slate才是Unreal UI的亲爹,UMG只是它儿子。你要是只会用UMG拖拖拽拽,那遇到复杂UI需求时就会很被动。我当年在做一个编辑器工具时,就因为这个吃了不少苦头。
Slate架构:从底层看UI
Slate的架构,我习惯把它理解成三个层次:
- 底层:渲染层,负责把控件画到屏幕上。这部分你基本不用碰。
- 中间层:控件层,也就是
SWidget体系。这是咱们今天的主角。 - 上层:布局与交互层,处理控件怎么排、怎么响应事件。
你想想看,Slate本质上就是一个控件树。每个控件都是一个SWidget,它们组合在一起,形成一棵树。引擎从根节点开始遍历,逐个绘制。这个思路跟HTML的DOM树很像,但Slate更轻量、更直接。
核心要点:Slate控件树是只读的,你不能像操作UMG那样在运行时动态增删控件。要改变UI结构,你得重建控件树。这一点很多新手会踩坑。
SWidget:一切控件的基类
SWidget是所有Slate控件的基类。它定义了控件的生命周期、绘制流程、事件处理等基础行为。我建议你把它想象成一个「纯虚接口」,虽然它并不是纯虚的。
每个SWidget都有几个关键方法:
| 方法 | 作用 |
|---|---|
ComputeDesiredSize() |
告诉父控件「我想占多大地方」 |
Paint() |
把自己画到屏幕上 |
OnMouseButtonDown() |
处理鼠标点击事件 |
OnKeyDown() |
处理键盘事件 |
你不需要每次都重写这些方法。大多数时候,你只需要组合现有的控件就行。只有当你需要自定义绘制行为时,才需要继承SWidget并重写Paint()。我在项目中遇到过需要绘制自定义图表的情况,那时候才真正深入了Paint()的实现。
小技巧:如果你只是想改改外观,别碰Paint()。用SetRenderTransform()、SetColorAndOpacity()这些现成的方法就够了。重写Paint()意味着你要自己处理裁剪、透明度、抗锯齿,工作量不小。
SCompoundWidget:组合控件的利器
SCompoundWidget是SWidget的一个子类,专门用来组合多个子控件。说白了,它就是给你搭积木用的。你定义一个SCompoundWidget,然后在它的Construct()方法里用SNew把子控件拼起来。
来看个例子:
class SMyCustomPanel : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SMyCustomPanel) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(STextBlock)
.Text(FText::FromString("Hello, Slate!"))
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SButton)
.Text(FText::FromString("Click Me"))
.OnClicked(this, &SMyCustomPanel::OnButtonClicked)
]
];
}
FReply OnButtonClicked()
{
UE_LOG(LogTemp, Log, TEXT("Button clicked!"));
return FReply::Handled();
}
};
看到没?ChildSlot就是你的画布,你在里面用SNew创建控件,用+操作符添加子项。这种声明式语法一开始可能不太习惯,但用久了你会发现它非常清晰——控件的层级结构一目了然。
注意:SLATE_BEGIN_ARGS和SLATE_END_ARGS这两个宏必须成对出现,缺一不可。我曾经因为漏写了SLATE_END_ARGS,编译报错找了半天,最后发现是这种低级错误。嗯,写代码时一定要细心。
布局控件:让UI排布更灵活
布局控件是Slate里最常用的东西。它们不负责显示内容,只负责安排子控件的位置和大小。常用的布局控件有这几个:
- SVerticalBox:垂直排列子控件。每个子控件占一行。
- SHorizontalBox:水平排列子控件。每个子控件占一列。
- SOverlay:子控件叠在一起,像图层一样。适合做浮动提示、遮罩。
- SScrollBox:可滚动的容器。内容超出范围时自动出现滚动条。
- SWrapBox:自动换行的容器。适合做标签、按钮组。
每个布局控件的Slot()方法都有一些参数,用来控制子控件的行为:
| 参数 | 说明 |
|---|---|
AutoHeight() / AutoWidth() |
子控件占自己需要的大小,不拉伸 |
FillHeight() / FillWidth() |
子控件占剩余空间的比例。比如两个控件都设FillHeight(1.0f),它们就各占一半 |
HAlign() / VAlign() |
子控件在Slot内的对齐方式。左对齐、右对齐、居中等等 |
Padding() |
子控件周围的间距。相当于CSS的padding |
我个人习惯把布局控件嵌套使用。比如一个SVerticalBox里放几个SHorizontalBox,每个SHorizontalBox里再放文本和按钮。这种嵌套方式很灵活,能实现几乎任何布局。
避坑指南:我曾经在做一个复杂面板时,把所有控件都塞进一个SVerticalBox里,结果滚动起来卡得要命。后来发现是AutoHeight()用太多,导致每次布局都要重新计算所有子控件的大小。解决办法是尽量用FillHeight(),或者把不需要动态改变大小的部分用SScrollBox包起来。
文本控件:STextBlock与更多选择
文本控件是UI里最基础的元素。Slate里最常用的文本控件是STextBlock。它的用法很简单:
SNew(STextBlock)
.Text(FText::FromString("Hello, World!"))
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 14))
.ColorAndOpacity(FLinearColor::White)
.Justification(ETextJustify::Center)
这里有几个关键点:
Text参数必须用FText,不能用FString。因为FText支持本地化,而FString不支持。这是Unreal的硬性要求。Font参数指定字体。你可以用FCoreStyle::GetDefaultFontStyle()获取默认字体,也可以加载自定义字体。ColorAndOpacity控制文本颜色。支持透明度,比如FLinearColor(1.0f, 0.0f, 0.0f, 0.5f)就是半透明的红色。Justification控制对齐方式。左对齐、居中、右对齐都行。
除了STextBlock,还有几个文本相关的控件:
- SEditableText:可编辑的单行文本。适合输入框。
- SEditableTextBox:带边框和样式的可编辑文本。比
SEditableText更常用。 - SRichTextBlock:支持富文本格式。可以嵌入图片、超链接、不同样式。
我建议你优先用STextBlock做静态文本,用SEditableTextBox做输入框。至于SRichTextBlock,它功能强大但配置复杂,适合做聊天窗口、文档编辑器这类场景。
重要提醒:文本控件的性能问题容易被忽视。如果你在列表里用了大量STextBlock,每个都设置不同的字体或颜色,那绘制开销会很大。解决办法是尽量复用字体和颜色,或者用FText::Format()合并多个文本。
实战:组合一个简单的信息面板
咱们把今天学的知识串起来,写一个简单的信息面板。这个面板包含一个标题、一段描述文本和一个关闭按钮。
class SInfoPanel : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SInfoPanel) {}
SLATE_ARGUMENT(FText, Title)
SLATE_ARGUMENT(FText, Description)
SLATE_EVENT(FOnClicked, OnCloseClicked)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
ChildSlot
[
SNew(SBorder)
.BorderImage(FCoreStyle::Get().GetBrush("GenericWhiteBox"))
.BorderBackgroundColor(FLinearColor(0.1f, 0.1f, 0.1f, 0.9f))
.Padding(10.0f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0, 0, 0, 5)
[
SNew(STextBlock)
.Text(InArgs._Title)
.Font(FCoreStyle::GetDefaultFontStyle("Bold", 16))
.ColorAndOpacity(FLinearColor::White)
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0, 0, 0, 10)
[
SNew(STextBlock)
.Text(InArgs._Description)
.Font(FCoreStyle::GetDefaultFontStyle("Regular", 12))
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f))
.AutoWrapText(true)
]
+ SVerticalBox::Slot()
.AutoHeight()
.HAlign(HAlign_Right)
[
SNew(SButton)
.Text(FText::FromString("关闭"))
.OnClicked(InArgs._OnCloseClicked)
]
]
];
}
};
这个例子展示了:
- 用
SLATE_ARGUMENT定义外部传入的参数 - 用
SLATE_EVENT定义回调事件 - 用
SBorder给面板加背景和边框 - 用
SVerticalBox做垂直布局 - 用
STextBlock显示标题和描述 - 用
SButton做关闭按钮
你可以在任何需要的地方用SNew(SInfoPanel)来创建这个面板,传入标题、描述和关闭回调。是不是很灵活?
个人经验:我习惯在Construct()方法里把控件树写得尽量清晰,每个Slot都加上注释。这样半年后回来看代码,还能一眼看出每个控件是干嘛的。别偷懒,代码是写给人看的。
好了,Slate的基础就讲到这里。下一章咱们会深入事件处理和自定义绘制,到时候你会看到Slate更强大的一面。记住,Slate的学习曲线虽然陡,但一旦掌握了,你就能做出任何你想要的UI——不管是编辑器工具还是游戏内HUD,都不在话下。