WPF自定义控件 —— 装饰器
顾名思义就是装饰用的,也就是说不改变原有的控件结构,但可以为控件添加一些新的功能,或是为控件的显示外观增加些东西。如MSDN中的例子:
本来TextBox四角没有圆点,但是通过装饰器可以为它加上。所以可以看成在TextBox上加了层。
这样就“无痛”的给控件进行了装饰。当然应用不单单这样加几个点而已,修饰嘛比如拖动控件的修饰
而之前比较著名的层拖拽是Bea StollinitzHow can I drag and drop items between data bound ItemsControls?
一.AdornerLayer
我们说层,是覆盖在控件上的一层东西,那么控件上能不能覆盖多个层呢?
答案当然是可以的,而这些层自然的要放在一个容器中,这个容器就叫做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; }
很容易我们就可以看出它实际是通过可视树进行查找,然后判断元素是否为AdornerDecorator或ScrollContentPresenter,如果是的话则取他们的AdornerLayer属性,也就是说AdornerLayer是由AdornerDecorator或ScrollContentPresenter产生的,打开本地MSDN ,键入ScrollContentPresenter
由红框中的文字可以得知ScrollContentPresenter属于ScrollViewer,也就是说有ScrollViewer的地方就会有AdornerLayer,打开ScrollViewer的链接我们又可以了解到ScrollViewer通常需要包装Panel控件
那么哪些控件默认样式是用到ScrollViewer的呢,据我所知继承于ItemsControl的控件,还有Window等常用控件等,当然这里就不一一列举了。
如果实在没有ScrollViewer的地方,或者需要直接在控件上加层,我们也可以手动在控件外面包个AdornerDecorator来产生AdornerLayer。
<AdornerDecorator> <TextBox Text="可以得到AdornerLayer"/> </AdornerDecorator>
那么AdornerLayer到底是种什么概念,为什么总会在控件之上呢?
再用Reflector打开ScrollContentPresenter或AdornerDecorator在GetVisualChild(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效果对层上的元素有影响。
探其究竟便又要用到Reflector了
原来是做了绑定,不过我一直未探明白的是DispatcherPriority的顺序意义是什么,到底适用在哪些场景,还望高手多多指点。
其次我们关心的另外一件事是,这个层有多大?是覆盖整个被装饰的控件还是整个屏幕或是整个窗口。一般MeasureOverride可以指定控件的大小,顺序是Measure->Arrange->Render(本想贴我布局中的那张图,可回头看有些地方居然是错的,不知道当时怎么想的,囧RZ)。那么来看看MeasureOverride到底干了什么?
从红线框中我们很容易看出他是用了装饰控件的呈现尺寸来修饰的。当然这只是默认,你也可以自己重载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 ^
另外对于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属性其实
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; }
三.默认控件的应用
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。
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同时观察到
所以他的有效作用只有一个。好的控件是能够更好的解耦,可解耦的前提是原来的控件有一定的预留,TemplatedAdorner便是预留了ReferenceElement来达到效果。
四.自定义个遮罩控件
说了这么多是不是技痒了,那先来做个简单的吧,有时当我们读取数据希望未显示完的列表不需要让客户操作,所以需要要这个遮罩层来档下,一方面为了不让客户操作具体控件,令一方面可以让客户看到事情进度或操作信息。那怎么做比较舒服呢?自然的,我希望遮罩只针对某个控件而已,因为其他地方并不影响,依然可以操作。在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在我的印象中更接近一个包装器,把原有的方法放入包装类的一个同名方法中,在同名方法中再加些其他的功能罢了。
如果说里面颜色块代表功能大小的化,很明显包装类的功能更强大。而且他增加功能的话又对原来的A类结构是无损的,用户通过接口来操作的话也无须知道实体类。
就WPF而言,用户所要的是呈现效果,在外包一层改变了显示效果但是不影响原有控件的效果和功能。所以他可以操作控件的外观常用的Border控件,以及改变控件现实大小的ViewBox都是继承于Decorator类,还有就是我们之上提到的AdornerDecorator。都是对原来控件外观或控制的扩展。
Decorator本身只是个单容器控件,只能对一个控件进行装饰,Panel的话是多容器,可以对多控件进行装饰,控件大小的位置的改变也是装饰的一种表现形式。本想做个简单的拖拽控件,网上搜索了下发现已经有人做了。http://codeblitz.wordpress.com/2009/06/10/wpf-dragdrop-decorator-for-itemscontrol/
网页似乎被墙,我看的是快照,示例程序我也放上来了以免以后下不到。
对于默认拖拽的显示和做法,下面也记录下:
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
转载请注明