外观
菜单与托盘
菜单由 *mygo.MenuItem 模板构建,可用作菜单栏、上下文菜单、Dock 菜单和托盘图标的菜单。
模板
go
menu := mygo.NewMenu([]*mygo.MenuItem{
{Role: mygo.RoleAppMenu},
{Label: "File", Submenu: []*mygo.MenuItem{
{Label: "New Note", Accelerator: "CmdOrCtrl+N", Click: func(item *mygo.MenuItem, win *mygo.Window) {
newNote()
}},
{Label: "Export…", Accelerator: "CmdOrCtrl+E", Click: exportNotes},
mygo.Separator(),
{Role: mygo.RoleClose},
}},
{Role: mygo.RoleEditMenu},
{Label: "View", Submenu: []*mygo.MenuItem{
{ID: "sidebar", Label: "Show Sidebar", Type: mygo.MenuItemCheckbox, Checked: true, Click: toggleSidebar},
mygo.Separator(),
{Label: "List", Type: mygo.MenuItemRadio, Checked: true},
{Label: "Grid", Type: mygo.MenuItemRadio},
mygo.Separator(),
{Role: mygo.RoleToggleDevTools},
{Role: mygo.RoleToggleFullScreen},
}},
{Role: mygo.RoleWindowMenu},
})一个 MenuItem 包含:
Label,以及Accelerator,即它的快捷键(见快捷键);Click,在菜单项被选择时于主线程调用,参数为该菜单项,以及在 Linux 和 Windows 上被点击的菜单栏或上下文菜单所属的窗口。其他菜单使用获得焦点的窗口,或 nil;Type:MenuItemNormal、MenuItemCheckbox、MenuItemRadio、MenuItemSeparator或MenuItemSubmenu;带有Submenu的项是子菜单,未特别设置时其他项为普通项。相邻的单选按钮项构成一组,组内勾选一项会取消勾选其他项。复选框和单选按钮项会在Click运行前切换Checked;Disabled、Hidden和ToolTip;Icon,显示在标签旁的图像(见图标);ID,用于通过Menu.ItemByID查找该项;Role,内置行为,自带标签和快捷键。
角色
角色提供用户对标准菜单项的期待行为,带有通常的标签和快捷键,可用 Label 和 Accelerator 覆盖:
- 编辑:
RoleUndo、RoleRedo、RoleCut、RoleCopy、RolePaste、RoleDelete、RoleSelectAll,macOS 上还有RolePasteAndMatchStyle。它们的按键首先交给内容,就像浏览器中一样:在 Windows 的页面里,WebView2 用页面自身的事件和剪贴板的所有格式进行剪切、复制和粘贴,而从菜单中选择"粘贴"则会把剪贴板的文本以键入的方式输入,因为页面可能无法读取剪贴板(当某个编辑角色收到页面无用的按键时也是如此,例如 Ctrl+Shift+C 对应的 Copy); - 格式化:
RoleBold、RoleItalic、RoleUnderline、RoleStrikethrough、RoleAlignLeft、RoleAlignCenter和RoleAlignRight,用于格式化原生 UI 的富文本编辑器或可编辑页面(document.execCommand)中的选区;Paste and Match Style 会把纯文本粘贴进富文本编辑器; - 页面:
RoleReload、RoleForceReload、RoleToggleDevTools(在原生 UI 窗口中,打开其检查器)、RoleResetZoom、RoleZoomIn、RoleZoomOut; - 窗口:
RoleToggleFullScreen、RoleMinimize、RoleZoom、RoleClose; - 文档:
RoleNew、RoleOpen、RoleOpenRecent、RoleSave、RoleSaveAs、RoleDuplicate、RoleRename、RoleMoveTo、RoleRevert; - 应用:
RoleQuit,macOS 上还有RoleAbout、RoleHide、RoleHideOthers、RoleUnhide、RoleFront、RoleServices、RoleStartSpeaking、RoleStopSpeaking; - 整个菜单:
RoleFileMenu、RoleEditMenu、RoleFormatMenu(Bold、Italic、Underline、Strikethrough 和各种对齐)、RoleViewMenu和RoleWindowMenu,macOS 上还有RoleAppMenu(以应用命名的菜单)、RoleWindow(列出窗口的菜单)和RoleHelp(带搜索框的菜单)。
带有 macOS 角色的项在其他平台上会被省略,因此一套模板适用于所有平台。
修改菜单项
对正在使用的菜单中的项,请用它们的方法进行修改,以便原生菜单随之更新:SetLabel、SetEnabled、SetVisible、SetChecked、SetAccelerator 和 SetIcon:
go
item := menu.ItemByID("sidebar")
item.SetChecked(false)Menu.Append 和 Menu.Insert 用于添加菜单项。
图标
菜单项的 Icon 是显示在其标签旁的图像,例如 VPN 托盘菜单中每个服务器对应的国旗。NewMenuIcon 从一张 PNG 图像创建一个图标,保留其自身颜色;若数据不是 PNG 图像则返回错误;每个图标只需创建一次,然后交给所有需要显示它的菜单项:
go
//go:embed flags/jp.png
var japanPNG []byte
japan, err := mygo.NewMenuIcon(japanPNG, false)
if err != nil {
log.Fatal(err)
}
servers := mygo.NewMenu([]*mygo.MenuItem{
{ID: "tokyo", Label: "Tokyo", Type: mygo.MenuItemRadio, Checked: true, Icon: japan},
{ID: "osaka", Label: "Osaka", Type: mygo.MenuItemRadio, Icon: japan},
{ID: "direct", Label: "Direct", Type: mygo.MenuItemRadio},
})图像会被放入 16×16 点的方框中,保持纵横比;请提供至少 32×32 像素,最好是 48×48 的图像,以便在高分辨率显示器以及 150% 和 200% 缩放下保持清晰。复选框或单选按钮项会同时显示其勾选标记。SetIcon 可在正在使用的菜单中替换某项的图标,传 nil 则移除它:
go
servers.ItemByID("tokyo").SetIcon(warning)
servers.ItemByID("osaka").SetIcon(nil)NewMenuIcon 的第二个参数 template(模板图),会让黑色加透明的图像采用菜单文本的颜色,就像 macOS 为其模板图像着色一样:在深色菜单和高亮背景上变为白色。Linux 则按原样显示模板图标。
无论有多少项显示某个图像、它们的菜单打开或重建多少次,包括同一图像创建的多个图标,MyGo 对每个图像只解码和缩放一次;当应用和菜单都不再持有某个图标时,它就释放由此生成的内容;Windows 10 菜单的位图随其菜单一起释放。不创建图标的应用不会链接绘制图标的代码。
- macOS 在所有菜单上,把图标(一个
NSImage)显示在勾选标记列之后,包括菜单栏自身的项,它们会在菜单栏中显示图标。 - Windows 11 的菜单由 MyGo 绘制,会在勾选标记与标签之间给图标留出单独的一列,与系统菜单一致,并按菜单的显示缩放进行缩放。Windows 10 把它们绘制为菜单项的位图(32 位,含 alpha 通道),位于勾选标记列中:被勾选的项会以带勾选边框的图标取代勾选标记。菜单栏自身的项不显示图标。
- Linux 在菜单栏的下级菜单和上下文菜单(GTK)的菜单项中显示图标;菜单栏自身的项不显示。托盘图标的菜单由面板绘制,面板通过 DBusMenu(
icon-data)获取图标:GNOME Shell 的 AppIndicator 扩展和 KDE Plasma 会显示它们,其他面板可能不显示。
菜单栏
go
mygo.App.SetMenu(menu)在 macOS 上,菜单栏属于应用。在应用设置自己的菜单之前,会得到一个默认菜单栏,包含应用菜单、File、Edit、View 和 Window 菜单:请在你的菜单中保留 RoleAppMenu 和 RoleEditMenu,它们使 Cmd+Q 以及复制粘贴可用。
在 Linux 和 Windows 上,菜单栏属于窗口。App.SetMenu 把菜单给每一个没有自己菜单的窗口,Window.SetMenu 给某个窗口设置自己的菜单;窗口默认没有菜单,带有文档的应用除外,它们会得到 File 和 Edit 菜单。App.SetMenu(nil) 移除菜单。
在 Windows 上,菜单栏和菜单跟随浅色或深色外观,与标题栏一致(见深色模式)。在 Windows 11 上,它们看起来与系统自身的菜单一样,如同文件资源管理器的菜单:更宽松的项目、圆角高亮,以及颜色更淡的快捷键。
设置了 AutoHideMenuBar 的窗口会隐藏菜单栏,把空间留给页面。单独按 Alt 或 F10 会显示菜单栏并让键盘焦点进入其中(Windows),或打开它的第一个菜单(Linux);用户选择某项或离开后,菜单栏再次隐藏。其快捷键始终有效:
go
mygo.NewWindow(mygo.WindowOptions{URL: "/", AutoHideMenuBar: true})Windows 上没有标题栏的窗口——无边框的或标题栏隐藏的——也没有空间放菜单栏:Alt 和 F10 会以从左上角弹出的方式打开菜单,位于页面自绘的标题栏下方。在 Linux 上,标题栏隐藏的窗口会像 AutoHideMenuBar 那样隐藏菜单栏,使窗口按钮保持在窗口顶部、页面的标题栏内;无边框窗口则在页面上方显示菜单栏。
全屏的窗口把整个屏幕留给页面:其菜单栏保持隐藏,直到退出全屏。Alt 和 F10 仍能打开它的菜单——在 Windows 上从左上角弹出,在 Linux 上则在菜单栏中打开,菜单打开期间菜单栏会显示。
上下文菜单
Menu.Popup 在窗口内鼠标所在位置显示菜单,并在其关闭后返回;PopupAt 在页面的指定位置显示菜单。可以在绑定的方法中显示它,由页面在右键时调用:
go
// ShowNoteMenu 显示某条笔记的上下文菜单。
func (n *Notes) ShowNoteMenu(ctx context.Context, id int64) {
menu := mygo.NewMenu([]*mygo.MenuItem{
{Label: "Rename", Click: func(*mygo.MenuItem, *mygo.Window) { n.startRename(id) }},
{Label: "Delete", Click: func(*mygo.MenuItem, *mygo.Window) { n.Delete(id) }},
})
menu.Popup(mygo.CallerWindow(ctx))
}ts
note.addEventListener("contextmenu", (e) => {
e.preventDefault();
Notes.showNoteMenu(id);
});页面若没有自己的处理函数,会显示 webview 的标准上下文菜单,包含针对文本的复制和粘贴,开发构建中还有"检查元素"。显示原生 UI的窗口通过 ContextMenu 为元素提供上下文菜单,其构建方式与其界面的其他部分相同。
webview 的菜单
Page.OnContextMenu 会在该标准菜单即将打开时修改它,就像 Electron 的 context-menu 事件一样。事件会告知被右键点击的内容(LinkURL、LinkText、ImageURL、MediaURL、SelectionText、Editable、FrameURL),其 Menu 包含 webview 自身的菜单项。你可以隐藏、重命名、禁用或移除它们,并添加应用自己的菜单项,它们与其他菜单中的项工作方式相同:
go
page.OnContextMenu(func(e *mygo.ContextMenuEvent) {
e.Menu.Remove(mygo.ContextMenuReload, mygo.ContextMenuInspect)
if e.LinkURL != "" {
e.Menu.Append(mygo.Separator(), &mygo.MenuItem{
Label: "Open in Browser",
Click: func(*mygo.MenuItem, *mygo.Window) { go mygo.Shell.OpenExternal(e.LinkURL) },
})
}
})e.PreventDefault() 不显示任何菜单;把 e.Menu 设为应用自己的菜单,则改为显示那个菜单。webview 的菜单项无论被移到何处,仍执行 webview 原本的动作,除非应用为某项设置了 Click,此时改由它执行。若页面自己的 contextmenu 处理函数调用了 preventDefault,则不打开任何菜单,监听器也不会被调用。
webview 的菜单项在所有提供它们的平台上具有相同的 ID:
| ID | 菜单项 |
|---|---|
ContextMenuBack、ContextMenuForward、ContextMenuReload、ContextMenuStop | 页面的导航 |
ContextMenuInspect | "检查元素",在带 DevTools 的构建中 |
ContextMenuUndo、ContextMenuRedo、ContextMenuCut、ContextMenuCopy、ContextMenuPaste、ContextMenuPasteAndMatchStyle、ContextMenuDelete、ContextMenuSelectAll | 编辑 |
ContextMenuSpellingGuess | 针对拼写错误单词给出的每个建议词 |
ContextMenuOpenLink、ContextMenuOpenLinkInNewWindow、ContextMenuDownloadLink、ContextMenuCopyLink | 链接 |
ContextMenuOpenImageInNewWindow、ContextMenuDownloadImage、ContextMenuCopyImage、ContextMenuCopyImageURL | 图像 |
ContextMenuOpenMediaInNewWindow、ContextMenuDownloadMedia、ContextMenuCopyMediaURL、ContextMenuMediaControls、ContextMenuMediaLoop、ContextMenuMediaFullScreen、ContextMenuPictureInPicture | 视频与音频 |
ContextMenuOpenFrameInNewWindow、ContextMenuLookUp、ContextMenuSearchWeb、ContextMenuShare、ContextMenuEmoji、ContextMenuPrint | 其余项 |
其他菜单项以平台自身的名称作为 ID:macOS 上是 WebKit 的标识符(WKMenuItemIdentifierTranslate),Linux 上是标准动作的昵称(unicode),Windows 上是 WebView2 的名称(createQrCode)。分隔符以及平台没有命名的项没有 ID。
各平台的做法:
- macOS 在 WebKit 菜单打开时原地修改它,因此"查找"、"翻译"、"服务"和拼写建议继续可用。WebKit 不提供被点击内容的信息:MyGo 的运行时从页面自身文档中的
contextmenu事件上报这些信息,因此在 frame 中点击不会报告任何内容,FrameURL保持为空。 - Linux 原地修改 WebKitGTK 的菜单。WebKitGTK 会告知链接、图像、媒体以及元素是否可编辑,选区则由页面提供。应用的菜单项不显示快捷键或图标,单选按钮项显示为复选框,且只有 WebKitGTK 标准动作的项可以重命名。
FrameURL保持为空。 - Windows 的一切信息都来自 WebView2(1.0.1185 及更新版本;旧版运行时从不调用监听器)。应用修改过的菜单由 MyGo 绘制,与其其他弹出菜单一样,其中 WebView2 的菜单项仍执行各自的动作;未做修改的菜单就是 WebView2 自身的。WebView2 不为拼写建议提供单词,所以 MyGo 绘制的菜单会省略它们;它也会省略
data:图像的 URL,此时由页面的contextmenu事件提供。
应用修改过的菜单会自动去掉位于开头、结尾或紧接另一个之后的分隔符——因为移除两个分隔符之间的项后就会留下这种情形。
Dock 菜单
在 macOS 上,App.Dock.SetMenu 会在右键点击 Dock 图标打开的菜单中,把菜单项添加在标准项之上。与 App.SetMenu 一样,它可以在 main 中、App.Run 之前调用:
go
mygo.App.Dock.SetMenu(mygo.NewMenu([]*mygo.MenuItem{
{Label: "New Window", Click: func(*mygo.MenuItem, *mygo.Window) { openMainWindow() }},
}))托盘图标
托盘图标在 macOS 上位于菜单栏,在 Windows 和 Linux 上位于通知区域。请在应用就绪后创建它:
go
//go:embed resources/tray.png
var trayIcon []byte
mygo.App.WhenReady(func() {
tray, err := mygo.NewTray(mygo.TrayOptions{
Icon: trayIcon,
IconIsTemplate: true, // macOS 会为其着色以匹配菜单栏
ToolTip: "Notes",
Menu: mygo.NewMenu([]*mygo.MenuItem{
{Label: "Open Notes", Click: func(*mygo.MenuItem, *mygo.Window) { openMainWindow() }},
mygo.Separator(),
{Role: mygo.RoleQuit},
}),
})
if err != nil {
log.Println("tray:", err)
}
_ = tray
})图标是 PNG 格式,最好是 16×16 点(高分辨率显示器用 32×32 的图像);模板图标——透明底黑色——可让 macOS 为浅色和深色菜单栏分别着色。带菜单的托盘在点击时显示菜单。没有菜单时,点击送达 OnClick,OnRightClick 收到右键点击,在 macOS 上还包括 Control 点击,因为 Control 点击在那里就是次级点击。用 SetIcon、SetMenu、SetToolTip 以及 macOS 上的 SetTitle(图标旁的文本)修改托盘;Destroy 将其移除。
托盘菜单的项可以显示图标,例如每个服务器旁的国旗,并像任何菜单的项一样变化:菜单下次打开时会显示它们的状态。
显示变化数字的标题,例如网速,应保持宽度不变,否则该项会时大时小,带动其图标和旁边的项移动。在 macOS 上,标题中的数字宽度都相同,因此把数值补齐到固定字符数即可保持宽度:用数字空格(figure space,U+2007)填充,它与数字等宽,而普通空格更窄。
go
// " 9.0 MB/s"、" 12.5 MB/s"、"128.0 MB/s":宽度都相同。
func speed(mbps float64) string {
return strings.ReplaceAll(fmt.Sprintf("%5.1f", mbps), " ", "\u2007") + " MB/s"
}
tray.SetTitle(speed(rate))只有数字是等宽的:字母保持各自的宽度,因此会切换单位的标题(9 KB/s,随后 1.2 MB/s)会随单位改变宽度。是否保持宽度稳定由应用决定,例如按上文那样始终以同一单位显示数值。
一个应用可以显示多个图标,例如菜单栏应用把每项测量值放在自己的条目中。在 macOS 上,新图标出现在已有图标的左侧,用户可以按住 ⌘ 拖动图标来移动它们:给每个图标一个在应用图标中唯一的 ID,那么每当它重新创建——下次启动时或 Destroy 之后——都会回到用户放置的位置。在 Windows 上,由用户选择在任务栏(而非溢出区)中显示哪些图标,带 ID 的图标会保持这一选择,无论应用以何种顺序创建图标。
模板图标会自动跟随菜单栏。彩色图标——例如按各自颜色绘制每项测量值的图标——需要针对它所显示的栏来绘制颜色:Tray.Dark 报告该栏是否为深色(macOS 菜单栏,受系统外观或其下方壁纸的色调影响,或 Windows 任务栏),OnAppearanceChange 在其变化时通知,以便重新设置图标:
go
draw := func() { tray.SetIcon(meterIcon(tray.Dark()), false) }
tray.OnAppearanceChange(draw)
draw()Tray.PopUpMenu 以编程方式打开用 SetMenu 设置的菜单。在 macOS 上,AppKit 把它放在状态项下方,并在菜单打开期间高亮其按钮。若只想在右键时显示菜单,同时保留主点击可用,可以在 OnRightClick 中设置菜单,调用 PopUpMenu,待其返回后用 SetMenu(nil) 清除菜单。
托盘图标不会让应用保持运行:最后一个窗口关闭时应用就退出(退出)。若想把窗口"关闭到托盘",请改为隐藏它,这样窗口和应用都得以保留。退出流程(如托盘菜单中的 RoleQuit)在窗口关闭之前开始,因此 OnClose 能够区分它触发的关闭与用户触发的关闭:
go
var quitting bool // OnBeforeQuit 和 OnClose 在主线程运行
mygo.App.OnBeforeQuit(func(*mygo.QuitEvent) { quitting = true })
win.OnClose(func(e *mygo.CloseEvent) {
if !quitting {
e.PreventDefault()
win.Hide()
}
})
// 没有菜单时点击图标,或点击其菜单中的某一项,可让窗口重新显示。
tray.OnClick(win.Show)在 Linux 上,托盘图标使用 AppIndicator,需要 libayatana-appindicator3:它们显示菜单;带 OnClick 监听器的图标会响应激活(需要包含 activate 信号的库版本;旧版本不会报告点击)。GNOME 的 AppIndicator 扩展在双击时发送该信号,并在双击时间间隔之后的单击时打开菜单。缺少该库时 NewTray 返回错误。
常驻菜单栏的应用在 macOS 上通常会去掉其 Dock 图标:
go
mygo.App.SetActivationPolicy(mygo.ActivationPolicyAccessory)关于打开原生 UI 窗口的图标,参见菜单栏示例。它在 macOS 和 Windows 上不设置托盘菜单,用 OnClick 切换一个用 Tray.Bounds 定位的无边框窗口,并在 OnBlur 或 Escape 时将其隐藏。在 Linux 上,通过 AppIndicator 菜单项打开同一个窗口。
快捷键
快捷键是由 + 连接的带修饰键的按键,例如 CmdOrCtrl+N 或 Alt+Shift+F4,不区分大小写。
修饰键:
| 修饰键 | 按键 |
|---|---|
CmdOrCtrl(CommandOrControl) | macOS 上为 Command,其他平台为 Control |
Cmd(Command)、Super、Meta、Win | macOS 上为 Command,其他平台为 Windows 键或 Super 键 |
Ctrl(Control) | Control |
Alt(Option、AltGr) | Alt,macOS 上为 Option |
Shift | Shift |
按键:
- 单个字符:字母、数字或
,、/之类的符号(+用Plus); F1到F24;Enter(Return)、Tab、Space、Backspace、Delete(Del)、Insert(Ins)、Escape(Esc);Up、Down、Left、Right、Home、End、PageUp、PageDown;Num0到Num9、NumDec、NumAdd、NumSub、NumMult、NumDiv(数字小键盘);VolumeUp、VolumeDown、VolumeMute、MediaPlayPause、MediaNextTrack、MediaPreviousTrack、MediaStop、PrintScreen、CapsLock、NumLock、ScrollLock。
注册全局快捷键使用同样的语法。