Slate UI 框架是UE4提供的自定义UI编程框架,不论是发布后的程序还是UE4本身的界面渲染,大部分界面都是在Slate 框架基础上实现的,我们在编辑器中经常使用到的UMG,也是在基于Slate封装实现的。
对于初学者而言,UMG已经能够满足绝大部分的需求,但如果我们需要实现一种功能更为复杂或表现更为灵活的界面控件,那我们或许需要学习使用Slate来搭建我们自己的控件。
首先我们来了解一下什么是Slate,以及如何去创建并显示一个简单的Slate UI到我们的程序中去。
Slate 是虚幻引擎的自定义 UI 编程框架,编辑器的大部分界面都是使用 Slate 构建的。
UE4关于Slate的官方文档:Slate UI框架
举一个简单的例子,我们在UMG中使用 Image 控件时,可以点击右上方的 Open Image 查看对应的C++代码 UImage,在其中我们能够找到这段函数:
TSharedRef<SWidget> UImage::RebuildWidget()
{
MyImage = SNew(SImage)
.FlipForRightToLeftFlowDirection(bFlipForRightToLeftFlowDirection);
return MyImage.ToSharedRef();
}
也就是说,UImage 内部还有一个 SImage 对象,我们在编辑器中搭建的 UI 界面,其实就是在使用已经封装好的 Slate 进行组合,进而搭建出漂亮的界面。
Slate 无法直观的预测我们搭建出的 UI 样式,这也是 UE 额外使用 UMG 去封装它的原因,但是我们可以用 Slate 写一个小控件,并用 UWidget 封装它,就可以在UMG编辑器中直观的像使用其他控件一样使用它们。
Slate 也提供了多种类型,包括 SPanel、SCompoundWidget、SLeafWidget 等等,我们可以根据自己的需要继承对应的类型,各种类型都可以在 UE4 中找到对应的控件进行参考学习。
首先,要使用Slate,通常需要在 build.cs 中引用 Slate 和 SlateCore 两个模块,有时还会用到 InputCore 模块。
PublicDependencyModuleNames.AddRange(
new string[]
{
"InputCore"
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Slate",
"SlateCore"
}
);
我们首先创建一个简单 Slate 类型,也可以直接在 UE4 编辑器中创建对应的C++类型:
class SMyCompoundWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SMyCompoundWidget)
{}
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
};
我们可以为我们的 Slate 控件声明一些自定义的参数,这些参数会在创建 Slate 对象时由创建它的对象提供,比较常用的几种声明方法如下:
SLATE_BEGIN_ARGS(SMyCompoundWidget)
: _Content()
, _IntAttribute (0)
, _BoolValue(false)
, _OnCustomEvent()
{}
// 声明一个默认 Slot 类型的参数,可以传入其他控件,这在 SCompoundWidget 派生类中很常见
SLATE_DEFAULT_SLOT( FArguments, Content )
// 声明一个 int32 类型的参数
// 既可以直接传入一个 int32 值,也可以传入一个返回值为float类型的方法
SLATE_ATTRIBUTE( int32, IntAttribute )
// 声明一个 bool 类型的参数
// 只能作为值传入
SLATE_ARGUMENT( bool, BoolValue )
// 声明一个事件参数,用来传入一个方法
SLATE_EVENT(FSimpleDelegate, OnCustomEvent)
SLATE_END_ARGS()
// 需要有一个 FArguments 形参,上述通过宏声明的参数会通过该参数传入
// 后面可以加一些其他形参
void SMyCompoundWidget::Construct( const FArguments& InArgs, float InFloat );
其他更多参数声明宏可以参考UE4源码中的 /Engine/Source/Runtime/SlateCore/Public/Widgets/DeclarativeSyntaxSupport.h
通过宏声明的自定义参数是可以拥有默认值的,即创建该控件的用户可以选择性的填写参数;
而如果有必需由创建者传入的参数,可以声明为 Construct 函数中的形参,创建时就必须被传入。
我们的类型中还需要有一个 Construct 函数,通常在这个函数中实现对参数的处理,以及内部其他控件的构建等等。
事实上这个函数除了一个 InArgs 参数外,我们还可以在后面加上一些其他的参数,举个例子:
void SMyCompoundWidget::Construct( const FArguments& InArgs, float InFloat )
{
// 我们可以在类型中声明成员变量,将传入的参数存起来
// 在宏中定义的参数,Slate 会自动加 _ 前缀作为 InArgs 的成员
IntAttribute = InArgs._IntAttribute;
BoolValue = InArgs._BoolValue;
OnCustomEvent = InArgs._OnCustomEvent;
FloatValue = InFloat;
// 在 SCompoundWidget 类型中声明了一个 FSimpleSlot 类型的 ChildSlot,可供我们在其下挂载其他 Slate 控件
ChildSlot
[
// 这里的挂载的 Slate 控件既可以是外部传入的,也可以是生成的
// 这里暂时以传入的 Content 为例
InArgs._Content.Widget
];
}
需要注意的是,因为我们在声明 IntAttribute 参数时使用的是 SLATE_ATTRIBUTE,所以参数类型其实是 TAttribute
TAttribute
上述的自定义参数是何时传入的呢?
这里我们需要简单介绍一下 Slate 控件对象的创建,通常有两种方式创建一个 Slate 对象(SNew 和 SAssignNew),这里以 SNew 为例介绍参数传入:
TSharedRef<SMyCompoundWidget> MyWidget = SNew(SMyCompoundWidget, 1.0f) // 这里的 1.0f 对应的是前面Construct函数中的第二个参数 InFloat
.IntAttribute(this, &SAnotherWidget::GetIntValue)
.BoolValue(true)
.OnCustomEvent(this, &SAnotherWidget::CustomFunction)
//.Content() // 因为使用的是 SLATE_DEFAULT_SLOT 声明,所以即使省略了这一行,[] 中的内容依然会默认当作该 Slot 参数
[
SNew(SButton)
];
SNew 函数从第二个参数开始,对应我们在 Construct 函数中额外声明的形参,必须传入一个对应的实参;
其后的参数是通过宏声明的参数,是可选的,如果希望使用默认值可以直接省略。
如果希望绑定 UObject 对象的方法,在绑定委托时需要加 _UObject 后缀,否则默认是使用的 CreateSP 方法创建代理,会导致报错(如:.IntAttribute_UObject(...)
和 .OnCustomEvent_UObject(...)
),当然还有其他不同的后缀用于绑定各种不同类型的方法。
通常我们会省略 .Content(),因为我们将其声明成了默认 Slot,所以直接用 [ ] 即可
Slate 本身提供了两种方式创建 Slate 控件:SNew、SAssignNew,其声明如下:
/**
* Slate widgets are constructed through SNew and SAssignNew.
* e.g.
*
* TSharedRef MyButton = SNew(SButton);
* or
* TSharedPtr MyButton;
* SAssignNew( MyButton, SButton );
*
* Using SNew and SAssignNew ensures that widgets are populated
*/
#define SNew( WidgetType, ... ) \
MakeTDecl<WidgetType>( #WidgetType, __FILE__, __LINE__, RequiredArgs::MakeRequiredArgs(__VA_ARGS__) ) <<= TYPENAME_OUTSIDE_TEMPLATE WidgetType::FArguments()
#define SAssignNew( ExposeAs, WidgetType, ... ) \
MakeTDecl<WidgetType>( #WidgetType, __FILE__, __LINE__, RequiredArgs::MakeRequiredArgs(__VA_ARGS__) ) . Expose( ExposeAs ) <<= TYPENAME_OUTSIDE_TEMPLATE WidgetType::FArguments()
SAssignNew 多了一个 ExposeAs 的参数,可以传入一个智能指针,在创建控件时可以直接为指针赋值。也就是说,下面两种创建方式其实是一样的
TSharedRef<SButton> Button = SNew(SButton);
ChildSlot
[
Button
];
TSharedPtr<SButton> Button;
ChildSlot
[
SAssignNew(Button, SButton)
];
如果你不关心这个控件的指针,可以直接使用 SNew 创建
ChildSlot
[
SNew(SButton)
];
UWidget 提供了 TakeWidget 方法,让我们可以直接从中取出 Slate 控件。
意味着我们也可以在 UMG 编辑器中创建控件,然后在 Slate 中使用它们:
ChildSlot
[
AnyUUserWidget->TakeWidget()
];
Slate 控件作为 UMG 控件的底层,当然也是可以直接添加到视口中的,虽然很少会直接这么做
// Slate 类型没有继承 UObject,所以需要使用智能指针来控制 GC
TSharedRef<SMyCompoundWidget> MyWidget = SNew(SMyCompountWidget, 1.0f);
// 添加到视口
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->AddViewportWidgetContent(MyWidget);
}
// 从视口移除
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->RemoveViewportWidgetContent(MyWidget);
}
更进一步,我们其实不一定要添加到 Viewport 上,我们也可以把它添加到某个额外的窗口或者某个标签页下。
实际上 Viewport 内部也有一个 SViewport 控件,我们甚至可以获取到并挪开它,给我们的 Slate 控件让路,是的,Slate 的功能很强大,但也非常复杂,好在简单的 Slate 逻辑也能满足我们大部分的需求,除非你想对编辑器做些什么。
更常见的情况是,我们会将 Slate 封装成一个 UWidget 控件,让我们能够在 UMG 可视化的编辑界面去使用它,具体我会放在下一篇文章(封装一个 Slate 控件到 UMG)里,介绍和演示如何封装一个简单的 UWidget 控件。
下面就是本文的示例代码啦,没有疑问的可以直接跳过啦~~~
SMyCompoundWidget.h
/**
* Slate 控件示例
*/
class DOWEHAVEAPLAN_API SMyCompoundWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SMyCompoundWidget)
: _Content()
, _IntAttribute(0)
, _BoolValue(false)
, _OnCustomEvent()
{}
// 声明一个默认 Slot 类型的参数,可以传入其他控件,这在 SCompoundWidget 派生类中很常见
SLATE_DEFAULT_SLOT(FArguments, Content)
// 声明一个 int32 类型的参数
// 既可以直接传入一个 int32 值,也可以传入一个返回值为float类型的方法
SLATE_ATTRIBUTE(int32, IntAttribute)
// 声明一个 bool 类型的参数
// 只能作为值传入
SLATE_ARGUMENT(bool, BoolValue)
// 声明一个事件参数,用来传入一个方法
SLATE_EVENT(FSimpleDelegate, OnCustomEvent)
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs, float InFloat);
private:
/** Int32 attribute */
TAttribute<int32> IntAttribute;
/** Bool value */
bool BoolValue;
/** Float value */
float FloatValue;
/** Delegate */
FSimpleDelegate OnCustomEvent;
};
SMyCompoundWidget.cpp
void SMyCompoundWidget::Construct(const FArguments& InArgs, float InFloat)
{
// 我们可以在类型中声明成员变量,将传入的参数存起来
// 在宏中定义的参数,Slate 会自动加 _ 前缀作为 InArgs 的成员
IntAttribute = InArgs._IntAttribute;
BoolValue = InArgs._BoolValue;
OnCustomEvent = InArgs._OnCustomEvent;
FloatValue = InFloat;
// 在 SCompoundWidget 类型中声明了一个 FSimpleSlot 类型的 ChildSlot,可供我们在其下挂载其他 Slate 控件
ChildSlot
[
// 这里的挂载的 Slate 控件既可以是外部传入的,也可以是生成的
// 这里暂时以传入的 Content 为例
InArgs._Content.Widget
];
}
以上就是我今天分享的内容,简单介绍了我对 Slate 的一些简单认识,供大家参考,希望对大家有所帮助,后续我会继续完善 Slate 的更多内容。