Skip to content

迁移到受检元素 ​

原生 UI 视图接收 *ui.Context,子元素构建器使用 func() 回调,控件 构造函数返回 ui.Element 值。本指南涵盖源码迁移、构建周期、控件的 持久标识和焦点绑定。

应用源码迁移 ​

在你的应用目录中运行迁移:

sh
go tool mygo migrate-ui .          # 预览受影响文件
go tool mygo migrate-ui -write .   # 写入源码迁移
go tool mygo vet .
go test ./...

提交前先审阅 diff。codemod 基于 Go 语法和实际的 UI 导入别名工作, 在同一包的多个文件间共享字段和辅助函数的返回声明。它会转换元素和 自定义部件的指针类型、元素的 nil 检查与零值赋值,把流式构造函数的键 移入 Context.Key,并把网格的 Rows(n) 重命名为 GridRows(n)。 Context 指针、子回调签名、普通模型指针和无关系的 nil 检查都会保留。 点导入(dot import)以及单独存储的元素上的键需要人工复查。它不会把 任意的轮询控制流移入回调。Changed 和 Submitted 轮询可以在同一 构建 pass 中提交本地绑定的值,如下文所述。

元素值与键 ​

go
var element ui.Element
if element.Valid() {
    element.Focus()
}

用 ui.Element{} 代替赋值或返回 nil。自定义部件(如 ui.SelectParts[T])也是包含受检元素的值。

元素在下一次构建 pass 之前失效,包括同一帧内的重建。代际(generation) 校验会拒绝它,即使其 arena 槽位已被复用。Tester 和开发构建会 panic, 消息中会指出相关的 pass 并建议使用 ui.Handle;带 mygo_noinspector 的生产构建返回空查询结果或忽略过期的修改。缺失(不存在)的零值在两种 模式下都保持安全。Valid 不会 panic。

在状态初始化之前指定键:

go
ui.TextInput(c.Key("search"), &a.query).ReadOnly(a.readOnly)
ui.Checkbox(c.Key(todo.ID), &todo.Done, todo.Title)
parts := ui.SelectBase(c.Key("choice"), &a.choice)

Element.Key 在容器的子元素或本地状态构建之前为其指定键。有状态的 控件(包括自定义基座控件)使用 Context.Key。键标识下一个外层控件, 并在其初始化状态之前被消费。

输入与动作 ​

控件在其构造函数返回之前,通过它绑定的指针写入用户的输入,因此绑定值 (以及本地副本)可以立即读取;Changed 和 Submitted 会报告这一点。 输入遵循用户操作所在帧的选项:在某次构建中设置的 Disabled、 ReadOnly 和滑块设置,支配其后各帧的输入。

go
ui.Button(c, "Save").Disabled(a.saving).OnClick(a.save)
ui.TextInput(c.Key("query"), &a.query).OnChange(a.search)
ui.TextInput(c.Key("draft"), &a.draft).OnSubmit(a.send)
c.OnShortcut(ui.Cmd, ui.KeyS, a.save)

点击、快捷键、变更和提交动作在配置与绑定输入之后、重建之前运行。 已处理的输入会在下一个 pass 之前被消费,因此动作不会重复执行。轮询 方法仍然可用;当动作改变正在构建的集合时,优先使用回调。I/O 留在 工作线程中,并用 Window.Update 发布模型结果。

尽可能把控件直接绑定到持久的模型字段上。每次构建都重新计算的本地值, 可以在构建时立即通过 Changed 或 Submitted 提交,或在构造之后用 回调提交:

go
viewed := a.isViewed(file)
if ui.Checkbox(c, &viewed, "Viewed").Changed() {
    a.setViewed(file, viewed)
}

对于选中值由多个模型字段派生而来的分段控件,同样的规则适用。运行 codemod 后请人工复查这些绑定;它无法推断如何把派生值写回你的模型。

持久标识 ​

存储 ui.Handle 而不是元素:

go
type app struct {
    query string
    search ui.Handle
}

func (a *app) view(c *ui.Context) {
    ui.TextInput(c.Key("search"), &a.query).Bind(&a.search)
}

// 在 UI 线程上,包括在 Window.Update 内部:
a.search.Focus()

控件隐藏时,焦点请求会合并并等待。关闭其窗口会取消该窗口的请求。 CancelFocus 取消更早的请求。

handle.Focused(c) 和 handle.FocusWithin(c) 读取持久标识,包括在 控件构造之前。c.Resolve(handle) 只返回当前 pass 的元素。 handle.Bounds(c) 在 UI 线程上读取已提交的控件盒子,包括从输入回调 中读取。这些是不同的查询:在移除控件的那次构建中仍能观察到先前的焦点; 提交那次构建才会移除其实际焦点。

一个 handle 支持在多个窗口中独立绑定。查询接收窗口的 Context。用 handle.Focus(c) 和 CancelFocus(c) 选择窗口;无参数的 Focus 形式 要求至多一个打开的绑定。在第一次绑定之前,它会等待那个第一个控件。 让每个窗口拥有自己的控件状态和焦点字段。Ref/RequestFocus 是 Handle/Focus 的别名。

ListState、ScrollState、GridState 和 Router 自带一个 Handle; 其他控件可以绑定任何应用拥有的 handle。用 state.Handle 实现持久焦点 和命令。

绑定到应用数据的焦点 ​

go
type pane int
const (none pane = iota; files; diff)

ui.List(c, &a.list, len(a.files)).FocusBind(&a.pane, files).
    Rows(func(row ui.ListRow) { ui.Text(row.Context, a.files[row.Index].Name) })

// 某个动作请求 diff 面板,在其隐藏时等待:
a.pane = diff

// 该请求等待期间,实际焦点可能不同:
focused := ui.FocusedValue(c, &a.pane)

FocusBind 要求一个可比较字段的指针和匹配的值。在一个窗口内,该字段 中的值是唯一的;其零值应保留表示无焦点。字段保存期望的焦点。当没有 请求在等待时,用户的焦点变化会更新它。设为零值会清除焦点。 FocusedValue 使用已提交的标识读取实际焦点,与构造顺序无关。隐藏的 绑定保留其期望请求,而实际焦点可能在另一个控件上或不存在。不同窗口 使用不同的字段。

列表与键盘命令 ​

go
a.filesView.OnShortcut(c, ui.Cmd, ui.KeyK, a.openSelected)
ui.List(c.Key("files"), &a.list, len(a.files)).Bind(&a.filesView).Grow(1).
    ItemKey(func(i int) any { return a.files[i].ID }).
    Selection(&a.selection).
    Rows(func(row ui.ListRow) {
        text := ui.Text(row.Context, a.files[row.Index].Name)
        if row.Selected() && row.ListFocused() {
            text.TextColor(c.Theme().Accent)
        }
    })

Handle 的快捷键可以在 Bind 之前声明。它只在构造之后、且该窗口为该 handle 构建了启用状态的控件时运行,因此隐藏的控件不接收命令。 ListRow 提供共享的 Context、索引、选中和列表焦点。 List(c, state, n, func(i int)) 也可以直接接受行回调。在调用 Rows 之前配置流式列表。

持久服务与验证 ​

捕获 c.Services() 用于剪贴板/URL 回调和后台重绘。Services 对窗口是 弱引用,且不含构建数据。剪贴板和 URL 方法在 UI 线程上运行;从其他 goroutine 调用 Invalidate 是安全的。发布模型变化时使用 Window.Update。

mygo vet 运行普通的 Go vet,并进行类型感知检查,针对存储在结构体 字段/包变量中、或被 goroutine 捕获/传递的元素、部件和 Context 指针。 它会跟踪导入、类型别名和推导变量;Handle、Services 和视图回调签名是 允许的。这些检查有助于强制构建周期;它们不能证明所有周期或 goroutine 安全性。运行时的代际检查仍然必要。

测试控件消失后又出现、等待中的焦点、列表重排、文本编辑/输入法、禁用 和只读设置,以及命令路由。运行常规测试并检查应用。实测开销和复现命令 参见性能。