This commit is contained in:
2025-08-02 12:09:34 +08:00
commit e70b01cdca
2785 changed files with 575579 additions and 0 deletions

View File

@@ -0,0 +1,225 @@
---
title: CommonUI
date: 2023-03-31 09:53:26
excerpt: 摘要
tags:
rating: ⭐
---
# 前言
CommonUI主要解决了**层叠式UI**输入事件处理以及**多平台**多输入设备管理问题。
其中比较重要的机制是CommonUI实现的**输入路由**功能,其代码位于:`CommonGameViewportClient.h` 和 `CommonUIActionRouterBase.h` 。
## 官方教学
- 文档
- https://docs.unrealengine.com/5.1/en-US/common-ui-quickstart-guide-for-unreal-engine/
- https://docs.unrealengine.com/5.1/en-US/overview-of-advanced-multiplatform-user-interfaces-with-common-ui-for-unreal-engine/
- 视频
- [Introduction to Common UI | Inside Unreal](https://www.youtube.com/live/TTB5y-03SnE?feature=share)
- [Lyra Cross-platform UI Development | Tech Talk](https://www.bilibili.com/video/BV1mT4y167Fm/?spm_id_from=333.999.0.0&vd_source=d47c0bb42f9c72fd7d74562185cee290)
# CommonUI
- 数据表
- CommonInputActionDataBase各平台输入绑定。
- 蓝图类资产
- CommonInputBaseControllerData各平台按键数据图标指定、鼠标键盘/手柄按键绑定)。在`ProjectSettings - Game - CommonInputSettings - PlatformInput - XX - Default - ControllerData`中指定。
- CommonUIInputDataUI Click/Back Action 绑定用数据。之后在`ProjectSettings - Game - CommonInputSettings - InputData - `中指定。
- Comfirm/Cancel DataTable需要填写CommonInputActionDataBase数据表。
## CommonUI特有功能
1. ⭐⭐**自带过渡效果**在Stack控件的`Detail-Transition`中可以设置控件激活/注销时过渡效果,同时还可以设置效果曲线类型与曲线数值。默认是**渐隐(Fade Only)**。
2. ⭐⭐⭐**CommonActivatableWidgetStack**会自动激活Stack顶部Widget。
3. ⭐BackHandle后退动作处理除了勾选BackHandle与IsModel之外需要还需重写OnHandleBackAction()
## CommonUI Setup&Data
1. 启用CommonUI插件。
2. `Engine - GeneralSettings - DefaultClasses - GameViewportClientClass`将其指定为**CommonGameViewportClient**或其子类。
3. 创建一个基于CommonInputActionDataBase的数据表并且创建按键事件映射表。
4. 创建新的按钮类继承自`CommonButtonbase并在TriggeringInputAction应用数据表
5. 创建一个继承自`CommonActivatbleWidget`的界面并设置为自动激活,并将按钮放入其中。
1. 响应按键映射的前提是自己或某一Parent为已经激活的`CommonActivatbleWidget`
### CommonInputActionDataBase
默认的数据表可以填写**键盘**、**手柄**、**平板**的输入事件表。但对于其他主机平台(PS、Xbox、Switch)往往需要调换手柄按键位置,这个时候可以通过**GamepadInputOverrides**对指定平台进行按键修改。需要**安装主机平台SDK**后才会显示对应选项。
### CommonBaseControllerData
用于指定对应平台的**图标**资源。之后在`ProjectSettings - Game - CommonInputSettings - PlatformInput - XX - Default - ControllerData`中指定。
### CommonUIInputData
UI Click/Back Action 绑定用数据。之后在`ProjectSettings - Game - CommonInputSettings - InputData - `中指定。
## CommonStyle
与UMG、Slate不同CommonUI没有通过**SlateWidgetStyle**而是通过继承对应的类来创建。每个CommonUI空间都有一个对应的Style类。之后可以在`ProjectSettings - Plugins - CommonUIEditor`的**TemplateTextStyle**、**TemplateButtonStyle**、**TemplateBorderStyle**指定CommonUI的默认风格。
## CommonUI控件
### UCommonUserWidget
CommonUI控件基类主要定义了
1. `TArray<FUIActionBindingHandle> ActionBindings`来管理控件的ActionBindings。
2. `TArray<TWeakObjectPtr<const UWidget>> ScrollRecipients`:滚动条控件。
3. `bool bConsumePointerInput`鼠标滑过控件时是否会产生反应。为true时返回FReply::Handled()
其他都是相关虚函数以及工具函数。
### CommonActivatableWidget
大致翻译官方注释的话:
CommonActivatableWidget的基础是能够在其生命周期内被 "**activated** "和 "**deactivated**"而不被其他方式修改或销毁。通常是为了以下几个目的:
1. 该控件可以自由打开或者关闭同时不会被从层次结构WidgetTree中移除。
2. 能从其他控件“退回”到这个控件例如使用SwitchWidget或者StackWidget
3. 这个部件在层次结构中的位置是这样的:它在可激活的部件树中定义了一个有意义的结点,通过这个结点,输入被路由到所有部件。(不太懂这条,应该是不会破坏输入路由的意思)
同时它还具备以下性质:
1. 创建后不会自动激活,需要调用函数手动**activated**。
2. 默认情况下不会注册BackAction。
3. 如果注册了BackAction那会在执行BackAction后自动**deactivated**。
主要实现了"**activated** "和 "**deactivated**"的相关逻辑,主要是可见性以及输入路由。
### UCommonActivatableWidgetContainerBase
添加的控件使用`FUserWidgetPool GeneratedWidgetsPool`控件池进行创建与生命周期管理。`TArray<TObjectPtr<UCommonActivatableWidget>> WidgetList`存储添加的控件指针。
内部使用SOverlay来显示SCommonAnimatedSwitcher用于显示控件与SSpacer。
### CommonButtonBase
与其他的CommonUI组件不同需要手动实现内部构造。
与普通的Button相比拥有更好的可定制性比如
- 其Style增加了设置各种事件发出声音的选项。
- 拥有一个**Selectable**选项可以用来实现CheckBox或者多选框。
### 其他控件
- CommonActionWidget继承自UWidget用来显示指定游戏平台对应键盘/手柄按键图标。需要注意ProjectSettings-Game-CommonInputSettings中的DefaultGamepadName需要与CommonInputBaseControllerData资产的GamepadName相同否则无法显示对应的按键图标。
- CommonActivatableWidgetSwitcher继承自UCommonAnimatedSwitcher重写几个虚函数来管理内部CommonActivatableWidget的 "**activated** "和 "**deactivated**"。
- CommonAnimatedSwitcher继承自UWidgetSwitcher主要实现了过渡动画具体在Slate控件里以及OnActiveWidgetIndexChanged、OnTransitioningChanged委托。
- CommonActivaableWidgetStack继承自UCommonActivatableWidgetContainerBase重写OnWidgetAddedToList(),添加控件时并显示末尾控件。
- CommonActivaableWidgetQueue继承自UCommonActivatableWidgetContainerBase重写OnWidgetAddedToList(),添加控件时并显示第一个控件。
- CommonBorder继承UBorder主要适配了CommonStyle。
- CommonHardwareVisibilityBorder继承CommonBorder重写OnWidgetRebuilt()根据CommonInputSubsystemULocalPlayerSubsystem的ComputedVisibilityTags来决定是否显示。
- CommonCustomNavigation继承UBorder暴露一个FOnCustomNavigationEvent委托用于定义自定义的Navigation行为。
- CommonTextBlock继承自UTextBlock主要适配了CommonStyle并且添加了TextScroll。
- CommonDateTimeTextBlock继承自UCommonTextBlock主要存储了时间FDateTime、int32、float)并对文字进行格式化位于UpdateUnderlyingText()提供了若干DateTime相关的方法。
- CommonNumericTextBlock继承自UCommonTextBlock实现了Number、Percentage、Seconds、Distance相关数字文字的格式化方法。
- CommonLazyImage继承自UImage使用异步加载Image或者Material并在加载完之后替换图片。控制载入的函数为SetBrushFromLazyMaterial()、SetBrushFromLazyDisplayAsset()在加载完成前显示默认图片。本质是使用UAssetManager::GetStreamableManager().RequestAsyncLoad()进行加载。其中MyLoadGuardSLoadGuard为旋转载入图标控件。
- CommonLazyWidget继承自UWidget默认Visibility为SelfHitTestInvisible。控制载入的函数为SetLazyContent()本质是使用UAssetManager::GetStreamableManager().RequestAsyncLoad()进行加载。在RebuildWidget()与OnStreamingComplete()中设置MyLoadGuard与Content的可见性。
- CommonListVIew继承自UListView主要是重写OnGenerateEntryWidgetInternal()针对UCommonButtonBase进行适配内部使用SCommonListView控件。
- CommonLoadGuard继承自UContentWidget维护一个MyLoadGuard(SLoadGuard)。并且暴露样式、委托以及SetLoadingText()、SetIsLoading()、IsLoading()。
- CommonVisibilitySwitcher继承自UOverlay一个适配了UCommonActivatableWidget的基础控件切换器。控件存在`TArray<TObjectPtr<UPanelSlot>> Slots;`
- CommonBoundActionBar继承自UDynamicEntryBoxBase适配CommonUI输入控制的版本。主要在OnWidgetRebuilt()中调用MonitorPlayerActions()绑定CommonUI输入路由的OnBoundActionsUpdated委托。
- CommonWidgetCarousel继承自UPanelWidget卡片旋转式的WidgetSwitcher。
- CommonWidgetCarouselNavBar继承自UWidget控制CommonWidgetCarousel控件用的导航栏。
- List
- CommonTileView继承自UTileView替换了内部使用的Slate控件为SCommonTileView。并且重写OnGenerateEntryWidgetInternal()对UCommonButtonBase进行适配替换成SCommonButtonTableRow
- CommonTreeView继承自UTreeView替换了内部使用的Slate控件为SCommonTreeView。并且重写OnGenerateEntryWidgetInternal()对UCommonButtonBase进行适配替换成SCommonButtonTableRow
- Pannel
- CommonHierarchicalScrollBox继承自UScrollBox替换了内部使用的Slate控件为SCommonHierarchicalScrollBox。
- CommonVisualAttachment继承自USizeBox使用Slate控件为SVisualAttachmentBox。添加一个Widget作为另一个Widget的零尺寸附件。其设计目的为不改变Widget的尺寸计算。
# 输入路由相关
## UCommonGameViewportClient
主要重写InputKey()、InputAxis()、InputTouch()之后通过HandleRerouteInput() 、HandleRerouteAxis()、HandleRerouteTouch()进行输入路由判断。其路由逻辑主要编写在
UCommonUIActionRouterBase是一个ULocalPlayerSubsystem子类。
## UCommonUIActionRouterBase
主要用于处理注册/卸载ActionBindings、维护ActivateTree以及输入控制。
## Input Binding & Process
### Process
以ProcessInput()为例,其关键代码为:
```c++
PersistentActions->ProcessHoldInput(ActiveMode, Key, InputEvent);
ActiveRootNode->ProcessHoldInput(ActiveMode, Key, InputEvent);
PersistentActions->ProcessNormalInput(ActiveMode, Key, Event);
ActiveRootNode->ProcessNormalInput(ActiveMode, Key, Event);
```
在对各种结果**依次**进行判断处理之后最终返回结果ERouteUIInputResult枚举。
```c++
bool FActionRouterBindingCollection::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
{
for (FUIActionBindingHandle BindingHandle : ActionBindings)
{
if (TSharedPtr<FUIActionBinding> Binding = FUIActionBinding::FindBinding(BindingHandle))
{
if (ActiveInputMode == ECommonInputMode::All || ActiveInputMode == Binding->InputMode)
{
for (const FUIActionKeyMapping& KeyMapping : Binding->NormalMappings)
{
// A persistent displayed action skips the normal rules for reachability, since it'll always appear in a bound action bar
const bool bIsDisplayedPersistentAction = Binding->bIsPersistent && Binding->bDisplayInActionBar;
if (KeyMapping.Key == Key && Binding->InputEvent == InputEvent && (bIsDisplayedPersistentAction || IsWidgetReachableForInput(Binding->BoundWidget.Get())))
{
// Just in case this was in the middle of a hold process with a different key, reset now
Binding->CancelHold();
Binding->OnExecuteAction.ExecuteIfBound();
if (Binding->bConsumesInput)
{
return true;
}
}
}
}
}
}
return false;
}
bool FActivatableTreeNode::ProcessNormalInput(ECommonInputMode ActiveInputMode, FKey Key, EInputEvent InputEvent) const
{
if (IsReceivingInput())
{
for (const FActivatableTreeNodeRef& ChildNode : Children)
{
if (ChildNode->ProcessNormalInput(ActiveInputMode, Key, InputEvent))
{
return true;
}
}
return FActionRouterBindingCollection::ProcessNormalInput(ActiveInputMode, Key, InputEvent);
}
return false;
}
```
### Binding
可以看得出主要是ActionBindings而其注册逻辑位于UCommonUIActionRouterBase::Tick()=>ProcessRebuiltWidgets=>RegisterWidgetBindings()之后依次对构建的FActivatableTree中**CommonUserWidget**的绑定数据进行注册RegisterWidgetBindings、RegisterInputTreeNode。每个**CommonUserWidget**都使用这个RegisterUIActionBinding()来注册绑定信息。
```c++
FUIActionBindingHandle UCommonUserWidget::RegisterUIActionBinding(const FBindUIActionArgs& BindActionArgs)
{
if (UCommonUIActionRouterBase* ActionRouter = UCommonUIActionRouterBase::Get(*this))
{
FBindUIActionArgs FinalBindActionArgs = BindActionArgs;
if (bDisplayInActionBar && !BindActionArgs.bDisplayInActionBar)
{
FinalBindActionArgs.bDisplayInActionBar = true;
}
FUIActionBindingHandle BindingHandle = ActionRouter->RegisterUIActionBinding(*this, FinalBindActionArgs);
ActionBindings.Add(BindingHandle);
return BindingHandle;
}
return FUIActionBindingHandle();
}
```
其绑定参数`FBindUIActionArgs`数据为:
| | |
| ---------------------- | ----------------------------------------------------------------- |
| ActionTag | 目前没找到用处可能用于按下时给角色添加按下的Tag |
| LegacyActionTableRow | 按键的映射表行 |
| InputMode | 输入模式只有当当前输入模式覆盖了Key的输入模式才触发事件,默认all |
| KeyEvent | 什么状态下触发(按下、松开、etc...) |
| bIsPersistent | 持久化为true则无视Activate规则始终响应按键输入 |
| bConsumeInput | 是否消耗掉本次输入(相同按键绑定不再能触发)默认为true |
| bDisplayInActionBar | 是否在CommonActionBar上实时显示按键提示 |
| OverrideDisplayName | 新的显示名 |
| OnExecuteAction | 当按键触发时执行的回调函数 |
| OnHoldActionProgressed | 当按住时的回调 |
# Lyra中的用法
- Safe-Zone
创建了若干控件:
- MaterialProgressBar继承自UCommonUserWidget
- LyraBoundActionButton继承自UCommonBoundActionButton

View File

@@ -0,0 +1,40 @@
---
title: STreeView与STableRow
date: 2022-08-09 13:55:15
tags: Slate
rating: ⭐️
---
# STreeView
参考网址:[UE4 STreeView的使用和示例(C++实现)](https://blog.csdn.net/wymdhr/article/details/104313596)
- RequestTreeRefresh 更新树形结构
## TreeView鼠标多选功能
代码位于STableRow.h的OnMouseButtonDown()。它作用是按住Ctrl 或者Shift时按下方向键可以多选物体。
### DragDrop机制
STreeView的拖拽事件会先在STableRow相应
- OnDragDetected():检测到拖拽事件。
- OnDragEnter():鼠标(拖拽状态)进入。
- OnDragLeave():鼠标(拖拽状态)离开。
- OnDragOver():鼠标拖拽物体过来。
- OnDrop拖拽结束。
拖拽操作类都继承自FDragDropOperation并调用类中的New函数来填充数据。并在对应的Event中获取到数据。
```c++
TSharedPtr<FDragAIGraphNode> DragNodeOp = DragDropEvent.GetOperationAs<FDragAIGraphNode>();
const TArray< TSharedRef<SGraphNode> >& DraggedNodes = DragNodeOp->GetNodes();
for (int32 Idx = 0; Idx < DraggedNodes.Num(); Idx++)
{
UAIGraphNode* DraggedNode = Cast<UAIGraphNode>(DraggedNodes[Idx]->GetNodeObj());
}
```
DragDrop不适用于多选的实现所以可以使用使用给Event绑定Lambda之后将节点实例传入进行操作。
### SListView的选择机制
STreeView的选择实现代码位于SListView声明于ITypedTableView.h几个主要的接口函数为
- Private_IsItemSelected()
- Private_IsItemSelectableOrNavigable()
- Private_SetItemSelection()
- Private_ClearSelection()
- Private_SelectRangeFromCurrentTo()

View File

@@ -0,0 +1,199 @@
---
title: Slate学习笔记Slate动态控制与其他技巧
date: 2021-02-19 9:22:10       
tags: Slate
rating: ⭐️
---
## 前言
最近在写MessageBox控件想兼容UMG与c++中调用Slate。略有心得遂有此文。
## 动态Slate
首先Slate控件树中创建占位用的NullWidget并使用.Expose()绑定Slot指针使得可以进行后续操作。
```c++
ChildSlot
.VAlign(VAlign_Fill)
.HAlign(HAlign_Fill)
[
SNew(SOverlay)
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.Expose(UseMenuSlot) // Expose it to a pointer so we can change the widget at runtime
[
SNullWidget::NullWidget
]
];
```
之后就可以通过函数对Slot进行操作了下面演示如何动态添加或者删除Button
```c++
UseMenuSlot->AttachWidget(
InText.Get().IsEmpty() ?
SNullWidget::NullWidget
:
SNew(SButton)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
[
SNew(STextBlock)
]
);
```
### 支持Expose()的容器
常用的:
SHorizontalBox
SVerticalBox
SOverlay
其他还有若干也支持的
## 设置指定长度的Widget
有的时候需要让控件保持指定的大小使用SBox的WidthOverride与HeightOverride即可。其他的控件都不支持这2个属性。
```c++
ChildSlot
[
SNew(SBox)
.WidthOverride(256)
.HeightOverride(100)
[
SNew(SButton)
]
];
```
## 指定大小控件的UMG与Slate兼容问题
UMG的根节点是一个CanvasPanel(UCanvasPanelSlot)。我认为直接给控件加一个SBox来控制大小的方式不太好。这会使得在UMG中的调整大小变得麻烦。所以我的方法是使用c++调用Slate控件时在外面再套一层SConstraintCanvas。
UCanvasPanelSlot本质上是SConstraintCanvas。UMG编辑器中显示的Size实际上是Offset。所以想要指定大小就需要设置Offset的Right与Bottom。
```c++
CurrentWidget = SNew(SConstraintCanvas)
+ SConstraintCanvas::Slot()
.Anchors(0.5)
.Alignment(FVector2D(0.5,0.5))
.Offset(FMargin(0,0,500,400))
[
SNew(SCustomWidget)
];
GetGameInstance()->GetGameViewportClient()->AddViewportWidgetContent(CurrentWidget.ToSharedRef());
```
## c++获取UMG控件
```c++
UPROPERTY(Meta = (BindWidget)) //直接获取蓝图中的按钮1
UButton* ButtonOne;
```
```c++
bool UFWAffectWidget::Initialize()
{
if (!Super::Initialize()) return false;
//方法1
RootPanel = Cast<UCanvasPanel>(GetRootWidget()); //获取根控件
if (RootPanel)
{
BGImage = Cast<UImage>(RootPanel->GetChildAt(0)); //下标1的BGImage控件
}
//方法2
UButton* ButtonTwo = (UButton*)GetWidgetFromName(TEXT("ButtonTwo"));
//绑定按钮事件方法1
ButtonOne->OnClicked.__Internal_AddDynamic(this,&UFWAffectWidget::ButtonOneEvent,FName("ButtonOneEvent"));
//绑定事件方法2委托FScriptDelegate
FScriptDelegate ButTwoDel;
ButTwoDel.BindUFunction(this, "ButtonTwoEvent");
ButtonTwo->OnReleased.Add(ButTwoDel);
return true;
}
```
参考文章https://blog.csdn.net/weixin_44200074/article/details/109100521
## 从UMG控件中获取Slate
```c++
TSharedRef<SWidget> UWidget::TakeWidget()
```
## 反射
### 遍历UProperty
```c++
for(TFiledIterator<UProperty> It(/*UClass**/)It;++It)
{
UProperty *newProperty=It;
}
```
### 根据名称寻找UProperty
```c++
UProperty *foundProperty = FindField<UProperty>(/*UClass**/,TEXT("PropertyName"));
```
### Get/Set UProperty
```c++
//根据PropertyPath获取属性值
FCachedPropertyPath PropertyPath(FString(TEXT("PropertyName")));
T Value;
ProperthPathHelpers::GetPropertyValue(Object,PropertyPath,Value);
//有获取就有设置
ProperthPathHelpers::SetPropertyValue(Object,PropertyPath,Value);
//还可以获取成String
FString PropertyValueStr;
ProperthPathHelpers::GetPropertyValueAsString(Object,PropertyPath,PropertyValueStr);
//同样,反向也可以
ProperthPathHelpers::SetPropertyValueFromString(Object,PropertyPath,PropertyValueStr);
//实际上我们平时在UE引擎中复制粘贴属性值就是将属性值获取为String以及从String这只属性值的整个过程。
```
引用文章https://zhuanlan.zhihu.com/p/121006155
更多反射技巧https://bebylon.dev/ue4guide/engine-programming/uobject-reflection/uobject-reflection/
## SCompoundWidget、SPanel、SLeafWidget的区别
### SCompoundWidget
SCompoundWidget对应于UMG中的“WidgetBlueprint控件蓝图用来作为控件容器。当我们在C++类向导中创建Slate类时创建的就是SCompoundWidget。
可以通过成员ChildSlot结合重载的[]操作符往控件里面添加其他控件
```c++
void SStandardSlateWidget::Construct(const FArguments& InArgs)
{
ChildSlot
[
SNew(STextBlock)
.Font(FSlateFontInfo("Veranda", 100))
.Text(NSLOCTEXT("HelloSlate", "HelloSlateText", "Hello, Slate!"))
];
}
```
### SPanel
即为SPanel子布局的属性包括Padding、Size、Horizontal Align、Vertical Align。
使用Slot添加子控件可以使用“+ Slot()”也可以调用AddSlot函数还是以教程1中的SStandardSlateWidget为例
```c++
void SStandardSlateWidget::Construct(const FArguments& InArgs)
{
ChildSlot
[
SAssignNew(VerticalBoxPtr, SVerticalBox)
+ SVerticalBox::Slot()
.Padding(1.0)
.FillHeight(0.3f)
.HAlign(HAlign_Fill)
.VAlign(VAlign_Top)
[
SNew(SButton)
]
+ SVerticalBox::Slot()
.FillHeight(0.5f)
.HAlign(HAlign_Center)
[
SNew(SButton)
]
];
VerticalBoxPtr->AddSlot()
.FillHeight(0.2f)
[
SNew(SButton)
];
}
```
### SLeafWidget
叶子控件故名思意该控件不能添加子控件。常用的为STextBlock与SImage。

View File

@@ -0,0 +1,255 @@
---
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. 混合使用蓝图上使用UMGc++使用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对应的SlateUMG本质就是使用UWidget包裹的SlateUUserWiget也能正常销毁理论是可以的。但很遗憾还在存在几个问题
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);
}
}
}
```

View File

@@ -0,0 +1,12 @@
---
title: UMG组件作用笔记
date: 2022-11-14 18:13:13
excerpt:
tags:
rating: ⭐
---
- ScaleBox可以根据ParentWidget的大小来对Children进行缩放。
- SizeBox一个方便控制Size的控件有OverrideSize、MinDesiredSize、MaxDesiredSize。
- Overlay可以让多个UMG控件叠在一起拥有同一个锚点可以认为是一个组合功能。
- RetainerBox优化用的控件可以调整UI的刷新频率根据帧数。

View File

@@ -0,0 +1,209 @@
---
title: 扩展UProgressBar以实现多重进度条控件
date: 2020-02-20 16:42:36       
tags: Slate
rating: ⭐️
---
## 前言
因为本人想制作一个类似血源诅咒Demo的关系所以需要实现一个类似的进度条。它的不同之处在于血量的进度条会显示实际血量与下次攻击后的最大回复血量。也就是一个进度条显示两个量。
UProgressBar为UMG进度条空间它本质上对Slate组件SProgressBar的封装。负责绑定数据、UI更新。所以我们应该先了解SProgressBar的绘制过程。
## OnPaint
Slate控件的绘制过程在OnPaint函数。查看代码后发现它就是在画一个个盒子并进行剪裁
```
int32 SCustomProgressBar::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
// Used to track the layer ID we will return.
int32 RetLayerId = LayerId;
//获取各种数据与资源
bool bEnabled = ShouldBeEnabled( bParentEnabled );
const ESlateDrawEffect DrawEffects = bEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect;
const FSlateBrush* CurrentFillImage = GetFillImage();
const FLinearColor FillColorAndOpacitySRGB(InWidgetStyle.GetColorAndOpacityTint() * FillColorAndOpacity.Get().GetColor(InWidgetStyle) * CurrentFillImage->GetTint(InWidgetStyle));
const FLinearColor ColorAndOpacitySRGB = InWidgetStyle.GetColorAndOpacityTint();
TOptional<float> ProgressFraction = Percent.Get();
FVector2D BorderPaddingRef = BorderPadding.Get();
const FSlateBrush* CurrentBackgroundImage = GetBackgroundImage();
//绘制底层背景
FSlateDrawElement::MakeBox(
OutDrawElements,
RetLayerId++,
AllottedGeometry.ToPaintGeometry(),
CurrentBackgroundImage,
DrawEffects,
InWidgetStyle.GetColorAndOpacityTint() * CurrentBackgroundImage->GetTint( InWidgetStyle )
);
if( ProgressFraction.IsSet() )
{
ECutomProgressBarFillType::Type ComputedBarFillType = BarFillType;
if (GSlateFlowDirection == EFlowDirection::RightToLeft)
{
switch (ComputedBarFillType)
{
case ECutomProgressBarFillType::LeftToRight:
ComputedBarFillType = ECutomProgressBarFillType::RightToLeft;
break;
case ECutomProgressBarFillType::RightToLeft:
ComputedBarFillType = ECutomProgressBarFillType::LeftToRight;
break;
}
}
//以下是进度条内部色块的绘制过程
const float ClampedFraction = FMath::Clamp(ProgressFraction.GetValue(), 0.0f, 1.0f);
switch (ComputedBarFillType)
{
...
//略去部分代码
case ECutomProgressBarFillType::LeftToRight:
default:
{
if (PushTransformedClip(OutDrawElements, AllottedGeometry, BorderPaddingRef, FVector2D(0, 0), FSlateRect(0, 0, ClampedFraction, 1)))
{
// Draw Fill
FSlateDrawElement::MakeBox(
OutDrawElements,
RetLayerId++,
AllottedGeometry.ToPaintGeometry(
FVector2D::ZeroVector,
FVector2D( AllottedGeometry.GetLocalSize().X, AllottedGeometry.GetLocalSize().Y )),
CurrentFillImage,
DrawEffects,
FillColorAndOpacitySRGB
);
OutDrawElements.PopClip();
}
break;
}
//略去部分代码
...
}
return RetLayerId - 1;
}
```
## 在UMG中绑定Tarray类型数据并传递给Slate
在开发过程中Slate部分很顺利。但在UMG的SynchronizeProperties函数中在对Tarray类型的数据进行绑定并传递给Slate控件这一步遇到了问题。
也就是使用OPTIONAL_BINDING_CONVERT与PROPERTY_BINDING宏绑定数据时遇到了问题。因为一开始没有搞明白TAttribute与TOptional是啥玩意。
TAttribute为Slate包装属性用的模板类TOptional是类似c++17 std::optional的模板类。
使用这两个宏还需要声明一些函数与委托。下面将一一介绍:
### OPTIONAL_BINDING_CONVERT
```
TAttribute<TOptional<TArray<float> >> PercentBinding=OPTIONAL_BINDING_CONVERT(TArray<float>, PercentArray, TOptional<TArray<float>>, ConvertFloatToOptionalFloatArray);
```
因为本人使用的变量名为PercentArray类型为TArray<float>所以需要声明返回TArray<float>类型的委托。
```
DECLARE_DYNAMIC_DELEGATE_RetVal(TArray<float>, FGetFloatArray);
```
并且委托名为PercentArrayDelegate。也就是变量名+“Delegate”
```
UPROPERTY()
FGetFloatArray PercentArrayDelegate;
```
另外需要实现函数ConvertFloatToOptionalFloatArray
```
TOptional<TArray<float>> ConvertFloatToOptionalFloatArray(TAttribute<TArray<float>> InFloatArray) const
{
return InFloatArray.Get();
}
```
### PROPERTY_BINDING
```
TAttribute<TArray<FSlateColor>> FillColorAndOpacityBinding = PROPERTY_BINDING(TArray<FSlateColor>, FillColorAndOpacityArray);
```
因为本人使用的变量名为FillColorAndOpacityArray类型为TArray<FSlateColor>但需要声明的委托为返回TArray<FLinearColor>类型。
```
DECLARE_DYNAMIC_DELEGATE_RetVal(TArray<FLinearColor>, FGetLinearColorArray);
```
并且委托名为FillColorAndOpacityArrayDelegate。也就是变量名+“Delegate”
```
UPROPERTY()
FGetLinearColorArray FillColorAndOpacityArrayDelegate;
```
另外还需要在类内使用PROPERTY_BINDING_IMPLEMENTATION宏
```
PROPERTY_BINDING_IMPLEMENTATION(TArray<FSlateColor>, FillColorAndOpacityArray);
```
## 其他主要操作
向Slate传递数据
```
void UMultipleProgressBar::SynchronizeProperties()
{
Super::SynchronizeProperties();
TAttribute< TOptional<TArray<float> >> PercentBinding=OPTIONAL_BINDING_CONVERT(TArray<float>, PercentArray, TOptional<TArray<float>>, ConvertFloatToOptionalFloatArray);
TAttribute<TArray<FSlateColor>> FillColorAndOpacityBinding = PROPERTY_BINDING(TArray<FSlateColor>, FillColorAndOpacityArray);
MyProgressBar->SetStyle(&WidgetStyle);
MyProgressBar->SetBarFillType(BarFillType);
MyProgressBar->SetBorderPadding(BorderPadding);
MyProgressBar->SetPercentArray(PercentBinding);
MyProgressBar->SetFillColorAndOpacityArray(FillColorAndOpacityBinding);
}
```
实现数据设置函数
```
UMultipleProgressBar::SetPercentArray
UMultipleProgressBar::SetFillColorAndOpacityArray
SMultipleProgressBar::SetPercentArray
SMultipleProgressBar::SetFillColorAndOpacityArray
SMultipleProgressBar::Construct
```
OnPaint中的绘制逻辑
```
int32 SMultipleProgressBar::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
{
...
//略去部分代码
//从传递进来的数据中取得Percent与Color数组
TArray<float> PercentArrayList = PercentArray.Get().GetValue();
TArray<FSlateColor> FillColorAndOpacityArrayList = FillColorAndOpacityArray.Get();
while (FillColorAndOpacityArrayList.Num() < PercentArrayList.Num())
{
FillColorAndOpacityArrayList.Add(FSlateColor());
}
for (int i = 0; i < PercentArrayList.Num(); i++)
{
//在循环中获取每个Percent与color最后进行绘制
const float ClampedFraction = FMath::Clamp(PercentArrayList[i], 0.0f, 1.0f);
const FLinearColor FillColorAndOpacitySRGB = (InWidgetStyle.GetColorAndOpacityTint() * FillColorAndOpacityArrayList[i].GetColor(InWidgetStyle) * CurrentFillImage->GetTint(InWidgetStyle));
switch (ComputedBarFillType)
{
case EMultipleProgressBarFillType::RightToLeft:
...
case EMultipleProgressBarFillType::FillFromCenter:
...
case EMultipleProgressBarFillType::TopToBottom:
...
case EMultipleProgressBarFillType::BottomToTop:
...
case EMultipleProgressBarFillType::LeftToRight:
default:
...
}
}
//略去部分代码
...
```
## 结语
具体操作的可以参考我的写的插件https://github.com/blueroseslol/BRPlugins
感觉有帮助的请给我的项目Star。