当前位置: 首页 > article >正文

【Godot4.3】自定义简易菜单栏节点ETDMenuBar

概述

Godot中的菜单创建是一个复杂的灾难性工作,往往无从下手,我也是不止一次尝试简化菜单的创建。

从自己去年的发明“简易树形数据”用于简化Tree控件获得灵感,于是尝试编写了用于表示菜单数据的EasyMenuData类,以及对应的纯文本数据格式和对应的MenuBar控件扩展ETDMenuBar

于是乎,你只需要在创建菜单栏时,添加一个ETDMenuBar控件,并为其data属性指定符合规定的简易数据,以及图标集。便可以轻松设计和获得复杂层次的菜单,以用于你创建的Godot桌面程序。

在这里插入图片描述

得到的效果:

运行效果

原理

  • MenuBar生成菜单的原理,可以参看我之前写的《【Godot4.2】菜单相关控件和节点完全解析》
  • 简易树形数据解析的原理,可以参看我之前写的《【Godot4.2】EasyTreeData通用解析》

EasyMenuData

  • 我创建了一个名叫EasyMenuData的类,可以解析形如下的数据,并通过其to_menu()方法返回一个PopUpMenu实例
文件
	新建 | 0 | -1 | Ctrl+S
	打开 | 1 | 0
	最近 | 1 | 1
		文件1
		文件2
		文件3
========
	关闭 | -1 | -1 | Ctrl+Q

其格式采用:

文本 | 图标索引 | 复选状态标记 | 快捷键

其中:

  • |是分隔符,顺序和意义对应,可以忽略后面的设定,但顺序依然不能改变,符合要求的形式举例如下:
文本
文本 | 图标索引
文本 | 图标索引 | 复选状态标记
文本 | 图标索引 | 复选状态标记 | 快捷键
  • 图标索引:为负数或超出icons属性提供的图标集范围时,将不显示
  • 复选状态标记:为负数则不显示复选框,大于等于0,显示复选框,0不选中,大于0选中
  • 快捷键:只需要指定以+号连接的字符串形式即可,不区分大小写

ETDMenuBar

  • 我创建了一个扩展的MenuBar类型ETDMenuBar,其data属性接收如下形式的数据:
文件
	新建 | 0 | -1 | Ctrl+S
	打开 | 1 | 0
	最近 | 1 | 1
		文件1
		文件2
		文件3
	---
	关闭 | -1 | -1 | Ctrl+Q
========
编辑
	撤销
	重做
	---
	清空

也就是在多个一级菜单之间用========进行分隔。

  • 运行场景后,会自动根据data属性给定的数据,生成菜单栏。

EasyMenuData源码

# ========================================================
# 名称:EasyMenuData
# 类型:类
# 简介:基于ETD构造PopupMenu的类
# 作者:巽星石
# Godot版本:v4.3.stable.steam [77dcf97d8]
# 创建时间:202522120:11:05
# 最后修改时间:20253122:24:40
# ========================================================
class_name EasyMenuData

var _root:EasyMenuItem

# 获取生成的菜单
func to_menu() -> PopupMenu:
	return _root.to_menu()

# 获取根节点信息
func get_root_data() -> String:
	return _root.data if _root else ""

# ============================ 内部类 ============================
# 单项数据
class EasyMenuItem:
	# ------- 图标集
	var icons:Array[Texture2D]
	var icon_width:float   # 图标最大宽度
	# ------- 菜单项信息
	var label:String       # 菜单项文本
	var icon:Texture2D     # 菜单项图标
	var shortcut:Shortcut  # 菜单项快捷键
	var show_checkbox:bool # 是否显示复选
	var checked:bool       # 是否选中
	# ------- 节点信息
	var data:String                    # 原始未解析的文本
	var deep:int                       # 节点深度
	var parent:EasyMenuItem            # 父节点
	var children:Array[EasyMenuItem]   # 子节点集合
	
	func _init(_data:String,_deep:int,_icons:Array[Texture2D],_icon_width:float) -> void:
		data = _data
		deep = _deep
		icons = _icons
		icon_width = _icon_width
		parse_data()
		children = []
	
	# 解析传入的数据
	func parse_data():
		if data != "" or data != "---":
			var ds = data.split(" | ",false)
			match ds.size():
				1:
					label = data
				2:
					label = ds[0]
					var idx = int(ds[1])
					icon = get_idx_icon(idx)
				3:
					label = ds[0]
					var idx = int(ds[1])
					icon = get_idx_icon(idx)
					show_checkbox = is_show_box(int(ds[2]))
					checked = int(ds[2])
					
				4:
					label = ds[0]
					var idx = int(ds[1])
					icon = get_idx_icon(idx)
					show_checkbox = is_show_box(int(ds[2]))
					checked = int(ds[2])
					shortcut = new_shortcut(ds[3])
	# 是否显示复选框
	func is_show_box(tag:int) -> bool:
		return true if tag >= 0 else false  # 仅在大于等于0显示
	
	# 获取对应下标的图标
	func get_idx_icon(idx:int) -> Texture2D:
		var icn:Texture2D
		if icons:
			if idx in range(icons.size()):
				icn = icons[idx]
		return icn
	
	# 按给定字符串创建并返回一个快捷键
	func new_shortcut(key_str:String) -> Shortcut:
		var sc:Shortcut
		if key_str != "":
			sc = Shortcut.new()
			var event := InputEventKey.new()
			event.pressed = true
			var keys = key_str.split("+",false)
			for key in keys:
				match key.to_lower(): # 小写
					"ctrl":
						event.ctrl_pressed = true
					"alt":
						event.alt_pressed = true
					"shift":
						event.shift_pressed = true
					_:
						event.keycode = OS.find_keycode_from_string(key)
			sc.events.append(event)
		return sc
	
	func get_path():
		var path = ""
		path += label
		if parent:
			path = parent.get_path() + "/" + path
		return path
		
	# 转为菜单项或子菜单
	func to_menu(menu:PopupMenu = null):
		var root_menu:PopupMenu
		if menu == null:  # 根节点
			menu = PopupMenu.new()
			if data != "":
				menu.name = label  # 名称与菜单项一致
			root_menu = menu
			for child in children:
				child.to_menu(root_menu)
			return root_menu
		else:
			# 添加菜单项
			if data == "---":  # 分割线
				menu.add_separator()
			else:
				menu.add_item(label)  # 文本
				var last = menu.item_count-1
				# 将路径以元数据形式存储
				menu.set_item_metadata(last,get_path())
				# 图标
				if icon: 
					menu.set_item_icon(last,icon)
					menu.set_item_icon_max_width(last,icon_width) # 设定图标宽度
				# 复选框
				menu.set_item_as_checkable(last,show_checkbox)
				if show_checkbox:
					menu.set_item_checked(last,checked)
				# 快捷键
				if shortcut:
					menu.set_item_shortcut(last,shortcut,true)
			# 如果有子节点
			if children.size()>0:
				# 创建子PopupMenu
				var sub_menu = PopupMenu.new()
				sub_menu.name = label  # 名称与菜单项一致
				menu.add_child(sub_menu)
				# 指定为当前项的子菜单
				menu.set_item_submenu_node(menu.item_count-1,sub_menu)
				for child in children:
					child.to_menu(sub_menu)
			

# ============================ 方法 ============================
# 创建并返回一个EasyTreeItem实例
func create_item(text:String,icons:Array[Texture2D],icon_width:float,p_node:EasyMenuItem = null) -> EasyMenuItem:
	var itm = EasyMenuItem.new(text,0,icons,icon_width)
	if _root:
		if p_node:
			itm.deep = p_node.deep + 1
			itm.parent = p_node
			p_node.children.append(itm)
		else:
			itm.deep = _root.deep + 1
			itm.parent = _root
			_root.children.append(itm)
	else:
		_root = itm
	return itm

# 由多行文本创建
static func new_with_etd_str(etd_str:String,icons:Array[Texture2D],icon_width:float) ->EasyMenuData:
	var edt = EasyMenuData.new()
	var items = etd_str.split("\n",false)    # 将ETD字符串按行切分为字符串数组
	var pre_itm:EasyMenuItem                 # 记录前一项
	var p_itm:EasyMenuItem = null            # 记录父节点

	# 遍历每行数据
	for i in range(items.size()):
		if items[i].strip_edges() != "":
			# 第1行直接添加为Tree控件的根节点(跳过下面if部分)
			# 从第2行开始比较当前行与前一行的缩进深度(也就是\t的数目)
			if i > 0: 
				var d_deep = deep(items[i-1]) - deep(items[i])  # 与前一行数据的缩进差值
				match d_deep:
					-1:                                # 缩进比前一项深:
						p_itm = pre_itm                # 将前一项作为父节点
					0:                                 # 缩进深度与前一项一样:
						p_itm = pre_itm.parent   # 父节点与前一项父节点一样
					_:                                 
						if d_deep>0:                   # 缩进比前一项浅
													   # 通过缩进差值计算获得合适的父节点
							p_itm = pre_itm            
							for j in range(d_deep+1):
								p_itm = p_itm.parent
			
			# 实际创建和添加TreeItemTree控件
			var itm:EasyMenuItem = edt.create_item(items[i].replace("\t",""),icons,icon_width,p_itm)
			pre_itm = itm                              # 将当前项记录为前一项
	return edt

# 返回字符串的Tab缩进值
static func deep(sttr:String):
	return sttr.rstrip(" ").count("\t")

ETDMenuBar源码

# ========================================================
# 名称:ETDMenuBar
# 类型:自定义控件(MenuBar扩展)
# 简介:基于EasyMenuData构造的MenuBar
# 作者:巽星石
# Godot版本:v4.3.stable.steam [77dcf97d8]
# 创建时间:202522120:49:44
# 最后修改时间:20253122:26
# ========================================================

class_name ETDMenuBar extends MenuBar

signal item_click(path:String) # 菜单项被点击


# ================================ 参数 ================================
@export_multiline var data:String   ## 菜单栏简易数据
@export var icons:Array[Texture2D]  ## 图标集合
@export var icon_width:float = 16.0 ## 图标最大宽度

func _ready() -> void:
	reload()

# ============================ 方法 ============================
# 重新加载
func reload():
	clear()
	for dt in data.split("=".repeat(8),false):
		var emd = EasyMenuData.new_with_etd_str(dt,icons,icon_width)
		var menu = emd.to_menu()
		set_connects(menu)
		add_child(menu)

# 递归形式为所有层级的菜单处理菜单项点击的信号处理
func set_connects(menu:PopupMenu):
	# 统一设置字号
	var font_size = get("theme_override_font_sizes/font_size")
	menu.set("theme_override_font_sizes/font_size",font_size)
	menu.connect("index_pressed",func(index:int):
		emit_signal("item_click",menu.get_item_metadata(index))
	)
	for sub_menu in menu.get_children():
		if sub_menu is PopupMenu:
			set_connects(sub_menu)

# 清空子节点
func clear():
	for child in get_children():
		child.queue_free()

菜单项点击的处理

只需要链接item_click信号,就可以处理菜单项的点击了。

在这里插入图片描述

信号参数path会返回菜单项的路径,类似于下面这样:

文件/最近/文件2
文件/关闭

这样通过写一个math分支语句来匹配菜单项的路径,就可以实现具体菜单项的功能。

美化

通过外面套一个PanelContainer,并且设定flat,可以快速的美化菜单栏。

而且我设定所有的子菜单都保持于MenuBar一致的字号,只要在MenuBar上设置一次就可以了。

在这里插入图片描述

总结

  • EasyMenuData其实对应的是单个PopupMenu的描述和生成,可以用于普通菜单生成,也可用于弹出菜单的设计
  • ETDMenuBar,则是对应菜单栏生成。

http://www.kler.cn/a/567074.html

相关文章:

  • 大模型能给舆情分析带来哪些突破?
  • AI工具导航平台功能模块之混合分类器功能说明文档
  • Spark基础篇 RDD、DataFrame与DataSet的关系、适用场景与演进趋势
  • DeepSeek开源周技术全景:边缘计算开启“算力觉醒”新纪元
  • 58区间和+44开发商购买土地(前缀和)
  • uniapp 系统学习,从入门到实战(五)—— 组件库与常用 UI 组件
  • 【MySQL】增删改查
  • 目录遍历文件包含测试
  • 基于Milvus 向量数据库和Sentence Transformer构建智能问答系统
  • SqlServer占用CPU过高情况排查
  • 【C++奇迹之旅】:字符串转换成数字将数字转换成字符串大全
  • 深度学习五大模型:CNN、Transformer、BERT、RNN、GAN详细解析
  • Android15 am命令 APP安装流程
  • anaconda配置pytorch
  • C++ primer plus 第四节 复合类型
  • 深入解析 Svelte:下一代前端框架的革命
  • 前端实现上传图片到OSS(Vue3+vant)
  • 《深入浅出 Vue.js 组件化开发》
  • 设计一个“车速计算”SWC,通过Sender-Receiver端口输出车速信号。
  • Prometheus + Grafana 监控