Details Panel Customization

The Details panel is now fully customizable. You can rearrange properties via a simple system, or you can fully customize them using Slate 用户界面框架 . You can also add other UI to the details using Slate syntax.

本页面的内容:

Setup instructions

  1. Create a class for customizing properties in. This must inherit from ILayoutDetails.

    • You implement one function: void LayoutDetails( IDetailLayoutBuilder& ).

    • The purpose of this class is to encapsulate customization for a classes properties. One instance of the class will be created for each Details panel that requires it.

  2. Set up a delegate that will be called when the Details panel recognizes properties for a specific class.

    • The sole purpose of this delegate is to create an instance of your customization class for a specific UObject that has properties. Remember that there often are multiple details views up at any point and each instance of the details view gets its own customization class instance. This allows you to store per detail instance data on your layout class.

    • Example (more examples are located in 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. Implement your customization in the LayoutDetails function of the class you made in step 1.

    • If this is an engine class, you should add your customization class (if it does not already exist) to the DetailCustomizations module. This module can be recompiled and reloaded without restarting the editor, making it useful for fast tweaking of properties.

    • Bind your delegate in FDetailCustomizationsModule.StartupModule and unbind it in FDetailCustomizationsModule.ShutdownModule.

    • Game specific classes should use a game specific module.

    • See examples in this document and the DetailCustomizations module (such as PrimitiveComponentDetails.cpp and StaticMeshComponentDetails.cpp).

Customizing

You handle all customization inside the LayoutDetails function of your customization class. This function accepts an IDetailLayoutBuilder, which is your interface to properties and how you pass back customization widgets.

The primary function of IDetailLayoutBuilder is to create categories where properties and other details reside. There are some other minor functions on this class which are self explanatory (most of them you will not need). The documentation for those can be found in DetailLayoutBuilder.h.

The first step in customizing is to edit a category:

virtual void LayoutDetails( IDetailLayoutBuilder& DetailBuilder ) override
{
    // Edit the lighting category
    IDetailCategory& LightingCategory = DetailBuilder.EditCategory("Lighting", TEXT("OptionalLocalizedDisplayName") );
}

The EditCategory function takes an FName for the category where properties will reside and an optional localized display name. If the display name is specified, it will override any existing display name. The category name does not have to be the same category name specified in the UPROPERTY macro, although it will reuse the UPROPERTY category if the names are the same. The macro category name is used as the default category if the property is not customized and in the tree view.

EditCategory returns an IDetailCategoryBuilder& which is what you use to add properties to a category. There are a two ways to do this:

Multibox Style Layout

An easy example using the LightingCategory created above:

// Add a property to the category.  The first param is the name of the property and the second is an optional display name override.
LightingCategory.AddProperty("bCastStaticShadow", TEXT("Static") );
LightingCategory.AddProperty("bCastDynamicShadow", TEXT("Dynamic") );
LightingCategory.AddProperty("bCastVolumetricTranslucentShadow", TEXT("Volumetric") );

This is the most basic example. It adds 3 properties stacked vertically and overrides their display names. (The text in the examples is not localized to save space, but should always be localized in general practice.)

Note that customized properties and categories always appear above non-customized ones. You can use this simple syntax to reorganize important properties which may otherwise be buried.

The result:

multibox_layout_vertical.png

A slightly more advanced example (located in PrimitiveComponentDetails.cpp):

// Create a non-collapsible group with the display name "Shadows" which is only visible if the CastShadow property is enabled. All properties below this call will appear in the same group until EndGroup or another BeginGroup is called
LightingCategory.BeginGroup( TEXT("Shadows"), GroupImageName, "CastShadow" );
    // Begin a new line.  All properties below this call will be added to the same line until EndLine() or another BeginLine() is called
    LightingCategory.BeginLine();
            // Add properties using their default look
            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();

The result:

multibox_layout_horizontal.png

Notes About Multibox Style Layout

Property Handles

The two main functions of property handles are to read and write the value of a property and to identify the property to Slate customization widgets. How properties are accessed by the details view/property tree is somewhat complicated, so property handles hide all that away and handles undo/redo, pre/post edit change, package dirtying, world switching etc., for you.

To get a property handle, you must ask the IDetailCategory where you want to customize it for one. You do this by calling IDetailCategory::GetProperty. Usually you simply pass in the name of the property as follows:

IDetailCategoryBuilder& LightingCategory = DetailBuilder.EditCategory( "Lighting" );
// Get a handle to the "bOverrideLightmapRes" property
TSharedPtr<IPropertyHandle> OverrideLightmapRes = LightingCategory.GetProperty( "bOverrideLightmapRes" );

Now you have a handle to the bool property bOverrideLightmapRes.

From here, you can read and write the value of that property and/or pass it to a slate widget for customization.

Useful property handle functions (For the complete documented list see PropertyHandle.h):

Function Description
IPropertyHandle::SetValue(const ValueType& InValue) and IPropertyHandle::GetValue(ValueType& OutValue) Writes and reads property values. These are overloaded for many built in types (including vectors and rotators). For complicated types like user structs, will need to get a child handle. See the advanced section at the end of this document.
ResetToDefault() Resets a property to its default.
IsValidHandle() Returns whether or not you have a valid property handle.
AsArray() Array property values are special. See the advanced section at the end of this document.

Other notes:

Handling failure cases when accessing values.

Remember that detail views can view multiple objects at once and it is not uncommon for users to select hundreds of Actors at once. In cases like these, you will undoubtedly have multiple values for one property. GetValue and SetValue return an FPropertyAccess::Result to help you determine whether or not accessing a value was successful. FPropertyAccess::MultipleValues will be a common return value.

/**
* Potential results from accessing the values of properties                   
    */
namespace FPropertyAccess
{
        enum Result
        {
            /** Multiple values were found so the value could not be read */
            MultipleValues,
            /** Failed to set or get the value (Property is no longer available, is not a compatible type, or is edit const are the likely cases) */
            Fail,
            /** Successfully set or got the value */
            Success,
        };
}

If you are customizing a low level typed property like an int or float, you must handle the multiple values case somehow.

    INT MyInteger;
    // Get the value of the property
    FPropertyAccess::Result MyResult = MyIntHandle->GetValue(MyInteger);

If MyResult is FPropertyAccess::MultipleValues, MyInteger will not be set. Sending that to a widget that displays it will show garbage value and initializing it before hand is not much better because it is still not the correct value. How this is handled is up to the customizer. For numeric types, using SNumericEntryBox is recommended which allows you to optionally return no value in its value attribute. It will then display a label you provide instead. See SNumericEntryBox.h.

Slate Layout

Slate layout allows you to completely customize the look and arrangement of properties. You pass your layout back to a category via IDetailCategoryBuilder::AddWidget which takes an arbitrary widget from Slate. To assist you in this, there are some customization widgets available to you:

SProperty

This is the customization widget. You use this widget to customize a property and/or embed the property in other slate declarative syntax. You create an SProperty using SNew but you also provide a handle to the property so it knows what to build. The handle is a non-optional parameter to SNew: SNew( SProperty, HandleToTheProperty )

Example:

// Edit the lighting category
IDetailCategoryBuilder& LightingCategory = DetailBuilder.EditCategory( "Lighting" );

// Get a handle to the bOverrideLightmapRes property
TSharedPtr<IPropertyHandle> OverrideLightmapRes = LightingCategory.GetProperty( "bOverrideLightmapRes" );

LightingCategory.AddWidget()
[
    SNew( SHorizontalBox )
    + SHorizontalBox::Slot()
    [
        // Make a new SProperty
        SNew( SProperty, EnableOverrideLightmapRes )
    ]
    + SHorizontalBox::Slot()
    .Padding( 4.0f, 0.0f )
    .MaxWidth( 50 )
    [
        SNew( SProperty, LightingCategory.GetProperty("OverriddenLightMapRes") )
        .NamePlacement( EPropertyNamePlacement::Hidden ) // Hide the name
    ]
];

The result:

sproperty.png

SProperty by default will generate a widget for the property. There are some basic customization attributes on SProperty for customizing the default look (such as the name). If you want to customize a property, you use the CustomWidget slot. Once you use the CustomWidget slot, SProperty no longer knows anything about how to set and get the value since you have made a custom widget. You need to use your property handle to get and set the value.

Example:

  // Customizes the OverridenLightMapRes property to display some text and a 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()
  {
        // Using the property handle created above, get its value and send it to the spinbox
        INT Value; // note lightmap res is an integer so it must be accessed as such.
        LightMapResValue.GetValue( Value );
        // Note HANDLE FAILURE CASES
        return Value;
  }

  void SetValueOnProperty( FLOAT NewValue )
  {
        // Using the property handle, set its value
        LightMapResValue.SetValue( NewValue )
  }
SProperty Notes

In addition to SProperty there are some other customization widgets that can be used.

SAssetProperty

SAssetProperty is an SProperty that displays a thumbnail of the asset as well as an entry box for changing the asset. You can change the size of the thumbnail as well. You can use this on UObject properties that have renderable thumbnails. If you use this on other types, it will not display anything.

sassetproperty.png

SFilterableDetail

SFilterableDetail is a widget that does not draw anything but filters everything in its content slot when a user types in the search box of the details view. This widget is useful for non-property based details. SProperty is already filtered so you do not need to set up an SFilterableDetail for those unless you want to group their filtering.

// Create a widget which will filter out everything in the content slot when "Create Blocking Volume" is not matched with a user's search term
// Note: The second parameter is the localized search term that matches the filter and the third parameter is the category where this filter should reside
SNew( SFilterableDetail, NSLOCTEXT("StaticMeshDetails", "BlockingVolumeMenu", "Create Blocking Volume"), &StaticMeshCategory )
.Content()
[
      // Create blocking volume menu
      SNew( SComboButton )
      .ButtonContent()
      [
            SNew( STextBlock )
            .Text( NSLOCTEXT("StaticMeshDetails", "BlockingVolumeMenu", "Create Blocking Volume") ) 
            .Font( IDetailLayoutBuilder::GetDetailFont() )
      ]
      .MenuContent()
      [
            BlockingVolumeBuilder.MakeWidget()
      ]
]

SResetToDefaultMenu

SResetToDefaultMenu is a menu which displays the yellow reset to default arrow. By default, SProperty adds a reset to default menu, but sometimes it makes sense to group more than one property into the same menu. (e.g Vector properties). You can add SProperty widgets to an SResetToDefaultMenu to handle this for you. Simply call AddProperty on SResetToDefaultMenu and then place the menu in any declarative syntax!

SArrayProperty

This widget allows you to customize an array of properties. You create one just like SProperty and also hook up a delegate which is called each time a widget for an array element is needed.

Example:

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

      DetailCategory.AddWidget()
      [
            SNew( SArrayProperty, MaterialProperty )
            // This delegate is called for each array element to generate a widget for it
            .OnGenerateArrayElementWidget( this, &FMeshComponentDetails::OnGenerateElementForMaterials )
      ];
}
...
/**
* Generates a widget for a materials element
* 
 * @param ElementProperty     A handle to the array element we need to generate
* @param ElementIndex        The index of the element we are generating
*/
TSharedRef<SWidget> FMeshComponentDetails::OnGenerateElementForMaterials( TSharedRef<IPropertyHandle> ElementProperty, INT ElementIndex )
{
      return 
            SNew( SAssetProperty, ElementProperty )
            .ThumbnailSize( FIntPoint(32,32) );
}

The result:

sarrayproperty.png

Customization Dos and Dont's

Advanced Tips

Accessing complicated properties.

A complicated property is defined as anything that cannot be resolved with just a property name. Usually this involves properties inside structs.

There are two ways to access complicated properties:

Accessing Arrays

You can access arrays via IPropertyHandle::AsArray. If the property handle is an array, this will return an IPropertyHandleArray which has functions for adding, removing, inserting, duplicating, and getting the number of elements of an array.

Hiding properties

You can hide properties altogether by calling IDetailLayoutBuilder::HideProperty. It takes either a name/path or a property handle.