15.4.2 创建和组合绘图
我们要保持事情,简单地画一个圆。我们可以用类似的方式,实现很多其他类型,但是,我们来看一个例子,可以添加更多的绘图。精确的形状并不是特别重要的,在我们可以讨论的更有趣的组合的话题之前,需要一些具体的东西。
创建和移动圆
创建绘图,大约就是在图形对象上提供绘制的函数,图形对象是这个函数参数值。图形类型具有 FillEllipse 方法,所以,基实现根本不应该棘手。值得注意的是,清单包含了少量的额外的噪音:添加另一个绘图只需要几行代码。清单 15.13 显示了 C# 和 F# 的实现。
Listing 15.13 Creating circle in F# and C#
// C# version
public static class Drawings {
public static IDrawing Circle(Brush brush, float size) {
return new Drawing(gr =>
gr.FillEllipse(brush, -size/2.0f, -size/2.0f, size, size)
);
}
}
// F# version
module Drawings =
let circle brush size =
drawing(fun g –>
g.FillEllipse(brush, -size/2.0f, -size/2.0f, size, size))
为了更好地组织代码,我们把函数放在名叫 Drawings 的组织单元内。在 C# 中,它被实现为静态类,而在 F# 中,使用模块。C# 代码实现 Circle 方法,创建一个新的 Drawing对象,把绘图函数给它作为参数。Lambda 函数调用 FillEllipse,指定画笔和大小。在 F# 中,我们把 circle 实现为一个简单的函数,取画笔和大小作为两个参数。
在传统的函数式设计中,这是写函数的首选方法,除非有一些逻辑原因,需要使用元组(例如,一个元组表示有两个坐标的点)。在 F# 中,我们一直在更经常使用元组化(tupled)的参数,以符合通常的 .NET 编码风格,但在这里,我们将使用函数的方法。开发组合库时,使用函数风格,通常是一个好主意,我们将很快看到这种设计如何可以方便地创建动画圆。
该函数使用我们早些时候实现的高阶函数 drawing,给它 lambda 函数画圆。现在,我们将使用 circle 作为唯一的绘图基元,并看到我们能用它来做什么。
如果我们创建了两个圆,它们两个的中心都是点 (0, 0)。这意味着,如果我们组合两个圆,就不会得到非常有趣的结果。清单 15.13 中的代码,可以指定一个圆的大小,但看来,我们忘了指定位置!其实这是经过故意的,因为我们要使用不同的方法指定位置。我们将创建一个圆,以 (0,0) 为中心,并将其移动到任何我们需要的点。
我们会把绘图的运动实现为函数(或方法),它取绘图和一对坐标作为参数。然后,返回新的绘图,绘制原来的图形,经过给定偏移量的平移。我们如何能够实现这个函数呢?我们可以把原始绘图绘制为位图,然后,绘制位图到指定的坐标,但有一个更简单的解决方案。我们将使用绘图的图形类型,直接支持平移转换。清单 15.14 显示了这两种语言的实现。
Listing 15.14 Translating drawings in F# and C#
// F# version
let translate x y (img:Drawing) =
drawing(fun g –>
g.TranslateTransform(x, y)
img.Draw(g)
g.TranslateTransform(-x, -y) )
// C# version
public static IDrawing Translate(this IDrawing img, float x, float y) {
return new Drawing(g => {
g.TranslateTransform(x, y);
img.Draw(g);
g.TranslateTransform(-x, -y); }
);
}
这个组合返回一个新的平移后的绘图值。这个实现使用的创建模式,绘制圆的代码相同。 F# 函数使用 drawing 基元,指定如何绘制平移的图形,而在 C# 中,我们直接创建一个新的 Drawing 对象,并指定绘图函数。C# 版本把 Translate 实现为扩展方法,这样,可以使用点表示法来调用。注意,我们为接口 IDrawing 加了一个方法。没有扩展方法,这是不可能的,因为,接口只能包含抽象成员。
这一次,这个实现稍微有趣一点了,它改变了使用的坐标系的原点,当在图形上使用 TranslateTransform 绘图时。这意味着,如果我们运行原始的绘图代码(例如,绘制一个圆),它仍将绘制在 (0,0),但是这个点其实会在图形表面的其它地方。一旦我们配置平移,再运行原始绘图,并重置变换。严格地说,我们可以运行代码,在 finally 块中恢复原始设置,但我们想保持代码的简单性。
现在,我们可以到处移动绘图,终于可以创建其他的东西,而不是绘制叠在一起的圆。然而,我们不想在所有的时候者使用绘图的集合。那么,我们如何可以从两个绘图创建一个绘图呢?
组合绘图
如果把所有的绘图保存在集合中,组合绘图,将不得不重复许多函数。我们可能要在集合中移动所有绘图,但平移只适用于单个绘图。相反,我们想要创建一个绘图,将绘制所有的组合值,我们将创建一个组合函数实现这个目标。要理解这个函数的工作方式,我们可以看看它的类型签名:
val compose : Drawing -> Drawing –> Drawing
这个函数取两个绘图值作为参数,并返回一个绘图。我们不需要指定任何偏移量,定义绘图的位置,因为我们可以在调用 compose 前,平移参数,使用上一节中的 translate 函数。这个实现是很简单,下面你可以看到。
Listing 15.15 Creating composed drawing in F# and C#
// F# version
let compose (img1:Drawing) (img2:Drawing) =
drawing(fun g –>
img1.Draw(g)
img2.Draw(g) )
// C# version
public static IDrawing Compose(this IDrawing img1, IDrawing img2) {
return new Drawing(g => {
img1.Draw(g);
img2.Draw(g); }
);
}
清单 15.15 再一次重复了我们一直在使用的创建绘图的模式。这一次,使用的新绘图的 lambda 函数,调用 Draw 方法,把两个原始绘图组合到一起。
使用刚刚已经实现的三个函数,可以在不同的位置,创建几个包含多种色彩的圆的绘图。实现其他基元绘图会很简单,但不会教给我们新的东西,因此,在这一章我们坚持用圆。
到目前为止,我们已经看到用于实现绘图的代码,但还没有使用。我们探索动画之前,让我们看一些代码,来创建一个简单的绘图。我们还不会创建完整的应用程序,因为,这样可以更容易演示绘图什么时候开始动起来,现在,我们只看一下代码。图 15.4 显示了我们要创建的简单绘图。
![15.4.2_创建和组合绘图 15.4.2_创建和组合绘图](https://i-blog.csdnimg.cn/blog_migrate/8ad9fb720eade29154b9488b13779aa9.jpeg)
图 15.4 两个圆,用 translate 移动,compose 组合。
我们只看 F# 版本的代码。一旦把所有的一切都转换成动画,C# 示例会更有趣。
open Drawings
let greenCircle = circle Brushes.OliveDrab 100.0f
let blueCircle = circle Brushes.SteelBlue 100.0f
let greenAndBlue =
compose (translate -35.0f 35.0f greenCircle)
(translate 35.0f -35.0f blueCircle)
代码首先通过引用 Drawings 模块,包含了用于处理绘图的所有函数。接下来,创建一个绿色、一个蓝色的圆,大小为 100 个像素。我们在不同的方向上移动这些圆,大约 50 个像素,然后,组合这两个平移过的绘图,创建一个新的绘图值。
注意
在这一节中,我们只实现了几个基本的绘图函数,但还有许多其他可能需要尝试的概念。可以创建新的基元,比如,正方形或位图,还有一些新的转换,比如,旋转和缩放。图形类型使得实现这些很容易,使用 RotateTransform 和 ScaleTransform。你首先可能想完成这一章,因此,可以看到每一个如何运行成动画。
现在,我们已经分别实现了绘图和时变值的概念,把这两者结合起来实现动画很简单。