顧名思義就是裝飾用的,也就是說不改變原有的控件結構,但可以為控件添 加一些新的功能,或是為控件的顯示外觀增加些東西。如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出來的,但是使用默認控 件拼出新控件卻是團隊溝通的橋梁,默認控件一般都能滿足需求,自己定義的話 可能效率有一定優勢,開始的時候也方便,到最後做大做復雜也挺麻煩,最主要 的是團隊成員的樣式套用就麻煩,整體效果就有影響。之前自己有個想法,就是 控件加快開發進度的,所以不好用的就不用了,實際上是也沒怎麼認真去想怎麼 用。默認控件的模式和思路都是值得研究和學習的。當然如果你的控件需要一定 的運行效率那就只能重做一份了。
關於模式,面向對象開發,都是希望 把責任分的更清晰,把功能切的更細,那把責任和功能切的更細的意義是什麼呢 ,易於維護,可團隊裡的每個人的思維形態並不一樣,你認為這樣好,可人家卻 很難理解,難理解之後溝通是不是也難了,開發效率怎麼上的去?所以利於溝通 的設計才是好的設計。當團隊思想慢慢的趨於一致,再發揮你的才干,你可能會 得到更好的幫助和建議。有些事不必要急於一時。