程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> WPF自定義控件 - 裝飾器

WPF自定義控件 - 裝飾器

編輯:關於.NET

顧名思義就是裝飾用的,也就是說不改變原有的控件結構,但可以為控件添 加一些新的功能,或是為控件的顯示外觀增加些東西。如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出來的,但是使用默認控 件拼出新控件卻是團隊溝通的橋梁,默認控件一般都能滿足需求,自己定義的話 可能效率有一定優勢,開始的時候也方便,到最後做大做復雜也挺麻煩,最主要 的是團隊成員的樣式套用就麻煩,整體效果就有影響。之前自己有個想法,就是 控件加快開發進度的,所以不好用的就不用了,實際上是也沒怎麼認真去想怎麼 用。默認控件的模式和思路都是值得研究和學習的。當然如果你的控件需要一定 的運行效率那就只能重做一份了。

關於模式,面向對象開發,都是希望 把責任分的更清晰,把功能切的更細,那把責任和功能切的更細的意義是什麼呢 ,易於維護,可團隊裡的每個人的思維形態並不一樣,你認為這樣好,可人家卻 很難理解,難理解之後溝通是不是也難了,開發效率怎麼上的去?所以利於溝通 的設計才是好的設計。當團隊思想慢慢的趨於一致,再發揮你的才干,你可能會 得到更好的幫助和建議。有些事不必要急於一時。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved