Slate, Tree View Widget, Ex: In-Editor File Structure Explorer
Contents
Overview
Dear Community,
Here's a tutorial to help you get started with Slate tree views!
This is the widget that has those arrows you can click to expand/contract subcategories!
In this example, I was making a hard drive file structure viewer, and I saved off a version that would be a great starting point :)
Video
This video shows the entire finished code that this code I am giving you here was the foundation for!
I wanted to keep this example simple, and also, not too implementation-specific.
You can use this coding as a starting point for your project-specific tree-view needs!
<youtube>https://www.youtube.com/watch?v=x40jXCy8dvk</youtube>
The Core Data
Trees are branching structures composed of a fundamental node data structure.
Here is that data structure for this example:
//Tree view data structure by Rama
#pragma once
typedef TSharedPtr< class FDDFileTreeItem > FDDFileTreeItemPtr;
/**
* The Data for a single node in the Directory Tree
*/
class FDDFileTreeItem
{
public:
/** @return Returns the parent or NULL if this is a root */
const FDDFileTreeItemPtr GetParentCategory() const
{
return ParentDir.Pin();
}
/** @return the path on hard disk, read-only */
const FString& GetDirectoryPath() const
{
return DirectoryPath;
}
/** @return name to display in file tree view! read-only */
const FString& GetDisplayName() const
{
return DisplayName;
}
/** @return Returns all subdirectories, read-only */
const TArray< FDDFileTreeItemPtr >& GetSubDirectories() const
{
return SubDirectories;
}
/** @return Returns all subdirectories, read or write */
TArray< FDDFileTreeItemPtr >& AccessSubDirectories()
{
return SubDirectories;
}
/** Add a subdirectory to this node in the tree! */
void AddSubDirectory(const FDDFileTreeItemPtr NewSubDir)
{
SubDirectories.Add(NewSubDir);
}
public:
/** Constructor for FDDFileTreeItem */
FDDFileTreeItem(const FDDFileTreeItemPtr IN_ParentDir, const FString& IN_DirectoryPath, const FString& IN_DisplayName)
: ParentDir( IN_ParentDir)
, DirectoryPath( IN_DirectoryPath)
, DisplayName( IN_DisplayName)
{
}
private:
/** Parent item or NULL if this is a root */
TWeakPtr< FDDFileTreeItem > ParentDir;
/** Full path of this directory in the tree */
FString DirectoryPath;
/** Display name of the category */
FString DisplayName;
/** Child categories */
TArray< FDDFileTreeItemPtr > SubDirectories;
};
Slate .h
// File Tree Viewer by Rama
#pragma once
//DD File Tree Item
#include "DDFileTreeItem.h"
//~~~ Forward Declarations ~~~
class UDDEdEngine;
typedef STreeView< FDDFileTreeItemPtr > SDDFileTreeView;
/**
* File Tree View
*/
class SDDFileTree : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS( SDDFileTree )
{}
SLATE_END_ARGS()
//~~~~~~~~
// DDEdEngine
//~~~~~~~~
public:
//owns this
TWeakObjectPtr<class UDDEdEngine> DDEdEngine;
/** Refresh the Tree */
//bool DoRefresh;
//~~~
public:
/** Widget constructor */
void Construct( const FArguments& Args, TWeakObjectPtr<class UDDEdEngine> IN_DDEdEngine );
/** Destructor */
~SDDFileTree();
/** @return Returns the currently selected category item */
FDDFileTreeItemPtr GetSelectedDirectory() const;
/** Selects the specified category */
void SelectDirectory( const FDDFileTreeItemPtr& CategoryToSelect );
/** @return Returns true if the specified item is currently expanded in the tree */
bool IsItemExpanded( const FDDFileTreeItemPtr Item ) const;
private:
/** Called to generate a widget for the specified tree item */
TSharedRef<ITableRow> DDFileTree_OnGenerateRow( FDDFileTreeItemPtr Item, const TSharedRef<STableViewBase>& OwnerTable );
/** Given a tree item, fills an array of child items */
void DDFileTree_OnGetChildren( FDDFileTreeItemPtr Item, TArray< FDDFileTreeItemPtr >& OutChildren );
/** Called when the user clicks on an item, or when selection changes by some other means */
void DDFileTree_OnSelectionChanged( FDDFileTreeItemPtr Item, ESelectInfo::Type SelectInfo );
/** Rebuilds the category tree from scratch */
void RebuildFileTree();
/** SWidget overrides */
virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) OVERRIDE;
private:
/** The tree view widget*/
TSharedPtr< SDDFileTreeView > DDFileTreeView;
/** The Core Data for the Tree Viewer! */
TArray< FDDFileTreeItemPtr > Directories;
};
Constructor and DDEdEngine
In my case I am calling this widget from within a custom version of the engine via a custom class that extends UnrealEdEngine.
I am passing in pointer to the UnrealEdEngine when the widget is first constructed.
You could use your HUD class instead!
I dont rely on any EdEngine functionality for this code sample, replace EdEngine with your own chosen parenting class.
/** Widget constructor */
void Construct( const FArguments& Args, TWeakObjectPtr<class UDDEdEngine> IN_DDEdEngine );
Instancing the Slate Widget
Here's an example of creating the widget:
//in the .h of parent class:
//TSharedPtr<class SDDFileTree> DDFileTree;
//in your parent class in which you want to create the widget
if( !DDFileTree.IsValid() )
{
SAssignNew( DDFileTree, SDDFileTree, Cast<UDDEdEngine>(this) );
}
Slate .cpp
// File Tree Viewer by Rama
#include "VictoryGame.h"
//This header
#include "SDDFileTree.h"
//The Data
#include "DDFileTreeItem.h"
void SDDFileTree::Construct(const FArguments& Args,TWeakObjectPtr<class UDDEdEngine> IN_DDEdEngine)
{
//Set DDEdEngine
DDEdEngine = IN_DDEdEngine;
//~~~~~~~~~~~~~~~~~~~
//Build Core Data
RebuildFileTree();
//Build the tree view of the above core data
DDFileTreeView =
SNew( SDDFileTreeView )
// For now we only support selecting a single folder in the tree
.SelectionMode( ESelectionMode::Single )
.ClearSelectionOnClick( false ) // Don't allow user to select nothing.
.TreeItemsSource( &Directories )
.OnGenerateRow( this, &SDDFileTree::DDFileTree_OnGenerateRow )
.OnGetChildren( this, &SDDFileTree::DDFileTree_OnGetChildren )
.OnSelectionChanged( this, &SDDFileTree::DDFileTree_OnSelectionChanged )
;
/*
// Expand the root by default
for( auto RootDirIt( Directories.CreateConstIterator() ); RootDirIt; ++RootDirIt )
{
const auto& Dir = *RootDirIt;
DDFileTreeView->SetItemExpansion( Dir, true );
}
// Select the first item by default
if( Directories.Num() > 0 )
{
DDFileTreeView->SetSelection( Directories[ 0 ] );
}
*/
ChildSlot.Widget = DDFileTreeView.ToSharedRef();
}
SDDFileTree::~SDDFileTree()
{
}
void SDDFileTree::RebuildFileTree()
{
Directories.Empty();
//~~~~~~~~~~~~~~~~~~~
//Root Level
TSharedRef<FDDFileTreeItem> RootDir = MakeShareable(new FDDFileTreeItem(NULL, TEXT("RootDir"), FString("RootDir")));
Directories.Add( RootDir );
TSharedRef<FDDFileTreeItem> RootDir2 = MakeShareable(new FDDFileTreeItem(NULL, TEXT("RootDir2"), FString("RootDir2")));
Directories.Add( RootDir2 );
//~~~~~~~~~~~~~~~~~~~
//Root Category
FDDFileTreeItemPtr ParentCategory = RootDir;
//Add
FDDFileTreeItemPtr EachSubDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Joy", "Joy"));
RootDir->AddSubDirectory(EachSubDir);
//Add
EachSubDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Song", "Song"));
RootDir->AddSubDirectory(EachSubDir);
//Add
FDDFileTreeItemPtr SongDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Dance", "Dance"));
EachSubDir->AddSubDirectory(SongDir);
//Add
SongDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Rainbows", "Rainbows"));
EachSubDir->AddSubDirectory(SongDir);
//Add
EachSubDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Butterflies", "Butterflies"));
RootDir->AddSubDirectory(EachSubDir);
//Refresh
if( DDFileTreeView.IsValid() )
{
DDFileTreeView->RequestTreeRefresh();
}
}
TSharedRef<ITableRow> SDDFileTree::DDFileTree_OnGenerateRow( FDDFileTreeItemPtr Item, const TSharedRef<STableViewBase>& OwnerTable )
{
if(!Item.IsValid())
{
return SNew( STableRow< FDDFileTreeItemPtr >, OwnerTable )
[
SNew(STextBlock)
.Text( FString("THIS WAS NULL SOMEHOW") )
];
}
return SNew( STableRow< FDDFileTreeItemPtr >, OwnerTable )
[
SNew(STextBlock)
.Text( Item->GetDisplayName() )
.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), 12))
.ColorAndOpacity(FLinearColor(1,0,1,1))
.ShadowColorAndOpacity(FLinearColor::Black)
.ShadowOffset(FIntPoint(-2, 2))
];
}
void SDDFileTree::DDFileTree_OnGetChildren( FDDFileTreeItemPtr Item, TArray< FDDFileTreeItemPtr >& OutChildren )
{
const auto& SubCategories = Item->GetSubDirectories();
OutChildren.Append( SubCategories );
}
//Key function for interaction with user!
void SDDFileTree::DDFileTree_OnSelectionChanged( FDDFileTreeItemPtr Item, ESelectInfo::Type SelectInfo )
{
//Selection Changed! Tell DDEdEngine!
UE_LOG(YourLog,Warning,TEXT("Item Selected: %s"), *Item->GetDisplayName());
}
FDDFileTreeItemPtr SDDFileTree::GetSelectedDirectory() const
{
if( DDFileTreeView.IsValid() )
{
auto SelectedItems = DDFileTreeView->GetSelectedItems();
if( SelectedItems.Num() > 0 )
{
const auto& SelectedCategoryItem = SelectedItems[ 0 ];
return SelectedCategoryItem;
}
}
return NULL;
}
void SDDFileTree::SelectDirectory( const FDDFileTreeItemPtr& CategoryToSelect )
{
if( ensure( DDFileTreeView.IsValid() ) )
{
DDFileTreeView->SetSelection( CategoryToSelect );
}
}
//is the tree item expanded to show children?
bool SDDFileTree::IsItemExpanded( const FDDFileTreeItemPtr Item ) const
{
return DDFileTreeView->IsItemExpanded( Item );
}
void SDDFileTree::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime )
{
// Call parent implementation
SCompoundWidget::Tick( AllottedGeometry, InCurrentTime, InDeltaTime );
//can do things here every tick
}
Important Functions
On Generate Row
This is the function that decides how each item in the tree gets displayed visually!
You could make it so each item in the tree is its own very fancy Slate widget, I just used TextBlock.
TSharedRef<ITableRow> SDDFileTree::DDFileTree_OnGenerateRow( FDDFileTreeItemPtr Item, const TSharedRef<STableViewBase>& OwnerTable )
{
if(!Item.IsValid())
{
return SNew( STableRow< FDDFileTreeItemPtr >, OwnerTable )
[
SNew(STextBlock)
.Text( FString("THIS WAS NULL SOMEHOW") )
];
}
return SNew( STableRow< FDDFileTreeItemPtr >, OwnerTable )
[
SNew(STextBlock)
.Text( Item->GetDisplayName() )
.Font(FSlateFontInfo(FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), 12))
.ColorAndOpacity(FLinearColor(1,0,1,1))
.ShadowColorAndOpacity(FLinearColor::Black)
.ShadowOffset(FIntPoint(-2, 2))
];
}
On Get Children
Get children is specific to the Tree View Slate widget, and determines what appears when a tree view item is expanded using the arrow, assuming it has any children.
This function tells Tree view whether the node has any children!
Note that GetSubDirectories() is my own custom function for my data type, but because OnGetChildren uses it, the Tree View knows when one of my custom data structure nodes has any children, and so the arrow should appear and the children should appear when the arrow is expanded.
void SDDFileTree::DDFileTree_OnGetChildren( FDDFileTreeItemPtr Item, TArray< FDDFileTreeItemPtr >& OutChildren )
{
const auto& SubCategories = Item->GetSubDirectories();
OutChildren.Append( SubCategories );
}
User Input
When the user clicks on an item in the tree view, this function below is run!
void SDDFileTree::DDFileTree_OnSelectionChanged( FDDFileTreeItemPtr Item, ESelectInfo::Type SelectInfo )
{
//Selection Changed! Tell DDEdEngine!
UE_LOG(YourLog,Warning,TEXT("Item clicked! %s"), *Item->GetDisplayName());
}
Core Function
The core function where the tree is made is below!
void SDDFileTree::RebuildFileTree()
{
Directories.Empty();
//~~~~~~~~~~~~~~~~~~~
//Root Level
TSharedRef<FDDFileTreeItem> RootDir = MakeShareable(new FDDFileTreeItem(NULL, TEXT("RootDir"), FString("RootDir")));
Directories.Add( RootDir );
TSharedRef<FDDFileTreeItem> RootDir2 = MakeShareable(new FDDFileTreeItem(NULL, TEXT("RootDir2"), FString("RootDir2")));
Directories.Add( RootDir2 );
//~~~~~~~~~~~~~~~~~~~
//Root Category
FDDFileTreeItemPtr ParentCategory = RootDir;
//Add
FDDFileTreeItemPtr EachSubDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Joy", "Joy"));
RootDir->AddSubDirectory(EachSubDir);
//Add
EachSubDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Song", "Song"));
RootDir->AddSubDirectory(EachSubDir);
//Add
FDDFileTreeItemPtr SongDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Dance", "Dance"));
EachSubDir->AddSubDirectory(SongDir);
//Add
SongDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Rainbows", "Rainbows"));
EachSubDir->AddSubDirectory(SongDir);
//Add
EachSubDir = MakeShareable(new FDDFileTreeItem(ParentCategory, "Butterflies", "Butterflies"));
RootDir->AddSubDirectory(EachSubDir);
//Refresh
if( DDFileTreeView.IsValid() )
{
DDFileTreeView->RequestTreeRefresh();
}
}
Core Data
The core data is set to be used in the constructor, here:
.TreeItemsSource( &Directories )
Summary
Now you know the basics of using a tree view!
You need
1. your own custom data structure that is a node in the tree
2. tree view .h
3. tree view .cpp
and that's all!
Enjoy!