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

智能控件设计器

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.88/5 (9投票s)

2007 年 12 月 1 日

10分钟阅读

viewsIcon

44147

downloadIcon

1922

本文介绍了SmartControlDesigner的工作原理和使用方法,它支持在设计时进行自定义控件设计

Screenshot - BorderSample.png

引言

在开发.NET控件元素时,我们公司KBSoft面临着在设计时提供复杂复合控件元素设计的任务。我们的目标是为包含大量复合部分(签名、图表、图像等)的控件元素提供方便的设计,这些部分将显示在其表面上。为了用户的舒适,这些元素应该在设计时通过鼠标选择。因此,这种复合控件元素的调整的便利性应该与表单上控件元素的调整相同,即用户可以使用鼠标选择、移动和调整不同控件元素的大小,并借助Visual Studio属性网格调整所选控件元素的特性。

对于这种问题陈述,首先想到的标准解决方案是——借助其他控件实现自定义控件元素的所有部分。但这种方法存在很多困难。众所周知,有时要从窗口中获得所需的行为是多么困难。例如,如果需要在控件元素上制作一个具有椭圆形边框的文本标记,或者在半透明背景上显示不透明文本,就会出现我们在文章半透明窗口上的不透明控件中提到的困难。此外,每个窗口都会占用一定的资源,接收消息等,而这些通常根本不需要。

如果您只需要借助 GDI+ 或 OpenGL 在控件元素表面绘制,并且不考虑放置在控件上的额外窗口,但同时又希望在设计时自由调整控件,那么就必须利用 Visual Studio 提供的机会,添加一定的视觉设计逻辑。为此,需要使用设计器和基于 `Component` 基类的 .NET 控件的组件模型。这种方法的唯一缺点是,在设计时实现良好的设计支持的复杂性很高。

本文包含了一个现成的设计器`SmartControlDesigner`,这是我们在KBSoft为内部需求创建的。该设计器能够支持调整控件元素表面上某些矩形区域的位置。对于这些区域,会以在WindowForms应用程序中选择表单上的控件元素时所绘制的标准样式绘制边框。此边框允许调整位置和区域大小。可以通过鼠标选择此类区域,其属性将显示在属性网格中。因此,调整控件的用户会感觉他正在处理表单上的常规控件元素。实际上,此区域永远不会是窗口,并且可以是内存中的简单对象,通过几个GDI函数绘制。在下图中,您将看到在IDE Visual Studio中设计的区域的外观。

Sample Image - maximum width is 600 pixels

SmartControlDesigner完全可以直接使用,在您的元素中实现几个接口后,您将获得一个在设计时完美可调的控件元素。当前设计器的实现示例可以作为开发更灵活、更舒适的设计时组件调整系统的基础。SmartControlDesigner源代码包含详细的注释和解释。

背景

为了在设计时管理控件元素行为,FCL中有一个类System.Windows.Forms.Design.ControlDesigner。它通过属性DesignerAttribute与控件元素类联系,方式如下

[Designer(typeof(KBSoft.Controls.SmartControlDesigner))]
public class UserControl1
{…}

在打开放置设计控件 UserControl1 的窗体时,会创建对象设计器(本例中为 SmartControlDesigner)。与设计器绑定的控件元素将通过此设计器的属性进行访问。为此,可以使用 ControlDesigner.Control 属性。

为了在设计时管理元素的构成,需要一套命令(添加元素、删除元素、调整元素属性等)。设计器允许添加一套在设计时为特定控件元素显示的命令。为此,需要在设计器中重新定义 `Verbs` 属性。示例如下。

public override DesignerVerbCollection Verbs
{
    get
    {
        return this.verbs;
    }
}

每个命令都由其名称和函数处理程序定义,当点击该命令时,函数处理程序将被激活。如果我们通过函数 `verbs.Add(DesignerVerb value)` 用当前属性集合 `DesignerVerbCollection` 填充返回值,那么设计时的命令列表将如下所示:

Sample Image - maximum width is 600 pixels

设计师的各种功能可以通过几个接口来使用。IDesignerHost 指的是它们。它允许控制在设计时设计的组件。接口ISelectionService允许对用户在设计时选择的组件的更改做出反应。而接口IComponentChangeService则提供删除或更改某个组件的事件。对这些接口的引用通常在Control Designer的重写函数Initialize中初始化。此函数在设计器创建后自动调用以实现初始化。用于获取接口引用的函数是GetService(Type serviceType),它将接口类型作为参数传递。下面您将看到SmartControlDesignerInitialize函数的一个片段,演示了必要接口的初始化和对控件元素的鼠标事件的订阅。

// Get interface IselectionService and save it into the field.

this.selectionService = this.GetService (typeof(ISelectionService))
                        as ISelectionService;

// Get interface IComponentChangeService and save it into the field.

this.componentChangeService = this.GetService (typeof
                        (IComponentChangeService))as IComponentChangeService;

// Get interface IDesignerHost and save it into the field.

this.designerHost = this.GetService (typeof(IDesignerHost))as IDesignerHost;

if (this.selectionService != null)
{
    // Subscription to the event of the chosen element change.

    this.selectionService.SelectionChanged += new EventHandler
                                (SelectionServiceSelectionChanged);
}

if (this.componentChangeService != null)
{
    // Subscription to the event of component deletion.

    this.componentChangeService.ComponentRemoving += new
        ComponentEventHandler (ComponentChangeServiceComponentRemoving);

    // Subscription to the event of change of any property of the component.

    this.componentChangeService.ComponentChanged +=
        new  ComponentChangedEventHandler
        (componentChangeService_ComponentChanged);
}

// Subscription to the mouse events from the control element.

this.Control.MouseDown += new MouseEventHandler (Control_MouseDown);
this.Control.MouseUp += new MouseEventHandler(Control_MouseUp);
this.Control.MouseMove += new MouseEventHandler(Control_MouseMove);
…

为了在设计时通过设计器控制控件元素的某个复合部分,它必须表示一个派生自`System.ComponentModel.Component`的类。然后,就可以借助之前以以下方式获得的`IDesignerHost`引用将该组件添加到主机(设计区域IDE Visual Studio,之后可以通过`PropertyGrid`编辑其属性)中:

this.designerHost.CreateComponent(Type type);

其中,类型为`Type type`的对象,表示派生自`Component`的引用类型,将作为函数参数传递。添加组件后,它将显示在Visual Studio的下部设计区域。其名称是根据组件类型名称形成的。

Sample Image - maximum width is 600 pixels

在通过`CreateComponent`函数获取对组件的引用后,此引用通常会传递给控件元素,因为它“知道”创建的元素应该如何绘制。

创建元素后,用户应该有机会以某种方式选择创建的组件以编辑其属性。在设计时调整组件需要支持两种选择机制。

首先,用户可以单击设计区域下半部分显示的组件图标(如上图所示)。组件的属性将显示在属性网格中。在这种情况下,区域中绘制的组件(图表、图像、标签等)应标记为已选择(例如,在框架中)。为了接收用户已选择组件的通知,有一个事件 `ISelectionService.SelectionChanged`。

其次,用户应该有机会在控件元素上绘制的组件区域内单击鼠标并选择它。为此,需要执行两个操作

  1. 重写设计器函数 `GetHitTest(Point point)`。此函数接收鼠标点击发生的点。如果函数返回 `true`,则表示事件将传输到控件元素的窗口(如果设计器订阅了来自控件的消息,如前所示,它将能够处理这些消息)。如果函数返回 `false`,则点击将由 Visual Studio 环境处理(这通常会导致选择控件元素作为所选组件)。
  2. 在设计器中处理鼠标点击的函数中,确定点击发生的组件,并使该组件在IDE Visual Studio环境中被选中。函数`ISelectionService.SetSelectedComponents(ICollection components)`用于此目的。

设计师使用说明

  1. 在您的设计器中实现接口`IRegionContainer`。此接口允许获取此控件元素包含的组件集合。借助此集合,设计器可以知道在设计时需要管理哪些组件。
  2. 在将存在于您的控件元素中的组件中实现`IRegion`接口。此接口确定组件的矩形区域。
  3. 使用 `DesignerAttribute` 属性将设计器 `SmartControlDesigner` 与控件元素关联起来。

接口`IRegion`包含`Bounds`属性(确定组件占用的矩形区域)和`ZOrder`(确定此区域的绘制顺序,即其与观察者的距离)。

接口 `IRegionContainer` 包含两个属性 - `Regions` 和 `VerbNames`,以及两个方法 - `RegionAdded` 和 `RegionRemoved`。`Regions` 属性应返回设计器应控制的组件集合(由 `IRegion` 的引用表示)。设计器将自动在 `IRegion` 区域周围绘制框架;通过鼠标支持区域大小更改;并在单击区域时在设计时选择组件。此属性返回的 `IRegion` 元素集合也应根据 `ZOrder` 值排序。设计器将从集合的末尾到开头搜索适当的区域。如果您了解此事实,则可以在选择重叠(彼此堆叠)区域时更改设计器行为。为此,只需更改排序逻辑。

这正是演示控件中实现此功能的方式,您可以在演示项目中找到它。

public RegionList Regions
{
    get
    {
        //  Create a backup collection, containing the resultant regions

        System.Collections.Generic.List<IRegion> regions = new
            System.Collections.Generic.List<IRegion>();

        //  Receive an array of elements ImageItem and copy

        //  these elements to regions.

        ImageItem[] images = new ImageItem[imagesList.Count];

        this.imagesList.CopyTo(images);

        regions.AddRange(images);

        //  Receive an array of elements RectItem and copy

        //  these elements to regions.

        RectItem[] rects = new RectItem[rectList.Count];

        this.rectList.CopyTo(rects);

        regions.AddRange(rects);

        //  Sort regions on ZOrder decrease.

        regions.Sort(delegate(IRegion r1, IRegion r2)
        {
            if (r1.ZOrder == r2.ZOrder)
                return 0;

            if (r1.ZOrder < r2.ZOrder)
                return 1;
            else
                return -1;
        }
        );

        return regions;
    }

    set { }
}

控件元素上显示着两个独立的组件集合——`imageList`和`rectList`。这些集合的元素被合并成一个`IRegion`链接集合。这两个集合的组件可以以任何方式在控件中绘制——这不会影响它们在设计时的行为。

`VerbNames`属性应返回对(`command_name`,`type_of_element_created`)的集合。这些命令将通过`Verbs`属性自动添加到设计器中。当单击命令`command_name`时,将创建`type_of_element_created`类型的组件。之后,将激活`RegionAdded`函数,并将创建的元素作为参数传递给它。通过此函数,控件得知在设计过程中添加了一个新元素。例如,此函数在演示应用程序中以下列方式实现

public void RegionAdded(IRegion region)
{
    ImageItem ri = region as ImageItem;
    RectItem rect = region as RectItem;

    if (ri != null)
        this.imagesList.Add(ri);
    else
        if (rect != null)
            this.rectList.Add(rect);
}

此函数确定在设计器中创建了哪个组件并将其添加到相应的集合中。

当在设计时发生组件删除时(用户选择了组件并单击删除),将调用剩余的函数`RegionRemoved`。在演示项目中,此函数定义如下

public void RegionRemoved( IRegion region )
{
    ImageItem ri = region as ImageItem;
    RectItem rect = region as RectItem;

    if (ri != null)
          this.imagesList.Remove(ri);
    else
        if (rect != null)
            this.rectList.Remove(rect);
}

这里执行了一个相反的过程——确定被删除元素的类型,并将其从相应的集合中删除。

因此,您可以在控件元素的表面上创建相当复杂的区域结构。借助 Regions 功能,您可以控制它们的放置逻辑(哪些控件元素绘制在其他元素之上)。

最后,需要注意的是,整个控件元素的删除必须以特殊方式处理。如果您从窗体中删除它,它的`Dispose`函数将被激活。绝对有必要激活设计器添加的其他组件中的`Dispose`函数;否则它们将保留在设计区域中,尽管它们没有可以显示它们的控件。`System.ComponentModel.Component`派生类的`Dispose`激活会导致它从IDE Visual Studio 2005中删除。

© . All rights reserved.