《Programming WPF》翻译 第7章 3.笔刷和钢笔
为了在屏幕上绘制一个图形,WPF需要知道你想要为图形填充什么颜色以及如何绘制它的边框。WPF提供了一些Brush类型支持各种绘图样式。Pen类增加这些笔刷以提供边框的厚度和样子。
在这一章,我们将要看一下各种类型的笔刷和钢笔类。可是,由于所有的笔刷和钢笔类最终是关于指出在哪里使用哪一种颜色,以及如何将它们联合在一起,我们必须首先看一下眼色是如何被表示的。
7.3.1 颜色
WPF在System.Windows.Media命名空间中使用了Color结构来表示一种颜色。注意到如果你以往工作于Windows Forms、ASP.NET或GDI+,Color机构是不同于那些技术使用的机构的——它们使用了System.Drawing命名空间的Color结构。WPF引进了这种新的Color结构,是因为它可以工作于浮点形式的颜色值——支持更高的颜色精度,以及更好的弹性。
Color颜色结构使用了四个数字,或者说是通道,来表示一种颜色。这些通道是红、绿、蓝以及Alpha。红、绿、蓝通道是计算机图形学中传统的表示颜色的方式。(这是因为颜色屏幕通过将这三种基本颜色混合在一起来工作。)一个0值表示颜色部分完全不存在;三个通道都是0对应黑色。Alpha通道表示颜色不透明度的等级。Color可以是不透明的、完全不透明的、以及在这两种极限值之间的任意值。WPF的合成引擎充分支持透明度,因此任何图形都可以被绘制为带有透明等级。0值用来表示完全透明。
Windows是传统的使用24位颜色信息,每个通道8位颜色,以表示“真彩色”;同时还有32位带有透明度的真彩色。这仅仅是充分的关于计算机屏幕的平均水平。常规计算机表现的颜色和亮度范围是,24位颜色对于大多数用途总是足够的。然而,对于很多图形化应用程序,这是不够的。例如,电影,相比于计算机屏幕,可以提供一个更宽范围的亮度,还有24位颜色对于将电影作为输出媒介的图形化工作,是完全不足够的,同样也不适用于很多医疗影像应用程序。即使对于计算机和视频影像,24位颜色也会引起一些问题。如果图像需要经过很多阶段的处理,这些增强24位原材料的限制。
WPF因此在它的颜色表示中支持一个相当高等级的细节。每个颜色通道使用16位代替8位。这种Color结构仍然在需要的地方支持8位通道的使用,因为大多数图像软件依赖于这样的表示。Color通过A、R、G、B属性暴露了这些8位通道,这些属性接受0到255间的值。这种更高级的定义表示法——通过ScA、ScR、ScG、ScB属性——也是有效的,这些属性代表了0到1范围内的单精度的浮点值。
ScA、ScR、ScG和ScB属性中的”Sc”涉及了这样的事实,它们支持标准的“Extended RGB colour spacescRGB”,颜色空间定义在IEC61966-2-2规范中。”sc”是“scene”的缩写,因为这通常是一个scenereferred的颜色空间。这意味着scRGB空间的颜色值代表了原始图像的颜色。这是不同于计算机图像通常是如何存储的。传统上说,我们已经使用了outputreferred颜色空间,颜色值不需要在显示于目标设备之前映射到那里。
使用Outputreferred颜色空间可以有效地工作,只要它们恰好设定了输入设备的目标。然而,scenereferred颜色空间保护了所有的可利用信息在捕获或生成图像时。为了更高精度的颜色表示,scenereferred模型因此是更清晰的。即使它们工作起来有点低效率。
这里还有一个Color类,它提供了一组标准的命名颜色,包括所有旧有的喜好,如PapayWhip、BurlyWood、LightGoldenrodYellow和Brown。
7.3.2 SolidColorBrush
SolidColorBrush是最简单的笔刷。它使用一种颜色给整个区域上色。它只有一个属性,Color。注意到这个颜色允许使用透明度,尽管在单词中使用了Solid。
我们已经看到广泛使用SolidColorBrush,即使我们并未提供名称涉及它。这是因为WPF创建这种类型的笔刷,一旦你详细指出了标记中颜色的名称。如果你
大都在标记中工作,你会很少需要指出你需要一个SolidColorBrush,因为你会得到一个默认值。(通常你会详细指明它以完整的词,唯一的原因是你想使用笔刷属性的数据绑定)。考虑下面这个示例:
xaml编译器会认出Yellow为Color类中一个标准命名的颜色,它会提供一个合适的SolidColorBrush。(参见附录A获取更多xaml映射字符串到属性值的信息)。这就不需要创建一个笔刷,因为存在一个Brushes类,在Colors中为每一个命名颜色提供了一组笔刷。
如果你的标记使用了数字颜色值,你还可以被提供一个SolidColorBush。示例7-26显示了各种各样带有数字颜色值的示例。它们都开始于一个#记号,并包含16进制的数字。一个由3个数字组成的号码,每一个数字分别表示红、黄、蓝。四个数字的号码解释为alpaa、红、黄、蓝。这些是紧凑的格式,但是仅为每个通道提供了4位。6到8个数字的号码允许RGB或ARGB各自为每个通道8位。(为了开发完全的16位精确度的scRGB,你需要使用属性-元素语法来设置属性。这里没有简化的文本。参见附录A获取更多关于xaml属性-元素的语法信息)
示例7-26
<Rectangle Fill="#1168ff" Width="50" Height="40" />
<Rectangle Fill="#8ff0" Width="130" Height="10" />
<Rectangle Fill="#72ff8890" Width="70" Height="30" />
SolidColorBrush是轻量的和直接的。然而,它导致了相当flat-looking的可视化。WPF提供更多有趣的笔刷,如果你想制作自己的用户界面——看起来更吸引人的。
7.3.3 LinearGradientBrush
使用LinearGradientBrush,被绘制的区域从一种颜色过渡为另一种颜色,或者即使一个颜色的序列。图7-32显示了一个简单的示例。
图7-32
这个笔刷从黑色淡出为白色,开始于左上角,结束于右下角。这种淡出总是以直线进行,你不能进行曲线的过渡,因此命名为“Linear”。示例7-27显示了图7-32的标记。
示例7-27
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Black" Offset="0" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
StartPoint和EndPoint属性指明了颜色过渡的开始和终结位置。这些坐标是相对于被填充区域的,因此(0,0)是左上角,(1,1)是右下角,如图7-33所示。(注意到如果笔刷绘制或宽或窄的区域,坐标系统从而是积压的)
图7-33
示例7-27使用了属性-元素语法来初始化笔刷。在这个特定的示例中,这不是严格必要的,因为xaml支持更复杂的语法。这严密地等价于示例7-27:
这种紧凑地语法允许设置最重要的方面。两对数字对应到StartPoint和EndPoint属性,以及剩余地两个条目是颜色名称。如果你想这种渐变是垂直的或水平的,你可以使用更简单的语法。
<Rectangle Width=”80” Height=”60” Fill=”HorizontalGradient Black White” />
在所有这些字符串表示中,开始和结束颜色都是详细指定的。这些符合示例7-27中的GradientStop元素。注意到在充分延伸的复合属性的版本中,每个GradientStop都有像Color的一个Offset属性。这支持了更详细的样式来完成字符表达式不能做的事情。它允许填充来传递多个颜色。示例7-28显示了带有多个颜色的LinearGradientBrush。
示例7-28
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<LinearGradientBrush.GradientStops>
<GradientStop Color="Black" Offset="0" />
<GradientStop Color="Orange" Offset="0.2" />
<GradientStop Color="Red" Offset="0.4" />
<GradientStop Color="Black" Offset="0.6" />
<GradientStop Color="Blue" Offset="0.8" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
结果显示在图7-34中。注意到早期显示的速记字符串的语法,并不支持多个颜色值。如果你想要这个效果,你不得不充分地使用属性-元素语法。
图7-34
LinearGradientBrush经常用于添加深度感到用户界面。示例7-29显示了一个典型的例子。它仅使用了两个形状——一对圆角矩形元素。(Grid不直接对外观产生影响。这使得调整图形的大小更容易——改变grid的Width和Height将会引起所有矩形的适当地调整大小。)第二个矩形是渐变填充的,从部分透明的白色渐变到完全透明的颜色,这将提供一个有趣的可视化效果。
示例7-29
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Grid.RowSpan="2" RadiusX="13" RadiusY="13"
Fill="VerticalGradient Green DarkGreen"
Stroke="VerticalGradient Black LightGray" />
<Rectangle Margin="3,2" RadiusX="8" RadiusY="12"
Fill="VerticalGradient #dfff #0fff" />
</Grid>
图7-35显示了结果。这是极其简单的一个图形,只包括两个形状。渐变填充的使用增加了深度,否则这些形状是不会传输的。
图7-35
7.3.4 RadialGradientBrush
RadialGradientBrush非常类似于LinearGradientBrush。它们都允许转变经历一系列的颜色。但是当LinearGradientBrush用直线绘制这些转变时,RadialGradientBrush渐变从一个外部的开始点,到一个椭圆的边界。这就展开了更多的机会使你的用户界面看起来不是很单调,如实例7-30所示。
示例7-30
<Rectangle.Fill>
<RadialGradientBrush Center="0.5,0.4" RadiusX="0.3" RadiusY="0.5"
GradientOrigin="0.25,0.4">
<RadialGradientBrush.GradientStops>
<GradientStop Color="White" Offset="0" />
<GradientStop Color="DarkBlue" Offset="1" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Rectangle.Fill>
</Rectangle>
RadialGradientBrush使用一个GradientStop列表对象来决定填充进行的颜色,正如LinearGradientBrush。这个示例使用了RadiusX和RadiusY属性来决定椭圆边界的大小,以及Center属性来设置椭圆的位置。这里选择的值使得填充边界完全的适合这个形状,正如示例7-36所示。这个形状落在了边界的外面,它的区域由最后的GradientStop的颜色填充。注意到填充的焦点在左边。这是因为GradientOrigin已经被设置了。(默认的焦点在椭圆中间。)
图7-36
示例7-30使得看到RadialGradientBrush属性的效果更容易,但这不是一个激动人心的例子。示例7-31显示了有点挑战的东西。这类似于示例7-29,它使用了少量的带有渐变填充的形状,来传输深度感和反射感,但是这次使用的是放射性填充。
示例7-31
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="20*" />
<RowDefinition Height="6*" />
</Grid.RowDefinitions>
<Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3" Margin="0.5">
<Ellipse.Fill>
<RadialGradientBrush Center="0.5,0.9" RadiusX="0.7" RadiusY="0.5">
<RadialGradientBrush.GradientStops>
<GradientStop Color="PaleGreen" Offset="0" />
<GradientStop Color="Green" Offset="1" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Grid.Row="1" Grid.Column="1">
<Ellipse.Fill>
<RadialGradientBrush Center="0.5,0.1" RadiusX="0.7" RadiusY="0.5">
<RadialGradientBrush.GradientStops>
<GradientStop Color="#efff" Offset="0" />
<GradientStop Color="Transparent" Offset="1" />
</RadialGradientBrush.GradientStops>
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Grid.RowSpan="3" Grid.ColumnSpan="3"
Stroke="VerticalGradient Gray LightGray" />
</Grid>
这次,使用了三种椭圆,其中两种使用了RadialGradientBrush填充,另一种使用了LinearGradientBrush绘制轮廓。第一个椭圆的填充,创建了发光体在图中的下方。第二个椭圆的填充添加了一个反光在图形的上方。第三个椭圆绘制了一条贝赛尔曲线在椭圆的外围。结果如图7-37所示。放射性填充建议使用一个曲面以及给这个图形一个些微透明的外观。
图7-37
7.3.5 ImageBrush和VisualBrush、DrawingBrush
使用某种类型的样式或者图片填充形状,这经常是有用的。WPF提供了三种笔刷,从而允许我们使用无论什么图形作为笔刷。ImageBrush让我们使用一张图片进行绘制。而DrawingBrush允许我们使用可伸缩的图形。VisualBrush允许我们使用任意的标记作为笔刷的图像。我们可以,有效地,使用我们的用户界面的一部分,来绘制另一部分。
7.3.5.1 TiteBrush
ImageBrush和VisualBrush、DrawingBrush都使用源图片的某种形式来进行绘制。它们的基类,TiteBrush,决定了如何延展源图片来填充有效的空间,是否要重复图像,以及如何定位形状中的一个图像。(TiteBrush是一个抽象的基类,因此你可以直接使用它。它存在用来为ImageBrush和VisualBrush、DrawingBrush定义公共的API)
图7-38显示了默认的TiteBrush行为。这个图像显示了3个矩形,为了你可以看到当笔刷变宽或变窄时发生了什么,同样的效果当笔刷形状匹配目标区域的形状。所有这三个矩形都使用ImageBrush绘制的,明确指出了所需的图像。
图7-38
当图7-38用一个ImageBrush绘制的时候,这个行为将会是和任何TiteBrush一样。不久我们将要看到ImageBrush更多的细节,但是目前示例7-32显示了基本的用法。
示例7-32
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.jpg" />
</Rectangle.Fill>
</Rectangle>
笔刷伸展了源图像来填充有效的区域。我们可以通过修改笔刷的Stretch属性来改变这个行为。默认值为Fill,但是我们可以通过指定为None以原始大小显示图像,正如示例7-33所示。
示例7-33
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.jpg" Stretch="None" />
</Rectangle.Fill>
</Rectangle>
这就保持了屏幕的宽高比,但是如果图像很大,它就会简单的裁剪以适应有效的空间,正如示例7-39。
图7-39
为了显示这些图片,你可能更想伸展图像以匹配有效的空间,而不用扭曲屏幕的宽高比。TiteBrush支持Uniform伸展模式,显示如图7-40。缩减源图像从而使它完全在有效的空间里。
图7-40
Uniform伸展模式典型地导致了图像小于被填充的区域。默认的行为是绘制图像,无论对齐属性指明在哪里,以及使余下的区域为透明的。然而,你可以为多余的空间选择其他的行为——通过TiteMode属性。默认为None,但是如果你指定了Tite,如示例7-34,这个图像就会被重复填充多余的空间。
示例7-34
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.jpg"
Stretch="Uniform" TileMode="Tile" />
</Rectangle.Fill>
</Rectangle>
图7-41显示了Uniform伸展模式和Tite图块模式的效果。这里有一个潜在的关于图块的问题。这经常使非常明显的在每一个图块开始重复的地方。如果你的目标使简单地用一种纹理填充一个区域,这种不连续可能有点刺眼。为了减轻这个问题,TiteBrush提供了3种其它图块模式:FlipX、FlipY和FlipXY。这些模式交替镜像图形,如图7-42所示。虽然镜像可以减少图块间地不连续,对于一些源文件而言,它可以非常充分地改变笔刷的外观。
图7-41
图7-42
一个折中的使用图块的方法是,伸缩图片从而它完全填充有效的空间,同时保持了屏幕的宽高比,以及如果需要就在一个方向上裁减。UniformToFill伸展模式实现了这个方法,显示如图7-43。
如果你要使用某种不重复样式的模式来填充一个区域,UniformToFill是最恰当的,因为它保证了可以绘制整个区域。这可能是不太恰当的——如果你的目标是简单的显示一个图像,如图7-43所示,这种伸缩模式会在必要的时候裁剪图像。如果你想要显示整张图片,Uniform是最好的选择。
图7-43
除Fill之外,所有的伸展模式存在一个额外的问题:如何定位这个图像。使用None和UniformToFill,裁剪动作发生了,因此WPF需要决定要显示哪一部分。使用Uniform,图片可能比需要填充的空间小,因此WPF需要决定把它放在哪里。
图片默认是居中的。在示例中(图7-39和图7-43),最中间的部分显示在图片被裁剪的位置。在Uniform的情形中,在图像比被绘制区域小的地方,图像被放置在区域中间位置(图7-40)。你可以使用AlignmentX和AlignmentY属性来改变这一点。这两个属性可以分别设置为Left、Middle或Right和Top、Middle或Bottom。示例7-35再次显示了UniformToFill模式,但是折椅此使用了Left和Bottom的对齐方式。图7-44显示了结果。
示例7-35
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.jpg" Stretch="UniformToFill"
AlignmentX="Left" AlignmentY="Bottom" />
</Rectangle.Fill>
</Rectangle>
图7-44
伸展和对齐属性是方便于使用的,但是它们不允许你选择在图形的任意部分设置焦点或详细指出缩放因素。TileBrush通过ViewBox、Viewport和ViewportUnits来支持这些样式。
ViewBox属性选择显示图像的一部分。默认的,这个属性设置为环绕整张图片,但是你可以改变它的焦点在一个特定的部分。图7-45显示了UniformToFill模式,但是使用一个ViewBox设置放大车子的前面部分。
图7-45
如示例7-36所示,ViewBox被详细指定为4个数字。前面两个是ViewBox左上角的坐标,后两个是ViewBox的宽度和高度。这些坐标是相对于圆图像的。在这种情形下,因为使用了一个ImageBrush,这些就都是源图像中的坐标。在DrawingBrush和VisualBrush的情形下,ViewBox会使用绘制源的坐标系统。
示例7-36
ImageSource="Images\Moggie.jpg" />
Viewbox使你可以选择定焦到源图像的哪个部分,而Viewport使你选择使用笔刷的图像结束在哪个位置。它的功能与对齐属性交叠,但是Viewport允许更多的控制。
图7-46说明了ViewBox和Viewport的联系。在左边,我们看到这种情形的源图像,但是它可能还是一个绘图或者可视化树。Viewbox详细指出了源图像的区域。在右边,我们看到了笔刷。Viewport详细指出了笔刷所在的区域。WPF将会伸缩和定位源图像,从而在Viewbox中详细指出的区域终结于被Viewport在被绘制区域详细指出区域。
Viewport并没有详细指出笔刷的范围。正如示例7-46显示,笔刷并不需要和Viewport的大小一样。笔刷不可以被裁剪到Viewport的大小。所有Viewport和Viewbox做的是确定源图像用目标笔刷绘制的比例和位置。示例7-37显示了Viewport和Viewbox的设置,对应到图7-46中的高亮区域。
图7-46
示例7-37
Viewbox="380,285,308,243"
Viewport="0.1,0.321,0.7, 0.557"
ImageSource="Images\Moggie.jpg" />
注意到示例7-37中,当Viewbox的坐标是相对于源图像的,Viewport适用0到1间的数值。默认的,Viewport坐标系统是基于笔刷的完全大小,(0,0)是在左上角,而(1,1)是在右下角。这意味着由Brush显示的图像区域总是相同的,不管笔刷的大小。这就导致了一个扭曲的行为——相似于默认的StretchMode的Fill,如图7-47所示。(事实上,Fill伸展模式等价于Viewbox设置为源图像的大小,以及Viewpoint为“0,0,1,1”。)
你可以使用ViewportUnit属性为Viewport详细指定不同的单位。默认为RelativeToBoundingBox,但是一旦你将它改为Absolute,这个Viewport使用用户界面的坐标系统来测量。
记住所有的伸缩和定位功能,对于所有派生于TileBrush的笔刷都是公共的。我们现在将来看一下详细指出独立的笔刷的样式。
7.3.5.2 ImageBrush
ImageBrush使用一个图片来绘制屏幕上的区域。ImageBrush用于在前面的章节创建所有图片。这个笔刷是直接了当的,你可以简单地告诉它使用哪一个图片在ImageSource属性上,正如示例7-38所示。
图7-47
示例7-38
<Rectangle.Fill>
<ImageBrush ImageSource="Images\Moggie.jpg" />
</Rectangle.Fill>
</Rectangle>
为了使图片文件对ImageBrush是有效的,简单地在VS2008中把它添加到你的工程中。示例7-38中这个文件在这个工程的名为Images的子目录中。这个图片必须作为一个资源嵌入到工程中。为了做到这一点,在VS2008中的“解决方案”面板中选择一个图片,接着在“属性”面板中,确保它的BuildAction属性被设置为Resource。这就嵌入这个图片到可执行程序中,支持ImageBrush在运行时找到它。(参见第6章获取更多关于管理二进制资源的信息。)可选择性的,你可以为这个属性详细指明一个绝对路径,例如,你可以显示一个来自网站的图像。
ImageBrush非常乐于处理带有透明通道的图像(又被称为“alpha”通道)。并不是所有的图像格式支持局部透明,但是,一些是可以的,如PNG和BMP格式。(其次,GIF也是可以的。它们也支持完全透明和完全不透明的像素。这时有效的1位alpha通道。)ImageBrush将给它特殊的权利,在alpha通道出现的地方。
7.3.5.3 DrawingBrush
ImageBrush是便利的,如果你有一个绘制时需要的图片。然而,图像并不非常适合于独立的分辨率。ImageBrush将为你的屏幕分辨率正确地缩放图像,但是在缩放时,图片易于变得模糊。DrawingBrush不会为这个问题所累,因为你通常提供一个可缩放的向量图作为它的资源。这就支持一个DrawingBrush在任意大小和分辨率保持清晰的和锐利的。
这个向量图由一个Drawing对象表示。这时一个抽象的基类。形状可以用GeometryDrawing绘制,这就允许你使用所有相同的由Path支持的几何体元素来构造绘图。你还可以通过ImageDrawing和VideoDrawing来使用图片和视频。文本由GlyphRunDrawing支持。最后,你可以使用DrawingGroup将这些联合在一起。
即使你只使用形状,你仍然可能想要通过DrawingGroup来分组这些形状。每个GeometryDrawing都有效的等同于一个单独的Path,因此如果你想使用不同的钢笔和笔刷来绘制,或者如果你想你的形状是交叠的而不是联合的,你将需要使用多个GeometryDrawing元素。示例7-39显示了使用一个Fill为DrawingBrush的Rectangle。
示例7-39
<Rectangle.Fill>
<DrawingBrush>
<DrawingBrush.Drawing>
<DrawingGroup>
<DrawingGroup.Children>
<GeometryDrawing Brush="VerticalGradient Green DarkGreen">
<GeometryDrawing.Pen>
<Pen Thickness="0.02"
Brush="VerticalGradient Black LightGray" />
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<RectangleGeometry RadiusX="0.2" RadiusY="0.5"
Rect="0.02,0.02,0.96,0.96" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="VerticalGradient #dfff #0fff">
<GeometryDrawing.Geometry>
<RectangleGeometry RadiusX="0.1" RadiusY="0.5"
Rect="0.1,0.07,0.8,0.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup.Children>
</DrawingGroup>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
示例7-39绘制了与早期图7-35同样的可视化。因为组成绘图的每一个矩形元素,使用了不同的线性渐变填充,它们都有自己的GeometryDrawing,嵌入到DrawingGroup中。
使用DrawingBrush,Viewbox默认为(0,0,1,1)。示例7-39中所有的坐标和大小都相对于坐标系统。如果你想要在一个更广范围的坐标上工作,你可以简单地设置Viewbox为你需要的范围。我们已经看到在示例7-36中如何使用Viewbox。使用DrawingBrush的唯一区别是,你使用它表示一个绘图区域,而不是一个图片。
注意到,我们可以使用Viewbox在图片的一小部分上设定焦点,正如我们早期使用ImageBrush一样。在示例7-39中,我们可以修改DrawingBrush以使用一个更小的Viewbox,正如图7-40所示。
示例7-40
结果是大多数的绘图现在都在Viewbox的外面,因此这个笔刷只显示了全部绘图的一部分,如图7-48显示的那样。
图7-48
DrawingBrush是极其强大的,正如它让你或多或少使用任意你喜欢的图形,如笔刷,以及因为它是基于向量的,结果在任意比例都保持为易碎的。它由一个缺点,如果你从标记中使用它,尽管:它有点笨重的——从xaml中使用。考虑一下示例7-39,生成了和示例7-29同样的外观,但是这些示例分别是28条线段长和10条线段长。
Drawing是非常麻烦的,因为它需要我们与几何体对象一起工作,而不是高级别结构如在示例7-29中使用的Grid或Rectangle。(注意到这么一个不是很敏感的问题,当在代码中使用这个笔刷时,这里高级别对象不再比几何体使用便利)。因此这确实是一个xaml的问题。幸运的是,VisualBrush运行我们使用这些高级别对象来绘制。
7.3.5.4 VisualBrush
VisualBrush可以使用任意派生自Visual的元素的内容来绘制。由于Visual是所有WPF用户界面元素的基类,这就意味着实际上你可以插入任何你想要的标记在一个VisualBrush中。示例7-41显示了使用VisualBrush填充的一个Rectangle。
示例7-41
<Rectangle.Fill>
<VisualBrush Viewbox="0,0,80,26">
<VisualBrush.Visual>
<Grid Width="80" Height="26">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Grid.RowSpan="2" RadiusX="13" RadiusY="13"
Fill="VerticalGradient Green DarkGreen"
Stroke="VerticalGradient Black LightGray" />
<Rectangle Margin="3,2" RadiusX="8" RadiusY="12"
Fill="VerticalGradient #dfff #0fff" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
在示例7-41中,笔刷的可视化是直接从示例7-29复制过来的,导致了一个更简单的等同于DrawingBrush的笔刷。(这个结果看起来非常像图7-35。VisualBrush的要点在于,它绘制区域就像它包装的可视化。)
你可能想知道究竟为什么要使用DrawingBrush,既然VisualBrush是非常简单的?一个原因是DrawingBrush更加有效。一个绘图不为每一个基础绘图承载一个完整的FrameworkElement性能消耗。虽然创建一个DrawingBrush可以更有效,它在运行时消费了比较少的资源。如果你想你的用户界面有独特的复杂的可视化,DrawingBrush将支持你以很少的消耗这么做。如果你打算使用动画,这种低消耗可能转变为更加平滑外观的动画。
DrawingBrush使得创建一个笔刷——像你的用户界面的某一部分——是非常容易的。你可能使用它来创建反光效果,或使用户界面看起来是在三维中旋转的。(这就超越了本书的范围。)
7.3.6 Pen
笔刷用于填充形状的内部。为了绘制形状的轮廓,WPF需要更多一些的信息,不仅需要一个笔刷为屏幕的区域着色,还需要知道绘制的线段有多厚以及你是否需要一个虚线样式和一个密封盖。Pen类提供了这些信息。
Pen总是基于笔刷的,意味着到目前为止所有我们看到的绘图效果,在我们绘制轮廓的时候,都可以使用它。笔刷使用Brush属性来设置。
#记住如果你与任何高级别的形状元素一起工作,你将不会直接和Pen一起工作。Pen用于盖子下面,但是你间接地设置所有属性。表7-1显示了Shape属性是如何对应到Pen属性的。你可以典型地直接处理Pen,仅在你工作在比较低的级别时,正如DrawingBrush中的GeometryDrawing。
线段的宽度由Thickness属性设置。为了简单的轮廓,这和Brush可能是你设置的仅有的属性。然而,Pen可以提供更多。例如,你可以设置一个虚线样式在DashArray属性上。这是一个简单的数字数组。每个数字对应到虚线框中特别的部分的长度。示例7-42说明了最简单的可能的样式。
示例7-42
这说明了虚线样式的第一个片段是一个长度。虚线样式进行了重复,以及由于仅有的一个长度片段已经被详细指定了,每一个片段都是长度1。示例7-49显示了结果。
图7-49
示例7-43显示了两个略微比较有趣的样式序列。注意到第二个例子提供了一个奇数的序列。这意味着第一次环绕,实心片段为6以及间隙大小为1,但是当重复一个序列时,实心片段将会是长度1以及间隙大小为6。因此,虚线样式的有效长度被加倍了。这两种样式的结果都在图7-50中显示。
示例7-43
<Rectangle Stroke="Black" StrokeThickness="5" StrokeDashArray="6 1 6" />
图7-50
拐角处被绘制成三种不同的方式。LineJoin属性可以被设置为Miter、Bevel或者Round,从左到右显示在图7-51中。
图7-51
对于开放的形状如Line或PolyLine,你可以详细指出形状的开始线段和结束线段——通过StartLineCap和EndLineCap属性。这些属性支持四种样式的洋葱皮:Round、Triangle、Flat和Squate,在图7-52中从上到下显示。Flat和Square在线段的终端都是方方正正的。区别在于Flat,扁平的终端交叉了线段的终点,但是对于Square,它的扩展超越了其自身。它跨越线段的数量等同于线段厚度的一半。
图7-52