Wails 顶部菜单栏用 menu 包创建,配合 runtime 包可以做文件操作、剪贴板、窗口控制等常见功能。
创建菜单
import (
"github.com/wailsapp/wails/v2/pkg/menu"
"github.com/wailsapp/wails/v2/pkg/menu/keys"
)
用 AddSubmenu 和 AddText 创建菜单项:
func (a *App) getMenu() *menu.Menu {
m := menu.NewMenu()
fileMenu := m.AddSubmenu("文件")
fileMenu.AddText("打开", keys.Control("o"), func(data *menu.CallbackData) {
// 点击执行
})
fileMenu.AddText("退出", &keys.Accelerator{}, func(data *menu.CallbackData) {
// 退出
})
return m
}
然后在 main.go 里挂上:
err := wails.Run(&options.App{
Title: "wails_study",
Menu: a.getMenu(),
Bind: []interface{}{a},
})
文件操作
用 runtime.OpenFileDialog 和 runtime.SaveFileDialog 打开系统原生对话框:
import "github.com/wailsapp/wails/v2/pkg/runtime"
func (a *App) getMenu() *menu.Menu {
m := menu.NewMenu()
fileMenu := m.AddSubmenu("文件")
fileMenu.AddText("打开文件", keys.Control("o"), func(data *menu.CallbackData) {
file, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择文件",
Filters: []runtime.FileFilter{
{DisplayName: "文本文件", Pattern: "*.txt;*.md"},
{DisplayName: "所有文件", Pattern: "*.*"},
},
})
if err == nil && file != "" {
fmt.Println("选中了:", file)
}
})
fileMenu.AddText("保存文件", keys.Control("s"), func(data *menu.CallbackData) {
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "保存文件",
DefaultFilename: "output.txt",
Filters: []runtime.FileFilter{
{DisplayName: "文本文件", Pattern: "*.txt"},
},
})
if err == nil && file != "" {
os.WriteFile(file, []byte("Hello Wails"), 0644)
}
})
fileMenu.AddSeparator()
fileMenu.AddText("退出", &keys.Accelerator{}, func(data *menu.CallbackData) {
runtime.Quit(a.ctx)
})
return m
}
剪贴板
写入和读取系统剪贴板文字:
func (a *App) getClipboardMenu() *menu.Menu {
m := menu.NewMenu()
editMenu := m.AddSubmenu("编辑")
editMenu.AddText("复制时间", keys.Control("c"), func(data *menu.CallbackData) {
runtime.ClipboardSetText(a.ctx, "当前时间:"+time.Now().Format(time.DateTime))
})
editMenu.AddText("粘贴并打印", keys.Control("v"), func(data *menu.CallbackData) {
text, err := runtime.ClipboardGetText(a.ctx)
if err != nil {
fmt.Println("读取剪贴板失败:", err)
} else {
fmt.Println("剪贴板内容:", text)
}
})
return m
}
全屏
全屏切换:
func (a *App) getScreenMenu() *menu.Menu {
m := menu.NewMenu()
screenMenu := m.AddSubmenu("屏幕")
screenMenu.AddCheckbox("全屏", false, keys.Key("f11"), func(data *menu.CallbackData) {
if runtime.WindowIsFullscreen(a.ctx) {
runtime.WindowUnfullscreen(a.ctx)
} else {
runtime.WindowFullscreen(a.ctx)
}
})
return m
}
用 AddCheckbox 配合 keys.Key("f11")——不用 modifier,直接按 F11。
窗口操作
控制窗口的最小化、最大化、置顶和刷新:
func (a *App) getWindowMenu() *menu.Menu {
m := menu.NewMenu()
windowMenu := m.AddSubmenu("窗口")
windowMenu.AddText("最小化", &keys.Accelerator{}, func(data *menu.CallbackData) {
runtime.WindowMinimise(a.ctx)
})
windowMenu.AddText("最大化", &keys.Accelerator{}, func(data *menu.CallbackData) {
if runtime.WindowIsMaximised(a.ctx) {
runtime.WindowUnmaximise(a.ctx)
} else {
runtime.WindowMaximise(a.ctx)
}
})
windowMenu.AddSeparator()
windowMenu.AddText("刷新页面", keys.Control("r"), func(data *menu.CallbackData) {
runtime.WindowReload(a.ctx)
})
return m
}
置顶
如果想把窗口始终保持在最前面,用 WindowSetAlwaysOnTop。注意它没有对应的判断方法,需要自己在代码里维护状态:
var alwaysOnTop = false
func (a *App) getWindowMenu() *menu.Menu {
m := menu.NewMenu()
windowMenu := m.AddSubmenu("窗口")
windowMenu.AddCheckbox("置顶", false, &keys.Accelerator{}, func(data *menu.CallbackData) {
alwaysOnTop = !alwaysOnTop
runtime.WindowSetAlwaysOnTop(a.ctx, alwaysOnTop)
})
return m
}
无边框窗口
在 main.go 里设置 Frameless: true 去掉原生标题栏,然后自己用 HTML/CSS 做一个:
err := wails.Run(&options.App{
Title: "wails_study",
Width: 1024,
Height: 768,
Frameless: true,
Menu: app.getMenu(),
Bind: []interface{}{app},
})
去掉边框后窗口无法拖动,需要在前端用一个可拖拽区域。Wails 通过 CSS 属性 --wails-draggable 控制:
.titlebar {
height: 40px;
background: #1a1a1a;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
--wails-draggable: drag;
}
.titlebar-title {
color: #fff;
font-size: 14px;
}
.titlebar-buttons {
display: flex;
gap: 8px;
--wails-draggable: no-drag;
}
.titlebar-buttons button {
width: 32px;
height: 24px;
border: none;
background: transparent;
color: #fff;
cursor: pointer;
border-radius: 4px;
}
.titlebar-buttons button:hover {
background: rgba(255,255,255,0.1);
}
.titlebar-buttons .close:hover {
background: #e81123;
}
--wails-draggable: drag 让该区域可拖动,按钮区域设置 no-drag 取消拖动以免干扰点击。
前端 Svelte 模板结合 runtime 的窗口方法控制最小化、最大化、关闭:
<div class="titlebar">
<span class="titlebar-title">wails_study</span>
<div class="titlebar-buttons">
<button on:click={minimise}>─</button>
<button on:click={toggleMaximise}>□</button>
<button class="close" on:click={closeApp}>✕</button>
</div>
</div>
import { WindowMinimise, WindowToggleMaximise, Quit } from '../wailsjs/runtime'
function minimise() { WindowMinimise() }
function toggleMaximise() { WindowToggleMaximise() }
function closeApp() { Quit() }
如果同时有菜单栏,菜单会因为没有原生标题栏而无法显示。解决方式是保留菜单,但自己实现标题栏(上面这种方式),Wails 的无边框窗口下菜单仍会渲染在顶部,需要把自定义标题栏放在菜单下方,或者直接用菜单项取代标题栏按钮。
打开链接
用 runtime.BrowserOpenURL 打开系统浏览器:
func (a *App) getHelpMenu() *menu.Menu {
m := menu.NewMenu()
helpMenu := m.AddSubmenu("帮助")
helpMenu.AddText("访问官网", &keys.Accelerator{}, func(data *menu.CallbackData) {
runtime.BrowserOpenURL(a.ctx, "https://wails.io")
})
return m
}
也可以打开其他应用的协议链接,比如 tencent://、weixin:// 等。
完整示例
把上面的功能组合在一起:
package main
import (
"context"
"fmt"
"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"
"github.com/wailsapp/wails/v2/pkg/runtime"
"os"
"time"
)
type App struct {
ctx context.Context
alwaysOnTop bool
}
func NewApp() *App {
return &App{}
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
func (a *App) getMenu() *menu.Menu {
m := menu.NewMenu()
// 文件
fileMenu := m.AddSubmenu("文件")
fileMenu.AddText("打开文件", keys.Control("o"), func(data *menu.CallbackData) {
file, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
Title: "选择文件",
Filters: []runtime.FileFilter{
{DisplayName: "文本文件", Pattern: "*.txt;*.md"},
},
})
if err == nil && file != "" {
fmt.Println("打开:", file)
}
})
fileMenu.AddText("保存文件", keys.Control("s"), func(data *menu.CallbackData) {
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "保存文件",
DefaultFilename: "output.txt",
})
if err == nil && file != "" {
os.WriteFile(file, []byte("Hello Wails"), 0644)
fmt.Println("保存到:", file)
}
})
fileMenu.AddSeparator()
fileMenu.AddText("退出", &keys.Accelerator{}, func(data *menu.CallbackData) {
runtime.Quit(a.ctx)
})
// 编辑
editMenu := m.AddSubmenu("编辑")
editMenu.AddText("复制时间", keys.Control("c"), func(data *menu.CallbackData) {
runtime.ClipboardSetText(a.ctx, "当前时间:"+time.Now().Format(time.DateTime))
})
editMenu.AddText("粘贴打印", keys.Control("v"), func(data *menu.CallbackData) {
text, err := runtime.ClipboardGetText(a.ctx)
if err == nil {
fmt.Println("剪贴板:", text)
}
})
// 窗口
windowMenu := m.AddSubmenu("窗口")
windowMenu.AddText("最小化", &keys.Accelerator{}, func(data *menu.CallbackData) {
runtime.WindowMinimise(a.ctx)
})
windowMenu.AddText("最大化", &keys.Accelerator{}, func(data *menu.CallbackData) {
if runtime.WindowIsMaximised(a.ctx) {
runtime.WindowUnmaximise(a.ctx)
} else {
runtime.WindowMaximise(a.ctx)
}
})
windowMenu.AddCheckbox("置顶", false, &keys.Accelerator{}, func(data *menu.CallbackData) {
a.alwaysOnTop = !a.alwaysOnTop
runtime.WindowSetAlwaysOnTop(a.ctx, a.alwaysOnTop)
})
windowMenu.AddSeparator()
windowMenu.AddText("刷新页面", keys.Control("r"), func(data *menu.CallbackData) {
runtime.WindowReload(a.ctx)
})
// 屏幕
screenMenu := m.AddSubmenu("屏幕")
screenMenu.AddCheckbox("全屏", false, keys.Key("f11"), func(data *menu.CallbackData) {
if runtime.WindowIsFullscreen(a.ctx) {
runtime.WindowUnfullscreen(a.ctx)
} else {
runtime.WindowFullscreen(a.ctx)
}
})
return m
}
func main() {
app := NewApp()
err := wails.Run(&options.App{
Title: "wails_study",
Width: 1024,
Height: 768,
Menu: app.getMenu(),
Bind: []interface{}{app},
})
if err != nil {
println("Error:", err.Error())
}
}
全局热键
菜单的快捷键(Accelerator)只在应用获得焦点时生效。如果想让快捷键在应用最小化或后台时也能触发,需要全局热键。
Wails v2 没有内置全局热键 API,可以用第三方库 golang.design/x/hotkey 实现。
注意它的实现:每个热键会占用一个 OS 线程(runtime.LockOSThread()),绑三五个没有问题,但不适合大量热键的场景。如果以后升级到 Wails v3,v3 内置了 app.GlobalShortcut 和 app.KeyBinding API,不需要第三方库。
安装:
go get golang.design/x/hotkey
示例:注册 Ctrl+Shift+S 作为全局热键,触发后通过 Event 通知前端。
import (
"golang.design/x/hotkey"
)
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
go a.registerGlobalHotkey()
}
func (a *App) registerGlobalHotkey() {
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
err := hk.Register()
if err != nil {
fmt.Println("全局热键注册失败:", err)
return
}
for {
<-hk.Keydown()
runtime.EventsEmit(a.ctx, "global_shortcut", "ctrl+shift+s")
}
}
前端监听:
EventsOn("global_shortcut", function(key) {
console.log("全局快捷键触发:", key)
})
这样无论应用是否在前台,按下 Ctrl+Shift+S 都会触发事件。
系统托盘
Wails v2 没有内置系统托盘 API,需要用第三方库实现。这里用 energye/systray,它是 getlantern/systray 的 fork,专门修复了 Wails 在 macOS 上的 ObjC 符号冲突,跨平台兼容性更好。
安装:
go get github.com/energye/systray
在 startup 里初始化:
import "github.com/energye/systray"
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
go systray.Run(a.onSystrayReady, a.onSystrayExit)
}
func (a *App) onSystrayReady() {
systray.SetIcon(icon.Data)
systray.SetTitle("Wails Study")
systray.SetTooltip("Wails Study")
showMenu := systray.AddMenuItem("显示窗口", "显示主窗口")
hideMenu := systray.AddMenuItem("隐藏窗口", "隐藏到托盘")
systray.AddSeparator()
quitMenu := systray.AddMenuItem("退出", "退出应用")
go func() {
for {
select {
case <-showMenu.ClickedCh:
runtime.WindowShow(a.ctx)
case <-hideMenu.ClickedCh:
runtime.WindowHide(a.ctx)
case <-quitMenu.ClickedCh:
runtime.Quit(a.ctx)
}
}
}()
}
func (a *App) onSystrayExit() {
// 清理
}
注意 systray.Run 会阻塞,必须在 go 里启动。图标文件放在项目中引用,或者用 base64 嵌入。
系统通知
energye/systray 没有内置通知 API,可以用 martinlindhe/notify 来发系统级的桌面通知。
安装:
go get github.com/martinlindhe/notify
使用:
import "github.com/martinlindhe/notify"
func (a *App) sendNotification() {
notify.Notify(
"Wails Study", // 应用名
"新消息", // 标题
"你有新的待办事项", // 正文
"", // 图标路径(空字符串使用默认图标)
)
}
带声音的提醒用 Alert:
notify.Alert("Wails Study", "错误", "操作失败", "")
各平台的实现:
| 平台 | 底层机制 |
|---|---|
| Windows | Windows 原生 Toast 通知 |
| macOS | osascript 调用 AppleScript 或 terminal-notifier |
| Linux | notify-send(需 libnotify) |
和菜单结合起来,比如文件保存成功后弹出通知:
fileMenu.AddText("保存文件", keys.Control("s"), func(data *menu.CallbackData) {
file, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
Title: "保存文件", DefaultFilename: "output.txt",
})
if err == nil && file != "" {
os.WriteFile(file, []byte("Hello Wails"), 0644)
notify.Notify("Wails Study", "保存成功", "文件已保存到: "+file, "")
}
})
总结
| 操作 | API |
|---|---|
| 打开文件 | runtime.OpenFileDialog |
| 保存文件 | runtime.SaveFileDialog |
| 复制到剪贴板 | runtime.ClipboardSetText |
| 从剪贴板粘贴 | runtime.ClipboardGetText |
| 全屏/退出 | runtime.WindowFullscreen / WindowUnfullscreen |
| 最小化/最大化 | runtime.WindowMinimise / WindowMaximise |
| 置顶 | runtime.WindowSetAlwaysOnTop(需自己记状态) |
| 刷新页面 | runtime.WindowReload |
| 打开链接 | runtime.BrowserOpenURL |
| 退出应用 | runtime.Quit |
