zoukankan      html  css  js  c++  java
  • WPF,Silverlight与XAML读书笔记第三十六 可视化效果之Visual

    说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。

    Visual与其它可视化类之间的继承关系如下图:

    其包含了绘制任何东西到屏幕上的底层构造,前面章节我们通过Image将Drawing绘制到屏幕上,Image派生自FrameworkElement,继承层次中多了UIElement与FrameworkElement,它们提供了一些如样式,数据绑定,资源,鼠标支持,路由事件支持等Drawing不需要的特性。如果要绘制大量Drawing到屏幕(如实现横屏滚轴效果),宿主于一个Image会导致很大不必要的开销。

        解决方法是使用WPF提供的DrawingVisual这个Visual的子类,其提供了轻量级方法用于将Drawing渲染到屏幕上。虽然DrawingVisual很轻量,其仍然提供了像透明和剪切这样的渲染效果。另外,通过Visual命中测试还可以实现与输入设备的基本交互。

    下面我们将分三部分来做详细阐述:

    • DrawingVisual的填充
    • DrawingVisual的呈现
    • Visual的命中测试

    DrawingVisual的填充

        我们需要调用DrawingVisual的RenderOpen方法得到一个DrawingContent的实例,通过这个对象进行绘制,完成后调用Close方法关闭这个对象。(另外注意,DrawingVisual的Drawing属性是只读的,不能通过设置这个属性完成填充。)

        下面是一段典型的填充DrawingVisual的代码:

    1 GeometryDrawing drawing = new GeometryDrawing();
    2 DrawingVisual visual = new DrawingVisual();
    3 using (DrawingContext dc = visual.RenderOpen())
    4 {
    5     dc.DrawDrawing(drawing);
    6 }

    这段代码之所以可以实现,因为DrawingContext实现了IDisposal接口,并且在Dispose方法中调用了Close方法。

        另外DrawingContext上DrawDrawing(或其它DrawXXX方法)调用的顺序可以直接将z-order的效果体现出来。DrawingContext上提供了一系列DrawingXXX方法,覆盖了Drawing类(GeometryDrawing,ImageDrawing,VideoDrawing和GlyphRunDrawing)提供的所有功能(绘制几何体,图像,文本甚至视频)。

        下面表格中列出了DrawingContext提供的方法

    DrawRectangle, DrawRoundedRectangle,

    DrawingEllipse,DrawLine

    绘制一些定制的图形(,而无须通过Geometry系列)

    DrawGeometry,DrawImage,DrawVideo,

    DrawGlyphRun,DrawText

    无须通过Drawing实例绘制Drawing

    DrawDrawing

    前文示例中使用的方法,通过Drawing实例绘制Drawing

    PushClip,PushEffect,

    PushGuidelineSet,PushOpacity,

    PushOpacityMask,PushTransform,Pop

    像绘画应用效果

    Close

    结束绘画命令序列

    上表中PushXXX系列与Pop命令用于将透明或旋转等应用到一系列命令上,命令还可以进行嵌套。如PushClip用于设置剪切区域,PushEffect用于应用BitmapEffect效果,Push方法会一直将效果向后续对象作用,直到使用Pop方法移除效果。(.NET Framework4.0中过时)

        DrawingContext类是最接近Win32(GDI)的类,所以其性能最高,但需注意的是,其仍然工作于保留模式(而非立即模式)中,所以绘制的图形直到实际用到才会显示。

    Visual的呈现

    对于是UIElement的Visual,只需要将其设置给ContentControl的Content属性即可在OnRender中被正确的渲染。对于一个不是UIElement的Visual(如DrawingVisual),需要将它们手工添加到某些UIElement的Visual树上(否则只会呈现出Visual对象执行ToString()的结果)。

        我们可以在一个顶层的UIElement中设置几千个Visual(而不是在其中放置几千个UIElment),从而可以大大提高应用程序的性能。

        将Visual添加到元素的方法是,自定义一个派生自UIElement的类,重写两个基类中protected virtual标记的成员函数:VisualChildrenCount和GetVisualChild。下面是一个自定义类的例子,这个例子中我们把Visual宿主到Window这个UIElement元素中:

     1 using System;
     2 using System.Windows;
     3 using System.Windows.Media;
     4 
     5 public class WindowHostingVisual : Window
     6 {
     7     DrawingVisual visual = null;
     8 
     9     public WindowHostingVisual()
    10     {
    11         Title = "Render by DrawingVisuals";
    12 
    13         visual = new DrawingVisual();
    14         using (DrawingContext dc = visual.RenderOpen())
    15         {
    16             //draw
    17         }
    18 
    19         AddVisualChild(visual);
    20         AddLogicalChild(visual);
    21     }
    22 
    23     //必须重写的方法
    24     protected override int VisualChildrenCount
    25     {
    26         get { return 1; }
    27     }
    28 
    29     protected override Visual GetVisualChild(int index)
    30     {
    31         if (index != 0)
    32             throw new ArgumentOutOfRangeException("index");
    33 
    34         return visual;
    35     }
    36 }

    如代码所示,VisualChildrenCount必须返回Window中Visual的个数,这个例子中此值为1。GetVisualChild方法通过索引值返回一个Visual对象,这里只有一个索引值,所以接收的索引值只能是0。

    注意,重写VisualChildrenCount与GetVisual方法会使Window的Content属性不再被绘制。另外,在构造函数中调用的AddVisualChild(定义于Visual类中)和AddLogicalChild(定义于FrameworkElement类中)方法将Visual分别注册到可视树与逻辑树,这样路由事件,命中测试和属性继承等才能正常工作。RemoveVisualChild和RemoveLogicalChild可以移除Visual注册。

    Visual命中测试

    命中测试指的是判断一个点(或一组点)是否与一个给定的对象相交。可视命中测试被所有的Visual对象支持。(由UIElement支持的输入命中测试

        Visual没有UIElement中所提供的输入事件(如MouseLeftButtonDown,MouseMove),要想让Visual也可以响应鼠标操作,需要通过处理所宿主的UIElement的鼠标事件,并在处理函数中通过Visual命中测试来确定鼠标动作是否与一个Visual相关,在相关情况下让Visual响应事件完成定义的操作。

    VisualTreeHelper.HitTest方法用于支持命中测试,其最简单的重载接受Visual根对象和要测试的坐标点(相对根元素的坐标)。该方法以根对象起向下遍历可视树,其返回值包含了被命中的最上层的Visual对象。下面是一段示例代码:

     1 protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
     2 {
     3     base.OnMouseLeftButtonDown(e);
     4 
     5     Point location = e.GetPosition(this);
     6         
     7     //对Window进行Visual命中测试
     8     HitTestResult result = VisualTreeHelper.HitTest(this, location);
     9 
    10     if (result.VisualHit == visual)
    11     {
    12         if (visual.Transform == null)
    13         {
    14             visual.Transform = new RotateTransform();
    15         }
    16         (visual.Transform as RotateTransform).Angle++;
    17     }
    18 }

    下面是一个综合示例,其中演示了将多个Visual添加到UIElement并管理它们,以及对多个Visual执行命中测试。

     1 using System;
     2 using System.Windows;
     3 using System.Windows.Media;
     4 using System.Collections.Generic;
     5 using System.Windows.Input;
     6 
     7 public class WindowHostingVisual : Window
     8 {
     9     List<Visual> visuals = new List<Visual>();
    10 
    11     public WindowHostingVisual()
    12     {
    13         Title = "绘制多个Visual";
    14         Width = 300;
    15         Height = 350;
    16 
    17         DrawingVisual bodyVisual = new DrawingVisual();
    18         DrawingVisual eyesVisual = new DrawingVisual();
    19         DrawingVisual mouthVisual = new DrawingVisual();
    20 
    21         using (DrawingContext dc = bodyVisual.RenderOpen())
    22         {
    23             // The body
    24             dc.DrawGeometry(Brushes.Blue, null, Geometry.Parse(
    25             @"M 240,250
    26               C 200,375 200,250 175,200
    27               C 100,400 100,250 100,200
    28               C 0,350 0,250 30,130
    29               C 75,0 100,0 150,0
    30               C 200,0 250,0 250,150 Z"));
    31         }
    32         using (DrawingContext dc = eyesVisual.RenderOpen())
    33         {
    34             // Left eye
    35             dc.DrawEllipse(Brushes.Black, new Pen(Brushes.White, 10),
    36               new Point(95, 95), 15, 15);
    37             // Right eye
    38             dc.DrawEllipse(Brushes.Black, new Pen(Brushes.White, 10),
    39              new Point(170, 105), 15, 15);
    40         }
    41         using (DrawingContext dc = mouthVisual.RenderOpen())
    42         {
    43             // The mouth
    44             Pen p = new Pen(Brushes.Black, 10);
    45             p.StartLineCap = PenLineCap.Round;
    46             p.EndLineCap = PenLineCap.Round;
    47             dc.DrawLine(p, new Point(75, 160), new Point(175, 150));
    48         }
    49 
    50         visuals.Add(bodyVisual);
    51         visuals.Add(eyesVisual);
    52         visuals.Add(mouthVisual);
    53 
    54         // 注册到可视树,逻辑树
    55         foreach (Visual v in visuals)
    56         {
    57             AddVisualChild(v);
    58             AddLogicalChild(v);
    59         }
    60     }
    61 
    62     protected override int VisualChildrenCount
    63     {
    64         get { return visuals.Count; }
    65     }
    66     protected override Visual GetVisualChild(int index)
    67     {
    68         if (index < 0 || index >= visuals.Count)
    69             throw new ArgumentOutOfRangeException("index");
    70         return visuals[index];
    71     }
    72 
    73     protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    74     {
    75         base.OnMouseLeftButtonDown(e);
    76 
    77         Point location = e.GetPosition(this);
    78         // Visual命中测试
    79         HitTestResult result = VisualTreeHelper.HitTest(this, location);
    80 
    81         if (result.VisualHit.GetType() == typeof(DrawingVisual))
    82         {
    83             DrawingVisual dv = result.VisualHit as DrawingVisual;
    84             if (dv.Transform == null)
    85                 dv.Transform = new RotateTransform();
    86             (dv.Transform as RotateTransform).Angle++;
    87         }
    88     }
    89 }

    提示:

    DrawingVisual派生自VisualGroup,由于VisualGroup用类似上文综合示例中的代码对VisualChildrenCount与GetVisualChild进行了重载,呈现及命中测试会对所有添加到VisualGroup的子对象起作用。所以,对于上面的例子,我们可以将后两个DrawingVisual(即eyesVisual,mouthVisual)作为子对象添加到第一个DrawingVisual(即bodyVisual)中,然后将bodyVisual关联到Window(UIElement)上即可,DrawingVisual(bodyVisual)会处理剩余工作。 

    对重叠的Visual进行命中测试

        实现这个目标,需要调用HitTest一个接受HittestResultCallback委托的重载,在HitTest返回之前,对每个命中测试成功的Visual会按最顶层到最底层调用该委托。参考下面这段示例代码:

     1 public HitTestResultBehavior HitTestCallback(HitTestResult result)
     2 {
     3     if (result.VisualHit.GetType()==typeof(DrawingVisual))
     4     {
     5         DrawingVisual dv = result.VisualHit as DrawingVisual;
     6         if (dv.Transform == null)
     7         {
     8             dv.Transform = new RotateTransform();
     9         }
    10         (dv.Transform as RotateTransform).Angle++;
    11     }
    12     //继续寻找命中
    13     return HitTestResultBehavior.Continue;
    14 }

    如代码所示,这里最大的不同是,HitTest函数不再返回任何值处理。HitResult的逻辑放在了回调方法中。回调方法返回HitTestResultBehavior枚举两个值之一:Continue或者Stop。从而可以在任何时候停止捕获剩余的Visual对象。代码中第二个参数接收HitTestFilterCallback类型的委托,可以实现在不停止处理的情况下跳过某些Visual的处理。另外注意这个重载方法没有像前面示例那样接收一个Point对象,而是一个包含Point的PointHitTestParameters对象。PointHitTestParameters类是HitTestParameters类(重载方法接收的参数类型)的子类,该类另一个子类是GeometryHitTestParameters,用于对区域进行命中测试。

    注意:

    不要在回调函数中修改Visual树,否则会导致不正确的结果。应该在回调过程中记录相关信息,并在HitTest返回(可以确保此时所有回调函数已返回)之后再修改。

    本文完

    参考:

    《WPF揭秘》

  • 相关阅读:
    python中的有趣用法
    python计算程序运行时间
    python OptionParser模块
    优酷界面全新改版
    python数值计算模块NumPy scipy安装
    IOS开发-通知与消息机制
    四川大学线下编程比赛第一题:数字填充
    矩形旋转碰撞,OBB方向包围盒算法实现
    【Cocos2d-x 粒子系统】火球用手指飞起来
    它们的定义AlertDialog(二)
  • 原文地址:https://www.cnblogs.com/lsxqw2004/p/4629230.html
Copyright © 2011-2022 走看看