WPF Adorner

看到这个标题,您可能会在脑中产生一个疑问:Adorner(装饰器)是什么?Adorner是WPF窗口中独立的一层,支持在界面元素之上执行独立的绘制及用户交互。可以说,Adorner在您的WPF程序中无处不在。在WPF中,从编辑框控件中光标的显示和选中效果的支持,到具有数据焦点的控件所具有的虚线外框,都是通过Adorner实现的。

什么是Adorner

鉴于您可能不熟悉Adorner这种组成,因此我在这里单独列出一节文字对其进行介绍。首先请您想象一下WPF如何对编辑框中光标和选中效果的支持:

按照较为常见的WPF开发方式,您可能需要为这两种情况分别提供一个非常繁琐的解决方案。

对于对光标的支持而言,我们可以根据当前的光标位置将字符串分为两个子串,并在依次显示这两个子串之间插入对光标的显示。但是这种方法会随着众多细节的加入变得十分繁琐:光标的显示需要占用一定的空间,如两个像素的宽度。那么在光标位置发生变化的时候,光标前后的字符会因为光标的位置变化而产生一定的位移。例如在上图中,如果将光标位置移动到“S”以后,那么字符“S”就应向前移动两个像素。在某些组成中,该实现是不能忍受的。例如在FlowDocument中,某些文字可能是以斜体的方式显示的,此时斜体的光标将可能导致光标两边的字符间距达到10个像素,甚至更多。

同样对于选中这一效果而言,我们的确可以通过在选中区域放入一个带有填充色的矩形来解决问题,但这同样需要动态地拆分字符串并动态计算矩形的大小:矩形的大小需要根据当前选中的子串经过布局计算得到,而经过布局计算后再次插入矩形将导致布局计算的重新开启。可以说,这个解决方案更会导致非常复杂的布局执行逻辑。

好了,现在发布答案。在WPF中,对光标以及选中效果的支持是通过Adorner类的派生类CaretElement来完成的。Adorner可以使一些界面元素的显示处于单独的层次中,因此不再参与其它界面元素的布局计算,从而使这些界面元素的显示不再影响软件界面的布局。WPF为光标及选中效果提供的解决方案正利用了Adorner所具有的最大特点:具有独立的布局系统。另外,Adorner所需要显示的内容常常显示在普通的界面元素之上也是其所具有的一大特点。

其实不仅仅是对光标的支持,WPF中还有众多对Adorner的使用。例如WPF通过FocusVisualStyle定义某个控件拥有输入焦点时所需显示的样式,如具有焦点的按钮上的虚线矩形:

总的来说,WPF中装饰器的常见应用包括:

向界面元素添加控制点,从而允许用户通过这些元素按照特定方式执行对元素的操作。如通过Grip调整大小、旋转、重新定位等。在界面元素上提供视觉效果,以提示用户当前元素处于特定状态。如在特定文本上绘制下划线等。遮挡界面元素的部分或全部,如IE上方的搜索栏在没有输入时显示当前搜索引擎的名称。
  现在我们从编程的角度来看看Adorner。

首先就是WPF对Adorner的支持。WPF对Adorner的绘制是在单独的一个层,AdornerLayer中完成的。该层总位于普通界面元素之上,并在执行布局计算时单独地执行measure-arrange流程。

那么谁具有Adorner呢?就现有的WPF实现代码来看,答案是AdornerDecorator以及ScrollContentPresenter。这两个组成常常由WPF内部实现逻辑隐式添加至程序界面中。

使用Adorner时的另一个组成就是其所装饰的元素。Adorner的绘制位置和其所装饰的元素相关,而该元素所在的位置则是由窗口的布局计算所决定的。同时Adorner的布局计算和窗口的布局计算是相互独立的,因此Adorner所装饰的元素是两种布局计算相互联系的一个关键点。

简单的Adorner编程

通常情况下,您可能需要按照如下形式将Adorner绑定到特定的界面元素:

调用静态方法AdornerLayer.GetAdornerLayer(),并将需要被Adorner装饰的界面元素当作参数传入。该函数会从该界面元素开始沿视觉树向上查找,并返回它所发现的第一个AdornerLayer。调用AdornerLayer.Add()函数将需要添加的装饰器加入AdornerLayer中。
  就以示例SampleAdorner为例:

1 AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(_label);
2 adornerLayer.Add(new ScaleAdorner(_label));

其中_label就是要修饰的界面元素,而SimpleCircleAdorner则是该界面元素所需要使用的Adorner类型。一般情况下,Adorner的派生类型需要考虑通过重写OnRender()或AddVisualChild()函数来指定Adorner如何绘制其外观:

 protected override void OnRender(DrawingContext drawingContext) 
 {
      // 绘制虚线矩形。注意右下角的矩形并不是由OnRender绘制的,而是由 
      // AddVisualChild()添加的 
      SolidColorBrush renderBrush = new SolidColorBrush(Colors.Green); 
      Pen renderPen = new Pen(new SolidColorBrush(Colors.Navy), 1); 
      renderPen.DashStyle = new DashStyle(new double[] { 2.5, 2.5 }, 0); 
  
     Rect rect = new Rect(0, 0, _adornedElement.ActualWidth, _adornedElement.ActualHeight);
     drawingContext.DrawRectangle(Brushes.Transparent, renderPen, rect);
     
     base.OnRender(drawingContext);13 }
     
      private void CreateGrip()
      {
         // Scaling grip   
          Rectangle rect = new Rectangle();
          rect.Stroke = Brushes.Blue;
          rect.Fill = Brushes.White;
          rect.Cursor = Cursors.SizeNWSE;
     
          rect.MouseDown += OnGripMouseDown;
          rect.MouseUp += OnGripMouseUp;
          rect.MouseMove += OnGripMouseMove;
          // 添加子元素,从而允许基类的OnRender()函数绘制该界面元素。
          // 同时需要重写VisualChildrenCount属性及GetVisualChild()函数
          AddVisualChild(rect);
          _scalingGrip = rect;
      }

同时,软件开发人员可能还需要考虑重写GetDesiredTransform()来指定Adorner的显示位置。在默认情况下,Adorner会使用其所装饰元素的左上角作为2-D坐标原点进行定位。但是在有些情况下,如目标元素使用了RenderTransform将元素绘制到了其它位置的时候,我们需要根据被修饰元素的实际信息执行适当地更改。

很多人在使用函数AdornerLayer.GetAdornerLayer()时都会对该函数如何工作的持有怀疑。首先,AdornerLayer存在于哪里?其次,对该函数进行调用时传入不同的界面元素是否能得到不同的AdornerLayer?搞清楚这些问题明显对我们编写更高效安全的代码有帮助。我是通过WPF源码得到这些问题的答案的:

 public static AdornerLayer GetAdornerLayer(Visual 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;
   }

也就是说,AdornerLayer.GetAdornerLayer()将返回遇到的第一个AdornerDecorator以及ScrollContentPresenter所关联的AdornerLayer。因此在调用该函数的时候,我们最好使用需要被装饰的元素作为参数,否则该函数所返回的可能并不是最接近该元素的Adorner。

该函数所提供的另外一个信息则是Adorner的显示范围。在上面的函数中,我们可以看出对Adorner的寻找是沿着视觉树向上进行的。由于一个视觉树的最根部就是当前窗口,因此AdorerLayer并不是全局的,也即不能超出当前窗口的显示范围。

Adorner的定位

前面已经提到,Adorner编程最需要考虑的一个问题就是Adorner的显示位置。虽然在前面的叙述中已经提到Adorner会默认使用其所装饰元素的左上角作为原点进行定位,但您可能对具体的布局计算何时发生,怎样根据被装饰元素进行定位存有兴趣。因此在本节中,我们将从WPF源码的层次分析Adorner的布局计算。

由于Adorner是存在于AdornerLayer中的显示元素,因此研究AdornerLayer的布局计算将作为了解Adorner定位方式的切入点。

首先是AdornerLayer的Measure-Arrange。在WPF中,如果AdornerDecorator以及ScrollContentPresenter的MeasureOverride()和ArrangeOverride()被调用,那么与之关联的AdornerLayer的相应布局计算函数将被调用。为AdornerDecorator组成所提供的大小将被传入到AdornerLayer的相应函数中,从而启动了对AdornerLayer的布局计算:

 // AdornerDecorator.MeasureOverride()
 protected override Size MeasureOverride(Size constraint) 
 { 
     Size size = base.MeasureOverride(constraint); 
     if (VisualTreeHelper.GetParent(this._adornerLayer) != null) 
          { 
                   this._adornerLayer.Measure(constraint);   
          }  
                 return size;
 }

被传递给AdornerLayer的MeasureOverride()以及ArrangeOverride()的调用将会使用相同大小调用各个Adorner的相应布局函数:

  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();
                  // 对Adorner.Arrange()进行调用,以控制布局
                  info.Adorner.Arrange(new Rect(location, info.Adorner.DesiredSize));
                  GeneralTransform desiredTransform = 
                      info.Adorner.GetDesiredTransform(info.Transform);
                  GeneralTransform proposedTransform = 
                      this.GetProposedTransform(info.Adorner, desiredTransform);
                  ……
              }
              ……
          }
      }
      return finalSize;
  }

在上面所展示的代码中,AdornerLayer.ArrangeOverride()函数还通过一系列对变换的操作控制各个Adorner所需要绘制的位置。首先,该函数获取了与Adorner相关联元素实际绘制位置的变换,并作为参数传入Adorner.GetDesiredTransform()函数中。查看代码后可以知道,Adorner.GetDesiredTransform()函数的默认实现仅仅是将传入的参数返回。鉴于该函数被声明为虚函数,因此非常容易地得知其是Adorner实现中用以控制绘制位置的扩展点。

综上所述,WPF将使用被装饰元素的左上角作为Adorner的默认坐标原点。同时,软件开发人员可以通过重写Adorner.GetDesiredTransform()函数来控制Adorner的显示位置。

Adorner的绘制

首先,对Adorner进行绘制的最直观方法就是重写OnRender()函数。重写OnRender()函数以控制外观实际上是UIElement类的派生类更改默认绘制行为的常用方法,而并非是Adorner所独有的扩展方式。在该函数中,软件开发人员也可以通过AdornedElement元素访问被装饰的界面元素。示例SampleAdorner也展示了这种绘制方式:

    protected override void OnRender(DrawingContext drawingContext) 
    { 
        SolidColorBrush renderBrush = new SolidColorBrush(Colors.Green); 
        Pen renderPen = new Pen(new SolidColorBrush(Colors.Navy), 1); 
        renderPen.DashStyle = new DashStyle(new double[] { 2.5, 2.5 }, 0); 
     
        Rect rect = new Rect(0, 0, _adornedElement.ActualWidth, _adornedElement.ActualHeight); 
        // drawingContext所提供的众多成员函数可以用来绘制各种图形。之所以在基类的OnRender() 
        // 函数调用之前执行对DrawRectangle()函数的调用则是为了能让基类OnRender()函数所显示
        // 的内容存在于当前所绘制内容之前
        drawingContext.DrawRectangle(Brushes.Transparent, renderPen, rect);
    
        // 由于Adorner可能包含其它可视组成,因此很多时候需要调用基类的OnRender()函数,
        // 执行对其它可视组成的绘制
        base.OnRender(drawingContext);
    }

您可能会问,如果需要在Adorner中显示WPF控件,我们应该怎么做呢?答案是调用AddVisualChild()函数。该函数用来设置两个Visual之间的关系。在调用了该函数以后,您还需要重写VisualChildrenCount属性,GetVisualChild()函数,以正确地反映该关系。而对于该界面元素的绘制则是通过基类的OnRender()函数所提供的默认执行逻辑完成的。

这就导致了一个问题:我们该如何控制OnRender()函数所绘制的界面元素的位置?答案是重写Adorner的ArrangeOverride()函数,并在该函数中调用界面元素的Arange()函数:

 protected override Size ArrangeOverride(Size finalSize)
 {
     Size size = base.ArrangeOverride(finalSize);
     if (_scalingGrip != null)
         _scalingGrip.Arrange(new Rect(finalSize.Width - 5, finalSize.Height - 5, 10, 10));
     return size;
 }

在绘制Adorner时,软件开发人员需要注意被装饰的元素上所使用的变换。就以控件的FocusStyle为例。具有Focus的控件所具有的虚线矩形即为FocusVisualStyle。其在AdornerLayer中绘制,并独立于控件的Style。如果一个控件使用了RenderTransform,那么由于FocusVisual绘制于AdornerLayer中,且最常见的AdornerLayer属于窗口,因此该控件的RenderTransform对FocusVisual不起作用。解决该问题的方法则是以该元素为子结点声明一个AdornerDecorator并将RenderTransform施行于其上。此时通过AdornerLayer.GetAdorner()所获得的AdornerLayer将是与AdornerDecorator所关联的AdornerLayer,从而同时应用了该RenderTransform。

另一个与Adorner相关的话题则是Adorner在ValidationTemplate中的使用。在创建ValidationTemplate的时候,软件开发人员可以用AdornedElementPlaceholder来表示被修饰的元素。整个模板除了AdornedElementPlaceholder之外均处于Adorner层中。例如在TextBox上使用Validation.ErrorTemplate标示下面的模板之后会在输入非法时显示一个叹号:

<ControlTemplate x:Key="validationTemplate">
   <StackPanel Orientation="Horizontal">
    <TextBlock Foreground="Red" FontSize="22" FontWeight="Bold" Margin="0,0,5,0">!</TextBlock>
    <AdornedElementPlaceholder />
   </StackPanel>
</ControlTemplate>

Adorner和用户的交互

虽然说Adorner是处于应用程序主渲染界面之外的额外一层,但是其仍会像其它界面元素一样接收输入事件。又由于AdornerLayer的z顺序总高于其所装饰的元素,因此Adorner可决定该用户操作是否需要被Adorner处理,并可以选择阻止输入事件向Adorner所修饰的界面元素传递。

该特性同样表现在点击测试这一功能上。在执行点击测试的函数调用时,如果函数探测到的是Adorner中的界面元素,那么该界面元素将被返回。当然,如果需要进行命中测试的并不是Adorner中的界面元素,那么您需要将Adorner的IsHitTestVisible属性设置为false。

反过来说,如果您希望处理Adorner中的界面元素所发出的消息,如鼠标按下的消息,那么您不能再仅仅在OnRender()函数中绘制出界面元素的外观,而需要将该界面元素真正添加到元素树中,就像AdornerSample中的ScaleAdorner一样:

  private void CreateGrip() 
  { 
      // Scaling grip 
      Rectangle rect = new Rectangle(); 
      rect.Stroke = Brushes.Blue; 
      rect.Fill = Brushes.White; 
      rect.Cursor = Cursors.SizeNWSE; 
   
      rect.MouseDown += OnGripMouseDown;
      rect.MouseUp += OnGripMouseUp;
      rect.MouseMove += OnGripMouseMove;
      AddVisualChild(rect);
      _scalingGrip = rect;
  }

在该函数中,我们创建了一个Rectangle的实例并通过AddVisualChild()函数将其添加至元素树中。同时,我们在该函数中还为Rectangle的实例添加了三个事件响应函数。通过这种方法,我们就可以在这些事件响应函数中为这些事件执行特殊的执行逻辑。如在MouseMove事件中,我们支持了在鼠标左键按下时对界面元素的大小进行更改:

 private void OnGripMouseMove(object sender, MouseEventArgs args) 
  { 
      if (args.LeftButton != MouseButtonState.Pressed) 
          return; 
   
      Rectangle rect = sender as Rectangle;
      if (rect == null || _adornedElement == null) 
          return; 
  
      Point point = args.GetPosition(_adornedElement);
      _adornedElement.Width = point.X > 0 ? point.X : 0;
      _adornedElement.Height = point.Y > 0 ? point.Y : 0;
  }
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值