WPF自定义控件 —— 装饰器

顾名思义就是装饰用的,也就是说不改变原有的控件结构,但可以为控件添加一些新的功能,或是为控件的显示外观增加些东西。如MSDN中的例子:

untitled

本来TextBox四角没有圆点,但是通过装饰器可以为它加上。所以可以看成在TextBox上加了层。

image

这样就“无痛”的给控件进行了装饰。当然应用不单单这样加几个点而已,修饰嘛比如拖动控件的修饰

image

image

而之前比较著名的层拖拽是Bea StollinitzHow can I drag and drop items between data bound ItemsControls?

 

一.AdornerLayer

我们说层,是覆盖在控件上的一层东西,那么控件上能不能覆盖多个层呢?

image

答案当然是可以的,而这些层自然的要放在一个容器中,这个容器就叫做AdornerLayer

然后问题又来了这个层是如何产生的?是我们人为放的,还是自动产生的(虽然自动实际上也是需要有人写的)?

我们知道AdornerLayer有个方法

public static AdornerLayer GetAdornerLayer(Visual visual);

可以得到某个Visual的所在的层,我们打开Reflector进行查看

 

public static AdornerLayer GetAdornerLayer(Visual visual)
{
    if (visual == null)
    {
        throw new ArgumentNullException("visual");
    }
    for (Visual visual2 = VisualTreeHelper.GetParent(visual) as Visual; 

        visual2 != null; visual2 = VisualTreeHelper.GetParent(visual2) as Visual)
    {
        if (visual2 is AdornerDecorator)
        {
            return ((AdornerDecorator)visual2).AdornerLayer;
        }
        if (visual2 is ScrollContentPresenter)
        {
            return ((ScrollContentPresenter)visual2).AdornerLayer;
        }
     }
    return null;
}

很容易我们就可以看出它实际是通过可视树进行查找,然后判断元素是否为AdornerDecoratorScrollContentPresenter,如果是的话则取他们的AdornerLayer属性,也就是说AdornerLayer是由AdornerDecoratorScrollContentPresenter产生的,打开本地MSDN ,键入ScrollContentPresenter

image

由红框中的文字可以得知ScrollContentPresenter属于ScrollViewer,也就是说有ScrollViewer的地方就会有AdornerLayer,打开ScrollViewer的链接我们又可以了解到ScrollViewer通常需要包装Panel控件

image

那么哪些控件默认样式是用到ScrollViewer的呢,据我所知继承于ItemsControl的控件,还有Window等常用控件等,当然这里就不一一列举了。

如果实在没有ScrollViewer的地方,或者需要直接在控件上加层,我们也可以手动在控件外面包个AdornerDecorator来产生AdornerLayer。

<AdornerDecorator>
    <TextBox Text="可以得到AdornerLayer"/>
</AdornerDecorator>

 

 

那么AdornerLayer到底是种什么概念,为什么总会在控件之上呢?

再用Reflector打开ScrollContentPresenterAdornerDecoratorGetVisualChild(int index)中应该会注意到下面的代码(下面代码在ScrollContentPresenter中获得)

private readonly AdornerLayer _adornerLayer = new AdornerLayer();

protected override Visual GetVisualChild(int index)
{
    if (base.TemplateChild == null)
    {
        throw new ArgumentOutOfRangeException("index", index, SR.Get("Visual_ArgumentOutOfRange"));
    }
    switch (index)
    {
        case 0:
            return base.TemplateChild;

        case 1:
            return this._adornerLayer;
    }
    throw new ArgumentOutOfRangeException("index", index, SR.Get("Visual_ArgumentOutOfRange"));
}

 

这里就很明白了,index 0通常是我们需要装饰的控件,1就是AdornerLayer。我们知道系统首先会画0层的东西,再画1层 ,导致1永远都在0上。所以其实AdornerLayer也存在于可视树,可以通过VisualTreeHelper来找到。而且你不管调整控件的z-index都是无用的,人家写死了嘛。

 

二.Adorner

      有了容器,自然的要往里面添加东西,要不然不是空空如也么,有了也等于没有。而AdornerLayer规定能够加入它这个容器的只能是Adorner的派生类。在此淫威下所以我们也不得不臣服,把类继承于Adorner这个抽象类。

public class SimpleTextBlockAdorner : Adorner
{
  private TextBlock _textBlock;
  public SimpleTextBlockAdorner(UIElement adornedElement)
    : base(adornedElement) 
  {
      _textBlock = new TextBlock();
      _textBlock.Foreground = Brushes.Green;
      _textBlock.Text = "AdornerText";

  }

  protected override Visual GetVisualChild(int index)
  {
      return _textBlock;
  }

  protected override int VisualChildrenCount
  {
      get
      {
          return 1;
      }
  }

  protected override Size ArrangeOverride(Size finalSize)
  {
      //为控件指定位置和大小
      _textBlock.Arrange(new Rect(new Point(-10, 20), _textBlock.DesiredSize));
      return base.ArrangeOverride(finalSize);
  }
}

CS中代码如下:

public Window1()
{
    InitializeComponent();

    TextBlock textBlock = new TextBlock();
    textBlock.FlowDirection = FlowDirection.RightToLeft;
    textBlock.Text = "FlowDirection.RightToLeft";
    //放一个层容器
    AdornerDecorator adornerDecorator = new AdornerDecorator();
    adornerDecorator.Child = textBlock;
    this.Content = adornerDecorator;

    //得到层容器
    var adornerLayer = AdornerLayer.GetAdornerLayer(textBlock);
    //在层容器中加层
    adornerLayer.Add(new SimpleTextBlockAdorner(textBlock));
}

 

当改变textBlock的FlowDirection属性时,会出现如下所示的结果。也就是说被装饰的元素的FlowDirection效果对层上的元素有影响。

image image

探其究竟便又要用到Reflector了

image

原来是做了绑定,不过我一直未探明白的是DispatcherPriority的顺序意义是什么,到底适用在哪些场景,还望高手多多指点。

 

其次我们关心的另外一件事是,这个层有多大?是覆盖整个被装饰的控件还是整个屏幕或是整个窗口。一般MeasureOverride可以指定控件的大小,顺序是Measure->Arrange->Render(本想贴我布局中的那张图,可回头看有些地方居然是错的,不知道当时怎么想的,囧RZ)。那么来看看MeasureOverride到底干了什么?

image

从红线框中我们很容易看出他是用了装饰控件的呈现尺寸来修饰的。当然这只是默认,你也可以自己重载MeasureOverride来指定大小。

 

可要是装饰控件(本文中TextBox)的大小改变了,装饰器(本文中的SimpleTextBlockAdorner)怎么侦测的到?

实际上当我们改变装饰控件的大小时候,多是改变控件的Height或Width, 以改变Height为例,他是产生于FrameworkElement中的,定义如下

public static readonly DependencyProperty HeightProperty = DependencyProperty.Register("Height", typeof(double), _typeofThis, new FrameworkPropertyMetadata((double) 1.0 / (double) 0.0, FrameworkPropertyMetadataOptions.AffectsMeasure, new PropertyChangedCallback(FrameworkElement.OnTransformDirty)), new ValidateValueCallback(FrameworkElement.IsWidthHeightValid));

就是说改变Height的时候会触发Measure方法,而Measure方法会沿可视树向上找到父容器(在本例中是AdornerDecorator),然后调用它的OnChildDesiredSizeChanged方法,而OnChildDesiredSizeChanged中调用的是父容器本身的Measure方法,Measure方法会重新改变子容器的大小,装饰控件(TextBox)和装饰层(AdornerLayer)本来就同属于AdornerDecorator,所以在AdornerDecorator的Measure方法中会调用装饰控件和装饰层的Measure方法,装饰层又会用同样的方法刷新它的子类也就是我们的SimpleTextBlockAdorner,子调用父,父又调用子,子接着调用父?不是死循环了么?所以这里WPF用了变量MeasureInProgress和MeasureDirty来控制,如果已经在Measure中,则不需要循环调用。

 

这样下来你是不是感觉布局系统是很耗费资源的呢?^ 0 ^

image

 

另外对于Adorner中的GetDesiredTransform方法,其实看过AdornerLayer中的布局方法ArrangeOverride就可窥其详了

protected override Size ArrangeOverride(Size finalSize)
{
    DictionaryEntry[] array = new DictionaryEntry[this._zOrderMap.Count];
    this._zOrderMap.CopyTo(array, 0);
    for (int i = 0; i < array.Length; i++)
    {
        ArrayList list = (ArrayList)array[i].Value;
        int num2 = 0;
        while (num2 < list.Count)
        {
            AdornerInfo info = (AdornerInfo)list[num2++];
            if (!info.Adorner.IsArrangeValid)
            {
                Point location = new Point();
                info.Adorner.Arrange(new Rect(location, info.Adorner.DesiredSize));
                GeneralTransform desiredTransform = info.Adorner.GetDesiredTransform(info.Transform);
                GeneralTransform proposedTransform = this.GetProposedTransform(info.Adorner, desiredTransform);
                int index = this._children.IndexOf(info.Adorner);
                if (index >= 0)
                {
                    Transform transform3 = (proposedTransform != null) ? proposedTransform.AffineTransform : null;
                    ((Adorner)this._children[index]).AdornerTransform = transform3;
                }
            }
            if (info.Adorner.IsClipEnabled)
            {
                info.Adorner.AdornerClip = info.Clip;
            }
            else if (info.Adorner.AdornerClip != null)
            {
                info.Adorner.AdornerClip = null;
            }
        }
    }
    return finalSize;
}

 

GeneralTransform desiredTransform = info.Adorner.GetDesiredTransform(info.Transform);

((Adorner)this._children[index]).AdornerTransform = transform3;

从中我们可以看出,分配的时候就是把从GetDesiredTransform得到的值又返回给Adorner的AdornerTransform属性,而AdornerTransform属性其实

image

RenderTransform属性我们总熟悉了吧,不熟悉?那看这个吧http://msdn.microsoft.com/zh-cn/library/system.windows.uielement.rendertransform.aspx

用RenderTransform可以比较肯定的说,速度要比普通布局快,因为它是在布局之后弄的,并不牵涉到反复的可视树传递引发,所以动画尽量以改变此值为主。

 

我另外标示的GeneralTransform proposedTransform = this.GetProposedTransform(info.Adorner, desiredTransform); 也一定好奇吧,做什么呢?其实就是开始说的FlowDirection问题,反转上面的控件用的。

private GeneralTransform GetProposedTransform(Adorner adorner, GeneralTransform sourceTransform)
{
    if (adorner.FlowDirection == base.FlowDirection)
    {
        return sourceTransform;
    }
    GeneralTransformGroup group = new GeneralTransformGroup();
    Matrix matrix = new Matrix(-1.0, 0.0, 0.0, 1.0, adorner.RenderSize.Width, 0.0);
    MatrixTransform transform = new MatrixTransform(matrix);
    group.Children.Add(transform);
    if ((sourceTransform != null) && (sourceTransform != Transform.Identity))
    {
        group.Children.Add(sourceTransform);
    }
    return group;
}

 

三.默认控件的应用

 

 image      

        GridSplitter   Grid上的拖拉控件,我想大家应该不用吃惊吧,它是写了个PreviewAdorner来移动。在网上看到了这个链接http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/dfff9b89-81a8-4bfc-852d-d08ccdffe6bb 提问者改变了Window的模板,并在模板中放了Grid和GridSplitter,为什么会报错?现在我们知道了,Window默认的模板中有ScrollViewer,可以产生AdornerLayer,而改变的模板中没有AdornerLayer的容器,而且Window已经是窗口的最高层控件,沿可视树向上找也不会有其他的控件,所以GridSplitter不可能获取到AdornerLayer,因此就抛了NullReferenceException。解决办法便是在GridSplitter  外面套一层有AdornerLayer的东西,或ScrollViewer或AdornerDecorator,在链接中回答者给出的是AdornerDecorator。

 

image

       Validation 验证时候用的模板,其实你看到的这些感叹号,外框都是在层上的,他和GridSplitter 不同的是他可以在外面定义个模板,可以让用户自己指定要呈现的东西,为此他它写了个TemplatedAdorner,为什么找不到Validation 的默认模板,因为它用代码写死了。当然如果你发现惊叹号,外框不在该有地方,也容易做了——肯定层的位置有问题嘛。

private static ControlTemplate CreateDefaultErrorTemplate()
{
    ControlTemplate template = new ControlTemplate(typeof(Control));
    FrameworkElementFactory factory = new FrameworkElementFactory(typeof(Border), "Border");
    factory.SetValue(Border.BorderBrushProperty, Brushes.Red);
    factory.SetValue(Border.BorderThicknessProperty, new Thickness(1.0));
    FrameworkElementFactory child = new FrameworkElementFactory(typeof(AdornedElementPlaceholder), "Placeholder");
    factory.AppendChild(child);
    template.VisualTree = factory;
    template.Seal();
    return template;
}

至于AdornedElementPlaceholder这个占位符,它的大小是验证控件(TextBox)的大小,可他却是在模板中定义的,那么他如何来知道具体的验证控件是什么呢,这里它经过TemplatedAdorner中的AdornedElement来达到效果。可以说的上奇巧淫技,它使得AdornedElementPlaceholder知道具体的TemplatedAdorner,可TemplatedAdorner并不知晓具体的AdornedElementPlaceholder,但在AdornedElementPlaceholder同时观察到

image

所以他的有效作用只有一个。好的控件是能够更好的解耦,可解耦的前提是原来的控件有一定的预留,TemplatedAdorner便是预留了ReferenceElement来达到效果。

 

四.自定义个遮罩控件

 

image

说了这么多是不是技痒了,那先来做个简单的吧,有时当我们读取数据希望未显示完的列表不需要让客户操作,所以需要要这个遮罩层来档下,一方面为了不让客户操作具体控件,令一方面可以让客户看到事情进度或操作信息。那怎么做比较舒服呢?自然的,我希望遮罩只针对某个控件而已,因为其他地方并不影响,依然可以操作。在Demo上就简化了。显示信息的模板可以自定义修改,有没有感觉和刚才说的TemplatedAdorner模板有类似。所以发挥拿来主义的精神。

代码就不在这里列举了,不过要注意的是要把上面的模板控件加入可视树,要不然会穿越,就达不到阻挡的作用,同理如若需要穿越操作的话,可以不加入可视树。

对于代码有些人喜欢完全的附加属性如Validation 那样的赋值,我个人比较喜欢用类赋值,如果不喜欢可以动动手自己改掉,调用代码如下:

<ListView ItemsSource="{Binding Employees}">
    <ControlLibrary:MaskAttach.MaskAttach>
        <ControlLibrary:MaskAttach x:Name="fff" DataContext="{Binding Progress}" Open="{Binding IsLoading}">
            <ControlLibrary:MaskAttach.Template>
                <DataTemplate>
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition />
                            <ColumnDefinition Width="40"/>
                        </Grid.ColumnDefinitions>
                        <Rectangle Grid.ColumnSpan="2" Fill="Black" Opacity="0.7"/>
                        <TextBlock Grid.Column="1" Margin="5" Foreground="White"  HorizontalAlignment="Right"  

VerticalAlignment="Center">
                        <AccessText Text="{Binding}"/>
                        <AccessText>%</AccessText>
                        </TextBlock>
                        <ProgressBar Margin="10" Value="{Binding Mode=OneWay}"  Height="20"/>
                    </Grid>
                </DataTemplate>
            </ControlLibrary:MaskAttach.Template>
        </ControlLibrary:MaskAttach>
    </ControlLibrary:MaskAttach.MaskAttach>

在这里还要说明的是,AdornerLayer.GetAdornerLayer取得层的时候最后是放在d.Dispatcher.BeginInvoke中取,因为有时候需要等上面加载完,对于DispatcherPriority我一般选的是Render

而DependencyProperty的属性值改变侦测,如果用Binding的话则需要对象一定要从FrameworkElement派生的,但可以利用DependencyPropertyDescriptor侦测:

var dpdDataContext = DependencyPropertyDescriptor.FromProperty(MaskAttach.DataContextProperty, maskAttach.GetType());
dpdDataContext.AddValueChanged(maskAttach, delegate
  {
      d.SetValue(MaskAttach.DataContextProperty, maskAttach.DataContext);
  });

 

 

五.Decorator

 

   当看到Decorator让人更容易的想到Decorator模式,Decorator在我的印象中更接近一个包装器,把原有的方法放入包装类的一个同名方法中,在同名方法中再加些其他的功能罢了。

image

    如果说里面颜色块代表功能大小的化,很明显包装类的功能更强大。而且他增加功能的话又对原来的A类结构是无损的,用户通过接口来操作的话也无须知道实体类。

    就WPF而言,用户所要的是呈现效果,在外包一层改变了显示效果但是不影响原有控件的效果和功能。所以他可以操作控件的外观常用的Border控件,以及改变控件现实大小的ViewBox都是继承于Decorator类,还有就是我们之上提到的AdornerDecorator。都是对原来控件外观或控制的扩展。

image

Decorator本身只是个单容器控件,只能对一个控件进行装饰,Panel的话是多容器,可以对多控件进行装饰,控件大小的位置的改变也是装饰的一种表现形式。本想做个简单的拖拽控件,网上搜索了下发现已经有人做了。http://codeblitz.wordpress.com/2009/06/10/wpf-dragdrop-decorator-for-itemscontrol/

网页似乎被墙,我看的是快照,示例程序我也放上来了以免以后下不到。

dragdrop_decorator

对于默认拖拽的显示和做法,下面也记录下:

1.拖拽一般是两个控件之间的数据交互,就是拖拉的时候(MouseDown)把数据放到一个地方,拖拉完之后(MouseUp)再把这个数据放到另一个控件中,所以拖拽的两个控件本身要提供拖拽,继承于UIElement的控件可以直接把AllowDrop属性设置为True.

2.在鼠标点击也就是MouseDown,一般注册MouseDown事件或直接重载OnMouseDown方法,把选中的数据放到变量中,你选中的是控件?控件是数据的呈现,所以你应该能拿的到数据。除非那个控件真的没有数据,那也就不需要拉了,有数据的话,我们把数据放到一个变量_mouseDownData中。

3.在鼠标移动的过程中,我们看到鼠标的样式是会动的(鼠标下面会有小方块),而且我们传数据也需要个方法传对吧,所在鼠标移动的时候有MouseMove做下面这句

DragDrop.DoDragDrop((ItemsControl)sender, _mouseDownData, DragDropEffects.Move | DragDropEffects.Copy);

4.鼠标放开就就是MouseUp的话,把传递的数据的变量mouseDownData清空或赋值为Null.

5.拖拉有两方,假设要把A数据拖到B上,那么A调用了DragDrop.DoDragDrop方法去放,B怎么接收的呢?B控件要注册PreviewDrop事件,通过DragEventArgs e参数来获得e.Data,其中数据类型一般先e.Data.GetDataPresent(typeof(数据类型))来判断有没有,然后通过e.Data.GetData(typeof(数据类型))具体拿值,e.Effects可以用来判断操作:是否要把数据添加进B控件。

6.假定拖拉到一般要取消怎么办?控件注册PreviewQueryContinueDrag事件在QueryContinueDragEventArgs e中对e.Action进行赋值操作,可以DragAction.Cancel当然也可以DragAction.Drop或者DragAction.Continue了。

 

关于拖拽这里还有个文章:http://www.cnblogs.com/taowen/archive/2008/10/30/1323329.html

 

六.纯粹的个人感概

      

      在我学习WPF的第一个月,可以说自我感觉最良好的时候,当时认为什么都可以做了,WPF不过偶尔,凭借着以前的Winform开发思路,在OnRender中大放光彩,认为什么都能做,所以之前的控件都是自己重做的,看微软默认的不爽就重写,没有的就自己造,后来慢慢的,开始MVVM,开始大量的转变控件,虽然默认的控件大部分到最后也都是Draw出来的,但是使用默认控件拼出新控件却是团队沟通的桥梁,默认控件一般都能满足需求,自己定义的话可能效率有一定优势,开始的时候也方便,到最后做大做复杂也挺麻烦,最主要的是团队成员的样式套用就麻烦,整体效果就有影响。之前自己有个想法,就是控件加快开发进度的,所以不好用的就不用了,实际上是也没怎么认真去想怎么用。默认控件的模式和思路都是值得研究和学习的。当然如果你的控件需要一定的运行效率那就只能重做一份了。

      关于模式,面向对象开发,都是希望把责任分的更清晰,把功能切的更细,那把责任和功能切的更细的意义是什么呢,易于维护,可团队里的每个人的思维形态并不一样,你认为这样好,可人家却很难理解,难理解之后沟通是不是也难了,开发效率怎么上的去?所以利于沟通的设计才是好的设计。当团队思想慢慢的趋于一致,再发挥你的才干,你可能会得到更好的帮助和建议。有些事不必要急于一时。

      在这里感谢很多热心人的帮助,特别是周永恒,高手虽多可忙的不少,乐于解答的更少,帮你分析的少之又少。而周永恒却可以帮你具体讲解分析,在这里表示由衷的谢意。

 

 

MaskControl   DragAndDropDecorator

转载请注明

posted @ 2009-09-16 14:47  Curry  阅读(15508)  评论(16编辑  收藏  举报