在设计时选择窗体控件
在您的 UI 设计器中访问窗体的控件。
引言
.NET 包含许多不同的 UITypeEditor
,例如用于编辑 Combo
和 Listbox
初始内容或定义为 Collection(of String)
的任何属性的 StringCollectionEditor
。还有一个不错的 ControlsCollectionEditor
,它允许用户向您的组件添加新控件。本文将展示如何实现一个 UITypeEditor
,该编辑器允许您从窗体上已有的控件中进行选择。
背景
如何为 UITypeEditor
(很多人简称其为 UIDesigner
)获取现有窗体控件列表,一直以来都有些神秘。Mike-MadBadger 在他的文章 'Accessing the Controls on a Form at Design Time' 中解决了难题。这是一篇非常出色的技巧文章,详细介绍了 UITypeEditor
的基本工作原理。我在这里不会重复这些细节,因为它是一篇简短、写得很好的文章。
正如他所指出的,他大量借鉴了 Saeed Serpooshan 在 2007 年发表的一篇文章。Saeed 的文章更深入,是一份出色的 UIDesigner
入门指南。Mike 在 Saeed 的工作基础上,展示了如何实现一个 UITypeEditor
,将其设为一组通用过程在一个抽象(VB 中是 MustInherit
)类中,然后用一个非常漂亮的对话框窗体替换了标准的下拉编辑器。
这两部分共同为高度可重用的 ControlsCollectionEditor
提供了一个绝佳的起点,这正是我在这里要介绍的。
存在的问题
首先,此版本仅供组件使用。例如,您正在编写一个 ExtenderProvider
(如 ErrorProvider
或 ToolTip
),它将新属性插入到各种控件上。它不适用于容器控件,例如,如果您的项目继承自 Panel
,那么在这种情况下,您应该能够直接拖放所需的控件。
一个主要问题是,某些常用控件似乎会进入窗体的控件集合,即使它们是组件。UIDesigner
实际上可以访问窗体上的组件,这似乎令人困惑。由于 UIDesigner
附加到定义为 Collection(Of Control)
的属性上,为什么其中会有 Components
?嗯,.NET 实际上不会将后台字段或集合传递给 Designer 代码。Mike 通过与传递给 Designer 的实例相关的某些属性获取了窗体的 Components
。因此,唯一可用的就是您容器的组件列表(context.Container.Components
)。
这很好,但尝试将 Components
添加到 Collection(Of Control)
中会适得其反。我立即遇到的一个问题是 DataGridViewColumn
。由于它只能与 DataGridView
一起使用/附加到 DataGridView
,因此几乎没有人真的想在此上下文中处理它,而且它实际上是一个组件,而不是一个控件。
另一种可能性是 TabPage
。有可能有人正在开发用于处理这些的组件,但它们也是专用控件,只能添加到 TabControl
中。还有像 TableLayoutPanel
这样的控件——由于它是不可见的,所以几乎没有人会在他们的控件列表中想要它。因此,在很多情况下,我们都需要从 Collection(Of Control)
中排除不同类型的控件。
在开发 UnDoManager
组件的过程中,我再次尝试使用 Mike-Saeed 的 ControlsCollectionEditor
来允许开发人员选择要管理的控件。由于并非所有控件都与用户交互(例如 Labels
和 GroupBox
),因此它们不受支持。最后一个问题是父窗体包含在列表中。在某些情况下这可能没问题,但对我的项目不行。因此,需要一种过滤控件的方法。由于这是我第三次重构该类,所以我决定彻底解决它。
因此,除其他外,我为他们的工作添加了一个选择或过滤机制,这样开发人员就可以选择允许的控件类型或排除某些类型。
实现
继承所有内容
Mike 重构了 Saeed 的类以充当抽象/MustInherit
类,然后在其上构建了他的 ControlsCollectionUIEditor
。它作为通用演示效果很好,但如所示,在现实世界中事情通常更复杂。所以,我做了和 Mike 一样的事情:我对他提供的类做了一些小的改动,使其成为 MustInherit
,并将向您展示如何将其用作基类。这只需要很少的代码来实现。
不允许使用窗体
首先,添加了一个标志来从控件列表中排除窗体。由于窗体继承自 Control,所以它们看起来就像另一个控件,并且可能出现在控件列表中。排除机制(稍后介绍)可以像 TableLayoutPanel
一样方便地用于窗体,但我还使用了一个 ExcludeForm
标志,以便那些只需要排除窗体的人使用。请记住,这适用于您正在开发的组件,因此您应该详细了解它能做什么和不能做什么。
一个专属俱乐部
固有地需要排除某些事物,例如 DataGridViewColumn
(组件,而非控件)、TableLayoutPanel
(不可见)以及可能还有 TabPage
(只能添加到 TabControl
)。因此,显然需要一个排除机制。
标准的 .NET ControlsCollection
编辑器有一个包含机制,这正是我需要的 UnDoManager
:一种只包含我的工具设计用于处理的控件类型的方法。为了获得最大的灵活性,我实现了两种方式:一个包含列表和一个排除列表。这样,您就可以根据需要允许少数类型进入或只排除少数类型。
在 Mike 的对话框窗体显示之前,Saeed 的基类将调用 MustOverride
的 LoadValues
。以下是应用过滤器的方法。
Dim bAdd As Boolean = True
Dim thisCtl As Control = Nothing
For Each obj As Object In context.Container.Components
'Cycle through the components owned by the form in the designer
bAdd = True
' exclude other components - this weeds out DataGridViewColumns which
' can only be used by a DataGridView
If TypeOf obj Is Control Then
thisCtl = CType(obj, Control)
If ExcludeForm Then
bAdd = Not (TypeOf thisCtl Is Form)
End If
' custom Include only these list
If (typeIncludeOnly IsNot Nothing) AndAlso (typeIncludeOnly.Count > 0) Then
If typeIncludeOnly.Contains(thisCtl.GetType) = False Then
bAdd = False
End If
End If
' custom exclude list
If (typeExclude IsNot Nothing) AndAlso (typeExclude.Count > 0) Then
If typeExclude.Contains(thisCtl.GetType) Then
bAdd = False
End If
End If
If bAdd Then
myCtl.Items.Add(thisCtl)
End If
End If
Next
我添加的这些各种设置只是 Protected Friend
变量。列表已经实例化,所以您所要做的就是将您的类型添加到它们。代码,例如
If (typeExclude IsNot Nothing) Then
尝试留意那些认为将其设置为 Nothing 是个好主意的人,以防万一其中有内容(没有默认值)。否则,我没有在其中添加 Try/Catch
块,因为这只是一个设计时工具,如果您使用不当,最好让异常通过,以便您知道。在这样的设计时工具中捕获它们实际上会使它们更难找到(Saeed 的代码中有一个就非常难找)。
其他小改动
Mike 的工作中最让我喜欢的一点是学会了如何使用模态对话框窗体而不是默认的下拉列表。然而,为了获得最大的灵活性,我实现了一个第二类来使用下拉方法。这些非常小而且非常丑陋,因为 .NET 会将完整的类型名称和其他“有用”的信息附加到控件名称中。不过,在某些情况下,这种方法可能更具吸引力。
结果是现在有两个类:ControlCollectionDropDownUIEditor
用于调用下拉 CheckboxList
,ControlCollectionDialogUIEditor
用于对话框窗体版本。还有其他类,但它们是 MustInherit
的基类。
大多数其他改动都集中在结构上,例如将其打包成 DLL 以防止丢失文件或意外更改关键内容。要使用 DLL 版本,请添加引用并导入命名空间。如果您选择文件方式,对话框窗体已合并到ControlCollectionEditor.vb 文件中,这样要跟踪的文件就少了一个。
它的样子
选择控件属性的省略号将显示对话框窗体。在这种情况下,我们将 Labels
和 GroupBox
从符合条件的列表中排除。
默认的 .NET DropDown
版本遵循相同的规则。
Using the Code
使用随附的演示,以下是实现 ControlCollectionUIEditor
所需的代码。(演示不执行任何操作,但为 ExampleComponent
提供了一个宿主,用于在 VS 中使用。您很可能需要清理并生成项目,因为 VS 需要项目的编译版本才能在项目中实现 ExampleComponent
。)
1. 用 EditorAttribute 装饰将实现 Collection(Of control) 的属性。
Private _TargetControls As New Collection(Of Control)
'EditorAttribute provides the name of the Editor required
<EditorAttribute(GetType(ExampleFormControlCollectionUIEditor), _
GetType(System.Drawing.Design.UITypeEditor))> _
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content)> _
Public Property TargetControls() As Collection(Of Control)
- 请注意,
Collection(Of Control)
来自System.Collections.ObjectModel
,而不是VisualBasic.Collection
。 - 设计器名称是您为
Class
属性创建的名称:在此例中是ExampleFormControlCollectionUIEditor
。 - 这与
UIEditor
无关,但为了让您的Component
正确工作,您还需要这些过程。
' these control whether or not to serialize the control
Public Sub ResetTargetControls()
_TgtControls = Nothing
End Sub
Public Function ShouldSerializeTargetControls() As Boolean
Return (_TgtControls IsNot Nothing)
End Function
注意您的属性名称是如何嵌入到过程名称中的。
2. 编写 UIEditor
99% 的工作已经完成,应该在 DLL 中,您只需要提供一个本地类。这个类可以与您的主项目在同一个文件中(您的属性必须如上所述进行装饰)。
Imports Plutonix.UIDesign
<System.Security.Permissions.PermissionSetAttribute(_
System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _
Public Class ExampleFormControlCollectionUIEditor
Inherits ControlCollectionDialogUIEditor ' modified base class in the DLL
Public Sub New()
MyBase.new()
MyBase.ExcludeForm = True ' defaults to False
' the sole types allowed in the list
typeIncludeOnly.Add(GetType(TextBox))
typeIncludeOnly.Add(GetType(ComboBox))
' ...
'
' to list a only a few types to exclude
typeExclude.Add(GetType(TabControl))
' ...
End Sub
End Class
注意:很难想象同时使用包含列表和排除列表的情况。选择最适合您用例的一种。上面同时展示了两种,以说明名称和用法。
注释:
- 类名(
ExampleFormControlCollectionUIEditor
)与属性EditorAttribute
中使用的名称完全相同。 typeIncludeOnly
和typeExcludeOnly
都是List(Of System.Type)
。- 两者都没有默认条目。我曾考虑将
TableLayoutPanel
和可能还有TabPage
作为默认排除项,但后来觉得不好,因为您无法轻松看到其中包含什么。而且,我已经使用了 4 次,似乎只包含某些类型是更常见的用例。 - 如果您将
typeIncludeOnly
留空(Count == 0
),则窗体上的所有控件都会显示在列表中。当添加类型时,这类似于ExtenderProvider
中的CanExtend
函数。 - 如前所述,可以通过使用
ExcludeForm
标志或将Form
添加到typeExcludeList
来排除窗体本身。如果您只需要修改控件方面的设置,那么这个标志就非常方便了。 - 如果您不需要调整任何设置,只需调用
MyBase.New
即可。
要进行测试,您必须清理并生成演示。然后在设计模式下,打开窗体,选择窗体托盘中的组件,然后在属性窗口中选择 'TargetContols
'。就是这样。Saeed 的工作将接管,使用 Mike 的对话框窗体来实现 .NET 集合编辑器。
演示中有一个示例组件,并且编码了 Dialog
和 DropDown
两个版本。要测试 DropDown
版本
- 将属性上的
EditorAttribute
更改为ExampleDropDownControlCollectionUIEditor
。 - 清理并生成,以便 VS 可以编译并在属性窗口中使用另一个编辑器。
类、成员引用
ControlCollectionDialogUIEditor 抽象类
允许开发人员使用模态对话框选择现有的窗体控件。使用 CheckedListBox
,因此可以选择多个控件。
ControlCollectionDropDownUIEditor 抽象类
提供一个 DropDown
CheckedListBox
,允许开发人员选择多个窗体控件。
typeIncludeOnly List(Of System.Type)
一个 System.Types
(控件)列表。只有列表中类型的控件才会显示在 UIEditor
中。
typeExclude List(Of System.Type)
一个 System.Types
(控件)列表。所有这些类型的控件都将被排除在 Dialog
或 DropDown UIEditor
之外。
ExcludeForm Boolean
指示是否将此组件的父窗体包含在列表中的标志。默认值为 True
。
CheckControlWidth Integer
仅适用于 ControlCollectionDropDownUIEditor
。设置下拉列表的宽度。最小值为 280,默认值为 400。
附录
除了重新编译以创建 NET 4.0 版本之外,更新还包括一个智能的 UIEnumEditor
。它将自动使用正确的控件来处理标志/位域 Enum
属性,并检测并使用描述(如果存在)。
.NET 原生处理声明为 Enum
的属性效果很好 - 除非它是位域或标志类型的 Enum
。
<Flags>
Public Enum FlagColors
None = 0
Red = 1
White = 2
Blue = 4
Green = 8
Yellow = 16
End Enum
默认的 NET UIEditor
似乎忽略或不知道 FlagsAttribute,并在属性窗口中使用单选 ListBox
,这不允许选择多个项目,而实际上应该允许。UIEnumEditor
提供一个下拉 CheckedListBox
来选择多个项目。
<Editor(GetType(UIEnumEditor), GetType(UITypeEditor))>
Public Property EnumOfFooExample As Foo
我通常会为 Enum
关联描述,当它们将在组合框或列表框中显示给用户时,有时我希望在 IDE 属性面板中使用这些描述。
Public Enum Stooges
<Description("Larry - Funny one")> Larry
<Description("Moe - 'Smart' One")> Moe
<Description("Curly - Sore One")> Curly
<Description("Shemp - One with bad haircut")> Shemp
<Description("CurlyJoe - Last one")> CurlyJoe
End Enum
<Editor(GetType(UIEnumEditor), GetType(UITypeEditor))>
Public Property EnumOfStooge As Stooges
然而,我不希望必须根据底层 Enum 的性质指定不同的 UIEditor
。这个 UIEnumEditor
旨在智能地根据 Enum
属性采取适当的操作。
简单 Enum
- 非标志/位域
Enum
将自动使用Description
文本(如果可用)。 - 任何缺少
Description
的成员将使用Enum
名称。 - 一个没有描述的简单
Enum
属性的外观/行为将与默认的 .NET 相同。
位域 Enum
- 标志/位域
Enum
将自动使用CheckedListBox
下拉列表,其中包含Enum
名称。 - 为了正常工作,
Enum
需要FlagsAttribute
,并且值必须是位域值(0、1、2、4、8、16、32、64...)。 - 这些将永远不会显示零值成员。该值应表示无,这可以通过不选择任何成员来指示。
您可以继承编辑器以更改默认行为。例如,告诉它为标志 Enum
属性显示描述。
Public Class EnumFlagFruitEditor
Inherits UIEnumEditor
Public Sub New()
MyBase.New()
Me.UseDescription = True ' change the default
Me.ControlWidth = 280
End Sub
End Class
由于设计时属性供开发人员使用,而不是最终用户,因此(对我而言)最好对位域属性使用 Enum
名称,以明确正在创建的组合。因此,默认行为是不将描述用于这些。要覆盖此行为,请将 UseDescription
属性设置为 True
。
同样,您可以通过继承并将 UseDescription
设置为 False
来强制简单/非位域 Enum
忽略描述(但这是默认的 .NET 行为)。
两个控件都是可调整大小的,但您可以使用 ControlWidth
设置初始下拉宽度。
演示说明了标志和非标志 Enum
属性的几种组合。然而,如果不查看代码(请参阅EnumCtl.vb),很难分辨出正在说明什么。该演示对正常和标志样式的 Enum
(无论是否有描述)都使用了相同的默认 UIEnumEditor
。
摘要
开发将在开发人员的 VS 设计器中运行的工具可能会令人困惑:毕竟,您正在使用 Visual Studio 来编写将在设计时在 VS 中运行的内容。这不是我们许多人每天都会做的事情,而且关于该主题的清晰参考资料并不多。
Saeed 关于此主题的文章内容丰富,可以帮助理解 UI Designer 的一些晦涩的方面。Mike 的文章同样有价值,将 Saeed 的工作抽象为一组基本工具,并将对话框窗体添加到工具集中。
此处提供的代码的目的是利用这些先前的工作,提供实现一个灵活且功能强大的控件集合编辑器的方法,该编辑器足以处理各种情况。
历史
- 2013-11-25
- 初始文章和示例代码 ver 1.02
- 2014-05-28
- 已更新 .NET 4 版本
- 添加了一个小的
UIEnumEditor
- 附录解释了相同内容