首页 > 代码库 > 第八章 用户界面(一)

第八章 用户界面(一)

第八章 用户界面(一)

 

在这一章,我们将学习程序员最重要的任务之一:把像素放到屏幕上艺术。在 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