Skip to content

列表 ​

ui.List 显示任意长度集合的行:它只构建可见的行以及之外的少数几行,因此百万行的列表和十行的列表一样快。行的高度取决于其内容,每行可能不同:列表在行显示时测量它们,并据此估算其他行。

go
ui.List(c, nil, len(app.files), func(i int) {
	ui.Text(c, app.files[i].Name).Padding(6, 12)
}).Grow(1)

给列表一个大小,或让它在父元素中生长。没有大小、或只有 MaxHeight 的列表,高度与其行等高。

布局 ​

Gap 设置行间距,Padding 为内容加内边距并随内容滚动,Justify(ui.End) 把填不满列表的行放在底部,比如聊天最早的消息。在列表里构建的其他内容只在没有行时显示,比如提示它为空的消息:

go
ui.List(c, nil, len(results), func(i int) {
	ui.Text(c, results[i].Title).Padding(6, 12)
}).Grow(1).Children(func() {
	if len(results) == 0 {
		ui.Text(c, "No results").TextColor(c.Theme().TextMuted).Padding(12)
	}
})

比列表更宽的行 ​

RowWidth(w) 让行至少 w DIP 宽,正如表格的列所要求的:当行比列表宽时,列表也横向滚动;当不比列表宽时,行会伸展到列表宽度。行上方的表头通过 ScrollWith 与行同行滚动;要裁剪表头,使滚出其外的内容不显示。ui.Table 就是这样处理其列的。

go
head := ui.Row(c).Clip().Children(app.titles)
list := ui.List(c, &app.files, len(app.files)).Grow(1).RowWidth(app.columnsWidth())
list.Rows(app.row)
head.ScrollWith(list)

列表状态 ​

列表按行而不是按偏移来保持位置,因此当周围的行被测量、增高、出现和消失时,可见的行仍留在原处;它的偏移能精确到 DIP 的几分之一地计量百万行。在你的状态里放一个 ui.ListState(代替 nil),用它来规定列表的行为并移动它:

  • Key 返回某行对应项的身份标识,如其 ID:行的状态(焦点、正在编辑的文本……)跟随其项;当上方新增或删除行(比如加载更早的消息)时,列表保持其位置和选中项。
  • KeepOffset 让带 Key 的列表在行移动时保持其偏移,而不是跟随顶部的项,适用于随变化重新排序的行,如进程监视器:行的状态和选中项仍跟随其项。
  • FollowEnd 让列表从末尾开始,并在行新增或增高时保持末尾可见,直到用户滚动离开,聊天或日志就是这样;滚回末尾后重新跟随。FollowAnimation 让它平滑滑动到末尾(像弹簧 &ui.Smooth 或一条曲线),而不是一下子跳到末尾:新增的内容从下方升起,如同聊天应用的滚动,用户滚动会中止它。
  • Header 指定哪些行是分节的标题行:位于顶部的节的标题固定在那里,其行从它下面滚过,直到下一个标题把它顶开。请给标题行加背景。
  • Label 返回某行的文本:列表有焦点时键入其开头字母即可选中它,如同在 Finder 或资源管理器中;辅助技术把它读作行的名称。

每个列表都用它自己的 ListState,并在构建列表之前设置好其字段。

选择行 ​

  • Selected 允许用点击,或在列表拥有键盘焦点时用 Up、Down、Home 和 End(Linux 和 Windows 上还有 Page Up 和 Page Down)选择一行,选中行以强调色显示;列表的 Changed 报告新的选择,Submitted 报告双击或 Enter。
  • Selection 允许用户选择多行,保存在 ui.Selection[K] 里,以各项的 key(Key)为键,没有 Key 时以索引为键:点击选中一行,Cmd+点击(Linux 和 Windows 上为 Ctrl+点击)添加或移除一行,Shift+点击或配合按键的 Shift 选中从最后选中行到目标行的所有行;Cmd+A 全选。在 Linux 和 Windows 上,配合按键的 Ctrl 只移动不选择,Ctrl+Space 添加或移除当前行。同时设置的 Selected 是最后选中的行。

按路径多选文件的例子:

go
type fileList struct {
	files  []File
	chosen ui.Selection[string]
	list   ui.ListState
}

func (app *fileList) view(c *ui.Context) {
	app.list.Key = func(i int) any { return app.files[i].Path }
	app.list.Label = func(i int) string { return app.files[i].Name }
	app.list.Selection = &app.chosen
	ui.List(c, &app.list, len(app.files), func(i int) {
		ui.Text(c, app.files[i].Name).Padding(6, 12)
	}).Grow(1)
	if ui.Button(c, fmt.Sprintf("Delete %d", app.chosen.Len())).Clicked() {
		for path := range app.chosen.All() {
			app.delete(path)
		}
		app.chosen.Clear()
	}
}

已从列表消失的项的 key 会留在选择集中,直到你自己移除它们。Element.ClickModifiers 给出任意一次点击的修饰键,供你自己实现同样行为的控件使用。

在带 Selection 的列表里,右键点击选择之外的行会先选中它再打开上下文菜单,如同 Finder 和 Qt 的做法,使菜单作用于打开它的那一行;右键点击已选中的行时,选择保持不变。MenuKeepsSelection 关闭这一行为:菜单在被点击的行上打开(构建菜单的回调知道是哪一行),而选择保持不变。只有 Selected 的列表从不在右键时改变选择。

go
app.files.Selection = &app.chosen
app.files.MenuKeepsSelection = true // 菜单只作用于被点击的行

DragSelect(配合 Selection)让指针从按下处所在行开始,拖过哪些行就选择哪些行,如同对指针下的行执行 Shift+点击(若按下时还按着 Cmd,Linux 和 Windows 上为 Ctrl,则在已选行之外追加),指针接近或越过顶部或底部时列表随之滚动。在未选中的行上按下时,指针按下即选中(像电子表格那样),而不是等抬起;在已选中的行上按下则等待抬起,这样拖动选择集时能保留它。DragSelecting 在指针按住拖动期间报告正在进行的拖选,让依赖选择结果的工作(如统计所选行之和)等到拖选结束再做。列表不能同时 DragSelect 和 Reorder。

ExtendTo(row) 选中从 Shift 扩展起点到 row 的行,如同对 row 执行 Shift+点击;可用于你自己的按键,比如把选择扩展到最后一行的按键:

go
if app.extendToEnd {
	app.list.ExtendTo(len(app.files) - 1) // 在列表构建之后调用
}

行里的内容可以判断双击落在它的哪个部分,而又不从行手里拿走这次点击(行照常进行选择):RowDoubleClicked 在指针位于该元素上时报告所在行的双击。若在元素上用 DoubleClicked,它会拿走点击,行就不再选择。

go
ui.List(c, &app.list, len(app.files), func(i int) {
	name := ui.Text(c, app.files[i].Name).Padding(6, 12)
	if name.RowDoubleClicked() {
		app.rename(i)
	}
})

滚动 ​

ScrollTo(row, align) 把某行显示在列表的 ui.Start、ui.Center 或 ui.End 处,ScrollIntoView(row) 以最少的滚动让它进入视野,ScrollToEnd 滚动到末尾,包括尚未构建的行:列表在布局这一帧时滚动,滚到该行测量后所在的位置。Visible 返回可见的首行和末行,AtEnd 报告末尾是否可见,以便列表接近已加载内容的末尾时加载更多。

一个聊天程序的例子:

go
app.chat.Key = func(i int) any { return app.messages[i].ID }
app.chat.FollowEnd = true
app.chat.FollowAnimation = &ui.Smooth
if first, _ := app.chat.Visible(); first < 5 && app.more {
	app.loadOlder() // 加载到可见消息之上,可见消息保持不动
}
if !app.chat.AtEnd() && ui.Button(c, "Jump to latest").Clicked() {
	app.chat.ScrollToEnd()
}
ui.List(c, &app.chat, len(app.messages), func(i int) {
	message(c, app.messages[i])
}).Grow(1).Justify(ui.End)

重新排序 ​

Reorder 允许用户把行拖到列表中的另一个位置:若被拖的行在选中行之中,回调收到全部选中行,否则只收到该行;列表按顺序传入这些行,以及它们要插到其前的行号(拖到末尾时传入行数)。请把它们移到那里:拖动时列表会显示它们将去的位置,并在靠近边缘时滚动。

go
app.list.Reorder = func(rows []int, to int) {
	app.items = move(app.items, rows, to)
}

本身是按钮的行(点击时打开其所显示内容)也可以重排:按下行上的按钮后,指针一旦移动就拖动该行,若不移动则照常点击按钮。

参见拖放。

焦点 ​

列表在 ListState.Handle 中保持其控件标识。焦点查询在列表构建前后都可用,列表隐藏时焦点请求会等待:

go
app.list.Handle.OnShortcut(c, ui.Cmd, ui.KeyK, app.openCurrentFile)
ui.List(c.Key("files"), &app.list, len(app.files)).Grow(1).
    Rows(func(row ui.ListRow) {
        text := ui.Text(row.Context, app.files[row.Index].Name).Padding(6, 12)
        if row.Selected() && row.ListFocused() {
            text.TextColor(c.Theme().Accent)
        }
    })

在 UI 线程上调用 app.list.Handle.Focus() 请求焦点,或用 Focus(c) 显式选择窗口。Focused(c) 报告焦点是否在列表上,FocusWithin(c) 还包括其后代元素。句柄的快捷键动作只在构建后存在启用状态的控件时才会执行。

ListRow 提供当前的 Context、行索引、选中状态和列表焦点。用 ListState.Selected 或 Selection 启用选择。ItemKey 给每个项一个稳定的 key。请在调用 Rows 之前配置它们。

对于使用 List(c, state, n, func(i int)) 的自定义行构建器,ListState 的轮询辅助函数在构建之后或其行回调内部解析本轮的列表。持久的焦点与命令处理使用 Handle。Table 通过其 ListState 共享这一标识,Outline 通过其 List 状态共享。

持有键盘焦点的行滚出视野后仍保持构建,因此编辑可以继续进行,Tab 也能把下一个获得焦点的行滚动到视野内。

无障碍 ​

辅助技术看到的是一个由 Label 命名的列表,其中的列表项以其内部文本或状态的 Label 命名,每项都说明它是全部行中的第几行,如"10000 行中的第 5 行",尽管列表只构建可见的那些。可选择行的列表把辅助技术的焦点放在选中的行上:Up 和 Down 在移动选择时会被朗读,聚焦某行也会选中它。屏幕阅读器移到视野之外时会让列表滚动过去,从而构建滚到的那些行。

分列的行请见表格;树请见大纲。