UE Slate

概述

呜哇哈哈哈哈哈哈哈哈哈哈哈哈哈哈
整理知识体系
源码版本5.3

SWidget,SCompoundWidget,Spanel,SLeafWidget,SUserWidget

我们写的所有slate类最终都继承自 swidget

  • SWidget

所有的Slate都继承自 SlateCore模块下

class SWidget
    : public FSlateControlledConstruction,
    public TSharedFromThis<SWidget>       // Enables 'this->AsShared()'

FSlateControlledConstruction 是一个Slate控制构造的类,重载了new 和 delete

private:

    /** Widgets should only ever be constructed via SNew or SAssignNew */
    void* operator new ( const size_t InSize )
    {
        return FMemory::Malloc(InSize);
    }

    /** Widgets should only ever be constructed via SNew or SAssignNew */
    void* operator new ( const size_t InSize, void* Addr )
    {
        return Addr;
    }
public:
    void operator delete(void* mem)
    {
        FMemory::Free(mem);
    }
};

还能看到一句话 Widgets should only ever be constructed via SNew or SAssignNew

从SWidget 这个类往下翻,能找到 Construct 和 SWidgetConstruct。并且让你千万别直接调。

    /** Construct a SWidget based on initial parameters. */
     UE_DEPRECATED(4.27, "SWidget::Construct should not be called directly. Use SNew or SAssignNew to create a SWidget")
    SLATECORE_API void Construct(
        const TAttribute<FText>& InToolTipText,
        const TSharedPtr<IToolTip>& InToolTip,
        const TAttribute< TOptional<EMouseCursor::Type> >& InCursor,
        const TAttribute<bool>& InEnabledState,
        const TAttribute<EVisibility>& InVisibility,
        const float InRenderOpacity,
        const TAttribute<TOptional<FSlateRenderTransform>>& InTransform,
        const TAttribute<FVector2D>& InTransformPivot,
        const FName& InTag,
        const bool InForceVolatile,
        const EWidgetClipping InClipping,
        const EFlowDirectionPreference InFlowPreference,
        const TOptional<FAccessibleWidgetData>& InAccessibleData,
        const TArray<TSharedRef<ISlateMetaData>>& InMetaData);

这里面大部分参数,直接打开一个UMG,在detail面板搜索,都能大概知道是干嘛的。
继续往下翻,是一些按键响应。这些方法在UMG,蓝图重现Funtion的时候都能直接重载出来,大概能知道什么意思
那么SWidget 他定义了最底层的UI控件,包括一些信息和按键响应。

SWidget 有三个子类 SCompoundWidget, Spanel, SLeafWidget 单Slot, 多 Slot,无Slot。

  • SCompoundWidget

    /**
    * A CompoundWidget is the base from which most non-primitive widgets should be built.
    * CompoundWidgets have a protected member named ChildSlot.
    */
    class SCompoundWidget : public SWidget
  • SPanel

    /**
    * A Panel arranges its child widgets on the screen.
    *
    * Each child widget should be stored in a Slot. The Slot describes how the individual child should be arranged with
    * respect to its parent (i.e. the Panel) and its peers Widgets (i.e. the Panel's other children.)
    * For a simple example see StackPanel.
    */
    class SPanel
    : public SWidget
  • SLeafWidget

    /**
    * Implements a leaf widget.
    *
    * A LeafWidget is a Widget that has no slots for children.
    * LeafWidgets are usually intended as building blocks for aggregate widgets.
    */
    class SLeafWidget
    : public SWidget

控件的绘制位于 OnPlant 函数
比如
class STextBlock : public SLeafWidget
就可以看 STextBlock 的 OnPlant 是怎么写的。

  • SUserWidget

    class SUserWidget : public SCompoundWidget
    {
    public:
    struct FArguments : public TSlateBaseNamedArgs<SUserWidget>
    {
        typedef FArguments WidgetArgsType;
        FORCENOINLINE FArguments()
            : _Content()
            , _HAlign(HAlign_Fill)
            , _VAlign(VAlign_Fill)
        {}
    
        SLATE_DEFAULT_SLOT( FArguments, Content )
        SLATE_ARGUMENT( EHorizontalAlignment, HAlign )
        SLATE_ARGUMENT( EVerticalAlignment, VAlign )
    
    };
    
    protected:
    void Construct( const FArguments& InArgs )
    {
        this->ChildSlot
        .HAlign( InArgs._HAlign )
        .VAlign( InArgs._VAlign )
        [
            InArgs._Content.Widget
        ];
    }
    };

    提供了一些基础的布局

从 Slate Event 开始

我们做了这样的一个Slate

是一个 SExpandableArea
然后三个按钮

    • 添加一行
    • 去掉最后一个
    • 删除全部

控制在 SExpandableArea 里面增删元素。

这个Slate要这样写

class SMainSlate : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SMainSlate)
    {}
    SLATE_END_ARGS()

    void Construct(const FArguments& InArgs);

    // ...
}

他的构造大概是这样。

void SMainSlate::Construct(const FArguments& InArgs)
{
    ChildSlot
    [
        SNew(SVerticalBox)
        + SVerticalBox::Slot()
        .Padding(2.f)
        .VAlign(VAlign_Fill)
        .HAlign(HAlign_Fill)
        [
            SNew(SExpandableArea)
            .AreaTitle(LOCTEXT("AreaTitle", "Area Title"))
            .InitiallyCollapsed(false)
            .Padding(8.f)
            .HeaderContent()
            [
                // 略,第一行那三个Button
            ]
            .BodyContent()
            [
                SNew(SScrollBox)
                + SScrollBox::Slot()
                [
                    SAssignNew(MyVerticalBox, SVerticalBox)
                ]
            ]
        ]
    ];
}

每一行呢都会加一个 SInputButton 出来,这个我们自己写的slate控件。并且 SInputButton 里面有一个下拉框。选中要回调到自己的一个函数

FReply SMainSlate::OnClickAdd()
{
    MyVerticalBox->AddSlot()
    [
        SNew(SInputButton)
        .OnFinishSlot(this, &SMainSlate::OnSlotFinished)
    ];
    return FReply::Handled();
}

我们可以看到我们把加出来的 SInputButton 的 OnFinishSlot 的这个事件绑定到了自己的 SMainSlate::OnSlotFinished 身上。(子类的事件绑定到父类上,并且用链式编程的方式)
这是怎么做到的呢?

看SInputButton

DECLARE_DELEGATE_TwoParams(FOnFinishSlot, TSharedRef<SWidget>, TSharedPtr<FString> &)

class SInputButton : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SInputButton)
    {}

    SLATE_EVENT(FOnFinishSlot, OnFinishSlot)

    SLATE_END_ARGS()

    FOnFinishSlot OnFinishSlot;
};

首先
DECLARE_DELEGATE_TwoParams(FOnFinishSlot, TSharedRef, TSharedPtr &)
FOnFinishSlot OnFinishSlot;
这是一个委托
我们在画 SInputButton 的时候,下拉框的选中事件,我们会出发这个委托
也就是

void SInputButton::OnSelectionChanged(TSharedPtr<FString> NewValue, ESelectInfo::Type SelectInfo)
{
    if (ComboBoxContent.IsValid())
    {
        ComboBoxContent->SetContent(SNew(STextBlock).Text(FText::FromString("")));
        OnFinishSlot.ExecuteIfBound(this->AsShared(), NewValue);
    }
}

最后调用到了 SMainSlate::OnSlotFinished

    SNew(SInputButton)
    .OnFinishSlot(this, &SMainSlate::OnSlotFinished)

【但是】
这个 .OnFinishSlot(this, &SMainSlate::OnSlotFinished) 语法不是很懂,委托不是该 BindUObject 吗。而且返回了啥东西能让他一直点下去。

首先我们看这里

class SInputButton : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SInputButton)
    {}

    SLATE_EVENT(FOnFinishSlot, OnFinishSlot)

    SLATE_END_ARGS()

看宏定义展开

#define SLATE_BEGIN_ARGS( InWidgetType ) \
    public: \
    struct FArguments : public TSlateBaseNamedArgs<InWidgetType> \
    { \
        typedef FArguments WidgetArgsType; \
        typedef InWidgetType WidgetType; \
        FORCENOINLINE FArguments()

#define SLATE_END_ARGS() \
    };

宏定义替换

    struct FArguments : public TSlateBaseNamedArgs<SInputButton>
    {
        typedef FArguments WidgetArgsType;
        typedef SInputButton WidgetType;
        FORCENOINLINE FArguments()
        {

        }

        // 这之间写的代码都会被加到这里
    }

我们看这个类是什么

/** Base class for named arguments. Provides settings necessary for all widgets. */
template<typename InWidgetType> // InWidgetType is SInputButton
struct TSlateBaseNamedArgs : public FSlateBaseNamedArgs
{
    typedef InWidgetType WidgetType;
    typedef typename WidgetType::FArguments WidgetArgsType;

    SLATE_PRIVATE_ATTRIBUTE_FUNCTION(FText, ToolTipText)
    // 一堆宏

};
struct FSlateBaseNamedArgs
{
    FSlateBaseNamedArgs() = default;

    // 一堆宏  

    TArray<TSharedRef<ISlateMetaData>> MetaData;
};

我们可以得出结论,这个结构维护了一个 MetaData 的 TArray

展开类 FSlateBaseNamedArgs 的宏

SLATE_PRIVATE_ATTRIBUTE_VARIABLE(FText, ToolTipText);

// 经过

#define SLATE_PRIVATE_ATTRIBUTE_VARIABLE( AttrType, AttrName ) \
        TAttribute< AttrType > _##AttrName

// 展开为

TAttribute<FText> _ToolTipText;
SLATE_PRIVATE_ARGUMENT_VARIABLE(bool, ForceVolatile) = false;

// 经过

#define SLATE_PRIVATE_ARGUMENT_VARIABLE( ArgType, ArgName ) \
        ArgType _##ArgName

// 展开为

bool _ForceVolatile = false

其实就是定义了一堆变量。

展开SLATE_PRIVATE_ATTRIBUTE_FUNCTION(FText, ToolTipText)

define SLATE_PRIVATE_ATTRIBUTE_FUNCTION( AttrType, AttrName ) \

...

        WidgetArgsType& ToolTipText( TAttribute< FText > InAttribute )
        {
            _ToolTipText = MoveTemp(InAttribute);
            return static_cast<WidgetArgsType*>(this)->Me();
        }

        WidgetArgsType& ToolTipText_Lambda(TFunction< FText(void) >&& InFunctor)
        {
            _ToolTipText = TAttribute< FText >::Create(Forward<TFunction< FText(void) >>(InFunctor));
            return static_cast<WidgetArgsType*>(this)->Me();
        }

这些函数是针对上述变量的操作。

那么得出结论,这个类封装了对于Slate最基础的属性和方法,比如 可见性 EVisibility Visibility 。。。

然后
SLATE_EVENT(FOnFinishSlot, OnFinishSlot)

define SLATE_EVENT( DelegateName, EventName ) \

...

这堆宏可以看到封装了一堆的
bind lambda, bind sp, bind raw, bind uobject
看起来是这个

        template< class UserClass, typename... VarTypes > \
        WidgetArgsType& EventName( UserClass* InUserObject, typename DelegateName::template TMethodPtr< UserClass, VarTypes... > InFunc, VarTypes... Vars )   \
        { \
            _##EventName = DelegateName::CreateSP( InUserObject, InFunc, Vars... ); \
            return static_cast<WidgetArgsType*>(this)->Me(); \
        } \

最后是一个代理名字
DelegateName _##EventName;
去宏后为
FOnFinishSlot _OnFinishSlot

我们得出结论1:
在定义我们自己的Slate控件的时候,继承自SWidget的某个子类。
用宏 SLATE_BEGIN_ARGS SLATE_END_ARGS。封装了一个局部结构 struct FArguments。里面为我们预生产了slate控件最基础的属性和函数代码。
这个struct对象会在 Construct(const FArguments& InArgs) 函数里面给你
并且我们可以用
SLATE_EVENT(FOnFinishSlot, OnFinishSlot) 在 struct 在里面快速定义一个委托。

【然后】
我不理解 FArguments 的struct,为什么能被 SNew(SInputButton) 点出来。

我们给从SNew开始看了,

    SNew(SInputButton)
    .OnFinishSlot(this, &SMainSlate::OnSlotFinished)
#define SNew( WidgetType, ... ) \
    MakeTDecl<WidgetType>( #WidgetType, __FILE__, __LINE__, RequiredArgs::MakeRequiredArgs(__VA_ARGS__) ) <<= TYPENAME_OUTSIDE_TEMPLATE WidgetType::FArguments()

构造了一个 MakeTDecl(...) 对象
并且用运算符重载,<<= SInputButton::FArguments() 传递了一个FArguments对象。 这个FArguments对象是啥上面已经了解了。

最后到了这个类, 既然是SNew的返回值,那么他应该就是我们链式编程的罪魁祸首。
里面的 _Widget 就是我们自己写的 SInputButton 的实例

/**
 * Utility class used during widget instantiation.
 * Performs widget allocation and construction.
 * Ensures that debug info is set correctly.
 * Returns TSharedRef to widget.
 *
 * @see SNew
 * @see SAssignNew
 */
template<class WidgetType, typename RequiredArgsPayloadType>
struct TSlateDecl
{
    TSlateDecl( const ANSICHAR* InType, const ANSICHAR* InFile, int32 OnLine, RequiredArgsPayloadType&& InRequiredArgs )
        : _RequiredArgs(InRequiredArgs)
    {
        if constexpr (std::is_base_of_v<SUserWidget, WidgetType>)
        {
            /**
             * SUserWidgets are allocated in the corresponding CPP file, so that
             * the implementer can return an implementation that differs from the
             * public interface. @see SUserWidgetExample
             */
            _Widget = WidgetType::New();
        }
        else
        {
            /** Normal widgets are allocated directly by the TSlateDecl. */
            _Widget = MakeShared<WidgetType>();
        }

        _Widget->SetDebugInfo(InType, InFile, OnLine, sizeof(WidgetType));
    }

    // ...

    /**
     * Complete widget construction from InArgs.
     *
     * @param InArgs  NamedArguments from which to construct the widget.
     *
     * @return A reference to the widget that we constructed.
     */
    TSharedRef<WidgetType> operator<<=( const typename WidgetType::FArguments& InArgs ) &&
    {
        _Widget->SWidgetConstruct(InArgs);
        _RequiredArgs.CallConstruct(_Widget.Get(), InArgs);
        _Widget->CacheVolatility();
        _Widget->bIsDeclarativeSyntaxConstructionCompleted = true;

        return MoveTemp(_Widget).ToSharedRef();
    }

    TSharedPtr<WidgetType> _Widget;
    RequiredArgsPayloadType& _RequiredArgs;
};

从 operator<<= 隐约可以看到
return MoveTemp(_Widget).ToSharedRef();
最后返回了_Widget的共享引用。
在这之间,做了一些构造工作,SWidgetConstruct 里面会调用到 Construct

另外 SAssignNew 多了一个 Expose

#define SAssignNew( ExposeAs, WidgetType, ... ) \
    MakeTDecl<WidgetType>( #WidgetType, __FILE__, __LINE__, RequiredArgs::MakeRequiredArgs(__VA_ARGS__) ) . Expose( ExposeAs ) <<= TYPENAME_OUTSIDE_TEMPLATE WidgetType::FArguments()
    /**
     * Initialize OutVarToInit with the widget that is being constructed.
     * @see SAssignNew
     */
    template<class ExposeAsWidgetType>
    TSlateDecl&& Expose( TSharedPtr<ExposeAsWidgetType>& OutVarToInit ) &&
    {
        // Can't move out _Widget here because operator<<= needs it
        OutVarToInit = _Widget;
        return MoveTemp(*this);
    }

    /**
     * Initialize OutVarToInit with the widget that is being constructed.
     * @see SAssignNew
     */
    template<class ExposeAsWidgetType>
    TSlateDecl&& Expose( TSharedRef<ExposeAsWidgetType>& OutVarToInit ) &&
    {
        // Can't move out _Widget here because operator<<= needs it
        OutVarToInit = _Widget.ToSharedRef();
        return MoveTemp(*this);
    }

多了一个赋值。这样 SNew 和 SAssignNew 的区别我们也找到了

倒回去

    SNew(SInputButton)
    .OnFinishSlot(this, &SMainSlate::OnSlotFinished)

SNew(SInputButton) 最后返回的就是 TSharedRef 根据 <<=
但是如果你用 点 链式编程,那么【点符号】都会作用在 args 上
<<= WidgetType::FArguments().OnFinishSlot(this, &SMainSlate::OnSlotFinished)
由于这个重载,最后会进去 ::Construct(const FArguments& InArgs)

综合起来

    SNew(SInputButton)
    .OnFinishSlot(this, &SMainSlate::OnSlotFinished)

这个过程,生产了一个中间量 FArguments,里面有一个宏定义生成的这种东西。

        template< class UserClass, typename... VarTypes > \
        WidgetArgsType& EventName( UserClass* InUserObject, typename DelegateName::template TMethodPtr< UserClass, VarTypes... > InFunc, VarTypes... Vars )   \
        { \
            _##EventName = DelegateName::CreateSP( InUserObject, InFunc, Vars... ); \
            return static_cast<WidgetArgsType*>(this)->Me(); \
        } \

把代理绑定到了 _OnFinishSlot 上,最后传给Construct。
所以

void SInputButton::Construct(const FArguments& InArgs)
{
    OnFinishSlot = InArgs._OnFinishSlot;

出发 OnFinishSlot 这个事件就可以了。

所以我们的基本套路

class SInputButton : public SCompoundWidget
{
public:
    SLATE_BEGIN_ARGS(SInputButton)
    {}

    SLATE_EVENT(FOnFinishSlot, OnFinishSlot)

    SLATE_END_ARGS()


SLATE_EVENT(FOnFinishSlot, OnFinishSlot) 在 begin {} end 里面定义委托
他会在 FArguments 里面生成一个 _OnFinishSlot 的委托并预生成代码。
我们在类内也要定义一个委托。方便我们调用。然后在construct里面绑定他两

===
除此之外
类似
SLATE_EVENT(FOnFinishSlot, OnFinishSlot)
的还有
SLATE_ARGUMENT(float, Value);
SLATE_ATTRIBUTE

都是帮忙生成一些函数,并返回 Me() 用于链式编程

UMG

UMG的使用

void AHUDBase::ShowTips(const FString& InTips)
{
    if (WndTipsAClass)
    {
        if (WndTipsA)
        {
            WndTipsA->RemoveFromParent();
            WndTipsA = nullptr;
        }

        WndTipsA = CreateWidget<UWndTipsA>(GetWorld(), WndTipsAClass);
        if (WndTipsA)
        {
            WndTipsA->SetTips(InTips);
            WndTipsA->AddToViewport();
        }
    }
}

以前写的一个ShowTips 的功能。核心就是 CreateWidget 和 AddToViewport。
你已经学会了使用,可以看看是怎么实现的了。

CreateWidget

template <typename WidgetT = UUserWidget, typename OwnerType = UObject>
WidgetT* CreateWidget(OwnerType OwningObject, TSubclassOf<UUserWidget> UserWidgetClass = WidgetT::StaticClass(), FName WidgetName = NAME_None)
{
    static_assert(TIsDerivedFrom<WidgetT, UUserWidget>::IsDerived, "CreateWidget can only be used to create UserWidget instances. If creating a UWidget, use WidgetTree::ConstructWidget.");

    static_assert(TIsDerivedFrom<TPointedToType<OwnerType>, UWidget>::IsDerived
        || TIsDerivedFrom<TPointedToType<OwnerType>, UWidgetTree>::IsDerived
        || TIsDerivedFrom<TPointedToType<OwnerType>, APlayerController>::IsDerived
        || TIsDerivedFrom<TPointedToType<OwnerType>, UGameInstance>::IsDerived
        || TIsDerivedFrom<TPointedToType<OwnerType>, UWorld>::IsDerived, "The given OwningObject is not of a supported type for use with CreateWidget.");

    SCOPE_CYCLE_COUNTER(STAT_CreateWidget);
    FScopeCycleCounterUObject WidgetObjectCycleCounter(UserWidgetClass, GET_STATID(STAT_CreateWidget));

    if (OwningObject)
    {
        return Cast<WidgetT>(UUserWidget::CreateWidgetInstance(*OwningObject, UserWidgetClass, WidgetName));
    }
    return nullptr;
}

第一个 static_assert 说明,CreateWidget 的模板必须是 UUserWidget 的子类。所有的UMG类都直接间接继承自 UUserWidget。
第二个 static_assert 在检测 OwnerType OwningObject,我们 CreateWidget(GetWorld(), WndTipsAClass); 给了一个 GetWorld(), 这里说明 UWidget,UWidgetTree,APlayerController,UGameInstance 也OK
SCOPE_CYCLE_COUNTER(STAT_CreateWidget); 锁
然后返回
Cast(UUserWidget::CreateWidgetInstance(*OwningObject, UserWidgetClass, WidgetName));

    static UMG_API UUserWidget* CreateWidgetInstance(UWidget& OwningWidget, TSubclassOf<UUserWidget> UserWidgetClass, FName WidgetName);
    static UMG_API UUserWidget* CreateWidgetInstance(UWidgetTree& OwningWidgetTree, TSubclassOf<UUserWidget> UserWidgetClass, FName WidgetName);
    static UMG_API UUserWidget* CreateWidgetInstance(APlayerController& OwnerPC, TSubclassOf<UUserWidget> UserWidgetClass, FName WidgetName);
    static UMG_API UUserWidget* CreateWidgetInstance(UGameInstance& GameInstance, TSubclassOf<UUserWidget> UserWidgetClass, FName WidgetName);
    static UMG_API UUserWidget* CreateWidgetInstance(UWorld& World, TSubclassOf<UUserWidget> UserWidgetClass, FName WidgetName);

我们看UWorld的版本

UUserWidget* UUserWidget::CreateWidgetInstance(UWorld& World, TSubclassOf<UUserWidget> UserWidgetClass, FName WidgetName)
{
    if (UGameInstance* GameInstance = World.GetGameInstance())
    {
        return CreateWidgetInstance(*GameInstance, UserWidgetClass, WidgetName);
    }
    return CreateInstanceInternal(&World, UserWidgetClass, WidgetName, &World, World.GetFirstLocalPlayerFromController());
}

最后到这里,代码也不长

UUserWidget* UUserWidget::CreateInstanceInternal(UObject* Outer, TSubclassOf<UUserWidget> UserWidgetClass, FName InstanceName, UWorld* World, ULocalPlayer* LocalPlayer)
{
    LLM_SCOPE_BYTAG(UI_UMG);

    if (!CreateWidgetHelpers::ValidateUserWidgetClass(UserWidgetClass))
    {
        return nullptr;
    }

    // 。。。略

    UUserWidget* NewWidget = NewObject<UUserWidget>(Outer, UserWidgetClass, InstanceName, RF_Transactional);

    if (LocalPlayer)
    {
        NewWidget->SetPlayerContext(FLocalPlayerContext(LocalPlayer, World));
    }

    NewWidget->Initialize();

    return NewWidget;
}

ValidateUserWidgetClass 合法性判断


bool CreateWidgetHelpers::ValidateUserWidgetClass(const UClass* UserWidgetClass)
{
    if (UserWidgetClass == nullptr)
    {
        FMessageLog("PIE").Error(LOCTEXT("WidgetClassNull", "CreateWidget called with a null class."));
        return false;
    }

    if (!UserWidgetClass->IsChildOf(UUserWidget::StaticClass()))
    {
        const FText FormatPattern = LOCTEXT("NotUserWidget", "CreateWidget can only be used on UUserWidget children. {UserWidgetClass} is not a UUserWidget.");
        FFormatNamedArguments FormatPatternArgs;
        FormatPatternArgs.Add(TEXT("UserWidgetClass"), FText::FromName(UserWidgetClass->GetFName()));
        FMessageLog("PIE").Error(FText::Format(FormatPattern, FormatPatternArgs));
        return false;
    }

    if (UserWidgetClass->HasAnyClassFlags(CLASS_Abstract | CLASS_NewerVersionExists | CLASS_Deprecated))
    {
        const FText FormatPattern = LOCTEXT("NotValidClass", "Abstract, Deprecated or Replaced classes are not allowed to be used to construct a user widget. {UserWidgetClass} is one of these.");
        FFormatNamedArguments FormatPatternArgs;
        FormatPatternArgs.Add(TEXT("UserWidgetClass"), FText::FromName(UserWidgetClass->GetFName()));
        FMessageLog("PIE").Error(FText::Format(FormatPattern, FormatPatternArgs));
        return false;
    }

    return true;
}

逻辑一目了然

核心就是这里的 NewObject

然后设置 LocalPlayer,点进去看 SetPlayerContext 还会遍历 WidgetTree 设置 LocalPlayer

void UUserWidget::SetPlayerContext(const FLocalPlayerContext& InPlayerContext)
{
    PlayerContext = InPlayerContext;

    if (WidgetTree)
    {
        WidgetTree->ForEachWidget(
            [&InPlayerContext] (UWidget* Widget) 
            {
                if (UUserWidget* UserWidget = Cast<UUserWidget>(Widget))
                {
                    UserWidget->SetPlayerContext(InPlayerContext);
                }
            });
    }

}

然后 Initialize

bool UUserWidget::Initialize()
{
    // If it's not initialized initialize it, as long as it's not the CDO, we never initialize the CDO.
    if (!bInitialized && !HasAnyFlags(RF_ClassDefaultObject))
    {
        // If this is a sub-widget of another UserWidget, default designer flags and player context to match those of the owning widget
        if (UUserWidget* OwningUserWidget = GetTypedOuter<UUserWidget>())
        {
#if WITH_EDITOR
            SetDesignerFlags(OwningUserWidget->GetDesignerFlags());
#endif
            SetPlayerContext(OwningUserWidget->GetPlayerContext());
        }

        UWidgetBlueprintGeneratedClass* BGClass = Cast<UWidgetBlueprintGeneratedClass>(GetClass());
        // Only do this if this widget is of a blueprint class
        if (BGClass)
        {
            BGClass->InitializeWidget(this);
        }
        else
        {
            InitializeNativeClassData();
        }

        if ( WidgetTree == nullptr )
        {
            WidgetTree = NewObject<UWidgetTree>(this, TEXT("WidgetTree"), RF_Transient);
        }
        else
        {
            WidgetTree->SetFlags(RF_Transient);

            InitializeNamedSlots();
        }

        if (!IsDesignTime() && PlayerContext.IsValid())
        {
            NativeOnInitialized();
        }

        bInitialized = true;
        return true;
    }

    return false;
}

看到这里同时对蓝图做了初始化 UWidgetBlueprintGeneratedClass
然后是 WidgetTree

AddToViewport / AddToPlayerScreen

有人用 AddToViewport 有人用 AddToPlayerScreen

bool UUserWidget::AddToPlayerScreen(int32 ZOrder)
{
    if (UGameViewportSubsystem* Subsystem = UGameViewportSubsystem::Get(GetWorld()))
    {
        if (ULocalPlayer* LocalPlayer = GetOwningLocalPlayer())
        {
            FGameViewportWidgetSlot ViewportSlot;
            if (bIsManagedByGameViewportSubsystem)
            {
                ViewportSlot = Subsystem->GetWidgetSlot(this);
            }
            ViewportSlot.ZOrder = ZOrder;
            Subsystem->AddWidgetForPlayer(this, GetOwningLocalPlayer(), ViewportSlot);
            return true;
        }
        else
        {
            FMessageLog("PIE").Error(LOCTEXT("AddToPlayerScreen_NoPlayer", "AddToPlayerScreen Failed.  No Owning Player!"));
        }
    }
    return false;
}

获取 UGameViewportSubsystem,和 LocalPlayer
核心逻辑在于SubSystem加上去的
Subsystem->AddWidgetForPlayer(this, GetOwningLocalPlayer(), ViewportSlot);

bool UGameViewportSubsystem::AddWidgetForPlayer(UWidget* Widget, ULocalPlayer* Player, FGameViewportWidgetSlot Slot)
{
    if (!Player)
    {
        FFrame::KismetExecutionMessage(TEXT("The Player is invalid."), ELogVerbosity::Warning);
        return false;
    }
    return AddToScreen(Widget, Player, Slot);
}

AddToScreen

void UUserWidget::AddToViewport(int32 ZOrder)
{
    if (UGameViewportSubsystem* Subsystem = UGameViewportSubsystem::Get(GetWorld()))
    {
        FGameViewportWidgetSlot ViewportSlot;
        if (bIsManagedByGameViewportSubsystem)
        {
            ViewportSlot = Subsystem->GetWidgetSlot(this);
        }
        ViewportSlot.ZOrder = ZOrder;
        Subsystem->AddWidget(this, ViewportSlot);
    }
}
bool UGameViewportSubsystem::AddWidget(UWidget* Widget, FGameViewportWidgetSlot Slot)
{
    return AddToScreen(Widget, nullptr, Slot);
}

显然,AddToViewport 和 AddToPlayerScreen 的区别就是 AddToScreen 的时候Player参数是不是空的。

bool UGameViewportSubsystem::AddToScreen(UWidget* Widget, ULocalPlayer* Player, FGameViewportWidgetSlot& Slot)
{
    if (!Widget)
    {
        FFrame::KismetExecutionMessage(TEXT("The Widget is invalid."), ELogVerbosity::Warning);
        return false;
    }

    if (UPanelWidget* ParentPanel = Widget->GetParent())
    {
        FFrame::KismetExecutionMessage(*FString::Printf(TEXT("The widget '%s' already has a parent widget. It can't also be added to the viewport!"), *Widget->GetName()), ELogVerbosity::Warning);
        return false;
    }
}

首先你 Widget 给合法吧,并且有父类的 Widget 是不能加到屏幕的

bool UGameViewportSubsystem::AddToScreen(UWidget* Widget, ULocalPlayer* Player, FGameViewportWidgetSlot& Slot)
{
    // 。。。

    SConstraintCanvas::FSlot* RawSlot = nullptr;
    TSharedPtr<SConstraintCanvas> FullScreenCanvas;
    {
        FSlotInfo& SlotInfo = ViewportWidgets.FindOrAdd(Widget);
        Widget->bIsManagedByGameViewportSubsystem = true;

        if (SlotInfo.FullScreenWidget.IsValid())
        {
            FFrame::KismetExecutionMessage(*FString::Printf(TEXT("The widget '%s' was already added to the screen."), *Widget->GetName()), ELogVerbosity::Warning);
            return false;
        }

        TPair<FMargin, bool> OffsetArgument = UE::UMG::Private::CalculateOffsetArgument(Slot);
        FullScreenCanvas = SNew(SConstraintCanvas)
            + SConstraintCanvas::Slot()
            .Offset(OffsetArgument.Get<0>())
            .AutoSize(OffsetArgument.Get<1>())
            .Anchors(Slot.Anchors)
            .Alignment(Slot.Alignment)
            .Expose(RawSlot);

        SlotInfo.Slot = Slot;
        SlotInfo.LocalPlayer = Player;
        SlotInfo.FullScreenWidget = FullScreenCanvas;
        SlotInfo.FullScreenWidgetSlot = RawSlot;
    }

    // 。。。
}

这一大段都是为了创建 SConstraintCanvas 这一 Slate
class SConstraintCanvas : public SPanel
这是一个SWidget的子类了。
猜想这是容纳我们即将创建的UMG的容器。
然后

    check(RawSlot);
    RawSlot->AttachWidget(Widget->TakeWidget());

先看 TakeWidget

TSharedRef<SWidget> UWidget::TakeWidget()
{
    LLM_SCOPE_BYTAG(UI_UMG);

    return TakeWidget_Private( []( UUserWidget* Widget, TSharedRef<SWidget> Content ) -> TSharedPtr<SObjectWidget> {
               return SNew( SObjectWidget, Widget )[ Content ];
           } );
}

我们创建的UMG是这个 UWidget,这里的this
返回的是 TSharedRef 由函数,TakeWidget_Private 提供,TakeWidget_Private的参数是一个返回SLate的 Lambda。
对应的Slate类是 SObjectWidget
这个是

class SObjectWidget : public SCompoundWidget, public FGCObject

然后回去 TakeWidget_Private 看这里

TSharedRef<SWidget> UWidget::TakeWidget_Private(ConstructMethodType ConstructMethod)
{
    //

    OnWidgetRebuilt()
}

很遗憾只看懂了这个函数,里面的 NativeConstruct 很眼熟

void UUserWidget::OnWidgetRebuilt()
{
    // When a user widget is rebuilt we can safely initialize the navigation now since all the slate
    // widgets should be held onto by a smart pointer at this point.
    WidgetTree->ForEachWidget([&] (UWidget* Widget) {
        Widget->BuildNavigation();
    });

    if (!IsDesignTime())
    {
        // Notify the widget to run per-construct.
        NativePreConstruct();

        // Notify the widget that it has been constructed.
        NativeConstruct();
    }
#if WITH_EDITOR
    else if ( HasAnyDesignerFlags(EWidgetDesignFlags::ExecutePreConstruct) )
    {
        bool bCanCallPreConstruct = true;
        if (UWidgetBlueprintGeneratedClass* GeneratedBPClass = Cast<UWidgetBlueprintGeneratedClass>(GetClass()))
        {
            bCanCallPreConstruct = GeneratedBPClass->bCanCallPreConstruct;
        }

        if (bCanCallPreConstruct)
        {
            NativePreConstruct();
        }
    }
#endif
}

我们回退到

bool UGameViewportSubsystem::AddToScreen(UWidget* Widget, ULocalPlayer* Player, FGameViewportWidgetSlot& Slot)
{
    // 。。。

    check(RawSlot);
    RawSlot->AttachWidget(Widget->TakeWidget());

    if (Player)
    {
        ViewportClient->AddViewportWidgetForPlayer(Player, FullScreenCanvas.ToSharedRef(), Slot.ZOrder);
    }
    else
    {
        // We add 10 to the zorder when adding to the viewport to avoid 
        // displaying below any built-in controls, like the virtual joysticks on mobile builds.
        ViewportClient->AddViewportWidgetContent(FullScreenCanvas.ToSharedRef(), Slot.ZOrder + 10);
    }
    OnWidgetAdded.Broadcast(Widget, Player);

    return true;
}

后面就是添加到视口了,然后广播事件。
添加到视口使劲翻会到这里

void SGameLayerManager::AddWidgetForPlayer(ULocalPlayer* Player, TSharedRef<SWidget> ViewportContent, const int32 ZOrder)
{
    TSharedPtr<FPlayerLayer> PlayerLayer = FindOrCreatePlayerLayer(Player);

    PlayerLayer->Widget->AddSlot(ZOrder)
    [
        ViewportContent
    ];
}

RemoveFromParent

void UWidget::RemoveFromParent()
{
    if (!HasAnyFlags(RF_BeginDestroyed))
    {
        if (bIsManagedByGameViewportSubsystem)
        {
            if (UGameViewportSubsystem* Subsystem = UGameViewportSubsystem::Get(GetWorld()))
            {
                Subsystem->RemoveWidget(this);
            }
        }
        else if (UPanelWidget* CurrentParent = GetParent())
        {
            CurrentParent->RemoveChild(this);
        }
        else
        {
            // warn
        }
    }
}

要么从 Subsystem 删除,要么有父节点,从父节点删除

最后让 UGameViewportClient 干活。

            if (UGameViewportClient* ViewportClient = World->GetGameViewport())
            {
                TSharedRef<SWidget> WidgetHostRef = WidgetHost.ToSharedRef();
                ViewportClient->RemoveViewportWidgetContent(WidgetHostRef);

                // We may no longer have access to our owning player if the player controller was destroyed
                // Passing nullptr to RemoveViewportWidgetForPlayer will search all player layers for this widget
                ViewportClient->RemoveViewportWidgetForPlayer(LocalPlayer.Get(), WidgetHostRef);
            }

其他

看到这里我们还有一些疑惑

UMG控件到Slate

我们打开图片控件,打开C++代码

class UImage : public UWidget
{

protected:
    TSharedPtr<SImage> MyImage;
}

我们看到他继承自 UWidget,并且包含了一个SImage的指针

好的,你已经学会UMG和Slate的关系了,下一步可以自定义UMG控件了

自定义UMG控件

TODO

Slate在游戏中的应用

UE编辑器扩展

工具

打开显示编辑器扩展点

用这个来找一个控件的代码在哪里

基础

创建一个ToolBar Button 插件

首先他是这样把东西加到菜单上的。

void FMyToolButtonModule::RegisterMenus()
{
    // Owner will be used for cleanup in call to UToolMenus::UnregisterOwner
    FToolMenuOwnerScoped OwnerScoped(this);

    {
        UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Window");
        {
            FToolMenuSection& Section = Menu->FindOrAddSection("WindowLayout");
            Section.AddMenuEntryWithCommandList(FMyToolButtonCommands::Get().PluginAction, PluginCommands);
        }
    }

    {
        UToolMenu* ToolbarMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.LevelEditorToolBar.PlayToolBar");
        {
            FToolMenuSection& Section = ToolbarMenu->FindOrAddSection("PluginTools");
            {
                FToolMenuEntry& Entry = Section.AddEntry(FToolMenuEntry::InitToolBarButton(FMyToolButtonCommands::Get().PluginAction));
                Entry.SetCommandList(PluginCommands);
            }
        }
    }
}
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇