如今的软件市场,竞争已经进入白热化阶段,功能强、运算快、界面友好、Bug少、价格低都已经成为了必备条件。这还不算完,随着计算机的多媒体功能越来越强,软件的界面是否色彩亮丽、是否能通过动画、3D等效果是否吸引用户的眼球也已经成为衡量软件的标准。
软件项目成功的三个要素是:资源、成本、时间。无论是为了在竞争中保持不败还是为了激发起用户对软件的兴趣,提高软件界面的美化程度、恰当的将动画和3D等效果引入应用程序都是一个必然趋势。然而使用传统的桌面应用程序开发工具和框架(如Winform、MFC、VB、Delphi等)进行开发时,为了使软件界面变漂亮、加入动画或者3D效果,边际成本会非常的高。体现在:
资源消耗增大:需要招聘懂得动画和3D编程的程序员,还需要更多的设计师、工薪和沟通成本随着上升。
开发时间增加:界面美化、动画和3D开发远远比业务逻辑开发困难、耗时。
成本增加:随着资源消耗的增加和开发周期的拉长,成本必然增加。
之所以会出现这种情况,根本原因在于传统开发工具和框架并没有原生的支持美化用户界面、向应用程序中添加动画和3D效果等功能。举个简单的例子,当用户提出需要把TextBox的外观改成圆角时,Winform和Delphi程序员只能通过派生新类并在底层做修改的方法来实现。类似的用户需求还有好多不得不实现,否则客户会怀疑我们的开发能力;即使实现了也没有什么额外的经济效益,因为这些东西在客户的眼里都是很简单的东西。
WPF的推出可谓是对症下药、专门解决上述问题。体现在:
XAML语言针对的是界面美化的问题,可以让设计师直接加入开发团队、降低沟通成本。
XAML的图形绘制功能非常强大,可以轻易绘出复杂的图标、图画。
WPF支持滤镜功能,可以像PhotoShop一样为对象添加各种效果。
WPF原生支持动画开发,无论是设计师还是程序员,都能够使用XAML或C#轻松开发制作绚丽的动画效果。
WPF原生支持3D效果,甚至可以将其它3D建模工具创建的模型导进来、为我所用。
Blend作为专门的设计工具让WPF如虎添翼,即能够帮助不了解编程的设计师快速上手,又能够帮助资深开发者快速建立图形或者动画的原型。
1.1 WPF绘图
与传统的.net开发使用GDI+进行绘图不同,WPF拥有自己的一套绘图API。使用这套API不但可以轻松绘制出精美的图形,还可以为各种图形添加类似与PhotoShop的“滤镜效果”及“变形效果”。本节我们就一起研究WPF图形API绘图,效果和变形等功能。
先观察下面一组图片:
显然,这组图片是矢量图(Vector Image),无论怎样放大缩小都不会出现锯齿。你可能会想:“这是组PNG格式的图片吗?”答案是“NO”。这组图是用XAML语言绘制的!XAML绘图本身就是矢量的,而且支持各式各样的填充和效果,甚至还可以添加滤镜,这些功能丝毫不亚于Photoshop。以前,使用PhotoShop制作出来的图形需要程序员使用.net的绘图接口进行二次转换才能应用到程序里,现在好了,直接把XAML代码拿来用就可以了。
绘图并不是VisualStudio的强项,这些漂亮的XAML矢量图是怎么画出来的呢?答案是借助Microsoft
Expression Studio中的Blend和Design两个工具。Blend我们已经介绍过了,用它可以直接绘制XAML图形;Design可以像PhotoShop或者FireWorks那样绘制图形,再由设计者决定导出xaml格式还是png格式。虽然“唯代码派”的程序员们在Visualstudio里一行一行写代码也能把复杂的图形以非可视化的形式创建出来,但在Blend和Design中画出原型再在Visual
Studio里面进行细节的修饰才是提高效率之道。
积沙成塔,集腋成裘,别看前面那些图片很复杂,但都是由几个有限的基本图形组成的。WPF的基本图形包括以下几个(它们都是Shap类的派生类):
Line:直线段,可以设置其笔触(Stroke)。
Rectangle:矩形,既有笔触,又有填充(Fill)。
Ellipse:椭圆,长宽相等的椭圆即为正圆,既有笔触又有填充。
Polygon:多边形,由多条直线线段围成的闭合区域,既有笔触又有填充。
PolyLine:折线(不闭合),由多条首尾相接的直线组成。
Path:路径(闭合区域),基本图形中功能最强的一个,可由若干直线,圆弧,被塞尔曲线组成。
1 直线
直线是最简单的图形。使用X1,Y1两个属性值可以设置它的起点坐标,X2,Y2两个属性值可以设置它的终点坐标。控制终点/起点做标就可以实现平行,交错等效果。Stroke(笔触)属性的数据类型是Brush(画刷),凡是Brush的派生类均可以用于给这个属性赋值。因为WPF提供多种渐变色画刷,所以画直线也可以画出渐变效果。同时,Line的一些属性还可以帮助我们画出虚线以及控制线段终点的形状。下面的例子综合了这些属性:
<Window x:Class="WpfApplication1.Window45" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window45" Height="293" Width="437"> <Grid> <Line X1="10" Y1="20" X2="260" Y2="20" Stroke="Red" StrokeThickness="10"></Line> <Line X1="10" Y1="40" X2="260" Y2="40" Stroke="Orange" StrokeThickness="6"></Line> <Line X1="10" Y1="60" X2="260" Y2="60" Stroke="Green" StrokeThickness="3"></Line> <Line X1="10" Y1="80" X2="260" Y2="80" Stroke="Purple" StrokeThickness="2"></Line> <Line X1="10" Y1="100" X2="260" Y2="100" Stroke="Black" StrokeThickness="1"></Line> <Line X1="10" Y1="120" X2="260" Y2="120" StrokeDashArray="3"
Stroke="Black" StrokeThickness="1"></Line> <Line X1="10" Y1="140" X2="260" Y2="140" StrokeDashArray="5"
Stroke="Black" StrokeThickness="1"></Line> <Line X1="10" X2="260" Y1="160" Y2="160" Stroke="Black"
StrokeThickness="6" StrokeEndLineCap="Flat"></Line> <Line X1="10" X2="260" Y1="180" Y2="180" Stroke="Black"
StrokeThickness="8" StrokeEndLineCap="Triangle"></Line> <Line X1="10" X2="260" Y1="200" Y2="200" StrokeEndLineCap="Round" StrokeThickness="10"> <Line.Stroke> <LinearGradientBrush EndPoint="0,0.5" StartPoint="1,0.5"> <GradientStop Color="Blue"></GradientStop> <GradientStop Offset="1" Color="Red"></GradientStop> </LinearGradientBrush> </Line.Stroke> </Line> </Grid> </Window> |
程序运行效果如下:
有一点需要特别注意,初学者认为绘图一定要在Canvas中完成(谁叫它的名字叫画布呢),其实不然,绘图可以在任何一种布局控件中完成,WPF会自动根据容器的不同计算图形的坐标,日常生活中,常用的绘图容器有Canvas和Grid。
2 矩形
矩形有笔触(Stroke,即边线)和填充(Fill)构成。Stroke属性的设置和Line一样,Fill属性的数据类型是Brush。Brush是一个抽象类,所以我们不可能拿一个Brush类的实例为Fill属性赋值而只能用Brush派生类来进行赋值。WPF绘图系统中包含非常丰富的Brush类型,常用的有:
SolidColorBrush:实心画刷。在XAML中可以使用颜色名称字符串直接赋值。
LinearGradientBrush:线性渐变画刷。色彩沿设定的直线方向,按设定的变化点进行渐变。
RadialGradientBrush:径向渐变画刷。色彩沿半径的方向、按设定的变化点进行渐变,形成圆形填充。
ImageBrsh:使用图片作为填充类容。
DrawingBrush:使用矢量图(Vector)和位图(BitMap)作为填充内容。
VisualBrush:WPF中的每个控件都是有FrameWrokElement派生而来的,而FrameWorkElment类又是由Visual类派生而来的。Visual意为“可视”之意,每个控件的可视化形象就可以通过Visual类的方法获得。获得这个可视化形象之后,我们可以用这个形象进行填充,这就是VisualBrush。比如我想把窗体上的某个控件拖到另外一个位置,当鼠标松开之前需要在鼠标指针下显示一个幻影,这个幻影就是使用VisualBrush填充出来的一个矩形,并让矩形捕捉鼠标的位置、随鼠标移动。
下面是使用不同画刷填充矩形的综合实例:
<Window x:Class="WpfApplication1.Window46" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window46" Height="390" Width="600"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="180" /> <ColumnDefinition Width="10" /> <ColumnDefinition Width="180" /> <ColumnDefinition Width="10" /> <ColumnDefinition Width="180*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="160" /> <RowDefinition Height="10" /> <RowDefinition Height="160" /> </Grid.RowDefinitions> <!--实心填充--> <Rectangle Grid.Row="0" Grid.Column="0" Stroke="Black" Fill="LightBlue"></Rectangle> <!--线性渐变--> <Rectangle Grid.Row="0" Grid.Column="2"> <Rectangle.Fill> <LinearGradientBrush StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="#FFB6F8F1" Offset="0"></GradientStop> <GradientStop Color="#FF0082BD" Offset="0.25"></GradientStop> <GradientStop Color="#FF95DEFF" Offset="0.6"></GradientStop> <GradientStop Color="#FF004F72" Offset="1"></GradientStop> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <!--径向渐变--> <Rectangle Grid.Row="0" Grid.Column="4"> <Rectangle.Fill> <RadialGradientBrush> <GradientStop Color="#FFB6F8F1" Offset="0"></GradientStop> <GradientStop Color="#FF0082BD" Offset="0.25"></GradientStop> <GradientStop Color="#FF95DEFF" Offset="0.75"></GradientStop> <GradientStop Color="#FF004F72" Offset="1.5"></GradientStop> </RadialGradientBrush> </Rectangle.Fill> </Rectangle> <!--图片填充--> <Rectangle Grid.Row="2" Grid.Column="0"> <Rectangle.Fill> <ImageBrush ImageSource="./01077_1.png" Viewport="0,0,0.3,0.3" TileMode="Tile"> </ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Grid.Row="2" Grid.Column="2"> <Rectangle.Fill> <DrawingBrush Viewport="0,0,0.2,0.2" TileMode="Tile"> <DrawingBrush.Drawing> <GeometryDrawing Brush="LightBlue"> <GeometryDrawing.Geometry> <EllipseGeometry RadiusX="10" RadiusY="10"></EllipseGeometry> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingBrush.Drawing> </DrawingBrush> </Rectangle.Fill> </Rectangle> <!--无填充,使用线性渐变填充边框--> <Rectangle Grid.Row="2" Grid.Column="5" StrokeThickness="10"> <Rectangle.Stroke> <LinearGradientBrush StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="White" Offset="0.3"></GradientStop> <GradientStop Color="Blue" Offset="1"></GradientStop> </LinearGradientBrush> </Rectangle.Stroke> </Rectangle> </Grid> </Window> |
运行效果如下图:
使用画刷的时候,建议先在Blend里面绘制图大致的效果然后再在Visual Studio里面微调。
接下来让我们看一个VisualBrush的例子。为了简单起见,目标控件是一个Button,实际工作中换成复杂的控件也一样。程序的XAML代码如下:
<Window x:Class="WpfApplication1.Window47" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window47" Height="300" Width="400" Background="Orange"> <Grid Margin="10"> <Grid.ColumnDefinitions> <ColumnDefinition Width="160" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="160" /> </Grid.ColumnDefinitions> <StackPanel Background="White" x:Name="spleft"> <Button Height="40" Content="OK" x:Name="btnReal" Click="btnReal_Click"></Button> </StackPanel> <Button Grid.Column="1" Content=">>" Margin="5,0"></Button> <StackPanel Grid.Column="2" Background="White" x:Name="spRight"> </StackPanel> </Grid> </Window> |
Button的事件处理器代码如下:
double o = 1;//不透明度指数 private void btnReal_Click(object sender, RoutedEventArgs e) { VisualBrush vb = new VisualBrush(this.btnReal); Rectangle rtg = new Rectangle(); rtg.Width = btnReal.Width; rtg.Height = btnReal.Height; rtg.Fill = vb; rtg.Opacity = o; o -= 0.2; this.spRight.Children.Add(rtg); } |
运行效果如下图:
3. 椭圆
椭圆也是一种常见的几何图形,它的使用方法和矩形没有什么区别。下面的例子是绘制一个球体,球体的轮廓是正圆(Circle),Width和Height相等的椭圆即为正圆:球体的光影使用径向渐变实现,XAML代码如下:
<Window x:Class="WpfApplication1.Window48" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window48" Height="300" Width="300"> <Grid> <Ellipse Height="140" Name="ellipse1" Stroke="Gray" Width="140" Cursor="Hand" ToolTip="A Ball"> <Ellipse.Fill> <RadialGradientBrush GradientOrigin="0.2,0.8" RadiusX="0.75" RadiusY="0.75"> <RadialGradientBrush.RelativeTransform> <TransformGroup> <RotateTransform Angle="90" CenterX="0.5" CenterY="0.5"></RotateTransform> </TransformGroup> </RadialGradientBrush.RelativeTransform> <GradientStop Color="#FFFFFFFF" Offset="0" /> <GradientStop Color="#FF444444" Offset="0.66" /> <GradientStop Color="#FF999999" Offset="1" /> </RadialGradientBrush> </Ellipse.Fill> </Ellipse> </Grid> </Window> |
运行效果如下图:
与前面提到的一样,椭圆的绘制和色彩填充在Blend里面完成的,在VS里面又做了一些相应的调整。
4 路径
路径(Path)可以说是WPF绘图最强大的工具,一来是因为它完全可以替代其它几种图形,而来它可以将直线,圆弧,贝塞尔曲线等基本元素组合起来,形成更复杂的图形。路径最重要的一个属性就是Data,Data的数据类型是Geometry(几何图形),我们正是使用这个属性将一些基本的线段拼接起来,形成复杂的图形。
为Data属性赋值的方法有两种:一种是标签式的标准语法,另外一种是专门用于绘制几何图形的“路径标记语法”。本小节我们使用标准标签语法认识各种线段,下一节我们将学习绘制几何图形的路径标记语法。
想要使用Path路径绘制图形,首先要知道几何图形数据是如何组合到Data属性中的。Path的Data属性是Geometry类,但是Geometry类是一个抽象类,所以我们不可能在XAML中直接使用<Geometry>标签。
<!--不可能出现--> <Path> <Geometry> <!----> </Geometry> </Path> |
我们可以使用Geometry的子类。Geometry的子类包括:
LineGeometry:直线几何图形。
RectangleGeometry:矩形几何图形。
EllipseGeometry:椭圆几何图形。
PathGeometry:路径几何图形。
StreamGeometry:PathGeometry的轻量级替代品,不支持Binding、动画等效果。
CombinedGeometry:由多个基本几何图形关联在一起,形成的单一几何图形。
GeometryGroup:由多个基本几何图形组合在一起,形成的几何图形组。
可能让大家比较迷惑的是:前面已经见过Line,Rectangle,Ellipse等类,怎么现在又出来了LineGeometry、RectangleGeometry、EllipseGeometry类呢?它们的区别在于前面介绍的Line,Rectangle,Ellipse都是可以独立存在的对象,而这些*Geometry类只能用于结合成其它几何图形、不能独立存在-----当我们在Blend里面选中一组独立的几何图形并在菜单里执行组合路径命令时,本质上就是把原来独立的Line,Rectangle,Ellipse对象转换成了*Geometry对象并结合成一个新的复杂几何图形。
回到Data的Path属性,下面这个例子简要的展示了各种几何图形:
<Window x:Class="WpfApplication1.Window49" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window49" Height="350" Width="340"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="160" /> <RowDefinition Height="160" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="160" /> <ColumnDefinition Width="160" /> </Grid.ColumnDefinitions> <!--直线--> <Path Stroke="Blue" StrokeThickness="2" Grid.Row="0" Grid.Column="0"> <Path.Data> <LineGeometry StartPoint="0,0" EndPoint="160,160"></LineGeometry> </Path.Data> </Path> <!--矩形路径--> <Path Stroke="Orange" Fill="Yellow" Grid.Row="0" Grid.Column="1"> <Path.Data> <RectangleGeometry Rect="20,20,120,120" RadiusX="10" RadiusY="10"></RectangleGeometry> </Path.Data> </Path> <!--椭圆路径--> <Path Stroke="Green" Fill="LawnGreen" Grid.Column="0" Grid.Row="1"> <Path.Data> <EllipseGeometry Center="80,80" RadiusX="60" RadiusY="40"></EllipseGeometry> </Path.Data> </Path> <!--自定义路径--> <Path Stroke="Yellow" Fill="Orange" Grid.Row="1" Grid.Column="1"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure StartPoint="25,140" IsClosed="True"> <PathFigure.Segments> <LineSegment Point="20,40"></LineSegment> <LineSegment Point="40,110"></LineSegment> <LineSegment Point="50,20"></LineSegment> <LineSegment Point="80,110"></LineSegment> <LineSegment Point="110,20"></LineSegment> <LineSegment Point="120,110"></LineSegment> <LineSegment Point="140,40"></LineSegment> <LineSegment Point="135,140"></LineSegment> </PathFigure.Segments> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> </Grid> </Window> |
运行效果如下图:
其实LineGeometry、RectangleGeometry、EllipseGeometry都比较简单,现在着重来看PathGeometry。可以说,WPF绘图的重点是Path,Path的重点在于PathGeometry。PathGeometry之所以这么重要的原因是因为Path的Figures属性可以容纳PathFigure对象,而PathFigure对象的Segments属性又可以容纳各种线段用来组合成复杂的图形。XAML代码结构如下:
<Path> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigure> <PathFigure.Segments> <!--线段内容--> </PathFigure.Segments> </PathFigure> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path> |
因为Figures是PathGeometry的默认内容属性、Segments是PathFigure的默认内容属性,所以常简化为这样:
<Path> <Path.Data> <PathGeometry> <PathFigure> <!--线段内容--> </PathFigure> </PathGeometry> </Path.Data> </Path> |
了解了上面两个格式之后,我们可以把眼光集中在各种线段上,它们是:
LineSegment:直线段。
ArcSegment:圆弧线段。
BezierSegment:三次方贝塞尔曲线段(默认的贝塞尔曲线指的就是三次方贝塞尔曲线,所以Cubic一词被省略)。
QuadraticBezierSegment:二次方贝塞尔曲线段。
PolyLineSegment:多直线段。
PolyBezierSegment:多三次方贝塞尔曲线段。
PolyQuadraticBezierSegment:多二次方贝塞尔曲线段。
在绘制这些线段的时候需要注意,所有的这些线段多是没有起点的(StartPoint),因为起点就是前一个线段的终点,而第一个线段的起点则是PathFigure的StartPoint。请看下面这些例子:
LineSegment最为简单,只需要控制它的终点(Point)即可。
<Window x:Class="WpfApplication1.Window50" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window50" Height="307" Width="384"> <Grid VerticalAlignment="Center" HorizontalAlignment="Center"> <Path Stroke="Green" Fill="LawnGreen" StrokeThickness="2"> <Path.Data> <PathGeometry> <PathFigure StartPoint="0,0" IsClosed="True"> <LineSegment Point="150,0"></LineSegment> <LineSegment Point="150,30"></LineSegment> <LineSegment Point="90,30"></LineSegment> <LineSegment Point="90,150"></LineSegment> <LineSegment Point="60,150"></LineSegment> <LineSegment Point="60,30"></LineSegment> <LineSegment Point="0,30"></LineSegment> </PathFigure> </PathGeometry> </Path.Data> </Path> </Grid> </Window> |
运行效果如下图:
ArcSegment用来绘制圆弧。point属性用来指明圆弧连接的终点;圆弧截取至椭圆,SIZE属性即是完整椭圆的横轴和纵轴半径,SweepDirection属性指明圆弧是顺时针方向还是逆时针方向;如果椭圆上的两个点位置不对称,那么这两点间的圆弧就会分为大弧和小弧,IsLargeArc属性用于指明是否使用大弧去连接;RotationAngle属性用来指明圆弧母椭圆的旋转角度,如下图所示是对几个属性的变化做出的详细对比:
BezierSegment(三次方贝塞尔曲线)由4个点决定:
(1)起点:即前一个线段的终点或PathFigure的StartPoint。
(2)终点:Point3属性,即曲线的终点位置。
(3)两个控制点:Point1和Point2属性。
初略的说,三次方贝塞尔曲线就是由起点出发走向Point1方向,再走向Point2方向,最后到达终点的平滑曲线,具体的算法请查阅维基百科“被塞尔曲线”词条。
如下代码是XAML代码表示的三次方贝塞尔曲线:
<Window x:Class="WpfApplication1.Window51" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window51" Height="300" Width="300"> <Grid> <Path Stroke="Black" StrokeThickness="2"> <Path.Data> <PathGeometry> <PathFigure StartPoint="0,0"> <BezierSegment Point1="250,0" Point2="50,200" Point3="300,200"> </BezierSegment> </PathFigure> </PathGeometry> </Path.Data> </Path> </Grid> </Window> |
运行效果如下图:
QuadraticBezierSegment(二次方贝塞尔曲线)与BezierSegment类似,只是控制点由两个变为了一个。也就是说QuadraticBezierSegment由3个点决定:
(1)起点:即前一个线段的终点或PathFigure的StartPoint。
(2)终点:Point2属性,即曲线的终止位置。
(3)控制点:Point1属性。
如下的代码就表示的是二次方贝塞尔曲线:
<Window x:Class="WpfApplication1.Window52" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window52" Height="339" Width="325"> <Grid> <Path Stroke="Blue" StrokeThickness="2"> <Path.Data> <PathGeometry> <PathFigure StartPoint="0,300"> <QuadraticBezierSegment Point1="150,-150" Point2="300,300"> </QuadraticBezierSegment> </PathFigure> </PathGeometry> </Path.Data> </Path> </Grid> </Window> |
运行效果如下图:
至此,简单的路径就介绍完了。如果想绘制出复杂的图形来,我们要做的仅仅是在PathFigure把Segment一段段的加上去。
GeometryGroup也是Geometry的一个派生类,它最大的特点是可以将一组PathGeometry组合在一起,如下面的例子:
<Window x:Class="WpfApplication1.Window53" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window53" Height="300" Width="300"> <Grid> <Path Stroke="Black" Fill="LightBlue" StrokeThickness="1"> <Path.Data> <GeometryGroup> <PathGeometry> <PathFigure StartPoint="0,0"> <BezierSegment Point1="250,0" Point2="50,200" Point3="300,200"></BezierSegment> </PathFigure> </PathGeometry> <PathGeometry> <PathFigure StartPoint="0,0"> <BezierSegment Point1="230,0" Point2="50,200" Point3="300,200"></BezierSegment> </PathFigure> </PathGeometry> <PathGeometry> <PathFigure StartPoint="0,0"> <BezierSegment Point1="210,0" Point2="50,200" Point3="300,200"></BezierSegment> </PathFigure> </PathGeometry> <PathGeometry> <PathFigure StartPoint="0,0"> <BezierSegment Point1="190,0" Point2="50,200" Point3="300,200"></BezierSegment> </PathFigure> </PathGeometry> <PathGeometry> <PathFigure StartPoint="0,0"> <BezierSegment Point1="170,0" Point2="50,200" Point3="300,200"></BezierSegment> </PathFigure> </PathGeometry> <PathGeometry> <PathFigure StartPoint="0,0"> <BezierSegment Point1="150,0" Point2="50,200" Point3="300,200"></BezierSegment> </PathFigure> </PathGeometry> <PathGeometry> <PathFigure StartPoint="0,0"> <BezierSegment Point1="130,0" Point2="50,200" Point3="300,200"></BezierSegment> </PathFigure> </PathGeometry> </GeometryGroup> </Path.Data> </Path> </Grid> </Window> |
运行效果如下图:
5 路径标记语法
Path是如此强大,可以让我们随心所欲的绘制图形,然而它的一大缺点是不容忽视的,那就是其标签语法的繁琐。一般情况下,复杂图形(Path)是由数10条线段连接而成,按照标签式语法,每条线段是一个标签(Segment)、每个标签占据一行,一个图形就要占几十行代码。而这仅仅是一个图形,要组成一个完整的图画又往往需要10几个图形组合在一起,有可能占据数百行代码!幸好这种事情没有发生,因为我们可以借助专供WPF绘图使用的路径标记语法(Path
MarkUp Syntax)来极大的简化Path的描述。
路径标记语法实际上就是各种线段的简记法,比如<LineSegment point="150,5"/>可以简写为"L
150,5",这个L就是路径标记语法中的一个绘图命令。不仅如此,路径标记语法还增加了一些更实用的绘图命令,比如H用来绘制水平线,“H
180”就是指从当前点画一条水平直线,终点的横坐标是180(你不需要考虑纵坐标,纵坐标和当前点一致)。类似的还有V命令,用来画竖直直线。
使用路径标记语法绘图一般分三步:移动至起点---绘图----闭合图形。这三步使用的命令稍有区别。移动到起点使用的移动命令M,绘图使用的是绘图命令,包括:L,H,V,A,C,Q等,下面会逐一介绍;如果图形是闭合的,需要使用闭合命令Z,这样最后一条线段的终点与第一条线段的起点间就会连接上一条直线段。
路径标记语法是不区分大小写的,所以A和a,H和h是等价的。在路径标记语法中使用两个Double类型的数值来表示一个点,第一个值表示的是横坐标(记做X),第二个值表示纵坐标(记作y),两个数字可以使用逗号分割(x,y)又可以使用空格分割(x
y)。由于路径标记语法中使用的空格作为两个点之间的分割,为了避免混淆,建议使用逗号作为点横纵坐标的分隔符。
如下图所示是常用的路径标记语法的总结:
在上述的命令中,S和T两个命令比较特殊。S用于绘制平滑的赛贝尔曲线,但只需要给出一个控制点,这个控制点相当于普通赛贝尔曲线的第二个控制点,之所以第一个控制点省略不写是因为平滑三次方赛贝尔曲线会把前一条贝塞尔曲线的第二空控制点以起点为对称中心的对称点当作当作自己的第一个控制点(如果前面的线段不是贝塞尔曲线,则第一个控制点和起点相同)。例如,下面两条曲线是等价的:
<Path Stroke="Red" Data="M 0,0 C 30,0 70,100 100,100 S 170,0 200,0"></Path> <Path Stroke="Blue" Data="M 0,0 C 30,0 70,100 100,100 C 130,100 170,0 200,0"></Path> |
与S相仿,T命令用于绘制平滑二次贝塞尔曲线,绘制的时候如果前面也是一条二次贝塞尔曲线的话,T命令会把前面的这段曲线的控制点以起点为对称中心的对称点当作自己的控制点(如果前面的线段不是二次贝塞尔曲线则控制点与起点相同)。下面两条曲线等价:
<Path Stroke="Red" Data="M 0,200 Q 100,0 200,200 T 400,200"></Path> <Path Stroke="Blue" Data="M 0,200 Q 100,0 200,200 Q 300,400 400,200"></Path> |
现在我们就可以使用路径标记语法来绘图了!使用方法是吧这这些命令串起来、形成一个字符串,然后赋值给Path的Data属性。使用Blend绘图时,Blend会自动使用路径标记语法来记录数据而不是是用代码量巨大的标签式语法。
6 使用Path剪切界面元素
实际工作中经常会遇到制作不规则的窗体或者控件,WPF在这方面做了良好的支持,仅需使窗体和控件的Clip属性就可以轻松做到。
Clip属性被定义在UIEelment类中,因此,WPF窗体的所有控件、图形都具有这个属性。Clip属性的数据类型是Geometry,与Path的Data属性一致。因此,我们只需要按照需求制作好特殊形状的Path并把Path的Data属性值赋值给目标窗体、控件或者其它图形,对目标的剪切就算完成了。请看下面这个不规则窗体的例子。
<Window x:Class="WpfApplication1.Window56" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window56" Height="250" Width="300" WindowStyle="None"
AllowsTransparency="True" WindowStartupLocation="CenterScreen" Background="Yellow"> <Grid VerticalAlignment="Center" HorizontalAlignment="Center"> <Path Stroke="Orange" Fill="Yellow" x:Name="clipPath0" Visibility="Hidden"
Data="M 55,100 A 50,50 0 1 1 100,60 A 110,95 0 0 1 200,60 A 50,50 0 1 1 250,100 A 110,95 0 1 1 55,100 Z"> </Path> <Button Content="Clip" Width="80" Height="25" Click="Button_Click"
HorizontalAlignment="Center" VerticalAlignment="Center"></Button> </Grid> </Window> |
如果想让一个窗体可以被裁切,那么其AllowsTransparency必须要设置为True,这个属性设为True之后,WindowStyle必须要设置为None。
Button的事件处理器中代码如下:
private void Button_Click(object sender, RoutedEventArgs e) { this.Clip = clipPath0.Data; } |
运行程序,单击按钮,运行效果如下图:
1.2 图形的效果和滤镜
以往,程序员们很头疼的一件事情就是要精确实现UI设计师们给出的样稿。有些时候程序员可以使用设计师提供的图片作为背景或者贴图,但这些图片往往都是位图而非矢量图,所以当应用了这些图片的窗体或控件的尺寸发生改变时,图片就会出现锯齿、马赛克或失真等情况。对于那些对用户体验要求比较高的软件程序员不能直接使用位图了,只能用代码来实现设计师的设计。要知道,设计师手里用的是PhotoShop,FireWorks这些专业的设计工具,加个阴影,生成个发光效果就是点点鼠标的事,同样的效果让程序员使用C#实现就没有那么容易了,有的时候甚至需要用到一些在游戏里面才会用到的技术和知识,这无疑增加了开发的难度和成本。
WPF的出现无疑是程序员的福音,因为不但像阴影,发光效果可以使用一两个属性来实现,就连通道、动态模糊这些高级的效果也可以轻松实现。同时,设计师和程序员还可以像为PhotoShop开发滤镜一样为WPF开发效果类库,届时只需要把类库引入到项目中就可以使用其中的效果了(微软官方网站和一些开源网站上已经有很多效果类库可供使用)。
在UIElement类的成员中你可以找到BitmapEffect和Effect这两个属性,这两个属性都是为UI元素添加效果。你可能会问:为做同一件事准备了两个属性,难道不冲突吗?答案是:的确冲突。WPF最早的版本里面只有BitmapEffect这个属性,这个属性使用CPU的运算能力为UI元素添加效果,这样做的问题就是效果一多或者让带有效果UI元素参与动画,程序的性能会因为CPU资源被大量占用而大幅降低(要么反应变慢,要么刷新或者动画变的很卡)。随后的版本中,微软决定转用显卡GPU的运算能力为UI元素添加效果,于是添加了Effect这个属性。这样即减少了对CPU的浪费又将应用程序的视觉效果拉平到与游戏程序一个级别。
因为有Effect这个属性替换BitmapEffect,所以你在MSDN文档里面看到BitmapEffect被标记为已过时,不过在WPF4.0中Bitmapeffect仍然可以使用,也就是说未来两三年里,bitmapEffect仍然可以使用。下面让我们尝试如何使用这两种效果:
1.2.1 简单易用的BitmapEffect
BitmapEffect定义在UIElement类中,它的数据类型是BitmapEffect类。BitmapEffect是一个抽象类,所以我们只能使用它的派生类来为UIElement的BitmapEffect属性赋值。BitmapEffect类的派生类并不多,包括以下几个:
BevelBitmapEffect:斜角效果。
BitmapEffectGroup:复合效果(可以把多个BitmapEffect组合在一起)。
BlurBitmapEffect:模糊效果。
DropShadowBitmapEffect:投影效果。
EmbossBitmapEffect:浮雕效果。
OuterGlowBitmapEffect:外发光效果。
每个效果都有自己的一系列属性来做调整,比如你可以调整投影效果的投影高度,阴影深度和角度,让用户感觉光是由某个角度投射下来;你也可以调整外发光效果的颜色和延展距离。下面一个DropShadowBitmapEffect的简单例子:
<Window x:Class="WpfApplication1.Window57" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window57" Height="300" Width="300"> <Grid> <Button Width="80" Height="50"> <Button.BitmapEffect> <DropShadowBitmapEffect Direction="-45" Opacity="0.75"
Color="Red" ShadowDepth="7"></DropShadowBitmapEffect> </Button.BitmapEffect> </Button> </Grid> </Window> |
运行效果如下图:
对于每个BitmapEffect的派生类MSDN都有相当详细的记述,请大家自行查阅。
1.2.2 丰富多彩的Effect
绘图软件Photoshop能够大获全胜的一个重要因素就是它的插件标准是公开的,这样,众多的第三方公司和人员就可以为它设计各种各样的插件、极大的丰富它的功能----众人拾柴火焰高。在众多的Photoshop插件中,最重要的一类就是滤镜,或者说是图片的效果。比如说我要把一张图片制作成老照片的效果,在没有滤镜的情况下,就需要手工调整很多图片属性才能做到,而使用滤镜,则只需要把滤镜加在图片上即可。显然使用滤镜效果可以获得如下好处:
提高工作效率。
得到更专业的效果。
对使用者的技术水平要求相对较低。
WPF中引进了这种滤镜插件的思想,其成功就是UIElement类的Effect属性。Effect属性的类型是Effect类,Effect是抽象类,也就是说UIElement的Effect属性可以接收Effect类的任何一个派生类实例作为自己的值。Effect类位于System.Windows.Media.Effects名称空间中,它的派生类有3个,分别是:
BlurEffect:模糊效果。
DropShadowEffect:投影效果。
ShaderEffect:着色器效果(抽象类)。
你可能会想----强大的Effect派生类怎么还没有已经废弃的BitmapEffect类多呢?答案是这样的:因为模糊和投影在编程中使用的最多,所以.NET
Framwork内建了这两个效果。这两个效果使用起来非常方便,而且请注意,这两个效果是使用GPU进行渲染,而不像BitmapEffect那样使用CPU渲染。ShaderEffect仍然是个抽象类,它就是流给滤镜开发人员的接口。只要你开发派生自ShaderEffect的效果类,别人就可以直接拿来用。
开发着色器效果需要使用Pixel Shader语言(简写和Photoshop一样,也是PS)和一些DirectX知识,超出了本书的范围。感兴趣的读者可以在微软的官网上找到它的SDK和开发文档。
对于大多数WPF开发人员来说,我们需要的是现成的滤镜效果,所以官方的滤镜包可以丛这里下载:http://wpffx.codeplex.com。解压下载的ZIP文件,可以看到分别为WPF和Silveright准备的两套滤镜。进入WPF文件夹,ShaderEffectLibrary文件夹里的项目就是效果库。效果库的使用方法如下:
首先到http://wpf.codeplex.com/releases/view/14962下载Shader
Effect BuildTask And Templates.zip。解压ZIP文件之后按照其中的Readme文档进行安装,配置。这是着色器效果的编译/开发环境,没有它,着色器效果项目将不能被编译。如果你想开发自己的效果滤镜,也必须安装这个环境。检验安装是否成功的方法是启动VS,查看是否可以新建WPF
Shader Effect Library项目,如下图所示:
新建一个WPF解决方案,把ShaderEffectLibrary中的项目添加进来,并为WPF项目添加对WPFShaderEffectLibrary项目的引用,你就可以使用效果库里面的效果了。
使用滤镜库,只需要设置几个属性,层次分明、动感十足的图片效果就出来了!这样的工作即可以由设计师来完成,也可以由程序员来完成。如果对效果不满意,直接在XAML文件里面修改并保存就可以了,而不必再像之前那样再用Photoshop等工具进行返工。同时,同一张原图片可以加载为不同的效果,也不必像之前一样先由设计师制作出多张图片再添加进应用程序,这样,程序的编译结果也会小很多。
1.3 图形的变形
当我们看到“变形”这个词时,首先会想起什么?拉长、挤扁?放大、缩小?还是... 变形金刚?其实WPF中的“变形“的含义很广,尺寸、位置、坐标系比例、旋转角度等的变化都算变形。
WPF中的变形是和UI元素分开的。举个例子,你可以设计一个”向左旋转45度“的变形,然后把这个变形赋值给不同的UI元素的变形控制属性,这些UI元素都会向左旋转45度了。这种将元素和变形控制属性分开的设计方案即减少了为UIElement类添加过多的属性,又提高了变形类实例的复用性,可谓一举两得。这种设计模式非常符合策略模式中的”有一个“比”是一个“更加灵活的思想。
控制编写的属性有两个,分别是:
RenderTransform:呈现变形,定义在UIElement类中。
LayoutTransform:布局变形,定义在FramworkElement类中。
因为FramworkElment类派生自UIElement类,而控件的基类Control类又派生自FramworkElment类中,所以在控件级别,你两个属性都可以看到。这两个属性都是依赖属性,它们的数据类型都是Transform抽象类,也就是说,Transform类的派生类均可为这两个属性赋值。
Transform抽象类的派生类有如下一些:
MatrixTransform:矩阵变形,把容纳被变形UI元素的矩形顶点看做是一个矩形进行变形。
RotateTransform:旋转变形,以给定的点为旋转中心,以角度为单位进行旋转变形。
ScaleTransform:坐标系变形,调整被变形元素的坐标系,可产生缩放效果。
SkewTransform:拉伸变形,可在横向和纵向上对被变形元素进行拉伸。
TranslateTransform:偏移变形,使被变形元素在横向或者纵向上偏移一个给定的值。
TransformGroup:变形组,可以把多个独立的变形合成为一个变形组、产生复合变形效果。
1.3.1 呈现变形
什么是呈现呢?相信大家都见过海市蜃楼吧!远远望去,远方的天空中漂浮着一座城市,而实际上那里没有城市,有的只是沙漠和海洋...
...,海市蜃楼形成的原因是密度不均的空气使光线产生折射,最终让人看到城市的影像呈现在本不应该出现的位置上---这就是城市影像的呈现出现了变形。WPF的RederTransform属性就是要起到这个作用,让UI元素呈现出来的属性与它本来的属性不一样!比如,一个按钮本来处于Canvas或者Grid的左上角,而我可以使用RenderTransform让它呈现在右下角并且旋转45°。
观察下面这个例子:
<Window x:Class="WpfApplication1.Window59" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window59" Height="334" Width="485"> <Grid Margin="10" Background="AliceBlue"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"> </ColumnDefinition> <ColumnDefinition Width="*"> </ColumnDefinition> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"> </RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Button Width="80" Height="80" Content="OK"> <Button.RenderTransform> <!--复合变形--> <TransformGroup> <!--旋转变形--> <RotateTransform CenterX="40" CenterY="40" Angle="45"></RotateTransform> <!--偏移变形--> <TranslateTransform X="300" Y="150"></TranslateTransform> </TransformGroup> </Button.RenderTransform> </Button> </Grid> </Window> |
在布局Grid里,布局分为两行两列,并且第一行行高,第一列列宽都是由Button来决定的。同时,我为Button的RenderTransform设置了一个复合变形,使用Transform将一个偏移变形和一个旋转变形组合在了一起。偏移变形将Button的呈现(而不是Button本身)向右移动300像素,向下移动150像素;旋转变形将Button的呈现向右旋转45°。在窗体的设计器里面,我们可以清晰的看到Button的位置并没有改变(第一行和第一列并没有变化),但Button却出现在了右下(300,150)的位置,并向右旋转了45°。如下图所示:
运行效果如下图:
用户并不能觉察到究竟是控件本身的位置、角度发生了变化,还是呈现的位置发生了变化。
为什么需要呈现变形呢?答案是:为了效率!在窗体上移动UI元素本身会导致窗体布局的改变,而窗体的布局的每一个(哪怕是细微的)变化都将导致所有的窗体元素的尺寸测算函数,位置测算函数、呈现函数等的调用,造成系统资源占用激增、程序性能陡降。而使用呈现变形则不会遇到这样的问题,呈现变形值改变元素显示在哪里,所以不牵扯布局的变化、只涉及窗体的重绘。所以,当你需要制作动画的时候,请切记要使用RenderTransform。
1.3.2 布局变形
与呈现变形不同,布局变形会影响窗体的布局、导致窗体布局的重新测算。因为窗体布局的重新测算和绘制会影响程序的性能,所以布局变形一般只用在静态变形上,而不用于绘制动画。
考虑这样一个需求:制作一个文字纵向排列的浅蓝色标题栏。如果我们使用呈现变形,代码如下:
<Window x:Class="WpfApplication1.Window60" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window60" Height="338" Width="471"> <Grid x:Name="titleBar" Background="LightBlue"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <TextBlock FontSize="24" Text="Hello Transformer" VerticalAlignment="Bottom" HorizontalAlignment="Center"> <TextBlock.RenderTransform> <RotateTransform Angle="-90"></RotateTransform> </TextBlock.RenderTransform> </TextBlock> </Grid> </Window> |
设计器中的效果如下:
尽管我们让显示文字的TextBlock“看起来”旋转了90°,但TextBlock本身并没有变化,改变的只是它的显示,所以,它的宽度仍然是吧宽度设为Auto的第一列撑的很宽。显然这不是我们希望看到的。
分析需求,我们实际需要的是静态改变TextBlock的布局,因此应该使用LayoutTransform。仅需要对上面的代码进行一处改动:
<Window x:Class="WpfApplication1.Window60" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window60" Height="338" Width="471"> <Grid x:Name="titleBar" Background="LightBlue"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <TextBlock FontSize="24" Text="Hello Transformer" VerticalAlignment="Bottom" HorizontalAlignment="Center"> <TextBlock.LayoutTransform> <RotateTransform Angle="-90"></RotateTransform> </TextBlock.LayoutTransform> </TextBlock> </Grid> </Window> |
设计器中的效果如图所示:
1.4 动画
何为动画?动画自然就是“会动的画”。所谓“会动”不光指位置会移动,还包括角度的旋转、颜色的变化、透明度的增减等。细心的读者已经发现,动画本质就是在一个时间段里对象位置、角度、颜色、透明度等属性值的连续变化。这些属性中,有些是对象自身的属性,有些则是上一节所学的图形变形的属性。有一点需要注意,WPF规定,可以用来制作动画的属性必须是依赖属性。
变化即是运动。“没有脱离运动的物体,也没有脱离物体的运动”,唯物主义如是说。WPF的动画也是一种运动,这种运动的主体就是各种UI元素,这种运动本身就是施加在UI控件上的一些Timerline派生类的实例。在实际工作中,我们要做的事情往往就是先设计好一个动画构思、用一个Timerline派生类的实例加以表达,最后让某个UI元素来执行这个动画、完成动画与动画主体的结合。
简单的动画用一个元素来完成就可以了,就像一个演员的独角戏,WPF把简单动画称为AnimationTimeline。复杂的(即并行的,复合的)动画就需要UI上多个元素协同完成,就像电影中的一段场景。复杂动画的协同包括有哪些UI元素参与动画、每个元素的动画行为是什么、动画何时开始何时结束等。WPF把一组协同的动画也称做Storyboard。
Timeline、AnimationTimeline、Storyboard的关系如下图所示:
本节分两部分,先研究了如何设计简单独立的动画,再研究如何把简单的动画组合在一起形成场景。
1.4.1 简单独立动画
前面说过,动画就是“会动的画”,而这个会动指的是能够让UI元素变形的某个属性值产生了连续的变化。任何一个属性都有自己的数据类型,比如UIElement的Width和Height属性为Double类型,Window的Title属性为string类型。几乎针对每个可能的数据类型,WPF的动画子系统都为其准备了相应的动画类,这些动画类均派生自AnimationTimeline。它们包括:
上面列出的这些类都带有Base后缀,说明它们都是抽象基类。完整的情况下,这些抽象的基类又能派生出三种动画,即简单动画、关键帧动画、沿路径运动的动画。例如DoubleAnimationBase,它完整的派生出了3个具体的动画,如下图所示:
而针对Int类型的Int32AnimationBase只派生出了Int32Animation和Int32AnimationUsingKeyFrames两个具体的动画类。BooleanAnimationBase和CharAnimationBase的派生类则更少,只有关键帧动画类。
因为WPF动画系统中Double类型属性用的最多,而且DoubleAnimationBase的派生类也最完整,所以本节只讲述DoubleAnimationBase的派生类。学习完这个类,其它的动画类型亦可触类旁通。
1,简单的线性动画
所谓简单的线性动画,就是指仅有变化起点、变化终点、变化幅度、变化时间4个要素构成的动画。
变化时间(Duration属性):必须指定,数据类型是Duration。
变化终点(To属性):如果没有指定变化终点,程序将采用上一次的动画的终点或默认值。
变化起点(From属性):如果没有指定变化的起点则以变化目标属性的当前值为起点。
变化幅度(By属性):如果同时指定了变化终点,变化幅度将被忽略。
让我们分析一个例子,简单的XAML代码如下:
<Window x:Class="WpfApplication1.Window61" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window61" Height="372" Width="475"> <Grid> <Button Width="80" Height="80" HorizontalAlignment="Left" VerticalAlignment="Top"> <Button.RenderTransform> <TranslateTransform X="0" Y="0" x:Name="tt"></TranslateTransform> </Button.RenderTransform> </Button> </Grid> </Window> |
用户界面上只包含了一个Button,这个Button的RederTransform属性值是一个名为tt的TranslateTransform对象,改变这个对象的X,Y值就会让Button的显示位置(而不是现在的真实位置)变化。Button的Click事件处理器代码如下:
private void Button_Click(object sender, RoutedEventArgs e) { DoubleAnimation dax = new DoubleAnimation(); DoubleAnimation day = new DoubleAnimation(); //指定起点 dax.From = 0; day.From = 0; //指定终点 Random rdm = new Random(); dax.To = rdm.NextDouble() * 300; day.To = rdm.NextDouble() * 300; //指定时长 Duration duration = new Duration(TimeSpan.FromMilliseconds(3000)); dax.Duration = duration; day.Duration = duration; //动画主体是TranslatTransform变形,而非Button this.tt.BeginAnimation(TranslateTransform.XProperty,dax); this.tt.BeginAnimation(TranslateTransform.YProperty,day); } |
因为TranslateTransform的X,Y属性均为Double类型,所有我们选用DoubleAnimation来使之产生变化。代码中声明的dax、day两个DoubleAnimation变量并分别为之创建引用实例。接下来的代码依次为它们设置了起始值、终止值、变化时间。最后,调用BeginAnimation方法,让dax作用在TranslateTransform的XProperty属性上,让day作用在TranslateTransform的YProperty属性上。运行程序,每次单击按钮,按钮都会从起始位置(窗体的左上角)向窗体的右下角长宽不超过300像素的矩形内的某点运动,完成运动的时长为300毫秒。运行效果如下图:
这段代码有以下几处值得注意的地方:
因为指定了daX和daY的起始值为0,所以每次按钮都会“跳”回窗体的左上角开始动画。如果想让按钮从当前位置开始下一次动画,只需要把“dax.From=0;”和"day.From=0"去掉即可。
尽管表现出来的是button在移动,但DoubleAnimation的作用目标并不是Button而是TranslateTransform实例,因为TranslateTransform实例是Button的RenderTransform属性,所以Button“看上去”是移动了。
前面说过,能用来制作动画效果的属性必须是依赖属性,TranslateTransform的XProperty和YProperty就是两个依赖属性值。
UIElement和Animation两个类都定义了BeginAnimation这个方法。TranslateTransform派生自Animation类,所以具有这个方法。这个方法的调用者就是动画要作用的目标对象,两个参数分别指明被作用的依赖属性(TranslateTransform.XProperty和TranslateTransform.YProperty)和设计好的动画(dax和day)。可以猜想,如果要动画改变Button的宽度和高度(这两个属性也是double类型),也应该首先创建DoubleAnimation实例,然后设置起至值和动画时间,最后调用Button的BeginAnimation方法,使用动画对象影响Button的WidthProperty和HeightProperty。
如果把事件处理器中的代码改成这样:
private void Button_Click(object sender, RoutedEventArgs e) { DoubleAnimation dax = new DoubleAnimation(); DoubleAnimation day = new DoubleAnimation(); //指定起点 //dax.From = 0; //day.From = 0; //指定终点 //Random rdm = new Random(); //dax.To = rdm.NextDouble() * 300; //day.To = rdm.NextDouble() * 300; dax.By=100D; day.By = 100D; //指定时长 Duration duration = new Duration(TimeSpan.FromMilliseconds(300)); dax.Duration = duration; day.Duration = duration; //动画主体是TranslatTransform变形,而非Button this.tt.BeginAnimation(TranslateTransform.XProperty,dax); this.tt.BeginAnimation(TranslateTransform.YProperty,day); } |
运行的效果如下:
2 高级动画控制
使用From、To、By、Duration几个属性进行组合已经可以制作很多不同效果的动画了,然而WPF的动画系统的控制属性远远不止这些。如果想制作出更加复杂或逼真的动画,还需要使用如下一些效果:
对于这些属性,大家可以自己动手尝试---对它们进行组合往往可以产生很多意想不到的效果。
在这些属性中,EasingFunction是一个扩展性非常强的属性。它的取值类型是一个IEasingFunction接口类型,而WPF自带的IEasingFunction派生类就有10多种,每个派生类都能产生不同的结束效果。比如BounceEase可以产生乒乓球弹跳式效果,我们可以直接拿来使用而不必花精力亲自创作。
如果把前面的例子改成这样:
private void Button_Click(object sender, RoutedEventArgs e) { DoubleAnimation dax = new DoubleAnimation(); DoubleAnimation day = new DoubleAnimation(); //设置反弹 BounceEase be = new BounceEase(); //设置反弹次数为3 be.Bounces = 3; be.Bounciness = 3;//弹性程度,值越大反弹越低 day.EasingFunction = be; //设置终点 dax.To = 300; day.To = 300; //指定时长 Duration duration = new Duration(TimeSpan.FromMilliseconds(2000)); dax.Duration = duration; day.Duration = duration; //动画主体是TranslatTransform变形,而非Button this.tt.BeginAnimation(TranslateTransform.XProperty,dax); this.tt.BeginAnimation(TranslateTransform.YProperty,day); } |
运行效果如下图:
3 关键帧动画
动画是UI元素属性连续发送变化产生的视觉效果。属性每次细微的变化都会产生一个新的画面,每个新画面就称为一帧,帧的连续播放就产生了动画效果。如同电影一样,单位时间内播放的帧数越多,动画的效果就会越细致。前面讲到的简单动画只设置了起点和终点,之间的动画帧都是由程序计算出来并绘制的,程序员无法进行控制。关键帧动画则允许程序员为一段动画设置几个“里程碑”,动画执行到里程碑所在的时间点时,被动画控制的属性值也必须达到设定的值,这些时间线上的“里程碑”就是关键帧。
思考这样一个需求:我想让一个Button用900毫秒的时间从左上角移动到右下角,但移动的路线不是直接走动而是走Z字形。如下图所示:
如果我们不知道有关键帧动画可用而只使用简单的动画,那么我们需要创建若干个简单的动画分别控制TranslateTransform的X和Y,比较棘手的是需要控制这些动画之间的协同。协同策略有两种,一种是靠时间来协同,也就是设置后执行动画的BeginTime以等待前面动画执行完毕,另一种是靠事件协同,也就是为先执行的动画添加Complated事件处理器,在事件处理器中开始下一段动画。因为是多个动画的协同,所以在动画需要改变的时候,代码的改动会比较大。
使用关键帧动画情况就会大有改观----我们只需要创建两个DoubleAnimationUsingKeyFrames实例,一个控制TranslateTransForm的X属性,另一个控制TranslateTransForm的Y属性即可。每个DoubleAnimationUsingKyeFrames各拥有3个关键帧用于指明X或Y在三个时间点(两个拐点和一个终点)应该达到什么样的值。
程序的XAML代码如下:
<Window x:Class="WpfApplication1.Window61" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window61" Height="372" Width="475"> <Grid> <Button Width="80" Height="80" HorizontalAlignment="Left" VerticalAlignment="Top" Click="Button_Click"> <Button.RenderTransform> <TranslateTransform X="0" Y="0" x:Name="tt"></TranslateTransform> </Button.RenderTransform> </Button> </Grid> </Window> |
Button的Click事件处理器代码如下:
private void Button_Click(object sender, RoutedEventArgs e) { DoubleAnimationUsingKeyFrames dakX = new DoubleAnimationUsingKeyFrames(); DoubleAnimationUsingKeyFrames dakY = new DoubleAnimationUsingKeyFrames(); //设置动画总时长 dakX.Duration = new Duration(TimeSpan.FromMilliseconds(900)); dakY.Duration = new Duration(TimeSpan.FromMilliseconds(900)); //创建,添加关键帧 LinearDoubleKeyFrame x_kf_1 = new LinearDoubleKeyFrame(); LinearDoubleKeyFrame x_kf_2 = new LinearDoubleKeyFrame(); LinearDoubleKeyFrame x_kf_3 = new LinearDoubleKeyFrame(); x_kf_1.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(300)); x_kf_1.Value = 200; x_kf_2.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(600)); x_kf_2.Value = 0; x_kf_3.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(900)); x_kf_3.Value = 200; dakX.KeyFrames.Add(x_kf_1); dakX.KeyFrames.Add(x_kf_2); dakX.KeyFrames.Add(x_kf_3); LinearDoubleKeyFrame y_kf_1 = new LinearDoubleKeyFrame(); LinearDoubleKeyFrame y_kf_2 = new LinearDoubleKeyFrame(); LinearDoubleKeyFrame y_kf_3 = new LinearDoubleKeyFrame(); y_kf_1.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(300)); y_kf_1.Value = 0; y_kf_2.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(600)); y_kf_2.Value = 180; y_kf_3.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(900)); y_kf_3.Value = 180; dakY.KeyFrames.Add(y_kf_1); dakY.KeyFrames.Add(y_kf_2); dakY.KeyFrames.Add(y_kf_3); //执行动画 tt.BeginAnimation(TranslateTransform.XProperty, dakX); tt.BeginAnimation(TranslateTransform.YProperty,dakY); } |
在这组关键帧动画中,我们使用了最简单的关键帧LinearDoubleKeyFrame,这种关键帧的特点就是只需要你给定时间点(KeyTime属性)和到达时间点时的目标属性值(Value属性)动画就会让目标属性值在两个关键帧之间匀速运动。比如这两句代码:
x_kf_1.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(300)); x_kf_1.Value = 200; x_kf_2.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromMilliseconds(600)); x_kf_2.Value = 0; |
x_kf_1关键帧处在时间线300毫秒处,目标属性值在这一时刻必须达到200(是什么属性这个时候并不知道,但要求这个属性值在这个时间一定要达到200),类似,x_kf_2在时间线的位置是600毫秒处,目标属性的值为0。当动画开始执行之后,程序会自动计算出目标属性在这两个关键帧之间的匀速变化。
前面的代码中,为关键帧的KeyTime属性使用的是KeyTime.FromTimeSpan静态方法,这样可以得到一个绝对的时间点。使用KeyTime.FromPercent静态方法则可以获得百分比计算的相对时间点,程序将整个关键帧的动画时长(Duration)视为100%。我们就可以把前面的代码改成这样:
x_kf_1.KeyTime = KeyTime.FromPercent(0.33); x_kf_1.Value = 200; x_kf_2.KeyTime = KeyTime.FromPercent(0.66); x_kf_2.Value = 0; x_kf_3.KeyTime = KeyTime.FromPercent(1); x_kf_3.Value = 200; dakX.KeyFrames.Add(x_kf_1); dakX.KeyFrames.Add(x_kf_2); dakX.KeyFrames.Add(x_kf_3); |
之后我们无论将dakX的Duration改为多少,3个关键帧都会将这个时间划分为均等的3段。
4 特殊关键帧
DoubleAnimationUsingKeyFrames的KeyFrames属性的数据类型是DoubleKeyFrameCollection,此集合类可接收的元素类型是DoubleKeyFrame。DoubleKeyFrame是一个抽象类,前面使用的LinearDoubleKeyFrame就是它的派生类之一。DoubleKeyFrame的所有派生类如下:
LinearDoubleKeyFrame:线性变化关键帧,目标属性值的变化是线性的、均匀的,即变化速率不变。
DisCreteDoubleKeyFrame:不连续变化关键帧,目标属性值变化是跳跃性的,跃迁的。
SplineDoubleKeyFrame:样条函数式变化帧,目标属性的变化值是一条贝塞尔曲线。
EasingDoubleKeyFrame:缓冲是变化关键帧,目标属性值以某种缓冲形式变化。
4个派生类中最常用的就是SplineDoubleKeyFrame(SplineDoubleKeyFrame可以替换LinearDoubleKeyFrame)。使用SplineDoubleKeyFrame可以方便的制作非匀速动画,因为它使用一条赛贝尔曲线来控制目标属性的变化速率。这条用于控制目标属性变化速率的贝塞尔曲线的起点是(0,0)和(1,1),分别映射着目标属性的变化起点和变化终点,意思是目标属性由0%变化到100%。这条贝塞尔曲线有两个控制点----ControlPoint1和ControlPoint2,意思是贝塞尔曲线从起点出发先想ControlPoint1移动、再向ControlPoint2移动,最后到达终点,形成一条平滑的曲线。如果设置ControlPoint1和ControlPoint2的横坐标值相等,比如(0,0)、(0.5,0.5)、(1,1)则贝塞尔曲线是一条直线,这时候SplineDoubleKeyFrame和LinearDoubleKeyFrame是等价的。当控制点的横纵坐标不相等时,贝塞尔曲线就会出现很多变化。如下图所示,这些是贝塞尔曲线控制点处的典型位置是出现的速率曲线,X1,Y1是ControlPoint0的坐标,X2,Y2是ControlPoint2的坐标。
下面是一个SplineDoubleKeyFrame的一个实例。程序的XAML代码如下:
<Window x:Class="WpfApplication1.Window62" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window62" Height="303" Width="544"> <Grid Margin="10" Background="AliceBlue"> <Button Width="80" Height="80" Content="Move"
HorizontalAlignment="Left" VerticalAlignment="Top" Click="Button_Click"> <Button.RenderTransform> <TranslateTransform x:Name="tt" X="0" Y="0"></TranslateTransform> </Button.RenderTransform> </Button> </Grid> </Window> |
Button的Click事件处理器代码如下:
private void Button_Click(object sender, RoutedEventArgs e) { //创建动画 DoubleAnimationUsingKeyFrames dakX = new DoubleAnimationUsingKeyFrames(); dakX.Duration = new Duration(TimeSpan.FromMilliseconds(1000)); //创建、添加关键帧 SplineDoubleKeyFrame kf = new SplineDoubleKeyFrame(); kf.KeyTime = KeyTime.FromPercent(1); kf.Value = 400; KeySpline ks = new KeySpline(); ks.ControlPoint1 = new Point(0,1); ks.ControlPoint2 = new Point(1,0); kf.KeySpline = ks; dakX.KeyFrames.Add(kf); //执行动画 this.tt.BeginAnimation(TranslateTransform.XProperty ,dakX); } |
关键帧动画会控制Button的位置变形、让Button横向运动。整个动画只有一个关键帧,这个关键帧使用的是SplineDoubleKeyFrame,变化速率控制曲线的两个控制点分别是(0,1)和(1,0)。与上图中的最后一幅图一致,因此目标属性会以快--慢---快的形式变化。程序的执行效果如下图所示:
5. 路径动画
如何让目标对象沿着一条给定的路径移动呢?答案是使用DoubleAnimationUsingPath类。DoubleAnimationUsingPath需要一个PathGeometry来指明移动路径,PathGeometry的数据信息可以用XAML中的Path语法书写。PathGeometry的另外一个重要属性是Source,Source属性的数据类型是PathAnimationSource枚举,枚举值可取X、Y或Angle。如果路径动画Source属性的取值是PathAnimationSource.X,意味着这个动画关注的是曲线上每一点横坐标的变化。如果路径动画Source属性的取值是PathAnimationSource.Y,意味着这个动画关注的是曲线上每一点纵坐标的变化;如果路径动画的Source属性取值是PathAnimationSource.Angle,意味着这个动画关注的是曲线上每一点切线方向的变化。
下面这个例子讲的是让一个Button沿着一条贝塞尔曲线做波浪运动。程序的XAML代码如下:
<Window x:Class="WpfApplication1.Window63" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window63" Height="314" Width="517"> <Grid x:Name="layoutRoot"> <Grid.Resources> <!--移动路径--> <PathGeometry x:Key="movePath" Figures="M 0,50 C 300,-100 300,400 600,120"></PathGeometry> </Grid.Resources> <Button Content="Move" Width="80" Height="80"
HorizontalAlignment="Left" VerticalAlignment="Top" Click="Button_Click"> <Button.RenderTransform> <TranslateTransform X="0" Y="0" x:Name="tt"></TranslateTransform> </Button.RenderTransform> </Button> </Grid> </Window> |
Button的Click事件处理代码如下:
private void Button_Click(object sender, RoutedEventArgs e) { //从XAML代码中获取移动路径数据 PathGeometry pg = this.layoutRoot.FindResource("movePath") as PathGeometry; Duration duration = new Duration(TimeSpan.FromMilliseconds(600)); //创建动画 DoubleAnimationUsingPath dpX = new DoubleAnimationUsingPath(); dpX.Duration = duration; dpX.PathGeometry = pg; dpX.Source = PathAnimationSource.X; DoubleAnimationUsingPath dpY = new DoubleAnimationUsingPath(); dpY.Duration = duration; dpY.PathGeometry = pg; dpY.Source = PathAnimationSource.Y; //执行动画 this.tt.BeginAnimation(TranslateTransform.XProperty,dpX); this.tt.BeginAnimation(TranslateTransform.YProperty,dpY); } |
感兴趣的话,黑可以为动画添加自动返回和循环控制代码:
dpX.AutoReverse = true; dpX.RepeatBehavior = RepeatBehavior.Forever; dpY.AutoReverse = true; dpY.RepeatBehavior = RepeatBehavior.Forever; |
程序运行的效果如下图:
1.4.2 场景
场景,StroyBoard就是并行执行一组动画(前面讲述的关键帧动画则是串行的执行一组动画)。
如果你是一位导演,当你对照剧本构思一个场景的时候脑子里一定想的是应该有多少个演员参加到了这个场景、它们都是什么演员、主角/配角/群众演员分别什么时候到场、每个演员该说什么?做什么?...
...演员具体用谁?由场景的需要来定。到时候开机的时候,一声令下,所有演员都会按照预先分配好的脚本进行表演,一个影视片段就算录成了。
设计WPF的场景时情况也差不多,先是把一组独立的动画组织在一个StoryBoard元素中、安排好它们的协作关系,然后指定哪个动画由哪个UI元素,哪个属性负责完成。StoryBoard设计好后,你可以为它选择一个恰当的触发时机,比如按钮按下时或者下载开始时。一旦触发条件被满足,动画场景就会开始执行,用户就会看到执行效果。
下面是一个SotryBoard例子。程序的XAML代码如下:
<Window x:Class="WpfApplication1.Window64" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window64" Height="159" Width="461"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="38" /> <RowDefinition Height="38" /> <RowDefinition Height="38" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="60" /> </Grid.ColumnDefinitions> <!--跑道(红)--> <Border Grid.Row="0" BorderBrush="Gray" BorderThickness="1"> <Ellipse Width="36" Height="36" Fill="Red" HorizontalAlignment="Left" x:Name="ballR"> <Ellipse.RenderTransform> <TranslateTransform X="0" Y="0" x:Name="ttR"> </TranslateTransform> </Ellipse.RenderTransform> </Ellipse> </Border> <!--跑道(绿)--> <Border Grid.Row="1" BorderBrush="Gray" BorderThickness="1,0,1,1"> <Ellipse Width="36" Height="36" Fill="Green" HorizontalAlignment="Left" x:Name="ballG"> <Ellipse.RenderTransform> <TranslateTransform X="0" Y="0" x:Name="ttG"> </TranslateTransform> </Ellipse.RenderTransform> </Ellipse> </Border> <!--跑道(蓝)--> <Border Grid.Row="2" BorderBrush="Gray" BorderThickness="1,0,1,1"> <Ellipse Width="36" Height="36" Fill="Blue" HorizontalAlignment="Left" x:Name="ballB"> <Ellipse.RenderTransform> <TranslateTransform X="0" Y="0" x:Name="ttB"> </TranslateTransform> </Ellipse.RenderTransform> </Ellipse> </Border> <!--按钮--> <Button Content="Go" Grid.RowSpan="3" Grid.Column="1" Click="Button_Click"></Button> </Grid> </Window> |
程序的UI效果图如下图,单击按钮后,三个小球分别在不同的时间开始向右以不同的速度移动。
Button的事件处理器代码如下:
private void Button_Click(object sender, RoutedEventArgs e) { Duration duration = new Duration(TimeSpan.FromMilliseconds(600)); //红色小球匀速运动 DoubleAnimation daRx = new DoubleAnimation(); daRx.Duration = duration; daRx.To = 400; //绿色小球做变速运动 DoubleAnimationUsingKeyFrames dakGx = new DoubleAnimationUsingKeyFrames(); dakGx.Duration = duration; SplineDoubleKeyFrame kfg = new SplineDoubleKeyFrame(400,KeyTime.FromPercent(1)); kfg.KeySpline = new KeySpline(1,0,0,1); dakGx.KeyFrames.Add(kfg); //蓝色小球变速运动 DoubleAnimationUsingKeyFrames dakBx = new DoubleAnimationUsingKeyFrames(); dakBx.Duration = duration; SplineDoubleKeyFrame kfb = new SplineDoubleKeyFrame(400,KeyTime.FromPercent(1)); kfb.KeySpline = new KeySpline(0,1,1,0); dakBx.KeyFrames.Add(kfb); //创建场景 Storyboard storyBoard = new Storyboard(); Storyboard.SetTargetName(daRx,"ttR"); Storyboard.SetTargetProperty(daRx, new PropertyPath(TranslateTransform.XProperty)); Storyboard.SetTargetName(dakGx, "ttG"); Storyboard.SetTargetProperty(dakGx, new PropertyPath(TranslateTransform.XProperty)); Storyboard.SetTargetName(dakBx, "ttB"); Storyboard.SetTargetProperty(dakBx, new PropertyPath(TranslateTransform.XProperty)); storyBoard.Duration = duration; storyBoard.Children.Add(daRx); storyBoard.Children.Add(dakBx); storyBoard.Children.Add(dakGx); storyBoard.Begin(this); storyBoard.Completed += (a, b) => { MessageBox.Show(ttR.X.ToString()); }; } |
毋庸置疑,使用C#代码实现StoryBoard非常的复杂,出了拿来研究或者遇到非要使用C#动态代码创建StoryBoard的情况,不然我们都是在XAML里面创建StoryBoard的。StoryBoard一般放在UI元素的Trigger里,Trigger在触发时会执行<BeginStoryBoard>标签中的SotryBoard实例:
<!--按钮--> <Button Content="Go" Grid.RowSpan="3" Grid.Column="1" Click="Button_Click"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard Duration="0:0:0.6"> <!--红色小球动画--> <DoubleAnimation Duration="0:0:0.6" To="400" Storyboard.TargetName="ttR" Storyboard.TargetProperty="X"> </DoubleAnimation> <!--绿色小球动画--> <DoubleAnimationUsingKeyFrames Duration="0:0:0.6" Storyboard.TargetProperty="X" Storyboard.TargetName="ttG"> <SplineDoubleKeyFrame KeyTime="0:0:0.6" Value="400" KeySpline="1,0,0,1"></SplineDoubleKeyFrame> </DoubleAnimationUsingKeyFrames> <!--蓝色小球动画--> <DoubleAnimationUsingKeyFrames Duration="0:0:0.6" Storyboard.TargetName="ttB" Storyboard.TargetProperty="X"> <SplineDoubleKeyFrame KeyTime="0:0:0.6" Value="400" KeySpline="0,1,1,0"></SplineDoubleKeyFrame> </DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> |
除了为Button添加了Trigger并去掉对Click事件的订阅之外,XAML代码的其它部分不做任何改动。可以看到,XAML代码编写动画比C#代码简洁了很多-----Blend生成的StoryBoard代码与之非常类似。
|