首页 / Wails 入门教程 / 应用菜单 Menu

Wails 入门教程

应用菜单 Menu

本教程共 42 篇 · 第 23 篇 · 更新于 2026-08-03

Wails桌面开发应用菜单Menu快捷键

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,里面的项用 AddTextAddSeparator

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 菜单项类型与快捷键

菜单项有五种类型:TextSeparatorSubmenuCheckboxRadio

日常用 AddText 加普通项,AddSeparator 加分隔线,AddCheckboxAddRadio 加可勾选项。

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 返回的就是一个子菜单句柄,可以继续往上加项。层数没有硬性限制,但太深会让用户点得烦,两层是大多数应用的甜点区。

菜单项还能临时禁用或隐藏。回调里修改 MenuItemDisabledHidden 字段,再调 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.RunMenu: 字段有没有传进去。很多人菜单写好了,却忘了把它挂到 App 上,白忙一场。另外,菜单文案尽量简短,太长会被系统截断。带快捷键的项,快捷键显示在右侧,文案和快捷键之间留一个空格即可,别手动拼一堆横线占位置。

常见误区

Warning

macOS 的 menu.AppMenu() 必须紧贴 NewMenu() 调用,挪到后面菜单就会缺左上角的应用项。

Note

快捷键 CmdOrCtrl 会自动按平台变成 Command 或 Ctrl,不用写两份。但显示文字时想区分平台,自己判断 runtime.GOOS 即可。

Tip

频繁改菜单时,用 MenuUpdateApplicationMenu 而不是每次 MenuSetApplicationMenu,开销更小,界面也不闪。