WPF 图形呈现疑难解答

本主题概述 WPF 可视化层。 其中重点介绍 Visual 类在 WPF 模型中对呈现支持的作用。

视觉对象的角色

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

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

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

  • 转换:针对视觉对象执行转换。

  • 剪裁:为视觉对象提供剪裁区域支持。

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

  • 边框计算:确定视觉对象的边框。

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

  • 事件处理

  • Layout

  • 样式

  • 数据绑定

  • 全球化

Visual 作为公共抽象类公开,子类必须从其中派生。 下图显示了 WPF 中公开的视觉对象的层次结构。

Diagram of classes derived from the Visual object

DrawingVisual 类

DrawingVisual 是一个轻量绘图类,用于呈现形状、图像或文本。 此类之所以为轻量类是因为它不提供布局或事件处理,从而提升其运行时性能。 因此,绘图非常适用于背景和剪贴画。 DrawingVisual 可用于创建自定义视觉对象。 有关详细信息,请参阅使用 DrawingVisual 对象

Viewport3DVisual 类

Viewport3DVisual 在 2D VisualVisual3D 对象之间起到桥梁作用。 Visual3D 类是所有 3D 可视化元素的基类。 Viewport3DVisual 要求定义一个 Camera 值和一个 Viewport 值。 借助照相机,可以查看场景。 视区确定投影映射到 2D 图面的位置。 有关 WPF 中 3D 的详细信息,请参阅 3D 图形概述

ContainerVisual 类

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

视觉对象中的绘图内容

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

绘图内容类型 说明
矢量图形 表示矢量图形数据以及任何相关的 BrushPen 信息。
映像 表示 Rect 所定义区域内的图像。
标志符号 表示用来呈现 GlyphRun(来自指定字体资源的一系列字形)的绘图。 这是文本的表示方式。
视频 表示用于呈现视频的绘图。

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

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

Visual 将其内容描述为一个或多个包含在 DrawingGroup 内的 Drawing 对象。 DrawingGroup 还描述了应用于其内容的不透明蒙板、转换、位图效果以及其他操作。 呈现内容时,DrawingGroup 操作按下列顺序应用:OpacityMaskOpacityBitmapEffectClipGeometryGuidelineSet,最后是 Transform

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

DrawingGroup order of operations
DrawingGroup 操作的顺序

有关详细信息,请参阅 Drawing 对象概述

可视化层中的绘图内容

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

// 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;
}
' 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

在可视化层中枚举绘图内容

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

注意

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

以下示例使用 GetDrawing 方法检索 VisualDrawingGroup 值并枚举它。

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

// 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 is DrawingGroup group)
        {
            EnumDrawingGroup(group);
        }
        else if (drawing is GeometryDrawing)
        {
            // Perform action based on drawing type.
        }
        else if (drawing is ImageDrawing)
        {
            // Perform action based on drawing type.
        }
        else if (drawing is GlyphRunDrawing)
        {
            // Perform action based on drawing type.
        }
        else if (drawing is VideoDrawing)
        {
            // Perform action based on drawing type.
        }
    }
}

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

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

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

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

如果要枚举包含默认 Button 控件的视觉对象,将看到如下所示的视觉对象层次结构:

Diagram of visual tree hierarchy

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

控件模板

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

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

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

如果要枚举包含 Button 控件的视觉对象和矢量图形指令列表,将看到如下所示的对象层次结构:

Diagram of visual tree and rendering data

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

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

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

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

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

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

可视化树

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

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

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

如果要枚举标记示例中包含 StackPanel 元素的视觉对象,将看到如下所示的视觉对象层次结构:

Diagram of visual tree hierarchy of a StackPanel control.

呈现顺序

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

Diagram of the visual tree rendering order

可视化元素根

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

与逻辑树的关系

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

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

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

Tree diagram
逻辑树关系图

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

有关逻辑树的详细信息,请参阅 WPF 中的树

使用 XamlPad 查看可视化树

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

Visual Tree Explorer panel in XamlPad

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

分析可视化性能

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

Visual Profiler display output
可视化探查器显示输出

视觉对象的呈现行为

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

保留的模式图形

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

Diagram of Win32 rendering sequence

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

Diagram of WPF rendering sequence

智能重绘

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

矢量图形

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

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

下图显示了其大小重设为 300% 的源图像。 请注意,当源图像作为位图图形图像拉伸而不是作为矢量图形图像缩放时会发生失真。

Differences between raster and vector graphics

以下标记显示所定义的两个 Path 元素。 第二个元素使用 ScaleTransform 将第一个元素的绘图指令大小调整为 300%。 请注意,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 设置进行显示的示例。

Graphics and text at different DPI settings
采用不同 DPI 设置的图形和文本

VisualTreeHelper 类

VisualTreeHelper 类是一个静态帮助程序类,用于在视觉对象级别为编程提供低级别的功能,这在非常具体的方案(如开发高性能的自定义控件)中很有用。 在大多数情况下,更高级别的 WPF 框架对象(如 CanvasTextBlock)提供了更大的灵活性和易用性。

命中测试

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

有关命中测试的详细信息,请参阅可视化层中的命中测试

枚举可视化树

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

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

// 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);
    }
}
' 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

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

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

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

另请参阅