在WPF用户界面中,绘制2D图形内容的最简单方法是使用形状(shape)——专门用于表示简单的直线、椭圆、矩形以及多变形的一些类。从技术角度看,形状就是所谓的绘图图元(primitive)。可组合这些基本元素来创建更复杂的图形。
关于WPF中形状的重要细节是,它们都继承自FrameworkElement类。因此,形状是元素。这样会带来许多重要的结果:
- 形状绘制自身。不需要管理无效的情况和绘图过程。例如,当移动内容、改变窗口尺寸或改变形状属性时,不需要手动重新绘制形状。
- 使用与其他元素相同的方式组织形状。换句话说,可在前面学过的任何布局容器中放置形状(尽管Canvas明显是最有用的容器,因为它允许在特定的坐标位置放置形状,当构建复杂的具有多个部分的图画时,这很重要)。
- 形状支持与其他元素相同的事件。这意味着为了处理焦点、按下键盘、移动鼠标以及单击鼠标等,不必执行任何额外工作。可使用用于其他元素的相同事件集,并同样支持工具提示、上下文菜单和拖放操作。
一、Shape类
每个形状都继承自抽象类System.Windows.Shapes.Shape。下图显示了形状类的继承层次。
图 WPF形状类
正如上面看到的,相对来说,只有很少一部分类继承自Shape类。Line、Ellipse以及Rectangle都很直观,Polyline是一系列相互连接的直线,Polygon是由一系列相互连接的直线形成的闭合图形。最后,Path类功能强大,能将多个基本形状组合成单独的元素。
尽管Shape类自身不能执行任何工作,但它定义了少量的重要属性。下表列出了这些属性。
表 Shape类的属性
二、矩形和椭圆
矩形和椭圆是两个最简单的形状。为创建矩形或椭圆,需要设置大家熟悉的Height和Width属性(这两个属性继承自FrameworkElement类)来定义形状的尺寸,然后设置Fill或Stroke属性(或同时设置这两个属性)使形状可见。还可以使用MinHeigth、MinWidth、HorizontalAlignment、VerticalAlignment以及Margin等属性。
下面举一个简单示例,该例在StackPanel面板上放置了一个椭圆和一个矩形,效果图如下所示:
<StackPanel> <Ellipse Fill="Yellow" Stroke="Blue" Height="50" Width="100" Margin="5" HorizontalAlignment="Left"></Ellipse> <Rectangle Fill="Yellow" Stroke="Blue" Height="50" Width="100" Margin="5" HorizontalAlignment="Left"></Rectangle> </StackPanel>
Ellipse类没有增加任何属性。Rectangle类只增加了两个属性:RadiusX和RadiusY。如果将这两个属性的值设为非零值,就可以创建出美观的圆形拐角。
可认为RadiusX和RadiusY属性是用于填充矩形拐角的椭圆。例如,如果将这两个属性都设为10,WPF会使用10个单位宽的圆形边缘绘制拐角。随着半径的增大,矩形拐角的更多部分会被替换。如果增加RadiusY属性的值,使其大于RadiusX属性的值,矩形拐角的左边和右边会更平缓,而顶部和底边的边缘会更尖锐。如果增大RadiusX属性的值,使其等于矩形宽度,并增加RadiusY属性的值,使其等于矩形的宽度,矩形最后会变成普通的椭圆。如下图所示:
<Window x:Class="Drawing.RoundedRectangles" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="RoundedRectangles" Height="447.744" Width="300"> <StackPanel> <TextBlock Margin="5,5,0,0">Corner radius of 5.</TextBlock> <Rectangle Fill="Yellow" Stroke="Blue" RadiusX="5" RadiusY="5" Width="100" Height="60" Margin="5" HorizontalAlignment="Left"> </Rectangle> <TextBlock Margin="5,5,0,0">Corner radius of 10.</TextBlock> <Rectangle Fill="Yellow" Stroke="Blue" RadiusX="10" RadiusY="10" Width="100" Height="60" Margin="5" HorizontalAlignment="Left"></Rectangle> <TextBlock Margin="5,5,0,0">Corner radius of 10 (X) and 25 (Y).</TextBlock> <Rectangle Fill="Yellow" Stroke="Blue" RadiusX="10" RadiusY="25" Width="100" Height="60" Margin="5" HorizontalAlignment="Left"></Rectangle> <TextBlock Margin="5,5,0,0">Corner radius of 100 (X) and 60 (Y).</TextBlock> <Rectangle Fill="Yellow" Stroke="Blue" RadiusX="100" RadiusY="60" Width="100" Height="60" Margin="5" HorizontalAlignment="Left"></Rectangle> </StackPanel> </Window>
三、改变形状的尺寸和放置形状
正如前面所知,赢编码尺寸通常不是创建用户界面的理想方法。它们会限制处理动态内容的能力,并会使应用程序本地化到其他语言变得更加困难。
当绘制形状时,不再总是关心这些问题。通常,需要更严格地控制形状的位置。然而,在许多情况下仍需要灵活一点设计。Ellipse和Rectangle为了适应可用的空间,都能自动改变自身。
如果为提供Height和Width属性,形状会根据它们的容器来设置自身的尺寸。在上一个示例中,如果删除Height和Width值(并且不设置MinHeight和MinWidth值),就会导致形状缩小到看不见,因为StackPanel面板为了适应其内容改变了尺寸。然而,如果强制StackPanel面板的宽度为整个窗口的宽度(通过将HorizontalAlignment属性设置为Stretch),并将椭圆的HorizontalAlignment属性设置为Stretch,删除椭圆的Width属性值,这时椭圆的宽度就是整个窗口的宽度。
可使用Grid容器构造更好的示例。如果使用按比例改变行尺寸的行为(默认行为),就可使用下面更精简的标记创建填满窗口的椭圆:
<Grid> <Ellipse Fill="Yellow" Stroke="Blue"></Ellipse> </Grid>
在上面的标记中,Grid面板填满了整个窗口。Grid面板包含了一个按比例改变尺寸的行,该行填满了整个Grid面板。最后,椭圆填满了整行。
改变形状尺寸的行为依赖于Stretch属性的值(该属性在Shape类中定义)。默认情况下,该属性被设置为Fill。如果改变指定明确的尺寸,这一设置会拉伸形状,使其填满容器。下表列出了Stretch属性的所有可能值。
表 Stretch枚举值
下图显示了Fill、Uniform、UniformToFill枚举值之间的区别.
<Window x:Class="Drawing.FillModes" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="FillModes" Height="270" Width="477" > <Grid ShowGridLines="True" Margin="5"> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <Ellipse Fill="Yellow" Stroke="Blue"></Ellipse> <Ellipse Fill="Yellow" Stroke="Blue" Grid.Column="1" Stretch="Uniform"></Ellipse> <Ellipse Fill="Yellow" Stroke="Blue" Grid.Column="2" Stretch="UniformToFill "></Ellipse> <TextBlock Grid.Row="1" TextAlignment="Center">Fill</TextBlock> <TextBlock Grid.Row="1" Grid.Column="1" TextAlignment="Center">Uniform</TextBlock> <TextBlock Grid.Row="1" Grid.Column="2" TextAlignment="Center">UniformToFill</TextBlock> </Grid> </Window>
通常,将Stretch的值设置为Fill相当于将HorizontalAlignment和VerticalAlignment属性设置为Stretch。但如果选择为形状设置固定的宽度和高度,二者就有区别了。对于这种情况,会简单地忽略HorizontalAlignment和VerticalAlignment值。而Stretch设置仍然起作用——该属性决定如何在给定的范围内改变形状内容的尺寸。
到目前位置,已看到如何改变Rectangle和Ellipse形状的尺寸,但如何准确地将它们放到期望的位置呢?WPF形状与其他元素使用相同的布局系统。然而,有些布局容器是不合适的。例如,通常不希望使用StackPanel、DockPanel以及WrapPanel面板,因为它们都被设计为独立的元素。Grid面板更灵活一些,因为它允许在同一个单元格中放置任意多个元素(尽管不能在单元格中的不同部分定位矩形和椭圆)。理想容器是Canvas,该容器要求使用Left、Top、Right或Bottom附加属性,为每个形状指定坐标。这样可以完全控制形状如何相互重叠:
<Canvas> <Ellipse Fill="Yellow" Stroke="Blue" Height="50" Width="100" Canvas.Top="50" Canvas.Left="100"></Ellipse> <Rectangle Fill="Yellow" Stroke="Blue" Height="50" Width="100" Canvas.Top="40" Canvas.Left="30"></Rectangle> </Canvas>
如果使用Canvas容器,标签的顺序是很重要的。在上面的示例中,矩形叠加在椭圆之上,因为在标签列表中首先出现的是椭圆,所以首先绘制椭圆。
请记住,Canvas容器不在需要占据整个窗口。例如,完全可以创建一个Grid面板,并在该Grid面板的某个单元格中使用Canvas容器,对于在可自由流动的动态用户界面中锁定一小部分绘图逻辑,这是一种非常好的方法。
四、使用Viewbox控件缩放形状
使用Canvas控件的唯一限制是图形不能改变自身的尺寸以适应更大或更小的窗口。对于按钮这非常合理(在这些情况下,按钮不改变尺寸),但是对于其他类似的图形内容,情况就未必如此了。
对于此类情况,WPF提供了简便的解决方法。如果希望联合Canvas控件的精确控制功能和方便的改变尺寸功能,可使用Viewbox元素。
Viewbox是继承自Decorator的简单类。该类只接受一个子元素,并拉伸或缩小子元素以适应可用的空间。当然,这个单一的子元素可以是布局容器,其中包含大量形状(或其他元素),这些元素将同步地改变尺寸。然而,Viewbox更长用于矢量图像而不是普通控件。
尽管可在Viewbox元素中放置单个形状,但这并不能提供任何实际的优点。反而,当需要封装构成一幅图画(drawing)的一组形状时,Viewbox元素才有用处。通常,将在Viewbox控件中放置Canvas,并在Canvas面板中放置形状。
下面的示例在Grid控件的第二行中放置了一个包含Canvas面板的Viewbox元素。Viewbox元素占用改行的整个高度和宽度。该行占用绘制自动改变尺寸的第一行剩余的所有空间,下面是标记:
<Grid Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <TextBlock> The first row of a grid</TextBlock> <Viewbox Grid.Row="1" HorizontalAlignment="Left" MaxHeight="500"> <Canvas Width="200" Height="150"> <Ellipse Fill="Yellow" Stroke="Blue" Canvas.Left="10" Canvas.Top="50" Width="100" Height="50" HorizontalAlignment="Left"></Ellipse> <Rectangle Fill="Yellow" Stroke="Blue" Canvas.Left="30" Canvas.Top="40" Width="100" Height="50" HorizontalAlignment="Left"></Rectangle> </Canvas> </Viewbox> </Grid>
下图显示了当改变窗口尺寸时,Viewbox控件如何调整自身。第一行没有变化。然而为填满额外控件,第二行进行了扩展。正如看到的,Viewbox控件中的形状也根据窗口增大的比例改变了他们的大小。
默认情况下,Viewbox元素按比例地执行缩放,保持它所包含内容的纵横比。在当前示例中,这意味着即使包含行的形状发生了变化(变宽或变高),内部形状也不会变形。相反,Viewbox元素使用适应可用空间内部的最大缩放系数。然而,可使用Viewbox.Stretch属性改变该行为。默认情况下,将该属性设置为Uniform。可将其改变为Fill,Viewbox元素中的内容会在两个方向上被拉伸以完全适应可用空间,即使可能会破坏原来的绘图也会如此。还可通过使用StretchDirection属性获得更大的控制权。默认情况下,该属性被设置为Both,但可使用UpOnly值创建只会增长而不会收缩超过其原始尺寸的内容,并且可以使用DownOnly创建只会缩小而不会增长的内容。
为时Viewbox元素执行其缩放工作,需要能够确定两部分信息:(如果不放在Viewbox元素中)内容应当具有的原始尺寸和希望内容具有的新尺寸。
第二个细节——新尺寸——非常简单。Viewbox元素根据Stretch属性,让其内部的内容使用所有可用空间,这意味着Viewbox元素越大,其内部的内容就越大。
第一个细节——原始尺寸,不使用Viewbox空间时的尺寸——隐含在定义嵌套内容的方式中。在前面的示例中,Canvas的尺寸被明确设置为200X150单位大小。因此,Viewbox从该开始点缩放图像。例如,椭圆最初是100单位宽,这意味着它占用Canvas面板一半的绘图空间。随着Canvas控件的增大,Viewbox元素会遵循这些比例,并且椭圆继续占用一半的可用控件。
然而,如果删除Canvas控件的Width和Height属性,分析会发生什么情况。现在,Canvas控件的尺寸被设置为0X0单位大小,所以Viewbox控件不能改变它的尺寸,并且嵌套在其中的内容不会显示(这与只使用Canvas控件时的行为不同。因为尽管Canvas控件的尺寸仍设置为0X0,但只要Canvas.ClipToBounds属性没有被设置为true,就仍然允许在Canvas控件之外的区域绘制形状。而Viewbox控件不能容忍这一错误)。
现在分析一下,如果在按比例改变尺寸的Grid面板的单元格中封装Canvas面板,并且没有指定Canvas面板的尺寸,情况又会怎样。如果没有使用Viewbox元素,该方法可工作得很好——拉伸Canvas面板以填充单元格,并且内部的内容是可见的。但如果将所有内容放在Viewbox元素中,这种方法就会失效。Viewbox控件不能确定最初尺寸,因此不能响应地改变Grid面板的尺寸。
可通过直接在能自动改变尺寸的容器(如Grid面板)中放置特定的形状(如Rectangle和Ellipse)来避免这个问题。然后Viewbox控件就能评估Grid面板为了适合其内容所需的最小尺寸,并且缩放Grid面板以适应可用空间。然而,在ViewBox元素中获取真正所希望的尺寸的最简单方法,是在具有固定尺寸的元素中封装内容,可以是Canvas面板、按钮或其他控件。这样,固定尺寸就变成了Viewbox控件进行计算所需要的原始尺寸。以这种方式硬编码尺寸不会限制布局的灵活性,因为Viewbox元素根据可用空间和布局容器按比例改变尺寸。
五、直线
Line形状表示连接一个点和另一个点的一条直线。起点和重点由4个属性设置:X1与Y1(用于第一个点)和X2与Y2(用于第二个点)。例如,下面是一条从点(0,0)伸展到点(10,100)的直线:
<Line Stroke="Blue" X1="0" Y1="0" X2="10" Y2="100"></Line>
对于直线,Fill属性不起作用,必须设置Stroke属性。
在直线中使用的坐标是相对于放置直线的矩形区域左上角的坐标。例如,如果在StackPanel面板上放置上面的直线,坐标(0,0)指向在StackPanel面板上放置该矩形区域的位置,这可能是窗口的左上角,也可能不是。如果StackPanel面板的Margin属性值不为0,或直线在其他元素之后,直线的开始点(0,0)与窗口顶部会有一定的距离。
然而,在直线中使用负坐标值是非常合理的。实际上,可为直线使用能超出为直线保留的空间的坐标,从而在窗口的其他任意部分绘制直线。对于到目前位置介绍的Rectangle和Ellipse形状;这是不可能的。然而,这一模型也有缺点,直线不能使用流内容模型。这意味着为直线设置Margin、HorizontalAlignment以及VerticalAlignment属性是没有意义的,因为它们没有任何效果。对于Polyline和Polygon形状具有相同的限制。
如果在Canvas面板上放置了Line形状,那么仍应用附加的位置属性(如Top和Left)。它们决定直线的开始位置。换句话说,两个直线坐标被平移一定的距离。分析下面的直线:
<Line Stroke="Blue" X1="0" Y1="0" X2="10" Y2="100" Canvas.Left="5" Canvas.Top="100"/>
这条直线从点(0,0)伸展到点(10,100),使用的坐标系统将Canvas控件上的点(5,100)作为点(0,0)。这相当于下面不使用Top和Left属性的直线:
<Line Stroke="Blue" X1="5" Y1="100" X2="15" Y2="200"/>
当在Canvas面板上放置Line形状时,是否使用位置属性由自己决定。通常,可通过选择好的开始点简化直线的绘制,还可以使移动部分图画变得容易。例如,如果在Canvas面板的特定位置绘制几条直线和其他形状,相对于附近的点绘制它们是不错的主意(通过使用相同的Top和Left坐标)。通过这种方法,可根据需要将整个图画移到新的位置。
六、折线
可以通过Polyline类绘制一系列相互连接的直线。只需要使用Points属性提供一系列X和Y坐标。从技术角度看,Points属性需要使用PointCollection对象,但在XAML中使用基于简单字符串的语法填充该集合。只需要提供点的列表,并在每个坐标之间添加空格或逗号。
Polyline形状可能只有两个点。例如下面的Polyline形状,从点(5,100)伸展到点(15,200):
<Polyline Stroke="Blue" Points="5 100 15 200"/>
为便于阅读,可在每个X和Y坐标之间使用逗号:
<Polyline Stroke="Blue" Points="5,100 15,200"/>
下面是绘制的更复杂Polyline形状。点不断右移,并在更高的Y值——比如(50,160),和更低的Y值——比如(70,130)之间摆动:
<Canvas> <Polyline Stroke="Blue" StrokeThickness="5" Points="10,150 30,140 50,160 70,130 90,170 110,120 130,180 150,110 170,190 190,100 210,240" > </Polyline> </Canvas>
下图显示了最终绘制的线条。
对于这个示例,通过代码使用各种相应地自动增加X和Y值的循环填充Point集合可能更容易。如果需要创建高度动态的图形,事实却是如此——例如,根据从数据库中提取的数据集改变其外观的图标。但是,如果只是希望构建固定的图形内容,就根本不需要形状的具体坐标。相反,可使用另一个工具,如Express Design,绘制恰当的图形,然后到处到XAML。
七、多边形
实际上,Polygon和Polyline是相同的。和Polyline类一样,Polygon类也有包含一系列坐标的Points集合。唯一的区别是:Polygon形状添加最后一条线段,将最后一个点连接到开始点(如果最后一个点就是第一个点,Polygon类和Polyline类就没有区别了)。可使用Fill画刷填充该形状的内部区域。通过修改上一节的示例,显示Polygon:
<Canvas> <Polygon Stroke="Blue" StrokeThickness="5" Fill="Yellow" Points="10,150 30,140 50,160 70,130 90,170 110,120 130,180 150,110 170,190 190,100 210,240" > </Polygon> </Canvas>
最终效果图如下所示:
对于线条从不相交的简单形状,填充其内部很容易做到。但有时会遇到更复杂的Polygon形状,哪些部分属于内部(并且应当被填充)以及哪些部分属于外部并不明显。
下面一个示例,该形状的特点是一条线段和其他多条线段相交,可能希望填充也希望不填充中央的不规则区域。显然,可通过将该图像分割成更小的形状来准确地控制填充区域。但不需要这么做。
<Polygon Stroke="Blue" StrokeThickness="1" Fill="Yellow" Points="15,200 68,70 110,200 0,125 135,125" > </Polygon>
每个Polygon和Polyline形状都有FillRule属性,该属性用于从两种填充方法中选择一种来填充区域。默认情况下,FillRule属性被设置为EventOdd。为了确定是否填充区域,WPF计算为了到达形状的外部必须穿过的直线的数量。如果是奇数,就填充区域;如果是偶数,就不填充区域。对于上图显示的中央区域,为了到达形状外部就必须经过两条直线,所以不会填充该区域。
WPF还遵循NonZero填充规则,该规则更加复杂。本质上,当使用NonZero填充规则时,WPF使用和EventOdd填充规则相同的方法计算穿过的直线的数量,但是会考虑经过的每条直线的防线。如果在经过的直线中,在某个方向上(比如从左项右)直线的数量等于相反方向(从右向左)上直线的数量,就不会填充区域。如果这两个直线数量的差不为0,就填充区域。对于上个示例,如果将FillRule属性设置为NonZero,J就会填充内部区域。
<Polygon Stroke="Blue" StrokeThickness="1" Fill="Yellow" FillRule="Nonzero" Points="15,200 68,70 110,200 0,125 135,125" > </Polygon>
有关NonZero规则的复杂问题在于填充设置依赖于形状的绘制,而不是形状自身的外观。
八、直线线帽和直线交点
当绘制Line和Polyline形状时,可使用StartLineCap和EndLineCap属性选择如何绘制直线的开始端和结束端(这些属性不影响其他形状,因为其他形状都是闭合的)。
StartLineCap和EndLineCap属性通常都设为Flat,这意味着直线在它的最后坐标处立即终止。其他选择包括Round(该设置会平滑地绘制拐角),Triangle(绘制直线的两条侧边最后交于一点)以及Square(该设置使直线端口具有尖锐边缘)。这两个设置都会增加直线的长度——换句话说,它们使直线超出了其他情况下的结束位置。额外的距离是直线宽度的一半。下图显示了直线端口处不同线帽之间的区别。
<Window x:Class="Drawing.LineCaps" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="LineCaps" Height="333" Width="376"> <Grid Margin="15"> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> </Grid.ColumnDefinitions> <Polyline Stroke="Blue" StrokeThickness="15" StrokeEndLineCap="Flat" SnapsToDevicePixels="True" Points="10,10 30,0 50,20 90,10 200,10" > </Polyline> <TextBlock Grid.Column="1">Flat Line Cap</TextBlock> <Polyline Stroke="Blue" StrokeThickness="15" Grid.Row="1" StrokeEndLineCap="Square" SnapsToDevicePixels="True" Points="10,10 30,0 50,20 90,10 200,10" > </Polyline> <TextBlock Grid.Row="1" Grid.Column="1">Square Line Cap</TextBlock> <Polyline Stroke="Blue" StrokeThickness="15" Grid.Row="2" StrokeEndLineCap="Round" SnapsToDevicePixels="True" Points="10,10 30,0 50,20 90,10 200,10" > </Polyline> <TextBlock Grid.Row="2" Grid.Column="1">Round Line Cap</TextBlock> <Polyline Stroke="Blue" StrokeThickness="15" Grid.Row="3" StrokeEndLineCap="Triangle" SnapsToDevicePixels="True" Points="10,10 30,0 50,20 90,10 200,10" > </Polyline> <TextBlock Grid.Row="3" Grid.Column="1">Triangle Line Cap</TextBlock> </Grid> </Window>
除Line形状外,所有形状都运行使用StrokeLineJoin属性扭曲它们的拐角,有4中选择。Miter值(默认值)使用尖锐的边缘,Bevel值切掉点边缘,Round值平滑地过渡边缘,Triangle值显示尖点。下图显示StrokeLineJoin效果图。
<Window x:Class="Drawing.LineJoins" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="LineJoins" Height="431" Width="303" > <Grid Margin="15"> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> </Grid.ColumnDefinitions> <Polyline Stroke="Blue" StrokeThickness="14" StrokeLineJoin="Bevel" SnapsToDevicePixels="True" Points="10,60 30,10 50,70 90,40" > </Polyline> <TextBlock Grid.Column="1" VerticalAlignment="Center">Bevel Line Join</TextBlock> <Polyline Stroke="Blue" StrokeThickness="14" Grid.Row="1" StrokeLineJoin="Round" SnapsToDevicePixels="True" Points="10,60 30,10 50,70 90,40" > </Polyline> <TextBlock Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">Round Line Join</TextBlock> <Polyline Grid.Row="2" Stroke="Blue" StrokeThickness="14" StrokeLineJoin="Miter" StrokeMiterLimit="1" SnapsToDevicePixels="True" Points="10,60 30,10 50,70 90,40" > </Polyline> <TextBlock Grid.Row="2" Grid.Column="1" VerticalAlignment="Center">Miter Line Join</TextBlock> <Polyline Grid.Row="3" Stroke="Blue" StrokeThickness="14" StrokeLineJoin="Miter" StrokeMiterLimit="3" SnapsToDevicePixels="True" Points="10,60 30,10 50,70 90,40" > </Polyline> <TextBlock Grid.Row="3" Grid.Column="1" VerticalAlignment="Center">Miter Line Join With Limit of 3</TextBlock> </Grid> </Window>
当为较宽并且角度非常小的直线拐角使用尖锐的边缘时,尖锐的拐角会不切实际地延伸很长一段距离。对于这种情况,可使用Bevel或Round设置修剪拐角。也可使用StrokeMiterLimit属性,当达到特定的最大长度时,该属性自动地剪切边缘。StrokeMiterLimit属性是一个系数,该系数是用于锐化拐角的长度和直线宽度的一半的比值。如果将该属性设置为1(这是默认值),就允许拐角延长直线宽度的一半距离。如果设置为3,就允许拐角延长直线宽度的1.5倍距离。如上图的最后一条直线使用了更高的锐化范围,从而具有更狭长的拐角。
九、点划线
除了为形状的边框绘制乏味的实线外,还可绘制点划线(dashed line)——根据指定的模式使用空白断开的直线。当在WPF中创建一条点划线时,不限制进行特定的预先设置。相反,可通过设置StrokeDashArray属性来选择实线段的长度和断开空白(空白)的长度。例如,分析下面的这条直线:
<Polyline Stroke="Blue" StrokeThickness="10" StrokeDashArray="1 2" Points="10,30 60,0 90,40 120,10 350,10" SnapsToDevicePixels="True"> </Polyline>
这条点划线的实线段长度值为1,空白长度值为2.这些值都是相对于直线宽度的。因此,如果直线宽度是10个单位(本例中设置的宽度),实线部分的长度就为10个单位,后面跟着20个单位的空白部分。直线在整个长度中重复该模式。
另一方面,如果像下面这种交换这两个值:
StrokeDashArray="2 1"
直线的实现部分就是20个单位长,空白部分为10个单位长。下图显示了这着两条直线。正如将会注意到得,当一条非常粗的线段位于拐角处时,它会被不均匀地割断。
<Window x:Class="Drawing.DashedLines" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="DashedLines" Height="401" Width="589" > <Grid Margin="15"> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> </Grid.ColumnDefinitions> <Polyline Stroke="Blue" StrokeThickness="10" StrokeDashArray="1 2" Points="10,30 60,0 90,40 120,10 350,10" SnapsToDevicePixels="True"> </Polyline> <TextBlock Grid.Column="1" VerticalAlignment="Center">Dash Pattern "1 2"</TextBlock> <Polyline Stroke="Blue" StrokeThickness="10" Grid.Row="1" StrokeDashArray="2 1" SnapsToDevicePixels="True" Points="10,30 60,0 90,40 120,10 350,10" > </Polyline> <TextBlock Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">Dash Pattern "2 1"</TextBlock> <Polyline Stroke="Blue" StrokeThickness="10" Grid.Row="2" StrokeDashArray="5 0.2 3 0.2" SnapsToDevicePixels="True" Points="10,30 60,0 90,40 120,10 350,10" > </Polyline> <TextBlock Grid.Row="2" Grid.Column="1" VerticalAlignment="Center">Dash Pattern "5 0.2 3 0.2"</TextBlock> <Polyline Stroke="Blue" StrokeThickness="10" Grid.Row="3" SnapsToDevicePixels="True" StrokeDashArray="3 0.5 2" Points="10,30 60,0 90,40 120,10 350,10" > </Polyline> <TextBlock Grid.Row="3" Grid.Column="1" VerticalAlignment="Center">Uneven Dash Pattern "2 0.5 2"</TextBlock> <Polyline Stroke="Blue" StrokeThickness="10" Grid.Row="4" SnapsToDevicePixels="True" StrokeDashArray="1 2" StrokeDashCap="Round" Points="10,30 60,0 90,40 120,10 350,10" > </Polyline> <TextBlock Grid.Row="4" Grid.Column="1" VerticalAlignment="Center">Dash Pattern with Rounded Caps</TextBlock> </Grid> </Window>
不见得非要使用整数值。例如,下面的StrokeDashArray属性设置完全合理:
StrokeDashArray="5 0.2 3 0.2"
这样的设置提供了更复杂序列——5X10单位长的点划线,然后是0.2X15单位长的空白,接下来是3X10单位长的实线和0.2X10单位长的空白。在该序列的尾部,直线从头开始重复该模式。
如果为StrokeDashArray属性提供的数值的个数是奇数,将发生一个有趣的现象。分析下面的示例:
StrokeDashArray="3 0.5 2"
当绘制该直线时,WPF首先绘制3倍直线宽度长的实线,然后是0.5倍直线宽度长的空白,在接下来时2呗直线宽度长的实线。但当在从头开始重复该模式时,首先是3倍直线宽度长的空白,接着是0.5被直线宽度长的实线,依次类推。本质上,点划线在线段和空白之间交替其模式。
如果希望从中间开始绘制模式,可使用StrokeDashOffset属性,该属性是一个从0开始的索引,该索引指向StrokeDashArray中的某个值。例如,在上一个示例中,如果将StrokeDashOffset属性设置为1,直线将从0.5倍直线宽度长的空白开始。如果设置为2,直线将会从2倍直线宽度长的线段开始。
最后,可控制如何为直线的断开边缘添加线帽。通常是一条平直的边缘,但可将StrokeDashCap属性设置为Bevel、Square以及Triangle等值。请记住,所有这些设置都会在点划线的端点增加直线宽度的一半长距离。如果没有考虑这一额外的距离,最终可能会使点划线相互重叠。解决方法是增加额外的空白以进行补偿。
十、像素对齐
WPF使用与设备无关的绘图系统。为字体和形状等内容指定的数值使用“虚拟”像素,在通常的96dpi显示器上,“虚拟”像素和正常像素的大小相同,但是在更高dpi的显示器上其尺寸会被缩放。换句话说,绘制50像素宽的矩形,根据设备的不同,实际上可能使用更多或更少的像素进行渲染。设备无关单位和物理像素之间的转换会自动进行,并且通常根本不需要考虑这个问题。
不同dpi设置之间的像素比很少是整数。例如, 在96dpi显示器上的50个像素,在120dpi显示器上会变为62.4996个像素(这不是一种错误的情况——实际上,当以设备无关单位提供数值时,WPF始终运行使用非整数的双精度值)。显然,无法在像素之间的点上放置一条边缘。WPF使用反锯齿特性进行补偿。例如,当绘制一条62.4992个像素长的红线,WPF可正常填充前62个像素,然后使用直线颜色(红色)和背景色之间的颜色为第63个像素着色。但在此存在一个问题。如果正在绘制直线、矩形或具有直角的多边形,这种自动反锯齿特性会在形状边缘导致一片模糊区域。
可能会认为,仅在显示分辨率不是96dpi的显示器上运行应用程序时,才会出现这个问题。然而,情况未必如此,因为所有形状都可以都可以使用小数值得长度和坐标设置尺寸,这会引起相同的问题。在绘制形状时,尽管可能没有使用小数值,但可以改变尺寸的形状——那些因为尺寸依赖于容器或被放在Viewbox元素中而被拉伸的形状——尺寸通常几乎总是小数。类似地,奇数单位宽的直线在两侧的像素数也是小数值。
模糊边缘问题未必是问题。实际上,根据正在绘制的图像类型,它可能看起来很正常。然而,如果不希望这种行为,可告诉WPF不要在特定形状使用反锯齿特性进行处理,反而WPF会将尺寸舍入到最近的设备像素。可通过将UIElement类的SnapsToDevicePixels属性设置为true来启动这个称为像素对齐(pixel Snapping)的特性。
为查看两者之间的区别,可观察下图中被放大的窗口,该窗口比较两个矩形。底部的矩形使用了像素对齐特性,而顶部的矩形没有使用。如果仔细观察,就会发现在未使用像素对齐特性的矩形的顶部和左部有一条很细的淡色边缘。
<Window x:Class="Drawing.PixelSnapping" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="PixelSnapping" Height="300" Width="300"> <Grid Margin="7"> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <TextBlock VerticalAlignment="Center">Not Snapped:</TextBlock> <Rectangle SnapsToDevicePixels="False" Grid.Column="1" Margin="10" Height="10" Fill="Red"></Rectangle> <TextBlock VerticalAlignment="Center" Grid.Row="1">Snapped:</TextBlock> <Rectangle SnapsToDevicePixels="True" Grid.Column="1" Grid.Row="1" Margin="10" Height="10" Fill="Red"></Rectangle> </Grid> </Window>