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

【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 EasyMenuDatavar _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 = _datadeep = _deepicons = _iconsicon_width = _icon_widthparse_data()children = []# 解析传入的数据func parse_data():if data != "" or data != "---":var ds = data.split(" | ",false)match ds.size():1:label = data2: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:Texture2Dif icons:if idx in range(icons.size()):icn = icons[idx]return icn# 按给定字符串创建并返回一个快捷键func new_shortcut(key_str:String) -> Shortcut:var sc:Shortcutif key_str != "":sc = Shortcut.new()var event := InputEventKey.new()event.pressed = truevar 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 scfunc get_path():var path = ""path += labelif parent:path = parent.get_path() + "/" + pathreturn path# 转为菜单项或子菜单func to_menu(menu:PopupMenu = null):var root_menu:PopupMenuif menu == null:  # 根节点menu = PopupMenu.new()if data != "":menu.name = label  # 名称与菜单项一致root_menu = menufor child in children:child.to_menu(root_menu)return root_menuelse:# 添加菜单项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:# 创建子PopupMenuvar 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 + 1itm.parent = p_nodep_node.children.append(itm)else:itm.deep = _root.deep + 1itm.parent = _root_root.children.append(itm)else:_root = itmreturn 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 MenuBarsignal 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,则是对应菜单栏生成。

相关文章:

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

概述 Godot中的菜单创建是一个复杂的灾难性工作,往往无从下手,我也是不止一次尝试简化菜单的创建。 从自己去年的发明“简易树形数据”用于简化Tree控件获得灵感,于是尝试编写了用于表示菜单数据的EasyMenuData类,以及对应的纯文…...

如何杀死僵尸进程?没有那个进程?

在题主跑代码的时候遇到了这样一种很奇怪的问题: 可以看到显卡0没有跑任何程序但是还是被占据着大量显存,这种进程称为“僵尸进程”,并且当我想kill它的时候,出现下面这种情况: 查过各种资料,最后我的解决…...

Solana 核心概念全解析:账户、交易、合约与租约,高流量区块链技术揭秘!

目录 1.Solana 核心概念简述 1.1. 账户(Account) 1.2. 交易(Transaction) 1.3. 交易指令(Instruction) 1.4. SPL 代币 1.5. 合约(Program) 1.6. 租约(Rent&#x…...

Leetcode-853. Car Fleet [C++][Java]

目录 一、题目描述 二、解题思路 Leetcode-853. Car Fleethttps://leetcode.com/problems/car-fleet/description/ 一、题目描述 There are n cars at given miles away from the starting mile 0, traveling to reach the mile target. You are given two integer array …...

012 rocketmq事务消息

文章目录 事务消息概念介绍交互流程事务消息原理TransactionListener接⼝TransactionProducer.javaTransactionConsumer.java 事务消息 内置topic中的消息对消费者不可见 本地事务mq消息事务消息 消息队列 RocketMQ 版提供的分布式事务消息适⽤于所有对数据最终⼀致性有强需求…...

ChatGPT与DeepSeek:开源与闭源的AI模型之争

目录 一、模型架构与技术原理 二、性能能力与应用场景 三、用户体验与部署灵活性 四、成本与商业模式 五、未来展望与市场影响 六、总结 随着人工智能技术的飞速发展,ChatGPT和DeepSeek作为两大领先的AI语言模型,成为了行业内外关注的焦点。它们在…...

Ollama的底层实现原理分析

一、背景 Ollama我们可以很方便的对DeepSeek等开源大模型进行部署,几条命令便能部署一个本地大模型服务,降低了非专业大模型开发者的门槛。 我们从中可以看到类似Docker的影子,ollama run 、ollama list等等,拉取对应大模型镜像&a…...

nginx 动态计算拦截非法访问ip

需求:在Nginx上实现一个动态拦截IP的方法,具体是当某个IP在1分钟内访问超过60次时,将其加入Redis并拦截,拦截时间默认1天。 技术选型:使用NginxLuaRedis的方法。这种方案通过Lua脚本在Nginx处理请求时检查Redis中的黑…...

商业秘密维权有哪些成本开支?

企业商业秘密百问百答之六十三:商业秘密维权费用项目有哪些? 在商业秘密维权过程中,原告可能需要支付多种费用,一般费用项目包括: 1、诉讼费。诉讼费是向法院支付的费用,包括起诉费、案件受理费等。这些费…...

使用UA-SPEECH和TORGO数据库验证自动构音障碍语音分类方法

使用UA-SPEECH和TORGO数据库验证自动构音障碍语音分类方法 引言 原文:On using the UA-Speech and TORGO databases to validate automatic dysarthric speech classification approaches 构音障碍简介 构音障碍是一种由于脑损伤或神经疾病(如脑瘫、肌萎缩侧索硬化症、帕金森…...

WebSocketHandler 是 Spring Framework 中用于处理 WebSocket 通信的接口

WebSocketHandler 是 Spring Framework 中用于处理 WebSocket 通信的接口,其主要作用是定义了如何处理 WebSocket 的各种事件和消息。以下是 WebSocketHandler 的主要作用和功能: ### 1. 处理 WebSocket 生命周期事件 WebSocketHandler 定义了多个方法来…...

Pikachu

一、网站搭建 同样的,先下载安装好phpstudy 然后启动Apache和Mysql 然后下载pikachu,解压到phpstudy文件夹下的www文件 然后用vscode打开pikachu中www文件夹下inc中的config.inc.php 将账户和密码改为和phpstudy中的一致(默认都是root&…...

如何使用 Jenkins 实现 CI/CD 流水线:从零开始搭建自动化部署流程

如何使用 Jenkins 实现 CI/CD 流水线:从零开始搭建自动化部署流程 在软件开发过程中,持续集成(CI)和持续交付(CD)已经成为现代开发和运维的标准实践。随着代码的迭代越来越频繁,传统的手动部署方式不仅低效,而且容易出错。为了提高开发效率和代码质量,Jenkins作为一款…...

Vue.js 学习笔记

文章目录 前言一、Vue.js 基础概念1.1 Vue.js 简介1.2 Vue.js 的特点1.3 Vue.js 基础示例 二、Vue.js 常用指令2.1 双向数据绑定(v-model)2.2 条件渲染(v-if 和 v-show)2.3 列表渲染(v-for)2.4 事件处理&am…...

数据存储:一文掌握RabbitMQ的详细使用

文章目录 一、RabbitMQ简介二、RabbitMQ的概述2.1 基本概念2.2 实际应用场景三、RabbitMQ的安装与配置3.1 安装RabbitMQ3.2 启用管理插件四、使用Python操作RabbitMQ4.1 安装Pika库4.2 生产者示例4.3 消费者示例4.4 发布/订阅模式示例五、RabbitMQ的高级特性5.1 消息持久化5.2 …...

辛格迪客户案例 | 祐儿医药科技GMP培训管理(TMS)项目

01 项目背景:顺应行业趋势,弥补管理短板 随着医药科技行业的快速发展,相关法规和标准不断更新,对企业的质量管理和人员培训提出了更高要求。祐儿医药科技有限公司(以下简称“祐儿医药”)作为一家专注于创新…...

FreeRtos实时系统: 十六.tickless低功耗模式

FreeRtos实时系统: 十六.tickless低功耗模式 一.tickless低功耗模式简介二.tickless模式详解三.tickless模式相关配置项四.tickless低功耗模式实验五.课堂总结 一.tickless低功耗模式简介 STM32低功耗模式: 二.tickless模式详解 为了可以降低功耗,又不…...

CSDN博客:Markdown编辑语法教程总结教程(上)

❤个人主页:折枝寄北的博客 Markdown编辑语法教程总结 前言1. CSDN Markdown编辑器功能简介1.1 基础操作界面1.2 创作助手和语法说明 2. Markdown编辑器语法2.1 目录2.2 标题2.2.1 标题级别设置2.2.2 标题居中 3. 文本样式3.1 强调文本(斜体&#xff09…...

多个pdf合并成一个pdf的方法

将多个PDF文件合并优点: 能更容易地对其进行归档和备份.打印时可以选择双面打印,减少纸张的浪费。比如把住宿发票以及滴滴发票、行程单等生成一个pdf,双面打印或者无纸化办公情况下直接发送给财务进行存档。 方法: 利用PDF24 Tools网站 …...

Spark基础篇 RDD、DataFrame与DataSet的关系、适用场景与演进趋势

一、核心概念与演进背景 1.1 RDD(弹性分布式数据集) 定义:RDD 是 Spark 最早的核心抽象(1.0版本引入),代表不可变、分区的分布式对象集合,支持函数式编程和容错机制。特点: 无结构化信息:仅存储对象本身,无法自动感知数据内部结构(如字段名、类型)。编译时类型安全…...

云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?

大家好,欢迎来到《云原生核心技术》系列的第七篇! 在上一篇,我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在,我们就像一个拥有了一块崭新数字土地的农场主,是时…...

Admin.Net中的消息通信SignalR解释

定义集线器接口 IOnlineUserHub public interface IOnlineUserHub {/// 在线用户列表Task OnlineUserList(OnlineUserList context);/// 强制下线Task ForceOffline(object context);/// 发布站内消息Task PublicNotice(SysNotice context);/// 接收消息Task ReceiveMessage(…...

STM32+rt-thread判断是否联网

一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...

最新SpringBoot+SpringCloud+Nacos微服务框架分享

文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...

【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验

系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...

ardupilot 开发环境eclipse 中import 缺少C++

目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...

docker 部署发现spring.profiles.active 问题

报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...

NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合

在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...

Linux nano命令的基本使用

参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...

全面解析数据库:从基础概念到前沿应用​

在数字化时代,数据已成为企业和社会发展的核心资产,而数据库作为存储、管理和处理数据的关键工具,在各个领域发挥着举足轻重的作用。从电商平台的商品信息管理,到社交网络的用户数据存储,再到金融行业的交易记录处理&a…...