255 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			255 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
---
 | 
						||
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,但对美术来说比较麻烦)<br>
 | 
						||
**优点**:使用蓝图迭代速度较快。
 | 
						||
 | 
						||
### Slate方案
 | 
						||
使用GameViewportClient作为UI管理。<br>
 | 
						||
**优点**:可以管理所有的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<UMessageBoxWidget>(this, TEXT("MessageBox"));
 | 
						||
	MessageBoxSize = FVector2D(500, 400);
 | 
						||
 | 
						||
	return true;
 | 
						||
}
 | 
						||
 | 
						||
TSharedRef<SWidget> 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<UMessageBox>(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的用法:<br>
 | 
						||
使用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的资料
 | 
						||
在写本文的时候找到了一些资料,打算以后再尝试。<br>
 | 
						||
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<FMulticastDelegateProperty*>(
 | 
						||
            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);
 | 
						||
		}
 | 
						||
	}
 | 
						||
}
 | 
						||
``` |