# 前言
上次开始研究编辑器开发的时候,扩展右键菜单使用的是一个叫做 FExtender 的东西,后面发现其实还有一种统一的,更加简洁的菜单扩展方式 ——UToolMenus。
也就是说,貌似目前扩展编辑器分为了 Extender 方式和统一的 UToolMenus 方式,听说 UToolMenus 是新版本的方式,经过一顿研究下来,也确实感觉
UToolMenus 明显好用不少。
注:另外听说 Extender 扩展貌似属于过时方式了。
# 示例
在具体开始之前,可以先看看一个简单的示例:以最简单的,想为关卡编辑器添加一个菜单项为例。
UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu"); | |
FToolMenuSection& Section = Menu->FindOrAddSection(NAME_None); | |
FToolMenuEntry& BuildEntry = Menu.AddSubMenu | |
( | |
"MySubMenu", | |
LOCTEXT("MySubMenu", "MySubMenu"), | |
LOCTEXT("MySubMenu_ToolTip", "MySubMenu Options"), | |
FOnGetContent::CreateStatic(&UCryLevelEditorMenu::GetMenuContent) | |
); | |
// 可选,调整插入位置 | |
BuildEntry.InsertPosition = FToolMenuInsert("Actions", EToolMenuInsertType::After); |
上述代码可以在菜单栏的帮助菜单的前一项,增加一个名为 MySubMenu 的菜单项。
效果如下:
简单来说大概步骤可以分别三步:
- 注册 / 创建菜单
- 查找 / 添加节点
- 添加自定义菜单 / 按钮
而 Extender 方式则是:
- 加载需要扩展的模块 (如 FContentBrowserModule)
- 获取需要扩展的菜单 (如 ContentBrowserModule.GetAllAssetContextMenuExtenders,不能错)
- 注册扩展器
... 虽然看着好像差不多,但是 Extender 的方式 1、2 步其实都挺容易出错的。 至少个人对比使用下来,感觉还是 UToolMenu 方式更好用。
补充下上面代码的 UCryLevelEditorMenu::GetMenuContent 方法,其中可以选择创建点击后的一系列子菜单等:
TSharedRef<SWidget> UCryLevelEditorMenu::GetMenuContent() | |
{ | |
FMenuBuilder MenuBuilder(true, nullptr); | |
const FText DisplayName = FText::FromString("Test"); | |
MenuBuilder.AddMenuEntry( | |
DisplayName, | |
LOCTEXT("Description", "Opens this in the editor"), | |
IconLevel(), | |
FUIAction(FExecuteAction::CreateLambda([]() | |
{ | |
ShowNotification(LOCTEXT("Tips", "Notification Tips!")); | |
})) | |
); | |
return MenuBuilder.MakeWidget(); | |
} |
注 1:并不一定需要用 GetMenuContent 方式添加子菜单,直接使用 AddMenuEntry 也可以。
注 2:... 唉呀,怎么感觉其实都挺麻烦的
# 详情
# UToolMenus
在上面示例中, 最开始有一句 UToolMenu* Menu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu");
代码,这句话的作用是:为
LevelEditor.MainMenu 菜单扩展,除了 ExtendMenu 还有个 RegisterMenu 方法,两者区别如下:
RegisterMenu = “注册 / 继承”—— 适合新建或需要父子继承的菜单
- 可以选择传入一个 “父菜单” 名,把父菜单的当前内容复制一份给新菜单,新菜单自带一套 “初始模板”,之后改新菜单不影响父菜单。
- 如果没有拿到对象就想扩展,还是得 ExtendMenu。
ExtendMenu = “就地扩展”—— 适合给已有菜单追加内容或临时创建空壳。
- 直接取同名菜单实例(没有就现建一个空壳),不会复制任何其他菜单的内容。
因此一般来说,应该:
- 优先用 ExtendMenu,而不是 RegisterMenu。
- 理由:ExtendMenu 才是 “真正往已有菜单里加东西”,RegisterMenu 应当只是 “登记一个全新的扩展点”。
看源码 RegisterMenu 甚至有个可选注册时发现菜单已注册的提示:
UToolMenu* UToolMenus::RegisterMenu(const FName InName, const FName InParent, EMultiBoxType InType, bool bWarnIfAlreadyRegistered) | |
{ | |
if (UToolMenu* Found = FindMenu(InName)) | |
{ | |
if (!Found->bRegistered) | |
{ | |
Found->MenuParent = InParent; | |
Found->MenuType = InType; | |
Found->MenuOwner = CurrentOwner(); | |
Found->bRegistered = true; | |
Found->bIsRegistering = true; | |
for (FToolMenuSection& Section : Found->Sections) | |
{ | |
Section.bIsRegistering = Found->bIsRegistering; | |
} | |
} | |
else if (bWarnIfAlreadyRegistered) | |
{ | |
UE_LOG(LogToolMenus, Warning, TEXT("Menu already registered : %s"), *InName.ToString()); | |
} | |
return Found; | |
} | |
UToolMenu* ToolMenu = NewToolMenuObject(FName(TEXT("RegisteredMenu")), InName); | |
ToolMenu->InitMenu(CurrentOwner(), InName, InParent, InType); | |
ToolMenu->bRegistered = true; | |
ToolMenu->bIsRegistering = true; | |
Menus.Add(InName, ToolMenu); | |
return ToolMenu; | |
} | |
UToolMenu* UToolMenus::ExtendMenu(const FName InName) | |
{ | |
if (UToolMenu* Found = FindMenu(InName)) | |
{ | |
Found->bIsRegistering = false; | |
for (FToolMenuSection& Section : Found->Sections) | |
{ | |
Section.bIsRegistering = Found->bIsRegistering; | |
} | |
// Refresh all widgets because this could be child of another menu being displayed | |
RefreshAllWidgets(); | |
return Found; | |
} | |
UToolMenu* ToolMenu = NewToolMenuObject(FName(TEXT("RegisteredMenu")), InName); | |
ToolMenu->bRegistered = false; | |
ToolMenu->bIsRegistering = false; | |
Menus.Add(InName, ToolMenu); | |
return ToolMenu; | |
} |
什么时候才需要 RegisterMenu?
- 正在写一个全新的面板 / 窗口,需要一个全新的顶级菜单名,而且确定它还没被任何模块注册过 —— 这时才先 RegisterMenu,再
ExtendMenu。 - 对 UE 自带的菜单(ContentBrowser、MainMenu、LevelEditor 工具栏等)一律跳过注册,直接 Extend。
也就是说:除非在造全新菜单,否则忘掉 RegisterMenu;一律 ExtendMenu 起手即可。
# FToolMenuSection
上面的示例中,取到扩展菜后,通过 FindOrAddSection 方法找到货添加一个空的节点,然后实现一个子菜单。
其实 FindOrAddSection 返回的对象有多个添加节点的方法:
方法 | 核心用途 | 动态性 | 返回类型 |
---|---|---|---|
AddEntry | 添加预构建的完整菜单项 | 静态 | void |
AddMenuEntry | 添加标准菜单按钮 | 静态 | FToolMenuEntry& |
AddDynamicEntry | 动态生成菜单内容 | 动态 | void |
AddSeparator | 添加视觉分隔线 | 静态 | FToolMenuEntry& |
AddSubMenu | 添加嵌套子菜单 | 子菜单可动态 | FToolMenuEntry& |
# 添加方法
# 1. AddEntry
- 最底层的通用方法,直接添加一个预构建的
FToolMenuEntry
对象 - 需要手动构造完整的菜单项对象(使用
FToolMenuEntry::InitXxx
系列方法) - 提供最大灵活性,可设置所有属性(图标、快捷键、动态行为等)
Section.AddEntry( | |
FToolMenuEntry::InitMenuEntry( | |
"DeleteAsset", | |
LOCTEXT("Delete", "删除"), | |
LOCTEXT("DeleteTooltip", "删除选中资源"), | |
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Delete"), | |
FUIAction(FExecuteAction::CreateLambda([](){ /* 删除逻辑 */ })) | |
) | |
); |
# 2. AddMenuEntry
- 添加标准菜单按钮的快捷方法
- 自动处理
FToolMenuEntry
的构造 - 支持设置基础属性:名称、标签、工具提示、图标、点击动作
- 返回菜单项引用便于后续修改
auto& Entry = Section.AddMenuEntry( | |
"SaveLevel", | |
LOCTEXT("Save", "保存"), | |
LOCTEXT("SaveTooltip", "保存当前关卡"), | |
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Save"), | |
FUIAction(FExecuteAction::CreateStatic(&ULevelEditorAction::Save)) | |
); |
# 3. AddDynamicEntry
- 动态生成菜单项(运行时根据条件创建)
- 接受一个委托,该委托在菜单实际显示时被调用
- 用于条件菜单项(如根据选中对象类型显示不同选项)
- 避免提前创建不会被显示的菜单项,优化性能
Section.AddDynamicEntry("DynamicTools", FNewToolMenuDelegate::CreateLambda( | |
[](FToolMenuSection& DynamicSection){ | |
if(GetSelectedObjects().Num() > 0){ | |
DynamicSection.AddMenuEntry(...); | |
} | |
} | |
)); |
# 4. AddSeparator
- 添加菜单分隔线(水平分割线):Section.AddSeparator (NAME_None)
- 用于视觉分组相关菜单项
# 5. AddSubMenu
- 添加嵌套子菜单
- 接受一个委托用于构建子菜单内容
- 可附加主菜单项的点击动作(
FNewToolMenuChoice
)
Section.AddSubMenu( | |
"ExportSubMenu", | |
LOCTEXT("Export", "导出"), | |
LOCTEXT("ExportTooltip", "导出选项"), | |
FSlateIcon(), | |
FNewToolMenuDelegate::CreateLambda([](UToolMenu* SubMenu){ | |
FToolMenuSection& SubSection = SubMenu->AddSection("ExportFormats"); | |
SubSection.AddMenuEntry("ExportPNG", ...); | |
SubSection.AddMenuEntry("ExportJPG", ...); | |
}) | |
); |
注:FNewToolMenuChoice 接受 FOnGetContent、FNewToolMenuWidget、FNewToolMenuDelegate、FNewMenuDelegate
# 使用策略
- 优先用
AddMenuEntry
适用于大多数标准菜单按钮场景 - 需要动态内容时用
AddDynamicEntry
特别是菜单项需要根据运行时状态变化时,例如通常用于扩展内容浏览器的右键资源菜单 - 复杂菜单项用
AddEntry
当需要设置高级属性(如自定义控件、动态图标等) - 组织菜单用
AddSeparator
+AddSubMenu
# 扩展区域
这里列一下几个常见的扩展点:
- LevelEditor.MainMenu:关卡编辑器主菜单
- LevelEditor.LevelEditorToolBar.PlayToolBar:关卡编辑器工具栏
- ContentBrowser.AssetContextMenu:内容浏览器右键资源菜单
- ContentBrowser.FolderContextMenu:内容浏览器右键文件夹 / 路径菜单
如果想扩展更多其他的扩展点,可以直接源码全局搜索:LevelEditor.MainMenu 之类的关键字,找到对应的扩展点,然后使用对应的扩展点名字通过
ExtendMenu 注册扩展器即可。
# 参考文档
- ttps://zhuanlan.zhihu.com/p/605181368
- ED04.ToolMenus | 在任意扩展点插入自定义按钮