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?

  1. Make the widget that will be showing each individual item implement the IUserObjectListEntry interface.
  2. Use SetListItems to provide UObject data to the UListView instance.
  3. 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.

Posted: