WPF之各种图形
本文主要讲解WPF中的基本图形知识,内容如下:
1。图形的基础知识准备
2。WPF中的图形体系结构
3。颜色和画刷
4。Shape
5。Drawing和Visual
1.1WPF中的坐标
1.1.1 WPF的默认坐标:WPF中平面坐标系主要包括原点位置、X和Y轴方向,以及坐标单位。WPF的默认坐标系原点位置在绘制区域的左上角,X轴向右增加,Y轴向下增加。
自定义坐标系:自定义坐标系主要通过Transform类来实现,一般可以使用ScaleTransform和TranslateTransform来进行坐标的反转和水平移动,如下:
<Canvas> <Canvas.RenderTransform> <TransformGroup> <ScaleTransform ScaleY="-1"></ScaleTransform> <TranslateTransform Y="200"></TranslateTransform> </TransformGroup> </Canvas.RenderTransform> <Line X1="0" Y1="0" X2="100" Y2="100" Stroke="Red" StrokeThickness="2"></Line> <Button Canvas.Top="100" Canvas.Left="120" Foreground="Green" Content="Test Button" FontSize="14"></Button> </Canvas>
上述代码为一个简单的进行自定义坐标的例子,一般来说图形的绘制会使用Canvas来布局,因为Canvas可以指定相对的Top和Left。代码中RenderTransform决定了坐标系的效果,通过ScaleTransform将Y轴的正方向从向下变为了向上(因为ScaleY是-1,则和之前的相反),TranslateTransform则将原点向下移动了200.效果如下:
可以看到直线的起始位置为下移200后的位置,现在还存在一个问题,进行翻转后按钮的文字也是倒的,所以要给按钮添加一个转换,即给按钮添加如下代码:
<Button.RenderTransform> <ScaleTransform ScaleY="-1"></ScaleTransform> </Button.RenderTransform>
1.1.2WPF中的对象变换
之前已经提到了Transform,当然Transform的类型不仅仅是上边的两种类型。WPF中的的变换对象均继承自类型Transform,类型如下:
TranslateTransform 平移变换,X,Y表示在X轴和Y轴上的平移量;
RotateTransform 旋转变换,Angle表示旋转的角度,CenterX和CenterY表示旋转的中心坐标;
ScaleTransform 缩放变换,ScaleX和ScaleY表示在X轴和Y轴上的缩放比例,当然像上述例子进行翻转也是ok的,CenterX和CenterY表示缩放的原点坐标;
SkewTransform 错切变换(也可以成为扭曲变换),AngleX和AngleY表示错切的角度,CenterX和CenterY表示错切的原点;
MatrixTransform 矩阵变换;
TransformGroup 当对一个元素执行多个变换的时候,可以通过TransformGroup来设置多个变换,实现组合变换,在这里切记,不同顺序的变换,看到的效果是完全不同的。(造成顺序很重要的一个原因就是,像旋转和缩放这样的变换是针对坐标系的原点进行的。 缩放以原点为中心的对象与缩放已离开原点的对象所得到的结果不同。 同样,旋转以原点为中心的对象与旋转已离开原点的对象所得到的结果也不同。)
这样说似乎不太明白,还使用刚才的例子,之前的例子中有两个Transform,ScaleTransform和TranslateTransform,我们的顺序也是先前者后后者,这样看到的效果是正常的,如果我们修改如下:
<TransformGroup> <TranslateTransform Y="200"></TranslateTransform> <ScaleTransform ScaleY="-1"></ScaleTransform> </TransformGroup>
这时候你会很“神奇”的发现,什么都没了,整个窗体都是空白的,原因就是坐标的问题哦,听我道来。前文的红色字体已经说了,缩放是以原点进行变换的,我们将Canvas向下移动了200,而原点为左上角(0,0)所以旋转的效果其实是在Canvas的范围之外的,所以我们并不能看到任何元素。要解决这个问题其实也很简单,那就是设置ScaleTransform的Centery即原点中心,如下代码:
<TransformGroup> <TranslateTransform Y="200"></TranslateTransform> <ScaleTransform ScaleY="-1" CenterY="200"></ScaleTransform> </TransformGroup>
这样我们看到的效果和之前看到的就是一样正确的,那是因为我们将ScaleTransform的中心原点Y轴向下移动了200,这样还是Canvas的左上角(0,0)位置。
1.2WPF的图形架构
1.2.1 WPF的二维图形体系结构首先从逻辑树(Logic Tree)和可视树(Visual Tree)。
XAML文件的结构就是一个树型结构,从一个根节点层层展开。这棵树就是“逻辑树”,树上的每一个节点都是一个完整而独立的元素。WPF中提供了遍历逻辑树和可视树的方法,即LogicTreeHelper和VisualTreeHelper。
逻辑树和可视树的区别:
(1)逻辑树的层次结构和XAML文件的组织结构类似,而可视树则与其他节点必须是继承自Visual和Visual3D;
(2)逻辑树主要是用在如下方面。
资源查找:如一个元素添加了一个画刷资源,则会按照逻辑树的节点层层向上查找,直至逻辑树根节点;
属性值继承:举个例子,比如某一个窗体设置了字体大小,则将按照逻辑树的层次结构依次影响其下面的节点。如果某一个节点没有字体大小属性,那么则会穿过该节点影响 该节点影响下面包含此属性的逻辑节点。(比如设置了Window 标签的属性FontSize,则会影响其下的逻辑节点)
(3)可视树主要用来描述用户界面的外观,可以通过自定义控件模板修改控件的可视树从而改变控件的外观。
1.2.2 WPF二维图形中重要元素
(1)Visual
可视树是WPF渲染的基础,抛开Visual3D,整个图形用户界面都是由Visual。即Visual的派生类构成了可视树,WPF从根节点开始渲染可视树。Visual主要作用是为WPF绘 制提供支持。
(2)Drawing
Visual可以想象成一个窗口,那么窗口里绘制的内容就交给了Drawing来进行描述。Drawing针对矢量,图像,文字甚至是视频派生了下面几种不同的Drawing。
一。GeometryDrawing:绘制集合图形。
二。ImageDrawing:绘制图像。
三。GlyphRunDrawing:绘制文本。
四。ViedoDrawing:播放音频和视频文件。
五。DrawingGroup:Drawing的集合。
VisualTreeHelper中的方法GetDrawing从Visual中获取Drawing集合,其输入参数是一个Visual,返回值是一个DrawingGroup。
(3)Geometry
绘制几何图形时GeometryDrawing需要需要几何数据,然后设置画刷和画笔来绘制。这个几何数据使用Geometry及其派生来来描述,Geometry只是用来描述二维图形 的数据,不负责显示,派生类中包括椭圆,矩形及直线等几何图形。
(4)Shape
是一种高级图形元素 ,派生自FrameworkElement,因此可以任意潜逃在控件或者容器中。
(5)四者之间的关系
一。WPF的可视树的每一个节点均派生自Visual;
二。Shape和Visual是继承关系。
Visual <-- UIElement <-- FrameworkElement <-- Shape;
三。Visual的绘制内容通过Drawing来描述,通过VisualTreeHelper的静态方法GetDrawing可以从一个Visual中获得一个DrawingGroup。从而可以遍历Visual中所有Drawing对象;
四。Drawing的一个派生类GeometryDrawing需要用Geometry来描述其几何数据;
五。Shape派生自Visual,因此具有Visual和Drawing之间的关系。并且Shape具有Fill属性,可以通过DrawingBrush和Drawing关联;
六。Path通过一个Data属性和Geometry关联。
1.3 颜色和画刷
1.3.1 WPF中的颜色
WPF中的颜色除了使用Colors枚举来进行访问外,还可以通过A(透明值),R,G,B(三原色)属性指定任意颜色。
Color color = new Color(); //设置为红色 color = Color.FromRgb(255, 0, 0); //设置为半透明的红色 color = Color.FromArgb(100, 255, 0, 0); //通过字符串方式为颜色赋值,字符串格式为"#aarrggbb".前两位为透明度A,后面依次为R,G,B,并且需要十六进制数值表示 color = (Color)ColorConverter.ConvertFromString("#FFFF0000");
1.3.2 画刷
WPF中有6种画刷:
(1)SolidColorBrush:单色或者是纯色画刷;
(2)LinearGradientBrush:线性渐变画刷,通过指定两个点连成一条线段,在两点之间的颜色进行线性插值,然后进行渐变填充;
(3)RadialGradientBrush:扩散渐变画刷,和LinearGradientBrush渐变不同,它是一个呈椭圆状向外散发;
(4)ImageBrush:图片画刷,将图片填充到控件区域;
(5)DrawingBrush:图形画刷,通过Drawing绘制几何图形,图像,文字甚至是视频,并且还可以形成更复杂的图形,最终将这些图形填充到控件区域;
(6)VisualBrush:使用Visual对象填充目标区域。所有的控件均派生自Visual,所以可以将所有的控件填充到区域,但是VisualBrush仅仅绘制了Visual的外观,填充的区域的 按钮是不能点击的。
LinearGradientBrush:
<Grid> <Grid.Background> <LinearGradientBrush> <GradientStop Color="Red" Offset="0"></GradientStop> <GradientStop Color="Blue" Offset="1"></GradientStop> </LinearGradientBrush> </Grid.Background> </Grid>
LinearGradientBrush画刷一般需要2个或者2个以上的Color对象和两个Point对象即Offset值,颜色的填充从第一个Point一直到最后一个Point连成一条线段渐变的填充颜色,填充的范围和Point的值有关系,填充的线段的中心点位两个Point的中心,整个第一个GradientStop颜色也是从起始Offset渐变到最后一个GradientStop颜色,中间以插值填充.
LinearGradientBrush有StartPoint起始位置和EndPoint结束为止,默认情况下StartPoint为(0,0),EndPoint为(1,1),如果起始位置位置不在左上角(0,0),终点位置也不在右下角(1,1),则有一部分超出了起始点和终点的范围。渐变画刷提供了SpreadMethod属性(适用于LinearGradientBrush和RadialGradientBrush),有3个值可选(Pad,Reflect,Repeat),三个值的区别看下图:
三幅图分别为Pad,Reflect,Repeat属性值对应的效果,Pad应该是没有什么变化,Reflect是进行了反射的处理,即左边和右边的颜色进行对称,第三个就是重复显示。
RadialGradientBrush:
RadialGradientBrush以一个起始点呈椭圆状向外散发渐变,与LinearGradientBrush共享GradientBrush的属性。RadialGradientBrush由Center,GradientOrigin,Radiusx,RadiusY属性决定渐变方式,Center表示填充椭圆的中心位置,相对坐标默认为(0.5,0.5),GradientOrigin表示渐变的源点,相对坐标的默认值是(0.5,0.5),即GradientOrigin和Center默认值是重叠在一起的;RadiusX和RadiusY椭圆的半径,其值均为0.5,表示默认该椭圆的长短半径是填充区域的长和宽的一半。
<Grid> <Grid.Background> <RadialGradientBrush SpreadMethod="Pad"> <GradientStop Color="Red" Offset="0.2"></GradientStop> <GradientStop Color="Blue" Offset="0.5"></GradientStop> <GradientStop Color="Yellow" Offset="1"></GradientStop> </RadialGradientBrush> </Grid.Background> </Grid>
Tile画刷:(ImageBrush,DrawingBrush,VisualBrush均派生自TileBrush)
TileBrush可以用重复图案来填充目标区域,图案可以是Image,Visual或者Drawing。画刷的内容分别由不同类型的画刷决定,ImageBrush通过ImageSource指定画刷内容;DrawingBrush通过Drawing属性指定;VisualBrush通过Visual属性指定。
TileBrush的属性:
Stretch属性:有4个枚举值None,Fill,Uniform和UniformToFill。
None,图片会被裁减。裁减的区域可以通过AlignmentX和AlignmentY确定;
<StackPanel Orientation="Horizontal"> <Rectangle Width="200" Height="200"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="None"></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="200" Height="200"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="None" AlignmentX="Center" AlignmentY="Top"></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="200" Height="200"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="None" AlignmentX="Center" AlignmentY="Bottom"></ImageBrush> </Rectangle.Fill> </Rectangle> </StackPanel>
Uniform,将图片按照比例进行缩放,图片的长度和段度取决于区域的最小值(即Height和Width中小的一个)。
<StackPanel Orientation="Horizontal"> <Rectangle Width="50" Height="300"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="Uniform"></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="100" Height="300"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="Uniform" ></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="200" Height="300"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="Uniform" ></ImageBrush> </Rectangle.Fill> </Rectangle> </StackPanel>
可以看到图片的高度取决于Rectgangle的Height和Width中小的那个值。(当值取决于Height则AlignmentY才有效,取决于Width则仅AlignmentX才有效)
UniformToFill,先考虑完全填充区域,然后考虑比例的缩放。(通过对图片的裁减,将图片完全填充到区域,超出的部分则被裁减,说到底就是根据Height和Width的大的 那个值决定图片的值。)
<StackPanel Orientation="Horizontal" Margin="10"> <Rectangle Width="50" Height="300"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="UniformToFill"></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="100" Height="300"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="UniformToFill" ></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="200" Height="300"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="UniformToFill" ></ImageBrush> </Rectangle.Fill> </Rectangle> </StackPanel> </StackPanel>
可以看到,图片超出的区域都被裁减了。
ViewBox属性:指定显示到区域的画刷内容,它是一个Rect类型,可以使用相对坐标和绝对坐标,由ViweboxUnits属性指定。相对坐标依然是图片的左上角(0,0),右下角为(1,1).默认值为(0,0,1,1)即显示全部内容。如果ViewBox为(0,0,0.5,0.5)则表示显示左上角1/4部分到区域。
Viewport属性:它是一个Rect类型,指定Brush映射到目标区域的哪个部分,可以使用相对坐标和绝对坐标,由ViweboxUnits属性指定。如果Viewport的值为(0,0,0.5,0.5)且TileMode属性为None,那么目标区域只有左上角.
TileMode:描述Brush如何填充到目标区域,枚举值如下。
None:不重复填充。
Tile:重复填充。
FlipX:在水平轴上对Brush隔列翻转填充。
FlipY:在垂直轴上对Brush隔行翻转填充。
FlipXY:在两个方向对Brush隔行隔列翻转填充。
<StackPanel Orientation="Horizontal" Margin="10"> <Rectangle Width="200" Height="200" Stroke="Black" StrokeThickness="1"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="UniformToFill" Viewport="0,0,0.5,0.5" TileMode="None"></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="200" Height="200" Stroke="Black" StrokeThickness="1" Margin="5"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="UniformToFill" Viewport="0,0,0.5,0.5" TileMode="Tile"></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="200" Height="200" Stroke="Black" StrokeThickness="1" Margin="5"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="UniformToFill" Viewport="0,0,0.5,0.5" TileMode="FlipX"></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="200" Height="200" Stroke="Black" StrokeThickness="1" Margin="5"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="UniformToFill" Viewport="0,0,0.5,0.5" TileMode="FlipY"></ImageBrush> </Rectangle.Fill> </Rectangle> <Rectangle Width="200" Height="200" Stroke="Black" StrokeThickness="1" Margin="5"> <Rectangle.Fill> <ImageBrush ImageSource="1.jpg" Stretch="UniformToFill" Viewport="0,0,0.5,0.5" TileMode="FlipXY"></ImageBrush> </Rectangle.Fill> </Rectangle> </StackPanel>
5幅图依次为None,Tile,FlipX,FlipY,FlipXY.
使用Brush制作一个特效:
<Grid > <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Image x:Name="imgVisual" Grid.Row="0" Source="1.jpg" Height="300" Width="200" Stretch="Fill"></Image> <Rectangle Grid.Row="1" Width="{Binding ActualWidth,ElementName=imgVisual}" Height="{Binding ActualHeight,ElementName=imgVisual}"> <Rectangle.Fill> <VisualBrush Visual="{Binding ElementName=imgVisual}"> <VisualBrush.RelativeTransform> <ScaleTransform ScaleX="1" ScaleY="-1" CenterX="0.5" CenterY="0.5"></ScaleTransform> </VisualBrush.RelativeTransform> </VisualBrush> </Rectangle.Fill> <Rectangle.OpacityMask> <LinearGradientBrush StartPoint="0,0" EndPoint="0,0.5"> <GradientStop Offset="0" Color="Red"></GradientStop> <GradientStop Offset="0.5" Color="Blue"></GradientStop> <GradientStop Offset="1" Color="Transparent"></GradientStop> </LinearGradientBrush> </Rectangle.OpacityMask> </Rectangle> </Grid>
实现了倒影的效果。
1.4 Shape
1.4.1 Rectangle(矩形)
<Rectangle Height="200" Width="200" Fill="Yellow"></Rectangle>
Rectangle通过指定Height和Width来设置高和宽,除此之外,Rectangle还可以实现圆角矩形,如下:
<Rectangle Width="200" Height="200" RadiusX="50" RadiusY="50" Fill="Yellow"></Rectangle>
通过指定RadiusX指定使矩形的角变圆的椭圆的 x 轴半径,指定RadiusY使矩形的角变圆的椭圆的 y 轴半径.
1.4.2 Ellipse(椭圆)
<Ellipse Width="200" Height="200" Fill="Yellow"></Ellipse>
除了和Rectangle拥有Height和Width属性之外,它们两个还有Stroke(设置边框颜色)和StrokeThickness(设置边框宽度).
1.4.3 Line(直线)
<Line X1="10" X2="100" Y1="10" Y2="100" Stroke="Black" StrokeThickness="2"></Line>
Line有四个属性X1,X2,Y1,Y2标识两个点的坐标,除此之外同样有Stroke何StrokeThickness。
1.4.4 Polyline和Polygon(线段)
Polyline:
<Polyline Points="10,10 60,10 110,100" Stroke="Black" StrokeThickness="2" Fill="Red"></Polyline>
Polygon:
<Polygon Points="10,10 60,10 110,100" Stroke="Black" StrokeThickness="2" Fill="Red"></Polygon>
Polyline和Polygon使用Points(Point类型)属性来表示一组线段,两个的区别是后者会自动添加一条从第一个点到最后一个点的连线。
1.4.5 线型,线帽,线的连接和填充规则
1.4.5.1 线型
线型由StrokeDashArray和StrokeDashOffset两个属性控制。StrokeDashArray类型为Double类型的集合,设置一串double数值描述线型。如果设置为"4,3"则表示填充4个单元(线的宽度,即StrokeThickness属性控制),然后空3个单元,并循环下去;如果设置为"4,1,3"则表示填充4个单檐,空1个单元,然后再填充3个单元,依次循环。StrokeDashOffset描述线型的开始位置,如果StrokeDashArray设置为(4,3),并且设置StrokeDashOffset为2,则表示线段的第一段只填充2个单元(4-2),空3个单元,然后再填充4个单元,继续循环。
<Line X1="10" X2="300" Y1="30" Y2="30" Stroke="Black" StrokeThickness="2" StrokeDashArray="2,3" ></Line> <Line X1="10" X2="300" Y1="40" Y2="40" Stroke="Black" StrokeThickness="3" StrokeDashArray="2,3" ></Line> <Line X1="10" X2="300" Y1="50" Y2="50" Stroke="Black" StrokeThickness="3" StrokeDashArray="2,3,1" ></Line>
设置StrokeDashOffset:
<Line X1="10" X2="300" Y1="30" Y2="30" Stroke="Black" StrokeThickness="2" StrokeDashArray="2,3" StrokeDashOffset="2"></Line> <Line X1="10" X2="300" Y1="40" Y2="40" Stroke="Black" StrokeThickness="3" StrokeDashArray="2,3" StrokeDashOffset="1"></Line> <Line X1="10" X2="300" Y1="50" Y2="50" Stroke="Black" StrokeThickness="3" StrokeDashArray="2,3,1" StrokeDashOffset="10" ></Line>
1.4.5.2 线帽
线的两端可使用StrokeStartLineCap和StrokeEndLineCap设置,WPF中有4种线帽可选择Flat(平线帽,默认),Square(方帽),Round(圆帽),Triangle(尖帽);Square和Flat的区别在于,Square会将线段的两头延伸线宽的一半作为方帽。StrokeDashCap控制线段内部每一小段的两端线帽,对线段的两端无效。
<Line X1="30" X2="300" Y1="30" Y2="30" Stroke="Black" StrokeThickness="10" StrokeStartLineCap="Round" StrokeEndLineCap="Triangle" ></Line> <Line X1="30" X2="300" Y1="30" Y2="30" Stroke="Black" StrokeThickness="10" StrokeEndLineCap="Triangle" ></Line> <Line X1="30" X2="300" Y1="30" Y2="30" Stroke="Black" StrokeThickness="10" StrokeStartLineCap="Square" StrokeEndLineCap="Triangle" ></Line>
1.4.5.3 线的连接
线的连接由StrokeLineJoin属性控制,WPF中有3种方式,Miter(尖角,默认),Bevel(平角),Round(圆角)
<Polyline Points="10,40 30,10 50,60 70,10 " Stroke="Black" StrokeThickness="10" StrokeLineJoin="Miter"></Polyline> <Polyline Points="10,40 30,10 50,60 70,10 " Stroke="Black" StrokeThickness="10" StrokeLineJoin="Bevel"></Polyline> <Polyline Points="10,40 30,10 50,60 70,10 " Stroke="Black" StrokeThickness="10" StrokeLineJoin="Round"></Polyline>
除此之外还有一个属性StrokeMiterLimit,用来控制尖角的长度防止角度过小导致的尖角很长,该值大于或等于1,描述的是尖角长度和线宽一半的最大比,即尖角长度最长不能超过 0.5 x StrokeThickness x StrokeMiterLimit。StrokeMiterLimit默认值为10.
1.4.5.4 填充规则
通过设置FillRule属性实现,只有Polyline和Polygon两种Shape才有该属性,该属性有两个值,EvenOdd和NonZero。
EvenOdd 枚举值确定形状上的某一点是否在“内部”。 它从该点沿任意方向画一条无限长的射线,然后计算该射线在指定形状中因交叉而形成的路径段数。 如果该数字为奇数,则该点在内部;如果该数字为偶数,则该点在外部。
Nonzero 枚举值确定形状上的某一点是否在“内部”。 它从该点沿任意方向画一条无限长的射线,然后检查形状段与该射线的交点 计数从零开始,每当段从左向右跨过射线时增加 1,而每当路径段从右向左跨过射线时减去 1。 计算交点的数目后,如果结果为零,则说明该点在路径外部。 否则,它位于路径内部。
注:详细解释可参考MSDN(帮助)
1.4.6 调整Shape大小(Strecth的设置)
Strecth仍然是四个None,Fill,Uniform,UniformToFill,具体解释参考前文中提到的。例子:
<StackPanel Orientation="Horizontal"> <Grid Height="300" Width=" 150"> <Ellipse Stroke="Black" StrokeThickness="2" Stretch="Fill" Fill="Green"></Ellipse> </Grid> <Grid Height="300" Width=" 150"> <Ellipse Stroke="Black" StrokeThickness="2" Stretch="Uniform" Fill="Yellow"></Ellipse> </Grid> <Grid Height="300" Width=" 150"> <Ellipse Stroke="Black" StrokeThickness="2" Stretch="UniformToFill" Fill="Gray"></Ellipse> </Grid> </StackPanel>
1.5 Drawing和Visual
1.5.1 Drawing包括所有需要绘制的信息,如几何形状,画笔和画刷。不仅能够绘制几何形状,还能够绘制图像 、文本,甚至视频。
Drawing派生类:
GeometryDrawing 绘制几何图形 Geometry:描述几何图形
Brush:描述如何填充
Pen:描述如何填充外部轮廓线
ImageDrawing 绘制图像 ImageSource:指定图像文件的路径
Rect:指定图像文件的所在位置
VideoDrawing 播放视频 Player:媒体播放器
Rect:指定媒体播放器的所在位置
GlyphRunDrawing 绘制文本 GlyphRun:描述绘制文本的信息
ForegroundBrush:指定前景色
DrawingGroup Drawing的集合,类似GeometryGrop Children:指定该集合下的子对象
注:Drawing并非一个元素,所以不能直接放在界面中,使用方法如下:
(1)将Drawing放在DrawingImage中,一般作为Image的属性。
<Image Height="300" Width="300"> <Image.Source> <DrawingImage> <DrawingImage.Drawing> <DrawingGroup> <GeometryDrawing Brush="Green"> <GeometryDrawing.Geometry> <EllipseGeometry RadiusX="50" RadiusY="50"> </EllipseGeometry> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingGroup> </DrawingImage.Drawing> </DrawingImage> </Image.Source> </Image>
(2)将Drawing放在DrawingBrush类中,DrawingBrush派生自Brush。
<Button Height="30" Width="200"> <Button.Background> <DrawingBrush> <DrawingBrush.Drawing> <DrawingGroup> <GeometryDrawing Brush="Red"> <GeometryDrawing.Geometry> <EllipseGeometry RadiusX="10" RadiusY="10"></EllipseGeometry> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingGroup> </DrawingBrush.Drawing> </DrawingBrush> </Button.Background> </Button>
(3)将Drawing放在DrawingVisual中。
(4)使用Drawing将图形、视频统一起来。
<Rectangle> <Rectangle.Fill> <DrawingBrush> <DrawingBrush.Drawing> <DrawingGroup> <VideoDrawing></VideoDrawing> <ImageDrawing></ImageDrawing> </DrawingGroup> </DrawingBrush.Drawing> </DrawingBrush> </Rectangle.Fill> </Rectangle>
1.5.2 Visual
WPF中引入了DrawingVisual类,该类只保留了渲染所必需的特性,汝透明、剪切。同时由于DrawingVisual派生自Visual,因此保留了Hit-test行为。
1.5.2.1 DrawingVisual和DrawingContext
DrawingVisual不能直接在XAML中设置Drawing属性,所以需要DrawingContext绘制Drawing。
FormattedText text = new FormattedText("Listenfly",new System.Globalization.CultureInfo("en-us"),FlowDirection.LeftToRight, new Typeface(this.FontFamily,FontStyles.Normal,FontWeights.Normal,new FontStretch()),this.FontSize,this.Foreground); DrawingVisual drawingVisual = new DrawingVisual(); DrawingContext drawingContext = drawingVisual.RenderOpen(); drawingContext.DrawText(text,new Point(2,2)); drawingContext.Close(); RenderTargetBitmap bmp = new RenderTargetBitmap(200, 20, 120, 100, PixelFormats.Pbgra32); bmp.Render(drawingVisual); img.Source = bmp;
通过DrawingContext的DrawText写入文本,DrawingContext通过DrawingVisual的RenderOpen方法得到,最后还要调用Close方法关闭。
使用Visual绘制一个兔子(摘自 WPF 葵花宝典):
<Application.Resources> <SolidColorBrush x:Key="headcolor" Color="#FFC9CACA"/> <!--第二个兔头--> <PathGeometry x:Key="geometryhead2" Figures="M136.75,142.11L130.298,147.411L130.798,159.911L133.798,173.411L146.298,199.911L150.798,206.911L157.798,207.411L193.298,205.411L199.298,202.911L210.798,198.911L212.798,191.411L217.298,167.911L215.798,144.911L212.798,134.411L203.298,130.911L184.298,129.411L168.798,130.911L156.298,133.411L136.75,142.11z"/> <PathGeometry x:Key="geometryear2" Figures="M140.5,140.11L127.5,117.61L126.5,112.11L132.5,105.11L136,102.61L142.5,101.61L145.5,105.11L156.5,132.61L140.5,140.11 M184.5,128.61L184.5,101.11L186.5,99.11L193.5,96.61L202,98.61L206,101.11L204,114.11L203.5,130.11L184.5,128.61z"/> <PathGeometry x:Key="geometryeyeorbit2" Figures="M142.5,171.11L142.25,172.61L148,177.86L157,180.61C157,180.61,162,181.61,162,180.86C162,180.11,169.5,175.61,169.5,175.61L174,169.86L171.25,157.61L142.5,171.11 M174,168.86L181.5,173.61L184,174.11L196.75,174.86L199.5,173.11L204.25,168.11L206.25,161.11L207.25,157.11L181,157.508L174.25,157.61L171.25,156.61L174,168.86z"/> <PathGeometry x:Key="geometryeyelid2" Figures="M142.25,150.36L139.75,154.36L138.25,164.11L140.25,170.36L142.5,171.11L150.25,168.61L158.75,164.61L165,160.86L168.5,159.61L171.25,157.61L171,149.61L166,145.36L161,143.86L153.5,143.36L145.75,147.11L142.25,150.36 M208.75,157.36L174.25,158.61L171.25,157.61L171.75,149.36L174.25,144.36L178.75,141.11L182.75,138.86L195,138.86L198.5,140.61L203.5,145.36L206.5,149.86L208.75,157.36z"/> <PathGeometry x:Key="geometryeyeball2" Figures="M167.75,159.61L171,158.86L170.25,161.61L169.25,162.86L165.75,162.61L164.75,161.86L167.75,159.61 M180.5,160.11L179.5,161.61L177.5,162.11L175.5,161.36L174.25,158.61L180.25,158.36L180.5,160.11z"/> <PathGeometry x:Key="geometrymouth2" Figures="M160,198.36L159,200.36L159,202.36L194.75,200.86L199.25,198.86L201.25,196.36L198.25,192.61L193.739,192.793L179.75,193.36L169.25,195.11L163.25,196.36L160,198.36z"/> <PathGeometry x:Key="geometrylongue2" Figures="M193.25,195.11L193.25,203.11L193,210.86L190.5,214.86L187,220.11L183.25,221.86L179,223.11L172,223.11L165.75,221.11L160.75,215.86L158.25,210.86L159,202.36L160.5,200.36L173,196.61L193.25,195.11z"/> <!--第一个兔头--> <PathGeometry x:Key="geometryhead1" Figures="M214.667,131.11L213.667,148.11L214.333,159.444L217,167.777L220,179.11L225,187.777L238,192.11L267,191.777L271.667,190.444L279.333,189.444L285,178.777L288.333,166.444L291.333,154.444L290.667,132.11L287,129.11L282,126.444L266,121.444L239,121.777L223,126.11L214.667,131.11z"/> <PathGeometry x:Key="geometryear1" Figures="M223,126.777L221,117.11L219.333,110.444L219.333,102.444L222.667,98.11L227.667,96.11L235.333,95.444L237.333,98.11L238.667,105.444L237.667,110.444L238.667,118.11L238,122.777L223,126.77 M274.667,100.444L270,108.444L267.333,115.444L267,122.11L272.667,123.777L281.333,126.444L283.333,120.777L287,118.444L289.667,113.777L291,107.444L284,101.11L274.667,100.444z"/> <PathGeometry x:Key="geometryeyeorbit1" Figures="M230,132.11L226.333,134.777L222.667,143.444L223,150.11L227.333,156.11L232.667,159.777L242,160.777L248.667,157.11L252.667,153.11L252.667,136.11L245.333,130.11L234.333,129.777L230,132.11 M277.667,133.444L272.667,130.777L267,130.11L263,129.777L258,131.444L255,134.444L252.667,136.11L252.667,153.777L255.333,156.777L261.333,159.777L268.667,161.11L275.667,158.444L279.667,154.444L282,150.444L282.333,143.11L279.667,136.11L277.667,133.444z"/> <PathGeometry x:Key="geometryeyelid1" /> <GeometryGroup x:Key="geometryeyeball1"> <EllipseGeometry Center="268,144.444" RadiusX="2.334" RadiusY="3"/> <EllipseGeometry Center="237.667,144.444" RadiusX="2.334" RadiusY="3"/> </GeometryGroup> <LineGeometry x:Key="geometrymouth1" StartPoint="247,186.11" EndPoint="257.667,186.11"/> <PathGeometry x:Key="geometrylongue1" /> </Application.Resources>
上述代码主要是兔子的头部、耳朵、眼睛、嘴巴四部分的Path路径,另外考虑到对各个部分实现点击的效果,所有分别有两个对应的Path。
绘制Visual需要一个放Visual的容器,该容器重写一个属性VisualChildCount(告诉WPF该容器VIsual的数量)和一个方法GetVisualChild(根据索引的到任意一个VIsual),代码如下:
public class DrawingVisualHost : FrameworkElement { // Create a collection of child visual objects. private VisualCollection _children; private DrawingVisual headdrawingvisual; private DrawingVisual eardrawingvisual; private DrawingVisual eyedrawingvisual; private DrawingVisual mousedrawingvisual; public DrawingVisualHost() { _children = new VisualCollection(this); headdrawingvisual = CreateHeadDrawingVisual(true); eardrawingvisual = CreateEarDrawingVisual(true); eyedrawingvisual = CreateEyeDrawingVisual(true); mousedrawingvisual = CreateMouseDrawingVisual(true); _children.Add(headdrawingvisual); _children.Add(eardrawingvisual); _children.Add(eyedrawingvisual); _children.Add(mousedrawingvisual); this.MouseLeftButtonUp += new System.Windows.Input.MouseButtonEventHandler(MyVisualHost_MouseLeftButtonUp); this.MouseRightButtonDown += new System.Windows.Input.MouseButtonEventHandler(DrawingVisualHost_MouseRightButtonDown); } void DrawingVisualHost_MouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) { _children.RemoveRange(0, 4); headdrawingvisual = CreateHeadDrawingVisual(true); eardrawingvisual = CreateEarDrawingVisual(true); eyedrawingvisual = CreateEyeDrawingVisual(true); mousedrawingvisual = CreateMouseDrawingVisual(true); _children.Add(headdrawingvisual); _children.Add(eardrawingvisual); _children.Add(eyedrawingvisual); _children.Add(mousedrawingvisual); } void MyVisualHost_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) { System.Windows.Point pt = e.GetPosition((UIElement)sender); HitTestResult hittestresult = VisualTreeHelper.HitTest(this, pt); if (headdrawingvisual == hittestresult.VisualHit) { // MessageBox.Show("击中了兔子头部!"); _children.Remove(headdrawingvisual); headdrawingvisual = CreateHeadDrawingVisual(false); _children.Insert(0, headdrawingvisual); } else if (eardrawingvisual == hittestresult.VisualHit) { // MessageBox.Show("击中了兔子耳朵!"); _children.Remove(eardrawingvisual); eardrawingvisual = CreateEarDrawingVisual(false); _children.Insert(1, eardrawingvisual); } else if (eyedrawingvisual == hittestresult.VisualHit) { // MessageBox.Show("击中了兔子眼部!"); _children.Remove(eyedrawingvisual); eyedrawingvisual = CreateEyeDrawingVisual(false); _children.Insert(2, eyedrawingvisual); } else if (mousedrawingvisual == hittestresult.VisualHit) { _children.Remove(mousedrawingvisual); mousedrawingvisual = CreateMouseDrawingVisual(false); _children.Insert(3, mousedrawingvisual); } VisualTreeHelper.HitTest(this, null, new HitTestResultCallback(myCallback), new PointHitTestParameters(pt)); } public HitTestResultBehavior myCallback(HitTestResult result) { if (result.VisualHit.GetType() == typeof(DrawingVisual)) { if (headdrawingvisual == result.VisualHit) { _children.Remove(headdrawingvisual); headdrawingvisual = CreateHeadDrawingVisual(false); _children.Insert(0, headdrawingvisual); } else if (eardrawingvisual == result.VisualHit) { _children.Remove(eardrawingvisual); eardrawingvisual = CreateEarDrawingVisual(false); _children.Insert(1, eardrawingvisual); } else if (eyedrawingvisual == result.VisualHit) { _children.Remove(eyedrawingvisual); eyedrawingvisual = CreateEyeDrawingVisual(false); _children.Insert(2, eyedrawingvisual); } else if (mousedrawingvisual == result.VisualHit) { _children.Remove(mousedrawingvisual); mousedrawingvisual = CreateMouseDrawingVisual(false); _children.Insert(3, mousedrawingvisual); } } else { _children.RemoveRange(0, 4); headdrawingvisual = CreateHeadDrawingVisual(true); eardrawingvisual = CreateEarDrawingVisual(true); eyedrawingvisual = CreateEyeDrawingVisual(true); mousedrawingvisual = CreateMouseDrawingVisual(true); _children.Add(headdrawingvisual); _children.Add(eardrawingvisual); _children.Add(eyedrawingvisual); _children.Add(mousedrawingvisual); } // Stop the hit test enumeration of objects in the visual tree. return HitTestResultBehavior.Continue; } private DrawingVisual CreateHeadDrawingVisual(bool change) { DrawingVisual drawingVisual = new DrawingVisual(); DrawingContext drawingContext = drawingVisual.RenderOpen(); GeometryDrawing headdrawing = new GeometryDrawing(); SolidColorBrush solidcolorbrush = this.FindResource("headcolor") as SolidColorBrush; if (solidcolorbrush == null) { drawingContext.Close(); return null; } if (change) { Geometry headgeometry = this.FindResource("geometryhead1") as Geometry; if (headgeometry == null) { drawingContext.Close(); return null; } headdrawing.Brush = solidcolorbrush; headdrawing.Geometry = headgeometry; } else { Geometry headgeometry = this.FindResource("geometryhead2") as Geometry; if (headgeometry == null) { drawingContext.Close(); return null; } headdrawing.Brush = solidcolorbrush; headdrawing.Geometry = headgeometry; } drawingContext.DrawDrawing(headdrawing); drawingContext.Close(); return drawingVisual; } private DrawingVisual CreateEarDrawingVisual(bool change) { DrawingVisual drawingVisual = new DrawingVisual(); DrawingContext drawingContext = drawingVisual.RenderOpen(); GeometryDrawing eardrawing = new GeometryDrawing(); SolidColorBrush solidcolorbrush = this.FindResource("headcolor") as SolidColorBrush; if (solidcolorbrush == null) { drawingContext.Close(); return null; } if (change) { eardrawing.Brush = solidcolorbrush; Geometry eargeometry = this.FindResource("geometryear1") as Geometry; eardrawing.Geometry = eargeometry; } else { eardrawing.Brush = solidcolorbrush; Geometry eargeometry = this.FindResource("geometryear2") as Geometry; eardrawing.Geometry = eargeometry; } drawingContext.DrawDrawing(eardrawing); drawingContext.Close(); return drawingVisual; } private DrawingVisual CreateEyeDrawingVisual(bool change) { DrawingVisual drawingVisual = new DrawingVisual(); DrawingContext drawingContext = drawingVisual.RenderOpen(); if (change) { OuterGlowBitmapEffect bitmapeffect = new OuterGlowBitmapEffect(); bitmapeffect.GlowColor = Colors.Black; bitmapeffect.GlowSize = 10; drawingContext.PushEffect(bitmapeffect, null); // 兔眼眶 DrawingGroup eyedrawing = new DrawingGroup(); GeometryDrawing eyeorbitdrawing = new GeometryDrawing(); eyeorbitdrawing.Brush = new SolidColorBrush(Colors.White); Geometry eyegorbiteometry = this.FindResource("geometryeyeorbit1") as Geometry; eyeorbitdrawing.Geometry = eyegorbiteometry; eyedrawing.Children.Add(eyeorbitdrawing); // 兔眼珠 GeometryDrawing eyeballdrawing = new GeometryDrawing(); eyeballdrawing.Brush = new SolidColorBrush(Colors.Black); Geometry eyeballgeometry = this.FindResource("geometryeyeball1") as Geometry; eyeballdrawing.Geometry = eyeballgeometry; eyedrawing.Children.Add(eyeballdrawing); drawingContext.DrawDrawing(eyedrawing); drawingContext.Pop(); } else { // 兔眼眶 DrawingGroup eyedrawing = new DrawingGroup(); GeometryDrawing eyeorbitdrawing = new GeometryDrawing(); eyeorbitdrawing.Brush = new SolidColorBrush(Colors.White); Geometry eyegorbiteometry = this.FindResource("geometryeyeorbit2") as Geometry; eyeorbitdrawing.Geometry = eyegorbiteometry; eyedrawing.Children.Add(eyeorbitdrawing); // 兔眼珠 GeometryDrawing eyeballdrawing = new GeometryDrawing(); eyeballdrawing.Brush = new SolidColorBrush(Colors.Black); Geometry eyeballgeometry = this.FindResource("geometryeyeball2") as Geometry; eyeballdrawing.Geometry = eyeballgeometry; eyedrawing.Children.Add(eyeballdrawing); drawingContext.DrawDrawing(eyedrawing); } drawingContext.Close(); return drawingVisual; } private DrawingVisual CreateMouseDrawingVisual(bool change) { DrawingVisual drawingVisual = new DrawingVisual(); DrawingContext drawingContext = drawingVisual.RenderOpen(); if (change) { GeometryDrawing mouthdrawing = new GeometryDrawing(); mouthdrawing.Brush = new SolidColorBrush(Colors.Black); mouthdrawing.Pen = new Pen(new SolidColorBrush(Colors.Black), 1); Geometry mouthgeometry = this.FindResource("geometrymouth1") as Geometry; mouthdrawing.Geometry = mouthgeometry; drawingContext.DrawDrawing(mouthdrawing); } else { GeometryDrawing mouthdrawing = new GeometryDrawing(); mouthdrawing.Brush = new SolidColorBrush(Colors.Black); mouthdrawing.Pen = new Pen(new SolidColorBrush(Colors.Black), 1); Geometry mouthgeometry = this.FindResource("geometrymouth2") as Geometry; mouthdrawing.Geometry = mouthgeometry; drawingContext.DrawDrawing(mouthdrawing); GeometryDrawing longuedrawing = new GeometryDrawing(); longuedrawing.Brush = new SolidColorBrush(Colors.Red); longuedrawing.Pen = new Pen(new SolidColorBrush(Colors.Red), 1); Geometry longuegeometry = this.FindResource("geometrylongue2") as Geometry; longuedrawing.Geometry = longuegeometry; drawingContext.DrawDrawing(longuedrawing); } drawingContext.Close(); return drawingVisual; } // Provide a required override for the VisualChildrenCount property. protected override int VisualChildrenCount { get { return _children.Count; } } // Provide a required override for the GetVisualChild method. protected override Visual GetVisualChild(int index) { if (index < 0 || index >= _children.Count) { throw new ArgumentOutOfRangeException(); } return _children[index]; } }
MyVisualHost_MouseLeftButtonUp事件中进行Hit-test的实现,通过VisualTree的一个静态方法HitTest,直接将当前鼠标点位作为参数传递,然后返回一个HitTestResult类型对象,其中有一个属性VisualHit属性。如果Visual检测到鼠标命中,则为该Visual;否则为null。
如果希望一次点击检测多个Visual对象(重叠情况,如兔子的头部和眼睛重叠),那么可以使用回调函数的方法(代码中的myCallback方法)检测鼠标是否命中。
最后附上 兔子的代码(我是兔子).