🌎️

【UE5】C++で自作データアセットの編集画面にツールバーを追加する

2024/12/12に公開

概要

この記事はUnreal Engine (UE) Advent Calendar 2024シリーズ1の12日目の記事です。

今回はC++を使って自作データアセットの編集画面を拡張するやり方についての記事となります。

想定するイメージとしては、データテーブルなど、一部のアセットの編集画面は独自の編集画面になっていて、詳細ビュー以外にもツールバーのボタンがあったりするので、今回はそれを自作することを目指します。

完成後のイメージはこちらとなります。

ツールバーにボタンを追加したデータアセットの編集画面

参考用にgitのリポジトリを作成したので、記事を見ながら適宜参考にしていただければ幸いです。
https://github.com/dariaGlint/adventcalender-2024-editor-extension

環境

UE5.4.4

やり方

必要なものは以下になります。

  • エディタモジュールを作成する
  • 専用のデータアセットのクラスを用意する
  • 作成したモジュールの中にあるIModuleInterfaceを実装してあるクラスにIHasToolBarExtensibilityインターフェースを実装する
  • アセットを開く編集画面を変えるためAssetDefinitionクラスを継承したクラスを作成する
  • 編集画面の見た目を変更するためFAssetToolKitを継承したクラスを作る

エディタモジュールを作成する

まずエディタモジュールを作成し、以下を追加します。

ExampleProject.Build.cpp
using UnrealBuildTool;

public class ExampleMessageTalkEditor : ModuleRules
{
    public ExampleMessageTalkEditor(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(
            new string[]
            {
                "Core", "ExampleMessageTalk",
            }
        );

        PrivateDependencyModuleNames.AddRange(
            new string[]
            {
                "CoreUObject",
                "Engine",
                "Slate",
                "SlateCore",
+               "AssetTools",
+               "AssetDefinition",
+               "UnrealEd"
+               "ExampleProject", //対象のデータアセットが別のモジュールにあるならそれを追加してください(今回はExampleProjectモジュールにある想定にしてます)
            }
        );
    }
}

専用のデータアセットを用意する

ツールバーにボタンを追加するデータアセットを作成します。
今回は配列の方が数が増えたかがわかりやすいと思い配列をもたせました。


#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "ExampleDataAsset.generated.h"

UCLASS()
class UExampleDataAsset : public UPrimaryDataAsset
{
    GENERATED_BODY()
public:
    UPROPERTY(EditAnywhere, BlueprintReadWrite)
    TArray<int> Values;
};

作成したモジュールの中にあるIModuleInterfaceを実装してあるクラスにIHasToolBarExtensibilityインターフェースを実装する

作成したエディタモジュールの中にIModuleInterfaceを実装してるモジュールクラスがあります
このモジュールクラスに対してツールバーの拡張をするためのインターフェースIHasToolBarExtensibilityを実装します。
ここで実装した内容は後で説明する編集画面の見た目をいじるクラスで使われます。

例:モジュール名がFExampleDataAssetEditorExtensionEditorだった場合

FExampleDataAssetEditorExtensionEditor.h
#pragma once

#include "CoreMinimal.h"
#include "ExampleDataAsset.h"
#include "Modules/ModuleManager.h"

class FExampleDataAssetEditorExtensionEditorModule : public IModuleInterface,
public IHasMenuExtensibility, public IHasToolBarExtensibility
{
public:
    virtual void StartupModule() override;
    virtual void ShutdownModule() override;
    void CreateExampleDataAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr< IToolkitHost >& InitToolkitHost,UExampleDataAsset* DataAsset);
    virtual TSharedPtr<FExtensibilityManager> GetMenuExtensibilityManager() override { return MenuExtensibilityManager;}
    virtual TSharedPtr<FExtensibilityManager> GetToolBarExtensibilityManager() override { return ToolBarExtensibilityManager;}

private:
    TSharedPtr<FExtensibilityManager> MenuExtensibilityManager;
    TSharedPtr<FExtensibilityManager> ToolBarExtensibilityManager;
};

FExampleDataAssetEditorExtensionEditor.cpp
#include "ExampleDataAssetEditorExtensionEditor.h"

#include "ExampleDataAssetEditor.h"

#define LOCTEXT_NAMESPACE "FExampleDataAssetEditorExtensionEditorModule"

void FExampleDataAssetEditorExtensionEditorModule::StartupModule()
{
    ToolBarExtensibilityManager = MakeShareable(new FExtensibilityManager);
    MenuExtensibilityManager = MakeShareable(new FExtensibilityManager);
}

void FExampleDataAssetEditorExtensionEditorModule::ShutdownModule()
{
    MenuExtensibilityManager.Reset();
    ToolBarExtensibilityManager.Reset();
}

void FExampleDataAssetEditorExtensionEditorModule::CreateExampleDataAssetEditor(const EToolkitMode::Type Mode,
	const TSharedPtr<IToolkitHost>& InitToolkitHost, UExampleDataAsset* DataAsset)
{
    TSharedRef<ExampleDataAssetEditor> NewDataTableEditor(new ExampleDataAssetEditor());
    NewDataTableEditor->InitExampleAssetDataEditor(Mode, InitToolkitHost, DataAsset);
}

#undef LOCTEXT_NAMESPACE
    
IMPLEMENT_MODULE(FExampleDataAssetEditorExtensionEditorModule, ExampleDataAssetEditorExtensionEditor)

アセットを開く編集画面を変えるためAssetDefinitionクラスを継承したクラスを作成する

アセットを開く編集画面を変えたいので、アセットを開くイベントから開く画面を変更させます。
アセットを開くイベントを行う場所はUAssetDefinitionでやります。
これはDataTableなど他のクラスでもその場所で行われていたからです。

AssetDefinition_ExampleDataAsset.h
#pragma once

#include "CoreMinimal.h"
#include "AssetDefinition.h"
#include "ExamplePrimaryDataAsset.h"
#include "UAssetDefinition_ExampleDataAsset.generated.h"

UCLASS()
class UAssetDefinition_ExampleDataAsset : public UAssetDefinition
{
    GENERATED_BODY()
public:
    //対象のクラスを指定する
    virtual TSoftClassPtr<UObject> GetAssetClass() const override { return UExamplePrimaryDataAsset::StaticClass(); }
    //アセットを開く
    virtual EAssetCommandResult OpenAssets(const FAssetOpenArgs& OpenArgs) const override;
    
    //以下2つは今回の主旨とは関係ないけど実装しないとハングするので実装する
    virtual FText GetAssetDisplayName() const override { return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_ExampleDataAsset", "ExampleDataAsset"); }
    virtual FLinearColor GetAssetColor() const override { return FLinearColor(FColor(62, 140, 35)); }
};
AssetDefinition_ExampleDataAsset.cpp
#include "AssetDefinition_ExampleDataAsset.h"

#include "ExampleDataAssetEditorExtensionEditor.h"

EAssetCommandResult UAssetDefinition_ExampleDataAsset::OpenAssets(const FAssetOpenArgs& OpenArgs) const
{
    TArray<UExampleDataAsset*> ExampleAssetToOpen;
    
    for (UExampleDataAsset* ExampleDataAsset : OpenArgs.LoadObjects<UExampleDataAsset>())
    {
        ExampleAssetToOpen.Add(ExampleDataAsset);
    }
    
    FExampleDataAssetEditorExtensionEditorModule& ExampleDataAssetEditorExtensionEditorModule = FModuleManager::LoadModuleChecked<FExampleDataAssetEditorExtensionEditorModule>("ExampleMessageTalkEditor");
    for (UExampleDataAsset* ExampleAsset : ExampleAssetToOpen)
    {
        ExampleDataAssetEditorExtensionEditorModule.CreateExampleDataAssetEditor(OpenArgs.GetToolkitMode(), OpenArgs.ToolkitHost, ExampleAsset);
    }
    
    return EAssetCommandResult::Handled;
}


詳細ビューとツールバーの表示を作成するクラスを作る

そのままだと何も表示されないので詳細ビューとツールバーを表示するように実装していきます。
ソースは以下となります。

ExampleDataAssetEditor.h
#pragma once
#include "ExampleDataAsset.h"

class ExampleDataAssetEditor : public FAssetEditorToolkit
{
public:
    virtual void InitExampleAssetDataEditor( const EToolkitMode::Type Mode, const TSharedPtr< class IToolkitHost >& InitToolkitHost,UExampleDataAsset* ExampleDataAsset);
    virtual void RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager) override;
    virtual void UnregisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager) override;
    void ExtendToolbar(TSharedPtr<FExtender> Extender);
    void FillToolbar(FToolBarBuilder& ToolbarBuilder);
    
    //以下4つは今回の主旨と外れるが実装しないとハングするため実装する
    virtual FName GetToolkitFName() const override;
    virtual FText GetBaseToolkitName() const override;
    virtual FString GetWorldCentricTabPrefix() const override;
    virtual FLinearColor GetWorldCentricTabColorScale() const override;
    
    TSharedRef<SDockTab> SpawnTab_DataTableDetails(const FSpawnTabArgs& Args);
    virtual void CreateAndRegisterDataTableDetailsTab(const TSharedRef<class FTabManager>& InTabManager);
    
    static const FName DetailsTabId;
    
    UExampleDataAsset* GetEditableExampleDataAsset() const;
private:
    TSharedPtr<class IDetailsView> PropertyView;
    
    bool CanAddItem() const;
    void AddItem_Execute();

};
ExampleDataAssetEditor.cpp
#include "ExampleDataAssetEditor.h"

#include "ExampleDataAssetEditorExtensionEditor.h"
#define LOCTEXT_NAMESPACE "ExampleDataAssetEditor"

const FName ExampleDataAssetEditor::DetailsTabId("ExampleDataAssetToolKit_ExampleDataAssetDetails");

void ExampleDataAssetEditor::InitExampleAssetDataEditor(const EToolkitMode::Type Mode,
	const TSharedPtr<class IToolkitHost>& InitToolkitHost, UExampleDataAsset* ExampleDataAsset)
{
    TSharedRef<FTabManager::FLayout> StandaloneDefaultLayout = FTabManager::NewLayout( "ExampleDataAssetLayout2" )
    ->AddArea
    (
        FTabManager::NewPrimaryArea()->SetOrientation(Orient_Vertical)
        ->Split
        (
            FTabManager::NewStack()
            ->AddTab(DetailsTabId, ETabState::OpenedTab)
            ->SetForegroundTab(DetailsTabId)
        )
    );
    //ツールバーを作るかどうか。これがfalseだと作られないのでツールバーが要らない方はfalseにしてもいいが保存ボタンなども消えてしまうのでおすすめしない
    const bool bCreateDefaultToolbar = true;
    
    //todo これはfalseにしてもなにか表示が出たり消えたりしなかったので知ってる方がいれば教えてくれると助かります…
    const bool bCreateDefaultStandaloneMenu = true;
    
    InitAssetEditor( Mode, InitToolkitHost,  TEXT( "ExampleDataAssetEditorApp" ), StandaloneDefaultLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, ExampleDataAsset );
    
    FExampleDataAssetEditorExtensionEditorModule& DataTableEditorModule = FModuleManager::LoadModuleChecked<FExampleDataAssetEditorExtensionEditorModule>( "ExampleDataAssetEditorExtensionEditor" );
    AddMenuExtender(DataTableEditorModule.GetMenuExtensibilityManager()->GetAllExtenders(GetToolkitCommands(), GetEditingObjects()));
    
    TSharedPtr<FExtender> ToolbarExtender = DataTableEditorModule.GetToolBarExtensibilityManager()->GetAllExtenders(GetToolkitCommands(), GetEditingObjects());
    ExtendToolbar(ToolbarExtender);
    
    AddToolbarExtender(ToolbarExtender);
    
    //これを呼ばないと表示されない
    RegenerateMenusAndToolbars();
}


void ExampleDataAssetEditor::RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
    WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_ExampleDataAsset Editor", "Example DataAsset Editor"));
    
    FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
    
    CreateAndRegisterDataTableDetailsTab(InTabManager);
}

void ExampleDataAssetEditor::UnregisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
    FAssetEditorToolkit::UnregisterTabSpawners(InTabManager);
}

void ExampleDataAssetEditor::ExtendToolbar(TSharedPtr<FExtender> Extender)
{
    Extender->AddToolBarExtension(
    "Asset",
    EExtensionHook::After,
    GetToolkitCommands(),
    FToolBarExtensionDelegate::CreateSP(this, &ExampleDataAssetEditor::FillToolbar)
    );
}

void ExampleDataAssetEditor::FillToolbar(FToolBarBuilder& ToolbarBuilder)
{
    ToolbarBuilder.BeginSection("ExampleDataAssetCommands");
    {
        ToolbarBuilder.AddToolBarButton(
            FUIAction(
                FExecuteAction::CreateSP(this, &ExampleDataAssetEditor::AddItem_Execute),
                FCanExecuteAction::CreateSP(this, &ExampleDataAssetEditor::CanAddItem)),
            NAME_None,
            LOCTEXT("AddValueText", "Add Value One"),
            LOCTEXT("AddValueOneTooltip", "Add Value this ExampleDataAsset"),
            FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Import"));
    }
    ToolbarBuilder.EndSection();

}

FName ExampleDataAssetEditor::GetToolkitFName() const
{
    return FName("ExampleDataAssetToolKit");
}

FText ExampleDataAssetEditor::GetBaseToolkitName() const
{
    return LOCTEXT( "AppLabel", "ExampleDataAsset Toolkit" );
}

FString ExampleDataAssetEditor::GetWorldCentricTabPrefix() const
{
    return LOCTEXT("WorldCentricTabPrefix", "ExampleDataAsset ").ToString();
}

FLinearColor ExampleDataAssetEditor::GetWorldCentricTabColorScale() const
{
    return FLinearColor( 0.0f, 0.0f, 0.2f, 0.5f );
}

TSharedRef<SDockTab> ExampleDataAssetEditor::SpawnTab_DataTableDetails(const FSpawnTabArgs& Args)
{
    check(Args.GetTabId().TabType == DetailsTabId);
    
    PropertyView->SetObject(GetEditableExampleDataAsset());
    
    return SNew(SDockTab)
        .Label(LOCTEXT("ExampleDataAssetTabDetails", "ExampleDataAsset Details"))
        .TabColorScale(GetTabColorScale())
        [
            SNew(SBorder)
            .Padding(2)
            .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
            [
                PropertyView.ToSharedRef()
            ]
        ];
}

void ExampleDataAssetEditor::CreateAndRegisterDataTableDetailsTab(const TSharedRef<class FTabManager>& InTabManager)
{
    FPropertyEditorModule & EditModule = FModuleManager::Get().GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
    FDetailsViewArgs DetailsViewArgs;
    DetailsViewArgs.NameAreaSettings = FDetailsViewArgs::HideNameArea;
    DetailsViewArgs.bHideSelectionTip = true;
    PropertyView = EditModule.CreateDetailView(DetailsViewArgs);
    
    InTabManager->RegisterTabSpawner(DetailsTabId, FOnSpawnTab::CreateSP(this, &ExampleDataAssetEditor::SpawnTab_DataTableDetails))
        .SetDisplayName(LOCTEXT("ExampleDataAssetTab", "Example DataAsset Details"))
        .SetGroup(WorkspaceMenuCategory.ToSharedRef());
}

UExampleDataAsset* ExampleDataAssetEditor::GetEditableExampleDataAsset() const
{
    return Cast<UExampleDataAsset>(GetEditingObject());
}

bool ExampleDataAssetEditor::CanAddItem() const
{
    return true;
}

void ExampleDataAssetEditor::AddItem_Execute()
{
    UExampleDataAsset* ExampleDataAsset = GetEditableExampleDataAsset();
    ExampleDataAsset->Values.Add(1);
}

詳細ビューの作り方の流れとしては以下になります。

  1. InitExampleAssetDataEditor関数でタブを追加する
  2. タブが追加されたらRegisterTabSpawners関数が呼ばれるので、CreateAndRegisterDetailsTab関数を呼んで詳細タブを作る様にIDetailViewのインスタンスを作成し保持しておくように設定する
  3. 紐づけたIDのタブの登録ができたらSpawnTab_DataTableDetailsがコールバックで呼ばれるので、そこでSlateを作ってPropertyViewを渡して詳細ビューを表示するようにする

ツールバーの作り方の流れとしては以下になります。

  1. InitExampleAssetDataEditor関数で後述するインターフェースIHasMenuExtensibility,IHasToolBarExtensibilityの2つを実装したモジュールを取得し、インターフェースから取ってきたFExtenderExtendToolbar関数に渡す。
  2. ExtendToolbar関数内でAddToolBarExtensionを呼び出し、ツールバーの準備が出来たら呼び出すコールバックとしてFillToolbar関数を渡す
  3. ToolbarBuilder.AddToolBarButtonでボタンを追加する。今回は独自機能としてボタンが押されたら1という値を追加して配列を増やす機能を追加してます。

ここまで実装して、自作のデータアセットを作成し、アセットを開いて編集画面が冒頭の様になっていれば成功です。

終わりに

記事は以上となります。

エディタ拡張の参考になれば幸いです。
ここまで読んでいただきありがとうございました。

参考

UE4 Asset Editor Menu and Toolbar

Discussion