WPF形状、画刷和变换
在许多用户界面技术中,普通控件和自定义绘图之间具有清晰的区别。通常来说,绘图特性只用于特定的应用程序。例如,游戏、数据可视化和物理仿真等。WPF具有一个非常不同的原则。它以相同的方式处理预先构建的控件和自定义绘制的图形。不仅可以使用WPF的绘图支持为用户界面创建付图形的可视化元素,还可以通过他最大限度的利用动画和控件模板等特性。本次分析WPF的2D绘图特性,首先是用于形状回执的基本元素。接着将分析如何使用画刷绘制替他们的边框和内部。然后学习如何使用变换对形状和元素进行旋转、扭曲以及其他操作。最后学习使形状和其他元素半透明。
12.1 理解形状
在WPF用户界面中,绘制2D图形内容的最简单方法是使用形状(shape):专门用于表示简单的直线、椭圆、矩形以及多边形的类。从技术角度来看,形状就是所谓的绘图图元(primitive)。WPF中形状的最重要的细节是,它们都继承自FrameworkElement类。因此,形状是元素。这样的结果是:
- 形状绘制自身。不需要管理无效的情况和绘图过程。例如,当移动内容、改变窗口尺寸或改变形状属性时,不需要手动重新绘制形状。
- 使用与其他元素相同的方式组织形状。也就是可以在任何布局容器中放置一个形状。
- 形状支持与其他元素相同的事件。这意味着为了处理焦点、按下键盘、移动鼠标以及单击鼠标等,不需要进行任何额外的工作。可以使用用于其他元素的相同的事件集,并且同样支持工具提示、上下文菜单和拖放操作。
12.1.1 Shape类
呢个形状都继承自抽象的System.Windows.Shapes.Shape类。如下图显示了形状类的继承层次。
只有很少一部分继承自Shape类。Line、Ellipse以及Rectangle都很直观,Polyline是一系列相互连接的直线Polygon是由一系列相互连接的直线形成的闭合图形。最后,Path类具有非常强大的功能,能够将多个基本形状组合成一个单独的元素。尽管Shape类自身不能执行任何工作,但是它定义了少量的重要属性,如下列表
File | 设置绘制形状表面(边框内的所有内容)的画刷对象 |
Stroke | 设置绘制形状边缘(边框)的画刷对象 |
StrokeThickness | 使用设备无关单位,设置边框的宽度。 |
StrokeStartLineCap 和StrokeEndLineCap |
决定直线开始端和结束端边缘的轮廓。这些属性只影响Line、Polyline以及Path形状。所有其他形状是闭合的,没开始点和结束点 |
StrokeDashArray、 StrokeDashOffset 和StrokeDashCap |
用于在形状周围创建点划线边框。可以控制点划线的尺寸和频率,以及每条点划线开始端和结束端边缘的轮廓 |
StrokeLineJoin和SrokeMiterLimit | 确定形状拐角处的轮廓。对于没有拐角的形状,如Line和Ellipse,这些熟悉不起作用 |
Stretch | 确定形状如何填充可用的区域。可以使用该属性创建能够扩展以适合其容器的形状。还可以为HorizontalAlignment或VerticalAlignmet属性(这些属性继承自FramenworkElement类)使用一个Streth值强制形状在一个方向上扩展 |
DefiningGeometry | 为形状提供一个Geometry对象。Geometry对象描述了形状的坐标和尺寸,不包括UIElement类的相关内容,例如对键盘和鼠标事件的支持。 |
GeometryTransform | 通过该属性可以应用一个Transform对象,改变用于绘制形状的坐标系统。从而可以扭曲、旋转或移动性状。 |
RenderedGeometry | 提供描述最终的、以渲染好的图形的Geometry对象。 |
12.1.2 矩形和椭圆
矩形和椭圆是最简单的形状。为了创建矩形或椭圆,需要设置熟悉的Height和Width属性来定义形状的尺寸,然后设置Fill或Stroke属性使形状可见。如下放置了一个椭圆和一个矩形
<StackPanel> <Ellipse Fill="Yellow" Stroke="Blue" Width="100" Height="50" Margin="5" HorizontalAlignment="Left"></Ellipse> <Rectangle Fill="Yellow" Stroke="Blue" Width="100" Height="50" Margin="5" HorizontalAlignment="Left"></Rectangle> </StackPanel>
Ellipse类没有增加任何属性。Rectangle类只增加了两个属性:RadiusX和RadiusY。如果将这两个属性的值设置为非零值,就可以创建出优美的圆形拐角。随着它们的值的增大矩形拐角部分会更多的被替换。如果一个值大于另一个值的话,值较大的一个方向的拐角会更加平缓,而另一个方向的拐角会更尖锐。如果它们的值分别等于矩形的宽和高时,矩形就会变成椭圆。
12.1.3 改变形状的尺寸和放置形状
硬编码尺寸通常不是创建用户界面的理想方法。它们会限制处理动态内容的能力,并且会使应用程序本地化到去他语言变得更加困难。通常,需要更加严格地控制形状的位置。Ellipse和Rectangle为了适应可用的空间,都能够自动改变它们自身。如果没有提供Height和Width属性,形状会根据他们的容器来设置自身的尺寸。改变形状尺寸的行为依赖于Strech属性的值(该属性是在Shape类中定义的)。默认情况下该属性的值为File。下表中是Stretch属性的所有值。
File | 形状拉伸其宽度和高度以及确切的适应其容器(如果设置了明确的高度和宽度此设置失效) |
None | 形状不被拉伸。除非使用Height和Width属性(或者使用MinHeight和MinWidth)将形状的宽度和高度设置为非0的值,否则不会显示形状 |
Uniform | 按比例改变形状的宽度和高度,直到形状到达容器边缘。 |
UniformToFill | 按比例改变形状的高度和宽度,直到形状填满了整个可用空间的高度和宽度。 |
如下示例,显示了上面三种枚举值的区别
<Ellipse Fill="Yellow" Stroke="Blue" Stretch="Fill"></Ellipse> <Ellipse Fill="Yellow" Stroke="Blue" Grid.Column="1" Stretch="Uniform"></Ellipse> <Ellipse Fill="Yellow" Stroke="Blue" Grid.Column="2" Stretch="UniformToFill "></Ellipse>
WPF形状使用于其他元素相同的布局系统。然而,有些局部容器是不合适的。如StackPanel、DockPanle以及WrapPanel面板,因为它们被设计为独立的元素。Grid面板更加灵活,因为它允许在同一单元格中放置任意多个元素。理想的容器是Canvas,该容器可以完全控制性状如何相互重叠:
<Canvas Grid.Row="1"> <Ellipse Fill="Yellow" Stroke="Blue" Canvas.Left="100" Canvas.Top="50" Width="100" Height="50" ></Ellipse> <Rectangle Fill="Yellow" Stroke="Blue" Canvas.Left="30" Canvas.Top="40" Width="100" Height="50" ></Rectangle> </Canvas>
12.1.4 使用ViewBox控件缩放形状
使用Canvas控件的唯一限制是图形不能改变自身的尺寸以适应更大或更小的窗口。WPF提供了比较容易的解决方法。如果希望联合Canvas控件的精确控制功能和容易的改变尺寸的功能,可以使用Viewbox元素。Viewbox是一个继承自Decorator的简单类。该类只接受一个子元素,并且拉伸或缩小其子元素以适应可用的空间。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.Stretch属性改变该行为。该属性可以使用Stretch的所有枚举值。还可以通过使用StretchDirection属性获得更大的控制。默认情况下,该属性值被设置为Both,但可以使用UnOnly值创建只会增加而不会收缩超过其元素尺寸的内容,并且可以使用DownOnly创建只会缩小而不会增长的内容。为使Viewbox元素执行其缩放工作,他需要能够确定两部分信息:1、新尺寸。Viewbox元素根据其Stretch属性,让其内部的内容使用所有可用的空间。2、原始尺寸,不使用Viewbox控件时的尺寸,隐含在定义嵌套内容的方式中。
12.1.5 直线
Line形状表示连接一个点和另一个点的一条直线。起点和终点由4个属性设置:X1与Y1和X2与Y2。例如,下面是一条从点(0,0)伸展到点(10,100)的直线
<Line Stroke="Blue" X1="0" Y1="0" X2="10" Y2="100"></Line>
对于直线,Fill属性不起作用,必须设置Stroke属性。在直线上使用的坐标是相对于放置直线的矩形区域左上角的坐标。在直线中使用负的坐标值是非常合理的。可以为直线使用使其超出为直线保留的空间的坐标,从而在窗口中其他任意部分上绘制直线。但是直线不能使用流内容模型。这意味着直线设置Margin、HorizontalAlignment以及VerticalAlignment属性是没有意义的。对于Polyline和Polygon形状具有同样的限制。
12.1.6 折线
通过Polyline类可以绘制一系列相互连接的直线。只需要使用Points属性提供一系列X和Y坐标。Points属性需要一个PointCollection对象,但是在XAML中使用基于简单字符串的语法填充改集合。只需要提供一个点的列表,并在每个坐标之间添加空格或逗号。为了更便于阅读,可在每个X和Y坐标之间使用逗号。如下示例:
<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>
12.1.7 多边形
Polygon和Polyline是相同的。和Polyline类一样,Polygon类也有一个包含一系列坐标的Points集合。唯一区别是Polygon形状添加最后一条线段,将最后一个点连接到开始点。可以使用Fill画刷填充该形状的内部区域。
<Polygon Stroke="Blue" StrokeThickness="5" Fill="Yellow" Canvas.Top="200" Points="10,150 30,140 50,160 70,130 90,170 110,120 130,180 150,110 170,190 190,100 210,240" > </Polygon>
12.1.8 直线线帽和直线交点
当绘制Line和Polyline形状时,可以使用StartLineCap和EndLineCap属性选择如何绘制直线的开始端和结束端。StarLineCap和EndLineCap属性通常都被设置为Flat,这意味着直线在它的最后坐标处立即终止。其他选择包括Round(该设置会平滑的绘制拐角)、Triangle(绘制直线的两条侧边最后交于一点),以及Square(该设置使直线端点具有尖锐的边缘)。这三个设置都会增加直线的长度额外的距离是直线宽度的一半。如下是以上四种的结束端的展示
<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>