理解形状

  在WPF用户界面中,绘制2D图形内容的最简单方法时使用形状(Shape)——专门用于表示简单的直线、椭圆、矩形以及多边形的一些类。从技术角度看,形状就是所谓的绘图图元(Primitive)。可组合这些基本元素来创建更复杂的图形

  关于 WPF 中形状的最重要细节是,它们都继承自FrameworkElement类。因此,形状是元素。这样会带来许多重要的结果:

  • 形状绘制自身:不需要管理无效的情况和绘图过程。例如,当移动内容、改变窗口尺寸或改变形状属性时,不需要手动重新绘制形状。
  • 使用与其他元素相同的方式组织形状:换句话说,可在任何布局容器中放置形状(尽管 Canvas 明显是最有用的容器,因为它允许在特定的坐标位置放置形状,当构建复杂的具有多个部分的图画时,这很重要)。
  • 形状支持与其他元素相同的事件:这意味着为了处理焦点、按下键盘、移动鼠标以及单击鼠标等,不必执行任何额外工作。可使用用于其他元素的相同事件集,并同样支持工具提示、上下文菜单和拖放操作。

Shape类

  每个形状都继承自抽象类System.Windows.Shapes.Shape。下图显示了形状类的继承层次。

WPF形状类

  相对来说,只有很少一部分类继承自Shape 类。LineEllipse 以及Rectangle 都很直观,Polyline 是一系列相互连接的直线,Polygon 是由一系列相互连接的直线形成的闭合图形。最后,Path 类功能强大,能将多个基本形状组合成单独的元素。

  尽管Shape类自身不能执行任何工作,但它定义了少量的重要属性,如下所示:

Shape类的属性
名称 说明
Fill 设置绘制形状表面(边框内的所有内容)的画刷对象
Stroke 设置绘制形状边缘(边框)的画刷对象
StrokeThickness 用设备无关单位设置边框的宽度
StrokeStartLineCap、StrokeEndLineCap 决定直线开始端和结束端边缘的轮廓。这些属性只影响LinePoyline以及(在这些情况下)Path形状。所有其他形状都是闭合的,没有开始点和结束点
StrokeDashArray、StrokeDashOffset、StrokeDashCap 用于在形状周围创建点划线边框。可控制电划线的尺寸和频率,以及每条点划线开始端和结束端边缘的轮廓
StrokeLineJoin、StrokeMiterLimit 确定形状拐角的轮廓。从技术角度看,这些属性影响不同直线相遇的顶点,如矩形的拐角。对于没有拐角的形状,如LineEllipse,这些属性不起作用
Stretch 确定形状如何填充可用的区域。可使用该属性创建能够扩展以适合其容器的形状。还可为 HorizontalAlignment或 VerticalAlignmet 属性(这些属性继承自FrameworkElement 类)使用 Stretch 值强制形状在某个方向上扩展
DefiningGeometry 为形状提供好Geometry对象。Geometry对象描述了形状的坐标和尺寸,不包括UIElement类的相关内容,例如对键盘和鼠标事件的支持
GeometryTransform 可通过该属性应用Transform对象,改版了用于绘制形状的坐标系统,从而可扭曲、旋转活移动形状。当为图形应用动画时,变换特别有用
RenderedGeometry 提供描述最终的、已渲染好的图形的Geomerty对象

接下来的几节将分析 Rectangle、Ellipse、Line 以及 Polyline。同时,还将介绍以下基础知识

  • 何改变形状的尺寸,以及如何在布局容器中组织形状。
  • 如何控制填充复杂形状的哪个区域。
  • 如何使用点划线和不同的线头终端(或称为“线帽”(cap))。
  • 如何使形状边缘与像素边界整洁地对齐。

矩形和椭圆

  矩形和椭圆是两个最简单的形状。为创建矩形或椭圆,需要设置大家熟悉的 Height 和 Width属性(这两个属性继承自 FrameworkElement类)来定义形状的尺寸,然后设置 Fil 或 Stroke 属性(或同时设置这两个属性)使形状可见。还可以使用 MinHeigth、MinWidth、HorizontalAlignment、VerticalAlignment 以及 Margin 等属性。

如果未设置StrokeFill属性,形状就根本不会显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<StackPanel>
<Ellipse Fill="Yellow"
Stroke="Blue"
Height="50"
Width="100"
Margin="5"
HorizontalAlignment="Left" />
<Rectangle Fill="Yellow"
Stroke="Blue"
Height="50"
Width="100"
Margin="5"
HorizontalAlignment="Left" />
</StackPanel>

  Elipse 类没有增加任何属性。Rectangle 类只增加了两个属性:RadiusX和 RadiusY。如果将这两个属性的值设为非零值,就可以创建出美观的圆形拐角。

可认为RadiusXRadiusY属性时用于填充矩形拐角的椭圆

改变形状的尺寸和放置行为

  正如您已经知道的,硬编码尺寸通常不是创建用户界面的理想方法。它们会限制处理动态内容的能力,并会使应用程序本地化到其他语言变得更加困难。

  当绘制形状时,不再总是关心这些问题。通常,需要更严格地控制形状的位置。然而,在许多情况下仍需更灵活一点儿的设计。Ellipse 和 Rectangle 为了适应可用的空间,都能自动改变白身。

1
2
3
4
<Grid>
<Ellipse Fill="Yellow"
Stroke="Blue" />
</Grid>

如果未提供 Height 和 Width 属性,形状会根据它们的容器来设置自身的尺寸。

  改变形状尺寸的行为依赖于 Stretch 属性的值(该属性在 Shape 类中定义)。默认情况下,该属性被设置为Fill。如果没有指定明确的尺寸,这一设置会拉伸形状,使其填满容器。

Stretch枚举值
名称 说明
Fill 形状拉伸其宽度和高度,从而可以正好适应其容器(如果设置了明确的高度和宽度,该设置就不起作用)
None 形状不被拉伸。除非使用 Height 和 Width 属性(或者使用 MinHeight 和 MinWidth 属性)将形状的宽度和高度设置为非0值,否则不会显示形状
Uniform 按比例改变形状的宽度和高度,直至形状到达容器边缘。如果为椭圆使用该值,最终将得到适应窗口的最大的圆。如果为矩形使用该值,将得到尽可能大的正方形(如果设置了明确的高度和宽度,形状就会在这些边界内改变尺寸。例如,如果将矩形的Width 属性设置为10 并将 Height 属性设置为 10,将只得到 10x10 大小的正方形)
UniformToFill 按比例改变形状的宽度和高度,直到形状填满了整个可用空间的高度和宽度。例如,如果在 100x200单位大小的窗口中放置使用此尺寸设置的矩形,将得到200x200单位大小的矩形,并且矩形的一部分会被剪裁掉(如果设置了明确的宽度和高度,就会在这些边界中改变形状尺寸。例如,如果将矩形的Width属性设为10,并将Height属性设为100,将得到100x100单位大小的矩形,并且会剪裁该矩形以适应不可见的10x100大小的方框)
填充Grid中的三个单元格

通常,将Stretch的值设置为Fill相当于将HorizontalAlignmentVericalAlignment属性设置为Stretch

  通过Canvas容器,可使用LeftTopRightBottom附加属性,为每个形状指定坐标。这样可以完全控制形状如何相互重叠:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Canvas>
<Ellipse Fill="Yellow"
Stroke="Blue"
Canvas.Left="100"
Canvas.Top="50"
Width="100"
Height="50" />

<Rectangle Fill="Yellow"
Stroke="Blue"
Canvas.Left="30"
Canvas.Top="40"
Width="100"
Height="50" />
</Canvas>

使用Viewbox控件缩放形状

  使用 Canvas 控件的唯一限制是图形不能改变自身的尺寸以适应更大或更小的窗口。对于按钮这非常合理(在这些情况下,按钮不改变尺寸),但是对于其他类型的图形内容,情况就未必如此了。例如,可能创建希望可以改变大小的复杂图形,从而可以充分利用可用的空间。

WPF 提供了简便的解决方法。如果希望联合Canvas 控件的精确控制功能和方便的改变尺寸功能,可使用 Viewbox元素。

  Viewbox时继承自Decorator的简单类(与Border类似)。该类只接受一个子元素,并拉伸活缩小子元素以适应可用空间。当然,这个单一的子元素可以时布局容器,其中可以包含大量形状(或其他元素),这些元素将同步地改变尺寸。然而,Viewbox更常用与矢量图像而不是普通控件。
  尽管可在Viewbox元素中放置耽搁形状,但这并不能提供任何实际的优点。反而,当需要封装构成一幅图画(drawing的一组形状),Viewbox元素才有用处。通常,将在Viewbox控件中放置Canvas面板,并在Canvas面板中放置形状。

Viewbox 元素执行的缩放和在 WPF 中当增加系统 DPI 设置时看到的缩放类似。它按比例改变屏幕上的每个元素,包括图像、文本、直线以及形状。例如,如果在 Viewbox 元素中放置一个普通按钮,尺寸的改变会影响它的整个尺寸、内部的文本以及周围边框的粗细。如果在Viewbox 元素内部放置一个形状元素,它会按比例地改变形状内部的区域和边框,从而当放大形状时,其边框也将变粗。

  默认情况下,Viewbox元素按比例地执行缩放,保持它所包含内容的纵横比Viewbox元素使用适应可用空间内部的最大缩放系数。然而,可使用Viewbox.Stretch属性改变该行为,默认情况下,将该属性设置为Uniform

  为使Viewbox元素执行其缩放工作,需要能够确定两部分信息:(如果不放在Viewbox元素中)内容应当具有原始尺寸和希望内容具有的新尺寸。

  • 第一个细节——原始尺寸,不实用Viewbox控件时的尺寸——隐含在定义嵌套内容的方式中
  • 第二个细节——新尺寸,Viewbox元素根据Stretch属性,让其内部的内容使用所有可用控件,这意味着Viewbox元素越大,其内部的内容就越大

如果删除 Canvas 控件的 Width 和 Height属性,分析会发生什么情况。现在,Canvas控件的尺寸被设置为 0X0单位大小,所以Vewbox控件不能改变它的尺寸,并且嵌套在其中的内容不会显示(这与只使用 Canvas 控件时的行为不同。因为尽管 Canvas 控件的尺寸仍设置为0X0,但只要 Canvas.ClipToBounds属性没有被设置为true,就仍然允许在 Canvas 控件之外的区域绘制形状。而 Viewbox 控件不能容忍这一错误)。

  如果在按比例改变尺寸的Grid面板的单元格中封装Canvas面板,并且没有指定Canvas面板的尺寸,如果没有使用Viewbox元素,该方法可工作得很好——拉伸Canvas面板以填充单元格,并且内部的内容时可见的。当如果将所有内容放在Viewbox元素中,这种方法就会失效。Viewbox控件不能确定最初尺寸,因此不能相应地改变Grid面板的尺寸。

  可通过直接在能自动改变尺寸的容器(如 Gird 面板)中放置特定的形状(如Rectangle 和Elipse)来避免这个问题。然后 Viewbox 控件就能够评估 Gird 面板为了适合其内容所需的最小尺寸,并且缩放 Grid 面板以适应可用空间。然而,在 Viewbox 元素中获取真正所希望的尺寸的最简单方法,是在具有固定尺寸的元素中封装内容,可以是Canvas面板、按钮或其他控件。这样,固定尺寸就变成了 Viewbox 控件进行计算所使用的原始尺寸。以这种方式硬编码尺寸不会限制布局的灵活性,因为 Viewbox元素根据可用空间和布局容器按比例改变尺寸。

直线

  Line 形状表示连接一个点和另一个点的一条直线。起点和终点由4个属性设置:X1与Y1(用于第一个点)和 X2 与 Y2(用于第二个点)。例如,下面是一条从点(0,0)伸展到点(10,100)的直线:

1
2
3
4
5
<Line Stroke="Blue"
X1="0"
Y1="0"
X2="100"
Y2="100" />

对于直线,Fill属性不起作用,必须设置Stroke属性

  在直线中使用的坐标是相对于放置直线的矩形区域左上角的坐标。例如,如果在 StackPanel面板上放置上面的直线,坐标(0,0)指向在 StackPanel面板上放置该矩形区域的位置。这可能是窗口的左上角,也可能不是。如果 StackPanel 面板的 Margin 属性值不为0,或直线在其他元素之后,直线的开始点(0.0)与窗口顶部会有一定的距离。

  然而,在直线中使用负坐标值是非常合理的。实际上,可为直线使用能超出为直线保留的空间的坐标,从而在窗口的其他任意部分绘制直线。对于到目前为止介绍的Rectangle和 Ellipse形状;这是不可能的。然而,这一模型也有缺点,直线不能使用流内容模型。这意味着为直线设置 Margin、HorizontalAlignment 以及 VerticalAlignment 属性是没有意义的,因为它们没有任何效果。对于 Polyline 和 Polygon 形状具有同样的限制。

可为直线使用 Height、Width 以及 Stretch属性,但这种做法不常用。基本技术是使用 Heigth和 Width 属性确定为直线分配的空间,然后使用 Stetch 属性改变直线的尺寸以填充该区域。

  如果在 Canvas 面板上放置了 Line 形状,那么仍应用附加的位置属性(如 Top 和 Lef)。它们决定直线的开始位置。换句话说,两个直线坐标被平移了一定的距离。分析下面的直线:

1
<Line Stroke="Blue" X1="0" Y1="0" X2="10" Y2="100" Canvas.Left="5" Canvas.Top="100"></Line>

折线

  可以通过Polyline类绘制一些列相互连接的直线。只需要使用Points属性提供一系列X和Y坐标。从技术角度看,Points属性需要提供PointCollection对象,但在XAML中使用基于简单字符串的语法填充该集合。只需要提供点的列表,并在每个坐标之间添加空格或逗号。

为了便于阅读,可在每个X和Y坐标之间使用逗号:

1
2
<Polyline Stroke="Blue"
Points="5,100 150,200 5,350" />

多边形

  实际上,PolygonPolyline时相同的。和Polyline类一样,Polygon类也有包含一系列坐标的Points集合。唯一的区别时:Polygon形状添加最后一条线段,将最后一个点连接到开始点(如果最后一个点就是第一个点,Polygon类和Polyline类就没有区别了)。可使用Fill画刷填充该形状的内部区域。

  对于线条从不相交的简单形状,填充其内部是很容易做到的。但有时会遇到更复杂的Polygon 形状,哪些部分属于内部(并且应当被填充)以及哪些部分属于外部并不明显。例如如下的五角星图案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<Grid ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Polygon Grid.Row="0 "
Grid.Column="0"
Stroke="Blue"
StrokeThickness="1"
Fill="Yellow"
Points="15,200 68,70 110,200 0,124 135,125"
FillRule="EvenOdd" />
<Polygon Grid.Row="0 "
Grid.Column="1"
Stroke="Blue"
StrokeThickness="1"
Fill="Yellow"
Points="15,200 68,70 110,200 0,124 135,125"
FillRule="Nonzero" />
<TextBlock Grid.Row="1"
Grid.Column="0"
Text="EvenOdd"
HorizontalAlignment="Center"
FontSize="20"/>
<TextBlock Grid.Row="1"
Grid.Column="1"
Text="Nonzero"
HorizontalAlignment="Center"
FontSize="20" />
</Grid>

每个Polygon和Polyline 形状都有FiRule 属性,该属性用于从两种填充方法中选择一种来填充区域。默认情况下,FilRule 属性被设置为EvenOdd。为了确定是否填充区域,WPF计算为了到达形状的外部必须穿过的直线的数量。如果是奇数,就填充区域;如果是偶数,就不填充区域。

  WPF 还遵循 Nonzero 填充规则,该规则更加复杂。本质上,当使用 Nonzero 填充规则时,WPF使用和 EvenOdd 填充规则相同的方法计算穿过的直线的数量,但是会考虑经过的每条直线的方向如果在经过的直线中,在某个方向上(比如从左向右)直线的数量等于相反方向(从右向左)上直线的数量,就不会填充区域。如果这两个直线数量的差不为0,就填充区域。

直线线帽和直线交点

  当绘制 LinePolyline 形状时,可使用 StartLineCapEndLineCap 属性选择如何绘制直线的开始端和结束端(这些属性不影响其他形状,因为其他形状都是闭合的)。

  StartLineCapEndLineCap 属性通常都设为 Flat,这意味着直线在它的最后坐标处立即终止。其他选择包括 Round(该设置会平滑地绘制拐角)、Triangle(绘制直线的两条侧边最后交于一点)以及 Square(该设置使直线端点具有尖锐边缘)。这三个设置都会增加直线的长度–换句话说,它们使直线超出了其他情况下的结束位置。额外的距离是直线宽度的一半。

  下图显示了直线端点处不同线帽之间的区别。除 Line 形状外,所有形状都允许使用StrokeLineJoin属性扭曲它们的拐角,有4种选择。Miter 值(默认值)使用尖锐的边缘,Bevel 值切掉点边缘,Round 值平滑地过渡边缘,Triangle 值显示尖点。

1
2
3
4
5
6
7
8
<Line X1="10"
Y1="10"
X2="200"
Y2="200"
Stroke="Blue"
StrokeThickness="10"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round" />

点划线

  除了为形状的边框绘制乏味的实线外,还可绘制点划线(dashed line)——根据指定的模式使用空白断开的直线。当在 WPF中创建一条点划线时,不限制进行特定的预先设置。相反,可通过设置 StrokeDashArray属性来选择实线段的长度和断开空间(空白)的长度。

1
2
3
4
<Polyline Stroke="Blue"
StrokeThickness="5"
StrokeDashArray="3 0.5 2"
Points="10,30 60,0 90,40 120,10 350,10" />

StrokeDashArray的值可以是小数奇数个,如果希望从中间开始绘制,可使用StrokeDashOffset属性,该属性时一个从0开始的索引,该索引指向StrokeDashArray中的某个值

像素对齐

  如您所知,WPF 使用与设备无关的绘图系统。为字体和形状等内容指定的数值使用“虚拟像素,在通常的 96 dpi 显示器上,“虚拟”像素和正常像素的大小相同,但是在更高 dpi 的显示器上其尺寸会被缩放。换句话说,绘制50像素宽的矩形,根据设备的不同,实际上可能使用更多或更少的像素进行渲染。设备无关单位和物理像素之间的转换会自动进行,并且通常根本不需要考虑这个问题。

根据正在绘制的图形类型,它可能看起来很正常。然而,如果不希望这种行为,可告诉 WPF 不要为特定形状使用反锯齿特性进行处理,反而WPF会将尺寸舍入到最近的设备像素。可通过将UIElement类的SnapsToDevicePixels属性设置为true来启用这个称为像素对齐(pixelsnapping)的特性。

画刷

  画刷填充区域,不管是元素的背景色、前景色以及边框,还是形状的内部填充和笔画(stroke)。最简单的画刷类型是 SolidColorBrsh,这种画刷填充一种固定、连续的颜色。在XAML 中设置形状的 StrokeFill 属性时,使用的是 SolidColorBrush 画刷,它们在后台完成绘制。
  下面时几个与画刷相关的更基本的方面:

  • 画刷支持更改通知,因为它们继承自Freezable类。因此,如果改变了画刷,任何使用画刷的元素都会自动重新绘制自身。
  • 画刷支持部分透明,为此,只需要修改Opacity属性,使背景能够透过前面的内容进行显。
  • 通过 SystemBrushes 类可以访问这样的画刷,此类画刷使用 Windows 系统设置为当前计算机定义的首选颜色。

  SolidColorBrush 画刷无疑非常有用,但还有其他几个继承自 System.Windows.Media,Brush 的类,通过这些类可得到更新颖的效果。

画刷类
名称 说明
SolidColorBrush 使用单一的连续颜色绘制区域
LinearGradientBrush 使用渐变填充绘制区域,渐变的阴影填充从一种颜色变化到另一种颜色
RadialGradientBrush 使用径向渐变填充绘制区域,除了是在圆形模式中从中心点向外部辐射渐变之外,这种画刷和线性渐变画刷类似
ImageBrush 使用可被拉伸、缩放活平铺的图像绘制区域
DrawingBrush 使用Drawing对象绘制区域,该对象可以包含已经定义的形状和位图
VisualBrush 使用Visual对象绘制区域。因为所有WPF元素都继承自Visual类,所以可使用该画刷将部分用户界面复制到另一个区域。当创建特殊效果时,比如部分反射效果,该画刷特别有用
BitmapCacheBrush 使用从Visual对象缓存的内容绘制区域。这种画刷和VisualBrush类似,但如果需要在多个地方重用图形内容或者频繁地重绘图形内容,这种画刷更高效

SolidColorBrush画刷

  在大多数控件中,通过三个字Foreground属性绘制文本颜色,并设置Background属性绘制文本背后的空间。形状使用类似但不同的属性:Stroke属性用于绘制形状的边框,而Fill属性用于绘制形状的内部。

1
2
3
4
5
6
<Button x:Name="cmd"
Background="Blue"
Foreground="White"
Content="Button"
Width="100"
Height="50" />
1
2
cmd.Background = new SolidColorBrush(Color.FromRgb(0, 255, 0));
cmd.Foreground = new SolidColorBrush(Colors.White);

LinearGradientBrush画刷

  可通过LinearGradientBrush画刷创建从一种颜色变化到另一种颜色的混合填充。下图为左下角到右上角的蓝色到绿色的渐变:

1
2
3
4
5
6
7
8
9
10
11
<Rectangle Width="150"
Height="100">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0 1" EndPoint="1 0">
<GradientStop Color="Blue"
Offset="0" />
<GradientStop Color="Green"
Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>

通过StartPointEndPoint设置(0 ~ 1)的起始和结束点位用于确定渐变的方向;通过GradientStopOffset设置(0~1)的偏移值

  渐变不仅可以使用在控件或元素上,也可以作用于文本上,如下渐变文本:

1
2
3
4
5
6
7
8
9
10
11
<TextBlock Text="This is a Text"
FontSize="50">
<TextBlock.Foreground>
<LinearGradientBrush>
<GradientStop Color="Red"
Offset="0" />
<GradientStop Color="Blue"
Offset="1" />
</LinearGradientBrush>
</TextBlock.Foreground>
</TextBlock>

RadialGradientBrush画刷

  RadialGradientBrush 画刷和 LinearGradientBrush 画刷的工作方式类似,也使用一系列具有不同偏移值的颜色。与LinearGradientBrush 画刷一样,可使用希望的任意多种颜色。区别是放置渐变的方式。

  为指定第一种颜色在渐变中的开始点,需要使用 GradientOrigin 属性。默认情况下,渐变的开始点是(0.5,0.5),该点表示填充区域的中心。

LinearGradientBrush 一样,RadialGradientBrush 也使用比例坐标系统,该坐标系统将(0,0)作为矩形填充区域的左上角坐标,将(1,1)作为右下角坐标。这意味着可使用(0,0)到(1,1)之间的任何坐标作为渐变的开始点。实际上,如果希望在填充区域之外定位开始点,甚至可超出这一范围。

  渐变从开始点以环形的方式向外辐射。渐变最终到达内部渐变圆的边缘,这里是渐变的终点。根据所期望的效果,渐变圆的中心可能和渐变开始点对齐,也可能和渐变开始点不对齐。超出内部渐变圆的区域以及填充区域的最外侧边缘,使用在:RadialGradientBrush.GradientStops集合中定义的最后一种颜色进行纯色填充。下图演示了径向渐变效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Grid>
<Grid.Background>
<RadialGradientBrush GradientOrigin="0.7,0.3"
RadiusX="0.5"
RadiusY="0.3">
<GradientStop Color="Red"
Offset="0" />
<GradientStop Color="Blue"
Offset="0.5" />
<GradientStop Color="Green"
Offset="1" />
</RadialGradientBrush>
</Grid.Background>
</Grid>

  可使用三个属性设置内部渐变圆的边界:CenterRadiusXRadiusY。默认情况下,Center属性被设置为**(0.5,0.5),该设置将限定圆的中心放在填充区域的中央,并且该点同时也是渐变开始点。
  RadiusXRadiusY属性决定了限定圆的尺寸,默认情况下这两个属性都被设置为
0.5**。这个值可能不够直观,因为它们根据填充区域的对角范围(一条从填充区域的左上角延伸到右下角的假想线)进行度量。这意味着半径0.5定义了一个圆,该圆的半径时对角线长度的一般。如果填充区域为正方形,使用勾股定理可计算出,该长度大约是填充区域宽度(或高度)的0.7倍。因此,如果用默认设置填充正方形区域,渐变就从中心点开始,并拉伸大约正方形宽度0.7倍的距离到达最外侧边界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<Ellipse Margin="5"
Stroke="Black"
StrokeThickness="1"
Width="200"
Height="200">
<Ellipse.Fill>
<RadialGradientBrush RadiusX="1"
RadiusY="1"
GradientOrigin="0.7,0.3">
<GradientStop Color="White"
Offset="0" />
<GradientStop Color="Blue"
Offset="1" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>

ImageBrush画刷

  可通过 IageBrush画刷使用位图填充区域。可使用最常见的文件类型,包括BMP、PNGGIF 以及 JPEG 文件。可通过设置 ImageSource 属性来指定希望使用的图像。

1
2
3
4
5
6
7
8
<Grid>
<Grid.Background>
<ImageBrush ImageSource="/Images/happyface.png"
Stretch="Uniform"
AlignmentX="Center"
AlignmentY="Center"/>
</Grid.Background>
</Grid>

  ImageBrush.ImageSource 属性和 Image 元素的 Source 属性的工作方式相同,这意味着也可以使用指向资源、外部文件或 Web站点的 URI设置 ImageSource 属性。也可通过为 ImageSource属性提供 DrawingImage 对象,创建使用由 XAML 定义的矢量内容的 ImageBursh 画刷。可通过这种方法降低开销(通过避免使用更耗资源的 Shape 类的派生类),或使用矢量图形创建平铺模式。

  如果容器的大小与背景图的大小不匹配,默认情况下会拉伸背景图,可通过Stretch属性控制拉伸方式,其取值方式如下所示:

Stretch属性
名称 说明
None 不填充和拉伸,采用背景图的原始尺寸
Fill 默认值,填充整个容器
Uniform 以填充容器短边为准
UniformToFill 以填充容器长边为准,因此短边部分的背景图可能会被裁剪

  如果绘制的图像比填充区域小,图像会根据 AlignmentX和 AlignmentY 属性进行对齐。未填充的区域保持透明。当使用Uniform设置进行缩放,并且填充区域的形状不同时,就会出现这种情况(在这种情况下,在上部或侧边会出现空白条)。如果将 Stretch 属性设置为 None,并且填充区域比图像大,也会出现这种情况。

  还可使用 Viewbox 属性从图像上剪裁有兴趣使用的一小部分。为此,需要指定4个数值以描述希望从源图像上剪裁并使用的矩形部分。前两个数值指定矩形开始的左上角,而后两个数值指定矩形的宽度和高度。唯一的问题是 Vewbox属性使用的是相对坐标系统,就像渐变画刷使用的坐标系统那样。这一坐标系统将图像的左上角指定为(0,0),将右下角指定为(1,1)。

1
2
3
4
5
6
7
8
9
<Grid>
<Grid.Background>
<ImageBrush ImageSource="/Images/happyface.png"
Stretch="Uniform"
AlignmentX="Center"
AlignmentY="Center"
Viewbox="0.1,0.1 0.3,0.5"/>
</Grid.Background>
</Grid>

  现在,Viewbox属性从(0.1,0.1)开始(从技术角度看,X坐标是宽度的0.1倍,Y坐标时高度的0.1倍)。然后伸展矩形以填充一个30%宽度和50%高度的小方框作为整幅图像(从技术角度看,矩形的长度为图像宽度的0.3倍,矩形的高度为图像高度的0.5倍)。根据StretchAlignmentX以及AlignmentY属性的设置,倍裁剪下来的部分图像会被拉伸活居中显示。

平铺的ImageBrush画刷

  除普通的ImageBrush画刷外,还有其他令人更加激动的内容。可通过在画刷的表面平铺图像来得到一些有趣的效果。
  平铺图像时,有两种选择:

  • 按比例平铺:填充区域始终具有相同数量的平铺图像。为适应填充区域,平铺的图像会扩展活收缩
  • 按固定尺寸平铺:平铺图像始终具有相同的尺寸。填充区域的尺寸决定了显示的平铺图像的数量

  为了平铺一幅图像,需要设置ImageSource 属性(指定希望平铺的图像)以及 ViewportViewportUnitsTileMode 属性。后三个属性决定了平铺图像的尺寸和排列方式。

  可使用Viewport 属性设置每幅平铺图像的尺寸。为使用按比例平铺模式,必须将ViewportUnits 属性设置为 RelativeToBoundingBox(这是默认设置)。然后使用在两个方向上的坐标范围都是从0到1的按比例坐标系统定义平铺图像的尺寸。换句话说,如果一幅平铺图像的左上角位于(0、0),右下角位于(1,1),就会占据整个填充区域。为得到平铺模式,为 Viewport属性设置的值应当比整个填充区域的尺寸小,如下所示:

1
2
3
4
5
6
7
<Grid>
<Grid.Background>
<ImageBrush ImageSource="/Images/happyface.png"
TileMode="Tile"
Viewport="0,0 0.5,0.5"/>
</Grid.Background>
</Grid>

  上面的标记创建了一个从填充区域的左上角(0,0)开始,并拉伸到中间点(0.5,0.5)的Viewport 方框。因此,不管填充区域的大小如何,填充区域始终包含4幅平铺图像。这种行为非常好,因为可确保平铺图像不会在形状的边缘被剪裁(当然,如果使用 ImageBrush 画刷填充非矩形区域,图像仍会被剪裁)。

  另一种定义平铺图像尺寸的方法是根据原始图像的尺寸使用绝对坐标。为此,将ViewportUnits 属性设置为 Absolute(而不是 RelativeToBoundBox)。下面举一个示例,该例将每幅平铺图像定义为 32x32 单位大小,并从左上角开始平铺:

1
2
3
4
5
6
7
8
<Grid>
<Grid.Background>
<ImageBrush ImageSource="/Images/happyface.png"
TileMode="Tile"
ViewportUnits="Absolute"
Viewport="0,0 32,32"/>
</Grid.Background>
</Grid>

  可改变 TileMode 值,设置平铺图像的翻转方式

TileMode枚举值
名称 说明
Tile 在可用区域复制图像
FlipX 复制图像,但垂直翻转每个第二列
FlipY 复制图像,但水平翻转每个第二行
FlipXY 复制图像,但垂直翻转每个第二列,并水平翻转每个第二行

  如果需要使平铺图像更无缝地混合,翻转行为通常是有用的。例如,如果使用FlipX,相邻的平铺图像总可以无缝地排列。

VisualBrush画刷

  VisualBrush 画刷不常用,使用这种画刷获取元素的可视化内容,并使用该内容填充任意表面。例如,可使用 VisualBrush 画刷将窗口中某个按钮的外观复制到同一窗口中的其他位置。然而,复制的按钮不能被单击,也不能通过任何方式与其进行交互。在此只是复制了元素的外观。

  例如,下面的标记片段定义了一个按钮和用于复制该按钮的 VisualBrush 画刷

1
2
3
4
5
6
7
8
9
<StackPanel>
<Button x:Name="cmd" Margin="3" Padding="5">Is this a real button?</Button>
<Rectangle Margin="3"
Height="100">
<Rectangle.Fill>
<VisualBrush Visual="{Binding ElementName=cmd}" />
</Rectangle.Fill>
</Rectangle>
</StackPanel>

尽管可在 VisualBrush 本身定义希望使用的元素,但通常使用绑定表达式引用当前窗口中的元素

  VisualBrush监视元素外观的变化。例如,如果复制某个按钮的可视化外观,而且此后按钮接收到焦点,VisualBmsh画刷会使用新的可视化内容重新绘制填充区域–一个具有焦点的按钮。VisualBrush 类继承自TileBrush类,因此,VisualBrush类也支持所有剪裁、拉伸以及翻转等特性。

BitmapCacheBrush画刷

  BitmapCacheBrush 画刷在许多方面和 VisualBrush 画刷类似。尽管 VisualBrush 类提供了用于引用其他元素的 Visual属性,但 BitmapCacheBrush 类提供了与此作用相同的 Target 属性。
  两者之间的关键区别是,BitmapCacheBrush画刷采用可视化内容(这些内容已经通过变换剪裁、效果以及透明设置进行了改变)并要求显卡在显存中存储该内容。这样一来,当需要时可快速地重新绘制内容,而不必要求 WPF执行任何额外的工作。

  为配置位图缓存,设置 BitmapCacheBrush.BitmapCache 属性(使用可预先确定的 BitmapCache对象)。下面是最简单的用法:

1
2
3
4
5
6
7
8
9
<StackPanel>
<Button x:Name="cmd" Margin="3" Padding="5">Is this a real button?</Button>
<Rectangle Margin="3"
Height="40">
<Rectangle.Fill>
<BitmapCacheBrush Target="{Binding ElementName=cmd}" BitmapCache="BitmapCache"/>
</Rectangle.Fill>
</Rectangle>
</StackPanel>

  BitmmapCacheBrush画刷存在严重缺点:渲染位图以及将其复制到显存的初始步骤需要比较短但可察觉到的额外时间。如果在窗口中使用 BitmapCacheBrush画刷,在窗口第一次绘制自身之前,当渲染 BitmapCacheBrush并复制其位图时,将会注意到延迟。因此,在传统窗口中,BitmapCacheBrush 起不到多大的帮助作用。

  然而,如果在用户界面中大量使用动画,值得考虑使用位图缓存。这是因为动画会强制窗口在每一秒内重新绘制许多次。如果具有复杂的矢量内容,从缓存位图中绘制窗口内容比从头重新绘制窗口要快。但即使是这种情况,也不应当立即使用 BitmapCacheBrush 画刷。可能更愿意通过为每个希望缓存的元素设置更高级的UIElement.CacheMode属性来应用缓存。对于这种情况,WPF 在后台使用 BitmapCacheBrush 画刷获取相同的效果,但需要做的工作更少。

变换

  通过使用变换(transform)。许多绘图人物将更趋简单;变换时通过不加通告地切换形状或元素有使用的坐标系统来改变形状活元素绘制方式的对象。在WPF中,变换由继承自System.WIndows.Media.Transform抽象类你的来表示。

变换类
名称 说明 重要属性
TranslateTransform 将坐标系统移动一定距离。如果希望在不同的地方绘制相同的形状,该变换非常有用 X、Y
RotateTransform 旋转坐标系统。正常绘制的形状绕着选择的中心点旋转 Angle、CenterX、CenterY
ScaleTransform 放大活缩小坐标系统,从而绘制更大或更小的图形。可在X和Y方向应用不同的缩放度,从而拉伸活压缩形状 ScaleX、ScaleY、CenterX、CenterY
SkewTransform 通过倾斜一定的角度扭曲坐标系统。例如,如果绘制正方形,通过该变换正方形会变成平行四边形 ScaleX、ScaleY、CenterX、CenterY
MatrixTransform 使用提供的矩阵的乘积修改坐标系统。这是最复杂的选择——为实现该变换,需要掌握一些数学技巧 Matrix
TransformGroup 组合多个变换,从而可以一次应用所有这些变换。应用变换的顺序是很重要的,因为这会影响最终结果。例如,首先使用RotateTransform旋转形状,然后使用TranslateTransform移动形状,这样做的结果和先移动再旋转的结果是不同的 N/A

  从技术角度看,所有变换都使用矩阵数学改变形状的坐标。不过,使用预先构建好的变换,如RotateTransform、ScaleTransform以及SkewTransform,比使用TranslateTransformMatrixTransform 并尝试为希望执行的操作构造正确的矩阵要简单得多。当使用 TransformGroup执行一系列变换时,WPF 将所有变换融合到单独的 MatrixTransform 变换中以确保获得最佳性能。

所有变换都(通过 Transform 类)继承自 Freezable 类,这意味着它们支持自动更改通知功能。如果改变了在形状中使用的变换,形状会立即重新绘制自身。

变换形状

  为变换形状,将RenderTransform属性指定为希望使用的变换对象。根据使用的变换对象,需要填充不同的属性以配置变换对象。下面的示例中将矩形旋转25°

1
2
3
4
5
6
7
8
9
10
11
12
<Canvas>
<Rectangle Width="80"
Height="10"
Stroke="Blue"
Fill="Yellow"
Canvas.Left="100"
Canvas.Top="100">
<Rectangle.RenderTransform>
<RotateTransform Angle="25" />
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>

  采用这种方式旋转形状时,是围绕形状的原点进行旋转的(左上角),有时希望绕不同的点旋转形状。与其他许多变换类一样,RoatteTransform变换也提供了CenterXCenterY属性。可以用这些属性指定将进行选准的中心。

1
2
3
4
5
6
7
8
9
10
11
12
<Canvas>
<Rectangle Width="80"
Height="10"
Stroke="Blue"
Fill="Yellow"
Canvas.Left="100"
Canvas.Top="100">
<Rectangle.RenderTransform>
<RotateTransform Angle="25" CenterX="45" CenterY="5"/>
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>

  使用 RotateTransform 的 CenterX和 CenterY属性时存在明显的限制。这些属性是使用绝对坐标定义的,这意味着需要了解绘制内容的中心点的准确位置。如果正在显示动态内容(例如,可变维度的图片或可改变尺寸的元素),就会出现问题。幸运的是,WPF通过方便的RenderTransforOrigin 属性,为这个问题提供了解决方法,所有形状都支持 RenderTransformOrigin 属性。该属性使用相对坐标系统设置中心点,相对坐标系统在两个方向上的范围都是从0到1。换句话说,点(0,0)被指定为左上角,点(1,1)表示右下角(如果形状区域不是正方形,那么会相应地拉伸坐标系统)。

1
2
3
4
5
6
7
8
9
10
11
12
13
<Canvas>
<Rectangle Width="80"
Height="10"
Stroke="Blue"
Fill="Yellow"
Canvas.Left="100"
Canvas.Top="100"
RenderTransformOrigin="0.5,0.5">
<Rectangle.RenderTransform>
<RotateTransform Angle="25"/>
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>

不管形状的尺寸时多少,点(0.5,0.5)都表示形状中心。实际上,RenderTransformOrigin属性通常比CenterXCenterY属性更有用

当设置 RenderTransformOrigin属性以指定旋转点时,可使用大于1或小于0的值,这时旋转点位于形状边界之外。例如,可使用具有这种设置的RotateTransform 变换,绕着某个非常远的点旋转大的弧形,例如绕点(5.5)进行旋转。

变换元素

  RenderTransform和 RenderTransformOrigin 属性并不限制只能用于形状。实际上,Shape 类的这些属性从 UIElement 类继承而来,这意味着所有 WPF 元素都支持这两个属性,包括按钮、文本框、TextBlock 控件、充满内容的整个布局容器等。令人感到惊讶的是,可旋转、扭曲以及缩放 WPF 用户界面中的任意一部分(尽管在大多数情况下不会这么做)。

  RenderTransform 不是在 WPF 基类中定义的唯一与变换相关的属性。FrameworkElement 类还定义了 LayoutTransform 属性。LayoutTransform 属性以相同的方式变换元素,但在布局之前执行其工作。这种情况的开销虽然更大些,但如果使用布局容器为一组控件提供自动布局功能这种方式是很关键的(Shape 类也提供了 LayoutTransfomm 属性,但很少需要使用该属性,因为通常使用容器(如 Canvas 面板)明确地放置形状,而不是使用自动布局)。

只有很少几个元素不能被变换,因为它们的呈现工作并非由 WPF 本身负责。不能被变换的元素的两个例子是 WindowsFormHostWebBrower 元素,WindowsFormHost 元素用于在 WPF窗口中放置 Windows 窗体控件,WebBrower 元素用于显示 HTML 内容。

  在一定程度上,当设置 LayoutTransform或RenderTransform属性时,WPF 元素不知道它们正在被修改。特别是,变换不会影响元素的ActualHeight和 ActualWidth 属性,它们仍记录着变换之前的值。这正是 WPF 能够保证流式布局以及外边距继续以相同的方式工作的部分原理即使应用了一个或多个变换也同样如此。

透明

使元素半透明

  可采用以下几种方式使元素具有半透明效果:

  • 设置元素的Opacity属性:每个元素(包括形状)都从UIElement基类继承了Opacity属性。不透明度(Opacity)是0到1之间的小数,1表示完全不透明(默认值),0 表示完全透明
  • 设置画刷的Opacity属性:每个画刷也从Brush基类继承了Opacity属性,可使用0到1之间的值设置该属性,以控制使用画刷绘制的内容的同名都
  • 使用具有透明Alpha值的颜色:所有alpha值小于255的颜色都是半透明,例如*#50FF69B4*,最前面的两位用于控制透明度,00表示全透明,FF表示无透明

透明掩码

  Opacity 属性使元素的所有内容都是部分透明的。OpacityMask 属性提供了更大的灵活性。可使元素的特定区域透明或部分透明,从而实现各种常见的以及新颖的效果。例如,可使用OpacityMask属性将形状逐渐褪色到完全透明。

  使用 SolidColorBrush 画刷设置 OpacityMask 属性没什么意义,因为可使用 Opacity 属性更容易地实现相同的效果。然而,当使用更特殊的画刷类型时,例如使用LinearGradient或RadialGradientBrush画刷,OpacityMask属性就变得更有用了。使用渐变将一种纯色变换到透明色,可创建在整个元素表面褪色的透明效果。例如,下面的按钮就使用了这种效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
<Button FontSize="14"
FontWeight="Bold">
<Button.OpacityMask>
<LinearGradientBrush StartPoint="0 0"
EndPoint="1 0">
<GradientStop Offset="0"
Color="Black" />
<GradientStop Offset="1"
Color="Transparent" />
</LinearGradientBrush>
</Button.OpacityMask>
<Button.Content>A Partially Transparent Button</Button.Content>
</Button>

  还可以结合使用OpacityMash属性和VisualBrush画刷来创建反射效果,如下最常见的WPF效果之一——具有镜像文本的文本框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox x:Name="txt"
FontSize="30">Here is some reflected text</TextBox>
<Rectangle Grid.Row="1"
RenderTransformOrigin="1,0.5">
<Rectangle.Fill>
<VisualBrush Visual="{Binding ElementName=txt}" />
</Rectangle.Fill>
<Rectangle.OpacityMask>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Offset="0.3"
Color="Transparent" />
<GradientStop Offset="1"
Color="#44000000" />
</LinearGradientBrush>
</Rectangle.OpacityMask>
<Rectangle.RenderTransform>
<ScaleTransform ScaleY="-1" />
</Rectangle.RenderTransform>
</Rectangle>
</Grid>