--- title: Slate学习笔记(二):UI混合使用方案 date: 2021-03-12 15:40:04      tags: Slate rating: ⭐️ --- ## 前言 之前在学习OnlineSubsytem,其中有一个将网络错误显示给用户的需求,这就需要实现一个MessageBox方案。所以在开发前我构造了以下几个思路: 1. UMG作为MessageBox主体,使用GameInstance来管理UI。 2. Slate作为MessageBox主体,使用GameViewportClient来管理UI。 3. 混合使用,蓝图上使用UMG,c++使用Slate。 下面说一下我的测试结果——上述方案的优缺点: ### UMG方案 这种方案也有两种实现思路: 1. 使用编辑器中创建的UMG Asset 2. 使用C++创建的UUserWidget 第一种方案除开需要用反射系统来绑定委托就是完美方案。但因为本人没解决这个问题,所以就用了C++方案。当然使用第一种方案也会导致使用GameInstanceSubsystem会变得比较麻烦(可以通过函数的方式来赋予UMG Class,但对美术来说比较麻烦)
**优点**:使用蓝图迭代速度较快。 ### Slate方案 使用GameViewportClient作为UI管理。
**优点**:可以管理所有的UI,也就是说所有添加到屏幕上或者移除的UI都会调用AddViewportWidgetContent与RemoveViewportWidgetContent。在ShooterGame中通过重写这两个函数,使用2个SWidget数组控制各种情况下UI的显示与隐藏。比如:当LoadingScreen出现时隐藏所有UI,结束时回复。 那GameViewportClient可以管理UMG么?经过测试,调用RemoveViewportWidgetContent移除UMG对应的Slate(UMG本质就是使用UWidget包裹的Slate),UUserWiget也能正常销毁,理论是可以的。但很遗憾还在存在几个问题: 1. 对于UMG这种套了一层UWidget的Slate,你无法分辨UWidget类型。 2. 在GameViewportClient里调用CreateWidget()与RemoveFromParent()会出现Access Violation Rendering Location 0x000000 的问题,尝试将函数与成员都写在GameInstance中,之后在GameViewportClient调用时,RemoveFromParent()还是会出现这个问题。 ## 实现 对于**混合使用**:本人不太喜欢混合使用方案,本人认为应该把相关的管理代码都写在一个地方。本人最后使用了方案1。使用Slate实现底层。分别创建UWidget(作为UMG编辑器中的控件)与UUserWidget版本(MessageBox主体)。这么做的好处在于: 1. 类型统一方便管理 2. 默认样式统一 3. C++与蓝图使用同一个函数 ### 通过c++设置UUserWidget内容 最早UMG刚出来是,可以通过RootWigdet(以前默认是UCanvasPannel)以AddChild方式添加UWiget。不过现在版本获取到RootWidget是空指针。所以只能通过RebuildWidget来设置UI控件了。这里定义了UMessageBoxWidget对象(UWidget),需要在Initialize()中初始化,NativeConstruct()会在RebuildWidget()后面调用。大致代码如下: ```c++ class RPGGAMEPLAYABILITY_API UMessageBox : public UUserWidget { GENERATED_BODY() public: virtual bool Initialize() override; UPROPERTY(Instanced, BlueprintReadWrite,EditAnywhere) UMessageBoxWidget* MessageBox; }; bool UMessageBox::Initialize() { if (!Super::Initialize()) return false; MessageBox = NewObject(this, TEXT("MessageBox")); MessageBoxSize = FVector2D(500, 400); return true; } TSharedRef UMessageBox::RebuildWidget() { return SNew(SConstraintCanvas) + SConstraintCanvas::Slot() .Anchors(0.5) .Alignment(FVector2D(0.5, 0.5)) .Offset(FMargin(0, 0, MessageBoxSize.X, MessageBoxSize.Y)) [ MessageBox->TakeWidget() ]; } ``` 如果想要在子类中预览结果,可以实现SynchronizeProperties()与NativeTick(); ### 绑定委托 Slate上的FOnClicked委托是静态的,UWidget为了能实时绑定委托用的是动态多播委托(BlueprintAssignable只支持动态多播委托),而蓝图中UFUNCTION只支持动态单播委托作为形参。 所以这里我在UMessageBoxWidget中声明了用于在蓝图UFUNCTION中绑定的动态委托。之后再在与Slate静态委托绑定的Handle函数中调用即可。 ```c++ FReply UMessageBoxWidget::SlateHandleConfirmButtonClicked() { TransmitOnConfirmButtonClickedEvent.ExecuteIfBound(); OnConfirmButtonClicked.Broadcast(); return FReply::Handled(); } ``` 注意2个委托的顺序,下面这段代码里我通过判断动态多播委托是否绑定函数,来决定是否需要绑定RemoveFromParent()来起到移除MessageBox的作用。因为上面的Handle函数在UWidget中非UUserWidget中,无法直接调用RemoveFromParent。无论是再传递委托到UUserWidget或者传递指针,都感觉比较啰嗦,所以我就这么写了。 ```c++ UMessageBox* URPGGameInstanceBase::ShowMessageBox(const FText& MessageText, const FText& ConfirmText, const FText& CancelText,const FTransmitDynamicEvent& OnConfirm, const FTransmitDynamicEvent& OnCancel, const FVector2D MessageBoxSize) { UMessageBox* MessageBox=CreateWidget(GetWorld(), UMessageBox::StaticClass()); MessageBox->MessageBox->MessageText = MessageText; MessageBox->MessageBox->ConfirmText = ConfirmText; MessageBox->MessageBox->CancelText = CancelText; MessageBox->MessageBoxSize= MessageBoxSize; //判断是否有绑定,没绑定则直接绑定RemoveFromParent() if (!MessageBox->MessageBox->OnConfirmButtonClicked.IsBound()) { MessageBox->MessageBox->OnConfirmButtonClicked.AddDynamic(MessageBox, &UMessageBox::RemoveFromParent); } if (!MessageBox->MessageBox->OnCancelButtonClicked.IsBound()) { MessageBox->MessageBox->OnCancelButtonClicked.AddDynamic(MessageBox, &UMessageBox::RemoveFromParent); } MessageBox->MessageBox->TransmitOnConfirmButtonClickedEvent = OnConfirm; MessageBox->MessageBox->TransmitOnCancelButtonClickedEvent = OnCancel; MessageBox->AddToViewport((int32)EUIZOrder::EUIZOrder_MessageBox); return MessageBox; } ``` 最后直接调用就可以了。 ```c++ ShowMessageBox(FText::FromString("TestMessage!!!"),FText::FromString(TEXT("确定")),FText::FromString(TEXT("取消"))); ``` PS.在开发过程中我还遇到按钮需要点两下才能按到情况,后来发现是因为在Slate中Button的IsFocusable设置为false造成的,改成true就可以了。 ## BindWidget meta 在找资料的过程中发现这个网站的UI资料挺不错的:https://benui.ca/unreal,这里我简单说明一下BindWidget meta的用法:
使用c++开发UI时其中一个最大的问题就是:如何从C ++控制蓝图创建的Widget? 可以使用BindWidget meta标签来解决。 ```c++ #pragma once #include "BindExample.generated.h" UCLASS(Abstract) class UBindExample : public UUserWidget { GENERATED_BODY() public: virtual void NativeConstruct() override; UPROPERTY(BlueprintReadOnly, meta = (BindWidget)) class UTextBlock* ItemTitle = nullptr; }; ``` ``` #include "BindExample.h" #include "Components/TextBlock.h" void UBindExample::NativeConstruct() { // Call the Blueprint "Event Construct" node Super::NativeConstruct(); // ItemTitle can be nullptr if we haven't created it in the // Blueprint subclass if (ItemTitle) { ItemTitle->SetText(TEXT("Hello world!")); } } ``` 这样就可以在C++获得对应控件的指针了,除此之外还有其他meta选择: - DesignerRebuild - BindWidget - BindWidgetOptional - OptionalWidget - BindWidgetAnim - BindWidgetAnimOptional - IsBindableEvent 其中Optional代表可选,也就是Widget不一定处于绑定状态,推荐使用。 ```c++ // [PropertyMetadata] This property if changed will rebuild the widget designer preview. Use sparingly, try to update most properties by // setting them in the SynchronizeProperties function. // UPROPERTY(meta=(DesignerRebuild)) DesignerRebuild, // [PropertyMetadata] This property requires a widget be bound to it in the designer. Allows easy native access to designer defined controls. // UPROPERTY(meta=(BindWidget)) BindWidget, // [PropertyMetadata] This property optionally allows a widget be bound to it in the designer. Allows easy native access to designer defined controls. // UPROPERTY(meta=(BindWidgetOptional)) BindWidgetOptional, // [PropertyMetadata] This property optionally allows a widget be bound to it in the designer. Allows easy native access to designer defined controls. // UPROPERTY(meta=(BindWidget, OptionalWidget=true)) OptionalWidget, // [PropertyMetadata] This property requires a widget animation be bound to it in the designer. Allows easy native access to designer defined animations. // UPROPERTY(meta=(BindWidgetAnim)) BindWidgetAnim, // [PropertyMetadata] This property optionally allows a animation widget be bound to it in the designer. Allows easy native access to designer defined animation. // UPROPERTY(meta=(BindWidgetAnimOptional)) BindWidgetAnimOptional, // [PropertyMetadata] Exposes a dynamic delegate property in the details panel for the widget. // UPROPERTY(meta=(IsBindableEvent)) IsBindableEvent, ``` 该网站其他感觉有用的技巧: - 虚幻4提高UI性能技巧:https://benui.ca/unreal/ui-performance/ - UPROPERTY的编辑条件(EditCondition)与可以编辑条件(CanEditChange):https://benui.ca/unreal/uproperty-edit-condition-can-edit-change/ - 虚幻4UI本地化技巧:https://benui.ca/unreal/ui-localization/ ## 有关通过反射绑定Delegate的资料 在写本文的时候找到了一些资料,打算以后再尝试。
Unlua的**FDelegatePropertyDesc** ```c++ virtual bool SetValueInternal(lua_State *L, void *ValuePtr, int32 IndexInStack, bool bCopyValue) const override { UObject *Object = nullptr; const void *CallbackFunction = nullptr; int32 FuncIdxInTable = GetDelegateInfo(L, IndexInStack, Object, CallbackFunction); // get target UObject and Lua function if (FuncIdxInTable != INDEX_NONE) { FScriptDelegate *ScriptDelegate = DelegateProperty->GetPropertyValuePtr(ValuePtr); FCallbackDesc Callback(Object->GetClass(), CallbackFunction); FName FuncName = FDelegateHelper::GetBindedFunctionName(Callback); if (FuncName == NAME_None) { // no delegate function is created yet lua_rawgeti(L, IndexInStack, FuncIdxInTable); int32 CallbackRef = luaL_ref(L, LUA_REGISTRYINDEX); FDelegateHelper::Bind(ScriptDelegate, DelegateProperty, Object, Callback, CallbackRef); } else { ScriptDelegate->BindUFunction(Object, FuncName); // a delegate function is created already } } return true; } ``` https://zhuanlan.zhihu.com/p/333751025 ZhuRong-HomoStation/UnrealEvent ```c++ void FEventBinder::BindToObject(UObject* InObject) { for (auto & BindMap : EventBindMap) { FMulticastDelegateProperty* Property = static_cast( InObject->GetClass()->FindPropertyByName(BindMap.Key)); for (auto& Delegate : BindMap.Value.AllDelegates) { if (Delegate.BindFunction == NAME_None || !Delegate.TargetActor) continue; FScriptDelegate BindDelegate; BindDelegate.BindUFunction(Delegate.TargetActor.Get(), Delegate.BindFunction); Property->AddDelegate(BindDelegate, InObject); } } } ```