WPF自定义控件 — 装饰器

来源:互联网 发布:淘宝店开店的策划方案 编辑:程序博客网 时间:2024/06/03 12:43

文档下载:http://www.ctdisk.com/file/1758322

 

顾名思义就是装饰用的,也就是说不改变原有的控件结构,但可以为控件添加一些新的功能,或是为控件的显示外观增加些东西。如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效果对层上的元素有影响。

imageimage

 

探其究竟便又要用到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可以比较肯定的说,速度要比普通布局快,因为它是在布局之后弄的,并不牵涉到反复的可视树传递引发,所以动画尽量以改变此值为主。

 

我另外标示的GeneralTransformproposedTransform =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了

 

教程下载

 

原创粉丝点击