- UI组件
- 跨平台
- 桌面应用
【免费下载链接】Terminal.Gui
Cross Platform Terminal UI toolkit for .NET
TableView 是 Terminal.Gui 中用于展示"无限大小"表格数据的核心视图控件:它本身不持有任何数据,而是通过ITableSource接口桥接任意数据模型,并内置键盘/鼠标导航、多单元格矩形选区、列级样式定制与复选框列。阅读本文后,你将掌握如何用DataTableSource、EnumerableTableSource<T>、TreeTableSource<T>驱动表格,如何编程式读写选择状态、定制列样式与渲染外观,以及如何通过事件响应用户操作。以下内容以官方文档 docfx/docs/tableview.md 为骨架,并结合仓库源码(Terminal.Gui/Views/TableView 目录)展开讲解。
TableView 在终端中的实际渲染效果:表头、网格线与单元格选区(动图来自仓库 docfx 文档资源)
数据源(Data Sources)
TableView不拥有数据。它只负责把Table属性上挂载的ITableSource渲染出来,因此你可以把任何数据模型(DataTable、对象集合、CSV、数据库查询结果等)适配到表格上。
ITableSource:核心接口
接口定义在 Terminal.Gui/Views/TableView/ITableSource.cs,实现它即可把任意数据模型桥接进 TableView:
public interface ITableSource { int Rows { get; } int Columns { get; } string [] ColumnNames { get; } object this [int row, int col] { get; } }从源码可以看出,TableView 只通过这 4 个成员访问数据:Rows/Columns决定表格尺寸,ColumnNames提供表头文本,索引器this[row, col]按 (行, 列) 返回单元格对象。这也是 TableView 能渲染"无限大小"数据的根本——它只按需读取可见区域附近的单元格。
内置实现一览
仓库在 Terminal.Gui/Views/TableView 下提供了多个现成实现:
| 类 | 适用场景 |
|---|---|
DataTableSource | 包装System.Data.DataTable,见 DataTableSource.cs |
EnumerableTableSource<T> | 用 lambda 把对象集合投影为列,见 EnumerableTableSource.cs |
ListTableSource | 把IList包装成多列布局 |
TreeTableSource<T> | 为行增加展开/折叠的树形行为,见 TreeTableSource.cs |
示例一:包装 DataTable
DataTableSource是可变包装器(源码注释明确说明允许修改被包装的DataTable),适合动态变化的数据:
DataTable dt = new (); dt.Columns.Add ("Name"); dt.Columns.Add ("Age", typeof (int)); dt.Rows.Add ("Alice", 30); dt.Rows.Add ("Bob", 25); TableView tv = new () { Table = new DataTableSource (dt) };其实现非常直接(DataTableSource.cs):Rows/Columns直接取自DataTable的行列计数,ColumnNames取各DataColumn的Caption,索引器透传DataTable.Rows[row][col]。
示例二:对象集合投影
EnumerableTableSource<T>把任意对象集合变成表格,列由字典中的 getter lambda 定义。注意源码中的语义:集合元素在构造时被冻结为数组快照(data.ToArray()),但元素对象的属性值允许后续变化:
TableView tv = new () { Table = new EnumerableTableSource<Process> ( Process.GetProcesses (), new Dictionary<string, Func<Process, object>> () { { "ID", p => p.Id }, { "Name", p => p.ProcessName }, { "Threads", p => p.Threads.Count }, }) };这种模式下,列的取值是延迟计算的:this[row, col]通过_lambdas[ColumnNames[col]](_data[row])动态求值(EnumerableTableSource.cs),因此表格可以实时反映对象属性的最新状态。
示例三:手写 CSV 解析
没有现成的 CSV 源?可以直接构造DataTable再包装:
DataTable dt = new (); string [] lines = File.ReadAllLines (filename); foreach (string h in lines [0].Split (',')) { dt.Columns.Add (h); } foreach (string line in lines.Skip (1)) { dt.Rows.Add (line.Split (',')); } TableView tv = new () { Table = new DataTableSource (dt) };仓库中的TableView.BuildDemoDataTable(TableView.cs)也展示了类似思路:它生成含字符串、日期、整数、浮点、DBNull、Unicode 等多类型列的演示数据,可用于快速验证 TableView 的渲染能力。
提示:
TableView构造函数接受可选参数TableView(ITableSource table),也可用无参构造后赋值Table属性。赋值Table属性时会自动重置选择(SetSelection(0,0,false))并触发重绘(TableView.cs)。
选择模型(Selection Model)
TableView 实现了IValue<TableSelection?>,把"光标 + 所有扩展选区"打包成一个完整的值暴露出来,方便以数据驱动方式读写选择状态。
关键类型
| 类型 | 说明 |
|---|---|
TableSelection | 不可变快照:SelectedCell(Point)+Regions(IReadOnlyList<TableSelectionRegion>),定义见 TableSelection.cs |
TableSelectionRegion | 一个连续矩形选区,含Origin(选区的起始角)、Rectangle(选区范围)与IsExtended,定义见 TableSelectionRegion.cs |
Value属性 | 当前TableSelection?。为null表示未设置表格或选择被清空 |
从 TableSelection.cs 源码可知:非空TableSelection一定带有非空SelectedCell(导航锚点);Regions为空表示只有光标单元格被选中。TableSelection还实现了IEquatable<TableSelection>,值相等比较基于SelectedCell与各Region,这保证了重复设置相同选择不会触发多余的重绘与事件。
SelectedCell:活动单元格
SelectedCell是当前活动单元格(导航锚点),类型为Point,其中X= 列索引、Y= 行索引。读取方式:tv.Value!.SelectedCell。
编程移动光标用SetSelection (col, row, extend)(扩展方法签名为SetSelection(int col, int row, bool extendExistingSelection, ICommandContext? ctx = null),实现见 TableView.Selection.cs)。源码中该方法的几个关键行为值得注意:
- 会自动把光标"吸附"到最近的可见列(隐藏列会被跳过);
- 当
extendExistingSelection = true且MultiSelect = true时,会在旧光标与新位置之间创建/延伸矩形选区,而不是简单移动; - 通过
GetNearestVisibleColumn保证光标永远落在可见列上。
多选(Multi-Selection)
MultiSelect默认为true,用户可创建矩形选区:
- Shift+方向键—— 从光标处向新位置延伸出一个选区
- Ctrl+Click—— 把点击的单元格并入选择(union)
- Space(
Command.ToggleExtend)—— 切换当前单元格的IsExtended状态 - Ctrl+A—— 全选
IsExtended = true的扩展选区会在键盘导航中持久保留;非扩展选区在光标移动时被清空。这个语义在 TableView.Selection.cs 的ClearMultiSelectedRegions(keepToggledSelections)中实现:清空时只保留IsExtended为真的区域。
从源码看,ToggleExtend(TableView.Selection.cs)对三种触发方式做了区分:
- 键盘(Space):若光标所在单元格已有扩展区域则取消扩展(toggle OFF),否则标记为扩展(toggle ON);
- 鼠标 Ctrl+Click:对目标单元格执行 union(已选中则移除,未选中则加入),并保留原光标位置;
- 鼠标 Alt+Click:以光标为锚点向点击单元格延伸矩形选区。
FullRowSelect:整行选择
FullRowSelect = true时,不再按单元格选择,而是整个行被选中。此时GetAllSelectedCells ()与IsSelected ()会把光标行的所有列都报告为已选中(见 TableView.Selection.cs 的实现分支)。
技巧 —— FullRowSelect 下的 Home/End 重绑定:默认情况下
Home/End会移动到当前行的首/末列(MoveCursorToStartOfRow/MoveCursorToEndOfRow)。整行选择模式下,把它们重绑定为Command.Start/Command.End(跳到表格首/末行)往往更实用。这也是UICatalogRunnable处理其场景列表时采用的模式:tableView.KeyBindings.Remove (Key.Home); tableView.KeyBindings.Add (Key.Home, Command.Start); tableView.KeyBindings.Remove (Key.End); tableView.KeyBindings.Add (Key.End, Command.End);值得补充的是:
MoveCursorToStartOfTable/MoveCursorToEndOfTable在FullRowSelect开启时只移动行(保持当前列),避免不必要的水平滚动(TableView.Selection.cs)。
读取选择状态
// 活动单元格位置 Point selectedCell = tv.Value!.SelectedCell; // (col, row) // 所有被选中单元格的坐标 IEnumerable<Point> cells = tv.GetAllSelectedCells (); // 判断某个单元格是否被选中 bool sel = tv.IsSelected (col, row);GetAllSelectedCells的完整实现(TableView.Selection.cs)展示了选择语义:存在多选区域时遍历区域包围盒内的单元格逐个判定;FullRowSelect时整行计入;否则至少包含光标单元格。IsSelected还遵循"隐藏列不可选"规则(ColumnStyle.Visible = false的列永远返回false)。
键盘与鼠标绑定(Key & Mouse Bindings)
默认键盘绑定
| 按键 | 命令/行为 |
|---|---|
| 方向键 | 光标移动一个单元格 |
| Shift+方向键 | 延伸选区 |
| PageUp / PageDown | 移动一页 |
| Home / End | 移动到当前行首/末列 |
| Ctrl+Home / Ctrl+End | 移动到首/末行 |
| Shift+Home/End/Ctrl+Home/Ctrl+End | 向行/表边界延伸选区 |
| Ctrl+A | 全选(Command.SelectAll) |
| Space | Command.ToggleExtend—— 切换当前单元格的扩展状态 |
从 TableView.cs 源码可见,TableView 的静态DefaultKeyBindings还内置了Emacs 风格导航:Ctrl+P(上)、Ctrl+N(下)、Ctrl+V(下翻页),并把Home/End绑定到Command.Start/Command.End(基础层另提供 Ctrl+Home/Ctrl+End)。注意这是一个进程级静态属性,源码注释特别警告:不要在并行单元测试中修改它。
默认鼠标绑定
| 鼠标事件 | 命令 |
|---|---|
| 单击 | Command.Activate—— 光标移动到点击的单元格 |
| Ctrl+Click | Command.ToggleExtend—— 把点击的单元格并入选区 |
| Alt+Click | Command.ToggleExtend—— 向点击单元格延伸矩形选区 |
| 双击 | Command.Accept |
| 滚轮 | 上/下/左/右滚动 |
鼠标绑定在构造函数中注册(TableView.cs):滚轮上下映射为行滚动、左右映射为列滚动,单击映射Activate,Ctrl/Alt 组合映射ToggleExtend,双击映射Accept。另外,OnActivated(TableView.cs)在处理鼠标激活时会先通过TryGetMouseCellHit把屏幕坐标换算成单元格坐标(ScreenToCell),并拒绝点击在表头、末行下方或末列右侧空白处的无效命中。
自定义绑定
TableView 复用标准的KeyBindings与MouseBindings基础设施。既可以覆盖静态的DefaultKeyBindings(进程级,影响所有实例),也可以像上文 Home/End 示例那样修改单个实例的绑定。所有导航与选区扩展命令(Command.Up/Down/Left/Right、Command.Start/End、Command.LeftStart/RightEnd、各*Extend命令、ToggleExtend、SelectAll等)均在构造函数中通过AddCommand注册(TableView.cs),重绑定时只需把按键关联到这些既有命令即可。
渲染与滚动(Rendering & Scrolling)
TableView 只渲染表格的可见部分。水平/垂直滚动通过ColumnOffset与RowOffset实现(底层由Viewport支撑)。
表格渲染模型
- 表头—— 列名,可选上划线(overline)、下划线(underline)与垂直分隔线(由
TableStyle控制) - 数据行—— 从
RowOffset起逐行渲染,直到填满视口 - 列—— 从
ColumnOffset起向右渲染,每列宽度由内容宽度决定(受MinCellWidth/MaxCellWidth与列级ColumnStyle约束)
TableView 类上的两个全局宽度属性(TableView.cs):
MaxCellWidth:任何列渲染的最大字符数,防止单列过长挤掉其他列,默认DEFAULT_MAX_CELL_WIDTH = 100;MinCellWidth:列的最小字符数。
此外还有NullSymbol(DBNull.Value的显示文本,默认"-")与SeparatorSymbol(不使用竖网格线时用于分隔单元格值的符号,默认空格)。
TableStyle:外观控制
TableStyle(定义见 TableStyle.cs)集中了所有渲染开关:
| 属性 | 默认值 | 说明 |
|---|---|---|
ShowHeaders | true | 是否显示表头行 |
ShowHorizontalHeaderOverline | true | 表头上方横线 |
ShowHorizontalHeaderUnderline | true | 表头下方横线 |
ShowVerticalCellLines | true | 单元格之间的竖线 |
ShowVerticalHeaderLines | true | 表头之间的竖线 |
ShowHorizontalBottomLine | false | 最后一行下方的横线 |
AlwaysShowHeaders | false | 滚动时锁定表头 |
ExpandLastColumn | true | 用最后一列填满剩余空间 |
SmoothHorizontalScrolling | true | 最小增量水平滚动 |
InvertSelectedCellFirstCharacter | false | 选中单元格首字符反色(模拟光标) |
RowColorGetter | null | 整行自定义着色委托 |
源码补充的细节:SmoothHorizontalScrolling为true时向右滚动只增加"显示新列所需的最小偏移"(可能在大列数 + 慢RepresentationGetter时变慢);为false时滚动偏移直接跳到当前选中列(等价于 PageRight 行为)。ExpandLastColumn为false时,末列右侧会绘制列结束线并留下不可选中的空白区。
另有两个文档中未列出的实用成员:HeaderScheme(表头的基础Scheme,为空时回退到视图方案)与AlwaysUseNormalColorForVerticalCellLines(即使FullRowSelect开启也强制用Scheme.Normal渲染竖线)。
EnsureCursorIsVisible
编程移动光标后,调用EnsureCursorIsVisible ()滚动视口使光标单元格可见;Update ()会自动完成这件事(TableView.Selection.cs 中实现了对行方向与列方向视口的精细调整,并区分平滑滚动与整列跳转两种模式)。Update()的调用链(TableView.cs)会依次执行EnsureValidScrollOffsets → EnsureValidSelection → EnsureCursorIsVisible → SetNeedsDraw,因此数据或样式变更后调用一次Update ()即可让表格自洽地刷新。
列样式(Column Styling)
用TableStyle.ColumnStyles按列索引定制样式:
tv.Style.ColumnStyles [2] = new ColumnStyle { Alignment = Alignment.End, MaxWidth = 20, MinWidth = 5, Format = "C2", // 货币格式 ColorGetter = args => args.CellValue is int v && v < 0 ? new Scheme () { Normal = new (Color.Red, Color.Black) } : null };TableStyle提供两个便捷入口:GetColumnStyleIfAny(col)只读查询;GetOrCreateColumnStyle(col)不存在时自动创建(TableStyle.cs)。
ColumnStyle 属性全解
ColumnStyle.cs 定义的完整属性如下:
| 属性 | 说明 |
|---|---|
Alignment | 列默认文本对齐方式 |
AlignmentGetter | 按单元格值返回对齐方式的委托(覆盖Alignment) |
ColorGetter | 按单元格返回Scheme的委托(返回 null 用默认) |
HeaderColorGetter | 按上下文返回表头Scheme的委托(返回 null 回退到TableStyle.HeaderScheme或视图默认方案) |
RepresentationGetter | 自定义object→string转换委托(未设置时用object.ToString()) |
Format | IFormattable.ToString的格式字符串,如"yyyy-MM-dd"、"C2" |
MaxWidth | 列最大宽度(字符),默认TableView.DEFAULT_MAX_CELL_WIDTH(100);超过表级MaxCellWidth时被忽略 |
MinWidth | 列最小宽度(字符);大于MaxWidth或表级MaxCellWidth时被忽略 |
MinAcceptableWidth | 列的柔性下限宽度,默认DEFAULT_MIN_ACCEPTABLE_WIDTH,用于"按可用空间灵活伸缩" |
Visible | 是否隐藏该列(影响渲染与可选性);MaxWidth = 0时恒为false |
TruncationIndicator | 内容超宽时追加的省略号文本,默认Glyphs.HorizontalEllipsis("…");设为 null/空串则静默裁剪 |
从 ColumnStyle.cs 的GetRepresentation实现可以看清优先级:先应用Format(要求值是IFormattable),否则用RepresentationGetter,最终回退object.ToString()。GetAlignment则优先AlignmentGetter,否则用静态Alignment。
复选框列(Checkbox Columns)
用CheckBoxTableSourceWrapperByIndex(按行索引)或CheckBoxTableSourceWrapperByObject<T>(按对象属性)给任意ITableSource增加一个复选框列。这两种包装器都继承自 CheckBoxTableSourceWrapper.cs 中的抽象基类,构造时会自动拦截 Space 键与单元格激活事件来实现勾选逻辑。
// 按行索引 CheckBoxTableSourceWrapperByIndex checkSrc = new (tv, tv.Table!); tv.Table = checkSrc; // 读取已勾选的行 HashSet<int> checked = checkSrc.CheckedRows;// 按对象属性(双向绑定) CheckBoxTableSourceWrapperByObject<MyObj> checkSrc = new ( tv, enumSource, obj => obj.IsSelected, (obj, val) => obj.IsSelected = val ); tv.Table = checkSrc;交互行为:Space切换所选行的勾选状态;点击复选框列表头可全选/全不选;设置UseRadioButtons = true则退化为单选(单行勾选)行为。从基类源码还可自定义渲染符号:CheckedRune(勾选符,默认Glyphs.CheckStateChecked)、UnCheckedRune(未勾选符)、以及单选模式下的RadioCheckedRune/RadioUnCheckedRune。
树形表(Tree Tables)
TreeTableSource<T>把TreeView<T>的展开/折叠能力与 TableView 的列渲染结合起来,实现"可展开行的表格":
TreeView<FileSystemInfo> tree = new () { TreeBuilder = new DelegateTreeBuilder<FileSystemInfo> ( d => d is DirectoryInfo dir ? dir.GetFileSystemInfos () : [], d => d is DirectoryInfo), AspectGetter = f => f.Name }; tree.AddObject (new DirectoryInfo ("/")); TreeTableSource<FileSystemInfo> src = new ( tv, "Name", tree, new Dictionary<string, Func<FileSystemInfo, object>> () { { "Size", f => f is FileInfo fi ? fi.Length : 0 }, { "Modified", f => f.LastWriteTime } }); tv.Table = src;从 TreeTableSource.cs 源码看其工作方式:
- 行数动态变化:
Rows取自_tree.BuildLineMap().Count,即展开状态下的可见行数; - 第 0 列渲染树结构:
this[row, 0]走GetColumnZeroRepresentationFromTree,输出分支线、展开/折叠符号与节点文本;后续列才走 lambda 取值; - 构造参数:
firstColumnName指定第 0 列(树列)的列名,subsequentColumns是附加列字典;源码要求传入的TreeView<T>应是新建视图、不要加入其他父容器; - 生命周期:实现
IDisposable,Dispose时反注册事件并释放内部 TreeView。
交互上,当树列获得焦点时,左/右方向键分别折叠/展开节点。
注意:
TreeTableSource只负责在第 0 列渲染树形结构,不会渲染TreeView.CheckboxMode的复选框。若需要复选框树形表,请用CheckBoxTableSourceWrapperByIndex或CheckBoxTableSourceWrapperByObject<T>包装TreeTableSource(参见上文"复选框列"一节)。
事件(Events)
TableView 遵循标准的IValue<T>与View事件模式:
| 事件 | 触发时机 |
|---|---|
ValueChanging | Value变更前触发;设Handled = true可取消变更 |
ValueChanged | Value变更后触发,用于响应选择变化 |
Accepted | 用户双击单元格或按下 Accept 键 |
Activating | 用户单击单元格(Command.Activate) |
从 TableView.Selection.cs 的Valuesetter 实现可以看到完整链路:ValueChanging事件可被Handled取消 → 写入新值并同步内部光标状态(SyncCursorFromValue)→ 触发OnValueChanged与ValueChanged(同时派发未类型化的ValueChangedUntyped)。因此订阅ValueChanged是响应一切选择变化(光标移动、选区延伸、全选、清空)的统一入口。
示例:响应选择变化
tv.ValueChanged += (sender, e) => { if (e.NewValue is { } sel) { statusBar.Text = $"Row {sel.SelectedCell.Y}, Col {sel.SelectedCell.X}"; } };注意e.NewValue为null的场景(未设置表格或选择被清空),因此示例用模式匹配is { }过滤空值。
示例:处理单元格激活
tv.Accepted += (sender, e) => { Point selectedCell = tv.Value!.SelectedCell; object cellValue = tv.Table! [selectedCell.Y, selectedCell.X]; MessageBox.Query ("Cell", $"Value: {cellValue}", "OK"); };延伸阅读
- 官方文档:docfx/docs/tableview.md
- 控件主实现:TableView.cs(含命令注册、鼠标绑定、
BuildDemoDataTable) - 选择与光标逻辑:TableView.Selection.cs、TableSelection.cs、TableSelectionRegion.cs
- 渲染样式:TableStyle.cs、ColumnStyle.cs
- 数据源与包装器:DataTableSource.cs、EnumerableTableSource.cs、ListTableSource.cs、TreeTableSource.cs、CheckBoxTableSourceWrapper.cs
- 实战场景:UICatalog 示例中的 TableEditor.cs 与 TableViewTest.cs 展示了 TableView 在真实应用中的用法。
- UI组件
- 跨平台
- 桌面应用
【免费下载链接】Terminal.Gui
Cross Platform Terminal UI toolkit for .NET
相关推荐
Formily Next 表格选择组件 SelectTable 完整实战指南:单选、多选、树形数据与异步数据源
Formily Next 表格选择组件 SelectTable 完整实战指南:单选、多选、树形数据与异步数据源 SelectTable 是 @formily/n
前端UI组件Terminal.Gui TableView深度实战:让终端表格轻松承载百万行数据
Terminal.Gui TableView深度实战:让终端表格轻松承载百万行数据 Terminal.Gui 是 .NET 平台下广受好评的跨平台终端 UI 工
UI组件跨平台桌面应用FiftyOne 深度估计实战指南:从多格式数据加载到多模型推理的完整工作流
FiftyOne 深度估计实战指南:从多格式数据加载到多模型推理的完整工作流 导读 深度估计(Depth Estimation)是计算机视觉中连接 2D 图像与
人工智能计算机视觉数据集数据可视化数据标注模型评测
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考