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

窗体区域扩展器提供程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (9投票s)

2004年11月5日

9分钟阅读

viewsIcon

64852

downloadIcon

2926

使用扩展器提供程序定义表单区域。

Sample Image

Sample Image

引言

如果你和我一样,你总是在寻找新的示例和文章,这些可能会帮助你了解新的或更好的设计、开发/编码和测试技术。像许多其他人一样,我发现微软的 Mike Harsh 撰写的文章《使用区域主控件》是一个有趣的示例,它展示了旨在帮助开发非线性(或者我应该说非矩形?)用户界面的控件。

尽管 Region Master Controls 库只是示例代码,但其整体质量和功能都相当高,尤其是设计时支持。当我第一次查看这些示例时,我并没有直接使用所呈现的控件或技术,但最近,我决定重新审视定义 FormRegion 的概念,而查看 Region Master Controls 是我的首要任务。

总之,长话短说,我开始摆弄 Region Master Controls 示例应用程序,几乎立刻就觉得所用的技术笨拙且有限。别误会我的意思,这种方法已经相当接近我的理想了,但我忍不住想知道为什么整个 RegionBuilder 类没有被构建成一个扩展器提供程序,以及为什么定义的区域仅限于那些实现了特殊接口(IRegionControl)的控件。难道不是每个 Control 都定义了一个 Region 吗(即使是定义 Control 表面边界的无聊旧矩形)?鉴于所有这些因素以及我想纠正的一些其他实现“缺陷”,我很快就开始编写我认为简单、高质量的实现,它从设计者的角度来看也“感觉良好”,文档完善,并且随时可以在你的应用程序中使用。

背景

本文重点关注两个主要主题:“定义表单区域”和 IExtenderProvider,但不会试图深入解释两者。幸运的是,Code Project 包含了关于这些以及许多其他主题的丰富信息。虽然理解本文内容不需要,但我还是提供了以下链接,因为你应该会发现它们对你的个人知识库是宝贵的补充

使用代码

在提供的 FormRegion 库中,FormRegionExtenderProvider 是一个 Component,可以拖放到 Form 上并显示在组件托盘中。它定义了两个 properties

  • Form - 其区域将被扩展的 Form
  • ShowFormAsRegion - true 表示将 Form 显示为其子控件的复合区域,false 表示显示整个 Form

并通过 IExtenderProviderForm 上的每个子控件提供“扩展属性”AddToFormRegion

提供 Form 属性是为了将 FormRegionExtenderProvider 与一个特定的 Form 相关联,当(且仅当)ShowFormAsRegion 属性为 true 时,该 FormRegion 将设置为其顶级子控件(AddToFormRegion 属性为 true)的复合区域。

请按照以下步骤使用 FormRegion 库,特别是 FormRegionExtenderProvider

  1. 在 VS 2003 工具箱中添加一个名为“FormRegion”的选项卡(右键单击工具箱,选择“添加选项卡”,然后输入名称)。
  2. 在选中“FormRegion”选项卡的情况下,通过选择“添加/移除项...”并浏览到程序集调试版本的位置来添加 FormRegion 库组件。
  3. 向您的项目添加一个 Form
  4. 从工具箱中选择 FormRegionExtenderProvider,通过将其拖放到第 3 步创建的 Form 上。
  5. 查看 FormRegionExtenderProvider 组件的属性,并将 Form 属性设置为第 3 步创建的 Form
  6. ShowFormAsRegion 属性设置为 true
  7. Form 添加控件,包括一个按钮或其他允许您关闭表单的逻辑(记住,没有标题栏?)。
  8. 构建并运行。

您应该会看到表单主体显示为顶级子控件的复合体。这是因为您在第 6 步中将 ShowFormAsRegion 属性设置为 true,并且默认情况下,Form 上的每个 ControlAddToFormRegion 扩展属性都是 true。也就是说,默认情况下,所有控件都包含在 Form 的复合区域中。要将顶级控件从区域中排除,请将其 AddToFormRegion“扩展属性”设置为 false

编写扩展器提供程序

FormRegionExtenderProvider 扩展了 System.ComponentModel.Component,因此它可以在 VS 2003 工具箱和组件托盘中显示。此外,要成为一个扩展器提供程序,我们需要实现 IExtenderProvider 接口。

实现 IExtenderProvider 实际上非常简单,因为它只有一个方法

bool CanExtend(object extendee) ;

设计器通过此方法将对象传递给扩展器提供程序,以使其确定是否要扩展指定对象的属性。对于 FormRegionProviderExtender,我们只关心 typeof(extendee)Control 并且它不是 Form。我们排除 Form 类型,因为 AddToFormRegionForm(特别是我们正在扩展的那个)没有意义。代码如下

public bool CanExtend(object extendee) {
    return ((extendee != null) && (extendee is Control) && !(extendee is Form)) ;
}

虽然这段代码足够了,但我们真正想做的是类似以下的事情,但由于各种原因我们不能。

public bool CanExtend(object extendee) {
    if ((Form==null) || (extendee == null) || !(extendee is Control)) return false ;
    return ((Control)extendee).Parent == Form ;
}

这样做会限制仅扩展与 FormRegionProviderExtender 关联的 Form 的顶级子控件(Parent == Form)。不幸的是,当设计器调用 CanExtend 时,Parent 的值未定义。另一个问题是控件的 Parent 值可能会改变,并且曾经是非顶级控件的控件可能会变成顶级控件。尽管这种情况非常深奥,但能够在设计器中为所有 Form 控件分配非默认值并没有什么坏处。如果我们不允许这样做,它将不得不在代码中处理,并且将是开发人员必须担心的一件事。

成为扩展器提供程序的第二步是使用 Getter 和 Setter 方法定义“扩展器属性”。不幸的是(恕我直言),这样做的技术有点模糊。我确信这是最佳选择,但它与我们通常用于开发自定义控件和 Windows 窗体应用程序的大多数模式都不正交。

首先,我们需要使用 ProviderProperty 属性注释我们的 FormRegionExtenderProvider 类,每个我们支持的“扩展器属性”对应一个。在指定属性时,我们提供“扩展器属性”的名称以及“扩展器属性”适用的基类型。代码如下

[ProvideProperty("AddToFormRegion",typeof(Control))]
public class FormRegionExtenderProvider : 
     System.ComponentModel.Component, IExtenderProvider { ... }

如您所见,“扩展属性”的名称是 AddToFormRegion,它应用于(通常而言)类型 Control 或派生自 Control 的类型。

现在,我们为每个属性定义 Getter/Setter 方法。这是通过定义具有以下模式的方法来完成的

<property type> Get<property name>(object extendee) {
    return <property value> of extendee ;
}

void Set<property name>(object extendee,<property type> value) {
    <property value> of extendee = value ;
}

此时,我的扩展器提供程序实现将与常规情况有所不同。主要原因是因为 AddToFormRegion 扩展属性的类型是 bool,因此它只有两个可能的值,其中一个默认值(true)。这意味着,默认情况下,每个 Control 都参与定义其父 FormRegion,因此我们唯一需要记住的是那些不参与(即被排除)的控件。

因此,我不是跟踪每个 extendee 及其关联的属性值,而是通过维护一个 HashTable 来简化它(这减少了实现和初始化代码),但只记住那些不参与(即被排除)的 extendee

这是实现

[Category("Misc"), 
  Description("true if the control should be added to the forms region")] 
[DefaultValue(true)] 
public bool GetAddToFormRegion(object extendee) { 
    return !properties.Contains(extendee) ; 
}

public void SetAddToFormRegion(object extendee,bool isAddedToFormRegion) { 
    if (extendee == null) return ; 

    if (!isAddedToFormRegion) { 
        properties[extendee] = false ; 
    } else { 
        properties.Remove(extendee) ; 
    } 
}

实现很简单。我们在构造函数中定义并初始化一个 HashTable 成员,即 properties。当设计器调用 SetAddToFormRegion 方法时,我们将 extendee 添加到 properties 集合(isAddedToFormRegionfalse),或者从 properties 集合中移除它(isAddedToFormRegiontrue)。在 GetAddToFormRegion 的情况下,我们只需确定 extendee 是否在 properties 中有条目(即是否被排除),然后反转查找结果。

编码表单区域

Form 呈现非标准形状很简单,只需创建一个 Region 并将其值设置为 Form.Region 属性。如果希望 Form 正常显示,则可以设置 Form.Region = null。在我们的例子中,创建 FormRegion 就是将所有顶级子控件的区域进行联合,这些控件的 AddToFormRegion“扩展器属性”为 true。尽管实际上基本算法看起来像这样,但实际实现并非如此简单。

foreach(Control c in Form.Controls) { 
    if (GetAddToFormRegion(c)) 
        region.Union(c.Region) ; 
}

Form.Region = region ;

首先,虽然 Control 实例具有返回 Control 区域的 Region 属性,但它通常为 null。这主要是因为大多数控件是矩形的,对于这种简单情况,不创建实际的 Region 成本更低(资源方面)。如果 Control 具有非空 Region,我们使用它,否则,我们创建一个表示 Control 边界矩形的 Region。我相信您知道 ClientRectangle 不代表 Control 的边界,而只代表客户区。要获取 Control 的真实边界,我们需要使用 Bounds 属性。ClientRectangleBounds 属性之间的一个关键区别是 ClientRectangle 的原点始终是 (0,0),而 Bounds 矩形的原点是相对于父控件原点的 X,Y 位置。这实际上是一件好事,因为我们需要调整区域相对于 Form 和屏幕的位置,以使其正确偏移以进行绘制。

这是带注释的完整代码

protected Region GetChildRegion(Control c) { 
    Region region = null ; 

    if (c is IControlRegionProvider) { 
        // ----------------------------------------------------------- 
        // If your control implements IControlRegionProvider then you 
        // need to handle offseting the controls X,Y relative to its 
        // parent form (unless you are using the equivalent of 
        // Control.Bounds to calculate the region.) 
        // ----------------------------------------------------------- 
        region = ((IControlRegionProvider) c).FormRegion ; 
    } 

    if (region == null) { 
        // ----------------------------------------------------------- 
        // If a control directly specifies a region, use it... 
        // ----------------------------------------------------------- 
        region = c.Region ; 
    } 

    // region not specified, assume the bounds 
    if (region == null) { 
        region = new Region(c.Bounds) ; 
    }

    // ------------------------------------------------------------------------- 
    // This tranlsates the overall region by offseting the position of the form 
    // on the screen relative to the "DesktopLocation". Thus the controls region 
    // is adjusted to its absolute position on the form. 
    // 
    // Note, that unlike the 'RegionMasterControls' implementation we do not 
    // offset the client's X,Y on the form as the 'Bounds' method already 
    // handles that for us (i.e., it is relative to the Form.) 
    // ------------------------------------------------------------------------- 
    Point formClientScreenLocation = Form.PointToScreen(new 
          Point(Form.ClientRectangle.Left, Form.ClientRectangle.Top)); 
    int x = formClientScreenLocation.X - Form.DesktopLocation.X ; 
    int y = formClientScreenLocation.Y - Form.DesktopLocation.Y ; 
    region.Translate(x, y) ; 

    return region ; 
}

protected virtual Region MakeFormRegion() {
    Region region = null ;

    // No form, no region
    if (Form == null)
        return null ;

    foreach(Control c in Form.Controls) {
        // If the control is not excluded from the region,
        // add it to the region
        if (!properties.Contains(c)) {
            // create region on demand
            if (region == null) {
                // Initialize to empty
                region = new Region(new Rectangle(0,0,0,0)) ;
            }

            // add child region to form region and dispose of child region
            using (Region cRegion = GetChildRegion(c)) {
                region.Union(cRegion);
            }
        } 
    }

    return region ;
}

代码包含 FormRegion 库的一个我尚未讨论的关键方面,那就是 IControlRegionProvider 接口。此接口允许 Control 提供自定义 Region(而不是设置通常不进行的 Control.Region 属性)。这类似于 Region Master Controls 中定义的 IRegionControl 接口。

IRegionControlProvider 是一个简单的接口,定义如下

/// <summary>
/// Optional interface implemented to provide more powerful support when using 
/// the FormRegionExtenderProvider class 
/// </summary>
public interface IControlRegionProvider { 
    /// <summary>
    /// Return the region representing the control when composited with other 
    /// controls to create the non-rectangular region for the parent Form 
    /// </summary>
    Region FormRegion { get ; } 
}

本质上,实现了 IControlRegionProvider 的自定义控件实现了一个方法 FormRegion,用于返回定义控件表面的 Region。在此过程中,控件有两个基本职责

  • 偏移控件表面的 Region,使其相对于 Form 的原点。
  • 如果自定义控件想要缓存 Region,它应该返回 (Region) cachedRegion.Clone();

FormRegion 库包含两个自定义控件,GradientPanelFormCaption,它们展示了 IControlRegionProvider 的实现。

关注点

虽然我更喜欢我的 FormRegionExtenerProvider,但 Region Master Controls 不仅仅是 RegionBuilder 的概念,它还包括一套漂亮的自定义控件,具有出色的设计器支持。我强烈建议您亲自查看一下。

历史

版本 1.0。

© . All rights reserved.