When developing editor plugins in UE, it is very convenient to automatically create property panels using the reflection information of USTRUCT, providing a configurable way, but there are often special customization requirements for the property panels, such as providing special panel options, displaying different types of values based on parameters, etc. The property panels in UE are applied in HotPatcher and ResScannerUE, which can conveniently configure and customize plugins. The official documentation from UE: Details Panel Customization.
This article starts with creating an independent Details
panel, and through specific cases in ResScannerUE, provides implementation methods for customizing property panels and property items.
Property Panel StructureDetailsView
Taking ResScannerUE as an example, starting the plugin creates a Dock that contains some basic operation buttons and a property panel:
The elements displayed in the property panel are all reflection properties of a USTRUCT structure. As an example of the above panel, it is a structure of FScannerConfig
:
1 | USTRUCT(BlueprintType) |
Just define a structure like this, and the UE property panel will create the corresponding types and sub-structure members, completely avoiding the need to write creation code for each property. The creation method is also very simple; just create an SStructureDetailsView
in the Construct
of a certain Slate class through the PropertyEditor
module.
First, include the PropertyEditor
module in build.cs
, and then you will be able to create it through PropertyEditor
:
1 | // .h |
The created panel needs to be bound to a UStruct to obtain the reflection information of the structure, and also bind a structure instance to get display and storage values. At the same time, the functionality of the property panel can be controlled by setting the parameters of FDetailsViewArgs
and FStructureDetailsViewArgs
, such as whether to search for property names, show scroll bars, etc.
After creating SettingsView
, the method to display it in the Slate control is:
1 | void SResScannerConfigPage::Construct(const FArguments& InArgs) |
It’s simply putting the created SettingsView
widget into a container for display, which includes all editable reflection properties bound to the UStruct, just like the panel display of ResScannerUE mentioned earlier.
Property Panel Customization
In the code for creating SettingView mentioned earlier, there is a line:
1 | SettingsView->GetDetailsView()->RegisterInstancedCustomPropertyLayout(FScannerConfig::StaticStruct(),FOnGetDetailCustomizationInstance::CreateStatic(&FScannerSettingsDetails::MakeInstance)); |
This specifies an instance of a DetailCustomization class for the current SettingsView, used for customization operations when creating the SettingView control. The declaration of this class is as follows:
1 | // ScannerConfigDetails.h |
In the CustomizeDetails
interface, you can perform some customization operations, such as creating a new Slate control in the property panel. For example, the Import
button in ResScannerUE:
1 | void FScannerSettingsDetails::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) |
You can use the DetailBuilder
in the interface to obtain the structure instance bound to the current SettingView, allowing you to retrieve data from the configuration and perform operations.
The above code retrieves the property’s Category
, creating a CustomRow element under RulesTable
, and within ValueContent()
you can directly write Slate code to create your custom control. Because you can directly access the bound structure instance, you can also directly call the structure’s functions in the control’s implementation, enabling the effect of clicking the Import
button to import from the DataTable into the configuration.
Property Customization in the Panel
In the previous section, we mentioned that we can create custom Slate controls in the property panel to achieve customized behavior. However, some requirements involve modifying the display of properties in the panel. For instance, in the property matching requirement for ResScannerUE:
- You can select the UClass type.
- Based on the selected UClass type, list the selectable properties, and dynamically create the value types of the selected properties to display in the panel.
- The property name-value is an array, with each element allowing a different property name and creating different value types.
I have implemented this, and the effect is as follows:
Below is an introduction to the implementation. If it is merely about obtaining reflection data, it’s actually not difficult. However, displaying it on the property panel becomes more complex.
- Traverse all properties based on the UClass.
- Get FProperty to obtain the current property type.
- Create different FProperty in the property panel.
As mentioned earlier, the property panel binds a USTRUCT and creates properties. My initial thought was to dynamically modify the UStruct, but I found that is not very reasonable since the UStruct is also fixed and cannot change the element types in an array.
So, what other methods are there? After reviewing the engine’s code, I discovered that UE can also perform custom operations in the property panel for certain types of properties. The core of this is IPropertyTypeCustomization
.
Its usage is similar to the IDetailCustomization
interface, both providing an interface for custom property panel controls. However, there are slight differences in their creation forms:
- DetailCustomization needs to be specified when creating SettingView, while PropertyTypeCustomization does not require this.
- DetailCustomization only takes effect in its own created SettingView, while PropertyTypeCustomization can take effect wherever that property is created.
PropertyTypeCustomization
is more flexible. Once the custom event is completed, it can correctly display in both SettingView and the structure instance created from a DataTable.
In ResScannerUE, I created a structure FPropertyMatchMapping
to store property names and values:
1 | USTRUCT(BlueprintType) |
Both PropertyName
and MatchValue
are strings, and the reason for using strings is that they can store values of any type. Subsequent property customization operations are targeted at the FPropertyMatchMapping
structure type.
Thus, the implementation ideas for property customization are:
- Create a custom
PropertyTypeCustomization
. - When the property panel is created, replace the original property creation logic and use
PropertyTypeCustomization
for the creation.
First, create the PropertyTypeCustomization
class for the FPropertyMatchMapping
type:
1 | class FCustomPropertyMatchMappingDetails : public IPropertyTypeCustomization |
Then, during module startup, register this class:
1 | void FResScannerEditorModule::StartupModule() |
This step informs the PropertyEditor
module that when creating the FPropertyMatchMapping
type in the property panel, it will create an instance of FCustomPropertyMatchMappingDetails
to execute the creation operation, thus shifting the control creation process of the specified class to our code.
Implementation of FCustomPropertyMatchMappingDetails
:
In the CustomizeHeader
function, you can directly create the property-value, but I did not find a method in the CustomizeHeader
parameters to access the entire parent structure instance, thus unable to retrieve the selected UClass. If creation does not depend on contextual property-value, it can be created directly here.
However, since what I want to implement depends on the selected UClass’s value, I only created the property name in CustomizeHeader
, while the value control creation was done in CustomizeChildren
:
1 | void FCustomPropertyMatchMappingDetails::CustomizeHeader(TSharedRef<IPropertyHandle> StructPropertyHandle, |
In CustomizeHeader
, I retrieved and stored the actual values of FPropertyMatchMapping
instances (both PropertyName and Value are FString). The elements of this structure are placed into CustomizeChildren
for creation because the IDetailChildrenBuilder
passed into CustomizeChildren
allows access to the parent structure to retrieve the selected UClass:
1 | void FCustomPropertyMatchMappingDetails::CustomizeChildren(TSharedRef<IPropertyHandle> StructPropertyHandle, |
After obtaining the UClass, traverse all reflection properties and create an SComboBox
:
1 | void FCustomPropertyMatchMappingDetails::OnClassValueChanged() |
This is how an SWidget is created through Slate and placed into the ValueContent
container created by StructBuilder.AddCustomRow
.
With the created SComboBox
, you can select the property name in the editor, and through the UClass
and property name, obtain the corresponding FProperty
, which is the reflection information of the property. Therefore, when the name selected in the ComboBox is different, the obtained FProperty
is also different, and the types it represents are also different. The next critical step is to dynamically create the corresponding types in the property panel based on the obtained FProperty
:
First, a container to store value controls needs to be created in CustomizeChildren
:
1 | StructBuilder.AddCustomRow(FText::FromString(TEXT("PropertyValue"))) |
PropertyContent
is a TSharedPtr<SBox>
, and the controls created afterwards will be displayed using it.
Returning to the earlier discussion about creating value controls based on FProperty, the IDetailChildrenBuilder
interface provides a function AddExternalStructureProperty
, which allows adding a certain property of an external structure to the current Details.
For example, the CompressionSettings
property of UTexture2D
:
1 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Compression, AssetRegistrySearchable) |
It is an enum, and I want to create it in a property panel that does not contain it and place it in the PropertyContent
created earlier:
1 | // Class is UTexture2D |
mStructBuilder
can be retrieved and stored in CustomizeChildren
parameters.
The code above is crucial in implementing the creation of corresponding value type controls based on FProperty
. By applying the UClass and property names, you can create the corresponding SWidget
and place it in a specific container for display!
Using the selected property name from the ComboBox, you can retrieve its FProperty
, and it can create the proper widget without including more code here due to space limit. If interested, you can check out the complete implementation in CustomPropertyMatchMappingDetails.cpp from ResScannerUE.
Generic Data Storage
Besides being able to display different properties in the panel, there also needs to be a general method to store them. Strings can be used to store all value information. UE also provides Value Widget
methods to read string values and set values from strings:
1 | FString Value; |
With these two functions, all types of values can be stored and retrieved through strings.