WPF自定义控件 —— 装饰器
摘自:http://www.cnblogs.com/Curry/archive/2009/09/16/1567757.html
顧名思義就是裝飾用的,也就是說不改變原有的控件結構,但可以為控件添加一些新的功能,或是為控件的顯示外觀增加些東西。如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出來的,但是使用默認控件拼出新控件卻是團隊溝通的橋梁,默認控件一般都能滿足需求,自己定義的話可能效率有一定優勢,開始的時候也方便,到最后做大做復雜也挺麻煩,最主要的是團隊成員的樣式套用就麻煩,整體效果就有影響。之前自己有個想法,就是控件加快開發進度的,所以不好用的就不用了,實際上是也沒怎么認真去想怎么用。默認控件的模式和思路都是值得研究和學習的。當然如果你的控件需要一定的運行效率那就只能重做一份了。
????? 關于模式,面向對象開發,都是希望把責任分的更清晰,把功能切的更細,那把責任和功能切的更細的意義是什么呢,易于維護,可團隊里的每個人的思維形態并不一樣,你認為這樣好,可人家卻很難理解,難理解之后溝通是不是也難了,開發效率怎么上的去?所以利于溝通的設計才是好的設計。當團隊思想慢慢的趨于一致,再發揮你的才干,你可能會得到更好的幫助和建議。有些事不必要急于一時。
????? 在這里感謝很多熱心人的幫助,特別是周永恒,高手雖多可忙的不少,樂于解答的更少,幫你分析的少之又少。而周永恒卻可以幫你具體講解分析,在這里表示由衷的謝意。
轉載于:https://www.cnblogs.com/jojinshallar/articles/3372323.html
總結
以上是生活随笔為你收集整理的WPF自定义控件 —— 装饰器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微信小程序点击图片全屏展示,并可以翻下一
- 下一篇: OGRE分析之设计模式