本篇我们来学习WPF的绘图,在2D绘图中主要有这么几个重要的类:Drawing、Visual和Shape,顺便讲下Brush和BitmapEffect。
1 2D绘图
1.1Drawing类
Drawing类表示形状和路径的二维图,它继承自Animatable类,所以支持数据绑定、动画和资源引用等。它有这么几个子类:
- GeometryDrawing:包含Geometry、用于填充的Brush以及绘画轮廓的Pen(都是属性)
- ImageDrawing:包含ImageSource以及定义边界的Rect
- VideoDrawing:包含MediaPlayer以及定义边界的Rect
- GlyphRunDrawing:包含GlyphRun类、低级别的文本类以及用于绘制前景色的Brush
- DrawingGroup:包含一组Drawing对象的集合类
Drawing并不是UIElement,所以本身没有绘画的能力,如果将其设置为窗体或者内容控件的内容,将只是显示其ToString的结果,要想在呈现Drawing,可以将DrawingImage、DrawingBrush和DrawingVisual这三者对象作为宿主容器。
1.1.1GeometryDrawing
GeometryDrawing,顾名思义就是画几何图形的,这个功能是由Geometry类型属性提供的。Geometry类大的方向可分为Basic Geometry和Aggregate Geometry:
Basic Geometry基本几何体又可分为:
- RectangleGeometry:绘制矩形,包括定义尺寸的Rect属性以及分别定义圆角X轴和Y轴的RadiusX和RadiusY
- EllipseGeometry:绘制椭圆,包括定义圆心的Center属性以及分别定义圆角X轴和Y轴的RadiusX和RadiusY
- LineGeometry:绘制直线,包括起点StartPoint属性和终点EndPoint属性
- PathGeometry:绘制任何几何图形,包括描述轮廓的Figures(默认内容属性)和填充效果FillRule,该属性是一组PathFigure集合,而PathFigure又包含表示PathSegment(路径片段)集合的Segments。
PathSegment是一个抽象类,它有以下几个实现类:LineSegment(线段)、PolyLineSegment(线段集合)、ArcSegment(曲线段)、BezierSegment(三次贝塞尔)、PolyBezierSegment(三次贝塞尔集合)、 QuadraticBezierSegment(二次贝塞尔)和PolyQuadraticBezierSegment(二次贝塞尔集合)。
Aggregate Geometry聚合集合体又可分为:
GeometryGroup:顾名思义,有一个或者多个Geometry组成,本身是Geometry类型,通过Transform属性来表现复杂图形
CombinedGeometry:他不是一个通用的Geometry集合,它通过GeometryCombineModel枚举器来合并有且仅有的两个Geometry。
// 摘要: // 指定可用于合并两个几何图形的不同方法。 public enum GeometryCombineMode { // 摘要: // 通过采用两个区域的并集合并两个区域。 所生成的几何图形为几何图形 A + 几何图形 B。 Union = 0, // // 摘要: // 通过采用两个区域的交集合并两个区域。 新的区域由两个几何图形之间的重叠区域组成。 Intersect = 1, // // 摘要: // 将在第一个区域中但不在第二个区域中的区域与在第二个区域中但不在第一个区域中的区域进行合并。 新的区域由 (A-B) + (B-A) 组成,其中 A // 和 B 为几何图形。 Xor = 2, // // 摘要: // 从第一个区域中除去第二个区域。 如果给出两个几何图形 A 和 B,则从几何图形 A 的区域中除去几何图形 B 的区域,所产生的区域为 A-B。 Exclude = 3, }
当我们在用PathGeometry来绘制复杂的图形时,需要写很多的代码,这时候MS总会想些办法来为我们减少工作量,这就是路径标记语法。
1.1.2ImageDrawing
很明显,ImageDrawing是用来绘制图像的,它通过ImageSource属性指定要绘制的图像,通过Rect属性来制定每个图像的位置和大小。
1.1.3VideoDrawing
播放媒体文件。 如果媒体为视频文件,则 VideoDrawing 会将其绘制到指定的矩形中。它包含一个获取或设置与绘制关联的媒体播放器的MediaPlayer类型的Player属性,包含一个获取或设置可在其中绘制视频的矩形区域的Rect属性。虽然可以在 XAML 中声明此类的实例,但是由于MediaPlayer类的依赖项,特别是 Open 和 Play 方法的缘故,如果不使用代码,则无法加载和播放其媒体。 要只在 XAML 中播放媒体,请使用 MediaElement。可以使用 VideoDrawing 和 MediaPlayer 来播放音频或视频文件。 加载并播放媒体的方法有两种。 第一种方法是使用 MediaPlayer 和 VideoDrawing 自身,第二种方法是创建您自己的 MediaTimeline,并将其与 MediaPlayer 和 VideoDrawing 一起使用,这种方法可以控制Video的播放,比如快进等。
// Create a MediaTimeline.MediaTimeline mTimeline = new MediaTimeline(new Uri(@"sampleMedia\xbox.wmv", UriKind.Relative)); // Set the timeline to repeat.mTimeline.RepeatBehavior = RepeatBehavior.Forever;// Create a clock from the MediaTimeline.MediaClock mClock = mTimeline.CreateClock();MediaPlayer repeatingVideoDrawingPlayer = new MediaPlayer();repeatingVideoDrawingPlayer.Clock = mClock;VideoDrawing repeatingVideoDrawing = new VideoDrawing();repeatingVideoDrawing.Rect = new Rect(150, 0, 100, 100);repeatingVideoDrawing.Player = repeatingVideoDrawingPlayer;
1.1.4GlyphRunDrawing
表示一个呈现GlyphRun的Drawing对象,而GlyphRun表示一序列标志符号,这些标志符号来自具有一种字号和一种呈现样式的一种字体。
GlyphRun是一种比Label等更低级别的文本展示方式,用于固定格式的文档表示和打印方案 。
1.1.5DrawingGroup
DrawingGroup用于将一个或者多个Drawing组合起来作为整体绘图,比如你可以将多个Drawing组合起来,使用Transform属性。
1.2Visual
Visual类是UIElement类的抽象基类,它是任何东西绘画到屏幕上的基本实现。DrawingVisual也是Visual的一个间接子类(直接继承自ContainerVisual),它主要是呈现Drawing,例如Image、Video等,而且它还支持通过Hit Testing来进行与输入设备的交互,但是它不能接收键盘、鼠标或笔针事件。
1.2.1DrawingVisual在屏幕上的显示
我们知道UIElement都能很好的在屏幕上显示,然而,DrawingVisual在作为ContentControl的内容时,只是显示其ToString的结果。为了让DrawingVisual显示在屏幕上,需要将其添加到UIElement的Visual树结构上,将该UIElment作为其宿主容器。为此,我们需要做这样的两个工作:一是自定义一个继承自UIElement的类;而是Override其VisualChildrenCount属性和GetVisualChild方法。
class WndVisual:UIElement { private DrawingVisual drawingVisual = null; public WndVisual() { drawingVisual = new DrawingVisual(); } protected override int VisualChildrenCount { get { return 1; } } protected override Visual GetVisualChild(int index) { if (index != 0) throw new ArgumentOutOfRangeException("index"); return drawingVisual; } protected override void OnRender(DrawingContext drawingContext) { //Drawing drawing = FindResource("glyphRunStyle") as GlyphRunDrawing; //drawingContext.DrawDrawing(drawing); base.OnRender(drawingContext); } protected override void OnRenderSizeChanged(SizeChangedInfo info) { GlyphRunDrawing glyphRun = Application.Current.MainWindow.FindResource("glyphRenStyle") as GlyphRunDrawing; if (glyphRun != null) { using (var drawingContext = drawingVisual.RenderOpen()) { drawingContext.DrawDrawing(glyphRun); } this.AddVisualChild(drawingVisual); } base.OnRenderSizeChanged(info); } }
1.2.2 Visual的Hit Testing
Hit Testing译为命中测试,它指的是判断一个点或者一组点是否与一个给定的对象相交。应用于鼠标时,通常是指鼠标指针的位置。
在WPF中有两种命中测试:Visual Hit Testing和Input Hit Testing。前者被所有的Visual对象支持,而后者仅被UIElement对象支持。这个很好理解,输入事件被定义在UIElement类中。这里主要讲Visual Hit Testing。通过VisualTreeHelper的HitTest方法来实现,来看下方法定义:
// // 摘要: // 通过指定 System.Windows.Point 返回命中测试的最顶层 System.Windows.Media.Visual 对象。 // // 参数: // reference: // 要进行命中测试的 System.Windows.Media.Visual。 // // point: // 要进行命中测试的点值。 // // 返回结果: // System.Windows.Media.Visual 的命中测试结果,作为 System.Windows.Media.HitTestResult // 类型返回。 public static HitTestResult HitTest(Visual reference, Point point); // // 摘要: // 使用调用方定义的 System.Windows.Media.HitTestFilterCallback 和 System.Windows.Media.HitTestResultCallback // 方法对指定的 System.Windows.Media.Visual 启动命中测试。 // // 参数: // reference: // 要进行命中测试的 System.Windows.Media.Visual。 // // filterCallback: // 表示命中测试筛选回调值的方法。 // // resultCallback: // 表示命中测试结果回调值的方法。 // // hitTestParameters: // 要进行命中测试的参数值。 public static void HitTest(Visual reference, HitTestFilterCallback filterCallback, HitTestResultCallback resultCallback, HitTestParameters hitTestParameters); // // 摘要: // 使用调用方定义的 System.Windows.Media.HitTestFilterCallback 和 System.Windows.Media.HitTestResultCallback // 方法对指定的 System.Windows.Media.Media3D.Visual3D 启动命中测试。 // // 参数: // reference: // 要进行命中测试的 System.Windows.Media.Media3D.Visual3D。 // // filterCallback: // 表示命中测试筛选回调值的方法。 // // resultCallback: // 表示命中测试结果回调值的方法。 // // hitTestParameters: // 要进行命中测试的三维参数值。 public static void HitTest(Visual3D reference, HitTestFilterCallback filterCallback, HitTestResultCallback resultCallback, HitTestParameters3D hitTestParameters);
第一个版本是一个简单的版本,它仅用于返回命中测试的最顶层的对象;第二个版本可用于重叠的Visual的命中测试的情况;第三个版本可用于重叠的Visual3D的命中测试的情况。
class WndDrawingVisual3:UIElement { DrawingVisual bodyVisual = null; DrawingVisual eyesVisual = null; DrawingVisual mouthVisual = null; public WndDrawingVisual3() { bodyVisual = new DrawingVisual(); eyesVisual = new DrawingVisual(); mouthVisual = new DrawingVisual(); using (DrawingContext dc = bodyVisual.RenderOpen()) { // The body dc.DrawGeometry(Brushes.Blue, null, Geometry.Parse( @"M 240,250 C 200,375 200,250 175,200 C 100,400 100,250 100,200 C 0,350 0,250 30,130 C 75,0 100,0 150,0 C 200,0 250,0 250,150 Z")); } using (DrawingContext dc = eyesVisual.RenderOpen()) { // Left eye dc.DrawEllipse(Brushes.Black, new Pen(Brushes.White, 10), new Point(95, 95), 15, 15); // Right eye dc.DrawEllipse(Brushes.Black, new Pen(Brushes.White, 10), new Point(170, 105), 15, 15); } using (DrawingContext dc = mouthVisual.RenderOpen()) { // The mouth Pen p = new Pen(Brushes.Black, 10); p.StartLineCap = PenLineCap.Round; p.EndLineCap = PenLineCap.Round; dc.DrawLine(p, new Point(75, 160), new Point(175, 150)); } bodyVisual.Children.Add(eyesVisual); bodyVisual.Children.Add(mouthVisual); this.AddVisualChild(bodyVisual); } protected override Visual GetVisualChild(int index) { if (index != 0) throw new ArgumentOutOfRangeException("index"); return bodyVisual; } protected override int VisualChildrenCount { get { return 1; } } protected override void OnMouseLeftButtonDown(System.Windows.Input.MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); /* // Retrieve the mouse pointer location relative to the Window Point location = e.GetPosition(this); // Perform visual hit testing HitTestResult result = VisualTreeHelper.HitTest(this, location); // If we hit any DrawingVisual, rotate it if (result.VisualHit.GetType() == typeof(DrawingVisual)) { DrawingVisual dv = result.VisualHit as DrawingVisual; if (dv.Transform == null) dv.Transform = new RotateTransform(); (dv.Transform as RotateTransform).Angle++; } * */ Point location = e.GetPosition(this); VisualTreeHelper.HitTest(this, new HitTestFilterCallback(hitTestFilterCallback), new HitTestResultCallback(HitTestCallback), new PointHitTestParameters(location)); } private HitTestResultBehavior HitTestCallback(HitTestResult result) { if (result.VisualHit.GetType() == typeof(DrawingVisual)) { DrawingVisual dv = result.VisualHit as DrawingVisual; if (dv.Transform == null) dv.Transform = new RotateTransform(); (dv.Transform as RotateTransform).Angle++; } return HitTestResultBehavior.Continue; } private HitTestFilterBehavior hitTestFilterCallback(DependencyObject potentialHitTestTarget) { if (potentialHitTestTarget == bodyVisual) return HitTestFilterBehavior.ContinueSkipSelf; return HitTestFilterBehavior.Continue; } //重写该方法,可实现自定义命中方式 protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters) { return base.HitTestCore(hitTestParameters); } }
1.3Shape
Shape和GeometryDrawing一样,也是基本的二位画图,它结合了Geometry、Brush和Pen。不同的是,Shape继承者FrameworkElement,可以直接在屏幕显示。它包括Brush类型的Fill属性和Stroke属性。
它有这么几个子类:
- Line:包括(X1,Y2)、(X2,Y2)这四个Double值,该坐标是相对坐标
- Rectangle:包括圆角的RadiusX和RadiusY,当RadiusX=RadiusY>=Width/2=Height/2时,表示一个圆,RadiusX>=Width/2 && RadiusY>=Height/2表示一个椭圆
- Ellipse:用最大的椭圆去填充由Width和Height决定的矩形区域,没有指定圆角的RadiusX和RadiusY和Center属性,当Width=Height时,表示一个圆
- Polyline:表示一组有序的线段,通过Points属性指定多个点,格式:Points="x1,y1 x2,y2"或者Points="x1 y1 x2 y2"
- Polygon:它比Polyline唯一多做的事情就是会自动封闭
- Path:它仅仅比Shape多了一个Geometry类型的Data属性,因此可以将任意的Geometry显示在界面,这里可以使用路径标记文法给Data赋值
1.4Brush
在使用XAML进行页面布局时,我们发现几乎很少直接用Color来交互,而是用Color的封装对象Brush来直接交互。它包括三种Color Brush(SolidColorBrush、LinearGradientBrush和RadialGradientBrush)和三种Tile Brush(DrawingBrush、ImageBrush和VisualBrush)
- SolidColorBrush:单色,它包含一个Color属性,该属性支持两种色彩空间:sRGB和scRGB,另外还可以使用ContextColor uri来使用ICC Profile
- LinearGradientBrush:线性渐变色,它包含一个GradientStops属性,该属性是一个GradientStop集合,GradientStop通过Offset和Color属性来控制
- RadialGradientBrush:径向渐变色,它的每个GradientStop都以相同的起始点呈椭圆状向外散发,通过GrandientOrigin属性指定起始点,默认值(.5,.5),当GrandientOrigin与Center不在一点时,会呈现很惊 人的效果
- DrawingBrush:包含Drawing属性,可以绘制形状、文本、视频、图像或其他绘图,可以通过Stretch、ViewBox、ViewPort、AlignmentX/AlignmentY来控制
- ImageBrush:包含ImageSource属性(矢量图),而ImageDrawing的ImageSource属性(位图)
- VisualBrush:包含Visual属性,如果直接将一个新的UIElement作为Visual属性值,它只是一个外观,不具有该UIElement的行为,关联一个屏幕上的UIElement则可具有其行为
1.5BitmapEffect
BitmapEffect位图效果指的是System.Windows.Media.Effects命名空间下的五种Effect,可被应用于UIElement、DrawingGroup和Viewport3DVisual等。这五种Effect分别是:
- BevelBitmapEffect:创建一个凹凸效果,该效果根据指定的曲线来抬高图像表面(已过时,用Effect代替)
- BlurBitmapEffect:模拟通过离焦透镜查看对象的情形(已过时,用BlurEffect代替)
- DropShadowBitmapEffect:以微小偏移量在可视对象之后应用阴影,通过模拟虚构光源的投影来确定偏移量(已过时,用DropShadowEffect代替)
- EmbossBitmapEffect:创建可视对象的凹凸贴图,从而制造人工光源下的深度和纹理效果(已过时,用Effect代替)
- OuterGlowBitmapEffect:围绕对象或颜色区域创建颜色光环(已过时,用BlurEffect代替)
从WPF4开始,这些类都已经过时了,用Effect及其子类来代替,这意味着你在WPF4环境中使用上面的类是没有效果的。我们来看看新的Effect子类:
注意:这三个类是派生自Effect类,并不是派生自BitmapEffect类,BitmapEffect和Effect是同级别的类。
WPF4使用Effect代替BitmapEffect的原因有这么几点:
- 使用的是UI线程渲染,而不是渲染线程,所以会导致性能下降
- 不支持像素着色器
- 使用的非托管代码实现,一来安全性要求高,在浏览器中无法使用;二来自定义不方便,需要使用mileffects.h等一些非托管API来实现
可以通过继承ShaderEffect抽象类来实现自定义的像素着色器,需要使用HLSL来编写着色器,然后编译成.ps文件供WPF使用,在上已经有了这样的第三方的自定义效果类。
这里随便讲一下WritableBitmap类:Image没有提供创建编辑位图的方法,这是WritableBitmap类的由来。先以一张图来说明继承关系:
首先需要说明的是,并不是所有的像素格式都是支持写入的,常见的支持写入的像素格式有:Bgra32、Pbgra32和Bgr32等。
WriteableBitmap bitmap = new WriteableBitmap(80, 80, 96, 96, PixelFormats.Bgra32, null); int stride = 80 * bitmap.Format.BitsPerPixel / 8;//计算跨距,每行像素数据需要的字节数量 /* //1.逐像素法 byte[] colors = { 0, 255, 0, 255 }; //按照B,G,R,G的顺序,此处表示绿色 for (int i = 0; i < 80; i++) { for (int j = 0; j < 80; j++) { Int32Rect rect = new Int32Rect(i, j, 1, 1); bitmap.WritePixels(rect, colors, stride, 0); } } * */ //2.一次写入法 Int32Rect rect1 = new Int32Rect(0, 0, 80, 60); byte[] colors1 = new byte[80 * 60 * bitmap.Format.BitsPerPixel / 8]; for (int i = 0; i < 60; i++)//行遍历 { for (int j = 0; j < 80; j++)//列遍历 { int offset = (j + 80 * i) * bitmap.Format.BitsPerPixel / 8;//计算像素的偏移量 colors1[offset] = 0; colors1[offset + 1] = 255; colors1[offset + 2] = 0; colors1[offset + 3] = 255; bitmap.WritePixels(rect1, colors1, stride, 0); } } Image img = new Image(); img.Source = bitmap; img.ToolTip = "Image"; grid.Children.Add(img); img.SetValue(Grid.RowProperty, 3); img.SetValue(Grid.ColumnProperty, 1);
对于大片像素的写入,一次写入法的效率显然要好很多。