《Programming WPF》翻译 第7章 绘图


WPF极易创建可视化的极好的应用程序。它提供了十分丰富的绘图能力,而且用来充分开发当今显卡的能力。这就支持设计者创建复杂的设计和使用动画,使UI比以往更加容易苏醒。

WPF的图形架构不仅适用于开发者。WPF中图形支持的主要方面是它的深层次综合其余的编程模型。它易于添加图形化元素到应用程序的一部分中,而不用困惑于编程技术的改变——需要很多用户界面技术。

由于WPF是一种表现技术,图形是一种重要的和实质的一部分框架。单独使用WPF图形化能力填充整本书——这将是可能的,因此我们只能真正的在表面上涂抹。在这一章中,我们将会看到一些基础的概念——在WPF应用程序中使用图形的背后。在下一章,我们将要看一下动画。

7.1图形基础

WPF使得在你的应用程序中使用图形很容易,以及更容易开发你的显卡的能力。这有很多图形构架的方面来达到这个目标。其中最重要的是综合。

7.1.1综合

图形化元素可以组合到你的用户界面的任何一部分中。很多GUI技术易于将图形分离到一个独立的世界。这就需要一个“操纵杆”——当移动一个世界的按钮、文本框和其它框架到另一个世界的Shape和图像中,由于在很多系统中,这两个世界有不同的编程模型。

例如,Windows FormsMac OSCocoa都提供了在窗体中排列控件的能力,以及建立一个与这些控件交互的程序。它们还提供了一些API,这些API提供了高级的、完全的可伸缩的2维空间绘图工具。(在Windows Forms中为GDI+,在OS X中为Quartz 2D。)但是这些绘图API是不同于控件API的。绘图基础是非常不同于这些系统中的控件的,你不可以自由的混合这两种。

WPF,另一方面,把Shape作为UI树中的元素,就像其他元素一样。因此,我们可以自由的混和它们在任意类型的元素中。示例7-1显示了各种各样的例子。

示例7-1

正如你看到的,图形化元素可以被无缝隙的添加到标记中。外观和图形一起工作——正如它和任何其它元素一起工作一样,结果如图7-1所示。

7-1

#虽然这个例子是在xaml中,你还可以使用代码创建元素。本章的大多数示例都使用xaml,因为标记的结构直接反射了被创建对象的结构。然而,究竟使用代码还是标记依赖于你正在做什么。如果你是在创建绘图,你多数会使用一个xaml来为这些绘图创建xaml,但是如果你从数据中创建图形,从代码中做些事情就是有意义的。

本章显示的大多数技术可以使用在代码或标记中。参见附录A获取更多XAML和代码间关系的信息。

不仅图形和其它内容可以在标记中并排放置在一起,它们还可以混合在一起。注意到图7-1,右手边的椭圆,被放置在TextBlock中。如果你想达到在WindowsForms中的排列效果,只使用Label控件是不够的,你将不得不写一个新的控件,从头开始绘制这些文本和椭圆。这种混合有两种方式,不仅你可以把你的控件混合到你的图形中,还可以使用图形化元素来自定义你的控件外观——如第五章所描述的。

这种混合不只是像文本块这样的简单元素,同时也适用于控件。例如,图72显示了一个按钮,其中混合了文本和图形作为标题。

7-2

传统的Windows中,你可以得到这种效果,以来于按钮可以被显示为一张图片。但是图片是相当没有弹性的,它们只是一个混合了图片的块,因此你不能容易的使图片的一部分产生交互或使选中的部分响应用户输入以产生动画。因此,在WPF中,把图形放入按钮,这种方式工作的有点不一样。按钮的标记显示在示例7-2中。

示例7-2

当然,带有图像的按钮并不是一个新事物,但是传统上,按钮允许设置一张图片来支持于此。例如,Windows Forms中,按钮有一个Image属性,以及在Cocoa中,NSButton有一个setImage方法。这种实现使相当没有弹性的——控件允许设置一个单独的标题和一张单独的图片。比较示例7-2,这里使用了StackPanel在按钮内部布局以及只增加它需要的内容。你可以使用任意的外观面板在Button中,带有任意类型的内容。示例7-3使用了一个Grid来排列文本和一些椭圆在Button中。结果如图7-3所示。

示例7-3

7-3

WPF中,很少需要提供非弹性属性如TextImage的元素。如果一个表示内嵌内容的元素是有意义的,那么它将会表示无论什么你选择提供的混合型元素。

如果你熟悉二维绘图技术如Quartz 2DGDI+或者旧式的GDI32,另一种高级的绘图方式会使你震惊。我们不再需要写一个函数来响应重画的请求。WPF可以为我们保持屏幕重画。这是因为WPF使我们表示将图形表示为对象。

7.1.2绘图对象模型

有很多GUO技术,想自定义外观的应用程序,需要能够在一开始就创建它们的外观。通常显示一个自定义的外观的技术是写代码来表现一系列绘图操作,从而创建显示。这些代码将会在相应的图形第一次需要显示的时候运行。在一些系统中,OS不会保留应用程序绘图的副本,因此改方法会以运行时每次需要重画某个区域而告终,例如,如果一个窗体是不透明而且没有被盖住的。

在使用这个方法时,在这个生成函数构建整张图片时,更新独立的元素通常是有问题的。即使是OS获得绘图的一份副本的地方,通常是获取到一个图像。这意味这一旦你想要改变绘图的一部分,你通常需要重画改变区域的每一个事物。

WPF提供了一种不同的方式:你可以添加表示图形化形状的对象到用户元素树上。形状元素是UI树中和其它元素一样的对象,因此你的代码可以在任何时刻修改它们。如果你改变某个有可视化效果的属性,如SizeLocation,或ColorWPF将会自动更新显示。

为了说明这个技术,示例7-4显示了一个简单的包含一些椭圆的窗体。其中每个椭圆都由一个Ellipse对象表示,我们将在后台代码文件中更新显示。

示例7-4

每个椭圆的MouseLeftButtonDown事件都由定义在该窗体对应后台文件的OnClick方法处理。如示例7-5所示,OnClick方法简单的增加了Width属性——当Ellipse激发这个事件的时候。点击的结果使这个椭圆更加宽了。

示例7-5

如果我们使用旧的方法在一个单独的生成函数中绘制每一个事物,这样的代码对于更新显示不充分地。正规地,这将告诉OS,屏幕不再是有效地,导致激发一个重画请求。但是在WPF中,这是不必要的——当你在Ellipse对象上设置一个属性时,WPF保证了屏幕会相应地更新。进一步说,WPF知道所有这些交叠项,正如图7-4所示,因此,为了获得正确的结果,它将会根据需要上上下下地重绘这些项。所有你必须去做的时调整这个对象的属性。

7-4

#由于GUI第一次开始呈现,尽管计算机内存容量已经成数量级的增长,仍然有一些情形,使得绘制这个对象模型的方法可能会非常昂贵。尤其是,因为应用程序处理的是巨大的数据集,如maps,在UI树中有一组完全的对象,镜像映射了底层数据的机构,可能使用太多的内存。用于,为了某中类型的图形或数据,它可能更便利的使用旧有的生成代码的样式。为此,WPF还支持一些轻量级的操作模式。这将在本章的后面进行介绍——在“可视化层次编程”一节。

你可能已经注意到,到目前为止,我们已经做的所有的图画都使用形状而不是图片,WPF当然支持图片,但存在一个好的使用形状的原因。几何形状可以被伸缩和旋转而不损失图片质量。高质量的变型是WPF绘图中的一个重要特征。

7.1.3分辨率的独立性

GUI第一次显示时,不仅显示卡可以显著的提高性能,显示器也可以。长期以来,唯一的主流显示技术是CRTCRT的颜色只提供了有限的分辨率。它们努力显示清晰度高于100px/I的图片。然而,纯平显示器,现在卖的比CRT多,可以在很大的余地胜过它。

本书的作者之一有一台使用两年的膝上型电脑,分辨率为150px/I。写作本书的时候,显示器可利用超过了200px/I。创建更高象素密度是可能的。尽管如此,在当前操作系统使用这些显示器仍然存在一个问题。每个事物都会以很小以至于不可用而告终。这是因为一种基于象素的开发文化,大多数应用程序使用象素按量配给它们的用户界面。

这并不是完全的技术限制的结果。从Wndows NT使用到现在,以一种独立于分辨率的方式绘图是可能的,因为绘图APIGDI32运行你应用变型到所有你的绘图中。在2001引进的GDI+,提供了同样的机制。但是正因为有效的样式并不意味着应用程序就可以使用它。大多数应用程序并不暴露它的可伸缩性。

不幸的是,在Win32中,将图形和其他UI元素分隔开意味着,即使应用程序没有暴露绘图API中的可伸缩性,其他UI部分不会自动地遵循。图7-5显示了一个Windows Forms应用程序,使用了GDI+来绘制图形,可以任意的大小伸缩。

 

7-5

注意到在图7-5中,虽然星星和“Hello World”文本已经被伸缩了,TrackbarLabel控件却并没有这样。这是因为绘图变型仅仅影响你使用GDI+绘制的那些图形。它们并不影讯整个UI。尽管Windows Forms提供了一些伸缩其余UI元素的样式,但这不是自动的。你不得不采用深思熟虑的非平凡的措施来建立一个独立于分辨率的UIWindows Forms中。

7.1.3.1伸缩和旋转

通过在底层支持变形,WPF解决了这样一个问题。取代以只在二维绘图级别提供伸缩性,WPF将其生成在底层的合成的引擎中。结果是UI中的每一个事物都可以被变形,不仅仅是用户绘制的图形。回到示例7-2我们简单的带有笑脸的按钮,我们可以通过只改变第一行来使用这个伸缩性:

<Button LayoutTransform=”scale 3 3”>

LayoutTransform属性在WPF所有的用户接口元素中是有效的,因此你可以伸缩整个窗体如同一个按钮一样简单。多种类型的变形是可以利用的,这将在后面更详细的讨论。目前,我们简单地要求扩大一个按钮——通过xy方向上的因数3。图7-6显示了扩大后地按钮。

7-6

比较原始的图7-2和图7-6,后者比前者更明显,正如你所希望的。更加意味深长地,细节变得更零碎了。按钮的圆角边缘比它们在一个较小的版本中更加容易看到。后者的形状定义得更好。而且我们的图形更加清晰。我们获取这种清晰度是因为WPF生成的按钮看上去和详细指明大小的按钮一样好。比较图7-6和图7-7中的示例。

7-7

7-7显示了发生了什么——如果你简单的扩大原始的小按钮的图像。有很多种扩大图像的不同方式。左边的示例使用了简单的算法,称为“”,有时又称为“”。为了是这张图片更大一些,象素被简单的重复了。这导致了对这张图像一种很方正的感觉。右边的示例使用了一种比较高级的插值算法。这使得圆角看起来是圆的,同时不会遭受矮矮胖胖的象素效果,而这会导致看起来很模糊。清晰,两个图像中没有一个可以达到图7-6的效果。

7.1.2分辨率,坐标系统,和“象素”

对伸缩图形的支持,意味着在你的应用程序中使用的坐标系统和屏幕使用的坐标系统间没有固定的关联。如果运行在一台高DPI的显示器上,即使你不用伸缩变形,变形会自动应用到你整个的应用程序上——这是真实的。

WPF中,如果不是物理性象素,那么默认的度量单位又是什么呢?答案是,有点混淆,象素。为了更加精确,真实的答案是,独立于装置的象素。

 

独立于装置的象素,WPF官方的定义是1/96每象素。如果你详细指出一个形状的宽度为96px,就意味着这是1英寸宽。WPF会使用与所需同样多的物理象素来填充一英寸,例如,高分辨率的膝上型显示器,分辨率达到150px/I。因此如果你绘制一个宽为96px的象素,WPF将生成150px/I的宽度。

#WPF依赖于系统级显示器设置,来决定物理象素的大小。你可以通过Windows Display Properties程序来调整它们。右击“桌面”,选择“属性”来显示这个程序,进下来进入到“设置”条。点击“高级”按钮,在打开的对话框中,选择“通用”条,这将告诉Windows

你的显示器分辨率。

你可能想知道为什么WPF使用有点奇怪的选择1/96英寸,以及为什么叫“pixel”。原因是,96dpiWindows中默认的DPI——当运行时使用正常的字体,因此长期考虑pixel正常的大小。这意味着在带有正常象素密度的显示器上,WPF会伸缩你的绘图为了它们保持正确的物理大小。

WPF为每一个伸缩优化图形化样式生成的能力,意味着它能理想的被用于利用增长的显示器分辨率。作为第一次,屏幕上的文字和图形会竞争这些清楚的碎片——我们已经希望来自打印机的。当然,为了所有实际中的工作,我们需要一组全面的绘图基础。

7.1.4形状,笔刷和钢笔

WPF绘图工具中的大多数类被归类为三种类型:形状,笔刷和钢笔。在这些主题上有很多变体,我们将会在后面详细检查它们。然而,为了完全获取任何地方的图形,对这些类一个基本的了解是强制的,

形状是用户界面树中的对象,提供了用于绘图的基本的创建块。我们已经看到的EllipsePathRectangle元素都是形状对象的例子。同样支持线条——基本线条和多段线条,分别使用LinePolyLine。还可以创建任意的填充形状。Polygon支持所有边都是直的形状,而一旦你需要弧形的边,Path类支持填充如同弧形一样的形状。图7-8显示了使用中的每一个形状。

7-8

不管你选择的是什么图形,你需要决定如何对其进行着色。为此,可以使用Brush。有很多可利用的Brush类型,最简单的是单颜色的SolidColorBrush。你可以使用LinearGradientBrushRadialGradientBrush达到更有趣的可视化效果。这些允许在界面的形状上改变颜色,这是一种很好的办法——提供了很深的印象。你也可以基于图像创建笔刷,ImageBrush使用了一个图片,DrawingBrush使用了一个可伸缩的绘图。最后VisualBrush使你在任何可视化树、任何用户界面中你喜欢和使用的大块上绘制其他的形状。这使得达到想反射你的用户界面的全部的效果很简单。

最后,钢笔用于绘制形状的轮廓。钢笔其实只是一个增强的笔刷。当你创建一个Pen对象的时候,你给了它一个Brush来告诉它应该怎么在屏幕上画画。Pen类只是增加了一些信息如线条的厚度,阴影样式。图7-9显示了一些可用的效果,使用了笔刷和钢笔。

7-9

7.1.5合成

图形构架的最后一个关键样式是合成。在计算机图形中,名词“合成”是指将多个形状和图片联合在一起形成最后输出的过程。WPF的合成模型是不同于传统Windows工作模型的,对于创建支持高质量可视化,这是至关紧要的。

在经典的Win32模型中,每个用户界面元素都独占它所拥有的应用程序窗体中的某个区域。在每一个顶级窗体中,任何在窗体中给定的象素都完全由那一个确切的元素控制。这就防止了元素是局部透明的。它同样排除了在元素的边缘使用抗锯齿处理,当联合非矩形元素的时候,这个技术是尤其重要的。虽然各种各样的hack已经被分成提供幻想在Win32中的透明度,它们有局限以及可以有点便利的与之工作。

WPF的组合模型支持任何形状的元素以及允许它们交叠。它还允许元素有任意局部或完全的透明化区域的混合。这意味着任何给定的屏幕上的象素都可能有多个有作用的可视化元素。进一步,WPF使用锯齿化处理在所有形状的边缘。这样减少了参差不齐的外观,更简单的绘图技术可以在屏幕上生成,导致一个平滑外观的图像。最后,组合引擎允许任何元素在组合前,将变形应用上去。

WPF的组合引擎使用了现代显卡来加速绘图的过程。在内,这可以在Direct3D模型的顶层实现。这看起来是多余的,因为多半WPF的绘图功能都是二维的,但是大多数面向3维的功能,在现代显卡上,可以还被用于绘制二维形状。例如,象素阴影可以被用于实现高级的ClearType机制,当组合文本到UI中。WPF开发了同样超快的多边形绘图机制——由SD游戏用来生成基本形状。进一步,WPF在显卡上缓存了可视化树的一部分,显著的促进了重绘的性能——当在屏幕上改动小的细节时。

现在让我们看一下支撑WPF图形系统的核心概念,让我们仔细的看一下细节。

7.2图形

图形时绘图的基础,代表用户界面树的元素。WPF支持多种不同的形状,并为它们每一个都提供了元素类型。

7.2.1基本图形类

在这一节列出的所有元素,派生于一个共同的抽象基类Shape。虽然你不能直接使用这个类,知道它还是有帮助的,因为它定义了一组共同的特性——你可以在任何形状上使用。这些共同的属性都被连接到形状的内部和外部被绘制的地方。

Fill属性详细指出了Brush要用于填充内部。LinePolyline这些类没有内部,所以它们没有Fill属性。
(这比通过有独立的ShapeFilledShape基类使继承层次复杂化的发式要简单的多)Stroke属性详细指出了用来画形状轮廓的Brush

#如果为你的形状没有详细指出它的FillStroke属性,这将是不可见的,因为这两种属性默认都是透明的。

这看起来特殊——Stroke属性是Brush类型。正如我们早时看到的,WPF定义了Pen类来详细指明一个线条的厚度、dsah样式以及样子,因此如果Stroke属性是Brush类型的,这将是更有意义的。WPF实际上确实在内部使用了Pen来绘制形状的边框。Stroke属性为Brush类型主要是因为它的便利。所有的Pen样式通过独立的Shape属性对外暴露,正如表7-1所示。这详细指明了该场景的标记——在你乐于使用默认的钢笔设置的地方,你不需要提供一个完整的Pen定义仅仅是设置边框颜色。

 

7-1

笔刷和钢笔都详细描述在“Brushes and Pens”一节,在本章的后面。

7.2.2矩形

Rectangle实现了它的名称所示的。无论任何形状,它可以被填充来绘制,作为一个边框。或者全部。不但绘制一个正常的矩形,它还能画一个圆角矩形。

Rectangle不提供任何属性用于设置它的大小和位置。它依赖于同样的外观机制如其它UI元素。位置由面板容器决定。长和宽都有它的父一级自动设置,或者使用标准外观属性来显示地设置WidthHeight

示例7-6显示了Canvas面板上的一个Rectangle。这里,WidthHeight都被显示地设置,以及外观被详细的指定——通过使用附属的Canvas.LeftCanvas.Top属性。

示例7-6

示例7-7显示了另一种方法。没有一个矩形有其显示设置的外观和大小。取代的,它们依赖于Grid容器。图7-10显示了结果。

示例7-7

Rectangle通常使用其父面板的坐标系统来排列。这意味着它的边缘将通常是垂直的或水平的,即使父一级面板被旋转过了,Rectangle也当然会随着它一起旋转。如果你想要旋转一个Rectangle相对于它的容器面板,你可以使用有效的RenderTransform属性在所有的用户界面元素上,正如示例7-8所证明的,这个示例说明了RenderTransform的使用来旋转一系列矩形。结果如图7-11所示。

示例7-8

7-11

为了绘制一个圆角矩形,使用RadiusXRadiusY属性,正如示例7-9所示。表7-12显示了结果。

示例7-9

7-12

7.2.3椭圆

Ellipse类似于Rectangle。明显地,它绘制了一个椭圆而不是一个矩形,但是Ellipse的大小、位置、填充和边框,都以与Rectangle同样的方式控制,正如示例7-10显示。结果显示在7-13

示例7-10

7-13

7.2.4 线条

Line元素绘制了一条直线,从一点到另一点。它有四个属性来控制起点和终点的位置:X1Y1X2Y2。这些坐标是相对于父面板选择放置Line的位置。考虑示例7-11

示例7-11

示例7-11使用了垂直的StackPanel来排列TextBlockLine元素的交错序列。TextBlock元素有灰色的背景使之易于看到每个元素垂直的区域。结果如图7-14所示。

7-14

正如你在图7-14看到的,Line元素像其它元素一样被放置在栈中。StackPanel被分配充分的高度以容纳Line。第一条Line是有趣的——在这条线上的TextBlock底部和该线段的起点间的一些空白。这是因为这条线段的Y1属性已经被设置为10,表明这条线段应该开始于放置Line元素位置的顶部的下面一点。第二个Line元素与上一条线段完全一致,因为它的Y2属性被设置为0,再次说明了这条线段终点的坐标系统是面板容器相对于放置Line的区域的。

这里没有办法自动设置线段的终点作为外观的一部分(你能依赖外观系统来定位EllipseRectangle元素,唯一的原因是它们的尺寸由矩形来决定,以及外观系统本质上处理了矩形的排列。)例如,你不能通知Line确切的宽度和父一级面板放置的位置一样。如果你想这么做,只需取代地使用Rectangle,你可以通过创建一个细矩形来绘制一条线段,如果需要就旋转它。或者你可以使用一个DrawingBrushVisualBrush,当这些可以自动伸缩图形来填充可利用的空间。

7.2.5 折线

Polyline使你绘制一系列连接的线段。取代现有的开始点和终结点的属性,Polyline有一个Points属性,包含了一个坐标对的清单,正如示例7-12所示。WPF简单地绘制了一条线段,按顺序通过每一个点,正如图7-15所示。

示例7-12

7-15

由于使用Line类,Polyline中点的坐标是相对于面板容器选择来放置Polyline的位置。

7.2.6多边形

Polygon非常相似于Polyline。它有一个Points属性,工作方式与Polyline相同。唯一的区别是Polyline一直绘制一个敞开的形状,而Polygon则总是绘制一个封闭的形状。为了说明这个不同,示例7-13包含了一个Polyline和一个Polygon。所有同样的属性都设置为一样的。

示例7-13

正如你在图7-16中看到的,Polyline忽略了Fill属性。这个形状并没有它的内部,它被置为敞开的。Polygon,另一方面,通过在首尾两条线段之间绘制一条额外的线段而关闭了形状,同时,它绘制了形状的内部。

7-16

因为我们可以自由的添加点到Polygon中的任何地方,这就易于以一个自交的形状而告终,它的边穿越了它自身。就是这样一个形状,这可能使含糊的关于有多少数量作为形状的内部。图7-17显示了这样一个形状以及两个可能的填充方式。

7-17

Polygon类提供了一个FillRule属性来选择一种潜在的处理任意区域的方式。(在一些绘图系统,这被描述为拓扑规则。)WPF支持两种填充规则。示例7-14是图7-17的标记,显示了使用中的填充规则。

示例7-14

默认规则是EvenOdd,这用于图7-17中的左边的Polygon。这是能理解的最简单规则。为了决定一个特定的围绕区域是在形状的内部和外部,EvenOdd规则对线段的数量进行计数——你不得不跨越从一个端点到另一个完全在形状外的端点。如果这个数量是奇数的,这个点就是在形状中;反之,就是在外面。

第二个填充规则,Nonzero,是非常精细的。从图7-17中,你可以已经想到任何密闭的区域被认为是在图形中的,但是这并不是那样十分简单的。Nonzero规则表现了一个和Nonzero类似的过程,而不是简单的对线段的数量计数。考虑到线段运行位置的方向——它增长或减少的每条线段跨越的数量,依赖于这个方向。如果在末端总计为nonzero,这个点被认为是在形状内部。

在图7-17中右边的PolygonNonzero规则导致了所有的密闭区域作为内部的一部分。然而,如果形状的轮廓沿着一条较轻微盘旋的路径,结果可能有一点更混合,如示例7-15所示。

示例7-15

示例7-15的结果显示在图7-18中。这说明了Nonzero规则并不是像第一次看到的那样直接。

7-18

 

Nonzero规则有点怪。它是由PostScript普及的,因此大多数绘图系统都支持它,但是这并不总是容易的从一个带有填充规则的Polygon中获取有用的结果。在Path元素的上下文中更加有意义——支持多重配置在一个单独的形状中。

7.2.7 路径

Path是目前为止最强大的形状。所有的形状——到目前为止我们已经看到的——已经被便利地提供,因为使用Path绘制所有这些形状是可能的。Path还使绘制相当复杂的形状成为可能——比我们之前看到的形状更复杂。

就像PolygonPath有一个FillRule属性来控制填充规则。代替Points属性,Path有一个Data属性,它的类型是Geometry,这是一个抽象的基类。一个Geometry对象表示一个特定的形状。这里有大量的具体类来表示不同类型的形状。其中三种听起来相当熟悉:RectangleGeometryEllipseGeometryLineGeometry。这些表示了我们先前见到的相同形状。因此这个Rectangle

<Rectangle Fill=”Blue” Width=”40” Height=”80”>

是有效的Path速记:

<Path Fill=”Blue”>

         <Path.Data>

                   <RectangleGeometry Rect=”0, 0, 40, 80” />>

         </Path.Data>

</Path>

在这一点,你可能想知道什么时候你要使用RectangleGeometryEllipseGeometryLineGeometryPath中,来取代更简单的RectangleEllipseLine。原因是Path使你可以使用一种特殊类型的几何对象,称为GeometryGroup,来创建一个带有多个几何体的形状。

这里有一个显著的不同在使用多个明显的形状和有一个单独的带有多个几何题的形状之间。让我们看一下示例7-16

示例7-16

这绘制了两个椭圆,一个在另一个的上面。他们都有一个黑色的轮廓,因此你可以看到更简单的一个在更大的另一个中,如图7-19所示。

7-19

既然Ellipse形状只是创建一个EllipseGeometry的简单方式,示例7-16中的代码与示例7-17中的代码是等价的。(正如你能看到的,使用Path是相当地更加冗长。这是为什么要提供Ellipse和其它简单的形状。)

示例7-17

因为示例7-17中的代码与示例7-16中的代码是等价的,它导致了准确的相同输出,正如图7-19所示。到目前为止,使用几何体取代形状,并没有在生成的结果中制造出不同,这是因为我们仍然使用多个形状。因此,我们现在将显示如何把所有的椭圆放进一个单独的Path以及看到这是如何影响到这些结果。示例7-18显示了改动后的标记。

示例7-18

这个版本正好有一个单独的路径。它的Data属性包含了一个GeometryGroup。这就允许任何数量的几何体对象被添加到同样的路径。这里我们已经添加了两个EllipseGeometry元素——先前在两个独立的路径中。结果如图7-20所示,明显不同于图7-19。在形状中间有一个洞。因为默认的奇偶填充规则在使用中,较小的椭圆在较大的椭圆中生成一个洞。

7-20

带洞的形状可以仅通过结合多个图形到一个单独的形状中来创建。你可以试着通过绘制一个内部的填充为白色的Ellipse,来得到类似的效果,如图7-20所示,但是这个“骗局”并不能工作——当你绘制一个形状在其他事物之上的时候,如图7-21所示。

7-21

#你可能想知道如果使用可以用transparent透明色绘制内部的椭圆,但是这也不能运行。绘图时使用全部的透明色等效于,根本绘制不出任何东西。这就是透明的含义。只有在形状中钉进一个洞,我们才能识破它。

为了理解为什么,想一想绘图的过程。当它生成我们的元素到屏幕时,WPF绘制这些元素一个接着一个。在这种情形中,它以文字开始——而无论什么在后面,接着它绘制了文字之上的形状——这将有效的删除形状后面的文字(当然它仍然在元素树中存在,因此WPF可以总是在以后重绘它,一旦你改变或者移除了图形。)由于你仅在文字上绘制,你不能绘制另一个形状在其上来“恢复绘制”一个洞到第一个形状。因此如果你想要一个洞在形状上,在你绘制这个形状之前,你最好确定这个洞在那里。

我们还没有看到最有弹性的几何体:PathGeometry。它可以绘制任意PolygonPolyline能够表示的形状,除此之外,还可以绘制更多的形状。

PolyGeometry包含了一个或更多的PathFigure对象,以及每一个PathFigure代表了一个单独的开放或密闭的形状在路径中。为了定义每个图形的轮廓形状,你使用一个PathSegment对象的序列。
 

#PathGeometry包含多个图形的能力有点交叠于Path包含多个几何体的能力。这恰恰是为了便利,如果你需要生成一个每一部分都是一个PathGeometry对象的形状,这对一个单独的带有多个PathFiguresPathGeometry有更多的影响。如果你只是想使用更简单的几何体,如LineGeometryRectangleGeometry,比较简单的办法是使用GeometryGroup以及同时要避免使用PathGemeetry

示例7-19显示了一个简单的路径。它仅包含了一个单独的图形和绘制了一个矩形。

示例7-19

结果显示在图7-22,这看起来是做了巨大的努力为获取一个简单的结果。我们使用了17个线段标记来达到我们看到的效果——一个单独的矩形。这是为什么WPF提供一些类为这些较简单的形状和几何体。你不用严格地需要任何东西——因为你可以取代的使用PathPathGeometry,但是它们要求更少的努力。正规的,你会为比较复杂的形状而使用Path

7-22

即使示例7-19生成了一个非常简单的结果,它说明了带有PathGeometry属性的Path大多重要的样式。对比之前的所有示例,几何体在路径的Data属性中。PathGeometry是一个PathFigures的集合,因此所有有趣的数据都在他的Figures属性中。这个示例仅包含了PathFigure,但是你可以添加任意多你想要的。PathFigure的形状由它的Segments属性中的项决定。

这些片断的序列总是以StartSegment开始,这个标记决定了路径的起始点。接下来,它会沿着一个或多个决定图形的形状的分割而行。在示例7-19中,这些全都是LineSeqment,因为图形只有直边,但是也提供一些圆弧的类型。最后,这个示例以CloseSegment终止,指明这是一个密闭的形状。如果我们想创建一个开放的形状(如Polyline),我们可以简单的忽略最后的CloseSegment

#你可能想知道为什么LineSegment不能像Line一样工作。对于Line,我们可以指出它的起点和终点,是示例7-11所示。这看起来比LineSegment简单,后者需要我们以StartSegment开始。

尽管如此,PathFigure中的线段不能工作,在图形的轮廓没有任何间隙。使用Line元素,每一个Line都是截然不同的独自形状,而使用PathFigure,每个片断都是形状轮廓的一部分。为了充分明确地定义一个图形,每个片断必须开始于先前结束的那个片断。这是为什么LineSegment只要为线段详细指明终止点。所有的片断类型以它们的方式工作。当然,图形需要开始于某处,这是为什么我们总是开始于一个StartSegment。这是不合理的使用StartSegment在其它任何地方而不是在图形的起点。

示例7-19并不是非常令人激动的。它只是使用了直线片断。替代的,我们可以创建更多有趣的形状,通过使用弧形片断类型中的一种。表7-2显示了所有的片断类型。

7-2

ArcSegment使你可以添加椭圆圆弧到形状的边缘。ArcSegment的使用比一个简单的LineSegment有一点更复杂。不仅要指出片段的终止点,我们必须还要通过Size属性指出椭圆的两个半径。

椭圆大小和线段的起始点终结点并未通过足够的信息来明确地定义圆弧,因为有很多种方式来在给定地约束下绘制一个椭圆形的圆弧。考虑一个带有特定起点和终点地片断,以及一个给定的大小和定位的椭圆。

为了这种片断,这里将通常有两种方式用来定位椭圆,从而它的起点和终点都在椭圆的边界上,正如图7-23所示。换句话说,有两种用一条特定的线段“切割”一个椭圆的方法。

7-23

作为两种分割椭圆式的任一种,就会有两种分割结果,一个小的和一个大的。这意味着有四种方式可以在两点间绘制一条弧线。

ArcSegment提供了两种标记,支持你选择你需要那种弧线。LargeArc决定你是选取较小的还是较大的那个分割片大小。SweepFlag选择在线段的哪一边绘制的分割。示例7-20显示了标记语法,说明所有的四种带有这些标记的组合。它同样显示了整个的椭圆。

示例7-20

#你可能想知道为什么Ellipse的宽度为140和高度为60,这是每个ArcSegmentSize属性值的两倍。这是因为ArcSegment解释了Size为椭圆的两个半径,而椭圆上的WidthHeight属性指出了全部的大小。

7-24显示了结果,以及,正如你能看到的,每一个形状都有一条径直的对角线以及一条椭圆的弧形。在所有的四种情形中,直线的边缘具有相同的长度和方向,圆弧的边缘来自同一个椭圆的不同部分。

在图7-24中,椭圆的轴是垂直和水平的。有时你可能想要使用一个椭圆,而轴并没有对齐到你的主轴。ArcSegment提供了Xrotation属性,允许你详细指出需要旋转的度数。

7-24

7-25显示了3中椭圆轴。它们使用了和图7-24相同的起点和终点以及相同的椭圆大小。唯一的不同是详细指出了XRocation45度,在分割前对其进行了旋转。

7-25

#切割椭圆的方式不只有两种,这里有两种退化的情形。第一种是将椭圆从中间切为相同的两半。在这种情形种LargeArc标记就是不相关的了,因为两个片断是相同的大小。

另一种情形是当椭圆非常的小,如果在椭圆可以被切割的最宽的点比片断的窄,这是没有办法正确绘制这个片段的。你应该试着避免这样。(如果你确实使椭圆非常小,WPF看起来按比例增加了椭圆使之足够大,在X轴和Y轴按比例保持它的样子。)

其余的四种圆弧类型来自表7-2BezierSegmentPolyBezierSegmentQuadraticBezierSegmentPolyQuadraticBezierSegment是同一个主题的四种变体。它们都绘制了贝赛尔曲线。

7.2.7.1贝赛尔曲线

贝赛尔曲线是基于一个特定的数学公式的连接两点的曲线片断。详细理解这个公式对于使用贝赛尔曲线是不必要的。它有用的地方在于为曲线图提供了相当程度的弹性。这使得它非常流行,大多数向量绘图程序都提供贝赛尔曲线。图7-26显示了各种贝赛尔曲线的片断。

7-26

7-26中显示的五条线中的每一条都是一个简单的BezierSegment。贝赛尔曲线在图形系统得到了非常广的使用——因为形状中广泛的变体,即使一个单独的片断也能够提供。它们还可以相当直接的使用。

作为所有片断类型,BezierSegment开始于上一个片断停止以及定义一个新的终结点的位置。它还需要两个控制点,来决定曲线的形状。图7-27再次显示了相同的曲线,但是曲线上带有控制点。它还显示了连接控制点到终止点片断的线段,因为这使得更加容易看到:控制点如何影响曲线形状。

7-27

控制点影响曲线形状的最明显方式是它们决定切线。在每个片断的起始点和终止点,曲线在那个点上运行的方向与连接起始点到相应的控制点的线段的方向是一样的。

这里还有一个次要的不太明显的控制点工作的方式。在开始点(或结束点)和它相应的控制点间的距离也会产生影响。这就从本质上决定了曲率的极端程度。

7-28显示了一组贝赛尔曲线类似于图7-27。这些结束与直线的切线是保持同样的,但是,在每种情形中,开始点和第一个控制点间的距离被减少到原先的四分之一,尔另一个和之前是一样的。正如你看道德,这减少了第一个控制点的影响,曲线的形状由远离终点的控制点支配。

7-28

示例7-21显示了图7-27中第二个曲线片断的标记,Point1属性决定了第一个控制点的位置,这个控制点联合了起始点。Point2定位了第二个控制点。Point3是终结点。

示例7-21

虽然贝赛尔曲线是弹性的,你很少使用到一个如此简单的曲线。当定义带有弯曲边缘的形状时,对于一个形状而言这是普通的——使用贝赛尔曲线定义它的边缘。WPF因此提供了一个PolyBezierSegment属性,这是一个Point结构的数组。每个贝赛尔曲线三个实体在这个数组中:两个控制点和一个终结点。(通常的,每条曲线开始于前一条曲线终结的位置。)示例7-22显示了一个带有两个曲线的片断。图7-29显示了结果。

示例7-22

7-29

这个标记有点不太便利,相较于使用一个BezierSegment元素序列。幸运的是,你可以以字符串的形式提供所有点数据。这等价于示例7-22:

<PolyBezierSegment Points="0,10 20,10 40,10 60,10 120,15 100,50" />

同样,如果你从代码中生成坐标,处理一个单独的PolyBezierSegment和传递给它一个Point数据的数组,这比处理一些独立的片断要容易的多。

贝赛尔曲线在曲线的形状上提供了许多控件。然而,你可能不总是想要弹性的等级。QuadraticBezierSegment使用了一个较简单的等式——仅使用了一个指向定义了曲线形状的控件。这没有提供同样范围的曲线形状,作为一个立方体的贝赛尔曲线,但是如果所有你想要的只是一个简单的形状,这就减少了三分之一你需要提供的坐标对数量。

QuadraticBezierSegment使用上与正常的BezierSegment类似。唯一的不同是他每有一个Point3属性,只有Point1Point2Point1是共享控制点,Point2是终结点。QuadraticBezierSegment是一个多曲线的装备。你以与PolyBezierSegment相同的方式使用它,除了你只需要为每个片断提供两个点。

7.2.7.2结合形状

Path自有它的玄机——我们至今没有检查到的。它有联合若干几何体以形成一个新几何体的能力。这是不同于添加到两个几何体到一个Path中,它联合了成对的几何体在某种程度上形成了一个单独的具有完整形状的新几何体。

示例7-23和示例7-24各自定义了路径,它们全都使用了同样的RectangleGeometryEllipseGeometry。区别是示例7-23把它们都放入了一个GeometryGroup中,而示例7-24把它们都放入了一个CombinedGeometry

示例7-23

示例7-24

7-30显示了示例7-23和示例7-24的结果。CombinedGeometry导致了带有多个图形的一个形状,CombinedGeometry生成了一个单独的图形。椭圆几何体在矩形几何体咬进去一口。这只是联合若干几何体方式中的一种。CombineMode属性决定了使用哪一个,图7-31显示了所有的五种可利用的类型。

7-30

7-31

联合模式有以下的效果:

Union

            建立了一个形状,任意的在原始的两个形状中任一个的点,都在这个新的形状中。

Intersect

            建立了一个形状,任意的同时在原始的两个形状中的点,都在这个新的形状中。

Xor

            建立了一个形状,任意的只在原始的两个形状中其中一个的点,都在这个新的形状中。

Execlude

建立了一个形状,任意的只在第一个而不在第二个形状中的点,都在这个新的形状中。

Complement

            建立了一个形状,任意的只在第二个而不在第一个形状中的点,都在这个新的形状中。

7.2.7.3数据属性文本格式

我们已经看到Path提供的所有的样式。正如你看到的,我们可以终结某些相当冗长的标记。幸运的是,Path元素提供了一个速记机制——允许大多数我们看到过的样式可以被开发而不用敲击太多代码。

到目前位置,我们已经设置了Data属性,使用了xaml的属性元素语法(见附录A获取更多语法的细节)。然而,我们可以替代的提供一个字符串。示例7-25显示了这两种技术。正如你看到的,使用字符串的形式只有16行这么短。

示例7-25

文本形式的Path.Data属性的语法是简单的。这个字符串包含一个命令序列。命令是一个依照某些数字化参数的字母。需要的参数数量由被选择的命令决定。线条仅需要一对坐标。曲线则需要更多的数据。

如果你忽略了这个字母,同样的命令将会被作为上一次使用。例如,示例7-25使用了L命令,这是“Line”的简写,代表了LineSegment。这只要求2个数字,线段终结点的坐标。以及在我们的示例中,这里有6个数字。这简单的指明了每行有3条线。表7-3列出了这些命令,它们的等价片断类型,以及它们的用法。

7-3

M命令有特殊的待遇。在任何地方使用StartSegment而不是第一个PathFigure片断是合理的。如果你使用M命令在区域中间,这就意味着你将要开始一个新的PathFigure。这支持多个图形在这个复杂文本格式中表示。

注意到有两种方式详细指明一个BezierSegmentC命令让你提供所有的控制点。S命令为你的外观在第一个片断生成第一个控制点,以及生成指向前一个片断镜像的第一个控制点。这就确保了片断的切线与前一个片断对齐,导致了以一种平滑的方式连接线段。

所有这些命令都可以两种方式使用。你可以详细指出命令以它的大写或小写形式。以大写的形式,坐标是相于Path元素的位置。如果命令是小写的,坐标是相对于路径中前一个片断的终结点。

现在我们已经检查到当前提供的形状,但是到目前位置,我们已经相当没有危险的在我们的为这些形状填充和轮廓的选择中。我们已经使用命名的颜色和简单的轮廓样式。WPF允许我们通过它的笔刷和钢笔类,使用更多不同种类的绘制样式。

posted @ 2008-03-24 14:49  包建强  Views(3003)  Comments(0Edit  收藏  举报