UE4 创建自定义 Slate UI 控件
文章目录
- UE4 创建自定义 Slate UI 控件
- 前言
- 一、什么是 Slate
- 二、创建自己的 Slate 控件
- 1.模块引用
- 2.定义 Slate 控件
- 2.1 自定义参数声明
- 2.2 获取自定义参数
- 2.3 传入自定义参数
- 3.创建 Slate 控件
- 3.1 SNew / SAssignNew
- 3.2 从 UWidget 中获取 Slate
- 4.显示 Slate 控件
- 三、示例代码
- 总结
前言
Slate UI 框架是UE4提供的自定义UI编程框架,不论是发布后的程序还是UE4本身的界面渲染,大部分界面都是在Slate 框架基础上实现的,我们在编辑器中经常使用到的UMG,也是在基于Slate封装实现的。
对于初学者而言,UMG已经能够满足绝大部分的需求,但如果我们需要实现一种功能更为复杂或表现更为灵活的界面控件,那我们或许需要学习使用Slate来搭建我们自己的控件。
首先我们来了解一下什么是Slate,以及如何去创建并显示一个简单的Slate UI到我们的程序中去。
一、什么是 Slate
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 控件
1.模块引用
首先,要使用Slate,通常需要在 build.cs 中引用 Slate 和 SlateCore 两个模块,有时还会用到 InputCore 模块。
PublicDependencyModuleNames.AddRange(
new string[]
{
"InputCore"
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"Slate",
"SlateCore"
}
);
2.定义 Slate 控件
我们首先创建一个简单 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);
};
2.1 自定义参数声明
我们可以为我们的 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 函数中的形参,创建时就必须被传入。
2.2 获取自定义参数
我们的类型中还需要有一个 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<int32>,而不再是 int32。
TAttribute<int32> IntAttribute 既可以是值,也可以是委托,通过 Get 函数即可获取结果。
2.3 传入自定义参数
上述的自定义参数是何时传入的呢?
这里我们需要简单介绍一下 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,所以直接用 [ ] 即可
3.创建 Slate 控件
3.1 SNew / SAssignNew
Slate 本身提供了两种方式创建 Slate 控件:SNew、SAssignNew,其声明如下:
/**
* Slate widgets are constructed through SNew and SAssignNew.
* e.g.
*
* TSharedRef<SButton> MyButton = SNew(SButton);
* or
* TSharedPtr<SButton> 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)
];
3.2 从 UWidget 中获取 Slate
UWidget 提供了 TakeWidget 方法,让我们可以直接从中取出 Slate 控件。
意味着我们也可以在 UMG 编辑器中创建控件,然后在 Slate 中使用它们:
ChildSlot
[
AnyUUserWidget->TakeWidget()
];
4.显示 Slate 控件
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 逻辑也能满足我们大部分的需求,除非你想对编辑器做些什么。
三、示例代码
下面就是本文的示例代码啦,没有疑问的可以直接跳过啦~~~
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 的更多内容。