贺老师 发表于 2020-12-29 10:46

Unreal Python 进阶菜单扩展

本文章转载自 智伤帝的个人博客 - 原文链接

前言

最近抽空深入研究了一下 Unreal Python 的菜单扩展。
扩展菜单的主要方法我之前的文章有提到过 Unreal PyToolkit 插件
当时主要参考论坛的一篇帖子用 Python 实现下拉菜单 链接
其实有个地方让我很困惑, menu 获取需要通过 ToolMenus 的 find_menu 实现
menus = unreal.ToolMenus.get()

# NOTE 获取主界面的主菜单位置
main_menu = menus.find_menu("LevelEditor.MainMenu")但是 find_menu 传入的 menu 字符串是从何而来的,完全就没有概念了。
我最先想到的还是从 C++ 入手 ~
C++ 源码探索

首先在 VScode 查 UToolMenus , 然后通过 F12 可以定位到头文件。
头文件名为 ToolMenus.h , 可以用 ctrl+P 去定位ToolMenus.cpp 脚本
ToolMenus.cpp 脚本里面可以找到 FindMenu 的函数。
可以看到是通过 Menus 字典来记录引擎中所有的 Menu 名称。
然而比较头疼的地方时 Menus 在头文件里面设置为了私有变量,无法通过 Python 亦或是 C++ 插件来获取到里面存储的值
迫不得已,我改了引擎源码,实现蓝图调用,然后通过 Python 获取字典存储的值。
{
    "ContentBrowser.AssetContextMenu": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_0"',
    "ContentBrowser.FolderContextMenu": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_1"',
    "MainFrame.MainMenu.Asset": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_2"',
    "LevelEditor.ActorContextMenu": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_3"',
    "ContentBrowser.AssetContextMenu.SoundWave": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_4"',
    "LevelEditor.MainMenu.Window": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_5"',
    "LevelEditor.MainMenu.Help": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_6"',
    "AssetEditor.SkeletalMeshEditor.ToolBar": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_7"',
    "LevelEditor.MainMenu.File": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_8"',
    "ContentBrowser.AssetContextMenu.LevelSequence": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_9"',
    "MediaPlayer.AssetPickerAssetContextMenu": ToolMenu'"/Engine/Transient.ToolMenu_0:ToolMenu_10"',
    "ContentBrowser.AssetContextMenu.CameraAnim": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_11"',
    "LevelEditor.LevelEditorToolBar": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_12"',
    "LevelEditor.MainMenu": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_13"',
    "LevelEditor.MainMenu.Edit": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_14"',
    "LevelEditor.LevelEditorToolBar.SourceControl": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_15"',
    "LevelEditor.LevelEditorToolBar.Cinematics": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_16"',
    "LevelEditor.LevelEditorToolBar.BuildComboButton": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_17"',
    "LevelEditor.LevelEditorToolBar.BuildComboButton.LightingQuality": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_18"',
    "LevelEditor.LevelEditorToolBar.BuildComboButton.LightingInfo.LightingDensity": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_19"',
    "LevelEditor.LevelEditorToolBar.BuildComboButton.LightingInfo.LightingResolution": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_20"',
    "LevelEditor.LevelEditorToolBar.BuildComboButton.LightingInfo": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_21"',
    "LevelEditor.LevelEditorToolBar.EditorModes": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_22"',
    "LevelEditor.LevelEditorToolBar.CompileComboButton": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_23"',
    "LevelEditor.LevelEditorToolBar.LevelToolbarQuickSettings": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_24"',
    "LevelEditor.LevelEditorToolBar.OpenBlueprint": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_25"',
    "LevelEditor.LevelEditorSceneOutliner.ContextMenu": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_30"',
    "MainFrame.MainMenu.File": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_31"',
    "MainFrame.MainTabMenu.File": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_32"',
    "MainFrame.MainMenu.Edit": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_33"',
    "MainFrame.MainMenu.Window": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_34"',
    "MainFrame.MainMenu.Help": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_35"',
    "MainFrame.MainMenu": ToolMenu'"/Engine/Transient.ToolMenus_0:ToolMenu_36"',
}现在除了 LevelEditor.MainMenu 我们有了更多的 menu 选项了。
比如上面显示的就有 ContentBrowser.AssetContextMenu 还有 ContentBrowser.FolderContextMenu 以及 LevelEditor.LevelEditorToolBar
由此可以得出, 应该可以用 Python 扩展这里相应的菜单的。
Python 右键菜单扩展

基于上面的 AssetContextMenu 和 FolderContextMenu 的信息
我们可以分别去扩展右键资弹出的源菜单以及右键文件夹弹出的菜单。
import unreal
menus = unreal.ToolMenus.get()

menu_name = "ContentBrowser.FolderContextMenu"
menu = menus.find_menu(menu_name)

entry = unreal.ToolMenuEntry(type=unreal.MultiBlockType.MENU_ENTRY)
entry.set_label("测试 entry")
# NOTE 注册执行的命令
typ = unreal.ToolMenuStringCommandType.PYTHON
entry.set_string_command(typ, "", 'print "entry 触发测试"')
menu.add_menu_entry('AssetContextSourceControl',entry)

# menus.refresh_all_widgets()本来 UI 修改需要 refresh_all_widgets 才会更新,因为右键菜单是右键生成时候会自动刷新,所以不执行也不影响。
后面生成 Toolbar 扩展的时候踩了这个坑。
另外 AssetContextSourceControl 这个名称是 从何而来 的
这就需要在编辑器配置里面开启 UI 名称的显示
开启上面的选项之后,右键菜单就会多出 绿色 的文字标注菜单的名称。
文件夹菜单扩展也是同理,将菜单的名称修改并且找到菜单相关的 section 进行添加即可。
Python Toolbar 添加

上面开启了 UI 显示之后,右键菜单可以显示了,但是 Toolbar 的显示依然是没有的。
这就需要重启一下 引擎。
有了上面的标注就可以通过上面类似的方法添加 Toolbar 按钮
import unreal
menus = unreal.ToolMenus.get()

menu_name = "LevelEditor.LevelEditorToolBar"
menu = menus.find_menu(menu_name)

# NOTE 生成类型改为 Toolbar 的按钮
entry = unreal.ToolMenuEntry(type=unreal.MultiBlockType.TOOL_BAR_BUTTON)
entry.set_label("测试 button")
# NOTE 注册执行的命令
typ = unreal.ToolMenuStringCommandType.PYTHON
entry.set_string_command(typ, "", 'print "entry 触发测试"')
menu.add_menu_entry('File',entry)

# NOTE 添加刷新才能立刻看到添加的效果
menus.refresh_all_widgets()执行上面的代码就可以在动态给 Toolbar 添加按钮了。
通过 Python 获取引擎生成的菜单

上面获取菜单名称的方式是通过 C++ 魔改引擎源码才能实现。
这样限制非常大,有没有更加友好的获取方式呢?
我看着上面 C++ 获取的字典,不由得计从心生~
menu 的引擎临时路径是有一定规律的,通过这个规律应该可以用 Python 动态读取到生成的菜单。
import unreal

def list_menu(num=1000):
    menu_list = set()
    for i in range(num):
      obj = unreal.find_object(None,"/Engine/Transient.ToolMenus_0:ToolMenu_%s" % i)
      if not obj:
            continue
      menu_name = str(obj.menu_name)
      if menu_name != "None":
            menu_list.add(menu_name)
    return list(menu_list)

print(list_menu())

LogPython: ['LevelEditor.LevelEditorToolBar', 'ContentBrowser.AssetContextMenu.LevelSequence', 'MediaPlayer.AssetPickerAssetContextMenu', 'ContentBrowser.AssetContextMenu', 'LevelEditor.LevelEditorToolBar.CompileComboButton', 'MainFrame.MainMenu', 'LevelEditor.MainMenu.Edit', 'LevelEditor.LevelEditorToolBar.BuildComboButton.LightingInfo.LightingResolution', 'LevelEditor.MainMenu', 'LevelEditor.MainMenu.File', 'AssetEditor.SkeletalMeshEditor.ToolBar', 'MainFrame.MainMenu.Edit', 'ContentBrowser.AssetContextMenu.CameraAnim', 'LevelEditor.MainMenu.Window', 'LevelEditor.LevelEditorToolBar.BuildComboButton', 'LevelEditor.LevelEditorToolBar.BuildComboButton.LightingQuality', 'MainFrame.MainMenu.File', 'LevelEditor.LevelEditorToolBar.BuildComboButton.LightingInfo.LightingDensity', 'LevelEditor.ActorContextMenu', 'ContentBrowser.AssetContextMenu.SoundWave', 'MainFrame.MainTabMenu.File', 'LevelEditor.LevelEditorToolBar.SourceControl', 'LevelEditor.LevelEditorToolBar.BuildComboButton.LightingInfo', 'LevelEditor.LevelEditorSceneOutliner.ContextMenu', 'MainFrame.MainMenu.Window', 'LevelEditor.LevelEditorToolBar.LevelToolbarQuickSettings', 'MainFrame.MainMenu.Asset', 'LevelEditor.LevelEditorToolBar.Cinematics', 'LevelEditor.MainMenu.Help', 'LevelEditor.LevelEditorToolBar.EditorModes', 'MainFrame.MainMenu.Help', 'ContentBrowser.FolderContextMenu', 'LevelEditor.LevelEditorToolBar.OpenBlueprint']我想要的效果实现了~
这样就不需要魔改引擎代码,也可以获取出到大部分的 menu 名称。
而且上面的数组只是默认开启引擎下的菜单,如果多打开一些编辑窗口,还可以获取到更多的 菜单 。
由于不知道到底有多少个菜单生成了,所以我默认定的循环数是 1000 ,基本是够用的,而且遍历速度很快。
另外还有部分的菜单是 None 无名氏,估计是注册的时候没有给定名称,也不好判断是哪里的菜单,所以我就过滤掉了。
通过 Python 扩展 AddNewContextMenu

AddNewContextMenu 就是没有选择任何资源的时候在 资源浏览器 右键弹出的菜单。
刚好我遇到了在这个菜单上进行扩展的需求,所以为了用 Python 实现踩了不少坑_(:з」∠)_
从这个名字可以知道,默认开启引擎的时候并没有加载到这个菜单。
使用上面写道 list_menu 函数是获取不到的,除非在 资源浏览器 进行右键触发。
这个时候 list_menu 就会多出这个 ContentBrowser.AddNewContextMenu 的菜单名称。
这就产生了很严重的问题,无法实现 C++ 插件的菜单嵌入效果。
不可能让使用者手动右键生成一下菜单,再让他点击什么按钮触发,将需要的 entry 添加到菜单里呀_(:з」∠)_
后来为了能够完成需求,我还是用 C++ 来解决了这个问题 参考知乎这篇文章
不过搞定了需求,周末还是抽空研究怎么通过 Python 来解决这个问题。
这个过程还实现了一些有趣的效果,比如先注册生成 menu ,导致右键菜单变成了我自己自定义的菜单了。
menus = unreal.ToolMenus.get()

menu_name = "ContentBrowser.AddNewContextMenu"
menu = menus.find_menu(menu_name)

# NOTE 如果已经注册则删除 | 否则无法执行 register_menu
if menus.is_menu_registered(menu_name):
    menus.remove_menu(menu_name)

menu = menus.register_menu(menu_name)
entry = unreal.ToolMenuEntry(type=unreal.MultiBlockType.MENU_ENTRY)
entry.set_label("测试 entry")
menu.add_menu_entry('',entry)其他菜单也可以尝试着这样魔改成自己的菜单。
如果可以配合引擎快捷键触发不同的菜单,还是有点意思的。
上面魔改的菜单要恢复也不难,将自己做的菜单 删除掉 ,默认右键就会生成回正常的菜单了。
问题是这个折腾依然没能解决实现我想要的效果_(:з」∠)_
后面还是想往 C++ 的方向入手,能不能在右键菜单事件添加 回调事件,配合触发 Python 脚本。
也的确在 C++ 文档里面找到相关的回调函数 链接
但是要如何接入 Python 还不是很确定。
后面思路一转,还有更加简单的实现方式,可以用定时器来做。
虽然定时器不是个好点子,听着就比较浪费资源,但是考虑到 Python 遍历 menu 的速度快到没感觉。
定时执行嵌入操作的卡顿应该感知不到。
import unreal
from Qt import QtCore
from functools import partial

def add_menu(timer):
    print("timer running...")
    menu = menus.find_menu("ContentBrowser.AddNewContextMenu")
   
    if not menu:
      return
   
    # NOTE 如果存在停止计时器
    timer.stop()
    print("timer stop")
   
    entry = unreal.ToolMenuEntry(type=unreal.MultiBlockType.MENU_ENTRY)
    entry.set_label("测试 entry")
    menu.add_menu_entry('ContentBrowserNewAdvancedAsset',entry)
   
    menus.refresh_all_widgets()

# NOTE 使用 Qt 的定时器实现 js setInterval 函数的效果
# NOTE Python 原生实现比较麻烦,用 Qt 的 Timer 比较简洁
timer =QtCore.QTimer()
timer.timeout.connect(partial(add_menu,timer))
timer.start(1000)完美通过定时器实现动态嵌入菜单。
PyToolkit Json 配置优化

通过上面的一轮折腾,之前 PyToolkit 提供的菜单配置完全可以更加灵活。
于是我又开始重写之前的 json 配置的读取和生成,通过递归的方法,自动处理多重嵌套菜单的效果。
具体的配置方法我写到了 PyToolkit 的帮助文档里面 链接~
上面四种嵌入可以完全通过 json 配置来完成~
总结

利用 Python 做菜单扩展的确方便了很多,但是 Python 也并不是万能的。
例如我之前研究过的 Sequencer 菜单工具栏嵌入 扩展就只能通过 C++ 来实现
因为 list_menu 没有找到相关的菜单名称。
最近博客因为各种原因写好了文章却没有更新,更新频率也下降了_(:з」∠)_
希望后续可以整理规划好时间,继续坚持做博客的记录。
页: [1]
查看完整版本: Unreal Python 进阶菜单扩展