Filling a vertical box with widgets is super common in UMG. But depending
on the complexity or number of widgets, you might start noticing frame skips
when the list is first populated. UListView
, and its siblings UTileView
and
UTreeView
are the solution to this!
Naive Implementation
The simplest way to populate a list of items is to instantiate a widget for every item in the list. If your list is less than 10 or so elements, this is totally fine.
However if there are many more elements in the list than would be displayed
on-screen, then it's worth considering UListView
.
Let's see how our basic example works before diving into UListView
.
BUIInventoryPanelNaive.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "BUIInventoryPanelNaive.generated.h"
UCLASS(Abstract)
class UBUIInventoryPanelNaive : public UUserWidget
{
GENERATED_BODY()
protected:
virtual void NativeConstruct() override;
UPROPERTY( meta = ( BindWidget ) )
class UVerticalBox* EntriesVerticalBox;
UPROPERTY( EditDefaultsOnly )
TSubclassOf<class UBUIInventoryEntry> EntryClass;
};
BUIInventoryPanelNaive.cpp
#include "BUIInventoryPanelNaive.h"
#include "Components/VerticalBox.h"
void UBUIInventoryPanelNaive::NativeConstruct()
{
Super::NativeConstruct();
// Imagine we have an inventory class that provides us with the following:
TArray<UBUIInventoryItem*> Inventory;
for ( UBUIInventoryItem* Item : Inventory )
{
// Instantiate the widget
UBUIInventoryEntry* Entry = CreateWidget<UBUIInventoryEntry>( this, EntryClass );
// Set up its contents
Entry->InitializeFromInventoryItem( Item );
// Add it to the list
EntriesVerticalBox->AddChildToVerticalBox( Entry );
}
}
UListView
Implementation
UListView
is Unreal's way of using a re-usable pool of widgets.
It only creates as many widgets as are needed to fill the on-screen list areal. As a widget scrolls out of view, it is removed and added to a pool of available widgets to be re-used. So even if your list 100+ items long, it will only ever have a few widgets instantiated at any one time.
So how do we use it?
- Make the widget that will be showing each individual item implement the
IUserObjectListEntry
interface. - Use
SetListItems
to provideUObject
data to theUListView
instance. - Done!
We update our entry widget to implement the required interface:
BUIInventoryEntry
BUIInventoryEntry.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Blueprint/IUserObjectListEntry.h"
#include "BUIInventoryEntry.generated.h"
UCLASS(Abstract)
class UBUIInventoryEntry : public UUserWidget, public IUserObjectListEntry
{
private:
GENERATED_BODY()
protected:
// IUserObjectListEntry
virtual void NativeOnListItemObjectSet(UObject* ListItemObject) override;
// IUserObjectListEntry
UPROPERTY(meta=(BindWidget))
class UTextBlock* NameLabel;
UPROPERTY(meta=(BindWidget))
class UImage* IconImage;
};
BUIInventoryEntry.cpp
#include "BUIInventoryEntry.h"
#include "Components/Image.h"
#include "Components/TextBlock.h"
void UBUIInventoryEntry::NativeOnListItemObjectSet(UObject* ListItemObject)
{
UBUIInventoryItem* Item = Cast<UBUIInventoryItem>(ListItemObject);
NameLabel->SetText(Item->DisplayName);
IconImage->SetBrushFromTexture(Item->Icon);
}
BUIInventoryPanel
Our naive implementation's UVerticalBox
is replaced with UListView
. We also
no longer need a TSubclassOf<T>
property for our Entry. Instead we set that
directly in the InventoryListView properties (see screenshot below).
BUIInventoryPanel.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "BUIInventoryPanel.generated.h"
UCLASS(Abstract)
class UBUIInventoryPanel : public UUserWidget
{
GENERATED_BODY()
protected:
virtual void NativeConstruct() override;
UPROPERTY( meta = ( BindWidget ) )
class UListView* InventoryListView;
};
BUIInventoryPanel.cpp
#include "BUIInventoryPanel.h"
#include "Components/ListView.h"
void UBUIInventoryPanel::NativeConstruct()
{
Super::NativeConstruct();
// Imagine we have an inventory class that provides us with the following:
TArray<UBUIInventoryItem*> Inventory;
// Tell the list view what to use as the data source when creating widgets
InventoryListView->SetListItems(InventoryItems);
}
We also need to update our Widget Blueprint so that our new List View widget knows what Widget Blueprint to use when creating the list entries:
Done!
That's it, we're done, now we can have a list with hundreds of items and only ever have a few widgets instantiated at a time!
Tree View and Item View
There are similar widgets called Item View that presents widgets in a grid, and
Tree View that allows widgets to be expanded and collapsed. The C++
implementation is basically identical. They have the same SetListItems
function, there are just different options for how each entry is presented.
Icons used in screenshots are Isle of Lore 2: Status Icons by Steven Colling.