---
# System prepended metadata

title: Unreal Detail View

---

# Unreal Detail View

> [!Note]
> 本教學使用 Unreal 5.4

> [!Caution]
> 應學長要求，需使用 UStruct 來包裝要設定的參數，並透過 `IStructureDetailsView` 來將物件顯示在 Detail 面版上。

Unreal 中的 Detail 面版其實是由 PropertyEditor 這個模組建立的。你可以在 Unreal Engine 原始碼中的`Engine/Source/Editor/PropertyEditor`下找到該模組。

Detail 面版的功用：
- 顯示特定 UObject 或 UStruct 物件的 property，並提供介面來編輯它
- 預設生成的 widget 會運用 `UPROPERTY()` 的內容，可簡化部分 widget 的撰寫速度
  [All UPROPERTY Specifiers](https://benui.ca/unreal/uproperty/)

該模組主要包含
- **PropertyEditorModule**
- **IDetailsView** - 給 UObject 用的 Detail 面版
- **FDetailsViewArgs** - 初始化 Detail 面版的參數
- **IStructureDetailsView** - 給 UStruct 用的 Detail 面版
- **FStructureDetailsViewArgs** - 初始化 `IStructureDetailsView`的額外參數

## 快速入門

1. 定義一個 struct，包含 GUI 下所有的參數，並寫上 `UPROPERTY()`
2. 將 struct 丟給 `IStructureDetailsView` 顯示
3. 如果不滿意預設的介面，再去看 `IDetailCustomization`
4. 如果某個自定義型別時常被當作 property 出現在 struct 內，再考慮用 `IPropertyTypeCustomization`

在最簡單的情況下，只需要做前2步即可。

## Dependency

在專案的`***.Build.cs`中的Dependency內加入`"PropertyEditor"`：

```csharp=
PrivateDependencyModuleNames.AddRange( // 新增私有依賴
    new string[] {
        // ...
        "PropertyEditor",
        // ...
    }
);
```

## 生成 Detail View

Detail View 可以透過`FPropertyEditorModule::CreateDetailView`來生成。範例如下：

```cpp=
// 取得Module
FPropertyEditorModule& M = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

// 設定參數
FDetailsViewArgs DetailArgs;
DetailArgs.bAllowSearch = false;
DetailArgs.bUpdatesFromSelection = false;
DetailArgs.bCustomNameAreaLocation = true;

// 生成Detail面版
auto DetailPanel = M.CreateDetailView(DetailArgs);

// 設定要顯示的物件
UData1* DataKeeper = NewObject<UData1>(); // 將 UData1 替換成自己的類別
DetailPanel->SetObject(DataKeeper);
```

`IDetailsView`是`SWidget`的子類，你可以把它和其他widget放在一起，如：

```cpp=
auto VBox = SNew(SVerticalBox)
    + SVerticalBox::Slot()
    .AutoHeight()
    [
        DetailPanel
    ];
```

## 生成 `IStructureDetailsView`

`IStructureDetailsView` 是用來顯示 UStruct 物件的，要注意的是需要將「要顯示的物件」用`FStructOnScope`來進行包裝，並且在`FPropertyEditorModule::CreateStructureDetailView`時當作參數傳遞過去。

```cpp=
// Get module
FPropertyEditorModule& M = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

// arguments
FDetailsViewArgs DetailArgs;
DetailArgs.bAllowSearch = false;
DetailArgs.bUpdatesFromSelection = false;
DetailArgs.bCustomNameAreaLocation = true;
DetailArgs.bShowScrollBar = false;
FStructureDetailsViewArgs StructArgs;

// 設定要顯示的物件
// 將 FStructFoo 替換成自己的類別，將 Foo 替換成自己的物件
TSharedPtr<FStructOnScope> Data = MakeShareable(new FStructOnScope(FStructFoo::StaticStruct(), (uint8*)&Foo));

// create panel
auto DetailPanel = M.CreateStructureDetailView(DetailArgs, StructArgs, Data);
```

`FStructOnScope`的建構子有兩個參數，第一個是物件的型別，第二個是物件的記憶體位址。經過封裝後，對`FStructOnScope`進行修改都會寫回`InData`的位址。

```cpp=
FStructOnScope::FStructOnScope(const UStruct* InScriptStruct, uint8* InData)
```

`IStructureDetailsView`也能和其他 widget 放在一起使用，如：

```cpp=
SNew(SVerticalBox)
+ SVerticalBox::Slot().FillHeight(1)
[
    DetailPanel->GetWidget().ToSharedRef()
]
```

# 客製化 Detail View

Property Editor 提供了兩種用來客製化的介面，使用時需要繼承其中一個介面並且向 PropertyEditorModule 註冊。這樣 Property Editor 就會在適當的時機調用你註冊的類別來改變 Detail View 的樣貌：

- **IPropertyTypeCustomization** - 客製化 property 的顯示方式（單個property）
- **IDetailCustomization** - 針對 Detail View 顯示的物件客製化（整個面版）

### 以 Struct Property 為例

接下來會針對下面這個 Struct 進行客製化。

```cpp=
USTRUCT()
struct FCoolNumber {
    GENERATED_BODY()

    UPROPERTY(EditAnywhere)
    int ThisNumberIsCool;
};
```

首先，繼承`IPropertyTypeCustomization`並定義`FCoolNumber_Customization`。 

```cpp=
class FCoolNumber_Customization : public IPropertyTypeCustomization
{
public:
    /// 對 Header 區塊客製化
    /// @param PropertyHandle - 指向要客製化的 Struct 變數
    /// @param HeaderRow - 將自定的 Widget 加入 HeaderRow 的 NameContent() 和 ValueContent()
    /// @param CustomizationUtils - 包含預設的字型
    void CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) override;

    /// 對 Children 區塊客製化
    /// @param PropertyHandle - 指向要客製化的 Struct 變數
    /// @param ChildBuilder - 用來建立 Children 區塊（藉由AddCustomRow()、AddProperty()、...）
    /// @param CustomizationUtils - 包含預設的字型
    void CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils) override;

    // 建立一個 FCoolNumber_Customization 的實例，之後「註冊」時會用到
    static TSharedRef<IPropertyTypeCustomization> CreateInstance() {
        return MakeShareable(new FCoolNumber_Customization);
    }

private:
    // 儲存 FCoolNumber::ThisNumberIsCool 的 Property Handle
    TSharedPtr<IPropertyHandle> ThisNumberIsCool;
};
```

在這個類別中要 override 兩個函數：
- `CustomizeHeader`
- `CustomizeChildren`

Header 指的是有向下箭號的那一列；而 Children 則是在 Header 下方，包含「設定 struct 的成員」的 widget。


:::spoiler CustomizeHeader 示例
在 `CustomizeHeader` 中，要將客製化的介面寫入參數`HeaderRow.NameContent()`內。

下面範例直接使用`CreatePropertyNameWidget`來生成預設的 Header。

```cpp=
void FCoolNumber_Customization::CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
    HeaderRow.NameContent()
    [
        PropertyHandle->CreatePropertyNameWidget()
    ];
}
```
:::

:::spoiler CustomizeChildren 示例
在 `CustomizeChildren` 內，要透過參數`ChildBuilder`來建立介面。

下方的示例是使用`ChildBuilder.AddCustomRow`來新增一個列，並分別在`NameContent()`和`ValueContent()`內加入內容。

※ 你可以用 `CreatePropertyNameWidget` 和 `CreatePropertyValueWidget` 來建立預設的 Name Content 和 Value Content

```cpp=
void FCoolNumber_Customization::CustomizeChildren(TSharedRef<IPropertyHandle> PropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
    // 拿到成員變數 ThisNumberIsCool 的 Property Handle
    ThisNumberIsCool = PropertyHandle->GetChildHandle("ThisNumberIsCool");

    // 檢查 Property Handle 是否合法
    if (ThisNumberIsCool.IsValid()) {
        ChildBuilder.AddCustomRow(FText::FromString("ThisNumberIsCool"))
        .NameContent()
        [
            SNew(STextBlock)
            .Text(FText::FromString("Wow a cool number"))
            // 設定字體
            .Font(CustomizationUtils.GetRegularFont())
        ]
        .ValueContent()
        [
            SNew(SSlider)
            .MinValue(0)
            .MaxValue(100)
            .Value_Lambda([this]() { // 從 Property Handle 讀值並顯示
                int n;
                ThisNumberIsCool->GetValue(n); 
                return n;
            })
            .OnValueChanged_Lambda([this](float n) { // 將新的值寫回 Property
                ThisNumberIsCool->SetValue((int)n);
            })
        ];
    }
}
```
:::

---

寫好 Customization 類別後，要把它註冊在 PropertyEditorModule。通常註冊的時機是在「你寫的模組」載入時。

```cpp=
class MyGameModule : public FDefaultGameModuleImpl {
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
};
```

```cpp=
void MyGameModule::StartupModule() {
    FPropertyEditorModule& M = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

    // 啟用 module 時註冊
    M.RegisterCustomPropertyTypeLayout(FCoolNumber::StaticStruct()->GetFName(),
        FOnGetPropertyTypeCustomizationInstance::CreateStatic( &FCoolNumber_Customization::CreateInstance ));
}
 
void MyGameModule::ShutdownModule() {
    FPropertyEditorModule& M = FModuleManager::Get().LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");

    // 關閉 module 時取消註冊
    M.UnregisterCustomPropertyTypeLayout(FCoolNumber::StaticStruct()->GetFName());
}

// 將 FooProject 替換成你的專案的名稱
// 如果你在寫Plugin請忽略下面這行
IMPLEMENT_PRIMARY_GAME_MODULE(MyGameModule, FooProject, "FooProject");
```

按照上面的示例完成後，只要你在 UObject 內加入型別為 `FCoolNumber` 的 Property，那麼 Detail View 會這樣顯示。

![image](https://hackmd.io/_uploads/H1o2CSy4kl.png)

※ Header 中的 Wow 是變數的名稱，這是`PropertyHandle->CreatePropertyNameWidget()` 建立的 widget 的預設行為

### 客製化整個Detail View

教學：<https://github.com/ibbles/LearningUnrealEngine/blob/master/Details%20Customization.md>

**Note:**
Unreal會先生成一個 Detail 面版 （使用 `UPROPERTY()` 的參數 + `IPropertyTypeCustomization`） ，然後才丟給 `IDetailCustomization` 的子類做進一步的客製化，所以在 `IDetailCustomization::CustomizeDetails` 內可以只針對要修改的 property 呼叫 `HideProperty` 然後再用 `AddProperty()`、`AddCustomRow()`等重新加入 GUI。

### 小結

不論是繼承`IPropertyTypeCustomization`還是`IDetailCustomization`，寫的方式其實蠻像的，主要是因為`IDetailChildrenBuilder`和`IDetailCategoryBuilder`有相似的介面。

- `AddProperty()`
   新增特定 property 的介面
- `AddProperty().CustomWidget()`
   新增 property，但`NameContent()`和`ValueContent()`的內容要自己寫
   和`AddCustomRow()`的差別在於：
   - `AddProperty().CustomWidget()` 會自動顯示 Reset to Default 的按鈕
   - `AddCustomRow()` 要自行設置
   ```cpp
    Builder.AddProperty(...).CustomWidget()
    .NameContent()
    [
        //...
    ]
    .ValueContent()
    [
        //...
    ];
    ```
- `AddCustomRow()`
   新增自定義的列
   
每一列中的常見設定，例如：

```cpp
Builder.AddCustomRow(...)
.NameContent()
[
    // 左邊（通常顯示 property 的名字）
]
.ValueContent()
[
    // 右邊（通常顯示修改 property 的值的 widget）
]
.ExtensionContent()
[
    // 右邊（放在 ValueContent() 中靠右的地方）
]
.WholeRowContent()
[
    // 直接讓 Widget 佔滿整列
]
// 覆寫 Reset to Default 的行為
.OverrideResetToDefault(FResetToDefault::Create(...));
```

# Reference

- [Medium - 建立Detail View](https://medium.com/@codekittah/custom-details-panels-in-unreal-engine-fpropertyeditormodule-6fe41ba7c339)
- [Unreal 4.27 官方教學 - Details面版自定義](https://dev.epicgames.com/documentation/zh-cn/unreal-engine/details-panel-customization?application_version=4.27)
  版本較舊，且4.27版不能單獨客製化Property，僅供參考
- [Learning Unreal Engine - Detail Customization](https://github.com/ibbles/LearningUnrealEngine/blob/master/Details%20Customization.md)

# FAQ

Q1: `IDetailLayoutBuilder::GetProperty()` 找不到Property

- [IDetailLayoutBuilder.GetProperty Not Work](https://forums.unrealengine.com/t/idetaillayoutbuilder-getproperty-does-not-work-when-the-path-refers-to-members-of-a-class/503448)
- [[UE5.4] [PropertyEditor] How to get property handle from IDetailLayoutBuilder?](https://forums.unrealengine.com/t/ue5-4-propertyeditor-how-to-get-property-handle-from-idetaillayoutbuilder/2176997)

A：`UPROPERTY(EditAnywhere)`

---

Q2. 如何做出像[TitleProperty](https://benui.ca/unreal/uproperty/#titleproperty)的效果

A： 當 struct 顯示在陣列內時，`HeaderRow.ValueContent()` 的內容會被放在原本 TitleProperty 會顯示的位置。範例如下：

```cpp=
void Foo_Customization::CustomizeHeader(TSharedRef<IPropertyHandle> PropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils)
{
    // 如果在陣列內，回傳的 index 不會是 INDEX_NONE
    if (PropertyHandle->GetArrayIndex() != INDEX_NONE) {
        HeaderRow
        .NameContent()
        [
            // 包含陣列操作用的widget
            PropertyHandle->CreatePropertyNameWidget()
        ]
        .ValueContent()
        [
            // 讓文字方塊垂直置中
            SNew(SBox)
            .VAlign(VAlign_Center)
            [
                // 更進階的做法是透過 PropertyHandle 來取得 Struct 內特定 Property 的值
                SNew(STextBlock)
                .Text(FText::FromString("Title"))
                .Font(CustomizationUtils.GetRegularFont())
            ]
        ];
    }
}
```

---

Q3. 如果 UStruct 只有一個成員，當它作為 property 顯示時，如何不顯示 Header Row？

A: 有兩種作法

1. 直接將該成員的 value widget 放在 `HeaderRow.ValueContent()` 內。
   可以參考`FBoneReferenceCustomization::CustomizeHeader`的作法，
2. 不在 HeaderRow 內加入任何東西，來隱藏 HeaderRow

第一種做法是 Unreal 慣用的做法，應該比較好。

---

Q4. 有條件的顯示／隱藏／啟用某個 property 的 GUI。

A:

- [EditCondition](https://benui.ca/unreal/uproperty/#editcondition) - 只有滿足特定條件才能編輯
- [EditConditionHides](https://benui.ca/unreal/uproperty/#editconditionhides) - 如果條件不滿足，不但不能編輯，也不顯示

EditCondition 能寫的判斷非常有限，如：判斷某個 bool 型別的 property 是否為 true、判斷型別為 enum 的 property 是否為特定的值。

更複雜的判斷需要在 `IDetailCustomization` 中自行實作，例如可以透過 `AddProperty().Visibility(...)` 或 `AddCustomRow().Visibility(...)` 來設定某一列顯示的條件。

`Visibility()` 的參數型別為 `TAttribute<EVisibility>`，你可以直接傳遞型別為 `EVisibility` 的值，或者用 `TAttribute<EVisibility>::CreateLambda()` 等來將函數封裝成 `TAttribute<EVisibility>`。封裝函數會使 widget 的可見性為動態的，unreal 每隔一段時間會執行被封裝的函數以判斷 widget 是否可見。

- `EVisibility::Visible` - 可見
- `EVisibility::Hidden` - 不可見，但仍然佔用空間
- `EVisibility::Collapsed` - 不可見，且不佔用空間


