4.4.1 创建用户界面
这一章,我们将使用 Windows Forms,它在很多方面都很简单,因此,从 F# 中使用其他技术应该不成问题。在 Windows Forms 中,用户界面是由组件(如Form, Button, or PictureBox)构成的,因此,首先要写的是构建用户界面控件的代码。此任务可以通过使用图形设计器进行简化,但我们的应用程序很简单,所以,就手工写代码了。在一些用户界面的框架(包括 WPF)中,控件结构可以用基于 XML 的文件描述,但在 Windows Forms中,是构建适当的类,然后,通过属性来配置。
在开始之前,我们需要在 Visual Studio 中配置项目。默认情况下,F# 项目不包含对所需 .NET 程序集的引用,因此,需要添加对 System.Windows.Forms 和 System.Drawing 的引用。即,在解决方案资源管理器中,使用添加引用选项;另外,当应用程序启动时,我们不想显示控制台窗口,可以打开项目属性,从输出类型下拉列表中选择 Windows 应用程序选项。配置好项目之后, 就可以写应用程序的第一部分了,如清单 4.6 所示。
清单 4.6 构建用户界面 (F#)
open System
open System.Drawing
open System.Windows.Forms
let mainForm = new Form(Width = 620, Height= 450, Text = "Pie Chart") [1]
let menu = new ToolStrip() | 构造应用程序的菜单
let btnOpen = newToolStripButton("Open") |
let btnSave = newToolStripButton("Save", Enabled = false) |
ignore(menu.Items.Add(btnOpen)) |
ignore(menu.Items.Add(btnSave)) |
let boxChart =
newPictureBox | 构造显示饼图的控件
(BackColor= Color.White, Dock = DockStyle.Fill, |
SizeMode= PictureBoxSizeMode.CenterImage) |
mainForm.Controls.Add(menu)
mainForm.Controls.Add(boxChart)
// TODO: Drawing of the chart & userinterface interactions
[<STAThread>] [2]
do
Application.Run(mainForm) <-- 启动程序主窗体
清单首先打开.NET 命名空间,其中有我们程序要使用的类。接下来,我们开始创建表现用户界面的控件,首选构建主窗口(也称为窗体)。我们使用 F# 语法,直接在初始化期间设置对象的属性[1],这样,代码更简短,同时也隐藏了代码中的副作用。代码在内部,使用这个语法,首先使用构造函数创建对象,然后,设置对象的属性,但我们看到的就是创建对象这一个操作。创建窗体时,我们使用了无参数构造函数,给构造函数指定参数也是可以的[这句话与代码不符]。这一点在后面的代码中,创建 btnSave 时,可以看到,其构造函数有一个字符串参数值。创建对象的类似语法现在 C# 3.0 中也是可以的,.NET 平台有一段有趣的历史(详细信息,请参补充材料“在 F#、C# 3.0 和 Cω 中构建类”)。
将工具栏按钮添加到菜单项集合,我们调用 Add 方法,它返回已添加项的索引。在 C# 中,调用这个方法,可以忽略返回值,而 F# 更严格。在函数编程中,返回值更重要,因此,忽略返回值通常就是错误。出于这个原因,当我们忽略返回值时,F# 编译器会报警。当然,要修改代码[即,不要返回值]也很容易,把对这个方法的调用包在 ignore 函数调用中。这个函数取任意值作为参数值,都返回 unit(表示没有返回值),因此,编译器不再报警。
清单继续构建菜单和 PictureBox 控件,这是用来显示饼图的。这一次,我们不使用 F# Interactive ,所以,在清单中有一个占位符,标记将来添加代码的点,用于绘制图表,并把绘图功能与用户界面连接起来。
清单 4.6 的最后部分是一段运行 Windows Forms 应用程序的标准代码。[2]它开始于 COM 技术的线程模型规范,由 Windows Forms 内部使用,使用标准的 .NET 特性值(STAThreadAttribute),因此,在 .NET 引用中,可以发现更多相关信息。在 C# 中,我们把此特性放在 Main 方法之前,但在 F# 中,包含可执行代码的源程序可以在任何地方;因为,我们需要应用这个特性,因此,使用 do 块,它将需要在应用程序启动时执行的代码组织在一起。
在 F#、C# 3.0 和 Cω 中构建类
我们曾经提到过,有些图形用户界面框架,使用 XML 描述如何构建控件。这是一种常见的做法,因为构建对象、设置属性,类似于构建 XML 节点、设置特性,这种相似性还是微软研究院的研究人员在 2003 年创造Cω 语言的动机,[Meijer, Schulte, and Bierman, 2003],它激发了许多功能,现在 C# 3.0 中仍在使用。在 Cω 中,构建ToolStripButton 控制的代码可以写成:
ToolStripButton btn =<ToolStripButton>
<Text>Save</Text>
<Enabled>True</Enabled>
<Image>{saveIco}</Image>
</ToolStripButton>
在 Cω 中,XML 语法直接集成在语言中。嵌套在 ToolStripButton 节点中的元素描述对象的属性;使用大括号语法,能够在类似 XML 代码中嵌入通常非 XML 的表达式。以这种方式构造对象的易用性可能激发 XAML 设计人员,这种基于 XML 的语言可以用在 WPF 中描述用户界面;在语言方面,它还激发了 C# 3.0 中称为对象初始值器(object initializers)的功能:
var btn = newToolStripButton("Save"){ Enabled = false, Image = saveIco };
这里没有使用基于 XML 的语法,但构造对象和描述属性的基本理念在本质上是相同的;这种语法还可以用来指定构造函数的参数,因为,属性可以在大括号中分别描述。清单 4.6 表明,这个功能在 F# 中一样可用:
let btn = newToolStripButton("Save", Enabled = false, Image = saveIco)
与 C# 3.0 的唯一区别是,在 F# 中,属性直接在构造函数的调用中指定;构造函数的参数值,是描述对象属性的一组键-值对。
参数化构建类的另一种方法,而且也是常规的方法调用,是使用命名的参数。关键的不同在于,参数名是构造函数或方法声明的一部分。命名的参数也可以用于初始化不可变的类,因为,在类创建之后,不再需要设置属性了。这个功能在 F# 中可用,在 F# 文档有更多内容;在 C# 中,命名参数在 4.0 中才引入,语法类似于 F# 中属性的规范。但是,要记住,[仅仅是语法相似]意思完全不同,这一点很重要。
到目前为止,我们已经实现了应用程序的骨架,但它还不能做任何事,至少,对我们的数据来说,不能做任何事。在下一节,我们要充填代码中缺少的部分,绘制的图表,将图表显示在 PictureBox 控件 boxChart 中。