이전 학습부터 만들어왔던 Detail View의 커스텀작업을 마무리 해 보았다.
이전 학습에서 체크박스를 이용한 Equipment 데이터의 기본값을 설정할 수 있도록 구현해놨으니, 이번에는 Do Action Data의 내용과, Hit Data의 내용을 출력해보았다.
💻 Details View에 Do Action Data, Hit Data 추가
먼저, 두 데이터들의 내용을 Details View에 출력하기 위해 SDoActionData, SHitData 클래스를 작성하였다.


두 클래스 모두 EquipmentData와 마찬가지로 속성 값을 커스터마이징하는것이기 때문에 IPropertyTypeCustomization을 상속받았고 두 클래스간의 소스코드는 거의 동일하므로 아래부터는 Do Action Data를 나타내는 방법에 대해서만 설명하였다.
📄 SDoActionData.cpp
#include "SDoActionData.h"
#include "SWeaponCheckBoxes.h"
#include "DetailWidgetRow.h"
#include "IDetailChildrenBuilder.h"
#include "IDetailPropertyRow.h"
TArray<TSharedPtr<class SWeaponCheckBoxes>> SDoActionData::CheckBoxes;
TSharedRef<IPropertyTypeCustomization> SDoActionData::MakeInstance()
{
return MakeShareable(new SDoActionData());
}
TSharedPtr<SWeaponCheckBoxes> SDoActionData::AddCheckBoxes()
{
TSharedPtr<SWeaponCheckBoxes> checkBoxes = MakeShareable(new SWeaponCheckBoxes());
int32 index = CheckBoxes.Add(checkBoxes);
return CheckBoxes[index];
}
void SDoActionData::EmptyCheckBoxes()
{
for (TSharedPtr<SWeaponCheckBoxes> ptr : CheckBoxes)
{
if (ptr.IsValid())
ptr.Reset();
}
CheckBoxes.Empty();
}
void SDoActionData::CustomizeHeader(TSharedRef<IPropertyHandle> InPropertyHandle, FDetailWidgetRow& InHeaderRow, IPropertyTypeCustomizationUtils& InCustomizationUtils)
{
if(CheckBoxes.Num() > 0)
{
int32 index = InPropertyHandle->GetIndexInArray(); // 현재 자기자신의 번호
CheckBoxes[index]->SetUtilities(InCustomizationUtils.GetPropertyUtilities()); // 새로고침을 위한 Utilities
InHeaderRow
.NameContent()
[
InPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
.MinDesiredWidth(FEditorStyle::GetFloat("StandardDialog.MinDesiredSlotWidth"))
.MaxDesiredWidth(FEditorStyle::GetFloat("StandardDialog.MaxDesiredSlotWidth"))
[
CheckBoxes[index]->Draw()
];
return;
}
/* 기본형 */
InHeaderRow
.NameContent()
[
InPropertyHandle->CreatePropertyNameWidget()
]
.ValueContent()
.MinDesiredWidth(FEditorStyle::GetFloat("StandardDialog.MinDesiredSlotWidth"))
.MaxDesiredWidth(FEditorStyle::GetFloat("StandardDialog.MaxDesiredSlotWidth"))
[
InPropertyHandle->CreatePropertyValueWidget()
];
}
void SDoActionData::CustomizeChildren(TSharedRef<IPropertyHandle> InPropertyHandle, IDetailChildrenBuilder& InChildBuilder, IPropertyTypeCustomizationUtils& InuCustomizationUtils)
{
if(CheckBoxes.Num() > 0)
{
int32 index = InPropertyHandle->GetIndexInArray();
CheckBoxes[index]->DrawProperties(InPropertyHandle, &InChildBuilder);
return;
}
/* 기본형 */
uint32 number = 0;
InPropertyHandle->GetNumChildren(number); // 자식의 개수
for (uint32 i = 0; i < number; i++)
{
TSharedPtr<IPropertyHandle> handle = InPropertyHandle->GetChildHandle(i);
IDetailPropertyRow& row = InChildBuilder.AddProperty(handle.ToSharedRef());
TSharedPtr<SWidget> name;
TSharedPtr<SWidget> value;
row.GetDefaultWidgets(name, value);
row.CustomWidget()
.NameContent()
[
name.ToSharedRef()
]
.ValueContent()
.MinDesiredWidth(FEditorStyle::GetFloat("StandardDialog.MinDesiredSlotWidth")) // 넓이 최솟값 고정
.MaxDesiredWidth(FEditorStyle::GetFloat("StandardDialog.MaxDesiredSlotWidth"))
[
value.ToSharedRef()
];
}
}
SDoActionData 클래스의 CustomizeHeader 함수와 CustomizeChildren 함수에서는 CheckBoxes가 1개 이상 있는지를 검사한 뒤, 존재한다면 해당 PropertyHandle의 데이터를 체크박스의 Name, Value로 초기화 해 주었다.
반대로 체크박스가 존재하지 않는다면 PropertyHandle의 기본형 데이터를 Name, Value로 초기화하여 출력하였다.
![]() |
![]() |
이후 SWeaponDetailView 클래스에 Do Action Data와 Hit Data의 카테고리와 항목들을 추가 및 배치하였다.
📂 배열 엘리먼트 추가/삭제 (UObject 클래스 함수)

Do Action Data의 내용들을 성공적으로 나타내고 나면, 이후 처리해야 하는 문제점은 엘리먼트의 추가/삭제이다.

엘리먼트를 추가하거나 삭제하면 SDoActionData 클래스의 CustomizeHeader에서 에러가 발생하는데, 이는 CheckBoxes의 개수가 처음 생성된 이후로 고정되어있기 때문이다.
따라서 배열의 개수가 증가하거나 감소함에 따라 체크박스의 개수도 함께 변화되도록 추가적인 처리를 해 주었다.

CWeaponAsset의 부모인 DataAsset의 부모인 UObject 클래스를 살펴보면 위와 같은 함수들이 존재한다.
void PreEditChange(FProperty* PropertyAboutToChange) : 자기 자신의 Property 하나에 대한 변화 시 호출
void PreEditChange( class FEditPropertyChain& PropertyAboutToChange) : Chain 배열 데이터가 변화 시 호출
bool CanEditChange( const FProperty* InProperty ) const : Property를 변경할 수 있는지
void PostEditChange() : Property에 대한 변화가 일어난 이후 호출
void PostEditChangeProperty( struct FPropertyChangedEvent& PropertyChangedEvent) : Property 하나에 대한 변화 이후에 호출
void PostEditChangeChainProperty( struct FPropertyChangedChainEvent& PropertyChangedEvent) : Chain 배열 데이터의 변화 이후에 호출
void PreEditUndo() / void PostEditUndo() : Ctrl+z 되돌리기 사용 이전/이후 호출
나는 배열의 엘리먼트 삽입/삭제를 처리하므로 PostEditChangeChainProperty 함수를 오버라이드하여 사용하였다.
🚩 PostEditChangeChainProperty

Property에 대한 설정은 Editor 모드에서 수행되어야 하는데 UCWeaponAsset 클래스는 런타임 모드로 작성된 cpp 파일이기 때문에 #if WITH_EDITOR ~ #endif 구문으로 에디터 모드에서 함수가 실행될 수 있도록 하였다.
따라서 FApp::IsGame() 함수로 런타임모드라면 함수가 진행되지 않도록 검사하였다.
이후 PropertyChangedEvent 구조체의 GetPropertyName을 가져오면 변경된 항목의 Property 이름이 반환되므로, Do Action Data가 변경되었는지 Hit Data가 변경되었는지 구분할 수 있다.

EPropertyChangeType 은 Property가 어떻게 변화된 상태인지를 분류하는 열거형이다.
1에 대한 쉬프트 연산으로 Type의 중복 상태를 체크할 수 있도록 되어있다.
ArrayAdd : 배열 항목 추가
ArrayRemove : 배열 항목 삭제
ArrayClear : 배열 전체 삭제
ValueSet : 값 설정
Duplicate : 복제
💡 깨알 쉬프트 연산 복습
1 << 0 : 1을 0만큼 왼쪽으로 밀어서 1(2) => 1
1 << 1 : 1을 1만큼 왼쪽으로 밀어서 10(2) => 2
1 << 2 : 1을 2만큼 왼쪽으로 밀어서 100(2) => 4
1 << 3 : 1을 3만큼 왼쪽으로 밀어서 1000(3) => 8
쉬프트 연산은 왼쪽으로 갈수록 2의 제곱으로 커지는것을 확인할 수 있었다.

DetailsView를 생성할 때 Details View의 Arguments를 "WeaponAssetEditorDetailsView"로 설정해두었기 때문에, FPropertyEditorModule 객체를 이용하여 FindDetailView 함수로 해당 디테일 뷰를 가져왔다.
이후, DetailsView가 존재한다면 해당 Detail View를 ForceRefresh로 갱신하였다.
🚩 정리하자면,
Details View에서 배열 엘리먼트를 삽입하거나 삭제하는 변화가 발생한 후에 CWeaponAsset 클래스의 PostEditChangeChainProperty 함수가 호출된다.
Post 함수가 호출되면서 Detail View를 갱신하여 다시 그리게 되고, 이는 SWeaponDetailsView 클래스를 다시 생성한다는 의미이다.
SWeaponDetailsView 클래스의 CustomizeDetails 함수가 호출될 때 if(RefreshByCheckBoxes가 false인 경우 SWeaponCheckBoxes 객체를 새로 생성하도록 구현해놨기 때문에 체크박스의 배열 개수가 갱신된 채로 새롭게 생성된다.
(RefreshByCheckBoxes 변수는 체크박스에 의해 갱신된 경우에만 true로 변환되기 때문에 Post 함수에서 갱신되었다고 true가 되지 않는다)
컴파일 후 프로그램을 실행시켜보면 배열 엘리먼트의 삽입/삭제가 성공적으로 이루어지는 것을 확인할 수 있었다.
