应用菜单 Menu
本教程共 42 篇 · 第 23 篇 · 更新于 2026-08-03
23. 应用菜单 Menu
本节目标
- 用 menu 包在 Go 侧拼出原生应用菜单。
- 给菜单项加子菜单、快捷键和勾选状态。
- 知道菜单只能在 Go 里更新,前端无法直接操作。
23-1 菜单写在 Go 侧
桌面应用头顶那条”文件 / 编辑 / 帮助”就是应用菜单。
Wails 的菜单是原生控件,不是网页画的。所以它在 Go 里定义,再交给框架渲染。
关键包就两个:menu 管菜单本身,menu/keys 管快捷键。
import (
"runtime"
"github.com/wailsapp/wails/v2"
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
"github.com/wailsapp/wails/v2/pkg/options"
rt "github.com/wailsapp/wails/v2/pkg/runtime"
)
Note菜单和上一章讲的”右键 HTML 菜单”是两码事。这里是系统原生菜单条,跨平台长相一致;右键菜单是你自己写的 React 组件。
23-2 拼一个应用菜单
先 menu.NewMenu() 建个空菜单,再往里加子菜单。
每个子菜单用 AddSubmenu,里面的项用 AddText、AddSeparator:
func (a *App) applicationMenu() *menu.Menu {
AppMenu := menu.NewMenu()
if runtime.GOOS == "darwin" {
AppMenu.Append(menu.AppMenu()) // macOS 必须紧跟 NewMenu 之后
}
FileMenu := AppMenu.AddSubmenu("文件")
FileMenu.AddText("打开", keys.CmdOrCtrl("o"), func(_ *menu.CallbackData) {
// 做点打开的事
})
FileMenu.AddSeparator()
FileMenu.AddText("退出", keys.CmdOrCtrl("q"), func(_ *menu.CallbackData) {
rt.Quit(a.ctx)
})
if runtime.GOOS == "darwin" {
AppMenu.Append(menu.EditMenu()) // macOS 要这个才能用复制粘贴快捷键
}
return AppMenu
}
最后在 wails.Run 里把菜单挂上:
err := wails.Run(&options.App{
Title: "码上学笔记",
Width: 800,
Height: 600,
Menu: app.applicationMenu(),
Bind: []interface{}{app},
})
Warning在 macOS 上,
menu.AppMenu()必须紧跟NewMenu()之后调用。顺序错了,苹果菜单(左上角那个带应用名的)就不显示。
23-3 菜单项类型与快捷键
菜单项有五种类型:Text、Separator、Submenu、Checkbox、Radio。
日常用 AddText 加普通项,AddSeparator 加分隔线,AddCheckbox 和 AddRadio 加可勾选项。
ViewMenu := AppMenu.AddSubmenu("视图")
ViewMenu.AddCheckbox("显示侧边栏", true, nil, func(cd *menu.CallbackData) {
// cd.MenuItem.Checked 能拿到最新勾选状态
})
快捷键靠 keys 包。最常用的是 CmdOrCtrl,在 Mac 上是 Command、其它平台是 Ctrl:
keys.CmdOrCtrl("o") // 打开
keys.OptionOrAlt("f") // 选项/Alt 组合
keys.Shift("n") // Shift 组合
keys.Combo("a", keys.ControlKey, keys.OptionOrAltKey) // 多修饰键
Note快捷键也能用字符串解析,适合存配置文件:
keys.Parse("Ctrl+Option+A")。写法和 Electron 一致。
回调拿到的 CallbackData 里有 MenuItem,能查到是谁触发了这次点击。多个单选按钮共用一个回调时特别有用。
Role 字段目前只在 macOS 有效,用来声明”这是标准应用菜单”或”这是编辑菜单”。其它平台忽略即可。
23-4 运行时动态更新
菜单不是定死就不能动的。想换文案、改勾选,先改菜单结构,再通知框架刷新。
两个运行时方法都在 Go 侧:MenuSetApplicationMenu 整块替换,MenuUpdateApplicationMenu 只刷新:
func (a *App) SwitchTheme(dark bool) {
a.dark = dark
a.buildMenu() // 重新生成 a.appMenu
rt.MenuUpdateApplicationMenu(a.ctx)
}
a.appMenu 是你在 OnStartup 里存下的菜单指针:
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.appMenu = a.applicationMenu()
}
Warning菜单的运行时方法只在 Go 里有。前端 JavaScript 不支持创建或修改应用菜单(官方文档明确写了 JS 侧暂不支持)。要更新菜单,从 Go 调
MenuUpdateApplicationMenu。
23-5 实战:带勾选的视图菜单
做个”视图”菜单,控制几个面板的显隐。用 Checkbox 类型,勾上就显示侧边栏,取消就隐藏。
func (a *App) viewMenu() *menu.Menu {
m := menu.NewMenu()
view := m.AddSubmenu("视图")
view.AddCheckbox("显示侧边栏", true, nil, func(cd *menu.CallbackData) {
a.showSidebar = cd.MenuItem.Checked
rt.EventsEmit(a.ctx, "sidebar:toggle", a.showSidebar)
})
return m
}
回调里 cd.MenuItem.Checked 已经是用户点击后的最新值。把它通过事件发给前端,React 那边监听 sidebar:toggle 切换显示。
Note菜单回调里拿不到 React 的状态,只能拿到菜单项自身。要驱动界面,用
runtime.EventsEmit发事件,前端EventsOn接收,这是 Go 推给前端的常规通道。
23-6 菜单的平台差异
菜单在三个平台长得不太一样,写的时候要心里有数。
macOS 最特殊:必须有 AppMenu()(左上角应用菜单)和 EditMenu()(复制粘贴那一套),否则快捷键和菜单结构都不正常。AppMenu() 还要紧贴 NewMenu() 调用。
Windows 和 Linux 的菜单条长在窗口标题栏下方,结构相近。但 Linux 不同桌面环境(GNOME、KDE)对菜单的渲染也有细微差别,真机多测。
Warning在 macOS 漏掉
EditMenu(),用户按 Command+C / V 会失灵。这不是样式问题,是功能断掉,发布前务必在苹果机器上验一遍。
23-7 子菜单嵌套与禁用隐藏
菜单还能嵌套。给某个子菜单项再挂一层子菜单,适合做”最近打开的文件”这类多级结构。
file := AppMenu.AddSubmenu("文件")
recent := file.AddSubmenu("最近打开") // 返回子菜单句柄,可继续加项
recent.AddText("notes.md", nil, func(_ *menu.CallbackData) {})
recent.AddText("todo.md", nil, func(_ *menu.CallbackData) {})
AddSubmenu 返回的就是一个子菜单句柄,可以继续往上加项。层数没有硬性限制,但太深会让用户点得烦,两层是大多数应用的甜点区。
菜单项还能临时禁用或隐藏。回调里修改 MenuItem 的 Disabled 或 Hidden 字段,再调 MenuUpdateApplicationMenu 即可。
func (a *App) SetSaveEnabled(on bool) {
a.saveItem.Disabled = !on
rt.MenuUpdateApplicationMenu(a.ctx)
}
Disabled 是灰掉但还占位置,Hidden 是直接消失。两者用途不同:暂时不可用的功能用禁用(让用户知道它在),真的不该出现的用隐藏。
单选组(Radio)有个自动规则:相邻的一串 Radio 会被当成一个组,点一个自动取消其它。想放两组单选,中间必须插一个非 Radio 项隔开,否则它们会混成一组。
Warning分隔线
AddSeparator不要连续堆。两个分隔线挨着在原生菜单里会合并成一条,还显得啰嗦。每组功能之间放一条就够。
Tip菜单项很多时,用分隔线把”打开 / 保存”和”退出”这类操作分组,视觉上清爽,用户也好找。禁用项记得在状态变化时及时刷新,否则界面和实际能力对不上。
23-8 排错清单
菜单的排错清单。
第一,macOS 左上角没有应用菜单。漏了 menu.AppMenu(),且它必须紧贴 NewMenu() 之后。
第二,Command+C/V 失灵。macOS 漏了 EditMenu()。
第三,菜单改了不刷新。改完结构要调 MenuUpdateApplicationMenu,且只能在 Go 侧调,前端不支持。
第四,快捷键不生效。确认 keys.CmdOrCtrl 的写法,且菜单确实挂到了 options.Menu。
Tip菜单不显示时,先确认
wails.Run里Menu:字段有没有传进去。很多人菜单写好了,却忘了把它挂到 App 上,白忙一场。另外,菜单文案尽量简短,太长会被系统截断。带快捷键的项,快捷键显示在右侧,文案和快捷键之间留一个空格即可,别手动拼一堆横线占位置。
常见误区
WarningmacOS 的
menu.AppMenu()必须紧贴NewMenu()调用,挪到后面菜单就会缺左上角的应用项。
Note快捷键
CmdOrCtrl会自动按平台变成 Command 或 Ctrl,不用写两份。但显示文字时想区分平台,自己判断runtime.GOOS即可。
Tip频繁改菜单时,用
MenuUpdateApplicationMenu而不是每次MenuSetApplicationMenu,开销更小,界面也不闪。