首页 > 代码库 > 7.2.2 在窗体上显示绘图

7.2.2 在窗体上显示绘图

7.2.2 在窗体上显示绘图


绘图与第四章的示例类似。因为绘图需要一定的时间,我们将在内存中创建位图,绘制好文档,然后,在窗体上显示位图,而不是每次窗体失效时都绘制文档。我们先看一下非常有用的函数式编程模式,这一节就将使用。


“Hole in the Middle(中间有洞)”模式


[真心不知道,Hole in the Middle 是什么意思?]
写代码的一个常见情形,是首先进行初始化,然后,是函数的核心部分,最后,是一些清理工作;在程序中的多个位置,重复执行类似的操作,初始化和清理部分没有变化,而只有核心部分不同。在内存中绘制位图的示例,用 C# 写的代码可能像这样:


var bmp = new Bitmap(width, height)
using(var gr = Graphics.FromImage(bmp)) { 
  (...)
}


这里,代码的核心部分是第三行的占位符,我们将用值 gr 来绘图。问题是,只使用面向对象的编程概念,不能简单地把执行初始化和清理的代码,包装成一个子程序,实现在绘制不同图形的所有地方共享。C# 语言支持这种模式众所周知,通常使用各种初始化和清理的类型;using 构造就是这种情况。我们如何为自己的代码模式实现类似的功能呢?
用函数式编程,解决起来很简单;可以写一个高阶函数,把核心部分包装到 lambda 函数中,并把它作为高阶函数的参数值:


var bmp = DrawImage(width, height, gr => {
    (...)
  });


从函数编程的角度来看,这仅是一个使用高阶函数的无趣示例,但是,它描述了这样一种情况,先执行一些初始化,接着是核心部分,然后是清理,而且这种情况非常普遍,所以,需要有一个专门的名字。这个名字是由 Brian Hurt 在他的博客《“Hole in the Middle(中间有洞)”模式》[Hurt, 2007] 中首次使用,它很好地描述了这种情况,即,每次使用代码,只有中间的部分需要用不同的功能填充。


清单 7.7 是用 F# 实现的函数,类似于补充材料“Hole in the Middle(中间有洞)模式”中的 DrawImage,有一点不同,用两个参数指定创建位图的大小,还能够指定图像边框的边距。


清单 7.7 绘制图形的函数 (F# Interactive)
 
> let drawImage (width:int, height:int) space coreDrawingFunc =
     let bmp = new Bitmap(width, height)
     use gr = Graphics.FromImage(bmp)    [1]
     gr.Clear(Color.White)
     gr.TranslateTransform(space, space)    <-- 移动整个图像
     coreDrawingFunc(gr)    [2]
     bmp
;;
val drawImage : int * int -> float32 -> (Graphics -> unit) –> Bitmap  [3]


我们使用这个函数来绘制图像,绘图的核心部分是作为最后参数给定的函数,从类型签名[3]可以看出,这个函数的参数为 Graphics 值,不返回结果;在代码的中间[2],创建位图和 Graphics 对象之后,被调用。在初始化阶段,我们还调用 TranslateTransform,提供图像的边距。
我们创建的 Graphics 对象实现了 IDisposable,所以,需要在完成绘图后释放。在 C# 中,我们使用知名的 using 构造;在 F# 中,用 use 关键字[1],可以做类似的工作。在清单 7.7 中,在返回结果的位图之前,自动释放 Graphics 对象。use 关键字的与 using 有点不同,我们将在第九章讨论。
最后,我们需要看代码的实际效果。现在,我们将用交互方式创建并测试窗体。清单 7.8 演示了如何绘制清单 7.5 的屏幕元素,以及把文档显示在窗体上。


清单 7.8 使用 WinForms 绘制文档 (F# Interactive)
> let docImage = drawImage (450, 400) 20.0f (drawElements elements)  [1]
val docImg : Bitmap


> open System.Windows.Forms
let main = new Form(Text = "Document", BackgroundImage = docImage,    | 创建有文档
                                Width = docImage.Width, Height = docImage.Height)   | 的窗体
main.Show();;                                                    |


绘制位图[1]的这一行可能要解释一下。我们调用 drawImage,它的最后一个参数是函数,指定了绘图的核心部分。因为我们曾经在 drawElements中实现了这个函数,你可能希望能够将它直接作为最后一个参数值来传递;然而,drawElements 有两个参数,而 drawImage 函数只有一个参数(需要绘制的 Graphics 对象)。我们使用散函数应用指定有 ScreenElement 值的列表;散应用的结果是函数,以 Graphics 对象,并绘制文档,这正是我们需要的 (图 7.2)。
 


图 7.2 保存在屏幕元素列表中的示例文档,使用 drawElements 函数在窗体上绘制。



我们以前使用的文档表示方式,虽然使创建文档的代码是有点笨拙,但是,能够轻松地实现绘制。在函数式编程中,我们经常会发现,不同的上下文需要不同数据结构:在某种程度上,期望的用法决定好的表示形式;对于函数式程序,同样的信息,在同一个程序中,有不同的表示形式,并不少见。现在,我们已经找到了适合绘制图像的表示形式,那么,还需要设计适合构造和处理的表示形式,然后,再写一个转换函数,实现两种表示形式的转换。

7.2.2 在窗体上显示绘图