使用 F# 和 WPF 进行海龟图形和 L-系统





5.00/5 (1投票)
本文将介绍如何使用海龟绘图程序,展示实现该程序所用的 F# 和 WPF 代码,并提供一些使用 L-系统语法生成分形形状的示例代码。

引言
这是一个我编写的海龟绘图程序,用于提高我对 F# 的认识。海龟显示为一个光标,当接收到相对于自身位置的简单命令(如左转、右转和前进)时,会在画布上绘制线条。这些简单的命令可以作为绘制复杂形状(如螺旋和分形)的构建块。F# 的交互式提示提供了一种控制海龟的有用方式。本文将介绍如何使用海龟绘图程序,展示实现该程序所用的 F# 和 WPF 代码,并提供一些使用 L-系统语法生成分形形状的示例代码。
如何使用海龟
安装和运行 F#
这些说明假定您正在使用 Visual Studio 中的 F# Interactive,无论是 Visual Studio 2010 还是免费的 Shell。有关安装 F# 的信息可以在 Microsoft F# 开发中心找到。打开文件 FSharpTurtle.fsx,使用 Ctrl-A 全选,然后按 Alt-Enter 将脚本加载到 F# Interactive 中。此时会显示一个带有海龟的画布。
海龟命令
海龟响应命令 left
、right
、forward
和 move
或它们的缩写 lt
、rt
、fd
和 mv
。这些命令后都跟一个浮点数和两个分号(例如 left 90.0;;
)。left
和 right
命令以给定角度转动海龟,forward
绘制一条线,move
则在不绘制线条的情况下移动海龟。输入 clear();;
可以清除画布并将海龟恢复到原始位置。可以使用 settings
变量更改默认颜色(白色背景、黑色线条、红色箭头)。F# 使用 <-
运算符更改变量的值。
settings.BackgroundColour <- Brushes.Black;;
settings.ArrowColour <- Brushes.Green;;
settings.LineColour <- Brushes.Red;;
示例:绘制正方形
在 F# Interactive 中输入以下命令可以在画布上绘制一个正方形。
for i = 1 to 4 do
forward 200.0
left 90.0;;
海龟会绘制一条线并左转,直到回到其原始位置。

示例:绘制螺旋
let rec polyspi (angle:float)(inc:float)(side:float)(times:int) =
if times > 0 then
forward side
right angle
polyspi angle inc (side + inc)(times - 1)
else None |> ignore
我从《海龟几何》(第 18-19 页) 中改编了上面的螺旋图案绘制函数。在 F# 中,let
关键字用于声明函数和变量,而 rec
表示函数是递归的。使用不同的参数调用此函数会创建不同形状的螺旋,因此可以使用偏函数应用(也称为柯里化)来创建更专业的 polyspi 函数。下面声明的函数预填充了其前 3 个 polyspi 参数,只接受最后一个参数 (times:int)
作为参数。
let polyspi90 = polyspi 90. 5. length
let polyspi95 = polyspi 95. 1. length
let polyspi117 = polyspi 117. 3. length
海龟绘图实现
类型定义
下面称为记录(records)的类型定义用于存储海龟的状态。海龟在画布上有一个 x, y 点和一个角度,分别由类型为 Point
的记录标签 P
和类型为 float 的记录标签 Angle
表示。关键词 mutable
用于指定变量的值可以被更改。
type Point = { mutable X : float
mutable Y : float}
type Turtle = { mutable P : Point
mutable Angle : float }
下面的代码构建了海龟记录,并将其放在窗口的中心。Turtle
类型可以从记录标签推断出来,因此无需指定。
let turtle = {P = {X = w.ActualWidth/2.;Y = w.ActualHeight/2.}; Angle = 0.}
海龟坐标
海龟命令使用方向和距离来从起始点绘制线条。程序使用三角函数来查找线条终点的 x, y 坐标。下面定义的 nextPoint
函数将 forward
命令的距离视为直角三角形的斜边。这结合海龟的角度,用于查找三角形的邻边(x)和对边(y)的长度,并将它们添加到起始点,从而得到线条的终点。
let nextPoint hypotenuse =
let newX = turtle.P.X + hypotenuse * Math.Cos(degreesRadians(turtle.Angle))
let newY = turtle.P.Y + hypotenuse * Math.Sin(degreesRadians(turtle.Angle))
let newPoint = {X = newX;Y = newY}
newPoint
WPF 画布和海龟箭头多边形
海龟绘图绘制在 WPF 画布上。WPF 画布通常不可滚动,但我在这里找到了一些关于可滚动 Canvas 的 C# 代码 示例,并用 F# 重写了它。下面的代码创建了一个窗口、一个可滚动画布、一个滚动查看器以及一个表示海龟的箭头。
let w = new Window(Topmost=true)
w.Show()
let c = new ScrollableCanvas()
c.Background <- Brushes.White
let scrollViewer = new ScrollViewer()
scrollViewer.HorizontalScrollBarVisibility <- ScrollBarVisibility.Auto
scrollViewer.VerticalScrollBarVisibility <- ScrollBarVisibility.Auto
w.Content <- scrollViewer
scrollViewer.Content <- c
let makeArrow() =
let arrow = new Polygon()
arrow.Fill <- Brushes.Red
let p1 = new System.Windows.Point(0.,20.)
let p2 = new System.Windows.Point(25.,10.)
let p3 = new System.Windows.Point(0.,0.)
let centrePoint = new System.Windows.Point(0.5,0.5)
let pCollection = new PointCollection()
pCollection.Add p1
pCollection.Add p2
pCollection.Add p3
arrow.RenderTransformOrigin <- centrePoint
arrow.Points <- pCollection
arrow
let mutable arrow = makeArrow()
c.Children.Add(arrow)
下面定义的 left
和 right
函数改变海龟的角度,然后用它来旋转箭头多边形。
let left deg = turtle.Angle <- turtle.Angle - deg
arrow.RenderTransform <- new RotateTransform(Angle = turtle.Angle)
let right deg = turtle.Angle <- turtle.Angle + deg
arrow.RenderTransform <- new RotateTransform(Angle = turtle.Angle)
moveTo
函数将箭头移动到 WPF 画布上的给定 x, y 坐标。
let moveTo x y = turtle.P.X <- x
turtle.P.Y <- y
Canvas.SetLeft(arrow, turtle.P.X - 12.5)
Canvas.SetTop(arrow, turtle.P.Y - 10.0)
forward
函数使用 nextPoint
获取线条的终点,绘制线条,然后将箭头光标移动到终点。let forward distance =
let next = nextPoint distance
let l = new Line()
l.X1 <- turtle.P.X
l.Y1 <- turtle.P.Y
l.X2 <- next.X
l.Y2 <- next.Y
l.Stroke <- settings.LineColour
c.Children.Add(l) |> ignore
moveTo next.X next.Y
使用 L-系统绘制分形形状

分形和 L-系统
分形是几何形状,由自相似的部分组成。分形形状(包括复杂、外观自然的结构)的生长可以使用 L-系统语法和海龟绘图来模拟。L-系统语法包含一组符号、一个起始符号字符串和描述符号如何被其他符号替换的产生规则。符号可以是变量或常量;常量在每一代中保持不变,变量则被替换为其他符号。其中一些符号代表海龟绘图命令。
DOL-系统
此海龟程序包含可用于绘制最简单的 L-系统(称为 DOL-系统)的代码,这些系统是确定性的(每个符号只有一个产生规则)且上下文无关的(产生规则仅取决于一个符号,而不取决于其相邻符号)。
L-系统实现
下面定义的数据类型可用于描述一个简单的 L-系统。它有一个起始符号字符串 Start
,一个产生规则字典 Rules
,其中 char
键是一个符号,string
值是生成的符号字符串。绘制形状时,海龟会向左或向右转动一定的 Angle
。
type LSystem = { Start : string
Rules : Dictionary<char,string< /> />
Angle : float}
可以使用下面定义的 l-系统绘制上面植物状的图像。
let branching =
let rules = new Dictionary<char, />()
rules.Add('F',"FF")
rules.Add('X',"F-[[X]+X]+F[+FX]-X")
rules.Add('+',"+")
rules.Add('-',"-")
rules.Add('[',"[")
rules.Add(']',"]")
{Start = "X";Rules = rules;Angle=22.5}
此 L-系统语法中的符号(X 除外)与下面定义的函数中的绘图命令匹配。pushTurtle
和 popTurtle
函数将海龟的当前状态添加到堆栈或从中移除,从而允许绘制分支形状。
let drawCommand (lsys:LSystem)(symbol:char) =
match symbol with
| 'F' -> forward length
| 'G' -> forward length
| 'f' -> move length
| '+' -> left (lsys.Angle)
| '-' -> right (lsys.Angle)
| '[' -> pushTurtle()
| ']' -> popTurtle()
| _ -> None |> ignore
drawLSystem
函数在生成 n
次迭代的 instructions
字符串后,绘制 L-系统 lsys
。
let drawLSystem (lsys:LSystem) (n:int) =
let instructions = generateString lsys n in
for command in instructions do
drawCommand lsys command
文件 FSharpTurtle.fsx 中提供了其他 L-系统示例。
参考文献
F#
海龟绘图
- 海龟几何 - Harold Abelson 和 Andrea diSessa
L-系统
WPF
历史
- 2010 年 10 月 10 日:初始发布