WPF 图形呈现概述
本主题概述了 WPF 视觉层。 其中重点介绍 Visual 类在 WPF 模型中对呈现支持的作用。
视觉对象的角色
Visual 类是每个 FrameworkElement 对象派生的基本抽象。 它还充当在 WPF 中编写新控件的入口点,并且可通过多种方式视为 Win32 应用程序模型中的窗口句柄(HWND)。
Visual 对象是核心 WPF 对象,其主要角色是提供呈现支持。 用户界面控件(如 Button 和 TextBox)派生自 Visual 类,并使用它来保存其呈现数据。 Visual 对象为以下项提供支持:
输出显示:呈现视觉对象的持久、序列化的绘图内容。
转换:针对视觉对象执行转换。
剪裁:为视觉对象提供剪裁区域支持。
命中测试:确定坐标或几何形状是否包含在视觉对象的边界内。
边框计算:确定视觉对象的边框。
但是,Visual 对象不包括对非呈现功能的支持,例如:
事件处理
布局
样式
数据绑定
全球化
Visual 作为公共抽象类公开,子类必须从其中派生。 下图显示了 WPF 中公开的视觉对象的层次结构。
从 Visual 对象 派生的类的
DrawingVisual 类
DrawingVisual 是用于呈现形状、图像或文本的轻型绘图类。 此类被视为轻量级,因为它不提供布局或事件处理,从而提高其运行时性能。 因此,绘图非常适合背景和剪贴画。 DrawingVisual 可用于创建自定义视觉对象。 有关详细信息,请参阅使用 DrawingVisual 对象。
Viewport3DVisual 类
Viewport3DVisual 提供 2D Visual 和 Visual3D 对象之间的桥梁。 Visual3D 类是所有 3D 视觉元素的基类。 Viewport3DVisual 要求定义一个 Camera 值和一个 Viewport 值。 相机允许你查看场景。 视区确定投影映射到 2D 图面的位置。 有关 WPF 中的 3D 的详细信息,请参阅 3D 图形概述。
ContainerVisual 类
ContainerVisual 类用作 Visual 对象的集合的容器。 DrawingVisual 类派生自 ContainerVisual 类,允许它包含视觉对象的集合。
在视觉对象中绘制内容
Visual 对象将其呈现数据存储为 矢量图形指令列表。 指令列表中的每个项都以序列化格式表示一组低级别的图形数据和关联的资源。 有四种不同类型的呈现数据可以包含绘图内容。
绘图内容类型 | 描述 |
---|---|
矢量图形 | 表示矢量图形数据以及任何关联的 Brush 和 Pen 信息。 |
图像 | 表示 Rect 所定义区域内的图像。 |
标志符号 | 表示用来呈现 GlyphRun(来自指定字体资源的一系列字形)的绘图。 这就是文本的表示方式。 |
视频 | 表示用于呈现视频的绘图。 |
通过 DrawingContext,可使用可视化内容填充 Visual。 使用 DrawingContext 对象的绘图命令时,实际上是在存储一组呈现数据,这些指令将由图形系统稍后使用;你不会实时绘制到屏幕。
创建 WPF 控件(如 Button)时,该控件会为绘图本身隐式生成呈现数据。 例如,设置 Button 的 Content 属性会导致该控件存储字形的呈现表示。
Visual 将其内容描述为包含在 DrawingGroup中的一个或多个 Drawing 对象。 DrawingGroup 还描述了应用于其内容的不透明蒙板、转换、位图效果以及其他操作。 呈现内容时,将按以下顺序应用 DrawingGroup 操作:OpacityMask、Opacity、BitmapEffect、ClipGeometry、GuidelineSet,然后 Transform。
下图插图显示了在呈现序列期间应用 DrawingGroup 操作的顺序。
DrawingGroup 操作的顺序
有关详细信息,请参阅 绘图对象概述。
可视化层中的绘图内容
你从不直接实例化 DrawingContext;但是,可以从某些方法(如 DrawingGroup.Open 和 DrawingVisual.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 方法检索 Visual 的 DrawingGroup 值并枚举它。
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 控件可以包含许多其他对象,包括 ClassicBorderDecorator、ContentPresenter和 TextBlock。
以下代码展示了在标记语言中定义的 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 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 视觉对象和绘图对象的呈现顺序。 将从位于可视化树中最顶层节点中的可视化元素根开始遍历。 然后,将按照从左到右的顺序遍历可视化元素根的子级。 如果可视化元素有子级,则将先遍历该可视化元素的子级,然后再遍历其同级。 这意味着子可视化元素的内容先于该可视化元素本身的内容呈现。
可视化元素根
根视觉对象 是可视化树层次结构中最顶层的元素。 在大多数应用程序中,根视觉对象的基类是 Window 或 NavigationWindow。 但是,如果你在 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 元素的逻辑对象,则会找到下面所示的逻辑对象的层次结构:
逻辑树图
可视化树和逻辑树都与当前应用程序元素集同步,反映元素的任何添加、删除或修改。 但是,树显示了应用程序的不同视图。 与可视化树不同,逻辑树不会扩展控件的 ContentPresenter 元素。 这意味着逻辑树和同一组对象的可视化树之间没有直接的一对一对应关系。 事实上,调用 LogicalTreeHelper 对象的 GetChildren 方法和 VisualTreeHelper 对象的 GetChild 方法时,使用与参数相同的元素生成不同的结果。
有关逻辑树的详细信息,请参阅 WPF中的
使用 XamlPad 查看可视化树
WPF 工具 XamlPad 提供了一个选项,用于查看和浏览与当前定义的 XAML 内容相对应的可视化树。 单击菜单栏上的 显示可视化树 按钮以显示可视化树。 下面将说明如何在 XamlPad 的“可视化树资源管理器”面板中将 XAML 内容扩展为可视化树节点:
XamlPad 中的
请注意,Label、TextBox和 Button 控件如何在 XamlPad 的 可视化树资源管理器 面板中显示单独的视觉对象层次结构。 这是由于 WPF 控件具有一个包含其可视化树的 ControlTemplate。 显式引用控件时,会隐式引用其视觉层次结构。
剖析视觉性能
WPF 提供了一套性能分析工具,可用于分析应用程序的运行时行为,并确定可以应用的性能优化类型。 Visual Profiler 工具通过直接映射到应用程序的可视化树来提供丰富的性能数据的图形视图。 在此屏幕截图中,Visual Profiler 中的 CPU 使用率 部分提供了对对象使用 WPF 服务(包括呈现和布局)的精确细分。
可视化探查器显示输出
视觉呈现行为
WPF 引入了多种影响视觉对象的呈现行为的功能:保留模式图形、矢量图形和设备无关图形。
保留的模式图形
了解视觉对象角色的关键之一是了解 即时模式 和 保留模式 图形系统之间的差异。 基于 GDI 或 GDI+ 的标准 Win32 应用程序使用即时模式图形系统。 这意味着,由于某个动作(例如调整窗口大小或对象更改其视觉外观),导致客户区的一部分被无效化时,应用程序负责重新绘制该部分。
Win32 呈现序列 的
相比之下,WPF 使用保留的模式系统。 这意味着具有视觉外观的应用程序对象定义一组序列化的绘图数据。 定义绘图数据后,系统将负责响应呈现应用程序对象的所有重新绘制请求。 即使在运行时,也可以修改或创建应用程序对象,并且仍依赖于系统来响应绘制请求。 保留模式图形系统中有一个强大功能,即绘图信息总是由应用程序保持为序列化状态,但是呈现功能仍由系统负责。 下图显示了应用程序如何依赖 WPF 来响应绘制请求。
智能重绘
使用保留模式图形的最大好处之一是 WPF 可以有效地优化应用程序中需要重绘的内容。 即使具有不同级别不透明度的复杂场景,通常也不需要编写特殊用途代码来优化重绘。 将此与 Win32 编程进行比较,通过最大程度地减少更新区域中的重绘量,可以在其中花费大量精力优化应用程序。 有关在 Win32 应用程序中优化重绘功能时涉及到的复杂度类型的示例,请参阅在更新区域中重绘。
矢量图形
WPF 使用 矢量图形 作为其呈现数据格式。 矢量图形(包括可缩放矢量图形(SVG)、Windows 图元文件(.wmf)和 TrueType 字体)存储呈现数据并将其传输为说明列表,说明如何使用图形基元重新创建图像。 例如,TrueType 字体是描述一组线条、曲线和命令的轮廓字体,而不是像素数组。 矢量图形的主要优点之一是能够缩放到任何大小和分辨率。
与矢量图形不同,位图图像将渲染数据存储为图像的逐像素表示,并针对特定分辨率预先渲染。 位图和矢量图形格式之间的主要区别之一是对原始源图像的保真度。 例如,当修改了某个源图像的大小发时,位图图形系统会拉伸该图像,而矢量图形系统会缩放该图像,从而保持图像的保真度。
下图显示了其大小调整为 300% 的源图像。 请注意,当将源图像作为位图图像进行拉伸,而不是作为矢量图像进行缩放时,会出现失真现象。
以下标记显示了定义的两个 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。 分辨率描述屏幕上显示的像素数。 随着分辨率的提高,像素变小,导致图形和文本显示更小。 当分辨率更改为 1600 x 1200 时,在设置为 1024 x 768 的监视器上显示的图形将显得要小得多。
另一个系统设置 DPI 描述屏幕英寸的大小(以像素为单位)。 大多数 Windows 系统具有 96 的 DPI,这意味着屏幕英寸为 96 像素。 增加 DPI 设置会使屏幕英寸变大;减小 DPI 会使屏幕英寸更小。 这意味着屏幕英寸的大小与现实世界英寸的大小不同;在大多数系统上,它可能不是。 随着 DPI 的增加,DPI 感知图形和文本会变大,因为你增加了屏幕英寸的大小。 增加 DPI 可使文本更易于阅读,尤其是在高分辨率下。
并非所有应用程序都感知 DPI:有些应用程序使用硬件像素作为主要度量单位;更改系统 DPI 不会影响这些应用程序。 许多其他应用程序使用 DPI 感知单元来描述字号,但使用像素来描述其他所有内容。 使 DPI 太小或太大可能会导致这些应用程序的布局问题,因为应用程序的文本随系统的 DPI 设置进行缩放,但应用程序的 UI 不会。 对于使用 WPF 开发的应用程序,已消除此问题。
WPF 支持使用与设备无关的像素作为其主要度量单位(而不是硬件像素)进行自动缩放;图形和文本可正确缩放,而无需应用程序开发人员执行任何额外工作。 下图显示了 WPF 文本和图形在不同 DPI 设置中的显示方式的示例。
采用不同 DPI 设置的图形和文本
VisualTreeHelper 类
VisualTreeHelper 类是一个静态帮助程序类,它提供在视觉对象级别编程的低级别功能,这在非常具体的方案中非常有用,例如开发高性能自定义控件。 在大多数情况下,高级 WPF 框架对象(如 Canvas 和 TextBlock)提供更大的灵活性和易用性。
命中测试
当默认命中测试支持不满足你的需求时,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)