UE4 Slate框架调研


1.前言

在自渲染方案中实现Lua与UE4进行控件绑定,实现根据不同的界面抽象数据自动生成UE4的UI界面。UE4中开发界面主要使用的是UMG和Slate两种方式,因此对UMG和Slate进行了调研,UMG使用可视化拖拽的方式进行开发,是对Slate进行了一层封装,实现了与蓝图的交互。

UMG控件的核心实现在Slate中,因此主要是对Slate进行调研。

2.Slate框架

SlateCore
  • UI layout
  • Fonts, Image, icons
  • Input testing
  • Styling
  • Base widget types[SWidget]
Slate
  • Specialized Slate Widgets — Buttons, labels, value inputs — Slides, drop-downs — Scrolling, views
  • GUI application framework
SlateRHIRendering
  • UI rendering
UMG
  • UMG/UObject widgets[UWidget]
  • Blueprintable widgets[UUserWidget]
  • Property bindings
  • Motion graphics, animation
UMGEditor
  • Editor tools for widget authoring
  • Widget Blueprints

3.Slate开发:

Slate是一个用户界面框架,既可以用于创建虚幻引擎编辑器,也可以用于在项目中创建自定义用户界面。

在Slate中,控件分为三种类型:

SLeafWidget不带子槽的控件,如TextBlock;

SPanel子槽数量为动态的控件,如SHorizontalBox,SVerticalBox;

SCompoundWidget子槽数量固定的控件如SDockTab,Border,Button等。

以上都继承于SWidget。

1)Slate使用C++进行开发,通过阅读源码,其主要实现是通过重载[]操作符和宏定义,比如:

    SNew(SBox).HAlign(HAlign_Fill).VAlign(VAlign_Fill)[
            SNew(STextBlock).Text(FText::FromString("测试1")),
            SNew(STextBlock).Text(FText::FromString("测试2")),
            SNew(STextBlock).Text(FText::FromString("测试3")),
            SNew(SHorizontalBox)
            + SHorizontalBox::Slot()[
                SNew(STextBlock).Text(FText::FromString("测试4"))
            ]
        ]

2)SBox的属性是HAlign是通过SLATE_ARGUMENT宏定义的:

SLATE_ARGUMENT(EHorizontalAlignment, HAlign)

定义:
#define SLATE_ARGUMENT( ArgType, ArgName ) \
        ArgType _##ArgName; \
        WidgetArgsType& ArgName( ArgType InArg ) \
        { \
            _##ArgName = InArg; \
            return this->Me(); \
        }

UI事件也是用类似的实现方式:

// SButton
SLATE_EVENT( FOnClicked, OnClicked )
SLATE_EVENT( FSimpleDelegate, OnPressed )
SLATE_EVENT( FSimpleDelegate, OnHovered )

3)Slate中的基类是SWidget,可以通过实现OnPaint方法用Canvas自绘控件,比如SButton的绘制:

int32 SButton::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
    bool bEnabled = ShouldBeEnabled(bParentEnabled);
    bool bShowDisabledEffect = GetShowDisabledEffect();

    const FSlateBrush* BrushResource = !bShowDisabledEffect && !bEnabled ? DisabledImage : GetBorder();

    ESlateDrawEffect DrawEffects = bShowDisabledEffect && !bEnabled ? ESlateDrawEffect::DisabledEffect : ESlateDrawEffect::None;

    if (BrushResource && BrushResource->DrawAs != ESlateBrushDrawType::NoDrawType)
    {
        FSlateDrawElement::MakeBox(
            OutDrawElements,
            LayerId,
            AllottedGeometry.ToPaintGeometry(),
            BrushResource,
            DrawEffects,
            BrushResource->GetTint(InWidgetStyle) * InWidgetStyle.GetColorAndOpacityTint() * BorderBackgroundColor.Get().GetColor(InWidgetStyle)
        );
    }

    return SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bEnabled);
}

4)SListView简单示例:

// 创建数据、进行绑定
TArray<TSharedPtr<FItemData>> Items;
    Items.Empty();
    Items.Add(MakeShareable(new FItemData{1, "Test1", 1}));
    Items.Add(MakeShareable(new FItemData{2, "Test2", 2}));
    Items.Add(MakeShareable(new FItemData{3, "Test3", 3}));
    TSharedPtr<SListView<TSharedPtr<FItemData>>> MyListView = SNew(SListView<TSharedPtr<FItemData>>).
        ListItemsSource(&Items)
        .OnGenerateRow(this, &SMyCompoundWidget::OnGenerateWidgetForList);
    MainWidget->AddSlot()[
        SNew(SBox).WidthOverride(100)
        [
            SNew(SListView<TSharedPtr<FItemData>>)
            .ListItemsSource(&Items)
            .OnGenerateRow(this, &SMyCompoundWidget::OnGenerateWidgetForList)
            .OnSelectionChanged(this, &SMyCompoundWidget::OnComponentSelected)
        ]
    ];

// 单元格
TSharedRef<ITableRow> SMyCompoundWidget::OnGenerateWidgetForList(TSharedPtr<FItemData> Item,
                                                                 const TSharedRef<STableViewBase>& OwnerTable)
{
    return
        SNew(STableRow< TSharedPtr<FString> >, OwnerTable)
        [
            SNew(STextBlock).ColorAndOpacity(FLinearColor::Blue).Text(FText::FromString(*Item.Get()->Name)).
                             ShadowColorAndOpacity(FLinearColor::Gray)
        ];
}

6)使用Widget Reflector来查看UMG的布局:

直接用Slate开发会比较费劲,先用UMG快速拖拽出来,然后用工具查看层级来写Slate,后面熟悉了就可以直接用Slate进行开发。

image-20220418194346396

4.Slate绘制

img

第一块是控件绘制,在主线程中,给每个控件分配LayerId,并从控件抽象出FSlateDrawElement。

第二块是绘制指令生成渲染指令,也是在主线程中,把FSlateDrawElement包装成FSlateRenderBatch,并根据控件的信息生成VertexBuffer。

第三块是合批并执行渲染,在渲染线程,把之前生成的FSlateRenderBatch按照LayerId从小到大排序,并尝试合批。最后把UI渲染到BackBuffer。

5.Slate使用体验:

1.Slate只能使用声明式语法进行创建控件和参数的设置,在复杂布局下,嵌套层级较多;
2.Slate参数不是继承的,扩展类需要重新实现这些参数;
3.参数可以在构造时设置,提供修改的属性相比Android或iOS要少很多;
4.界面修改需要重新编译或启动项目;

从整体的调研来看,Slate能够实现大部分常见的控制绘制,但写起来有一定的难度,如果常见的控件不满足需求,可以自定义控件。操作符重复嵌套也会增加后期的维护成本,如果将控件进行重写,使用起来会更加方便,但也会增加前期的开发成本。

1)了解更多
https://zhuanlan.zhihu.com/p/38462130
https://zhuanlan.zhihu.com/p/45682313
https://blog.csdn.net/ywjun0919/article/details/86690585
https://zhuanlan.zhihu.com/p/346275251
https://blog.csdn.net/u011718663/article/details/117713057
2)自定义widget示例:
void SMyWidget::Construct(const FArguments& InArgs)
{
    int32 BoxID = 0;
    CheckBoxes.SetNum(2);
    SUserWidget::Construct(
        SUserWidget::FArguments().HAlign(HAlign_Fill).VAlign(VAlign_Fill)
        [
            SNew(SGridPanel)
            .FillColumn(0, 1).FillColumn(1, 3)
            .FillRow(0, 1).FillRow(1, 1).FillRow(2, 1).FillRow(3, 1)
            + SGridPanel::Slot(0, 0).HAlign(HAlign_Center).VAlign(VAlign_Center)
            [
                SNew(STextBlock).Text(FText::FromString(TEXT("百分比")))
            ]
            + SGridPanel::Slot(0, 1).HAlign(HAlign_Center).VAlign(VAlign_Center)
            [
                SNew(STextBlock).Text(FText::FromString(TEXT("二选一")))
            ]
            + SGridPanel::Slot(0, 2).HAlign(HAlign_Center).VAlign(VAlign_Center)
            [
                SNew(STextBlock).Text(FText::FromString(TEXT("下拉选项")))
            ]
            + SGridPanel::Slot(1, 0).HAlign(HAlign_Fill).VAlign(VAlign_Center)
            [
                SNew(SHorizontalBox)
                + SHorizontalBox::Slot().FillWidth(3)
                [
                    SAssignNew(Slider, SSlider)
                ]
                + SHorizontalBox::Slot().FillWidth(1)
                [
                    SNew(STextBlock).Text_Lambda([this]()
                    {
                        return FText::FromString(FString::SanitizeFloat(Slider.IsValid() ? Slider->GetValue() : 0.f));
                    })
                ]
            ]
            + SGridPanel::Slot(1, 1).HAlign(HAlign_Fill).VAlign(VAlign_Fill)
            [
                SNew(SHorizontalBox)
                + SHorizontalBox::Slot().FillWidth(1)[
                    SAssignNew(CheckBoxes[0], SCheckBox)
                    .IsChecked(ECheckBoxState::Checked)
                    .OnCheckStateChanged(FOnCheckStateChanged::CreateLambda([this](ECheckBoxState NewState)
                    {
                        switch (NewState)
                        {
                        case ECheckBoxState::Checked:
                            for (int i = 0; i < CheckBoxes.Num(); ++i)
                            {
                                if (i != 0 && CheckBoxes[i].IsValid())
                                {
                                    CheckBoxes[i]->SetIsChecked(ECheckBoxState::Unchecked);
                                }
                            }
                            break;
                        case ECheckBoxState::Unchecked:
                            CheckBoxes[0]->SetIsChecked(ECheckBoxState::Checked);
                            break;
                        }
                    }))[
                        SNew(STextBlock).Text(FText::FromString(TEXT("选项一")))]
                ]
                + SHorizontalBox::Slot().FillWidth(1)
                [
                    SAssignNew(CheckBoxes[1], SCheckBox)
                ]
                + SHorizontalBox::Slot().FillWidth(1)
                [
                    SAssignNew(CheckBoxes[1], SCheckBox)
                    .OnCheckStateChanged(FOnCheckStateChanged::CreateLambda([this](ECheckBoxState NewState)
                    {
                        switch (NewState)
                        {
                        case ECheckBoxState::Unchecked:
                            CheckBoxes[1]->SetIsChecked(ECheckBoxState::Checked);
                            break;
                        case ECheckBoxState::Checked:
                            for (int i = 0; i < CheckBoxes.Num(); ++i)
                            {
                                if (i != 1 && CheckBoxes[i].IsValid())
                                {
                                    CheckBoxes[i]->SetIsChecked(ECheckBoxState::Unchecked);
                                }
                            }
                            break;
                        }
                    }))[
                        SNew(STextBlock).Text(FText::FromString(TEXT("选项二")))]
                ]
            ]
            + SGridPanel::Slot(1, 2).HAlign(HAlign_Fill).VAlign(VAlign_Center)
            [
                SNew(SComboBox<TSharedPtr<FString>>)
                .OnGenerateWidget_Lambda(
                                                        [](TSharedPtr<FString> Item)
                                                        {
                                                            return SNew(STextBlock).Text(FText::FromString(*Item));
                                                        }
                                                    ).OnSelectionChanged_Lambda(
                                                        [this](TSharedPtr<FString> Item, ESelectInfo::Type Type)
                                                        {
                                                            if (ComboText.IsValid())
                                                            {
                                                                ComboText->SetText(*Item);
                                                            }
                                                        }).OptionsSource(new TArray<TSharedPtr<FString>>({
                                                        MakeShareable(new FString(TEXT("0"))),
                                                        MakeShareable(new FString(TEXT("1"))),
                                                        MakeShareable(new FString(TEXT("2")))
                                                    }))[
                    SAssignNew(ComboText, STextBlock)
                ]
            ]
            + SGridPanel::Slot(1, 3).HAlign(HAlign_Center).VAlign(VAlign_Center)[
                SNew(SButton)[
                    SNew(STextBlock).Text(FText::FromString(TEXT("确定")))
                ]
            ]
        ]);
}

最终的效果:

image-20220418193729750

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注