细节面板的变量绑定逻辑
很久之前在细节面板实现过一个变量编辑的逻辑,基于 FInstancedPropertyBag 和 FInstancedStruct 实现了在细节面板的一些计算逻辑,功能为先,制作时大多是通过 IPropertyTypeCustomization 来完成的,反射的细节面板并不是特别漂亮,虽然运算时提供了筛选,但左值的选择不太直观,只能通过命名来确定,因此最近考虑研究下 UE 里的细节面板的变量绑定的逻辑是如何实现的,考虑把样式部分搞过来参考下。
写在前面:
UE 里这一块的功能都很特异化,不是很易于拓展,网上也几乎没有相关拓展的资料。
若要完成自定义的功能,能够复用的大抵只有一个 UI,核心的绑定逻辑都是需要自己来实现的(除非逻辑很一致,可以参考
FSmartObjectDefinitionBindingExtension
和FStateTreeBindingExtension
)。理论上目前有了 FInstancedPropertyBag,可以考虑提供一个方便拓展的方案,后续笔者凑点时间的话可能会考虑提供一个。
UMG 绑定流程分析
系统中有很多面板存在绑定逻辑,例如 UMG、动画蓝图、Control Rig、StateTree 等,笔者这里以 UMG 为例。
UI 创建
大多变量绑定的 UI 都是由继承 IDetailPropertyExtensionHandler 来实现的,以针对单行来自定义 (如 FStateTreeBindingExtension
, FAnimGraphNodeBindingExtension
等)。UMG 这里是基于细节面板的自定义拓展的,但最终逻辑都是一致的:
class FBlueprintWidgetCustomization : public IDetailCustomization
{
...
/** IDetailCustomization interface */
virtual void CustomizeDetails( IDetailLayoutBuilder& DetailLayout ) override;
/** Make a property binding widget */
static TSharedRef<SWidget> MakePropertyBindingWidget(TWeakPtr<FWidgetBlueprintEditor> InEditor, UFunction* SignatureFunction, TSharedRef<IPropertyHandle> InDelegatePropertyHandle, bool bInGeneratePureBindings, bool bAllowDetailsPanelLegacyBinding);
...
void CreateEventCustomization( IDetailLayoutBuilder& DetailLayout, FDelegateProperty* Property, UWidget* Widget );
...
};
UI 构建的逻辑在 MakePropertyBindingWidget 方法里,核心就是 FPropertyBindingWidgetArgs 参数的构建,这里参数构建与业务密切相关,一会逐个详细解析,先整体捋一捋 UMG 绑定这一块的执行流程:
TSharedRef<SWidget> FBlueprintWidgetCustomization::MakePropertyBindingWidget(TWeakPtr<FWidgetBlueprintEditor> InEditor, UFunction* SignatureFunction, TSharedRef<IPropertyHandle> InPropertyHandle, bool bInGeneratePureBindings, bool bAllowDetailsPanelLegacyBinding)
{
...
FPropertyBindingWidgetArgs Args;
Args.MenuExtender = FExtender::Combine(MenuExtenders);
Args.Property = InPropertyHandle->GetProperty();
Args.BindableSignature = SignatureFunction;
Args.BindButtonStyle = &FCoreStyle::Get().GetWidgetStyle<FButtonStyle>("Button");
...
Args.OnAddBinding = FOnAddBinding::CreateLambda([InEditor, Objects](FName InPropertyName, const TArray<FBindingChainElement>& InBindingChain)
Args.OnRemoveBinding = FOnRemoveBinding::CreateLambda([InEditor, Objects, InPropertyHandle, ActiveExtensions](FName InPropertyName)
...
IPropertyAccessEditor& PropertyAccessEditor = IModularFeatures::Get().GetModularFeature<IPropertyAccessEditor>("PropertyAccessEditor");
return PropertyAccessEditor.MakePropertyBindingWidget(InEditor.Pin()->GetBlueprintObj(), Args);
}
参数构建完成后,使用 IPropertyAccessEditor::MakePropertyBindingWidget 方法传入构建的参数即可完成 PropertyBinding SWidget 的构建,UMG 这里将返回的 SWidget 赋值到了 PropertyRow 的 ValueWidget 里,进而完成了细节面板反射 Value 部分的自定义:
void FBlueprintWidgetCustomization::CreateEventCustomization( IDetailLayoutBuilder& DetailLayout, FDelegateProperty* Property, UWidget* Widget )
{
TSharedRef<IPropertyHandle> DelegatePropertyHandle = DetailLayout.GetProperty(Property->GetFName(), Property->GetOwnerChecked<UClass>());
UWidgetBlueprint* BlueprintObj = Blueprint.Get();
if (!BlueprintObj)
{
return;
}
const bool bHasValidHandle = DelegatePropertyHandle->IsValidHandle();
if(!bHasValidHandle)
{
return;
}
IDetailCategoryBuilder& PropertyCategory = DetailLayout.EditCategory(FObjectEditorUtils::GetCategoryFName(Property), FText::GetEmpty(), ECategoryPriority::Uncommon);
IDetailPropertyRow& PropertyRow = PropertyCategory.AddProperty(DelegatePropertyHandle);
PropertyRow.OverrideResetToDefault(FResetToDefaultOverride::Create(FResetToDefaultHandler::CreateSP(this, &FBlueprintWidgetCustomization::ResetToDefault_RemoveBinding)));
FString LabelStr = Property->GetDisplayNameText().ToString();
LabelStr.RemoveFromEnd(TEXT("Event"));
FText Label = FText::FromString(LabelStr);
const bool bShowChildren = true;
PropertyRow.CustomWidget(bShowChildren)
.NameContent()
[
SNew(SHorizontalBox)
.ToolTipText(Property->GetToolTipText())
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(0,0,5,0)
[
SNew(SImage)
.Image(FAppStyle::Get().GetBrush("GraphEditor.Event_16x"))
]
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(Label)
]
]
.ValueContent()
.MinDesiredWidth(200)
.MaxDesiredWidth(250)
[
MakePropertyBindingWidget(Editor.Pin(), Property->SignatureFunction, DelegatePropertyHandle, false, true)
];
}
Args 的构建
FPropertyBindingWidgetArgs Args 参数的构建需要我们创建一系列的代理、参数,以供 S 类的 UI 展示使用及交互时回调,
我们构建的参数最终会传递到 SPropertyBinding 的 成员 Args 中:
void SPropertyBinding::Construct(const FArguments& InArgs, UBlueprint* InBlueprint, const TArray<FBindingContextStruct>& InBindingContextStructs)
{
Blueprint = InBlueprint;
BindingContextStructs = InBindingContextStructs;
Args = InArgs._Args;
...
在 UI 交互时,会借由 Args 转发并调用我们创建的代理来执行相关逻辑,例如:
void SPropertyBinding::HandleAddBinding(TArray<TSharedPtr<FBindingChainElement>> InBindingChain)
{
if(Args.OnAddBinding.IsBound())
{
const FScopedTransaction Transaction(LOCTEXT("BindDelegate", "Set Binding"));
TArray<FBindingChainElement> BindingChain;
BindingChain.Reserve(InBindingChain.Num());
Algo::Transform(InBindingChain, BindingChain, [](TSharedPtr<FBindingChainElement> InElement)
{
return *InElement.Get();
});
Args.OnAddBinding.Execute(PropertyName, BindingChain);
}
}
或 TAttribute 类的:
FSlateColor SPropertyBinding::GetCurrentBindingColor() const
{
if(Args.CurrentBindingColor.IsSet())
{
return Args.CurrentBindingColor.Get();
}
return FLinearColor(0.25f, 0.25f, 0.25f);
}
因此核心的逻辑就是构建好这些代理的回调方法。
我们先以 UMG 中的 OnAddBinding 为例:
Args.OnAddBinding = FOnAddBinding::CreateLambda([InEditor, Objects](FName InPropertyName, const TArray<FBindingChainElement>& InBindingChain)
{
UWidgetBlueprint* ThisBlueprint = InEditor.Pin()->GetWidgetBlueprintObj();
UBlueprintGeneratedClass* SkeletonClass = Cast<UBlueprintGeneratedClass>(ThisBlueprint->SkeletonGeneratedClass);
ThisBlueprint->Modify();
TArray<FFieldVariant> FieldChain;
Algo::Transform(InBindingChain, FieldChain, [](const FBindingChainElement& InElement)
{
return InElement.Field;
});
UFunction* Function = FieldChain.Last().Get<UFunction>();
FProperty* Property = FieldChain.Last().Get<FProperty>();
check(Function != nullptr || Property != nullptr);
for (const TWeakObjectPtr<UObject>& ObjectPtr : Objects)
{
UObject* Object = ObjectPtr.Get();
// Ignore null outer objects
if ( Object == nullptr )
{
continue;
}
FDelegateEditorBinding Binding;
Binding.ObjectName = Object->GetName();
Binding.PropertyName = InPropertyName;
Binding.SourcePath = FEditorPropertyPath(FieldChain);
if ( Function != nullptr)
{
Binding.FunctionName = Function->GetFName();
UBlueprint::GetGuidFromClassByFieldName<UFunction>(
Function->GetOwnerClass(),
Function->GetFName(),
Binding.MemberGuid);
Binding.Kind = EBindingKind::Function;
}
else if( Property != nullptr )
{
Binding.SourceProperty = Property->GetFName();
UBlueprint::GetGuidFromClassByFieldName<FProperty>(
SkeletonClass,
Property->GetFName(),
Binding.MemberGuid);
Binding.Kind = EBindingKind::Property;
}
ThisBlueprint->Bindings.Remove(Binding);
ThisBlueprint->Bindings.AddUnique(Binding);
}
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(ThisBlueprint);
});
OnAddBinding 提供了两个参数 DECLARE_DELEGATE_TwoParams(FOnAddBinding, FName /*InPropertyName*/, const TArray<FBindingChainElement>& /*InBindingChain*/);
,InPropertyName
是发起绑定的 Property 的 Name,InBindingChain
是选中的绑定目标的访问路径属性链,例如当我在 UMG 拖入一个进度条,并将 Percent 参数绑定到 UMG 蓝图中定义的一个 LinearColor 类型中的 R 时,传入的参数结果为:
也正因此,这里获取最终的 Property 时取的是 Last()。
回调到这里,可以通用的绑定部分就已结束,剩下的就是如何具体完成绑定逻辑的构建了,绑定逻辑的这一块各个系统都不太一样,都是依赖这些来自 UI 回调的参数,以及我们在构建代理匿名函数时传入的信息,根据具体业务来具体实现的。
接下来来跟下 UMG 这一块的实现逻辑。
UMG 的绑定逻辑
(这里最好了解一些蓝图的基础知识,UMG 蓝图的相关逻辑不再赘述,可以简单理解为)
接上段的 OnAddBinding,会发现这一段代码的逻辑很简单,只是基于入参构造了一个 FDelegateEditorBinding Binding 对象,构建的参数值如:
然后将其添加到了 ThisBlueprint->Bindings 中,这里的 ThisBlueprint 就是当前正在编辑的 UI 蓝图对象。
存储到这里以后,当我们编译时,便会将这一绑定关系传递到 UWidgetBlueprintGeneratedClass 的 TArray< FDelegateRuntimeBinding > Bindings 成员中:
void FWidgetBlueprintCompilerContext::FinishCompilingClass(UClass* Class)
{
if (Class == nullptr)
return;
UWidgetBlueprint* WidgetBP = WidgetBlueprint();
...
// Only check bindings on a full compile. Also don't check them if we're regenerating on load,
// that has a nasty tendency to fail because the other dependent classes that may also be blueprints
// might not be loaded yet.
const bool bIsLoading = WidgetBP->bIsRegeneratingOnLoad;
if ( bIsFullCompile )
{
SanitizeBindings(BPGClass);
// Convert all editor time property bindings into a list of bindings
// that will be applied at runtime. Ensure all bindings are still valid.
for ( const FDelegateEditorBinding& EditorBinding : WidgetBP->Bindings )
{
if ( bIsLoading || EditorBinding.IsBindingValid(Class, WidgetBP, MessageLog) )
{
BPGClass->Bindings.Add(EditorBinding.ToRuntimeBinding(WidgetBP));
}
}
示例说明
todo
Comments 1 条评论
东神,强