Skip to content

视图 ​

视图是从应用状态到其界面的函数。MyGo 在主线程上调用它,在输入、模型变化之后以及有动画进行时构建帧。一帧可以包含多次构建过程,因此在绘制之前就能显示某个动作的结果。

窗口有一个稳定的 *ui.Context。子构建器共享它,并在回调期间切换其当前父元素。把它传给构建视图一部分的辅助函数。

每个 ui.Element 值只属于一次构建过程。访问控件之前会检查它的拥有者、槽位和世代。可选元素用 Valid 检查,ui.Element{} 表示不存在。过期的使用在开发模式和 Tester 中会被诊断出来;生产环境中相应的方法返回空结果或忽略该操作。

不要把构建期的值存进应用状态或后台任务。持久化的控件标识应存储 ui.Handle,构建之间的回调应使用 ui.Services。模型数据以及 ListState 这样的控件状态属于应用本身。

go
// todoList 是应用的状态,长期存在。
type todoList struct {
	todos []Todo
	draft string
}

// view 根据它构建界面。
func (app *todoList) view(c *ui.Context) {
	ui.Column(c).Fill().Padding(16).Gap(8).Children(func() {
		for i := range app.todos {
			ui.Checkbox(c, &app.todos[i].Done, app.todos[i].Title)
		}
		if ui.TextInput(c, &app.draft).Placeholder("New to-do").Submitted() {
			app.todos = append(app.todos, Todo{Title: app.draft})
			app.draft = ""
		}
	})
}

在应用启动时创建一次状态,并把它的视图交给窗口:

go
app := &todoList{}
mygo.NewWindow(mygo.WindowOptions{Title: "To-dos", Content: ui.View(app.view)})

ui.View(app.view) 是窗口的内容(mygo.WindowOptions.Content);一个 Content 可以服务于多个窗口,每个窗口各有自己的元素状态。

这些指南中的示例都是这种视图的一部分:c 是视图的 *ui.Context,app 是它的接收者,即保存状态的你自己类型的值,这里的 todoList 就是这样。像 app.volume 这样的字段或 app.save() 这样的方法,都是你在该类型上声明的。长期存在的值,例如路由,应当作为字段与状态的其余部分一起创建,绝不能在视图中创建,因为视图每帧都会运行。

事件与动作 ​

Clicked 报告自上一帧以来是否被点击,让控制流就保持在控件旁边:

go
if ui.Button(c, "Save").Clicked() {
    app.save()
}

控件在其构造函数返回时接收用户的输入,因此内联的保存操作能看到先于它构建的控件的最新修改。

OnClick 把动作安排在视图构建完成、且绑定的输入已应用之后执行:

go
ui.Button(c, "Save").Disabled(app.saving).OnClick(app.save)

当内联的 return、break 或 continue 有用时使用轮询;对于修改正在构建的集合的动作,使用回调,以便循环在模型变化之前完成:

go
for _, item := range app.items {
    ui.Row(c.Key(item.ID)).Children(func() {
        ui.Text(c, item.Title).Grow(1)
        ui.Button(c, "Delete").OnClick(func() {
            app.removeItem(item.ID)
        })
    })
}

已处理的输入会在重建之前被消费,因此一个动作针对该输入只运行一次。MyGo 会在绘制帧之前重建发生变化的状态。

会修改值的控件接收指向该值的指针,因此不需要处理器:ui.Checkbox(c, &app.settings.Sync, "Sync") 把字段绑定到控件。与 Dear ImGui 和 egui 一样,控件在其构造函数返回前通过指针写入用户的修改,因此下一条语句读到的就是新值。Changed 报告值在本次构建过程中发生了变化,Submitted 报告在单行文本输入中按下了 Enter。

go
if ui.TextInput(c, &app.query).Placeholder("Search").Changed() {
	app.results = search(app.query)
}

本地副本同样可行,包括从 map 中读出的值,既可与其来源值比较,也可通过 Changed 判断:

go
viewed := app.viewed[file.ID]
if ui.Checkbox(c, &viewed, "Viewed").Changed() {
    app.viewed[file.ID] = viewed
}

chosen := app.language
ui.RadioGroup(c, func() {
    for _, tag := range app.languages {
        ui.Radio(c, &chosen, tag, i18n.Autonym(tag))
    }
})
if chosen != app.language {
    app.setLanguage(chosen)
}

容器与其子元素一起接收输入,就在构建它的调用返回时:单选组的方向键会在组内所有单选按钮之间移动。输入遵循用户操作时那一帧的选项:显示为只读的输入不接收键入,而本次构建中设置的 Disabled、ReadOnly 或 Step 会约束其后各帧的输入,与其显示效果一致。

OnChange 和 OnSubmit 在构造和绑定输入之后、重建之前运行。当动作应当等到视图构建完成后才执行时(例如修改正在渲染的集合),使用回调。它们也可以提交本地副本:

go
viewed := app.viewed[file.ID]
ui.Checkbox(c, &viewed, "Viewed").OnChange(func() {
    app.viewed[file.ID] = viewed
})

键(Key) ​

在构造之前把键交给控件,这样当周围的集合变化时,控件的状态会跟随对应的条目:

go
ui.Checkbox(c.Key(todo.ID), &todo.Done, todo.Title)
ui.TextInput(c.Key("search"), &app.query)

键在兄弟元素之间必须唯一,其比较方式与 Go 比较 map 键相同:指针以其指向的对象为键,无论对象内容如何,因此条目的指针和 ID 一样可以充当键。Go 无法比较的键(如切片)或不等于自身的键(如 NaN),按其打印出的值来比较。Element.Key 可以在容器的子元素或局部状态构建之前为容器设定键。有状态的控件和自定义基础控件使用 Context.Key。

从其他 goroutine 修改状态 ​

视图在主线程上读取你的状态。要从其他 goroutine 修改状态,使用 Window.Update,它在主线程上运行一个函数,然后绘制新的一帧:

go
win := mygo.NewWindow(mygo.WindowOptions{Content: ui.View(app.view)})
go func() {
	items, err := fetchItems()
	win.Update(func() { app.items, app.err = items, err })
}()

Window.Invalidate 只绘制新帧,适用于你自己加锁保护的状态。在视图内,c.Invalidate() 请求再来一帧,c.After(d) 请求在一段延迟后来一帧,例如时钟的下一秒。

元素的状态 ​

元素可以用 ui.Local 在帧与帧之间保存自己的状态,适用于你自己构建的控件;参见自定义控件。

检查构建生命周期 ​

在应用目录下运行 mygo vet,找出被存入结构体、包级变量或 goroutine 的构建期值。源码迁移命令参见迁移,实测的帧开销参见性能。