概述
呜哇哈哈哈哈哈哈哈哈哈哈哈哈哈哈
整理知识体系
源码版本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
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
SCOPE_CYCLE_COUNTER(STAT_CreateWidget); 锁
然后返回
Cast
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
对应的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);
}
}
}
}