Windows 窗体的自动运行时制表符顺序管理






4.93/5 (75投票s)
2004年9月29日
7分钟阅读

232985

5437
本文介绍 TabOrderManager,这是一个根据不同高级方案自动调整 Windows 窗体上 Tab 键顺序的类。
引言
窗体上良好的 Tab 键顺序非常重要。它们允许有经验的用户更快地与应用程序交互,甚至对于无法操作鼠标的用户来说,它们可能是启用应用程序的必要条件。您可能有时会希望在运行时设置 Tab 键顺序。例如,您可能允许用户自定义窗体控件的可见性或位置,即使您不确切知道最终窗体会是什么样子,您也希望有一个专业的 Tab 键顺序。或者您可能不想在设计时因设计更改而担心维护复杂窗体的 Tab 键顺序。TabOrderManager
类将帮助您轻松自动化在运行时应用良好 Tab 键顺序的过程。而 TabSchemeProvider
组件使在 Windows 窗体设计器中设置 Tab 键策略变得简单。
背景
本文假设您对使用 C# 或 Visual Basic .NET 在 .NET Framework 中进行 Windows 窗体编程有基本的了解。
使用代码
TabOrderManager
类目前提供 C# 和 Visual Basic .NET 两种版本。它非常简单:给定一个容器控件(可能是 Form
,但也可能是 GroupBox
、TabControl
、Panel
等),调用 SetTabOrder
方法将自动调整子控件上的 TabIndex
属性,以实现“先横向再纵向”或“先纵向再横向”的 Tab 键顺序。默认情况下,此策略由子容器控件继承。但是,使用 SetSchemeForControl
方法,您可以为特定容器覆盖该策略。一个有用的情况是,您希望用户先横向遍历一系列 GroupBox
,然后纵向遍历一组封闭的 TextBox
。
这是一个调用 TabOrderManager
在 C# 中设置先横向 Tab 键策略的示例
// In constructor after InitializeComponent (or whatever other
// code might set controls' TabIndex properties).
(new TabOrderManager(this)).SetTabOrder(TabOrderManager.TabScheme.AcrossFirst);
在 Visual Basic.NET 中
'// In constructor after InitializeComponent (or whatever
'// other code might set controls' TabIndex properties).
Dim tom As TabOrderManager = New TabOrderManager(Me)
tom.SetTabOrder(TabOrderManager.TabScheme.AcrossFirst)
实现
为了实现给定的 Tab 方案策略,我们需要对控件进行排序,然后设置它们的 Tab 键顺序。关键在于控件如何排序。如果主要方案是“先横向再纵向”,我们的主要排序优先级是按控件的 Top
属性值。当且仅当 Top
属性相同时,我们才退回到按 Left
属性值排序。“先纵向再横向”的 Tab 键方案则相反。我们使用 .NET Framework 内置的基于自定义 IComparer
实现对集合进行排序的功能。我们的 IComparer
称为 TabSchemeComparer
,其 Compare
方法实现了上述排序策略。
如果给定控件本身是一个容器(即它有子控件),我们需要递归。如果使用了方案覆盖功能,我们需要在递归的每一层检查是否需要更改 Tab 方案。单个容器的 Tab 方案覆盖使用用户创建的 TabOrderManager
中的 HashTable
进行跟踪。当我们递归时,我们创建一个新的 TabOrderManager
并向下传递覆盖。创建辅助 TabOrderManager
的过程对客户端代码是不可见的。要查看 Tab 方案覆盖的实际效果,您可以选择在演示应用程序的 Tab 控件内的分组框中添加一个“先向下”覆盖。
TabSchemeProvider 组件
您可能还希望在 Windows 窗体设计器中配置您的 Tab 方案,而无需编写任何代码。TabSchemeProvider
组件是 TabOrderManager
类的一个薄包装器,允许您做到这一点。TabSchemeProvider
实现了 IExtenderProvider
接口,以动态地向您的容器控件添加一个 TabScheme
属性。对 IExtenderProvider
的完整讨论超出了本文的范围。在这里,我将简单解释如何利用 TabSchemeProvider
功能并讨论有趣的实现细节。
TabSchemeProvider 用法
在随附的解决方案中,TabOrder
类库项目包含 TabOrderManager
类和 TabSchemeProvider
组件。构建此项目后,您可以通过右键单击、选择“添加/移除项”并浏览到您编译的 TabOrder.dll 程序集来将 TabSchemeProvider
组件添加到您的 Windows 窗体工具箱中。一旦它在工具箱中,您就可以将其拖放到 Windows 窗体上。它位于组件托盘中。但是,如果您现在检查 Form
、GroupBox
、Panel
或 UserControl
的属性窗口,您将看到一个新的 TabScheme
属性,您可以将其设置为 None
(默认值)、AcrossFirst
或 DownFirst
。在运行时,在 Form Load
事件期间,每个容器的选定 Tab 方案将向下传播到其子控件,就好像您在相关容器上调用了 TabOrderManager.SetTabOrder
一样。
TabSchemeProvider 实现
TabSchemeProvider
控件有一些值得注意的实现细节。正如我上面所说,TabSchemeProvider
的有趣功能来自于 TabOrderManager
类。因此,要使其工作,只需为顶级窗体创建 TabOrderManager
实例,为所有设置了 TabScheme
属性的其他容器控件添加覆盖,并在 Form Load
事件中调用 TabOrderManager.SetTabOrder
(该事件发生在窗体设计器生成的代码将所有控件放置在 Form
的控件层次结构中之后)。
令人惊讶的困难部分是获取我们需要其 Load
事件的顶级 Form
的引用。像 TabSchemeProvider
这样的组件在运行时不会放置在 Form
上,因此不会自动访问控件层次结构。这与具有 Control.Parent
或 Control.TopLevelControl
等属性的 Control
不同。对于 TabSchemeProvider
组件而言,当 Form
本身是需要设置 Tab 键顺序的控件之一时,这不是问题。在这种情况下,设计器会生成如下一行代码:
//
// DemoForm
//
this.tabSchemeProvider1.SetTabScheme(this,
SMcMaster.TabOrderManager.TabScheme.AcrossFirst);
TabSchemeProvider
可以检测到正在设置 Form
的 TabScheme
,并使用给定的 Form
实例连接 Load
事件处理程序。问题解决了。但是,当 Form
具有默认的 Tab 方案 TabOrderManager.TabScheme.None
时会发生什么呢?在这种情况下,设计器不会生成这样的代码行,并且 TabSchemeProvider
从何处获取其 Form
实例也不再明确。有人可能会认为,当为非 Form
控件调用 SetTabScheme
时,您可以直接从 Control.TopLevelControl
属性中获取 Form
引用。不幸的是,这不起作用,因为通常情况下,SetTabScheme
可能会在 Control
和/或其父级添加到 Form
之前被调用,并且在这种情况下 Control.TopLevelControl
为 null
。
解决这个问题的一个方法是移除扩展器 TabScheme
属性上的 DefaultValue
属性,有效地强制 Windows 窗体设计器为每个支持的容器控件(包括顶级窗体)生成一个 SetTabScheme
调用。这不是很令人满意。因此,我一直努力,直到找到了另一种解决方案。
最终的实现认识到,当 TabSchemeProvider
组件通过调用 SetTabScheme
通知容器希望管理其 Tab 键顺序时,有两种可能性:
- 该控件已经是
Form
控件层次结构的一部分(包括控件**是**窗体的情况)。那么我们可以直接立即获取Form
的引用并挂钩其Load
事件。 - 该控件尚未成为
Form
控件层次结构的一部分。在这里,我们为控件及其所有祖先挂钩ParentChanged
事件。最终,该控件或其某个祖先将被添加到Form
中,我们可以在产生的ParentChanged
处理程序中挂钩Form
的Load
事件。
当我们最终通过处理这两种可能性中的任何一种发现窗体引用时,我们已经拥有所需的信息,并且可以缩短对 ParentChanged
事件的进一步处理。
最后,如果您在给定的窗体上使用多个 TabSchemeProvider
,则生成的 Tab 键顺序将取决于它们处理 Form Load
事件的顺序。换句话说,行为是未定义的,因此您应该避免这种情况。
历史
- 首次发布:2004年9月28日
- 添加了
TabSchemeProvider
组件:2004年10月28日