# 前言

最近研究了下虚幻的编辑器扩展,记录一下一个需求:我有一个 UUserWidget 子类,后续想要所有可以直接打开的界面,都基于该类创建蓝图。

然后就想将其放在内容浏览器的右键空白区域的资源创建菜单上,以便快速创建该类的蓝图。

# 方法

虚幻的编辑器也就最近开始研究了下,不大熟悉,折腾了半天,结果发现不下于三种添加资源创建菜单的方法:

  1. 通过 AssetTypeActions 配合自定义 Factory
  2. 通过 UAssetDefinitionDefault 配合自定义 Factory
  3. 自定义添加编辑器菜单
  4. 直接将类型添加到 UUMGEditorProjectSettings->FavoriteWidgetParentClasses (这种方式会出现在创建用户控件蓝图的界面中)

其中第四种方法是最简单的方法,只需要将 UUserWidget 子类添加到 FavoriteWidgetParentClasses
列表中即可。当然其表现与其它方式会有所不同,但是也大差不大。下面一一介绍。

# 自定义添加编辑器菜单

这种方式是最开始想到的方式,因为从概念上讲,也是最容易想到的:直接手动扩展编辑器,添加一个菜单项:

void FContentBrowserExtensionBase::OnInitialize()
{
	FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>(
		"ContentBrowser");
	auto& MenuExtenders = ContentBrowserModule.GetAllAssetContextMenuExtenders();
	// 创建扩展委托
	FContentBrowserMenuExtender_SelectedPaths MenuExtenderDelegate =
		FContentBrowserMenuExtender_SelectedPaths::CreateLambda([this](const TArray<FString>& SelectedPaths)
		{
			TSharedRef<FExtender> MenuExtender = MakeShared<FExtender>();
			MenuExtender->AddMenuExtension(
				"ContentBrowserNewAdvancedAsset",
				EExtensionHook::After,
				nullptr,
				FMenuExtensionDelegate::CreateRaw(this, &FContentBrowserExtensionBase::AddMenuExtensions)
			);
			return MenuExtender;
		});
	ExtensionHandle = MenuExtenderDelegate.GetHandle();
	MenuExtenders.Add(MenuExtenderDelegate);
}
void FContentBrowserExtensionBase::AddMenuExtensions(FMenuBuilder& MenuBuilder)
{
	MenuBuilder.AddMenuEntry(
		FText::FromString("My Custom Widget"),
		FText::FromString("Create Widget based on MyBaseUserWidget"),
		FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Plus"),
		FUIAction(FExecuteAction::CreateLambda([]()
		{
			UWidgetBlueprintFactory* Factory = NewObject<UWidgetBlueprintFactory>();
			Factory->ParentClass = UCryPageBase::StaticClass();
			FAssetToolsModule::GetModule().Get().CreateAssetWithDialog(
				UWidgetBlueprint::StaticClass(),
				Factory
			);
		}))
	);
}

常规通过扩展挂点的形式挂到创建菜单中,不过没找到怎么挂到专门的创建用户界面的子菜单中... 所以只是简单实现了一下。

效果如下:

注:SelectedPaths 可以获取路径的,这个实现会有个保存资源弹窗,如果有路径的话,也可以直接创建资源。

# AssetTypeActions 扩展资源创建菜单

也就是直接继承 FAssetTypeActions_Base,然后填充参数。

注意:该方法需要手动在模块的 StartupModule 中注册,否则不会生效。

官方 Lyra 示例就是采用的这种方法,具体可以参考 Lyra 的 FAssetTypeActions_LyraContextEffectsLibrary 及
ULyraContextEffectsLibraryFactory 类:

class FAssetTypeActions_LyraContextEffectsLibrary : public FAssetTypeActions_Base
{
public:
	// IAssetTypeActions Implementation
	virtual FText GetName() const override { return NSLOCTEXT("AssetTypeActions", "AssetTypeActions_LyraContextEffectsLibrary", "LyraContextEffectsLibrary"); }
	virtual FColor GetTypeColor() const override { return FColor(65, 200, 98); }
	virtual UClass* GetSupportedClass() const override;
	virtual uint32 GetCategories() override { return EAssetTypeCategories::Gameplay; }
};

ULyraContextEffectsLibraryFactory 类是用来创建蓝图的工厂类,源码就不单独贴了,有兴趣查一下应该很多信息。

我最开始也是按照这个方法来做的,结果不小心了翻到了下引擎中其它使用 FAssetTypeActions_Base 的地方的源码,例如:

class
// UE_DEPRECATED(5.2, "The AssetDefinition system is replacing AssetTypeActions and this was replaced by AssetDefinition_ClassTypeBase.  Please see the Conversion Guide in AssetDefinition.h")
ASSETTOOLS_API FAssetTypeActions_ClassTypeBase : public FAssetTypeActions_Base
{
}

目前已经过时,摆明了就是不推荐使用了。

于是根据注释所说,查了下 AssetDefinition。

# AssetDefinition 扩展资源创建菜单

就如同前面所说,因为不小心发现 AssetTypeActions 已经过时,所以就开始研究 AssetDefinition。

不像 AssetTypeActions 的方式,AssetDefinition 会自动扫描并注册,所以不用手动注册,确实省心不少,加之 Factory 也是自动扫描,那就更方便了。

另外查看 UFactory 源码可以发现:像 GetDisplayName ()、GetMenuCategories () 等方法,依然还是在依赖 AssetTypeActions,所以目前如果是通过
AssetDefinition 配合 Factory 来扩展资源创建菜单,必须重写 UFactory::GetMenuCategories () 等依赖 AssetTypeActions 的方法。

不然引擎去找这个资源对应的 AssetTypeActions 就会找不到,导致无法显示菜单。如下所示:

uint32 UFactory::GetMenuCategories() const
{
	FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
	UClass* LocalSupportedClass = GetSupportedClass();
	if (LocalSupportedClass)
	{
		TWeakPtr<IAssetTypeActions> AssetTypeActions = AssetToolsModule.Get().GetAssetTypeActionsForClass(LocalSupportedClass);
		if (AssetTypeActions.IsValid())
		{
			return AssetTypeActions.Pin()->GetCategories();
		}
	}
	// Factories whose classes do not have asset type actions fall in the misc category
	return EAssetTypeCategories::Misc;
}

所以,如果要通过 AssetDefinition 扩展资源创建菜单,必须重写 UFactory::GetMenuCategories () 等方法。

Factory 整体实现:

UCLASS(hidecategories = Object, MinimalAPI)
class UPageWidgetFactory : public UFactory
{
	GENERATED_BODY()
public:
	UPageWidgetFactory();
	//~ Begin UFactory Interface
	virtual bool ShouldShowInNewMenu() const override { return true; }
	virtual FText GetDisplayName() const override;
	virtual uint32 GetMenuCategories() const override;
	virtual FText GetToolTip() const override;
	virtual FString GetDefaultNewAssetName() const override;
	virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags,
	                                  UObject* Context, FFeedbackContext* Warn, FName CallingContext) override;
	virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags,
	                                  UObject* Context, FFeedbackContext* Warn) override;
	//~ End UFactory Interface
};
UPageWidgetFactory::UPageWidgetFactory()
{
	SupportedClass = UWidgetBlueprint::StaticClass();
	// ParentClass = UCryPageBase::StaticClass();
	bCreateNew = true;
	bEditAfterNew = true;
}
FText UPageWidgetFactory::GetDisplayName() const
{
	return FText::FromString("Create New Page");
}
uint32 UPageWidgetFactory::GetMenuCategories() const
{
	return EAssetTypeCategories::UI;
}
FText UPageWidgetFactory::GetToolTip() const
{
	return FText::FromString("Creates a new Page widget blueprint");
}
FString UPageWidgetFactory::GetDefaultNewAssetName() const
{
	return TEXT("PageNew");
}
UObject* UPageWidgetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags,
                                              UObject* Context, FFeedbackContext* Warn, FName CallingContext)
{
	UWidgetBlueprint* NewBP = CastChecked<UWidgetBlueprint>(FKismetEditorUtilities::CreateBlueprint(
		UCryPageBase::StaticClass(), InParent, Name, EBlueprintType::BPTYPE_Normal, UWidgetBlueprint::StaticClass(),
		UWidgetBlueprintGeneratedClass::StaticClass(), CallingContext));
	// 编译蓝图(可选)
	// FKismetEditorUtilities::CompileBlueprint(NewBP);
	if (NewBP)
	{
		FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(NewBP);
	}
	return NewBP;
}
UObject* UPageWidgetFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags,
                                              UObject* Context, FFeedbackContext* Warn)
{
	return FactoryCreateNew(Class, InParent, Name, Flags, Context, Warn, NAME_None);
}

AssetDefinition 实现:

UCLASS()
class UCryPageAssetDefinition : public UAssetDefinitionDefault
{
	GENERATED_BODY()
public:
	//~UAssetDefinition interface
	virtual TConstArrayView<FAssetCategoryPath> GetAssetCategories() const override;
	virtual TSoftClassPtr<UObject> GetAssetClass() const override;
	virtual FText GetAssetDisplayName() const override;
	virtual FText GetAssetDisplayName(const FAssetData& AssetData) const override;
	virtual FLinearColor GetAssetColor() const override { return FLinearColor::Yellow; }
	virtual const FSlateBrush* GetIconBrush(const FAssetData& InAssetData, const FName InClassName) const override;
	virtual EAssetCommandResult OpenAssets(const FAssetOpenArgs& OpenArgs) const override;
	//~End of UAssetDefinition interface
};
TConstArrayView<FAssetCategoryPath> UCryPageAssetDefinition::GetAssetCategories() const
{
	// 自定义分类:static const auto Categories = {FAssetCategoryPath (LOCTEXT ("MyCustomAssetsCategoryName", "MyCustom")) };
	static const auto Categories = {FAssetCategoryPath(EAssetCategoryPaths::UI)};
	return Categories;
}
TSoftClassPtr<UObject> UCryPageAssetDefinition::GetAssetClass() const
{
	return UCryPageBase::StaticClass();
}
FText UCryPageAssetDefinition::GetAssetDisplayName() const
{
	return UCryPageBase::StaticClass()->GetDisplayNameText();
}
FText UCryPageAssetDefinition::GetAssetDisplayName(const FAssetData& AssetData) const
{
	if (AssetData.IsValid())
	{
		if (auto Class = AssetData.GetClass())
		{
			if (Class->HasMetaData("DisplayName"))
			{
				return FText::FromString(Class->GetMetaData("DisplayName"));
			}
		}
	}
	return FText::FromString(TEXT("Custom CryPageWidget"));
}
const FSlateBrush* UCryPageAssetDefinition::GetIconBrush(const FAssetData& InAssetData, const FName InClassName) const
{
	return Super::GetIconBrush(InAssetData, InClassName);
}
EAssetCommandResult UCryPageAssetDefinition::OpenAssets(const FAssetOpenArgs& OpenArgs) const
{
	for (const FAssetData& AssetData : OpenArgs.Assets)
	{
		if (UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset()))
		{
			FKismetEditorUtilities::BringKismetToFocusAttentionOnObject(Blueprint);
		}
	}
	return EAssetCommandResult::Handled;
}

效果如下:

# 通过 FavoriteWidgetParentClasses 扩展资源创建菜单

这种方式是最简单的方法,只需要将 UUserWidget 子类添加到 FavoriteWidgetParentClasses 即可。

理论上编辑器中应该有设置方式,就算没有,直接修改 Config 文件也行。

不过由于我是直接从源码看到的,加上正好在做编辑器扩展,因此就直接通过代码设置了:

void UCryPageCreator::OnInitialize()
{
	TSoftClassPtr<UUserWidget> NewClassRef = UCryPageBase::StaticClass();
	GetMutableDefault<UUMGEditorProjectSettings>()->FavoriteWidgetParentClasses.AddUnique(NewClassRef);
}

嗯.. 没错,就两行代码 (甚至可以直接一行解决)。

效果如下:

# 总结

  1. 自定义添加编辑器菜单:可以用,但是只是用于添加新资源创建就不推荐使用了。
  2. AssetTypeActions 扩展资源创建菜单:过时,不推荐使用。
  3. AssetDefinition 扩展资源创建菜单:新版方法,推荐使用。
  4. 通过 FavoriteWidgetParentClasses 扩展资源创建菜单:最简单的方法,可以用。

以上就是我自己发现的几种扩展编辑器资源创建菜单的方法,虽然第四种仅适用于 UserWidget 蓝图,不过如果其它类型的蓝图系统本身带了类似的创建界面,应该都可以找找有没有差不多的扩展方式 —— 实在不行,貌似貌似还可以直接实现一个这种界面,不过那就走远了。

另外也许还有其它方式,不过暂时没有深入了解就不清楚了。