65.9K
CodeProject 正在变化。 阅读更多。
Home

在设计时选择窗体控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (7投票s)

2013年11月25日

CPOL

12分钟阅读

viewsIcon

29897

downloadIcon

1224

在您的 UI 设计器中访问窗体的控件。

引言

.NET 包含许多不同的 UITypeEditor,例如用于编辑 ComboListbox 初始内容或定义为 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(如 ErrorProviderToolTip),它将新属性插入到各种控件上。它不适用于容器控件,例如,如果您的项目继承自 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 来允许开发人员选择要管理的控件。由于并非所有控件都与用户交互(例如 LabelsGroupBox),因此它们不受支持。最后一个问题是父窗体包含在列表中。在某些情况下这可能没问题,但对我的项目不行。因此,需要一种过滤控件的方法。由于这是我第三次重构该类,所以我决定彻底解决它。

因此,除其他外,我为他们的工作添加了一个选择或过滤机制,这样开发人员就可以选择允许的控件类型或排除某些类型。

实现

继承所有内容

Mike 重构了 Saeed 的类以充当抽象/MustInherit 类,然后在其上构建了他的 ControlsCollectionUIEditor。它作为通用演示效果很好,但如所示,在现实世界中事情通常更复杂。所以,我做了和 Mike 一样的事情:我对他提供的类做了一些小的改动,使其成为 MustInherit,并将向您展示如何将其用作基类。这只需要很少的代码来实现。

不允许使用窗体

首先,添加了一个标志来从控件列表中排除窗体。由于窗体继承自 Control,所以它们看起来就像另一个控件,并且可能出现在控件列表中。排除机制(稍后介绍)可以像 TableLayoutPanel 一样方便地用于窗体,但我还使用了一个 ExcludeForm 标志,以便那些只需要排除窗体的人使用。请记住,这适用于您正在开发的组件,因此您应该详细了解它能做什么和不能做什么。

一个专属俱乐部

固有地需要排除某些事物,例如 DataGridViewColumn(组件,而非控件)、TableLayoutPanel(不可见)以及可能还有 TabPage(只能添加到 TabControl)。因此,显然需要一个排除机制。

标准的 .NET ControlsCollection 编辑器有一个包含机制,这正是我需要的 UnDoManager:一种只包含我的工具设计用于处理的控件类型的方法。为了获得最大的灵活性,我实现了两种方式:一个包含列表和一个排除列表。这样,您就可以根据需要允许少数类型进入或只排除少数类型。

在 Mike 的对话框窗体显示之前,Saeed 的基类将调用 MustOverrideLoadValues。以下是应用过滤器的方法。

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 用于调用下拉 CheckboxListControlCollectionDialogUIEditor 用于对话框窗体版本。还有其他类,但它们是 MustInherit 的基类。

大多数其他改动都集中在结构上,例如将其打包成 DLL 以防止丢失文件或意外更改关键内容。要使用 DLL 版本,请添加引用并导入命名空间。如果您选择文件方式,对话框窗体已合并到ControlCollectionEditor.vb 文件中,这样要跟踪的文件就少了一个。

它的样子

选择控件属性的省略号将显示对话框窗体。在这种情况下,我们将 LabelsGroupBox 从符合条件的列表中排除。

默认的 .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 中使用的名称完全相同。
  • typeIncludeOnlytypeExcludeOnly 都是 List(Of System.Type)
  • 两者都没有默认条目。我曾考虑将 TableLayoutPanel 和可能还有 TabPage 作为默认排除项,但后来觉得不好,因为您无法轻松看到其中包含什么。而且,我已经使用了 4 次,似乎只包含某些类型是更常见的用例。
  • 如果您将 typeIncludeOnly 留空(Count == 0),则窗体上的所有控件都会显示在列表中。当添加类型时,这类似于 ExtenderProvider 中的 CanExtend 函数。
  • 如前所述,可以通过使用 ExcludeForm 标志或将 Form 添加到 typeExcludeList 来排除窗体本身。如果您只需要修改控件方面的设置,那么这个标志就非常方便了。
  • 如果您不需要调整任何设置,只需调用 MyBase.New 即可。

要进行测试,您必须清理并生成演示。然后在设计模式下,打开窗体,选择窗体托盘中的组件,然后在属性窗口中选择 'TargetContols'。就是这样。Saeed 的工作将接管,使用 Mike 的对话框窗体来实现 .NET 集合编辑器。

演示中有一个示例组件,并且编码了 DialogDropDown 两个版本。要测试 DropDown 版本

  1. 将属性上的 EditorAttribute 更改为 ExampleDropDownControlCollectionUIEditor
  2. 清理并生成,以便 VS 可以编译并在属性窗口中使用另一个编辑器。

类、成员引用

ControlCollectionDialogUIEditor 抽象类

允许开发人员使用模态对话框选择现有的窗体控件。使用 CheckedListBox,因此可以选择多个控件。

ControlCollectionDropDownUIEditor 抽象类

提供一个 DropDown CheckedListBox,允许开发人员选择多个窗体控件。

typeIncludeOnly List(Of System.Type)

一个 System.Types(控件)列表。只有列表中类型的控件才会显示在 UIEditor 中。

typeExclude List(Of System.Type)

一个 System.Types(控件)列表。所有这些类型的控件都将被排除在 DialogDropDown 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
    • 附录解释了相同内容
© . All rights reserved.