# 前言
最近研究了下虚幻的编辑器扩展,记录一下一个需求:我有一个 UUserWidget 子类,后续想要所有可以直接打开的界面,都基于该类创建蓝图。
然后就想将其放在内容浏览器的右键空白区域的资源创建菜单上,以便快速创建该类的蓝图。
# 方法
虚幻的编辑器也就最近开始研究了下,不大熟悉,折腾了半天,结果发现不下于三种添加资源创建菜单的方法:
- 通过 AssetTypeActions 配合自定义 Factory
- 通过 UAssetDefinitionDefault 配合自定义 Factory
- 自定义添加编辑器菜单
- 直接将类型添加到 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); | |
} |
嗯.. 没错,就两行代码 (甚至可以直接一行解决)。
效果如下:
# 总结
- 自定义添加编辑器菜单:可以用,但是只是用于添加新资源创建就不推荐使用了。
- AssetTypeActions 扩展资源创建菜单:过时,不推荐使用。
- AssetDefinition 扩展资源创建菜单:新版方法,推荐使用。
- 通过 FavoriteWidgetParentClasses 扩展资源创建菜单:最简单的方法,可以用。
以上就是我自己发现的几种扩展编辑器资源创建菜单的方法,虽然第四种仅适用于 UserWidget 蓝图,不过如果其它类型的蓝图系统本身带了类似的创建界面,应该都可以找找有没有差不多的扩展方式 —— 实在不行,貌似貌似还可以直接实现一个这种界面,不过那就走远了。
另外也许还有其它方式,不过暂时没有深入了解就不清楚了。