风蚀之月

Slate文件目录树基础实现

19 Aug 2015 UE4 Slate

目前UMG中并没有提供TreeView类,要使用TreeView必须使用Slate。

当前UE4版本4.8.3。

TreeView在UE4编辑器中被广泛的使用,但是出于某些不可知的原因无法在UMG中进行直接使用。鉴于制作复杂度较高的界面需要使用到Slate,对Slate进行专门的研究是有必要的。

本文参照Unreal官方社区文档Slate, Tree View Widget完成。

请注意:原始文档中并没有给出具体的文件浏览器实现,而是提供了作为其基础的目录树使用方式。

目录树数据结构

要实现目录树并不需要太复杂的数据结构,实现基本的功能即可。

TreeView中本身需要的数据是数组型的,这里定义的数据结构是节点使用的。

DDFileTreeItem.h

#pragma once

typedef TSharedPtr< class FDDFileTreeItem > FDDFileTreeItemPtr;

/**
* <目录树节点
*/

class FDDFileTreeItem
{
public:

/** @return <返回父节点 */
const FDDFileTreeItemPtr GetParentCategory() const
{
  return ParentDir.Pin();
}

/** @return <返回节点的路径【只读】 */
const FString& GetDirectoryPath() const
{
  return DirectoryPath;
}

/** @return <返回显示名称【只读】 */
const FString& GetDisplayName() const
{
  return DisplayName;
}

/** @return <返回子节点【只读】 */
const TArray< FDDFileTreeItemPtr >& GetSubDirectories() const
{
  return SubDirectories;
}

/** @return <返回可写入的子节点引用 */
TArray< FDDFileTreeItemPtr >& AccessSubDirectories()
{
  return SubDirectories;
}

/** <为当前节点添加子目录 */
void AddSubDirectory(const FDDFileTreeItemPtr NewSubDir)
{
  SubDirectories.Add(NewSubDir);
}

public:
/** <构造函数 */

FDDFileTreeItem(const FDDFileTreeItemPtr IN_ParentDir, const FString& IN_DirectoryPath, const FString& IN_DisplayName)
: ParentDir(IN_ParentDir)
, DirectoryPath(IN_DirectoryPath)
, DisplayName(IN_DisplayName)
{

}

private:

/** <父节点引用; */
TWeakPtr< FDDFileTreeItem > ParentDir;

/** <完整路径 */
FString DirectoryPath;

/** <显示名称 */
FString DisplayName;

/** <子节点数组 */
TArray< FDDFileTreeItemPtr > SubDirectories;

};

 

节点的数据结构较为简单,直接在头文件中对函数进行了实现。

目录树Slate控件

由于原始文档中提供的代码中引用的Engine是不必要的,故而在这里直接替换成了较为通用的HUD引用,这样就不会导致代码有项目相关性了。

这里先上代码,其他的一些说明放在代码后面。

SDDFileTree.h

#pragma once

/* <节点数据结构 */

#include "DDFileTreeItem.h"

typedef STreeView< FDDFileTreeItemPtr > SDDFileTreeView;

/**
* <目录树Slate
*/

class SDDFileTree : public SCompoundWidget
{

public:

SLATE_BEGIN_ARGS(SDDFileTree){}

SLATE_ARGUMENT(TWeakObjectPtr<class AHUD>, OwnerHUD)

SLATE_END_ARGS()

public:

TWeakObjectPtr<AHUD> OwnerHUD;

/** <刷新目录 */

//bool DoRefresh;

public:

void Construct(const FArguments& InArgs);

/** Destructor */
~SDDFileTree();

/** @return <返回当前被选中的目录 */
FDDFileTreeItemPtr GetSelectedDirectory() const;

/** <选择目录 */
void SelectDirectory(const FDDFileTreeItemPtr& CategoryToSelect);

/** @return <返回节点是否展开 */
bool IsItemExpanded(const FDDFileTreeItemPtr Item) const;

private:

/** <生成单个节点元素 */
TSharedRef<ITableRow> DDFileTree_OnGenerateRow(FDDFileTreeItemPtr Item, const TSharedRef<STableViewBase>& OwnerTable);

/** <获得子节点 */
void DDFileTree_OnGetChildren(FDDFileTreeItemPtr Item, TArray< FDDFileTreeItemPtr >& OutChildren);

/** <当选中项发生变化时 */
void DDFileTree_OnSelectionChanged(FDDFileTreeItemPtr Item, ESelectInfo::Type SelectInfo);

/** <构建目录树数据 */
void RebuildFileTree();

/** <重写Tick方便以后实现目录刷新 */
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;

private:

/** <TreeView控件 */
TSharedPtr< SDDFileTreeView > DDFileTreeView;

/** <目录树的数据 */
TArray< FDDFileTreeItemPtr > Directories;

};

 

SDDFileTree.cpp

#include "Test_mp.h"
#include "SDDFileTree.h"
#include "DDFileTreeItem.h"

void SDDFileTree::Construct(const FArguments& InArgs)
{
  OwnerHUD = InArgs._OwnerHUD;
  RebuildFileTree(); /* <构建目录树数据 */

  //Build the tree view of the above core data
  DDFileTreeView =
    SNew(SDDFileTreeView)
    .SelectionMode(ESelectionMode::Single) // 只允许选中一个项目
    .ClearSelectionOnClick(false) // 不允许不选中内容
    .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.AttachWidget(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(NSLOCTEXT("Your Namespace", "twns", "THIS WAS NULL SOMEHOW"))
      ];

  }

  return SNew(STableRow< FDDFileTreeItemPtr >, OwnerTable)
    [
      SNew(STextBlock)
        .Text(FText::FromString(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!
  UE_LOG(LogTemp, 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
}

代码的结构还是比较清晰的,这里需要注意的是,针对TreeView的事件是在控件本身生成的时候注册的。

其中,OnGenerateRow事件是STreeView和SListView的必须事件。当控件需要进行元素显示时,会对这个函数进行调用,获得需要显示的元素。

另外,代码中的OnSelectionChanged函数缺乏必要的安全检测,在使用中需要留意。

用于显示的数据是在Construct中通过调用RebuildFileTree()函数生成的。

最终结果

完成了上面的步骤之后,直接在HUD中对控件进行展示即可。

SAssignNew(DDFileTree, SDDFileTree).OwnerHUD(this);

if (GEngine->IsValidLowLevel())
{
  GEngine->GameViewport->AddViewportWidgetContent(SNew(SWeakWidget).PossiblyNullContent(DDFileTree.ToSharedRef()));
}

if (DDFileTree.IsValid())
{
  DDFileTree->SetVisibility(EVisibility::Visible);
}

启动预览,由于没有进行布局,可以在最上方看到目录树的展示。

[![U10$_IA57URMV_PO0]6JUC](https://blog.ch-wind.com/wp-content/uploads/2017/03/U10_IA57URMV_PO06JUC.png)

至此,目录树的基本功能已经实现。