WPF 图形呈现疑难解答

本主题概述 WPF 可视化层。 本主题重点讲述 WPF 模型中呈现支持的 Visual 类的角色。

本主题包括下列各节。

  • Visual 对象的角色
  • 如何使用可视化对象来生成控件
  • 可视化树
  • 可视化对象的呈现行为
  • VisualTreeHelper 类
  • 相关主题

Visual 对象的角色

Visual 类是每个 FrameworkElement 对象所派生自的基本抽象。 该类还充当在 WPF 中编写新控件的入口点。在 Win32 应用程序模型中,该类在许多方面可以被视为窗口句柄 (HWND)。

Visual 对象是一个核心 WPF 对象,它的主要角色是提供呈现支持。 用户界面控件(如 ButtonTextBox)派生自 Visual 类,并使用该类来保持它们所呈现的数据。 Visual 对象为下列功能提供支持:

  • 输出显示:呈现 Visual 对象的持久的序列化绘图内容。

  • 转换:对可视对象执行转换。

  • 剪辑:为 Visual 对象提供剪辑区域支持。

  • 命中测试:确定 Visual 对象的边界内是否包含坐标或几何形状。

  • 边界框计算:确定 Visual 对象的边框。

但是,Visual 对象不包括对非呈现功能的支持,如:

  • 事件处理

  • 布局

  • 样式

  • 数据绑定

  • 全球化

Visual 作为子类必须派生自的公共抽象类进行公开。 下图显示了 WPF 中所公开的可视化对象的层次结构。

Visual 类的层次结构

从 Visual 对象派生的类的示意图

DrawingVisual 类

DrawingVisual 是一个用于呈现形状、图像或文本的轻量绘图类。 此类之所以被视为轻量,是因为它不提供布局或事件处理功能,从而能够改善运行时性能。 因此,绘图最适于背景和剪贴画。 DrawingVisual 可用于创建自定义可视化对象。 有关更多信息,请参见使用 DrawingVisual 对象

Viewport3DVisual 类

Viewport3DVisual 在二维 VisualVisual3D 对象之间起到桥梁作用。 Visual3D 类是所有三维可视化元素的基类。 Viewport3DVisual 要求您定义一个 Camera 值和一个 Viewport 值。 可以借助照相机来查看场景。 投影映射到二维图面的区域称作视区。 有关 WPF 中三维形状的更多信息,请参见三维图形概述

ContainerVisual 类

ContainerVisual 类用作 Visual 对象集的容器。 DrawingVisual 类派生自 ContainerVisual 类,这允许它包含可视化对象的集合。

可视化对象中的绘图内容

Visual 对象将它的呈现数据另存为向量图形指令列表。 指令列表中的每一项都以序列化格式表示一组低级别的图形数据及其相关资源。 共有四种不同类型的呈现数据可以包含绘图内容。

绘图内容类型

说明

向量图形

表示向量图形数据以及任何相关的 BrushPen 信息。

Image

表示 Rect 所定义区域中的图像。

标志符号

表示用来呈现 GlyphRun(来自指定字体资源中的一系列标志符号)的绘图。 这就是文本的表示方式。

视频

表示用来呈现视频的绘图。

通过 DrawingContext,您可用可视化内容填充 Visual。 当您使用 DrawingContext 对象的绘图命令时,实际上是存储一组日后将由图形系统使用的呈现数据,而不是实时绘制到屏幕上。

当您创建 WPF 控件(如 Button)时,该控件会为绘图对象本身隐式生成呈现数据。 例如,设置 ButtonContent 属性会导致该控件存储标志符号的呈现表示。

Visual 将其内容描述为一个或多个包含在 DrawingGroup 中的 Drawing 对象。 DrawingGroup 还描述了应用于其内容的不透明度蒙版、变换、位图效果及其他操作。 呈现内容时,DrawingGroup 操作按下列顺序应用:OpacityMaskOpacityBitmapEffectClipGeometryGuidelineSetTransform

下图显示了在呈现过程中 DrawingGroup 操作的应用顺序。

DrawingGroup 操作的顺序

DrawingGroup 操作顺序

有关更多信息,请参见Drawing 对象概述

在可视化层绘制内容

绝不能直接实例化 DrawingContext;但可以通过某些方法(例如 DrawingGroup.OpenDrawingVisual.RenderOpen)获取绘图上下文。 下面的示例从 DrawingVisual 中检索 DrawingContext 并将其用于绘制矩形。

        ' Create a DrawingVisual that contains a rectangle.
        Private Function CreateDrawingVisualRectangle() As DrawingVisual
            Dim drawingVisual As New DrawingVisual()

            ' Retrieve the DrawingContext in order to create new drawing content.
            Dim drawingContext As DrawingContext = drawingVisual.RenderOpen()

            ' Create a rectangle and draw it in the DrawingContext.
            Dim rect As New Rect(New Point(160, 100), New Size(320, 80))
            drawingContext.DrawRectangle(Brushes.LightBlue, CType(Nothing, Pen), rect)

            ' Persist the drawing content.
            drawingContext.Close()

            Return drawingVisual
        End Function
// Create a DrawingVisual that contains a rectangle.
private DrawingVisual CreateDrawingVisualRectangle()
{
    DrawingVisual drawingVisual = new DrawingVisual();

    // Retrieve the DrawingContext in order to create new drawing content.
    DrawingContext drawingContext = drawingVisual.RenderOpen();

    // Create a rectangle and draw it in the DrawingContext.
    Rect rect = new Rect(new System.Windows.Point(160, 100), new System.Windows.Size(320, 80));
    drawingContext.DrawRectangle(System.Windows.Media.Brushes.LightBlue, (System.Windows.Media.Pen)null, rect);

    // Persist the drawing content.
    drawingContext.Close();

    return drawingVisual;
}

在可视化层枚举绘图内容

此外,Drawing 对象还可提供用来枚举 Visual 内容的对象模型。

注意注意

您在枚举可视化层的内容时,就是相当于在检索 Drawing 对象,而不是以向量图形指令列表形式检索呈现数据的基础表示。

下面的示例使用 GetDrawing 方法来检索 VisualDrawingGroup 值并枚举该值。

public void RetrieveDrawing(Visual v)
{
    DrawingGroup dGroup = VisualTreeHelper.GetDrawing(v);
    EnumDrawingGroup(dGroup);

}

 // Enumerate the drawings in the DrawingGroup.
 public void EnumDrawingGroup(DrawingGroup drawingGroup)
 {
     DrawingCollection dc = drawingGroup.Children;

     // Enumerate the drawings in the DrawingCollection.
     foreach (Drawing drawing in dc)
     {
         // If the drawing is a DrawingGroup, call the function recursively.
         if (drawing.GetType() == typeof(DrawingGroup))
         {
             EnumDrawingGroup((DrawingGroup)drawing);
         }
         else if (drawing.GetType() == typeof(GeometryDrawing))
         {
             // Perform action based on drawing type.  
         }
         else if (drawing.GetType() == typeof(ImageDrawing))
         {
             // Perform action based on drawing type.
         }
         else if (drawing.GetType() == typeof(GlyphRunDrawing))
         {
             // Perform action based on drawing type.
         }
         else if (drawing.GetType() == typeof(VideoDrawing))
         {
             // Perform action based on drawing type.
         }
     }
 }

如何使用可视化对象来生成控件

WPF 中的许多对象都由其他可视化对象组成,这意味着它们可以包含子代对象的各种层次结构。 WPF 中的许多用户界面元素(如控件)都由多个表示不同类型呈现元素的可视化对象组成。 例如,Button 控件可以包含许多其他对象,其中包括 ClassicBorderDecoratorContentPresenterTextBlock

下面的代码显示的是在标记中定义的 Button 控件。

<Button Click="OnClick">OK</Button>

如果您要枚举包含默认 Button 控件的可视化对象,则将发现如下所示的可视化对象层次结构:

可视化树层次结构的关系图

可视化树层次结构示意图

Button 控件包含一个 ClassicBorderDecorator 元素,该元素又包含一个 ContentPresenter 元素。 ClassicBorderDecorator 元素负责为 Button 绘制边框和背景。 ContentPresenter 元素负责显示 Button 的内容。 在本例中,由于您要显示文本,因此 ContentPresenter 元素中包含一个 TextBlock 元素。 Button 控件使用 ContentPresenter,这意味着该控件的内容可以由其他元素(如 Image)或几何形状(如 EllipseGeometry)来表示。

控件模板

将控件扩展为控件层次结构的关键在于 ControlTemplate。 控件模板为控件指定默认的可视化层次结构。 当您显式引用某个控件时,会隐式引用它的可视化层次结构。 您可以重写控件模板的默认值,以便为控件创建自定义的可视化外观。 例如,您可以修改 Button 控件的背景颜色值,以便它使用线性渐变颜色值,而不使用纯色值。 有关更多信息,请参见 Button 样式和模板

用户界面元素(如 Button 控件)包含几个向量图形指令列表,这些列表描述控件的全部呈现定义。 下面的代码显示的是在标记中定义的 Button 控件。

<Button Click="OnClick">
  <Image Source="images\greenlight.jpg"></Image>
</Button>

如果您要枚举包含 Button 控件的可视化对象和向量图形指令列表,则将发现如下所示的可视化对象层次结构:

可视化树和呈现数据的关系图

可视化树和呈现数据示意图

Button 控件包含一个 ClassicBorderDecorator 元素,该元素又包含一个 ContentPresenter 元素。 ClassicBorderDecorator 元素负责绘制所有构成按钮边框和背景的离散图形元素。 ContentPresenter 元素负责显示 Button 的内容。 在本例中,由于您要显示图像,因此 ContentPresenter 元素中包含一个 Image 元素。

对于可视化对象和向量图形指令列表的层次结构,需要注意多个事项:

  • 该层次结构中的排序表示绘图信息的呈现顺序。 从可视化元素的根,按照从左到右、从上到下的顺序遍历子元素。 如果某个元素有可视化子元素,则会先遍历该元素的子元素,然后再遍历该元素的同级。

  • 层次结构中的非叶节点元素(如 ContentPresenter)用于包含子元素,它们并不包含指令列表。

  • 如果可视化元素既包含向量图形指令列表又包含可视化子级,则会先呈现父级可视化元素中的指令列表,然后再呈现任何可视化子对象中的绘图。

  • 向量图形指令列表中的项按照从左到右的顺序呈现。

可视化树

可视化树中包含某个应用程序的用户界面所使用的所有可视化元素。 由于可视化元素中包含持久的绘图信息,因此您可以将可视化树视为场景图,其中包含将输出写入显示设备所必需的全部呈现信息。 该树汇集了由该应用程序在代码或标记中直接创建的所有可视化元素。 该可视化树还包含由元素(如控件和数据对象)的模板扩展功能创建的所有可视化元素。

下面的代码显示的是在标记中定义的 StackPanel 元素。

<StackPanel>
  <Label>User name:</Label>
  <TextBox />
  <Button Click="OnClick">OK</Button>
</StackPanel>

如果您要枚举包含标记示例中 StackPanel 元素的可视化对象,将发现如下所示可视化对象的层次结构:

可视化树层次结构的关系图

可视化树层次结构示意图

呈现顺序

通过可视化树,可以确定 WPF 可视化对象和绘图对象的呈现顺序。 将从位于可视化树中最顶层节点中的可视化元素根开始遍历, 然后将按照从左到右的顺序遍历可视化元素根的子级。 如果某个可视化元素有子级,则将先遍历该可视化元素的子级,然后再遍历其同级。 这意味着子可视化元素的内容先于该可视化元素本身的内容而呈现。

可视化树呈现顺序的关系图

可视化树呈现顺序示意图

可视化元素根

可视化元素根是可视化树层次结构中最顶层的元素。 在大多数应用程序中,可视化元素根的基类是 WindowNavigationWindow。 但是,如果您在 Win32 应用程序中承载可视化对象,则可视化元素根将是在 Win32 窗口中承载的最顶层的可视化元素。 有关更多信息,请参见 教程:在 Win32 应用程序中承载 Visual 对象

与逻辑树的关系

WPF 中的逻辑树表示应用程序在运行时的元素。 尽管您不直接操作该树,但是该应用程序视图对于了解属性继承和事件路由非常有用。 与可视化树不同,逻辑树可以表示非可视化数据对象(如 ListItem)。 在许多情况下,逻辑树密切映射到应用程序的标记定义。 下面的代码显示的是在标记中定义的 DockPanel 元素。

<DockPanel>
  <ListBox>
    <ListBoxItem>Dog</ListBoxItem>
    <ListBoxItem>Cat</ListBoxItem>
    <ListBoxItem>Fish</ListBoxItem>
  </ListBox>
  <Button Click="OnClick">OK</Button>
</DockPanel>

如果您要枚举包含标记示例中 DockPanel 元素的逻辑对象,则将发现如下所示逻辑对象的层次结构:

逻辑树的关系图

树示意图

可视化树和逻辑树与当前的应用程序元素集合同步,并反映对元素进行的任何添加、删除或修改。 但是,这些树表示不同的应用程序视图。 与可视化树不同,逻辑树不展开控件的 ContentPresenter 元素。 这意味着同一组对象的逻辑树和可视化树之间没有直接的一对一对应关系。 实际上,在将同一个元素用作参数的情况下,调用 LogicalTreeHelper 对象的 GetChildren 方法与调用 VisualTreeHelper 对象的 GetChild 方法会生成不同的结果。

有关逻辑树的更多信息,请参见 WPF 中的树

使用 XamlPad 查看可视化树

WPF 工具 (XamlPad) 提供了一个用来查看和浏览可视化树的选项,该树与当前所定义的 XAML 内容相对应。 单击菜单栏上的**“显示可视化树”按钮可显示相应的可视化树。 下面将说明如何在 XamlPad 的“可视化树资源管理器”**面板中将 XAML 内容扩展为可视化树节点:

XamlPad 中的“可视化树资源管理器”面板

XamlPad 中的“可视化树资源管理器”面板

请注意 LabelTextBoxButton 控件如何在 XamlPad 的**“可视化树资源管理器”**面板中各自显示一个可视化对象层次结构。 这是由于 WPF 控件具有一个包含其可视化树的 ControlTemplate。 当您显式引用某个控件时,会隐式引用它的可视化层次结构。

分析可视化性能

WPF 提供了一套性能分析工具,来帮助您分析应用程序的运行时行为,并确定可以应用的性能优化的类型。 可视化探查器工具通过直接映射到应用程序的可视化树来为性能数据提供一个丰富的图形视图。 在此屏幕快照中,可视化探查器的**“CPU Usage”**部分可使您可以清楚地了解对象对 WPF 服务(如呈现和布局)的使用情况。

可视化探查器显示输出

可视化探查器显示输出

可视化对象的呈现行为

WPF 引进了几个影响可视化对象呈现行为的功能:保留的模式图形、矢量图形和与设备无关的图形。

保留的模式图形

了解即时模式保留模式图形系统之间的区别是了解 Visual 对象角色的要点之一。 基于 GDI 或 GDI+ 的标准 Win32 应用程序使用即时模式图形系统。 这意味着应用程序负责重新绘制工作区中由于某项操作(如调整窗口大小)或者对象的可视化外观发生变化而失效的部分。

Win32 呈现顺序的关系图

Win32 呈现序列示意图

与之相比,WPF 使用保留模式系统。 这意味着具有可视化外观的应用程序对象定义一组序列化绘图数据。 在定义了绘图数据之后,系统会响应所有的重新绘制请求来呈现应用程序对象。 甚至在运行时,您也可以修改或创建应用程序对象,并仍旧依赖系统响应绘制请求。 保留模式图形系统中有一个强大功能,那就是绘图信息总是由应用程序保持为序列化状态,但是呈现功能仍由系统负责。 下面的关系图演示应用程序如何依赖 WPF 来响应绘制请求。

WPF 呈现顺序的关系图

WPF 呈现序列示意图

智能重绘

使用保留模型图形的最大好处之一就是,WPF 可以高效率地优化需要在应用程序中重绘的内容。 即使您有一个具有各种不透明度的复杂场景,通常也不必编写特殊用途的代码来优化重绘功能。 请将智能重绘功能与 Win32 编程进行比较,在后者中,可以通过最小化更新区域中的重绘量来尽力优化应用程序。 有关在 Win32 应用程序中优化重绘功能时涉及到的复杂度类型的示例,请参见在更新区域中重绘

向量图形

WPF 使用向量图形作为其呈现数据的格式。 向量图形(包括可缩放的向量图形 (SVG)、Windows 元文件 (.wmf) 和 TrueType 字体)存储呈现数据,并以指令列表的形式传输该呈现数据,这些指令描述如何使用图形基元来重新创建图像。 例如,TrueType 字体是描述一组直线、曲线和命令(而不是像素数组)的矢量字。 矢量图形的主要好处之一就是能够伸缩到任何大小和分辨率。

与矢量图形不同,位图图形以图像的逐像素表示形式来存储呈现数据,而且在特定的分辨率下预先呈现。 位图图形格式和矢量图形格式的主要区别之一就是对原始图像的保真度。 例如,当某个源图像的大小发生变化时,位图图形系统会拉伸该图像,而向量图形系统会伸缩该图像,从而保持图像的保真度。

下图显示了源图像在放大到 3 倍时的情况。 请注意,当源图像作为位图图形拉伸时会发生失真,而当源图像作为矢量图形伸缩时,则不会发生失真。

光栅图形和矢量图形之间的区别

光栅图与矢量图之间的区别

下面的标记显示所定义的两个 Path 元素。 第二个元素使用 ScaleTransform 将第一个元素的绘图指令放大到 3 倍。 请注意 Path 元素中的绘图指令保持不变。

<Path
  Data="M10,100 C 60,0 100,200 150,100 z"
  Fill="{StaticResource linearGradientBackground}"
  Stroke="Black"
  StrokeThickness="2" />

<Path
  Data="M10,100 C 60,0 100,200 150,100 z"
  Fill="{StaticResource linearGradientBackground}"
  Stroke="Black"
  StrokeThickness="2" >
  <Path.RenderTransform>
    <ScaleTransform ScaleX="3.0" ScaleY="3.0" />
  </Path.RenderTransform>
</Path>

关于分辨率和与设备无关的图形

可通过以下两个系统因子来确定屏幕上的文本大小和图形大小:分辨率和 DPI。 分辨率描述出现在屏幕上的像素数量。 由于分辨率变大,因此像素会变小,从而导致所显示的图形和文本会变小。 在将显示器的分辨率从 1024 x 768 更改为 1600 x 1200 时,显示器上所显示的图形会小得多。

另一个系统设置 (DPI) 以像素数来描述屏幕英寸的大小。 大多数 Windows 系统的 DPI 都为 96,这意味着一屏幕英寸等于 96 个像素。 增加 DPI 设置会使屏幕英寸变大,减小 DPI 会使屏幕英寸变小。 这意味着屏幕英寸与实际的英寸不相等;在多数系统上,二者很有可能不相等。 当您增加 DPI 时,屏幕英寸会变大,因此支持 DPI 的图形和文本也会变大。 增加 DPI 可能会增强文本的可读性,在高分辨率下尤其如此。

并非所有的应用程序都支持 DPI:一些应用程序将硬件像素作为其主要计量单位;更改系统 DPI 不会对这些应用程序产生任何影响。 许多其他应用程序都使用支持 DPI 的单位来描述字号,使用像素来描述任何其他内容。 DPI 太小或太大都可能会导致这些应用程序出现布局问题,因为应用程序的文本会随着系统的 DPI 设置而伸缩,而应用程序的 UI 却不会出现此类问题。 对于使用 WPF 开发的应用程序,此问题已经消除。

WPF 支持通过将与设备无关的像素(而不是硬件像素)用作其主要计量单位来自动伸缩;图像和文本会适当伸缩,而无需应用程序开发人员执行任何额外的工作。 下图显示了 WPF 文本和图形在不同 DPI 设置下的显示方式的示例。

不同 DPI 设置下的图形和文本

采用不同 DPI 设置的图形和文本

VisualTreeHelper 类

VisualTreeHelper 类是一个静态帮助器类,它提供了一个要在可视化对象级别编程的低级功能,该类在非常特殊的方案(如开发高性能自定义控件)中非常有用。 在大多数情况下,更高级的 WPF 框架对象(如 CanvasTextBlock)提供更大的灵活性且更易于使用。

命中测试

VisualTreeHelper 类提供了当默认的命中测试支持无法满足您的需要时,针对可视化对象的命中测试方法。 可以在 VisualTreeHelper 类中使用 HitTest 方法来确定几何形状或点坐标值是否位于给定对象(如控件或图形元素)的边界内。 例如,您可以使用命中测试来确定鼠标在对象边框中的单击点是否落在圆形几何形状内部。您还可以选择重写对命中测试的默认实现来执行自己的自定义命中测试计算。

有关命中测试的更多信息,请参见可视化层中的命中测试

枚举可视化树

VisualTreeHelper 类提供了用来枚举可视化树成员的功能。 若要检索父级,请调用 GetParent 方法。 若要检索可视化对象的子级或直接子代,请调用 GetChild 方法。 此方法返回父级在指定索引处的子 Visual

下面的示例演示如何枚举一个可视化对象的所有子代,如果您对序列化可视化对象层次结构的所有呈现信息感兴趣,则可能希望使用该技术。

        ' Enumerate all the descendants of the visual object.
        Public Shared Sub EnumVisual(ByVal myVisual As Visual)
            For i As Integer = 0 To VisualTreeHelper.GetChildrenCount(myVisual) - 1
                ' Retrieve child visual at specified index value.
                Dim childVisual As Visual = CType(VisualTreeHelper.GetChild(myVisual, i), Visual)

                ' Do processing of the child visual object.

                ' Enumerate children of the child visual object.
                EnumVisual(childVisual)
            Next i
        End Sub
// Enumerate all the descendants of the visual object.
static public void EnumVisual(Visual myVisual)
{
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(myVisual); i++)
    {
        // Retrieve child visual at specified index value.
        Visual childVisual = (Visual)VisualTreeHelper.GetChild(myVisual, i);

        // Do processing of the child visual object.

        // Enumerate children of the child visual object.
        EnumVisual(childVisual);
    }
}

在大多数情况下,逻辑树更能表示 WPF 应用程序中的元素。 尽管您不直接修改逻辑树,但是该应用程序视图对于了解属性继承和事件路由非常有用。 与可视化树不同,逻辑树可以表示非可视化数据对象(如 ListItem)。 有关逻辑树的更多信息,请参见 WPF 中的树

VisualTreeHelper 类提供用来返回可视化对象边框的方法。 可以通过调用 GetContentBounds 来返回可视化对象的边框。 可以通过调用 GetDescendantBounds 来返回可视化对象及其所有子代的边框。 下面的代码演示如何计算可视化对象及其所有子代的边框。

            ' Return the bounding rectangle of the parent visual object and all of its descendants.
            Dim rectBounds As Rect = VisualTreeHelper.GetDescendantBounds(parentVisual)
// Return the bounding rectangle of the parent visual object and all of its descendants.
Rect rectBounds = VisualTreeHelper.GetDescendantBounds(parentVisual);

请参见

参考

Visual

VisualTreeHelper

DrawingVisual

概念

优化性能:二维图形和图像处理

可视化层中的命中测试

使用 DrawingVisual 对象

教程:在 Win32 应用程序中承载 Visual 对象

优化 WPF 应用程序性能