窗体区域扩展器提供程序






4.80/5 (9投票s)
2004年11月5日
9分钟阅读

64852

2926
使用扩展器提供程序定义表单区域。
引言
如果你和我一样,你总是在寻找新的示例和文章,这些可能会帮助你了解新的或更好的设计、开发/编码和测试技术。像许多其他人一样,我发现微软的 Mike Harsh 撰写的文章《使用区域主控件》是一个有趣的示例,它展示了旨在帮助开发非线性(或者我应该说非矩形?)用户界面的控件。
尽管 Region Master Controls 库只是示例代码,但其整体质量和功能都相当高,尤其是设计时支持。当我第一次查看这些示例时,我并没有直接使用所呈现的控件或技术,但最近,我决定重新审视定义 Form
的 Region
的概念,而查看 Region Master Controls 是我的首要任务。
总之,长话短说,我开始摆弄 Region Master Controls 示例应用程序,几乎立刻就觉得所用的技术笨拙且有限。别误会我的意思,这种方法已经相当接近我的理想了,但我忍不住想知道为什么整个 RegionBuilder
类没有被构建成一个扩展器提供程序,以及为什么定义的区域仅限于那些实现了特殊接口(IRegionControl
)的控件。难道不是每个 Control
都定义了一个 Region
吗(即使是定义 Control
表面边界的无聊旧矩形)?鉴于所有这些因素以及我想纠正的一些其他实现“缺陷”,我很快就开始编写我认为简单、高质量的实现,它从设计者的角度来看也“感觉良好”,文档完善,并且随时可以在你的应用程序中使用。
背景
本文重点关注两个主要主题:“定义表单区域”和 IExtenderProvider
,但不会试图深入解释两者。幸运的是,Code Project 包含了关于这些以及许多其他主题的丰富信息。虽然理解本文内容不需要,但我还是提供了以下链接,因为你应该会发现它们对你的个人知识库是宝贵的补充
- 由 James T. Johnson 撰写的 了解 IExtenderProvider。
- 由 Chris Beckett 撰写的 使用 C# 和 IExtenderProvider 的菜单图像 - 一个更好的捕鼠器!。
- 由 Jibin Pan 撰写的 交互式异形表单。
- 由 Weiye Chen 撰写的 为表单和按钮创建位图区域。
使用代码
在提供的 FormRegion 库中,FormRegionExtenderProvider
是一个 Component
,可以拖放到 Form
上并显示在组件托盘中。它定义了两个 properties
Form
- 其区域将被扩展的Form
。ShowFormAsRegion
-true
表示将Form
显示为其子控件的复合区域,false
表示显示整个Form
。
并通过 IExtenderProvider
为 Form
上的每个子控件提供“扩展属性”AddToFormRegion
。
提供 Form
属性是为了将 FormRegionExtenderProvider
与一个特定的 Form
相关联,当(且仅当)ShowFormAsRegion
属性为 true
时,该 Form
的 Region
将设置为其顶级子控件(AddToFormRegion
属性为 true
)的复合区域。
请按照以下步骤使用 FormRegion 库,特别是 FormRegionExtenderProvider
。
- 在 VS 2003 工具箱中添加一个名为“FormRegion”的选项卡(右键单击工具箱,选择“添加选项卡”,然后输入名称)。
- 在选中“FormRegion”选项卡的情况下,通过选择“添加/移除项...”并浏览到程序集调试版本的位置来添加 FormRegion 库组件。
- 向您的项目添加一个
Form
。 - 从工具箱中选择
FormRegionExtenderProvider
,通过将其拖放到第 3 步创建的Form
上。 - 查看
FormRegionExtenderProvider
组件的属性,并将Form
属性设置为第 3 步创建的Form
。 - 将
ShowFormAsRegion
属性设置为true
。 - 向
Form
添加控件,包括一个按钮或其他允许您关闭表单的逻辑(记住,没有标题栏?)。 - 构建并运行。
您应该会看到表单主体显示为顶级子控件的复合体。这是因为您在第 6 步中将 ShowFormAsRegion
属性设置为 true
,并且默认情况下,Form
上的每个 Control
的 AddToFormRegion
扩展属性都是 true
。也就是说,默认情况下,所有控件都包含在 Form
的复合区域中。要将顶级控件从区域中排除,请将其 AddToFormRegion
“扩展属性”设置为 false
。
编写扩展器提供程序
FormRegionExtenderProvider
扩展了 System.ComponentModel.Component
,因此它可以在 VS 2003 工具箱和组件托盘中显示。此外,要成为一个扩展器提供程序,我们需要实现 IExtenderProvider
接口。
实现 IExtenderProvider
实际上非常简单,因为它只有一个方法
bool CanExtend(object extendee) ;
设计器通过此方法将对象传递给扩展器提供程序,以使其确定是否要扩展指定对象的属性。对于 FormRegionProviderExtender
,我们只关心 typeof(extendee)
是 Control
并且它不是 Form
。我们排除 Form
类型,因为 AddToFormRegion
对 Form
(特别是我们正在扩展的那个)没有意义。代码如下
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
都参与定义其父 Form
的 Region
,因此我们唯一需要记住的是那些不参与(即被排除)的控件。
因此,我不是跟踪每个 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
集合(isAddedToFormRegion
为 false
),或者从 properties
集合中移除它(isAddedToFormRegion
为 true
)。在 GetAddToFormRegion
的情况下,我们只需确定 extendee
是否在 properties
中有条目(即是否被排除),然后反转查找结果。
编码表单区域
让 Form
呈现非标准形状很简单,只需创建一个 Region
并将其值设置为 Form.Region
属性。如果希望 Form
正常显示,则可以设置 Form.Region = null
。在我们的例子中,创建 Form
的 Region
就是将所有顶级子控件的区域进行联合,这些控件的 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
属性。ClientRectangle
和 Bounds
属性之间的一个关键区别是 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 库包含两个自定义控件,GradientPanel
和 FormCaption
,它们展示了 IControlRegionProvider
的实现。
关注点
虽然我更喜欢我的 FormRegionExtenerProvider
,但 Region Master Controls 不仅仅是 RegionBuilder
的概念,它还包括一套漂亮的自定义控件,具有出色的设计器支持。我强烈建议您亲自查看一下。
历史
版本 1.0。