首页 > 代码库 > 第八章 用户界面(一)
第八章 用户界面(一)
第八章 用户界面(一)
在这一章,我们将学习程序员最重要的任务之一:把像素放到屏幕上艺术。在 F# 中,所要做的就是调用库和 API,在这方面有很多选择,因为有了 .NET 平台,就更加多样。第一个选择是要确定是想创建桌面应用程序,它运行在本地,用一系列窗口和控件把信息显示给用户;还是想创建网站应用程序,应用程序的界面用 HTML 定义,然后,通过浏览器渲染。
用 .NET 创建桌面应用程序,可以有四种选择:WinForms、Windows Presentation Foundation (WPF,[ 原文这里有笔误 ])、GTK# 和 DirectX。在这一章,我们将学习 WinForms、WPF 和GTK#,但不包括 DirectX。WinForms、WPF 和GTK# 对窗口和控件有基本相同的隐喻(metaphors)[ 不懂说的是什么 ]。WinForms 最古老也最简单,我们已经遇到过几个WinForms 的示例了;WPF 是一个新的库,比WinForms 稍微复杂一些,但是,也更一致,提供了更多的功能,包括令人印象深刻的3D 图形;GTK# 提供了比其他两个库更好的平台。因为有了 Mono,现在WinForms 可以运行在所有平台上,Mono 团队建议 GTK# 运行在非 Windows 平台。DirectX 的目标是想得到更快 3D 图形的游戏制造商;WPF [ 同误 ] 提供了更简单的产生 3D 图形的方法,因此,本书不讨论DirectX。
要创建网站应用程序,可以使用 ASP.NET 框架,它提供了简单的方法创建基于服务器的、动态 HTML 的应用程序。ASP.NET 提供了灵活的方式产生 HTML,以响应来自浏览器的 HTML 请求。近年来,网站应用程序已经有了极大地进步,因此,ASP.NET 平台的进步也毫不为奇。ASP.NET 中增加了ASP.NET AJAX、ASP.NET MVC 和 Silverlight,作为竞争对手的平台Mono Rail,也有了很大的进步。然而,本书并不讨论这些事情,因为我们的目标只是为了展示用 F# 生成 HTML 的基础知识。
本章将要学习的每个主题,已经写在全书中了,因此,这里只关注一些基本知识。无论选择哪种技术创建用户界面,都需要在之前花点时间学习如何使用库。
介绍WinForm
WinForms 是包含在 System.Drawing.dll和 System.Forms.Windows.dll 的类,编译所有的 WinForms 都需要添加对它们的引用。这个库是基于 System.Windows.Forms.Form 类的,它代表显示给用户的窗口。当创建这个类的实例时,本质上就可创建了一个新的窗口。然后,必须创建一个事件循环(event loop),它可以保证用户响应与窗口的交互;通过调用 System.Windows.Application.Run 方法,并把创建的窗体对象传递进去即可完成。通过设置属性、调用方法,可以控制窗体的外观。看下面的例子:
open System.Drawing
openSystem.Windows.Forms
//create a new form
let form = new Form(BackColor = Color.Purple, Text = "IntroducingWinForms")
//show the form
Application.Run(form)
[
1、需要引用 System.Drawing,System.Windows.Forms;
2、交互运行用 form.Show()
以下同
]
这个例子不能运行在 F# 的交互模式 fsi 下,是因为不能从 fsi 启动事件循环。为了能在 fsi 下运行,有一个简单的方法,调用窗体的 Show 方法,或设置窗体的 Visible 为 true。看下面的代码:
open System.Drawing
open System.Windows.Forms
let form = newForm(BackColor=Color.Purple,Text="IntroducingWinForms",Visible=true);;
不论哪种方法,都可以动态地和窗体进行交互。例如:
> form.Text <- "Dynamic!!!";;
使用 WinForms,有两种可选的方案:自己画窗体,或者使用控件构建。下面我们首先看一下如何自己画窗体,然后再尝试用控件。
画 WinForm
自己画窗体,就要负责把像素表示到屏幕上。这种低级方法对许多 F# 用户仍有吸引力,因为他们相信许多 WinForms 库中控件不可能非常适合显示一些数据结构,或函数和算法的结果。然而,要注意这种方法会耗费大量时间,通常更多地花在寻找抽象表现逻辑的图形库上。
为了画 WinForm,需要将事件处理程序附加到窗体或控件的 Paint 事件上。就是说,每次 Windows 请求绘制窗体时,函数都会调用。传递到此函数的事件参数有一个属性 Graphics,它包含一个类的实例,也叫 Graphics,这个类的方法(如 DrawLine)可以在窗体上绘制像素。下面的例子显示一个简单的窗体,在上面画了一个饼:
let form =
// create a new form setting the minimum size
let temp = new Form(MinimumSize = new Size(96, 96))
// repaint the form when it is resize
temp.Resize.Add (fun _ -> temp.Invalidate())
// a brush to provide the shapes color
let brush = new SolidBrush(Color.Red)
temp.Paint.Add (fun e ->
// calculate the width and height of the shape
let width, height = temp.Width - 64, temp.Height - 64
// draw the required shape
e.Graphics.FillPie (brush, 32, 16, width,height, 0, 290))
// return the form to the top level
temp
Application.Run(form)
// form.Show()
可以在图 8-1 中看到这个窗体,即代码的运行结果。
图 8-1. 包含饼图的 WinForm
这个图像的大小和窗体相关,因此,无论什么时候改变窗体大小时,必须告诉窗体必须重画自己,这是通过把事件处理函数附加到 Resize 事件实现的。在这个函数中,调用窗体的 Invalidate 方法,告诉窗体需要重画自己。
现在,我们再看一个更完整的例子。假设我们想创建一个窗体,显示 Tree 类型,在下面的代码中定义,显示结果如图 8-2。
//The tree type
type ‘a Tree =
|Node of ‘a Tree * ‘a Tree
|Leaf of ‘a
//The definition of the tree
let tree =
Node(
Node(
Leaf "one",
Node(Leaf "two", Leaf"three")),
Node(
Node(Leaf "four", Leaf"five"),
Leaf "six"))
图 8-2. 显示树型结构的 WinForm
可以用清单8-1 的代码画出这个树。
清单 8-1 画树
open System
open System.Drawing
openSystem.Windows.Forms
//The tree type
type ‘a Tree =
|Node of ‘a Tree * ‘a Tree
|Leaf of ‘a
//The definition of the tee
let tree =
Node(
Node(
Leaf "one",
Node(Leaf "two", Leaf"three")),
Node(
Node(Leaf "four", Leaf"five"),
Leaf "six"))
// Afunction for finding the maximum depth of a tree
let getDepth t =
let rec getDepthInner t d =
match t with
| Node (l, r) ->
max
(getDepthInner l d + 1.0F)
(getDepthInner r d + 1.0F)
| Leaf x -> d
getDepthInner t 0.0F
//Constants required for drawing the form
let brush = new SolidBrush(Color.Black)
let pen = new Pen(Color.Black)
let font = new Font(FontFamily.GenericSerif, 8.0F)
// auseful function for calculating the maximum number
//of nodes at any given depth
let raise2ToPower (x : float32) =
Convert.ToSingle(Math.Pow(2.0,Convert.ToDouble(x)))
let drawTree (g : Graphics) t =
// constants that relate to the size and position
// of the tree
let center = g.ClipBounds.Width / 2.0F
let maxWidth = 32.0F * raise2ToPower (getDepth t)
// function for drawing a leaf node
let drawLeaf (x : float32) (y : float32) v =
let value = http://www.mamicode.com/sprintf "%A" v
let l = g.MeasureString(value, font)
g.DrawString(value, font, brush, x -(l.Width / 2.0F), y)
// draw a connector between the nodes when necessary
let connectNodes (x : float32) y p =
match p with
| Some(px, py) -> g.DrawLine(pen, px,py, x, y)
| None -> ()
// the main function to walk the tree structure drawingthe
// nodes as we go
let rec drawTreeInner t d w p =
let x = center - (maxWidth * w)
let y = d * 32.0F
connectNodes x y p
match t with
| Node (l, r) ->
g.FillPie(brush, x - 3.0F, y - 3.0F, 7.0F,7.0F, 0.0F, 360.0F)
let d = (d + 1.0F)
drawTreeInner l d (w + (1.0F / d))(Some(x, y))
drawTreeInner r d (w - (1.0F / d))(Some(x, y))
| Leaf v -> drawLeaf x y v
drawTreeInner t 0.0F 0.0F None
//create the form object
let form =
let temp = new Form(WindowState = FormWindowState.Maximized)
temp.Resize.Add(fun _ -> temp.Invalidate())
temp.Paint.Add
(fun e ->
e.Graphics.Clip <-
new Region(new Rectangle(0, 0, temp.Width,temp.Height))
drawTree e.Graphics tree)
temp
form.Show()
//Application.Run(form)
注意如何定义函数 drawTree,它有两个参数:Graphics 对象和需要画的树:
let drawTree (g : Graphics) t =
这是画 WinForm 的通常模式,创建一个函数,参数为 Graphics 对象和需要画的数据类型,这样,函数可以很容易被其他窗体和控件重用。
为了实现 drawTree,首先计算出这个函数的两个常量center 和 maxWidth。不能在函数 drawTree 的外边看到这些常量,这样在函数 drawTree 的内部使用,而不必须作为参数来传递:
// constants that relate to the size andposition
// of the tree
let center = g.ClipBounds.Width / 2.0F
let maxWidth = 32.0F * raise2ToPower(getDepth t)
实现函数的其余部分,是通过把任务分解成许多内部函数。定义 drawLeaf 去画叶结点:
// function for drawing a leaf node
let drawLeaf (x : float32) (y : float32) v=
letvalue = http://www.mamicode.com/any_to_string v
letl = g.MeasureString(value, font)
g.DrawString(value,font, brush, x - (l.Width / 2.0F), y)
用 connectNodes 在适当的地方画结点之间的连接线:
// draw a connector between the nodes whennecessary
let connectNodes (x : float32) y p =
matchp with
|Some(px, py) -> g.DrawLine(pen, px, py, x, y)
|None -> ()
最后,定义一个递归函数 drawTreeInner,实际完成遍历这个 Tree 类型,并画出来:
// the main function to walk the treestructure drawing the
// nodes as we go
let rec drawTreeInner t d w p =
letx = center - (maxWidth * w)
lety = d * 32.0F
connectNodesx y p
matcht with
|Node (l, r) ->
g.FillPie(brush,x - 3.0F, y - 3.0F, 7.0F, 7.0F, 0.0F, 360.0F)
letd = (d + 1.0F)
drawTreeInnerl d (w + (1.0F / d)) (Some(x, y))
drawTreeInnerr d (w - (1.0F / d)) (Some(x, y))
|Leaf v -> drawLeaf x y v
该函数使用参数保存递归调用期间的值,可以知道,因为这是内部函数,不会被外边程序初始化成不正确的值而滥用,在外边根本看不到它。隐藏参数保存递归调用期间的临时值,是函数编程的另一个常用模式。
在某种程度上,这个画树的函数还是令人满意的,仅用了简短的 86 行 F# 代码,就提供了树不错的层次视图。然而,这种方法只能用于小规模的图形。当画更为复杂的图形时,代码数就会迅速膨胀,规则出所有的几何图形是非常耗时的。为了有效地控制复杂性,F# 可以使用控件,我们在下一章讨论。
为了更好地在 WinForm 上画图,应该了解 System.Drawing.dll 中的 System.Drawing 命名空间。还应该注意两个方面,第一,需要学习如何使用 Graphics 对象,特别是重载前缀为 Draw 或 Fill 的方法。表 8-1 中汇总了Graphics 对象的方法。
表 8-1 System.Drawing.Graphics 对象中的重要方法
方法名 | 描述 |
DrawArc | 画椭圆的一部分 |
DrawBezier | 绘制贝塞尔曲线,这条曲线由两个端点和两个控制曲线角度的自由点表示 |
DrawCurve | 画曲线,由点数组定义 |
DrawClosedCurve | 画封闭曲线,由点数组定义 |
DrawEllipse | 画椭圆,由矩形或点的矩形集合表示 |
DrawPie | 画椭圆的一部分,用矩形和两条射线表示,说明起止角度 |
DrawLine | 画线,用两点 |
DrawLines | 画一组线,用点数组 |
DrawPolygon | 画多边形,是一组封闭直线集合,用点数组 |
DrawRectangle | 画矩形,用座标和宽、高表示 |
DrawRectangles | 画一组矩形,用矩形数组表示 |
FillClosedCurve | 画实心的封闭曲线,用点数组表示 |
FillEllipse | 画实心椭圆,由矩形、矩形点集合表示 |
FillPie | 画实心椭圆的一部分,用矩形和两条射线表示,说明起止角度 |
FillPolygon | 画实心多边形,由封闭线条构成,用点数组表示 |
FillRectangle | 画实心矩形,用座标和宽、高表示 |
FillRectangles | 画一组实心矩形,用矩形数组表示 |
DrawIcon | 画图标,由 System.Drawing.Icon 类型指定 |
DrawImage | 画图,由 System.Drawing.Image 类型指定 |
DrawImageUnscaled | 画不能缩放的图,由 System.Drawing.Image 类型指定 |
DrawString | 画字符串 |
MeasureString | 测量字符串,因此可以计算出放在图形中的位置 |
DrawPath | 画 System.Drawing.Drawing2D.GraphicsPath 表示的轮廓。 这个类可以向其中添加前边描述的几何结构,比如:圆弧、矩形、椭圆和多边形,以节省每次重新计算的时间(这对于想画一些复杂但完全固定的图形非常有用) |
FillPath | 同上面的 DrawPath 功能,但是实心图形 |
第二个需要关注的部分是与 System.Drawing.Graphics 对象密切相关的 System.Drawing的命名空间,需要学习如何使用Graphics 对象的方法创建 Icon、Image、Pen 和 Brush 对象。表 8-2 展示了如何通过各自的构造函数创建这些对象。
表 8-2 使用System.Drawing.Graphics 对象创建对象
代码段 | 描述 |
Color.FromArgb(33, 44, 55) | 用 红、绿、蓝组件创建颜色 |
Color.FromKnownColor(KnownColor.Crimson) | 用枚举 KnownColor 的成员颜色 |
Color.FromName("HotPink") | 用名字,以字符串形式创建颜色 |
new Font(FontFamily.GenericSerif, 8.0f) | 创建一个新的 generic serif 字体、8 点高 |
Image.FromFile("myimage.jpg") | 用文件创建图片 |
Image.FromStream(File.OpenRead ("myimage.gif")) | 用流创建图片 |
new Icon("myicon.ico") | 用文件创建图标 |
new Icon(File.OpenRead("myicon.ico")) | 用流创建图标 |
new Pen(Color.FromArgb(33, 44, 55)) | 创建有颜色的笔,可以画线 |
new Pen(SystemColors.Control, 2.0f) | 用颜色、2 个像素宽度创建笔,可以画线 |
new SolidBrush(Color.FromName("Black")) | 创建实心笔刷,能够画实心图形 |
new TexturedBrush(Image.FromFile ("myimage.jpg")) | 用图片创建纹理笔刷,用图片充填图形 |
如果你喜欢使用标准对象,可以使用 System.Drawing 命名空间中的几个类,它预定义了一些对象,包括:Brushes、Pens、SystemBrushes、SystemColors、SystemFonts、SystemIcons、SystemPens。下面简单例子就说明了如何使用这些预定义的对象:
open System.Drawing
let myPen = Pens.Aquamarine
let myFont = SystemFonts.DefaultFont