外观
窗口(Windows)
窗口是一个原生窗口,显示从 URL 加载的网页,或者显示 MyGo 从其 Content 绘制的原生 UI。在应用就绪后,用 mygo.NewWindow 创建窗口:
go
mygo.App.WhenReady(func() {
win := mygo.NewWindow(mygo.WindowOptions{
Title: "Editor",
URL: "/",
Width: 1200,
Height: 800,
MinWidth: 600,
MinHeight: 400,
StateKey: "editor",
})
win.OnClosed(func() { log.Println("closed") })
})显示原生 UI 的窗口用 Content 代替 URL:
go
mygo.NewWindow(mygo.WindowOptions{Title: "Inspector", Content: ui.View(app.view)})本指南的所有内容都适用于这两类窗口,只有页面及其相关选项除外—— 页面只有显示网页的窗口才有。和所有 MyGo API 一样,窗口可以在任意 goroutine 中创建和修改;调用会转到驱动 UI 的主线程。
选项
零值的 WindowOptions 是一个 800×600、在屏幕上居中、可见且可调整大小的 窗口。尺寸和位置都以设备无关像素为单位。
| 选项 | |
|---|---|
Title | 窗口标题;默认为应用名称,页面的 <title> 会取代它 |
URL | 窗口创建后加载;/ 及其他不带 scheme 的 URL 是前端的页面 |
Content | 显示 MyGo 绘制的原生 UI,而非页面 |
Width、Height | 窗口的尺寸;若用 UseContentSize,则是所显示内容的尺寸 |
X、Y | 左上角;两者都为 0 时窗口居中 |
MinWidth、MinHeight、MaxWidth、MaxHeight | 调整大小的限制;0 表示无限制 |
FitContent | 将页面区域调整为页面内容的尺寸并随之变化,受上述限制约束,如同浮动窗口那样 |
StateKey | 在此键下记住窗口的边界和状态,见下文 |
Hidden | 创建窗口但不显示 |
Maximized、FullScreen | 以最大化或全屏状态创建 |
Frameless | 无标题栏和边框:由页面绘制,见自定义标题栏,或原生 UI |
TitleBarStyle | 隐藏标题栏,但把窗口按钮保留在页面上方,见自定义标题栏,或保留在原生 UI上方 |
TrafficLightPosition | 移动隐藏标题栏的窗口按钮(macOS) |
TitleBarHeight | 页面在窗口按钮下方绘制的标题栏高度(Linux、Windows) |
HideWindowButtons | 隐藏隐藏标题栏的窗口按钮,标题栏保留其顶部调整边缘、阴影和圆角:页面自行绘制按钮并调用 Minimize、Maximize、Unmaximize 和 Close |
Transparent、Vibrancy | 透明窗口,以及透明页面或原生 UI背后的材质(macOS、Windows 11 22H2):VibrancyGlass 和 VibrancyGlassClear 是 macOS 26 的 Liquid Glass。在 Windows 上,显示原生 UI 的透明窗口在不绘制之处透出背后的内容;带材质的窗口没有菜单栏:Alt 和 F10 会在弹出菜单中打开其菜单 |
CornerRadius | 圆角化无边框窗口的角:在 macOS 上其内容和材质会按圆角裁剪,Windows 11 会赋予它自己的圆角 |
BackgroundColor | 在页面绘制之前填充窗口的颜色,用 CSS 语法,如 "#1e1e1e",或用 "light-dark(#f5f5f7, #1e1e1e)" 跟随外观 |
Opacity | 介于 0 和 1 之间 |
DisableResize、DisableMove、DisableMinimize、DisableMaximize、DisableClose、DisableFullScreen、DisableShadow | 取消相应能力。在 Wayland 下的 Linux 上,DisableShadow 使无边框窗口或隐藏的标题栏不带 GTK 绘制的阴影和边框 |
AlwaysOnTop | 使窗口保持在其他窗口之上 |
SkipTaskbar | 让窗口不出现在任务栏中(Linux、Windows) |
AutoHideMenuBar | 仅当键盘处于菜单栏中时才显示菜单栏(通过 Alt 或 F10)(Linux、Windows),见菜单栏 |
Parent、Modal | 子窗口,对其父窗口为模态;要把它放在父窗口某部分旁边,见浮动窗口 |
Panel | 启动器或快速输入条的窗口,悬浮于所有应用之上,见面板 |
Page | 窗口页面的选项:其预加载脚本、可信来源、Web 检查器、缩放、user agent 及其后缀、轻扫与捏合 |
显示窗口而不闪烁
显示网页的窗口在页面绘制之前会显示其 BackgroundColor(默认为白色), 所以要让它与页面背景一致。当页面跟随深色模式时,用 light-dark(<light>, <dark>) 同时给出两种颜色:窗口会随外观切换。要想只在 页面就绪后才显示窗口,就把它们创建为隐藏,并在 OnReadyToShow 中显示:
go
win := mygo.NewWindow(mygo.WindowOptions{URL: "/", Hidden: true, BackgroundColor: "#1e1e1e"})
win.OnReadyToShow(win.Show)原生 UI 的窗口两者都不需要:它们没有要加载的页面,显示时即绘制第一帧。
记住窗口的位置
设置了 StateKey 的窗口,会把位置、尺寸以及最大化和全屏状态记在用户数据 目录的 window-state.json 中;该文件在窗口关闭和应用退出时写入。下次启动 时,用同一个键创建的下一个窗口会恢复它们,只要它会出现在已连接的显示器上。 这些信息会覆盖 X、Y、Width、Height、UseContentSize、Maximized 和 FullScreen,后者仍是首次启动时的默认值。给每类窗口各自的键: "main"、"preferences"。
把数据存在别处(例如自己的数据库)的应用,可以用 WindowStateStore 把窗口 状态存在那里——在创建第一个带 StateKey 的窗口之前设置好,就不会写 window-state.json:
go
type windowStates struct{ db *sql.DB }
func (s windowStates) LoadWindowStates() (map[string]mygo.WindowState, error) {
var data []byte
err := s.db.QueryRow(`SELECT value FROM settings WHERE key = 'windows'`).Scan(&data)
if err != nil {
return nil, err
}
var states map[string]mygo.WindowState
return states, json.Unmarshal(data, &states)
}
func (s windowStates) SaveWindowStates(states map[string]mygo.WindowState) error {
data, err := json.Marshal(states)
if err != nil {
return err
}
_, err = s.db.Exec(`INSERT OR REPLACE INTO settings (key, value) VALUES ('windows', ?)`, data)
return err
}
mygo.App.SetWindowStateStore(windowStates{db})MyGo 在创建第一个带 StateKey 的窗口时加载一次状态,并在这种窗口关闭和 应用退出时,在主线程上保存全部状态。没有状态或加载失败时,窗口按其选项 打开;保存失败会在下次重试。
恢复页面
窗口的位置随其 StateKey 恢复;页面则随其会话状态恢复:Page.SessionState 保存前进/后退列表,连同引擎保存的滚动位置和表单字段;PageOptions.SessionState 在新窗口或 web view 中用它取代 URL 恢复页面,后退项也包含在内。应用把状态 保存在存放数据的地方,以便下次启动时恢复页面,或在页面不显示时关闭它、稍后 再恢复,就像浏览器的休眠标签页:
go
win.OnClose(func(e *mygo.CloseEvent) {
if state, err := win.Page().SessionState(); err == nil {
os.WriteFile(sessionFile, state, 0o600)
}
})
// 下次启动时:
state, _ := os.ReadFile(sessionFile)
win := mygo.NewWindow(mygo.WindowOptions{StateKey: "main", URL: "/", Page: mygo.PageOptions{SessionState: state}})状态是不透明的字节,格式由 MyGo 版本化:包含页面的 URL 和引擎自身的状态。 平台无法读取的状态——在其他平台保存的,或由以其他方式编码它的 web 引擎 保存的——只会恢复页面当时所在的 URL;而根本不是状态的(缺失的文件)则加载 URL。状态可能包含用户在表单中输入的内容:把它当作用户数据来保管。 LoadHTML 加载的 HTML 不在其中。
WebKit 随时保存滚动位置和表单字段:它会在用户停止滚动片刻之后保存所显示 页面的滚动位置(WebKitGTK 2.38 及 Safari 16 的 WebKit 或更新版本),表单字段 只在滚出视野时才保存(比如窗口被隐藏时),其他历史条目的滚动位置则在页面 离开它们时保存。此后保存的状态包含这些内容,因此一个在显示时被保存的页面 会回到其滚动位置,但表单字段是它上次离开视野时的样子。
| 平台 | 会话状态 |
|---|---|
| macOS | WKWebView 的 interactionState,macOS 12 或更新;更早版本只有页面的 URL |
| Linux | webkit_web_view_get_session_state,序列化后保存,以及 webkit_web_view_restore_session_state |
| Windows | 只有页面的 URL:WebView2 没有前进/后退列表的 API |
尺寸、位置与状态
go
win.SetSize(1024, 768)
win.SetContentSize(1024, 700) // 页面区域
win.SetPosition(100, 100)
win.SetBounds(mygo.Rectangle{X: 100, Y: 100, Width: 1024, Height: 768})
win.Center()
win.Maximize() // Unmaximize、ToggleMaximize、IsMaximized
win.Minimize() // Restore、IsMinimized
win.SetFullScreen(true) // ToggleFullScreen、IsFullScreen
win.Hide() // Show、ShowInactive(不获取焦点)、IsVisible
win.Focus() // Blur、IsFocusedBounds、ContentBounds、Size、ContentSize 和 Position 读回这些值; mygo.Screen 给出各显示器的位置。Bounds 是整个窗口 的边界,包括标题栏和边框(Linux X11 上还包括窗口管理器的边框), ContentBounds 则是页面或原生 UI 显示所在区域(位于菜单栏下方)的边界。 在 Wayland 下的 Linux 上,合成器决定窗口的位置,忽略应用设置的位置,其装饰 也不属于 Bounds。
其他属性:
SetTitle、SetResizable、SetMovable、SetMinimizable、SetMaximizable、SetClosable、SetMinimumSize、SetMaximumSize、SetAlwaysOnTop、SetOpacity、SetHasShadow、SetBackgroundColor和SetVibrancy修改选项所设置的内容。在 Windows 上,SetVibrancy改变创建时 带材质窗口的材质,在其他窗口中则不显示材质。在 Linux 上,窗口保持其创建时 的阴影状态(DisableShadow)。SetIcon(png)给窗口自己的图标,显示在其标题栏和任务栏按钮上 (Linux、Windows)。SetSkipTaskbar让窗口从任务栏隐藏(Linux、Windows)。SetAutoHideMenuBar隐藏菜单栏直到 Alt 或 F10,或永久重新显示 (Linux、Windows)。SetVisibleOnAllWorkspaces让它在每个 Space 或虚拟桌面上显示 (macOS、Linux);在 macOS 上还会显示在其他全屏应用之上,如同 overlay 一样,即使设置了DisableFullScreen也一样——前提是应用是 accessory 类型 (ActivationPolicyAccessory、App.Dock.Hide):macOS 在那里不显示常规 应用的窗口,只显示面板。SetContentProtection(true)让内容不出现在截图和录屏中。SetIgnoreMouseEvents(true)让点击穿透到其背后的内容。SetActiveAppearance(true)让窗口在其他应用位于前台时仍保持激活状态的 绘制(macOS),使悬浮其上的面板所采用的通透 Liquid Glass (VibrancyGlassClear)保持通透。它使用一个私有的 AppKit 调用,未来的 macOS 可能取消它:上架 Mac App Store 的应用应当不使用它。SetWindowButtonsTheme(mygo.ThemeDark)为浅色应用中的深色背景绘制隐藏 标题栏的窗口按钮,例如页面顶部深色视图之上;ThemeLight则用于浅色背景;ThemeSystem重新跟随应用。页面和菜单保持应用的外观(Windows;macOS 和 Linux 的按钮保持应用外观)。SetProgressBar在任务栏按钮(Windows)、Dock 图标(macOS)或启动器条目 (支持它的 Linux dock)上显示任务进度:gowin.SetProgressBar(mygo.ProgressBar{Value: 0.4}) win.SetProgressBar(mygo.ProgressBar{State: mygo.ProgressIndeterminate}) win.SetProgressBar(mygo.ProgressBar{}) // 完成FlashFrame(true)请求用户注意:任务栏按钮闪烁(Windows)、窗口被标记为 紧急(Linux)或 Dock 图标弹跳(macOS)。
子窗口与模态窗口
带 Parent 的窗口保持在父窗口之上,并随父窗口关闭。它可以出现在任意显示器 上,父窗口移动时它原地不动,所有平台皆如此。在 macOS 上,父窗口每次到前台, 子窗口都被排在其上;父窗口 AlwaysOnTop 时它仍在其上;随父窗口进入全屏; 父窗口最小化或隐藏时它也隐藏。加上 Modal,它还会像对话框一样阻塞父窗口 直到自己关闭:
go
prefs := mygo.NewWindow(mygo.WindowOptions{
URL: "/preferences",
Parent: win,
Modal: true,
Width: 500,
Height: 400,
})浮动窗口
浮动窗口是没有边框、背景和任务栏按钮的窗口,由其父窗口拥有,放置在父窗口的 某个矩形(其锚点)旁边:组合框的下拉列表、悬停卡片、带表单的弹出面板。与 原生 UI 的 overlay 或页面的弹出面板不同,它可以超出父窗口 的边缘,其位置由显示器的工作区决定,而非父窗口的内容。它的页面或 原生 UI绘制其全部外观,包括背景:
go
fl := mygo.NewFlyout(mygo.FlyoutOptions{
Parent: win,
Anchor: mygo.Rectangle{X: 20, Y: 40, Width: 120, Height: 28}, // 位于 win 的内容区域
Placement: mygo.PlacementBottomStart,
Gap: 4,
Width: 240,
Height: 300,
Content: ui.View(app.suggestions),
})锚点以 DIP 为单位,相对于父窗口的内容区域;对于打开浮动窗口的元素, ui.Element.Bounds 给出的正是这个坐标系的矩形;零尺寸的矩形把它锚定到一个 点。Placement 指定浮动窗口放在锚点的哪一侧,以及沿该侧如何对齐,如 "bottom-start"、"right" 或 "top-end"。若工作区在那个位置放不下,浮动 窗口会翻到另一侧,或另一端(如果那里放得下),然后滑回工作区之内,再缩小以 适应,如同 GTK 放置菜单、Wayland 合成器放置弹出窗口的方式。它保持在父窗口 之上,随父窗口移动而跟随,并随父窗口关闭。SetAnchor 让它相对另一个矩形 定位,SetSize 在其所在位置调整大小;IsFlyout 区分浮动窗口与其他窗口。
带 Focusable 时,浮动窗口在显示时获取键盘,并像菜单一样在用户点击其外部、 其他窗口获取键盘或应用被停用时关闭。OnClose 监听者会收到通知,并可使其 保持打开——Wayland 除外,其合成器会直接将其关闭。在 Linux 和 Windows 上, 点击只关闭浮动窗口,如同关闭它们的菜单;在 macOS 上,点击会继续传给指针下 的对象,但落在锚点上时不会——如同 AppKit 的弹出面板。因此,点击打开它的按钮 会将其关闭,而不会再次打开。菜单、供选择的列表和弹出面板是可聚焦的,并自行 处理按键:方向键、回车。
不带 Focusable 时,浮动窗口从不获取键盘——键盘留在父窗口——而它会一直 存在直到被关闭:工具提示、悬停卡片,或父窗口中文本输入框的建议列表——由其 视图驱动并按需关闭,例如输入框失去键盘时。(在 Wayland 上,只有获取键盘的 弹出窗口才能得知其外部的点击。)
两种浮动窗口都会在其内容未接住的 Escape 键上关闭,视同用户主动关闭——即 原生 UI 中没有元素或快捷键接收它,或页面的处理函数没有用 preventDefault() 取消它,就像 Web 的弹出面板。
Shadow 让它在 macOS 上获得系统窗口阴影:阴影跟随浮动窗口所绘内容的形状 并描出一圈发丝线(因此在 macOS 上不要自绘边框);在 Windows 上,Windows 11 会像圆角化其菜单一样圆角化浮动窗口的角并为其描边(因此在那里既不要画边框 也不要画圆角)。
在 macOS 上,Popover 让它改为系统弹出面板的外观:它显示在 NSPopover 中, 由其绘制材质、圆角和指向锚点中部的箭头,并带有动画。AppKit 把它放在 Placement 指定的那一侧,或相反的一侧;放置的其余细节和 Gap 不适用。和 AppKit 的弹出面板一样,它在显示期间持有键盘,并且带 Focusable 时在用户点击 别处时关闭。其内容不在材质之上绘制背景:在原生 UI 中,c.Vibrancy() 会报告 是否处于材质中,因此视图在其他场合自行绘制面板:
go
func (app *App) about(c *ui.Context) {
c.Root().Background(ui.Transparent)
box := ui.Column(c).Fill() // 位于弹出面板的材质之上
if !c.Vibrancy() {
box = ui.Column(c).Fill().Background(c.Theme().Surface).Radius(10)
}
box.Padding(12).Children(func() { ui.Text(c, "MyGo") })
}浮动窗口也可以不挂在窗口上,而是挂在托盘图标上,充当菜单栏应用的面板:给 它 Tray 来代替 Parent。它出现在图标旁边,位于 Placement 指定的一侧, 悬浮在其他应用的窗口之上。在 macOS 菜单栏下方,PlacementBottom 让它居中 于图标之下(带 Popover 时,系统弹出面板的箭头指向图标);在 Windows 上, 同样的放置方式在任务栏位于屏幕底部时,会翻到图标上方。点击图标不会关闭可 聚焦的浮动窗口,因此图标的点击可以切换它的开关。当浮动窗口——应用唯一的 窗口——关闭时,要让应用继续运行:
go
mygo.App.OnWindowAllClosed(func() {})
tray.OnClick(func() {
if panel != nil {
panel.Close()
return
}
panel = mygo.NewFlyout(mygo.FlyoutOptions{
Tray: tray,
Placement: mygo.PlacementBottom,
Width: 280,
Height: 230,
Focusable: true,
Popover: true,
Content: ui.View(app.panel),
})
panel.OnClosed(func() { panel = nil })
})Linux 的托盘图标既不报告点击也不报告位置:从图标的菜单项打开浮动窗口,它 会显示在主显示器工作区的中央。examples/menubar-flyout 就是这样一款应用。
让浮动窗口随内容定尺寸
带 FitContent 时,浮动窗口采用其页面内容的尺寸并随之变化,如同浏览器为 扩展的弹出窗口定尺寸:带列表的菜单栏弹出面板、悬停卡片、启动器的结果。 Width 和 Height 只在页面报出自己的尺寸之前作为其尺寸——页面会在 DOM 就绪之前报告——因此以 Hidden 创建、从 OnReadyToShow 显示的浮动窗口, 一显示就是最终尺寸。MinWidth、MinHeight、MaxWidth 和 MaxHeight 约束 它,工作区也一样,如同任何浮动窗口:
go
fl := mygo.NewFlyout(mygo.FlyoutOptions{
Tray: tray,
Placement: mygo.PlacementBottom,
FitContent: true,
MaxWidth: 400,
MaxHeight: 600,
URL: "/panel",
})MyGo 在页面中的运行时把根元素(html)按内容宽度(width: max-content) 布局,在限制之内,使文本在 MaxWidth 处换行,并用 ResizeObserver 监视其 尺寸:高度就是内容的高度,包括 body 的边距;超过 MaxHeight 时页面滚动, 滚动条占用的空间也计入尺寸。给 html 设定自身尺寸(如 height: 100%)的 页面会让浮动窗口保持那个尺寸:应让根元素和 body 采用其内容的尺寸。尺寸是 页面的 CSS 像素乘以其缩放比例,以 DIP 为单位;不改变结果的新尺寸不会触发 调整。原生 UI(Content)不支持随内容定尺寸。
WindowOptions.FitContent 对窗口的页面区域做同样的事,面板也是如此,受其 MinWidth、MinHeight、MaxWidth、MaxHeight 及其显示器工作区的约束。 它只在内容尺寸变化时跟随,且只跟随发生变化的那个维度,因此用户对可调大小 窗口设定的尺寸在此之前保持不变,窗口最大化或全屏期间也不跟随。
各平台的实现方式:
| macOS | Linux | Windows | |
|---|---|---|---|
| 窗口 | 无边框、不激活的 NSPanel,显示期间作为其父窗口的子窗口;用于托盘时处于菜单层级 | GTK_WINDOW_POPUP:X11 上是 override-redirect,Wayland 上是父窗口的 xdg_popup;用于托盘时是保持在上方、无装饰的 utility 窗口 | 被拥有的 WS_POPUP、WS_EX_TOOLWINDOW,不重定向位图;用于托盘时 topmost 且无拥有者 |
| 放置 | 在屏幕的可见框架内 | X11 上在显示器的工作区内;Wayland 上用合成器的 positioner(gdk_window_move_to_rect) | 在显示器的工作区内(MonitorFromRect) |
| 键盘 | 可聚焦时成为 key window;父窗口仍是 main window | 可聚焦时抓取该 seat 以及应用的输入,如同 GTK 的菜单 | 可聚焦时激活,否则 WS_EX_NOACTIVATE |
在 Wayland 上,弹出窗口只显示在最顶层的窗口之上:显示一个浮动窗口会关闭 其他正在显示的,但不会关闭它自己的祖先——正如用户的预期。examples/flyout 打开一个颜色列表、一个便签编辑器和一个关于对话框——在 macOS 上后者是弹出 面板。
面板
面板是启动器、命令面板或快速输入条的窗口,正如 Spotlight 和 Raycast 的那样: 由全局快捷键从任何地方唤起,悬浮在每个应用的 窗口之上,出现在用户当前所在的 Space 或虚拟桌面上,全屏应用之上也是如此, 并且不出现在任务栏、Dock 的窗口列表和应用切换器中。它不能最小化、最大化或 进入全屏,在 Linux 和 Windows 上没有菜单栏。在 macOS 上,显示它会让它获得 键盘而不激活应用:用户原先所在的应用保持激活——其菜单栏照常显示——并在 面板隐藏时收回键盘。点击面板也保持原应用激活。
go
mygo.App.SetActivationPolicy(mygo.ActivationPolicyAccessory) // 无 Dock 图标(macOS)
mygo.App.OnWindowAllClosed(func() {}) // 没有窗口也继续运行
mygo.App.WhenReady(func() {
panel := mygo.NewWindow(mygo.WindowOptions{
Panel: true,
Hidden: true,
Frameless: true,
Transparent: true,
Vibrancy: mygo.VibrancyHUD,
Width: 680,
Height: 420,
URL: "/launcher",
})
// 用隐藏而不是关闭,以便下次立即显示:
// 在按下 Escape、Cmd-W 或 Alt-F4 时,以及用户切往别处时。
panel.OnClose(func(e *mygo.CloseEvent) {
e.PreventDefault()
panel.Hide()
})
panel.OnBlur(panel.Hide)
mygo.GlobalShortcut.Register("Alt+Shift+Space", func() {
if panel.IsVisible() {
panel.Hide()
return
}
// 位于指针所在的显示器,自上而下约三分之一处。
d := mygo.Screen.DisplayNearestPoint(mygo.Screen.CursorScreenPoint())
width, height := panel.Size()
panel.SetPosition(d.WorkArea.X+(d.WorkArea.Width-width)/2, d.WorkArea.Y+(d.WorkArea.Height-height)/3)
panel.Show()
})
})结果会增减的面板(如启动器)用 FitContent 采用页面的尺寸,如同 浮动窗口那样:给它 MaxHeight,而不是指定 一个 Height。
页面未用 preventDefault() 处理、原生 UI 也未接住的 Escape 会关闭面板, 视同用户主动关闭;OnClose 监听者可以把它转化为隐藏面板,如上文所示。启动器 通常从一开始就是 accessory 应用,没有 Dock 图标:在 mygo.config.ts 的 macos.infoPlist 中给它 LSUIElement,或在 App.Run 之前设置其激活策略, 如上文所示。常规应用在启动时即被激活,如同用户打开的任何应用:它显示的第一个 窗口——面板也一样——会把应用带到前台。
各平台的实现方式:
| macOS | Linux | Windows | |
|---|---|---|---|
| 窗口 | 带 nonactivating 样式的 NSPanel,永不作为 main window | utility 窗口(_NET_WM_WINDOW_TYPE_UTILITY),保持在上方,不出现在任务栏和分页器中;Wayland 下是 overlay 层的 surface(gtk-layer-shell) | topmost 工具窗口(WS_EX_TOOLWINDOW),由一个隐藏窗口拥有,因此永没有任务栏按钮,带 Windows 11 的圆角 |
| 工作区 | 每个 Space,作为全屏辅助窗口(CanJoinAllSpaces、FullScreenAuxiliary),不出现在 Mission Control 和窗口切换循环中 | 每个工作区(sticky) | 只显示在其出现的那个虚拟桌面上 |
| 键盘 | key window,应用保持非激活 | 活动窗口;在 layer surface 上显示期间独占输入 | 前台窗口,全局快捷键的按键让它得以取得 |
在 Wayland 下——应用既不能放置也不能提升窗口——若合成器支持 layer shell 协议(Sway、Hyprland、KDE Plasma、COSMIC……)且安装了 libgtk-layer-shell.so.0,面板就是 layer shell 的一个 surface,正如那里的 启动器:它显示在 overlay 层,在全屏窗口之上,不出现在任务栏,在用户所在的 输出上居中,或由 X 和 Y 或 SetPosition 放置的位置。它在显示期间保持 键盘,如同那里的其他启动器,以便从一开始就收到按键:它只在隐藏时才失去焦点, 所以要按 Escape 关闭它——用户在那里就是按 Escape。在其他地方,例如没有 layer shell 的 GNOME,它和其他窗口一样是普通窗口:合成器决定它显示在哪里、 谁在它上面。
事件
监听者在主线程运行:保持简短,耗时的工作交给 goroutine。每个 On… 方法 返回一个移除该监听者的函数。
| 事件 | 时机 |
|---|---|
OnClose(func(e *mygo.CloseEvent)) | 窗口即将关闭,由用户或 Close 触发;e.PreventDefault() 使其保持打开 |
OnClosed | 窗口已关闭 |
OnFocus、OnBlur | 获得或失去键盘焦点 |
OnShow、OnHide | 显示或隐藏 |
OnResize、OnMove | 调整大小或移动 |
OnMaximize、OnUnmaximize、OnMinimize、OnRestore | 状态改变 |
OnEnterFullScreen、OnLeaveFullScreen | 进入或离开全屏 |
OnReadyToShow | 其第一个页面已就绪可显示,或其原生 UI 绘制了第一帧 |
OnFileDrop | 文件被拖放到其页面上,见拖放文件;或拖放到未接收它们的原生 UI 上,见拖放文件 |
对有未保存更改的窗口,关闭前先询问:
go
win.OnClose(func(e *mygo.CloseEvent) {
if !doc.Dirty() {
return
}
e.PreventDefault()
go func() {
res, _ := mygo.Dialog.Message(mygo.MessageOptions{
Parent: win,
Type: mygo.MessageQuestion,
Message: "Save changes before closing?",
Buttons: []string{"Save", "Don't Save", "Cancel"},
})
switch res.Button {
case 0:
doc.Save()
win.Destroy()
case 1:
win.Destroy()
}
}()
})Close 像用户那样关闭窗口,会询问 OnClose 监听者;Destroy 立即关闭。
页面
一个窗口一次显示一个页面,并带有历史记录,就像浏览器的标签页。 win.Page() 返回当前页面;显示原生 UI的窗口返回 nil:
go
page := win.Page()
page.LoadURL("/settings") // LoadHTML, LoadFile
page.Reload() // ReloadIgnoringCache, Stop
page.GoBack() // GoForward, CanGoBack, CanGoForward
page.URL() // 当前页面
page.SetZoomFactor(1.25) // ZoomFactorWindowOptions.Page 配置页面:
go
mygo.NewWindow(mygo.WindowOptions{
URL: "/",
Page: mygo.PageOptions{ZoomFactor: 1.25, DevTools: mygo.DevToolsEnabled},
})| 选项 | |
|---|---|
PreloadScript | 在页面自身脚本之前运行的 JavaScript |
PreloadAllFrames | 也在页面的 iframe 中运行 PreloadScript,不论来源——那些 iframe 里没有 window.mygo |
TrustedOrigins | 其他可以调用 Go 的来源,见谁可以调用 |
DevTools | DevToolsAuto(开发构建中的检查器)、DevToolsEnabled 或 DevToolsDisabled;后者还会开启或关闭 WebView2 自身的按键行为(如 F5 重新加载)(Windows);对显示原生 UI 的窗口,则是其检查器 |
ZoomFactor、UserAgent | 页面的缩放(1 为 100%)和 user agent |
SafariTabFocus | 像 Safari 那样把 Tab 留给文本输入框和带 tabindex 的元素——除非系统的键盘导航已开启;默认情况下 Tab 也能到达链接、按钮、复选框和选择框(macOS) |
PictureInPicture | 允许页面的视频进入画中画,见画中画(macOS;WebView2 本来就允许) |
SwipeNavigation | 水平轻扫在页面历史中前进后退,如同浏览器,见手势 |
PinchZoom | 捏合放大页面,如同浏览器,见手势 |
UserAgentSuffix | 追加到 user agent 之后而非替换它(UserAgent 是替换),如 "MyApp/1.2";它也会追加到 UserAgent 之后,SetUserAgent 会保留它,首尾空格会被去掉 |
ContentRules | 拦截页面加载内容并隐藏其部分的规则,见拦截内容 |
BackgroundThrottling | ThrottlingOn(web view 自身的行为)或 ThrottlingOff,后者让页面在窗口隐藏、最小化或被遮挡时继续运行,见后台页面 |
Autoplay | AutoplayUserGesture(web view 自身的策略)或 AutoplayAllow,后者允许页面在用户点击或输入之前播放有声媒体,隐藏窗口中也可以,见自动播放 |
Session | 页面 cookie、存储和缓存的存放处,独立于其他会话,见会话;nil 表示默认会话 |
UserAgentSuffix 适用于想看到应用名称的站点,也适用于那些因为 user agent 里没有浏览器名称就退供旧版页面给裸 web view 的站点: "Version/17.0 Safari/605.1.15" 让页面看起来像 Safari。
页面事件,在 win.Page() 上:
| 事件 | 时机 |
|---|---|
OnWillNavigate(func(e *mygo.NavigateEvent)) | 页面即将导航,例如点击链接之后;e.PreventDefault() 取消它。e.UserInitiated 表明是用户的手势(点击链接,或其处理函数设置 location.href 的点击)触发的,还是页面脚本自行触发的。LoadURL 及其同类方法不会触发该事件。 |
OnDidNavigate(func(url string)) | 一次导航已提交 |
OnDOMReady | 页面的 DOM 已就绪(DOMContentLoaded) |
OnDidFinishLoad | 页面加载完成 |
OnDidFailLoad(func(err *mygo.LoadError)) | 页面加载失败 |
OnPageTitleUpdated(func(e *mygo.TitleEvent)) | 页面的 <title> 改变;阻止该事件可保持窗口标题不变 |
OnThemeColorChanged(func(c color.NRGBA, ok bool)) | 页面的主题色改变;页面不再有主题色时 ok 为 false |
OnRenderProcessGone(func(reason string)) | web 内容进程崩溃或被杀死;Reload 可恢复 |
OnAudibleChanged(func(audible bool)) | 页面开始或停止发出声音 |
OnCaptureStateChanged(func(kind mygo.CaptureKind, s mygo.CaptureState)) | 页面开始或停止采集摄像头、麦克风或屏幕,或采集被静音或取消静音 |
让窗口停留在应用自己的页面,其他链接在浏览器中打开:
go
win.Page().OnWillNavigate(func(e *mygo.NavigateEvent) {
if u, err := url.Parse(e.URL); err == nil && (u.Scheme == "http" || u.Scheme == "https") && e.UserInitiated {
e.PreventDefault()
go mygo.Shell.OpenExternal(e.URL)
}
})开发期间,应用的页面来自 dev server,是 http URL:当规则依赖 scheme 时, 检查主机名,或用 mygo.IsDev()。
打开新窗口的链接会经过窗口的 open 处理函数,见链接与新窗口。
轻扫与捏合
页面默认既不随轻扫后退,也不随捏合缩放——桌面应用都不如此:SwipeNavigation 和 PinchZoom 为窗口的页面和 web view 的页面打开它们。
go
mygo.NewWindow(mygo.WindowOptions{
URL: "https://example.com/",
Page: mygo.PageOptions{SwipeNavigation: true, PinchZoom: true},
})捏合放大页面显示的内容而无需重新布局,如同浏览器,并且不影响 ZoomFactor ——后者以另一个尺寸重新布局页面。
SwipeNavigation | PinchZoom | |
|---|---|---|
| macOS | 触控板或 Magic Mouse 在页面之间的轻扫,按系统设置的配置(allowsBackForwardNavigationGestures) | 触控板的捏合与智能缩放(allowsMagnification) |
| Linux | 触控板上两指划动,触摸屏上单指划动(enable-back-forward-navigation-gestures) | 触摸屏的捏合,以及触控板的捏合——GTK 只在 Wayland 上报告后者。WebKitGTK 的缩放没有可关闭的设置:MyGo 从 WebKitGTK 2.38 起关闭其缩放手势,更早版本总是缩放 |
| Windows | 触摸屏上的划动(IsSwipeNavigationEnabled) | 触摸屏的捏合(IsPinchZoomEnabled);没有 ICoreWebView2Settings5 的 WebView2 Runtime 总是缩放 |
WebKitGTK 和 WebView2 默认开启捏合,WebView2 还默认开启轻扫:除非页面要求, MyGo 会关闭它们。
主题色
ThemeColor 返回页面的主题色,即其 <meta name="theme-color">,应用可以 据此为其标题栏、工具栏或窗口背景着色以匹配页面,如同 Safari 所做; OnThemeColorChanged 在其变化时通知:
go
page := win.Page()
page.OnThemeColorChanged(func(c color.NRGBA, ok bool) {
if !ok {
c = defaultToolbarColor // 页面没有主题色
}
toolbar.SetBackground(ui.RGB(c.R, c.G, c.B))
})页面可以为浅色外观和深色外观各设一个颜色,通过 meta 的 media:
html
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#111111" media="(prefers-color-scheme: dark)">颜色取自页面 <head> 中第一个名为 theme-color、其 media 缺失或匹配、 且 content 是颜色的 meta 的 content,如同浏览器的选取方式。它由页面的引擎 计算,因此任何 CSS 颜色都可以(dodgerblue、rgb(30 144 255 / 50%)、 oklch(…)),结果以 8 位 sRGB、直通 alpha 给出。事件只在颜色改变时触发: 页面添加、移除或更改其 meta 时;应用外观(Theme) 改变使另一个 meta 的 media 匹配时;以及导航到不同颜色的页面时。刚开始加载 的页面会保持前一个页面的颜色直到其 DOM 就绪,因此在同色页面之间来回不会触发 事件。
MyGo 在页面中的运行时监视这些 meta,各平台方式相同,适用于任何来源的页面, 可信与否都是。运行时未注入的页面——例如 web view 显示的图像或 PDF——没有 主题色。
声音
IsAudible 表明页面是否正在发出声音——来自其任意 frame,包括另一个站点的 视频;OnAudibleChanged 在该状态改变时通知,可用于标签页或窗口上的喇叭 图标,或媒体应用的状态。SetAudioMuted 静音整个页面,如同浏览器静音标签页, 而页面不会看到其元素被静音;静音在页面导航后依然有效:
go
page := win.Page()
page.OnAudibleChanged(func(audible bool) {
log.Println("playing:", audible) // 在窗口的标签页上显示喇叭图标
})
// 一个菜单项,静音或取消静音所选中的窗口。
mute := &mygo.MenuItem{Label: "Mute or Unmute Window", Click: func(_ *mygo.MenuItem, win *mygo.Window) {
if win == nil || win.Page() == nil {
return // 没有窗口,或窗口显示的是原生 UI
}
win.Page().SetAudioMuted(!win.Page().IsAudioMuted())
}}紧跟 NewWindow 之后静音的页面一开始就是静音的,即使其 web view 尚未创建。 被静音的页面仍可能是可闻的——正如标签页会显示带斜线的喇叭;引擎决定暂停或 无声的媒体源何时不再计数,声音停止后可能需要片刻。
| 平台 | 发出声音 | 静音 |
|---|---|---|
| macOS | WebKit 私有的 _isPlayingAudio,以观察方式获取;若 WebKit 不再响应则为 false | WebKit 私有的 _setPageMuted:,如同 Safari 静音标签页;若 WebKit 不再响应则无操作 |
| Windows | WebView2 的 IsDocumentPlayingAudio(ICoreWebView2_8);更早的 Runtime 为 false | 同一接口的 IsMuted;更早的 Runtime 无操作 |
| Linux | webkit_web_view_is_playing_audio | webkit_web_view_set_is_muted,WebKitGTK 2.30 或更新;更早版本无操作 |
平台无法静音时,IsAudioMuted 为 false。对显示原生 UI 的窗口以及窗口关闭后, 两者都是 false。web view 的页面同样有这些方法。
后台页面
web view 会拖慢没人能看到的页面以省电:窗口一旦隐藏、最小化或位于其他窗口 之后,其定时器最多每秒触发一次,requestAnimationFrame 会等待,macOS 还可能 将其挂起。在后台播放、同步或计算的页面用 ThrottlingOff 保持运行,如同 Electron 的 backgroundThrottling: false:
go
mygo.NewWindow(mygo.WindowOptions{
URL: "/player",
Page: mygo.PageOptions{BackgroundThrottling: mygo.ThrottlingOff},
})其定时器保持原速率,其进程保持可见页面的优先级,窗口和 web view 都一样。在 macOS 和 Windows 上,被遮挡窗口的页面 也保持动画帧;但隐藏或最小化窗口的页面在 macOS 和 Linux 上仍会进入 hidden 状态(document.visibilityState),Windows 上隐藏窗口中的 web view 也一样 ——那时它什么都不绘制:必须持续运行的逻辑请放在定时器里。
| macOS | WKPreferences.inactiveSchedulingPolicy 设为 none(macOS 14+),关闭 WebKit 私有的隐藏页面定时器节流、CSS 动画挂起和进程抑制,并关闭窗口遮挡检测,使被遮挡的窗口被视为可见 |
| Linux | 关闭 WebKitGTK 的 HiddenPageDOMTimerThrottling 和 HiddenPageCSSAnimationSuspension 特性(WebKitGTK 2.42+;更早版本无对应机制) |
| Windows | 其浏览器以 --disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows 运行 |
在 Windows 上,这些参数属于 WebView2 环境,应用的所有页面共享它,而它由 第一个显示页面的窗口或 web view 创建:把选项给那一个。此后应用的任何页面都 不受定时器节流;较晚请求的页面为时已晚,日志会说明。应用的两个进程也共享 浏览器,因此必须一致:若第二个进程的第一个页面的要求与正在运行的环境不同, WebView2 会拒绝向其提供 web view。隐藏窗口原生 UI中的 web view 仍会进入 hidden 状态——其窗口离开屏幕时本应如此。
自动播放
WebKitGTK 和 WebView2 只允许页面在用户点击或输入之后开始有声媒体:在此之前, 未静音 <audio> 或 <video> 的 play() 会以 NotAllowedError 拒绝,而正在 播放的静音媒体在取消静音后会再次暂停。隐藏窗口的页面永远得不到这样的手势, 所以在其中完全无法播放声音。界面是原生 UI、并在隐藏窗口的页面 中播放媒体的应用,可以用 AutoplayAllow 让该页面播放:
go
mygo.NewWindow(mygo.WindowOptions{
URL: "/player",
Hidden: true,
Page: mygo.PageOptions{Autoplay: mygo.AutoplayAllow},
})此后 play() 会 resolve,autoplay 元素带着声音开始播放,窗口和 web view 都一样。默认的 AutoplayUserGesture 让页面遵循 其 web view 自身的策略。
| macOS | WKWebViewConfiguration.mediaTypesRequiringUserActionForPlayback 设为 none |
| Linux | 页面主框架导航的网站策略允许自动播放(WEBKIT_AUTOPLAY_ALLOW,WebKitGTK 2.30+;更早版本无对应机制) |
| Windows | 其浏览器以 --autoplay-policy=no-user-gesture-required 运行 |
在 Windows 上,和 ThrottlingOff 一样,该参数属于 WebView2 环境,应用的所有页面共享它,而它由第一个显示页面的窗口或 web view 创建:把选项给那一个。此后应用的每个页面都可以无需手势播放,保留了 AutoplayUserGesture 的页面也不例外;较晚请求的页面为时已晚,日志会说明。 在第一个窗口是原生 UI 的应用中,播放页面的窗口往往就是第一个显示页面的, 它的选项就够了。第一个窗口显示页面的应用,要给那个窗口也加上该选项,或者 先创建播放的那个页面。应用的两个进程必须达成一致,如同 ThrottlingOff 的 情形。
摄像头与麦克风
CaptureState 表明页面是否在采集摄像头、麦克风或屏幕(getUserMedia()、 getDisplayMedia())——来自其任意 frame——以及采集是否被静音:CaptureNone、 CaptureActive 或 CaptureMuted。OnCaptureStateChanged 在该状态改变时 通知,可用于窗口或菜单栏中的录制指示,如同 Safari 所示。SetCaptureState 无需页面配合即可静音、取消静音或停止采集,就像通话应用的静音按钮:静音的 采集保留页面的轨道,轨道收到静音或黑帧;停止的采集则结束轨道。
go
page := win.Page()
page.OnCaptureStateChanged(func(kind mygo.CaptureKind, s mygo.CaptureState) {
if kind == mygo.CaptureMicrophone {
log.Println("microphone live:", s == mygo.CaptureActive) // 显示录制指示
}
})
// 一个菜单项,静音或取消静音所选窗口的麦克风。
mute := &mygo.MenuItem{Label: "Mute or Unmute Microphone", Accelerator: "CmdOrCtrl+Shift+M", Click: func(_ *mygo.MenuItem, win *mygo.Window) {
if win == nil || win.Page() == nil {
return
}
p := win.Page()
switch p.CaptureState(mygo.CaptureMicrophone) {
case mygo.CaptureActive:
p.SetCaptureState(mygo.CaptureMicrophone, mygo.CaptureMuted)
case mygo.CaptureMuted:
p.SetCaptureState(mygo.CaptureMicrophone, mygo.CaptureActive)
}
}}页面先请求设备,由权限决定。页面接受变更后,CaptureState 和事件才会反映出来——在 SetCaptureState 之后片刻;页面未采集相应设备时, SetCaptureState 无操作。页面无法自行取消采集的静音:再次请求设备会进入 权限处理函数,如同新的采集。
| 平台 | 采集状态 | 静音与停止 |
|---|---|---|
| macOS | WKWebView 的 cameraCaptureState 和 microphoneCaptureState(macOS 12+),以及 WebKit 私有的 _displayCaptureState(macOS 13+),以观察方式获取;若 WebKit 不响应则为 CaptureNone | setCameraCaptureState:completionHandler:、setMicrophoneCaptureState:completionHandler: 以及私有的 _setDisplayCaptureState:completionHandler:;当摄像头与麦克风只有其一被静音时,静音或取消静音页面的声音(SetAudioMuted)会短暂静音另一个,因为 WebKit 将二者一起静音 |
| Linux | webkit_web_view_get_camera_capture_state 及其麦克风与屏幕的同类函数,WebKitGTK 2.34 或更新,以及它们的 notify 信号 | 它们的 setter;静音或取消静音页面的声音(SetAudioMuted)会在 WebKitGTK 中取消所有采集的静音,MyGo 会立即重新静音,因此被静音的采集可能短暂处于活动状态 |
| Windows | 永远是 CaptureNone:WebView2 不报告采集 | 无操作 |
对显示原生 UI 的窗口以及窗口关闭后,CaptureState 是 CaptureNone。 web view 的页面同样有这些方法。
画中画
PageOptions.PictureInPicture 让页面的视频浮出到画中画——一个悬浮在每个 应用之上的小窗口,正如它们在 Safari 中的行为:视频控件上的按钮、其上下文 菜单中的项,以及页面脚本里的 requestPictureInPicture()。WKWebView 把画中画 留给 Safari,所以 macOS 上的页面需要这个选项;WebView2 无论设置如何都允许 视频浮出。TogglePictureInPicture 让视频浮出,或让已浮出的视频回到页面, 无需在页面中点击;IsPictureInPictureActive 表明是否有视频浮出,无论由谁 触发:
go
win := mygo.NewWindow(mygo.WindowOptions{
URL: "/player",
Page: mygo.PageOptions{PictureInPicture: true},
})
// 一个菜单项,让所选窗口的视频浮出画中画。
pip := &mygo.MenuItem{Label: "Picture in Picture", Accelerator: "CmdOrCtrl+Shift+P", Click: func(_ *mygo.MenuItem, win *mygo.Window) {
if win != nil && win.Page() != nil {
win.Page().TogglePictureInPicture()
}
}}| 平台 | |
|---|---|
| macOS | WebKit 私有的 _allowsPictureInPictureMediaPlayback 首选项,以及用于 WebKit 为其媒体控件所选视频的 _togglePictureInPicture、_canTogglePictureInPicture 和 _isPictureInPictureActive;若 WebKit 不再响应,它们都无操作或返回 false。带着浮出视频导航的页面会被排除在前进/后退缓存之外(私有的 _setUsesPageCache:),否则 WebKit 恢复它时会误认为其视频仍浮出 |
| Windows | Chromium 的机制,在每个页面默认开启。TogglePictureInPicture 调用正在播放视频的 requestPictureInPicture(),否则调用最大视频的,在页面的 document 中(不含其 frame),以用户手势的身份;IsPictureInPictureActive 向页面询问 document.pictureInPictureElement,frame 中的视频会让它为 null |
| Linux | 无:WebKitGTK 没有画中画,因此 TogglePictureInPicture 无操作,IsPictureInPictureActive 为 false |
对显示原生 UI 的窗口以及窗口关闭后,两者都无操作或为 false。 web view 的页面同样有它们。
页内查找
FindInPage 高亮一段文本的匹配项并报告数量,如同浏览器的查找栏;在用户 输入时调用它,并用 FindNext 跳到下一个匹配:
go
res, err := win.Page().FindInPage("mygo", mygo.FindOptions{FindNext: true})
fmt.Printf("%d of %d\n", res.Active, res.Matches)
win.Page().StopFindInPage()打印、PDF 与截图
go
win.Page().Print() // 打印对话框
pdf, err := win.Page().PrintToPDF(mygo.PDFOptions{
PageSize: mygo.PageA4,
Landscape: false,
Margins: &mygo.Margins{Top: 0.5, Right: 0.5, Bottom: 0.5, Left: 0.5}, // 英寸
Background: true, // 打印背景色和图像
})
png, err := win.CapturePage() // 窗口显示的内容,输出为 PNGPrintToPDF 为打印布局页面,因此其打印样式表生效。零值的 PDFOptions 打印纵向 Letter 纸、0.4 英寸边距、不含背景,如同浏览器的打印对话框。
PDF 的文本可以像页面的文本一样复制和搜索。在 macOS 上,WebKit 打印出的 PDF 对许多汉字会命名为部首,例如用 ⼈(U+2F08)表示 人——系统字体用同一个 字形绘制它们。PrintToPDF 询问已安装的字体其字形对应哪些字符,并在 PDF 中 附加修正后的文本映射,页面外观保持不变。未安装字体(如 web 字体)中的文本 仍保留部首。
下载
页面跟随带 download 属性的链接,或收到无法显示的响应(如附件)时,会 下载文件。默认情况下,文件进入下载目录,采用页面或服务器建议的名称。 OnWillDownload 可以选择其他路径或取消下载,OnDownloadDone 报告其结果:
go
win.Page().OnWillDownload(func(e *mygo.DownloadEvent) {
if strings.HasSuffix(e.SuggestedName, ".exe") {
e.PreventDefault()
return
}
dir, _ := mygo.App.Path(mygo.PathDocuments)
e.Path = filepath.Join(dir, e.SuggestedName)
})
win.Page().OnDownloadDone(func(d *mygo.Download) {
if d.Err == nil {
mygo.Shell.ShowItemInFolder(d.Path)
}
})权限
页面会请求摄像头、麦克风、位置或通知。默认情况下,应用自己的页面——包括 自定义 scheme 的页面(见信任)——得到所请求的权限, 其他页面什么都得不到。Page.SetPermissionHandler 改为自行决定;它在主线程 运行:
go
win.Page().SetPermissionHandler(func(req mygo.PermissionRequest) bool {
return req.Origin == "https://meet.example.com" &&
!slices.Contains(req.Permissions, mygo.PermissionGeolocation)
})操作系统也可能询问用户。macOS 会——对摄像头和麦克风每个应用询问一次—— 并且会终止使用它们而未在 Info.plist 中说明原因的应用:在 mygo.config.ts 的 macos.infoPlist 中添加用途说明:
ts
export default defineConfig({
macos: {
infoPlist: {
NSCameraUsageDescription: "Scan documents with the camera.",
NSMicrophoneUsageDescription: "Record voice notes.",
},
},
});登录与证书
位于 HTTP 认证(Basic、Digest、NTLM)之后的页面——来自其服务器或代理—— 会要求用户名和密码。Page.SetAuthHandler 在主线程应答,给出凭据,或返回 空的 AuthResponse 以取消,从而显示服务器的拒绝:
go
win.Page().SetAuthHandler(func(req mygo.AuthRequest) mygo.AuthResponse {
if req.Host == "router.lan" && !req.IsProxy {
return mygo.AuthResponse{Username: "admin", Password: password}
}
return mygo.AuthResponse{}
})web view 会在会话余下时间里保留服务器已接受的凭据。没有处理函数时, WebKitGTK 和 WebView2 显示自己的登录对话框,macOS 则不显示。要想以应用自己 的方式询问用户,先取消、提示,待处理函数拿到凭据后再 Reload。
证书无法验证的服务器——例如使用自签名证书的 dev server——会加载失败,如同 在浏览器中。Page.SetCertificateErrorHandler 可以仍然加载它:只允许你预期 的证书——CertificateError.Certificate 持有它(DER 编码)——绝不要不论证书 地放行某个主机:
go
win.Page().SetCertificateErrorHandler(func(e mygo.CertificateError) bool {
sum := sha256.Sum256(e.Certificate)
return e.Host == "dev.example" && sum == devCertificateSHA256
})在 Linux 上,只询问页面本身,不询问它加载的资源;在 Linux 和 Windows 上, 对某个主机放行的证书会在应用退出前对所有窗口保持放行。客户端证书交由系统 处理。
拦截内容
内容规则拦截页面加载的内容并隐藏其部分,如同广告和跟踪器拦截器。它们以 WebKit 的内容拦截器格式 编写:一个规则的 JSON 数组,每条规则是一个 trigger——用 URL 正则匹配加载 ——和一个 action。mygo.CompileContentRules 编译它们,页面通过 PageOptions.ContentRules 或 Page.SetContentRules 应用:
go
rules, err := mygo.CompileContentRules("ads", `[
{"trigger": {"url-filter": "^[^:]+://+([^:/]+\\.)?ads\\.example\\.com[:/]"}, "action": {"type": "block"}},
{"trigger": {"url-filter": ".*", "if-domain": ["*news.example"]},
"action": {"type": "css-display-none", "selector": ".sponsored"}}]`)
if err != nil {
return err
}
win := mygo.NewWindow(mygo.WindowOptions{URL: "https://news.example/",
Page: mygo.PageOptions{ContentRules: []*mygo.ContentRules{rules}}})trigger 接受 url-filter、url-filter-is-case-sensitive、if-domain、 unless-domain、if-top-url 和 unless-top-url 之一、resource-type 和 load-type(first-party 或 third-party)。action 有 block、 block-cookies、make-https、带 selector 的 css-display-none,以及 ignore-previous-rules——它撤销先前规则对某次加载的作用。正则是 WebKit 的: 不支持选择(|)、计数重复和 \d 之类的字符类,仅限 ASCII。域名用小写、 punycode。选择器无法被引擎解析的规则会被跳过;任何其他错误都会使整个列表 失败。
CompileContentRules 对长列表耗时较长,所以不要在主线程调用。引擎按 id 保留 编译结果:同样的规则在后续启动时立即加载,同一 id 下编译的其他规则会取代 先前的。WebKit 限制单个列表的规则数(150,000 条,旧版本 50,000 条),因此 长列表要拆成几个并一起应用;页面按顺序应用其列表,每个列表的 ignore-previous-rules 只撤销自己列表的规则。SetContentRules 替换页面的 列表,传入空则移除它们:此后的加载不再受其影响,页面在重新加载之前仍显示 此前加载的内容。
macOS 和 Linux(WebKitGTK 2.24 或更新)用 WebKit 的内容拦截器应用规则。 WebView2 没有,所以在 Windows 上,MyGo 会观察应用了规则的页面的每个请求, 并让被拦截的请求失败:被拦截的 fetch 会 reject,XMLHttpRequest 会报错, 与 WebKit 一致;而被拦截的页面会显示 WebView2 针对 403 响应的错误页。 make-https 匹配的内容会被重定向;元素由每个文档采纳的样式表隐藏。在 Windows 上:
- WebSocket 不会被拦截:WebView2 不向 MyGo 展示其请求。
- 弹出窗口(
resource-type为popup)不匹配。 block-cookies移除请求的 cookie,不移除响应的 cookie。- 自身
style写了display: ... !important的元素会保留:样式表属于页面, 而 WebKit 的属于用户,用户的优先。
Web 检查器
页面的 OpenDevTools、CloseDevTools、ToggleDevTools 和 IsDevToolsOpened 控制其检查器(如果它有),见 Web 检查器。
页面的进程
ProcessID 返回运行页面的操作系统进程 ID,供应用自己的任务管理器、内存 报告,或判断繁忙进程属于哪个窗口使用;未知时为 0:
go
pid := win.Page().ProcessID()| 平台 | 进程 |
|---|---|
| macOS | 页面的 web 内容进程(com.apple.WebKit.WebContent),取自 WebKit 私有的 _webProcessIdentifier;若 WebKit 不再响应则为 0。页面导航到另一个站点或其进程崩溃后可能换到另一个进程,所以每次要用时重新询问,而不是保留 ID |
| Windows | WebView2 的浏览器进程(msedgewebview2.exe),应用的 web view 共享它,其子进程渲染页面 |
| Linux | 无:WebKitGTK 没有获取页面进程的 API,因此永远是 0 |
web view 尚未启动进程时、窗口关闭后,以及显示原生 UI 的窗口,同样是 0。
查找窗口
mygo.Windows()列出打开的窗口,mygo.FocusedWindow()返回获得焦点的 窗口,mygo.WindowByID(id)返回具有某个ID()的窗口——页面以runtime().windowId知道它。mygo.CallerWindow(ctx)返回其页面调用了某个绑定方法的窗口,mygo.CallerPage(ctx)返回页面:窗口的页面,或原生 UI 窗口中 web view 的 页面(Web view)。给自定义 scheme 请求的r.Context(), 它们返回发起请求的窗口和页面。mygo.App.OnWindowCreated对每个新窗口都会调用。
原生访问
win.NativeHandle() 返回原生窗口——macOS 上是 NSWindow*,Linux 上是 GtkWindow*,Windows 上是 HWND——供 MyGo 未覆盖的 API 使用。用 mygo.RunOnMain 在主线程调用原生 API。