Skip to content

How tree view is done

In this section we will talk about how we implemented the tree view that can be seen in the side bar of hte explorer level

Data files

To implement tree view we need to have data that will be displayed there for this purpose 2 new classes were created. Each of those classes is inhering UObject so that we can later interpret and visualize these data.

CPP_TreeViewEntry

This is representing the roots of the tree. This is data that the class contains

FString FolderName - name of the folder that will be displayed

bool IsChild - is the folder child of another folder ?

int Depth - depth inside the tree, this will determine the padding of the folder

TArray<UCPP_TreeViewEntryChild*> Children - children of the folder componet that are visible in the scene and user can interact with

TArray<UCPP_TreeViewEntry*> SubFolders - other subfolders (not implemented and you can try to implement sub folders )

In addtion methods that are used to populate this class can be found as well. The methods are self explanatory and documented in code so i will not be explaineing them here.

We have used Builder pattern to aid us with readability since each creational method like SetActor or SetName returns reference to itself meaning we can chain call those functions like following

builder->SetName("name")->SetActor(actor)->SetComponent(component)->Create()

CPP_TreeViewChildEntry

Since we have two distinct visual components that display different information to the user, we needed to create a separate data class purely for the child components of the tree view.

This class bridges the gap between the user interface and world interaction by referencing the actual components in the scene.

The class contains the following properties:

  • UCPP_TreeViewEntry* Parent: The parent of this child entry.
  • AActor* BodyPart: A general reference to the body part. This is a pointer to the Blueprint that holds the skeletal mesh for rendering and the static mesh for the picker. For more details, see the Adding New Parts to the Model section.
  • UMeshComponent* BodyPartComponent: The mesh component it references. This could be either a skeletal or a static mesh component. We've chosen to allow you to pick either type, in case you want to add a picker mesh later.
  • FString Name: The name of the component.
  • int Depth: The depth value, used to calculate the padding of the children in the tree view.

This class also contains helper functions that are self-explanatory and return a reference to the class itself for method chaining.

These classes are primarily used for holding the data and for the creation of the tree view itself.

Tree View Widget

This widget lacks comprehensive documentation, but it shows great potential for displaying deeply nested data structures. We have only scratched the surface of what this widget is capable of, and we recommend exploring it further if you want to leverage its full potential.

The tree view widget is represented in C++ by the CPP_TreeView class, which inherits from the UUserWidget class. It contains a pointer to the TreeViewComponent, which must be bound to work properly.

In this class, we do the following: - Populate the data structure described above. - Push the populated data structure to the tree view.

Simple Example

Let’s suppose you want to add muscles. We'll assume you’ve already followed the steps in the Adding New Parts to the Model chapter. To add a new entry to the tree, you need to go to the NativePreconstruct function and use the provided helper function to push the new entry to the array of tree entries.

    TreeViewEntries.Push(CreateTreeViewEntry("Muscles", FName("Muscles")));

The CreateTreeViewEntry function accepts two main parameters:

  • The name of the folder to be displayed in the tree view.
  • The tag associated with the Blueprint (or Actor) that holds: Skeletal mesh as the merged body part. Static meshes as its children (picker meshes).

This function works by retrieving all actors that match the provided tag. It then iterates over their child components and creates a CPP_TreeViewEntryChild for each SkeletalMeshComponent associated with the retrieved Blueprint (or Actor).

This process highlights one of the main reasons it's crucial to follow the standards we've established. If you decide to change these standards, you'll need to update the relevant classes accordingly. Otherwise, the tree view won't function as intended.

If all operations are successful (check the console for any warnings or errors), the new folder will appear in the tree view component. If any child components were found, they will appear under the folder as well.

During the NativeConstruct event, we iterate over all root-level items and execute the AddItem function on the UTreeView component. This populates Unreal Engine's internal data structure and additionally triggers the NativeOnListItemObjectSet function of the IUserObjectListEntry interface.

The importance of NativeOnListItemObjectSet will be discussed further below.

To specify the UUserWidgetBluerpint that is going to be displayed as an itme of the tree view we have configured the ListEntires of the TreeViewComponet to be WBP_TreeViewItem

Tree view entry widget

Associated classes CPP_TreeViewEntry, CPP_TreeViewEntryWidget,WBP_TreeViewEntryWidget

To style the folder entries in the tree view, we created a new class called CPP_TreeViewEntryWidget. This class inherits from UUserWidget and implements the IUserObjectListEntry interface. Additionally, we created a Blueprint class based on CPP_TreeViewEntryWidget to customize the appearance of the entries.

Within CPP_TreeViewEntryWidget, you’ll find bindings to components that are populated with data defined earlier. For instance, consider the folder label text. How does Unreal Engine know what text to display there? Suppose we are adding a folder named "Muscles" — how do we assign this label to the text field?

To accomplish this, we implement the IUserObjectListEntry interface, which includes an event triggered each time AddItem is called. This event receives item data as a UObject parameter, which we can then cast to the appropriate type, in this case, CPP_TreeViewEntry. By casting the data, we gain access to the child properties previously documented.

Since we’ve set up bindings to the text box, we can easily control the text that displays. In practice, this setup is straightforward and works as follows:

// UCPP_TreeViewEntry.cpp

void UCPP_TreeViewEntryWidget::NativeOnListItemObjectSet(UObject* ListItemObject)
{
    IUserObjectListEntry::NativeOnListItemObjectSet(ListItemObject);

    auto TreeViewEntryData = Cast<UCPP_TreeViewEntry>(ListItemObject);

    FolderName->SetText(FText::FromString(TreeViewEntryData->GetFolderName()));

    // explained later 
    for (auto &Child : TreeViewEntryData->GetChildren())
    {
        Children->AddItem(Child);
    }
}

We were not able to figure out how to use tree view to its full extend but you can. This means that to display children of the CPP_TreeViewEntry we have used another build in wiget called ListView this widget works similuarly to the TreeView in a sense that it can display N componentes that have the same styling and that we can choose styling that we want thanks to the UMG

What this means in practise is that the parent a.k.a (UCPP_TreeViewEntryWidget) holds a bindable pointer to the ListView which we can populate with children passed to it during creation process

Simulation event graph
Tree view item holds list view where each child that represents the skeletal mesh is located

Tree view child entry widget

Associated Classes: CPP_TreeViewEntryChild, CPP_TreeViewEntryChildWidget, WBP_TreeViewEntryChildWidget, MeshSelector

As mentioned earlier, each TreeView entry contains a list view, representing the skeletal meshes that can be interacted with. To populate this list view, we created the CPP_TreeViewEntryChild class, which manages both the data and visual appearance of items within the ListView.

This process follows the same approach used for tree view items. The only difference is that we now iterate through the Children field of the CPP_TreeViewEntry class and execute AddItem on the bound pointer of the ListView.

void UCPP_TreeViewChildEntryWidget::NativeOnListItemObjectSet(UObject* ListItemObject)
{
    IUserObjectListEntry::NativeOnListItemObjectSet(ListItemObject);
    if(ListItemObject)
    {
        // cast from UObject to *CPP_TreeViewEntryChild to get the right data 
        auto TreeViewChildEntry = Cast<UCPP_TreeViewEntryChild>(ListItemObject);
        ActorName->SetText(FText::FromString(TreeViewChildEntry->GetName()));
        ReferencingActor = TreeViewChildEntry->GetActor();
        ReferencingComponent = TreeViewChildEntry->GetComponent();
    }
}

As seen in the implementation, both the referencing actor and the specific component are stored, allowing efficient access to both the Blueprint that contains the picker and merged meshes, as well as the pointer to the individual merged body part. This design is more efficient than repeatedly retrieving all children and helps reduce memory bandwidth usage.

The logic for hiding child elements is straightforward. A key detail is that when we hide the skeletal mesh, we also disable the collision on all its child components (picker meshes) to ensure they are truly perceived as hidden. This is implemented inside the HideAll and ShowAll functions in C++ and they are called from the Blueprint for better organizations and maintainability.

Highlighting is managed through the MeshSelector class, which we retrieve as a pointer using the AnatomyUtils helper namespace. Additionally, we pass the component (the skeletal mesh) referenced by the child element as a parameter to the highlight function.

void UCPP_TreeViewChildEntryWidget::Highlight()
{
    AnatomyUtils::GetMeshSelector(GetWorld())->HighlightComponent(ReferencingComponent);
}