Details 面板自定义

在虚幻编辑器的 Details 面板中自定义属性显示的指南。

Windows
MacOS
Linux

现可对 Details 面板进行完全的自定义。可通过一个简单的系统重排属性,或使用 Slate UI框架 进行完全自定义。也可使用 Slate 语法将其他 UI 添加到 Details。

设置说明

  1. 创建一个类进行属性自定义。这必须继承自 ILayoutDetails

    • 实现一个函数:void LayoutDetails( IDetailLayoutBuilder& )

    • 此类的作用是封装类属性的自定义。每个需要的 Details 面板均会创建一个类实例。

  2. Details 面板识别特定类的属性时,设置将被调用的委托。

    • 此委托的唯一作用是为拥有属性的特定 UObject 创建一个自定义类的实例。注意:在任意点上通常存在多个细节视图,细节视图的每个实例获得其自身的自定义类实例。这使您可在 layout 类上存储每个细节实例数据。

    • 范例(可在 DetailCustomizations.cpp 中查看更多范例):

      FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
      PropertyModule.RegisterCustomPropertyLayout( ABrush::StaticClass(), FOnGetDetailLayoutInstance::CreateRaw( &FBrushDetails::MakeInstance ) );
      
      ...
      static TSharedRef<ILayoutDetails> FBrushDetails::MakeInstance()
      {
          return MakeShareable( new FBrushDetails );
      }
  3. 在步骤 1 中设置的类 LayoutDetails 函数中实现自定义。

    • 如这是一个引擎类,则需将自定义类(如它尚不存在)添加到 DetailCustomizations 模块。不重启编辑器即可重编译和重新加载此模块,便于快速调整属性。

    • FDetailCustomizationsModule.StartupModule 中绑定委托,在 FDetailCustomizationsModule.ShutdownModule 解除绑定。

    • 游戏特定的类应使用游戏特定的模块。

    • 查看此文档和 DetailCustomizations 模块中的范例(如 PrimitiveComponentDetails.cpp 和 StaticMeshComponentDetails.cpp)。

自定义

在自定义类的 LayoutDetails 函数中处理所有自定义。此函数接受一个 IDetailLayoutBuilder。它是属性的接口和传回自定义控件的方式。

IDetailLayoutBuilder 的基函数是创建属性和其他细节存在的类。此分类上还拥有部分其他自解释的下函数(多数都不需要)。DetailLayoutBuilder.h 中有关于它们的文档。

自定义的第一步是编辑类别:

virtual void LayoutDetails( IDetailLayoutBuilder& DetailBuilder ) override
{
    // 编辑灯光类别
    IDetailCategory& LightingCategory = DetailBuilder.EditCategory("Lighting", TEXT("OptionalLocalizedDisplayName") );
}

EditCategory 函数为属性所在的类型接受一个 FName,和一个任选本地化显示名。如显示名已指定,它将覆盖现有的显示名。不要求类别名必须是 UPROPERTY 宏中指定的相同类别名。但如果名称相同,它将重新使用 UPROPERTY 类别。如属性未被自定义且不在树状视图中,宏类别名将用作默认类别。

EditCategory 返回一个 IDetailCategoryBuilder&,其可用于将属性添加到类别。有两种方法进行操作:

Multibox 式 Layout

之前创建的 LightingCategory 的简单使用范例:

// 添加属性到类别。第一个参数是属性名,第二个参数是可选显示名覆盖。
LightingCategory.AddProperty("bCastStaticShadow", TEXT("Static") );
LightingCategory.AddProperty("bCastDynamicShadow", TEXT("Dynamic") );
LightingCategory.AddProperty("bCastVolumetricTranslucentShadow", TEXT("Volumetric") );

这是最基础的范例。它添加 3 个垂直堆栈的属性,并覆写它们的显示名。(范例中文本的本地化并非用于节约空间,而是固定的操作。)

注意:自定义属性和类别固定出现在非自定义属性和类别上方。可使用此简单语法识别重要属性,否则它们可能被掩盖。

结果:

multibox_layout_vertical.png

一个稍微更高级的范例(PrimitiveComponentDetails.cpp 中):

// 使用显示名"Shadows"创建一个不可重叠组,它在 CastShadow 属性启用后方可见。在 EndGroup 或另一个 BeginGroup 被调用之前,此调用下的所有属性均出现在同一个组中。
LightingCategory.BeginGroup( TEXT("Shadows"), GroupImageName, "CastShadow" );
    // 开始新的一行。在 EndLine() 或另一个 BeginLine() 被调用之前,此调用下的所有属性均被添加到同一行中。
    LightingCategory.BeginLine();
            // 使用默认外观添加属性。
            LightingCategory.AddProperty("bCastStaticShadow", TEXT("Static") );
            LightingCategory.AddProperty("bCastDynamicShadow", TEXT("Dynamic") );
            LightingCategory.AddProperty("bCastVolumetricTranslucentShadow", TEXT("Volumetric") );
    LightingCategory.BeginLine();
            LightingCategory.AddProperty("bCastInsetShadow", TEXT("Inset") );
            LightingCategory.AddProperty("bCastHiddenShadow", TEXT("Hidden") );
            LightingCategory.AddProperty("bCastShadowAsTwoSided", TEXT("Two Sided") );
LightingCategory.EndGroup();
  • BeginGroup 用于新建一组属性。它接受组显示的名称、显示在名称旁的任选图片名称(Slate 笔刷名),以及一个任选编辑条件属性(如为 false,将从视图中隐藏整个组,使其属性无法被修改。)这些编辑条件与 UPROPERTY 宏机制编辑条件相同,唯一的不同是它们操作的是一组属性,而非一个。未来版本中将添加更多类似的内容!

  • AddProperty 使用其默认外观添加属性。它通常只需要一个参数 — 属性名。更复杂的属性(如结构体中的属性)需要查阅更多信息。如有需要,可查看 高级提示 部分或 DetailCategoryBuilder.h 中的文档。

  • BeginLine 可新建一行属性。通过 AddProperty 添加的所有属性均默认在新的行中创建。BeginLine 可确保所有属性被添加,直到下一个 BeginLineEndLine 处于同一行上。

结果:

multibox_layout_horizontal.png

Multibox 式 layout 的要点

  • 它的功能不够强,但未来将按需加入更多功能。当前的设计功能是用于快速重组。

  • Slate 布局要求对属性(属性句柄)的更高级的访问。在需要对属性外观进行自定义时尤其如此。

属性句柄

属性句柄两个主函数的作用是读写属性值,并使 Slate 自定义控件识别属性。细节视图/属性树访问属性的方式有些复杂,因此属性句柄将这些全部隐藏,执行撤销/重新执行、编辑前/后变更、包污染、世界切换等处理。

如需获取属性句柄,必须询问 IDetailCategory 在何处进行自定义。调用 IDetailCategory::GetProperty 即可执行。通常只需按以下方式传入属性名:

IDetailCategoryBuilder& LightingCategory = DetailBuilder.EditCategory( "Lighting" );
// 获得"bOverrideLightmapRes"属性的句柄
TSharedPtr<IPropertyHandle> OverrideLightmapRes = LightingCategory.GetProperty( "bOverrideLightmapRes" );

现在即拥有布尔属性 bOverrideLightmapRes 的句柄。

此时即可读写属性数值和/或将其传入 Slate 控件进行自定义。

实用属性句柄函数(在 PropertyHandle.h 中可查看完整记录列表):

函数

描述

IPropertyHandle::SetValue(const ValueType&amp; InValue)IPropertyHandle::GetValue(ValueType&amp; OutValue)

读写属性数值。它们针对多种内置类型(包括矢量和旋转体)重载。对于用户结构体之类的复杂类型而言,需要获取一个子句柄。查阅此文档最后的高级部分。

ResetToDefault()

将属性重置为默认。

IsValidHandle()

返回是否拥有有效属性句柄。

AsArray()

阵列属性值为特殊。查阅此文档最后的高级部分。

其他要点:

  • 如属性无法被找到或不会出现在细节视图中,从 GetProperty 返回的句柄可能为无效。检查 IsValidHandle() 确认。在无效句柄上调用函数不会出现崩溃。

  • 不应将属性句柄存储在 layout 类之外,除非它们为弱指针。在内部,它们访问的数据为弱指针,因此在无效属性上尝试设置或获取数值不会出现崩溃;但如果将其存储且不进行清理,得到的是对无用对象的引用。

  • 如尝试读/写访问不支持属性的数据类型(如 String 属性的 float),操作将失败,但数据不会受损。

访问数值时处理失败操作。

注意:细节视图可同时查看多个对象,用户可一次性选择数百个 Actor。在这种情况下,一个属性肯定会拥有多个数值。GetValue 和 SetValue 返回一个 FPropertyAccess::Result,以确定数据访问是否成功。FPropertyAccess::MultipleValues 将成为普通返回值。

/**
* 访问属性数值可能出现的结果                   
    */
namespace FPropertyAccess
{
        enum Result
        {
            /** 找到多个值,因此无法读取值 */
            MultipleValues,
            /** 设置或获取数值失败(原因可能是属性不可用、为不兼容类型或编辑常量) */
            Fail,
            /** 成功设置或获得值 */
            Success,
        };
}

如要自定义低级类型的属性(如 intfloat),必须设法处理多数值的状况。

    INT MyInteger;
    // 获得属性的值
    FPropertyAccess::Result MyResult = MyIntHandle->GetValue(MyInteger);

如 MyResult 为 FPropertyAccess::MultipleValuesMyInteger 不会被设置。将其发送至展示它后将显示垃圾值的控件,并将其在尚未变好之前将其初始化,因为它还不是正确的值。如何处理,取决于自定义者。针对数字类型,推荐使用 SNumericEntryBox,通过它可选择性地在其值属性中不返回值。然后将显示设置的标签。查看 SNumericEntryBox.h

Slate 布局

通过 Slate 布局可对属性的外观和排列进行完全的自定义。通过 IDetailCategoryBuilder::AddWidget 将布局传回类型,它从 Slate 接收一个任意控件。此处的自定义控件对您有所帮助:

SProperty

这是一个自定义控件。可使用此控件自定义属性和/或将属性嵌入另一个 slate 声明语法。使用 SNew 创建一个 SProperty,再为属性提供一个句柄,使其了解构建的内容。句柄是 SNew 不可选的参数:SNew( SProperty, HandleToTheProperty )

范例:

// 编辑灯光类别
IDetailCategoryBuilder& LightingCategory = DetailBuilder.EditCategory( "Lighting" );

// 获取 bOverrideLightmapRes 属性的句柄
TSharedPtr<IPropertyHandle> OverrideLightmapRes = LightingCategory.GetProperty( "bOverrideLightmapRes" );

LightingCategory.AddWidget()
[
    SNew( SHorizontalBox )
    + SHorizontalBox::Slot()
    [
        // 制作新 SProperty
        SNew( SProperty, EnableOverrideLightmapRes )
    ]
    + SHorizontalBox::Slot()
    .Padding( 4.0f, 0.0f )
    .MaxWidth( 50 )
    [
        SNew( SProperty, LightingCategory.GetProperty("OverriddenLightMapRes") )
        .NamePlacement( EPropertyNamePlacement::Hidden ) // 隐藏名称
    ]
];

结果:

sproperty.png

SProperty 将默认生成一个属性的控件。SProperty 上有一些基础自定义属性,可对默认外观进行自定义(如名称)。如需自定义属性,使用 CustomWidget 槽。使用 CustomWidget 槽后,SProperty 便无法了解关于如何设置和获取数值的内容,因为已经构建了一个自定义控件。需要使用属性句柄来获取并设置数值。

范例:

  // 自定义 OverridenLightMapRes 属性,以便显示一些文本和一个 spinbox 
  TSharedPtr<IPropertyHandle> LightMapResValue = LightingCategory.GetProperty("OverriddenLightMapRes")
  SNew( SProperty, LightMapResValue )
  .CustomWidget()
  [
        SNew( SHorizontalBox )
        + SHorizontalBox::Slot()
        .VAlign( VAlign_Center )
        .Padding( 2.0f )
        [
              SNew( STextBlock )
              .Text( TEXT("Lightmap Res") )
        ]
        + SHorizontalBox::Slot()
        [
              SNew( SSpinBox )
              .MinSliderValue( 0 )
              .MaxSliderValue( 1024 )
              .OnValueCommitted( &SetValueOnProperty )  
              .Value( &GetValueFromProperty
        ]
  ]
  ...
  FLOAT GetValueFromProperty()
  {
        // 使用以上创建的属性句柄,获取其数值并发送至 spinbox
        INT Value; // 注意:光照图分辨率为整数,因此必须这样进行访问。
        LightMapResValue.GetValue( Value );
        // 注意句柄出现失败的情况
        return Value;
  }

  void SetValueOnProperty( FLOAT NewValue )
  {
        // 使用属性句柄,设置其数值
        LightMapResValue.SetValue( NewValue )
  }
SProperty 要点
  • 即时制作了自定义控件,SProperty 仍然固定显示重设为默认。SProperty 存在一个参数,可开启此行为。如存在一行属性时,便无需在每个属性上显示重设为默认,在最后显示一个大菜单即可。查看下方的 SResetToDefaultMenu

  • 如将无效句柄传到 SProperty,它将不会显示。

SProperty 外,还可使用其他自定义控件。

SAssetProperty

SAssetProperty 是一个 SProperty,显示资源的缩略图和变更资源的输入框。还可变更缩略图的尺寸。可将其用于拥有可渲染缩略图的 UObject 属性。如将其用于其他类型上,将不会显示任何内容。

sassetproperty.png

SFilterableDetail

SFilterableDetail 是一个不进行绘制的控件,但用户在细节视图的搜索框中输入内容时,它可对其内容槽中的所有内容进行过滤。此属性适用于不以属性为基础的细节。SProperty 已进行过滤,因此无需对它们设置 SFilterableDetail,除非需要将它们的过滤分组。

// "Create Blocking Volume"与用户的搜索项不匹配时,创建对内容槽中所有内容进行过滤的控件。
// 注意:第二个参数为和过滤匹配的本地化搜索项;第三个参数为该过滤所处的类别。
SNew( SFilterableDetail, NSLOCTEXT("StaticMeshDetails", "BlockingVolumeMenu", "Create Blocking Volume"), &StaticMeshCategory )
.Content()
[
      // 创建阻挡体积域菜单
      SNew( SComboButton )
      .ButtonContent()
      [
            SNew( STextBlock )
            .Text( NSLOCTEXT("StaticMeshDetails", "BlockingVolumeMenu", "Create Blocking Volume") ) 
            .Font( IDetailLayoutBuilder::GetDetailFont() )
      ]
      .MenuContent()
      [
            BlockingVolumeBuilder.MakeWidget()
      ]
]

SResetToDefaultMenu

SResetToDefaultMenu 是显示黄色重置到默认箭头的菜单。SProperty 默认添加一个重置到默认菜单,但有时有必要将多个属性建组放入相同菜单中。(如 Vector 属性)。可将 SProperty 控件添加到 SResetToDefaultMenu 进行处理。在 SResetToDefaultMenu 上调用 AddProperty,再把菜单放置在声明式语法中即可!

SArrayProperty

通过此控件可自定义属性阵列。创建一个和 SProperty 的控件,并与一个委托挂钩。每次需要阵列元素的控件时,便会调用此委托。

范例:

void FMeshComponentDetails::LayoutDetails( IDetailLayoutBuilder& DetailLayout )
{
      IDetailCategoryBuilder& DetailCategory = DetailLayout.EditCategory("Rendering");
      TSharedRef<IPropertyHandle> MaterialProperty = DetailCategory.GetProperty( "Materials" );

      DetailCategory.AddWidget()
      [
            SNew( SArrayProperty, MaterialProperty )
            // 此委托针对每个阵列元素调用,为其生成控件。
            .OnGenerateArrayElementWidget( this, &FMeshComponentDetails::OnGenerateElementForMaterials )
      ];
}
...
/**
* 为一个材质元素生成一个控件
* 
 * @param ElementProperty     我们需要生成的阵列元素句柄
* @param ElementIndex        我们生成的元素索引
*/
TSharedRef<SWidget> FMeshComponentDetails::OnGenerateElementForMaterials( TSharedRef<IPropertyHandle> ElementProperty, INT ElementIndex )
{
      return 
            SNew( SAssetProperty, ElementProperty )
            .ThumbnailSize( FIntPoint(32,32) );
}

结果:

sarrayproperty.png

自定义注意事项

  • 自定义属性和读写值时检查错误情况。注意:细节视图常同时查看多个对象,每个对象均拥有不同的值。自定义属性应足以处理多值的常见情况。

  • 存储关于自定义类选择的所有数据。部分非属性细节需要选中的 Actor 进行自定义。可从 IDetailLayoutBuilder 获取选中的 Actor。可在自定义类上存储此选择集或与选择密切相关的内容。它必将处于周围,而选择在其细节视图中仍保持不变。

  • 不使用 FActorIteratorFSelectedActorIteratorGEditor->GetSelectedActorIterator。注意:Details 面板可被锁定,如被锁定,这些内容将在全局选择集或 Actor(并非 Details 面板中选中的 Actor)列表上运行!使用它们将访问不同数据。可从 IDetailLayoutBuilder 获取选中 Actor 的列表。

  • 不保留对 layout 类的强引用或属性句柄(根本无需保留)。注意:细节视图(尤其是关卡编辑器的细节视图)可能基于用户选择随时发生变化,因此对 layout 类的引用很容易失效。自定义细节防止此情况发生时,将检查 layout 类共享指针的唯一性。

高级提示

访问复杂属性

无法通过属性名进行分解的属性即被定义为复杂属性。这通常为结构体中的属性。

访问复杂属性的方式有两种:

  • 返回属性句柄或添加属性到类型的函数使用任选参数分解属性。

    范例:

    TSharedPtr<IPropertyHandle> IDetailCategoryBuilder::GetProperty(  FName PropertyPath, UClass* ClassOutermost , FName InstanceName) 

    参数

    描述

    Path

    属性的路径。可为一个属性名称,或格式 outer.outer.value[optional_index_for_static_arrays] 中的一个路径。

    ClassOutermost

    访问被自定义的当前类外的属性时可选的外部类。

    InstanceName

    相同类存在多个 UProperty 时可选的实例名(如存在两个完全相同的结构体,实例名即为其中一个结构体变量名)。

    范例:

    struct MyStruct
    { 
        INT StaticArray[3];
        FLOAT FloatVar;
    }
    
    class MyActor
    { 
        MyStruct Struct1;
        MyStruct Struct2;
        FLOAT MyFloat
    }
    • MyActor 中的 Struct2 访问索引 2 处的 StaticArray 时,路径为 "MyStruct.StaticArray[2]",实例名为 "Struct2"

    • MyActor 自定义函数外访问相同的 StaticArray 时需要执行上述操作,但 ClassOutermost 应为 MyActor::StaticClass()

    • MyActor 中访问 MyFloat 时可传入 "MyFloat",因为属性名很明确。

  • 如拥有一个属性句柄,可通过其名称获取子属性句柄:

    TSharedPtr<IPropertyHandle> IPropertyHandle::GetChildHandle( FName ChildName )

    参数

    描述

    ChildName

    子项的属性名。它将进行递归,直到找到。不支持路径,阵列子项无法通过此方式访问。

访问阵列

可通过 IPropertyHandle::AsArray 访问阵列。如属性句柄为一个阵列,将返回一个 IPropertyHandleArray。它拥有添加、移除、插入、复制和获取阵列元素数量的函数。

隐藏属性

调用 IDetailLayoutBuilder::HideProperty 后可完全隐藏属性。它接受一个名称/路径或属性句柄。

Select Skin
Light
Dark

Welcome to the new Unreal Engine 4 Documentation site!

We're working on lots of new features including a feedback system so you can tell us how we are doing. It's not quite ready for use in the wild yet, so head over to the Documentation Feedback forum to tell us about this page or call out any issues you are encountering in the meantime.

We'll be sure to let you know when the new system is up and running.

Post Feedback