Terminal.Gui 导航术语表(Navigation Lexicon):焦点、焦点链与 Tab 导航体系完全指南
2026/9/23 22:49:37 网站建设 项目流程
  • UI组件
  • 跨平台
  • 桌面应用

【免费下载链接】Terminal.Gui

Cross Platform Terminal UI toolkit for .NET

项目地址:https://gitcode.com/gh_mirrors/te/Terminal.Gui
点击查看免费下载

导读

本文以 Terminal.Gui 官方文档中的导航术语表为核心骨架,系统梳理该 .NET 跨平台终端 UI 工具包中与"焦点与导航"相关的基础术语(Cursor、Focus、Focus Chain、TabStop、TabGroup 等),并结合 Navigation Deep Dive、Application.DefaultKeyBindingsApplicationNavigation源码,深入讲解每个术语在代码层面的真实含义、默认按键映射与可验证的行为细节。读完本文,你将能准确理解 Terminal.Gui v2 的焦点模型,能区分TabStopTabGroup的导航差异,并能在自己的应用里正确配置键盘导航与焦点行为。


术语表总览:理解 Terminal.Gui 导航的 9 个核心词汇

navigation-lexicon.md定义了 Terminal.Gui 导航体系的 9 个基础术语,它们构成了整个焦点与键盘导航模型的语言基础:

术语含义
Cursor向用户指示键盘输入将作用在何处的视觉指示器。每个终端会话有且仅有一个 Cursor。详见 Cursor 深度文档
Enter / Gain指某个原本未聚焦的 View 即将成为聚焦状态。"视图进入焦点"与"视图获得焦点"是同一含义。这两个词是 v1 时代的遗留术语
Focus某个 UI 元素(View)处于被选中状态、准备好接收用户输入的状态。拥有焦点的元素通常会响应键盘事件与其他交互
Focus Chain可接收焦点的 UI 元素的有序序列:从当前聚焦的元素开始,沿其父(SuperView)链一路延伸到焦点树的根部(Application.Top)。整个应用中只有一个焦点链拥有焦点(top.HasFocus == true),且每个焦点链中有且仅有一个View 是最聚焦(most-focused)的——它才是真正接收键盘输入的那个
Focus Ordering可聚焦 View 被遍历导航的顺序。UI 框架中通常用它来支持屏幕阅读器、提升应用可访问性。v1 中通过TabIndex/TabIndexes实现
Leave / Lose指某个原本聚焦的 View 即将失去聚焦状态。"视图离开焦点"与"视图失去焦点"是同一含义。同样为 v1 遗留术语
Navigation用户在应用视图层次结构中移动焦点的用户体验
Tab描述所有键盘上都有的Tab键、比空格更宽的文本断点,或作为键盘导航停靠点的 UI 元素。该词源自打字机,并因所有键盘上都存在Tab键而被强化
TabGroup一个作为其他可聚焦视图容器的ViewCommand.NextTabGroupCommand.PreviousTabGroup的默认按键分别是Key.PageDown.WithCtrlKey.PageUp.WithCtrl(见Application.DefaultKeyBindings)。这些按键使用户可以通过键盘在视图层次结构中上下导航
TabStop键盘导航的最终停靠点 View。此处的"ultimate"指该 View 没有可聚焦的子视图。Command.NextTabStopCommand.PreviousTabStop的默认按键分别是Key.TabKey.Tab.WithShift。这些按键只在同级视图(peer-views)之间导航

注意术语表中隐含的版本差异:术语表中对 TabGroup/TabStop 默认键的描述(PageDown.WithCtrl/PageUp.WithCtrl)来自文档编写时的状态,而当前仓库源码中Application.DefaultKeyBindings的实际映射为Tab/Tab.WithShift(NextTabStop/PreviousTabStop)与F6/F6.WithShift(NextTabGroup/PreviousTabGroup)。以当前仓库源码为准,下文将结合源码详细说明。


从术语到源码:默认导航键的实际绑定

Application.DefaultKeyBindings的真实映射

术语表引用了Application.DefaultKeyBindings,它定义在 Terminal.Gui/App/Application.cs,是当前仓库中导航键绑定的权威来源:

public static Dictionary<Command, PlatformKeyBinding>? DefaultKeyBindings { get; set; } = new () { [Command.Quit] = Bind.All (Key.Esc), [Command.Suspend] = Bind.NonWindows (Key.Z.WithCtrl), [Command.Arrange] = Bind.All (Key.F5.WithCtrl), [Command.NextTabStop] = Bind.All (Key.Tab), [Command.PreviousTabStop] = Bind.All (Key.Tab.WithShift), [Command.NextTabGroup] = Bind.All (Key.F6), [Command.PreviousTabGroup] = Bind.All (Key.F6.WithShift), [Command.Refresh] = Bind.All (Key.F5) };

与术语表(PageDown.WithCtrl/PageUp.WithCtrl)相比,当前源码使用:

  • Tab/Shift+Tab—— 在TabStop视图之间导航;
  • F6/Shift+F6—— 在TabGroup容器之间导航。

Navigation Deep Dive 中明确说明:F6的选择遵循了 Windows 平台的通用键盘加速键约定(Windows 应用的"常用键盘加速键"中 F6 用于在不同区域间移动焦点)。

这些键绑到了哪里?

ApplicationKeyboard在 Terminal.Gui/App/Keyboard/ApplicationKeyboard.cs 中将上述命令与Application.Navigation.AdvanceFocus关联起来:

AddCommand (Command.NextTabStop, () => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)); AddCommand (Command.PreviousTabStop, () => App?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop)); AddCommand (Command.NextTabGroup, () => App?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup)); AddCommand (Command.PreviousTabGroup, () => App?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));

同时,方向键也映射到同级导航命令(ApplicationKeyboard.cs):

KeyBindings.ReplaceCommands (Key.CursorRight, Command.NextTabStop); KeyBindings.ReplaceCommands (Key.CursorDown, Command.NextTabStop); KeyBindings.ReplaceCommands (Key.CursorLeft, Command.PreviousTabStop); KeyBindings.ReplaceCommands (Key.CursorUp, Command.PreviousTabStop);

这些绑定全部注册为KeyBindingScope.Application作用域——这是优先级最低的绑定作用域,因此任何 View 都可以覆盖这些默认按键行为。典型例子是Editor默认覆盖Key.Tab,从而允许用户在文本中直接输入制表符\t

关键结论:ApplicationKeyboard只负责"按键→命令"的翻译,真正的焦点移动逻辑在ApplicationNavigation.AdvanceFocus()中实现。这两个类共同构成了术语表中 "Navigation" 一词的运行时实体。


Focus 与 Focus Chain:应用级焦点模型

唯一性铁律:One Focus Per App

术语表强调:整个应用同一时刻只有一个焦点链拥有焦点,且每个焦点链中只有一个 View 是最聚焦的(接收键盘输入的那个)。这与 navigation.md 中列出的第一条导航原则(Tenet)"One Focus Per App" 完全一致——框架必须保证不会出现两个 View 同时成为"最聚焦"视图的情况。

在代码层面,这个"最聚焦视图"由ApplicationNavigation维护。查看 Terminal.Gui/App/ApplicationNavigation.cs:

private View? _focused; public event EventHandler<EventArgs>? FocusedChanged; /// <summary>Gets the most focused <see cref="View"/> in the application, if there is one.</summary> public View? GetFocused () => _focused;
  • GetFocused()返回应用中最聚焦(most-focused)的 View;如果没有视图拥有焦点,返回null(极为罕见)。它取代了 v1 的View.MostFocused/Application.TopRunnable.MostFocused模式。
  • FocusedChanged/FocusedChanging事件在最聚焦视图已改变 / 即将改变时触发。前者适合做全局响应(例如AdornmentsEditor根据当前焦点视图更新编辑器),后者适合在整个应用层面拦截/否决焦点变更。
  • SetFocused()是内部方法,它记录焦点变化、强制光标刷新并触发FocusedChanged

Focus Chain 的判断规则

术语表中的 "Focus Chain" 定义可翻译为如下可验证的代码事实(HasFocus属性):

  1. v.HasFocus == true,则v的整个 SuperView 链上所有视图必须都是可聚焦的
  2. 链上所有祖先视图的HasFocus也均为true
  3. v的更深层可聚焦子视图中,最深的那一个同样HasFocus == true

因此v.HasFocus == true并不必然意味着v是最聚焦视图——如果它有可聚焦子视图,那么真正接收输入的是链中最深的那一个(可由Application.Navigation.GetFocused()查询)。以Window -> Dialog -> Button三层结构为例:

window.HasFocus == true; // 位于焦点链中 dialog.HasFocus == true; // 位于焦点链中 button.HasFocus == true; // 实际最聚焦的视图 var mostFocused = Application.Navigation.GetFocused (); // 返回 button

View.HasFocus的底层字段是私有布尔值_hasFocus,它是判断视图是否拥有焦点的最终事实来源(见 View.Navigation.cs 中HasFocus属性实现)。

焦点链的视觉反馈

用户如何"看出"焦点链?答案是ColorScheme.Focus属性:

  • 处于焦点链中的视图使用其ColorScheme.Focus样式渲染;
  • 最聚焦的视图(焦点链最深处)可能通过View.Cursor显示终端光标;
  • 同一时刻只显示一个终端光标,由ApplicationNavigation统一管理。

自定义焦点样式可在绘制阶段依据HasFocus分支处理:

protected override void OnDrawContent (Rectangle viewport) { var attribute = HasFocus ? GetFocusColor () : GetNormalColor (); Driver.SetAttribute (attribute); // ... 绘制内容 }

TabStop 与 TabGroup:键盘导航的两级停靠点

一个 View 什么时候"可聚焦"?

要理解 TabStop/TabGroup,先要明确"可聚焦"的完整判定链。一个 View 要获得焦点必须同时满足(仅键盘导航需要第 4 条):

  1. Visible=true
  2. Enabled=true
  3. CanFocus=true
  4. TabStop!=TabBehavior.NoStop(仅对键盘导航而言)。

前三条对鼠标导航同样有效:一个Visible && Enabled && CanFocus == true的视图可以被鼠标点击聚焦,也可以被代码显式SetFocus()TabStop只影响键盘导航,对鼠标导航毫无影响。

TabBehavior枚举的四个取值

TabBehavior定义于 Terminal.Gui/ViewBase/Navigation/TabBehavior.cs,结合 navigation.md 的说明,各取值语义如下:

取值语义对键盘导航的影响
null(未初始化)视图仍在初始化中;作为set_CanFocus的触发信号,自动把TabStop置为TabStop(最常见的便捷用例)。判断键盘可聚焦性时等价于NoStop视同不可用
TabBehavior.NoStop阻止用户通过键盘导航让该视图(及其子视图)获得焦点跳过;仍可被鼠标或代码聚焦
TabBehavior.TabStop可聚焦视图,且没有可聚焦子视图。NextTabStop/PreviousTabStop只在同级视图(SuperView.SubViews)之间推进作为同级间导航的最终停靠点
TabBehavior.TabGroup可聚焦视图,同时是其他可聚焦视图的容器,支持跨容器键盘导航(平铺与重叠布局均适用)。NextTabGroup/PreviousTabGroup在整个应用范围内跨越所有TabGroup视图推进(除非被某个NoStopSuperView 阻断)作为组间导航的停靠点

源码枚举值:NoStop = 0TabStop = 1TabGroup = 2

典型容器的默认配置

从源码看,框架内建容器的TabStop配置(见 navigation.md 的 TabBehavior 一节):

  • FrameView—— 平铺场景的可见容器:TabStop = TabBehavior.TabGroupArrangement = ViewArrangement.Fixed
  • Window—— 重叠场景的可见容器:TabStop = TabBehavior.TabGroupArrangement = ViewArrangement.Movable | ViewArrangement.Resizable | ViewArrangement.Overlapped

这意味着典型的终端 UI 是"外层 TabGroup(容器)+ 内层 TabStop(控件)"的两级结构:Tab/Shift+Tab 在同级控件之间移动,F6/Shift+F6 在不同容器之间跳转

何时使用 NoStop?

NoStop的典型用途是"看得见、能被鼠标点中、但不能被 Tab 键盘导航打扰"的视图。注意它是递归阻断的:一个NoStop视图的子视图也无法通过键盘导航获得焦点(但鼠标/代码仍可聚焦)。这与 v1 中"CanFocusTabStop紧密耦合、充满魔法逻辑"的做法形成对比——v2 的目标是解耦这些概念CanFocus == true的视图可以同时TabStop == NoStop且仍可被鼠标聚焦。


AdvanceFocus:导航的实际执行者

应用级:ApplicationNavigation.AdvanceFocus

Application.Navigation.AdvanceFocus (direction, behavior)把导航请求转发到当前顶层视图:

public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior) { if (App?.Popovers?.GetActivePopover () is { Visible: true } visiblePopover) { return visiblePopover.AdvanceFocus (direction, behavior); } return App?.TopRunnableView is { } && App.TopRunnableView.AdvanceFocus (direction, behavior); }

注意两个细节(ApplicationNavigation.cs):

  • Popover 优先:如果有可见的活动 Popover,焦点在其内部推进——这保证弹出层不会被底层视图的导航抢占;
  • 该方法由app.Init()期间创建的应用级按键绑定所调用,同时作为public便捷方法对外开放。

视图级:View.AdvanceFocus

真正复杂的遍历逻辑在View.AdvanceFocus(View.Navigation.cs 起)。从实现骨架可以推断其核心流程:

  1. 先尝试在当前视图内推进:若存在聚焦的子视图,递归调用其AdvanceFocus
  2. 若未推进成功,则尝试换行(wrap)或上移到 SuperView,即AdvanceFocusChain()——在焦点链层面寻找下一个符合behavior过滤条件的视图;
  3. 找不到下一个时,通常会回绕到同级第一个符合条件的视图;容器(TabGroup)则借助PreviouslyFocused记录恢复上次聚焦的子视图。

方法签名public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior)返回true表示焦点已改变(或保持在原视图),false表示未能推进。

导航方向与行为过滤

参数取值说明
directionNavigationDirection.Forward/Backward前进 / 后退遍历方向
behaviorTabBehavior?TabStop/TabGroup/NoStop/null作为过滤器,只命中符合该行为的视图

例如AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)表示"在同级 TabStop 视图中向前推进"——这正是Tab键的语义;AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup)正是Shift+F6的语义。

鼠标导航与 RestoreFocus

鼠标导航遵循"之前是否聚焦过"的规则(见 navigation.md 的 Mouse Navigation 一节):

  • 若容器之前聚焦过,系统记录其子视图中上次最聚焦的那个,点击容器时调用RestoreFocus()恢复该子视图焦点;
  • 若容器之前未聚焦,调用AdvanceFocus()寻找下一个合适的聚焦目标。

因此框架必须包含清除RestoreFocus()焦点缓存的逻辑:当某个原本可聚焦的视图因Visible等变化而变得不可聚焦时,缓存必须失效,否则会尝试恢复到一个不可聚焦的视图上。


编程接口速查:代码控制焦点

让视图获得焦点

SetFocus()是开发者让视图获得焦点的首要公开方法,v2 中它可能返回false(视图不可聚焦或变更被取消时):

if (myButton.SetFocus ()) { Console.WriteLine ("Button now has focus"); } else { Console.WriteLine ("Could not focus button"); } // 等价写法:直接设置 HasFocus 属性(效果与 SetFocus() 相同,也可能会失败) myButton.HasFocus = true;

让视图失去焦点

让其他视图获得焦点是最典型的"失去焦点"方式;此外视图在失去可聚焦条件时也会自动失去焦点:

otherView.SetFocus (); // 焦点转移 Application.Navigation.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); myView.CanFocus = false; // 若持有焦点则失去 myView.Visible = false; // 若持有焦点则失去 myView.Enabled = false; // 若持有焦点则失去

监听焦点变化

视图级通过HasFocusChangingHasFocusChanged事件(以及对应的OnHasFocusChanging/OnHasFocusChanged虚方法)感知焦点变化:

view.HasFocusChanging += (sender, e) => { if (e.NewValue && !ValidateCanFocus ()) { e.Cancel = true; // 阻止获得焦点 } }; view.HasFocusChanged += (sender, e) => { if (e.CurrentValue) { OnViewGainedFocus (); } else { OnViewLostFocus (); } };

子类还可以直接覆写OnHasFocusChanging(CancelEventArgs<bool> e)并在条件满足时e.Cancel = true,这是"灵活覆盖"原则(Tenet: Flexible Overrides)的典型用法。

应用级监听与拦截

var app = Application.Create (); app.Init (); // 监听全局焦点变化 app.Navigation.FocusedChanged += (sender, e) => { var focused = app.Navigation.GetFocused (); StatusBar.Text = $"Focused: {focused?.GetType ().Name ?? "None"}"; }; // 在应用层面阻止特定视图获得焦点 app.Navigation.FocusedChanging += (sender, e) => { if (e.NewView is SomeRestrictedView) { e.Cancel = true; // 阻止焦点变更 } }; // 编程式导航 Application.Navigation.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); Application.Navigation.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup);

v2 与 v1 的关键差异

能力v1v2
最聚焦视图查询Application.TopRunnable.MostFocusedApplication.Navigation.GetFocused()
Add()副作用自动把祖先链CanFocus全部置为true不自动修改任何CanFocus
CanFocusTabStop紧密耦合、自动联动解耦,各自独立设置
导航方法数量分散在Application/Runnable中约十余个方法统一收敛为Application.Navigation.AdvanceFocus

v2 中Add(view)仍会在view.CanFocus == true时自动设置其TabStop(便捷性保留),但不会因为TabStop变化而自动修改CanFocus,也不再向上传染CanFocus。开发者需要为期望获得焦点的每个视图显式设置CanFocus

var container = new FrameView () { Title = "Container", CanFocus = true, // 必须显式设置 TabStop = TabBehavior.TabGroup }; var button = new Button () { Text = "Click Me", CanFocus = true, // 必须显式设置 TabStop = TabBehavior.TabStop // Add() 会自动设置,但可显式覆盖 }; container.Add (button); // 不会自动把 container 的 CanFocus 设为 true

常用导航模式实战

对话框导航

var dialog = new Dialog () { Title = "Settings", CanFocus = true, TabStop = TabBehavior.TabGroup }; var okButton = new Button () { Text = "OK", IsDefault = true }; var cancelButton = new Button () { Text = "Cancel" }; // Tab 在按钮之间导航,Enter 激活默认按钮 dialog.Add (okButton, cancelButton);

双栏容器导航

var leftPanel = new FrameView () { Title = "Options", TabStop = TabBehavior.TabGroup, X = 0, Width = Dim.Percent (50) }; var rightPanel = new FrameView () { Title = "Preview", TabStop = TabBehavior.TabGroup, X = Pos.Right (leftPanel), Width = Dim.Fill () }; // F6 在左右面板之间跳转,Tab 在面板内部控件之间移动

列表导航

var listView = new ListView () { CanFocus = true, TabStop = TabBehavior.TabStop }; // 方向键导航条目,Enter 选择,Space 切换 listView.KeyBindings.Add (Key.CursorUp, Command.Up); listView.KeyBindings.Add (Key.CursorDown, Command.Down); listView.KeyBindings.Add (Key.Enter, Command.Accept);

访问性最佳实践

Terminal.Gui 导航体系的设计目标之一是可访问性(navigation.md 的 Accessibility Considerations 一节):

  • 键盘可达:一切功能都应能通过键盘操作;内置 View 均通过单元测试保证至少有一个可推进焦点的导航键(见测试 AllViewsNavigationTests.cs 中的AllViews_AtLeastOneNavKey_Leaves,它验证所有内置 View 都满足"至少一个导航键可推进焦点")。
  • 焦点指示清晰:焦点指示不依赖单一颜色,热键以下划线字符视觉标示。
  • 提供有意义的标签与逻辑 Tab 顺序
// 提供有意义的标签(下划线字符即热键) var button = new Button () { Text = "_Save Document", HotKey = Key.S }; // 设置逻辑 Tab 顺序 container.TabStop = TabBehavior.TabGroup; foreach (var view in container.Subviews) { view.TabStop = TabBehavior.TabStop; } // 为鼠标动作提供键盘替代 view.KeyBindings.Add (Key.F10, Command.Context); // 等效右键 view.KeyBindings.Add (Key.Space, Command.Activate); // 等效点击

内置视图的输入交互对照

以下是 navigation.md 提供的内置 View 输入交互总表(节选核心视图),帮助理解不同控件在"热键、激活、接受、点击聚焦"上的差异:

ViewHotKeysActivate CmdAccept CmdHotKey CmdClick FocusRightClick
View1OnSelectOnAcceptFocusFocus-
Label1OnSelectOnAcceptFocusNextFocusFocusNext
Button1OnSelectFocus+OnAcceptFocus+OnAcceptHotKeySelect
CheckBox1OnSelect+AdvanceOnAcceptOnAcceptSelectSelect
ListView1MarkUnMarkRowOpenSelected+OnAcceptOnAcceptSetMark+OnSelectedChanged-
TextField1-OnAcceptFocusFocusContextMenu
Editor1-OnAcceptFocusFocusContextMenu

表头解读:

  • States:视图可拥有的视觉/功能状态数量;
  • Static:是否为纯展示型(非交互);
  • Default:是否可作为默认按钮(Enter 激活);
  • Activate CmdCommand.Activate触发时的行为;
  • Accept CmdCommand.Accept触发时的行为;
  • HotKey Cmd:按下视图热键时的行为;
  • Click Focus:点击时(若CanFocus == true)的行为;
  • DblClick / RightClick / GrabMouse:双击、右键、是否捕获鼠标进行拖拽。

总结:把术语表翻译成开发实践

回到 navigation-lexicon.md 这份术语表,它不仅是阅读文档的"字典",更是理解 Terminal.Gui v2 导航设计哲学的第一块拼图。把 9 个术语串联成一条开发主线,你可以记住:

  1. Focus是"视图准备好接收输入"的状态,Focus Chain是它的传递路径,整个应用只有一条焦点链活跃,链中只有一个 View 最聚焦;
  2. Cursor是唯一的视觉聚焦指示器,由ApplicationNavigation每帧统一管理(ApplicationNavigation.UpdateCursor),只跟随最聚焦视图;
  3. TabStop / TabGroup / Tab共同构成两级键盘导航:Tab/Shift+Tab在同级 TabStop 间移动,F6/Shift+F6在 TabGroup 容器间跳转,默认绑定以 Application.cs 的DefaultKeyBindings为准,并可通过配置覆盖;
  4. Enter/GainLeave/Lose是 v1 遗留的说法,对应 v2 的HasFocusChanging/HasFocusChanged事件体系;
  5. Navigation / Focus Ordering是这一整套机制的最终目标:让用户(包括依赖键盘与屏幕阅读器的用户)在复杂视图层次中始终有路可走。

如需继续深入,建议依次阅读:导航深度文档、键盘深度文档、光标管理、鼠标深度文档,并在 UICatalog 场景集 中通过AllViewsTester等场景实际体验 Tab / F6 导航行为。

  • UI组件
  • 跨平台
  • 桌面应用

【免费下载链接】Terminal.Gui

Cross Platform Terminal UI toolkit for .NET

项目地址:https://gitcode.com/gh_mirrors/te/Terminal.Gui
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询