【Godot】传入字典创建多级菜单

Godot 4.0 beta6
下方有 3.5 版本的代码

#============================================================
#    Menu List
#============================================================
# - datetime: 2022-11-27 01:01:10
#============================================================
##菜单列表
##
##类似 test() 脚本中的菜单结构,可以生成不断嵌套的菜单。
class_name MenuList
extends BoxContainer


# 菜单被点击
signal menu_pressed(idx: int, menu_path: String)


# 自动增长的菜单 idx。用以下面添加菜单项时记录添加的菜单的 idx
var _auto_increment_menu_idx := -1
# 菜单路径对应 PopupMenu
var _menu_path_to_popup_menu_map := {}
# 菜单的 idx 对应的菜单路径
var _idx_to_menu_path_map := {}


#============================================================
#  内置
#============================================================
#func _ready():
#  test()

func test():
    init_menu({
        "File": [
            "Open", "Save", "Save As",
            {
                "Export": [
                    "Export",
                    "Export as..."
                ]
            }
        ],
        "Edit": [
            "Undo",
            {
                "number": [
                    "1",
                    "2",
                    "3",
                ],
                "item_02": [
                    "a",
                    "b",
                    "c",
                    {"d": ["1", "2", "3", "4"]},
                ]
            },
        ],
    })


#=====================================================
#   Set/Get
#=====================================================
## 获取弹窗菜单
##[br]
##[br][code]menu_path[/code]  菜单路径
func get_popup_menu(menu_path: String) -> PopupMenu:
    return _menu_path_to_popup_menu_map.get(menu_path) as PopupMenu


#=====================================================
#   自定义方法
#=====================================================
## 初始化菜单
func init_menu(data: Dictionary):
    add_menu.call_deferred(data, "/")


## 添加菜单项
##[br]
##[br][code]menu_data[/code]  这个菜单项包含的数据
##[br][code]parent_menu_path[/code]  父级菜单路径
func add_menu(menu_data, parent_menu_path: String):
    var parent_popup_menu : PopupMenu = get_popup_menu(parent_menu_path)

    _auto_increment_menu_idx += 1
    if parent_menu_path != "/":
        # Dictionary
        if menu_data is Dictionary:
            for menu_name in menu_data:
                add_menu( menu_data[menu_name], parent_menu_path.path_join(menu_name))

        # Array
        elif menu_data is Array:
            for data in menu_data:
                add_menu(data, parent_menu_path)

        # String
        elif menu_data is String:
            # 添加子菜单
            if not _menu_path_to_popup_menu_map.has(parent_menu_path):
                create_menu(parent_menu_path, null)
            parent_popup_menu = get_popup_menu(parent_menu_path)
            # 不是 Array 和 Dictionary 类型时,只能是 String 类型了
            var menu_name : String = menu_data
            if not menu_name.begins_with("-"):
                _idx_to_menu_path_map[_auto_increment_menu_idx] = "%s/%s" % [parent_menu_path, menu_name] 
                parent_popup_menu.add_item(menu_name, _auto_increment_menu_idx)
            else:
                parent_popup_menu.add_separator()

        else:
            assert(false, "错误的数据类型:" + str(typeof(menu_data)) )

    else:

        # 没有父级菜单,则添加为菜单按钮
        for menu_name in menu_data:
            # 添加菜单按钮
            var menu = MenuButton.new()
            menu.switch_on_hover = true
            menu.text = menu_name
            add_child(menu)

            # 设置属性
            var menu_path = parent_menu_path.path_join(menu_name) 
            _set_popup_menu(menu_path, menu.get_popup())

            # 添加这个按钮菜单的子菜单
            add_menu(menu_data[menu_name], menu_path)


## 创建菜单
##[br]
##[br][code]menu_path[/code]  菜单路径
##[br][code]parent_menu[/code]  父级菜单
func create_menu(menu_path: String, parent_menu: PopupMenu):
    # 切分菜单名
    var parent_menu_names := menu_path.split("/")
    # 因为切分后 0 索引都是空字符串,所以移除
    parent_menu_names.remove_at(0)

    # 逐个添加菜单
    parent_menu = get_popup_menu("/" + "/".join(parent_menu_names.slice(0, 1)))
    for i in parent_menu_names.size():
        var sub_menu_path = "/" + "/".join(parent_menu_names.slice(0, i + 1))
        # 没有这个菜单则添加
        if not _menu_path_to_popup_menu_map.has(sub_menu_path):
            var menu_name = parent_menu_names[i]
            var menu_popup = _create_popup_menu(sub_menu_path)
            parent_menu.add_child(menu_popup)
            parent_menu.add_submenu_item( menu_name, menu_name )
        # 开始记录这个菜单,用以这个菜单的下一级别的菜单
        parent_menu = get_popup_menu(sub_menu_path)



#=====================================================
#   连接信号
#=====================================================
# 创建这个路径的菜单
func _create_popup_menu(path: String) -> PopupMenu:
    var menu_popup = PopupMenu.new()
    menu_popup.name = path.get_file()
    _set_popup_menu(path, menu_popup)
    return menu_popup


# 设置菜单属性
func _set_popup_menu(menu_path: String, menu_popup: PopupMenu):
    print(" >>> 已添加 ", menu_path)
    self._menu_path_to_popup_menu_map[menu_path] = menu_popup
    # 点击菜单时
    menu_popup.id_pressed.connect(func(id):
        self.menu_pressed.emit(id, _idx_to_menu_path_map[id])
    )


Godot 3.5 rc2

将脚本挂载到一个 HBoxContainerVBoxContainer 节点上

在这里插入图片描述

下面 test() 方法即是一个示例,可以扩展脚本重写 _menu_pressed() 方法操作点击的菜单项,或者连接 menu_pressed 信号进行对点击的菜单进行操作

#=====================================================
#   菜单列表
#=====================================================
#  类似 test() 脚本中的菜单结构,可以生成不断嵌套的菜单
#=====================================================

class_name MenuList
extends BoxContainer


func _ready():
    test()

func test():
    # 测试创建
    init_menu({
        "File": [
            "Open", "Save", "Save As",
            {
                "Export": [
                    "Export",
                    "Export as..."
                ]
            }
        ],
        "Edit": [
            "Undo",
            {
                "number": [
                    "1",
                    "2",
                    "3",
                ],
                "item_02": [
                    "a",
                    "b",
                    "c",
                    {"d": [1, 2, 3, 4]},
                ]
            },
        ],
    })



signal menu_pressed(id, path)


var idx := -1
var popup_menu_map := {}
var popup_item_path_map := {}


#=====================================================
#   Set/Get
#=====================================================
## 获取弹窗菜单
func get_popup_menu(path: String) -> PopupMenu:
    return popup_menu_map[path] as PopupMenu


#=====================================================
#   自定义方法
#=====================================================
## 初始化菜单
func init_menu(data: Dictionary):
    call_deferred("add_menu", data)


## 添加菜单项
func add_menu(menu_data, popup : PopupMenu = null, parent_label: String = ""):

    idx += 1
    if popup != null:
        # Dictionary
        if menu_data is Dictionary:
            for key in menu_data:
                add_menu(
                    menu_data[key]
                    , popup
                    , parent_label.plus_file(key)
                )

        # Array
        elif menu_data is Array:
            for item in menu_data:
                add_menu(item, popup, parent_label)

        # String
        else:
            # 添加子菜单
            create_menu(parent_label, popup)

            # 添加菜单项
            var sub_popup := get_popup_menu(parent_label)
            var label : String = str(menu_data)
            if not label.begins_with("-"):
                popup_item_path_map[idx] = parent_label + "/" + label
                sub_popup.add_item(label, idx)

            else:
                sub_popup.add_separator()

    else:
        for key in menu_data:
            var menu = MenuButton.new()
            menu.switch_on_hover = true
            menu.text = str(key)
            add_child(menu)

            parent_label = "/" + key
            __popup_item_data__(parent_label, menu.get_popup())
            add_menu(menu_data[key], menu.get_popup(), parent_label)


## 创建菜单
func create_menu(menu_path: String, to_menu: PopupMenu = null):
    if menu_path == "":
        return

    if not popup_menu_map.has(menu_path):
        var arr : Array = menu_path.split("/")
        var last_menu : PopupMenu = to_menu
        for idx in range(1, arr.size()):
            var label : String = arr[idx]
            var sub_path = "/".join(arr.slice(0, idx))
            if not popup_menu_map.has(sub_path):
                var item_label = sub_path.right( sub_path.find_last("/") ).trim_prefix("/")
                var temp_popup = PopupMenu.new()
                temp_popup.name = item_label
                __popup_item_data__(sub_path, temp_popup)

                last_menu.add_child(temp_popup)
                last_menu.add_submenu_item( item_label, item_label )
            last_menu = get_popup_menu(sub_path)


## 点击菜单
func _menu_pressed(idx: int, path: String):
    printt(idx, path)
    pass


#=====================================================
#   连接信号
#=====================================================
# 弹窗菜单路径
func __popup_item_data__(path: String, popup: PopupMenu):
    popup_menu_map[path] = popup
    if not popup.is_connected("id_pressed", self, "__popup_id_pressed__"):
        popup.connect("id_pressed", self, "__popup_id_pressed__")


# 点击菜单连接信号
func __popup_id_pressed__(id: int):
    _menu_pressed(id, popup_item_path_map[id])
    emit_signal("menu_pressed", id, popup_item_path_map[id])

发表评论