In this chapter, you will learn about one of the most common tasks a programmer needs to perform: the art of putting pixels on the screen. In F#, this is all about the libraries and APIs that you call, and you have a lot of choices in this area, with more emerging as the .NET platform involves. The first choice you need to make is whether you want to build a desktop application; an application that runs locally and uses a series of windows and controls to display information to the user; or a web application, where you define the application's interface in HTML, which is then rendered by a browser.
You have four GUI library choices when creating desktop applications in .NET: WinForms, Windows Presentation Foundation (WFP), GTK#, and DirectX. In this chapter, you'll learn about WinForms, WFP and GTK#, but not DirectX. WinForms, WFP, and GTK# have the same basic metaphors of windows and controls. WinForms is the oldest and simplest, and you've already met a few WinForms examples. WFP is a new library; it's slightly more complex than WinForms, but it's also more consistent and offers more features, including impressive 3D graphics. GTK# offers much better platform support than the other two libraries. WinForms now runs on all platforms, thanks to the Mono implementation, and GTK# is recommended by the Mono team for non-Windows platforms. DirectX is mainly targeted at game producers who want fast 3D graphics. WFP offers a simpler way to produce 3D graphics, so this book won't cover DirectX.
To create a web application, you can use the ASP.NET framework, which gives you a simple way to create server-based dynamic HTML applications. ASP.NET provides a flexible way for you to generate HTML in response to a HTTP request from a browser. Web applications have evolved greatly in recent years; not surprisingly, the ASP.NET platform has evolved greatly alongside it. ASP.NET has added ASP.NET AJAX, ASP.NET MVC, and Silverlight, and a rival platform called Mono Rail has also emerged. However, this book doesn't cover these things because the aim is to show off the basics of generating HTML using F#.
Whole books have been written on each of the topics you'll learn about in this chapter, so be aware that this chapter focuses mainly on the basics. Whichever technology you choose to create your UIs, you're probably going to have to invest some time learning how the library works before you can create great UIs.
The WinForms classes are contained in the System.Drawing.dll
and System.Forms.Windows.dll
, and you will need to add references to these to compile all of the WinForms examples. The libraries are based on the System.Windows.Forms.Form
class, which represents a window that you show to the user. You essentially create a new window when you create an instance of this class. You must then create an event loop, a way of ensuring that you respond to user interactions with the window. You do this by calling the System.Windows.Forms.Application.Run
method and passing it the form object you created. You can control the look of the form by setting its properties and calling its methods. The following example demonstrates how to do this:
open System.Drawing open System.Windows.Forms // create a new form let form = new Form(BackColor = Color.Purple, Text = "Introducing WinForms") // show the form Application.Run(form)
This example will not work with F# interactive, fsi
, because you cannot start an event loop from within fsi
. To work with forms in fsi
, you call the form's Show
method or set the form's Visible
property to true
. This example shows the second technique:
> open System.Drawing open System.Windows.Forms let form = new Form(BackColor=Color.Purple, Text="Introducing WinForms", Visible=true);;
Either way, you can dynamically interact with your form object:
> form.Text <- "Dynamic !!!";;
When working with WinForms, you can take one of two approaches: draw the forms yourself or use controls to build them. You'll begin by drawing your own forms, and then move on to using controls.
Drawing your own forms means that you take responsibility for the pixels that appear on the screen. This low-level approach might appeal to those F# users who believe that many controls that come with the WinForms library are not perfectly suited to displaying data structures or the results of functions and algorithms. However, be warned that this approach can be time-consuming, and your time is usually better spent looking for a graphics library that abstracts away some of the presentation logic.
To draw a WinForm, you attach an event handler to the Paint
event of the form or the control. This means that your function is called every time Windows requests the form to be drawn. The event argument that is passed into this function has a property called Graphics
, which contains an instance of a class also called Graphics
. This class has methods (such as DrawLine
) that allow you to draw pixels on the form. The following example shows a simple form where you draw a pie on it:
open System.Drawing open System.Windows.Forms 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, 32, width, height, 0, 290)) // return the form to the top level temp Application.Run(form)
You can see the form that results from executing this code in Figure 8-1.
This image is linked to the size of the form, so you must tell the form to redraw itself whenever the form is resized. You do this by attaching an event-handling function to the Resize
event. In this function, you call the form's Invalidate
method, which tells the form that it needs to redraw itself.
Now let's look at a more complete WinForms example. Imagine you want to create a form to display the Tree
type defined in the next code example (see Figure 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"))
You can draw this tree with the code shown in Listing 8-1.
Example 8.1. Drawing a Tree
open System open System.Drawing open System.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")) // A function 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) // a useful 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 = 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 drawing the // 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 Application.Run(form)
Note how you define a function, drawTree
. This function has two parameters, the Graphics
object and the tree you want to draw:
let drawTree (g : Graphics) t =
This is a common pattern when drawing WinForms. Creating a function that takes the Graphics
object and a data type to be drawn allows different forms and controls to reuse the function easily.
To implement drawTree
, you first calculate a couple of constants the function will use: center
and maxWidth
. Functions outside drawTree
can't see these constants, so you can use them in drawTree
's inner functions without having to pass them around as parameters:
// 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)
You implement the rest of the function by breaking it down into inner functions. You define drawLeaf
to take care of drawing leaf nodes:
// function for drawing a leaf node let drawLeaf (x : float32) (y : float32) v = let value = sprintf "%A" v let l = g.MeasureString(value, font) g.DrawString(value, font, brush, x - (l.Width / 2.0F), y)
You use connectNodes
to take care of drawing the connections between nodes, where appropriate:
// 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 -> ()
Finally, you define drawTreeInner
as a recursive function that does the real work of walking the Tree
type and drawing it:
// the main function to walk the tree structure drawing the // 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
This function uses parameters to store values between recursive calls. Because this is an inner function, you know that the outside world cannot misuse it by initializing it incorrectly; this is because the outside world cannot see it. Hiding parameters to store working values between recursive function calls is another common pattern in functional programming.
In some ways, this tree-drawing function is satisfactory: it gives a nice hierarchical overview of the tree in a fairly concise 86 lines of F# code. However, this approach scales only so far. As you draw more complicated images, the number of lines of code can grow rapidly, and working out all the geometry can become time-consuming. To help manage this complexity, F# lets you use controls, as you'll learn in the next section.
To make the most of drawing on WinForms, you should get to know the System.Drawing
namespace contained in System.Drawing.dll
. You should also concentrate on two areas. First, you need to learn how to use the Graphics
object, particularly its overloads of methods prefixed with either Draw
or Fill
. Table 8-1 provides a helpful summary of the methods on the Graphics object.
Table 8.1. Important Methods on the System.Drawing.Graphics
Object
Method Name | Description |
---|---|
| Draws a portion of an ellipse |
| Draws a Bézier spline, which is a curve represented by two endpoints and two free-floating points that controll the angle of the curve |
| Draws a curved line defined by an array of points |
| Draws a closed curved line defined by an array of points |
| Draws the outline of an ellipse, which you represent with a rectangle or rectangular set of points |
| Draws a portion of the outline of an ellipse, which you represent with a rectangle and two radial lines that illustrate the start and finish angles |
| Draws a single line from two points |
| Draws a set of lines from an array of points |
| Draws the outline of a polygon, which is a closed set of lines from an array of points |
| Draws the outline of a rectangle, which you represent with a coordinate, as well as its width and height |
| Draws the outline of a set of rectangles from an array of rectangles |
| Draws a solid closed curve defined by an array of points |
| Draws a solid ellipse, which you represent with a rectangle or rectangular set of points |
| Draws a portion of a solid ellipse, which you represent with a rectangle and two radial lines that show the start and finish angles |
| Draws a solid polygon, which is a closed set of lines from an array of points |
| Draws a solid rectangle, which you represent with a coordinate and its width and height |
| Draws a solid set of rectangles from an array of rectangles. |
| Draws an image specified by the |
| Draws an image specified by the |
| Draws an image specified by the |
| Draws a string of characters |
| Gives the dimensions of a string of characters, so you can calculate where you want to place it on the image |
| Draws an outline, which you represent with the |
| Provides the same functionality as |
The second area you need to concentrate on in the System.Drawing
namespace is closely related to the System.Drawing.Graphics
object; you need to learn how to create the Icon, Image, Pen
, and Brush
objects used by the methods of the Graphics object. Table 8-2 shows examples of how to create these objects via their constructors.
Table 8.2. Creating Objects Used with the System.Drawing.Graphics
Object
Snippet | Description |
---|---|
| Creates a color from its red, green, and blue components |
| Creates a color from a member of the |
| Creates a color from its name in string form |
| Creates a new generic serif font that is eight points tall |
| Creates a new image from a file |
| Creates a new image from a stream |
| Creates a new icon from a file |
| Creates a new icon from a stream |
| Creates a colored pen that you can use to draw lines |
| Creates a pen, that you can use to draw lines, from a color and with a width of 2 pixels |
| Creates a solid brush that you can use to draw filled shapes |
| Creates a new textured brush from an image and draws a filled shape with an image mapped across it |
If you prefer to use standard objects, you can use several classes in the System.Drawing
namespace that contain predefined objects, including Brushes, Pens, SystemBrushes, SystemColors, SystemFonts, SystemIcons
, and SystemPens
. The following quick example illustrates how to use these predefined objects:
open System.Drawing let myPen = Pens.Aquamarine let myFont = SystemFonts.DefaultFont
A control is simply a class that derives from System.Windows.Forms.Control
. You can display any class that derives from this class in a form by adding it to the Controls
collection on the form object.
Next, you'll look at a way to draw the tree using controls. The WinForms library defines a TreeView
class, which exists specifically for displaying tree-like structures; obviously you use this control to display the tree. To use TreeView
, you create an instance of it and configure it by setting its properties and calling its methods. Most importantly, you add the nodes you want to display to its Nodes
collection. Once the control is ready to be displayed, you add it to the form's Controls
collection.
The TreeView
class uses TreeNode
objects to represent nodes, so you define the function mapTreeToTreeNode
to walk the tree structure recursively and create a TreeNode
graph. The program in Listing 8-2 produces the tree in Figure 8-3.
Example 8.2. Drawing a Tree via a TreeView
Control
open System.Windows.Forms // 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")) // A function to transform our tree into a tree of controls let mapTreeToTreeNode t = let rec mapTreeToTreeNodeInner t (node : TreeNode) = match t with | Node (l, r) -> let newNode = new TreeNode("Node") node.Nodes.Add(newNode) |> ignore mapTreeToTreeNodeInner l newNode mapTreeToTreeNodeInner r newNode | Leaf x -> node.Nodes.Add(new TreeNode(sprintf "%A" x)) |> ignore let root = new TreeNode("Root") mapTreeToTreeNodeInner t root root // create the form object let form = let temp = new Form() let treeView = new TreeView(Dock = DockStyle.Fill) treeView.Nodes.Add(mapTreeToTreeNode tree) |> ignore treeView.ExpandAll() temp.Controls.Add(treeView) temp Application.Run(form)
This code is about half the length of Listing 8-1, where you drew the tree yourself. It is also more functional because it allows you to fold away parts of the tree that don't interest you. This greatly improves the size of tree that you display in a manageable way.
In this example, you use the dock style to control how the control looks. You do this by setting the control's Dock
property with a member of the DockStyle
enumeration. A docked control takes up as much space as is available in the portion of the form that contains it. For example, you can dock a control on the left side if you use DockStyle.Left
, on the right side if you use DockStyle.Right
, at the top if you use DockStyle.Top
, on the bottom if you use DockStyle.Bottom
, and on the whole form if you use DockStyle.Fill
. This is great when you have only a few controls because it creates a nice, dynamic effect where the controls are resized when the user resizes the form. However, this approach doesn't work well with a lot of controls because it is difficult to get many controls to fit together nicely using this technique. For example, if you have two controls that are docked to the left, it can be confusing to determine which one is supposed to be the leftmost one and how much of the left side they should both take up. If you have a lot of controls, a better solution is to control their layout explicitly using the Top
and Left
properties. You can create a dynamic effect by using the Anchor
property to anchor the control to the edge of the containing form. The following example creates a form with a single textbox on it; this form grows or shrinks as the user resizes the form:
open System open System.Windows.Forms let form = // create a form let temp = new Form()
// create a text box and set its anchors let textBox = new TextBox(Top=8,Left=8, Width=temp.Width - 24, Anchor = (AnchorStyles.Left ||| AnchorStyles.Right ||| AnchorStyles.Top)) // add the text box to the form and return the form temp.Controls.Add(textBox) temp [<STAThread>] do Application.Run(form)
Unfortunately, this method of working with controls is not always satisfactory. Here you displayed only one control, but often you want to display tens (or even hundreds) of controls on a form. Writing all the code to create and configure the controls can quickly become tedious and error-prone. To help you get around this, Visual Studio provides some form designers that allow you to create forms visually. However, no designer exists for F# at this time, so the next section will explain how to use F# to work with forms created with the C# designer.
One of the difficulties facing the WinForms programmer when working with controls is that there are many controls from which to choose. This chapter covers only one control. Unfortunately, there's no substitute for experience when it comes to learning what works. The MSDN library (http://msdn.microsoft.com
) provides an excellent reference, but the volume of information in that library can be a little off-putting for those new to the subject. Table 8-3 flattens out this learning curve slightly by summarizing some of the most useful controls.
Table 8.3. Common WinForm Controls and How to Use Them
Control | Description |
This control to displays text information to the user; most other controls should be accompanied by a | |
| This control provides a box for entering text. The default is a single line of text, but you can change this to support multiple-line entry if you set the |
This control is a textbox similar in a lot of respects to the previous control; it allows you limit the data a user can enter by setting the | |
This control provides a button for the user to click; as with the | |
This control's name is slightly misleading. You don't really use this as a label, but as a type of button that looks like an HTML link. This control works great for users used to a web environment; it also lets you indicate that clicking the button will open a web page. | |
This control displays a box for the users to check if you have a set of options that are not mutually exclusive. | |
This control behaves a lot like a | |
This control allows the user to pick a date via a drop-down calendar. | |
This control allows a user to pick a date from a calendar that is permanently on display. | |
This control allows a user to make a selection from a drop-down list; this is great for showing a dynamic set of data via data binding (see Chapter 9 for more details on this). | |
This control is similar to a | |
This control provides an excellent way to display information from a database table, although you can use it to display any kind of tabular data. You should always choose this option over the older | |
This control is also great for showing dynamic data; however, it is most useful for displaying data in a tree-like form. | |
This control gives your users feedback about any long-running activity that is vital for a usable application. | |
This control provides a way to display and edit rich text documents, which is useful if your users want a little more formatting than what the standard textbox offers. | |
This control displays HTML documents; it's useful because a lot of information is available in HTML format. | |
This control breaks your form into different sections; this is highly effective when you combine the control with | |
| This control is a horizontal scroll bar; you can use it to fit more information on a |
| This control is a vertical scroll bar; you use it to fit more information on a |
| This control is a form that uses a series of tabs to display user controls. |
F# does not yet have a form designer of its own; however, thanks to the great interoperability of .NET, it is easy to use forms created with the Visual Studio's form designer in F#. You have two approaches. First, you can create an F# library and call functions from this library in your Windows form. Second, you can create a library of forms and use them from your F# application. You learn both approaches in this chapter, as well as their comparative strengths and weaknesses. Both examples will rely on the same Fibonacci calculator (see Figure 8-4).
This book is about F#, and you don't need knowledge of any other programming language for the majority of its material. However, this topic requires that you understand a little of bit about another .NET programming language: C#. Specifically, you'll see two short C# listings in this section. You can easily replace the C# code with Visual Basic .NET code if you feel more comfortable with that language.
Your main consideration in creating an F# library that you can use from a form: You want to make it easy to use that library from your form. In this case, you'll create a function to calculate the Fibonacci number, which takes and returns an integer. This makes things simple because a form has no problem using the .NET integer type. You want the library to be reasonably efficient, so you also want to create a lazy list of Fibonacci numbers and define a function that can get
the nth number:
module Strangelights.Fibonacci // an infinite sequence of Fibonacci numbers let fibs = (1,1) |> Seq.unfold (fun (n0, n1) -> Some(n0, (n1, n0 + n1))) // a function to get the nth fibonacci number let get n = Seq.nth n fibs
It's easy to use this function from a form; you just need to reference your F# .dll
from the Visual Studio form project. You can use the module Strangelights.Fibonacci
by opening the Strangelights
namespace and treating Fibonacci
as if it were a class in C#. The following example shows you how to call the function in C# and place the result in a control. Note that this form was created with Visual Studio 2005, so the control definitions are in a separate source file:
using System; using System.Windows.Forms; using Strangelights; namespace CSApp { public partial class FibForm : Form { public FibForm() { InitializeComponent(); } private void calculate_Click(object sender, EventArgs e) { // convert input to an integer int n = Convert.ToInt32(input.Text); // caculate the apropreate fibonacci number n = Fibonacci.get(n); // display result to user result.Text = n.ToString(); } } }
If you want to use the form created in C# from F#, you need to expose certain controls as properties. You don't need to expose all of the controls—just the ones that you want to interact with from F#. The following example shows how to do this in C#; again, any designer-generated code is hidden in a separate file:
using System; using System.Windows.Forms; namespace Strangelights.Forms { public partial class FibForm : Form { // public constructor for the form public FibForm() { InitializeComponent(); } // expose the calculate button public Button Calculate { get { return calculate; } } // expose the results label public Label Result { get { return result; } }
// expose the inputs text box public TextBox Input { get { return input; } } } }
It is then very straightforward to reference the C# .dll
from F# and create an instance of the form and use it. The following example demonstrates the code you use to do this:
open System.Windows.Forms open Strangelights.Forms // an infinite sequence of Fibonacci numbers let fibs = (1,1) |> Seq.unfold (fun (n0, n1) -> Some(n0, (n1, n0 + n1))) // a function to get the nth fibonacci number let getFib n = Seq.nth n fibs let form = // create a new instance of the form let temp = new FibForm() // add an event handler to the form's click event temp.Calculate.Click.Add (fun _ -> // convert input to an integer let n = int temp.Input.Text // caculate the apropreate fibonacci number let n = getFib n // display result to user temp.Result.Text <- string n) temp Application.Run(form)
You can use both techniques to produce similar results, so the question remains: which is best to use for which occasions? The problem with a C# form calling F# is that you will inevitably end up writing quite a bit of C# to glue everything together. It can also be difficult to use some F# types, such as union types, from C#. Given these two facts, I generally create a C# forms library and use this from F#. You can learn more about making F# libraries ready for use with other .NET languages in Chapter 14.
Chapter 7 introduced the Event
module, which can be useful when working with events in WinForms. When working with events in a WinForm, you often encounter cases where no event exactly fits what you want. For example, the MouseButton
event is raised when either the left or right mouse button is clicked, but you might want to respond only to the click of the left-mouse button. In this case, you might find it useful to use the Event.filter
function to create a new event that responds only to a click of the leftmouse
button. The next example demonstrates how to do this:
open System.Windows.Forms let form = // create a new form let temp = new Form() // subscribe the mouse click event filtering so it only // reacts to the left button temp.MouseClick |> Event.filter (fun e -> e.Button = MouseButtons.Left) |> Event.add (fun _ -> MessageBox.Show("Left button") |> ignore) // return the form temp Application.Run(form)
Here you use the filter
function with a function that checks whether the left-mouse button is pressed; the resulting event is then piped forward to the listen
function that adds an event handler to the event, exactly as if you had called the event's .Add
method. You could have implemented this using an if
expression within the event handler, but this technique has the advantage of separating the logic that controls the event firing from what happens during the event itself. Several event handlers can reuse the new event, depending on your needs.
Listing 8-3 demonstrates how to use more of Event
's functions to create a simple drawing application (see Figure 8-5). Here you want to use the MouseDown
event in different ways: first, you use it to monitor whether the mouse is pressed at all; second, you use it to split the event into left- or right-button presses using the Event.partition
function. You can use this to control the drawing color, whether red or black:
Example 8.3. Using Events to Implement a Simple Drawing Application
open System open System.Drawing open System.Windows.Forms let form = // create the form let temp = new Form(Text = "Scribble !!")
// some refrence cells to hold the applications state let pointsMasterList = ref [] let pointsTempList = ref [] let mouseDown = ref false let pen = ref (new Pen(Color.Black)) // subscribe to the mouse down event temp.MouseDown.Add(fun _ -> mouseDown := true) // create a left mouse down and right mouse down events let leftMouse, rightMouse = temp.MouseDown |> Event.partition (fun e -> e.Button = MouseButtons.Left) // use the new left and right mouse events to choose the color leftMouse.Add(fun _ -> pen := new Pen(Color.Black)) rightMouse.Add(fun _ -> pen := new Pen(Color.Red)) // the mouse up event handler let mouseUp _ = mouseDown := false if List.length !pointsTempList > 1 then let points = List.toArray !pointsTempList pointsMasterList := (!pen, points) :: !pointsMasterList pointsTempList := [] temp.Invalidate() // the mouse move event handler let mouseMove (e: MouseEventArgs) = pointsTempList := e.Location :: !pointsTempList temp.Invalidate() // the paint event handler let paint (e: PaintEventArgs) = if List.length !pointsTempList > 1 then e.Graphics.DrawLines (!pen, List.toArray !pointsTempList) !pointsMasterList |> List.iter (fun (pen, points) -> e.Graphics.DrawLines(pen, points))
// wire up the event handlers temp.MouseUp |> Event.add mouseUp temp.MouseMove |> Event.filter(fun _ -> !mouseDown) |> Event.add mouseMove temp.Paint |> Event.add paint // return the form object temp [<STAThread>] do Application.Run(form)
You can also publish events you create in this way on the form's interface, which means the code that consumes the form can also take advantage of these events.
Again, a big problem facing a programmer working with events in WinForms is the volume of events available, which can make choosing the right one difficult. Perhaps surprisingly, most events are defined on the class Control
, with each specialization providing only a handful of extra events. This generally makes life a bit easier because, if you have used an event with a control, odds are it will also be available on another control. Table 8-4 provides a helpful summary of the most common events on the Control
class:
Table 8.4. A Summary of Events on the Control Class
Event | Description |
| This event is caused by the user clicking the control. It is a high-level event, and although it is ordinarily caused by the user clicking with the mouse, it might also be caused by the user pressing Enter or the spacebar when hovering over a control. A series of events— |
| This event is raised when a user clicks the mouse twice in quick succession. Note that the user's operating system settings determine the interval the two clicks must occur in. You need to be careful when handling this event because every time this event is raised, a |
| This event is raised when the control becomes active—either because the user presses Tab to enter it, the programmer calls |
| This event is raised when the control is deactivated—either because the user presses Tab to leave it, the programmer calls |
| This event is part of a sequence of events that you can use to get detailed information about the state of the keyboard. To get details about when a key is first pressed, use |
| This event is raised whenever a user moves the control. |
| This event is useful for finding out whether the mouse is hovering over a control; you can use it to give a user more information about the control. The events |
| This event occurs when the form will be repainted by Windows; you need to handle this event if you want to take care of drawing the control yourself. For more information about this, see the "Drawing WinForms" section earlier in this chapter. |
| This event occurs when the user resizes the form; you could handle this event to adjust the layout of the form to a new size. |
So far you've looked only at a script style of programming, where you use an existing form and controls to put forms together quickly. This style of programming is great for the rapid development of single-form applications, but it has some limitations in cases where you want to create applications composed of multiple forms quickly, or you want to create libraries of forms for use with other .NET languages. In these cases, you must take a more component-oriented approach.
Typically, you want to use some forms repeatedly when you create a large WinForms application; furthermore, you typically want these forms to be able to communicate with each other by adjusting each other's properties and calling each other's methods. You usually do this by defining a new form class that derives from System.Windows.Forms
. Listing 8-4 shows a simple example of this, using the class syntax introduced in Chapter 5.
Example 8.4. A Demonstration of Creating a New Type of Form
open System open System.Windows.Forms // a class that derives from "Form" and add some user controls type MyForm() as x = inherit Form(Width=174, Height=64) // create some controls to add the form let label = new Label(Top=8, Left=8, Width=40, Text="Input:") let textbox = new TextBox(Top=8, Left=48, Width=40) let button = new Button(Top=8, Left=96, Width=60, Text="Push Me!") // add a event to the button do button.Click.Add(fun _ -> let form = new MyForm(Text=textbox.Text) form.Show()) // add the controls to the form do x.Controls.Add(label) do x.Controls.Add(textbox) do x.Controls.Add(button) // expose the text box as a property member x.Textbox = textbox // create an new instance of our form let form = let temp = new MyForm(Text="My Form") temp.Textbox.Text <- "Next!" temp [<STAThread>] do Application.Run(form)
Executing the preceding code produces the forms shown in Figure 8-6.
In the preceding example, you create a form with three fields: label, textbox
, and button
. You can then manipulate these fields using external code. At the end of the example, you create a new instance of this form and then set the Text
property of the textbox
field.
Events can be exposed on the interface of a form in much the same way that fields can. This takes a little more work because of some inherent restrictions. The idea is to create a new event, store this event in a field in the class, and finally, to make this event a subscriber to the filtered event. You can see this demonstrated in the next example, where you filter the MouseClick
event to create a LeftMouseClick
:
open System.Windows.Forms // a form with addation LeftMouseClick event type LeftClickForm() as x = inherit Form() // create the new event let event = new Event<MouseEventArgs>() // wire the new event up so it fires when the left // mouse button is clicked do x.MouseClick |> Event.filter (fun e -> e.Button = MouseButtons.Left) |> Event.add (fun e -> event.Trigger e) // expose the event as property [<CLIEvent>] member x.LeftMouseClick = event.Publish
Forms you create in this component-based manner will undoubtedly be easier to use than forms you create with a more scripted approach; however, you still face pitfalls when you create libraries for other .NET languages. Please refer to Chapter 13 for more information about making F# libraries usable by other .NET languages.
WPF is a library that offers a completely new programming model for user interfaces. It is aimed at creating desktop applications that have more pizzazz than the ones that are created with WinForms. WPF also comes with a new XML-based language called XAML, which you can use to code the bulk of this form's layout, leaving you free to use F# to code the interesting parts of your application.
Several XAML designers now exist; these designers allow F# users to design their interface using a graphical WYSWIG tool and then add interactivity to it using F#. For example, Mobiform offers a designer called Aurora (www.mobiform.com/products/aurora/aurora.htm
), and Microsoft offers a designer called Expression Blend (www.microsoft.com/expression/products/overview.aspx?key=blend
).
WPF is part of .NET 3.0, and it installs by default if you use Windows Vista. Users of other versions of Windows need to install .NET 3.0 to get access to WPF; the easiest way to do this is to download the .NET Windows SDK for Windows Server 2008 and .NET Framework 3.5 (http://is.gd/521hd
). To make the examples in this section work, you need to add references to the following dlls: PresentationCore.dll, PresentationFramework.dll
, and WindowsBase.dll
.
The first example you'll look at shows you how to create a simple form in XAML and then display it to the user using F# (see Listing 8-5). This example shows the XAML definition of a form with four controls: two labels, a textbox, and a button.
Example 8.5. A Simple Form Created in XAML
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" > <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="64" /> <ColumnDefinition Width="128" /> <ColumnDefinition Width="128" /> <ColumnDefinition Width="128" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="24"/> </Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" >Input: </Label> <TextBox Name="input" Grid.Column="1" Text="hello" /> <Label Name="output" Grid.Row="0" Grid.Column="2" ></Label> <Button Name="press" Grid.Column="3" >Press Me</Button> </Grid> </Window>
To make this XAML definition of a form useful, you need to do two things. First, you must load the form's definition and show it to the user; however, doing only this will offer no interaction with the user. Thus, the second thing you need to do is make the form interactive. To do this, you use F# to add event handlers to the controls; in this case, you use it to add an event handler to the button that places the contents of the textbox into the second label. The function createWindow
is a general-purpose function for loading a XAML form. You then use this function to create the value window
, and you pass this value to the form's FindName
method to find the controls within the form, so you can interact with them. Finally, you create an instance of the Application
class in the main
function and use this to show the form (see Listing 8-6).
Example 8.6. Displaying the XAML Form and Adding Event Handlers to It
open System open System.Collections.Generic open System.Windows open System.Windows.Controls open System.Windows.Markup open System.Xml // creates the window and loads the given XAML file into it let createWindow (file : string) = using (XmlReader.Create(file)) (fun stream -> (XamlReader.Load(stream) :?> Window)) // create the window object and add event handler // to the button control let window = let temp = createWindow "Window1.xaml" let press = temp.FindName("press") :?> Button let textbox = temp.FindName("input") :?> TextBox let label = temp.FindName("output") :?> Label press.Click.Add (fun _ -> label.Content <- textbox.Text ) temp // run the application let main() = let app = new Application() app.Run(window) |> ignore [<STAThread>] do main()
To get this program to compile, you must add references to PresentationCore.dll, PresentationFramework.dll
, and WindowsBase.dll
, which you can usually find0 in the directory C:Program FilesReference AssembliesMicrosoftFrameworkv3.0
. In the other examples in this chapter, you didn't need to add references because the compiler referenced the libraries automatically. You can see the form created by the preceding example as in Figure 8-17.
Another great advantage of WPF is the huge number of controls it offers. One control that you'll dig a little deeper into is Viewport3D
, which offers the ability to create impressive 3D graphics, something not readily available with the WinForms library. You'll learn how you can use this control to display a 3D plane and then map an equation over it.
The example starts with the XAML script. Both XAML and 3D graphics are huge topics, and it would it exceed this book's ambitions to cover them fully. But you'll still learn enough about them to get an idea of what they involve, as well as how you might and branch off and explore these topics on your own. The following XAML script describes a window with one control, a Viewport3D
, on it. The script is fairly lengthy because making a 3D scene requires that you include quite a few elements. You begin by defining a camera, so you know which direction you're looking at the scene from. You do this using the <Viewport3D.Camera>
element:
<Viewport3D.Camera> <PerspectiveCamera Position="0,0,2" LookDirection="0,0,-1" FieldOfView="60" /> </Viewport3D.Camera>
The tags inside <Model3DGroup>
describe what the scene will look like, while the <AmbientLight Color="White" />
tag describes how you will light the scene, and the <GeometryModel3D.Geometry>
tag describes the 3D shape in the scene:
<GeometryModel3D.Geometry> <MeshGeometry3D /> </GeometryModel3D.Geometry>
Here you could use the <MeshGeometry3D />
tag to describe all the objects that make up the scene by providing the specific points that make up the objects; however, you don't use this tag to describe the points that make up the shape because it is a lot easier to do this in F# than in XAML. The <GeometryModel3D.Material>
tag describes what the surface of the shape will look like:
<GeometryModel3D.Material> <DiffuseMaterial> <DiffuseMaterial.Brush> <ImageBrush ImageSource="venus.jpg" /> </DiffuseMaterial.Brush> </DiffuseMaterial> </GeometryModel3D.Material>
The <GeometryModel3D.Transform>
tag describes a transformation that you want to apply to the shape; that is, it describes the angle you want to rotate the shape by:
<GeometryModel3D.Transform> <RotateTransform3D> <RotateTransform3D.Rotation> <AxisAngleRotation3D x:Name="MyRotation3D" Angle="45" Axis="0,1,0"/> </RotateTransform3D.Rotation> </RotateTransform3D> </GeometryModel3D.Transform>
You do this mainly so you can use the <Viewport3D.Triggers>
tag to define an animation that you use to alter the angle you display the shape at over time:
<Viewport3D.Triggers> <EventTrigger RoutedEvent="Viewport3D.Loaded"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation From="-80" To="80" Duration="0:0:12"
Storyboard.TargetName="MyRotation3D" Storyboard.TargetProperty="Angle" RepeatBehavior="Forever" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Viewport3D.Triggers>
Listing 8-7 shows the complete example, so you can see how these various sections hang together.
Example 8.7. A XAML Definition of a 3D Scene
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Viewport3D Name="ViewPort"> <Viewport3D.Camera> <PerspectiveCamera Position="0,0,2" LookDirection="0,0,-1" FieldOfView="60" /> </Viewport3D.Camera> <Viewport3D.Children> <ModelVisual3D> <ModelVisual3D.Content> <Model3DGroup > <Model3DGroup.Children> <AmbientLight Color="White" /> <GeometryModel3D> <GeometryModel3D.Geometry> <MeshGeometry3D /> </GeometryModel3D.Geometry> <GeometryModel3D.Transform> <RotateTransform3D> <RotateTransform3D.Rotation> <AxisAngleRotation3D x:Name="MyRotation3D" Angle="45" Axis="0,1,0"/> </RotateTransform3D.Rotation> </RotateTransform3D> </GeometryModel3D.Transform>
<GeometryModel3D.Material> <DiffuseMaterial> <DiffuseMaterial.Brush> <ImageBrush ImageSource="venus.jpg" /> </DiffuseMaterial.Brush> </DiffuseMaterial> </GeometryModel3D.Material> </GeometryModel3D> </Model3DGroup.Children> </Model3DGroup> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D.Children> <Viewport3D.Triggers> <EventTrigger RoutedEvent="Viewport3D.Loaded"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation From="-80" To="80" Duration="0:0:12" Storyboard.TargetName="MyRotation3D" Storyboard.TargetProperty="Angle" RepeatBehavior="Forever" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Viewport3D.Triggers> </Viewport3D> </Window>
You will use F# to extend Listing 8-7 in Listing 8-8, borrowing a couple of functions from Listing 8-6; note that you save the code in Listing 8-7 to a file called Window2.xaml
. You use the createWindow
function to load the window and use a similar main
function to display the window. You then use the findMeshes
function to find any meshes in the picture (a mesh is a set of points used to describe the 3D plane). You find the meshes by walking the various objects in the Viewport3D
and building up a list:
// finds all the MeshGeometry3D in a given 3D view port let findMeshes ( viewport : Viewport3D ) = viewport.Children |> Seq.choose (function :? ModelVisual3D as c -> Some(c.Content) | _ -> None)
|> Seq.choose (function :? Model3DGroup as mg -> Some(mg.Children) | _ -> None) |> Seq.concat |> Seq.choose (function :? GeometryModel3D as mg -> Some(mg.Geometry) | _ -> None) |> Seq.choose (function :? MeshGeometry3D as mv -> Some(mv) | _ -> None)
You should keep this function generic, so it can work with any Viewport3D
. It is highly likely that you will want to grab a list of all the meshes in your 3D scene for any 3D work you do in XAML and F#. The reason: You will probably want to use F# to manipulate your meshes in some way. Then you use createPlaneItemList, createSquare, createPlanePoints, createIndicesPlane
, and addPlaneToMesh
to add a flat plane to the mesh object in the scene. The function mapPositionsCenter
centers the plane, placing it in the middle of the scene. Finally, a clever little function called changePositions
maps the function movingWaves
repeatedly across the plane ten times a second. The core of changePositions
creates a new Point3DCollection
from the Point3D
objects contained in the old collection using the function movingWaves
to decide what the new Z position should be:
let changePositions () = let dispatcherTimer = new DispatcherTimer() dispatcherTimer.Tick.Add (fun e -> let t = (float DateTime.Now.Millisecond) / 2000.0 let newPositions = mesh.Positions |> Seq.map (fun position -> let z = movingWaves t position.X position.Y new Point3D(position.X, position.Y, z)) mesh.Positions <- new Point3DCollection(newPositions)) dispatcherTimer.Interval <- new TimeSpan(0,0,0,0,100) dispatcherTimer.Start()
You use the DispatcherTimer
class to execute the code on the thread that created the form, which means you don't need to call back to this thread to update the form. You need to call this class at least ten times a second to create a smooth animation effect (see Listing 8-8 for the complete example).
Example 8.8. Displaying and Interacting with a 3D XAML Scene
open System open System.Collections.Generic open System.IO open System.Windows open System.Windows.Controls open System.Windows.Markup open System.Windows.Media
open System.Windows.Media.Media3D open System.Windows.Threading open System.Xml // creates the window and loads the given XAML file into it let createWindow (file : string) = using (XmlReader.Create(file)) (fun stream -> let temp = XamlReader.Load(stream) :?> Window temp.Height <- 400.0 temp.Width <- 400.0 temp.Title <- "F# meets Xaml" temp) // finds all the MeshGeometry3D in a given 3D view port let findMeshes ( viewport : Viewport3D ) = viewport.Children |> Seq.choose (function :? ModelVisual3D as c -> Some(c.Content) | _ -> None) |> Seq.choose (function :? Model3DGroup as mg -> Some(mg.Children) | _ -> None) |> Seq.concat |> Seq.choose (function :? GeometryModel3D as mg -> Some(mg.Geometry) | _ -> None) |> Seq.choose (function :? MeshGeometry3D as mv -> Some(mv) | _ -> None) // loop function to create all items necessary for a plane let createPlaneItemList f (xRes : int) (yRes : int) = let list = new List<_>() for x = 0 to xRes - 1 do for y = 0 to yRes - 1 do f list x y list // function to initialize a point let point x y = new Point(x, y) // function to initialize a "d point let point3D x y = new Point3D(x, y, 0.0) // create all the points necessary for a square in the plane let createSquare f (xStep : float) (yStep : float) (list : List<_>) (x : int) (y : int) = let x' = float x * xStep let y' = float y * yStep
list.Add(f x' y') list.Add(f (x' + xStep) y') list.Add(f (x' + xStep) (y' + yStep)) list.Add(f (x' + xStep) (y' + yStep)) list.Add(f x' (y' + yStep)) list.Add(f x' y') // create all items in a plane let createPlanePoints f xRes yRes = let xStep = 1.0 / float xRes let yStep = 1.0 / float yRes createPlaneItemList (createSquare f xStep yStep) xRes yRes // create the 3D positions for a plane, i.e., the thing that says where // the plane will be in 3D space let createPlanePositions xRes yRes = let list = createPlanePoints point3D xRes yRes new Point3DCollection(list) // create the texture mappings for a plane, i.e., the thing that // maps the 2D image to the 3D plane let createPlaneTextures xRes yRes = let list = createPlanePoints point xRes yRes new PointCollection(list) // create indices list for all our triangles let createIndicesPlane width height = let list = new System.Collections.Generic.List<int>() for index = 0 to width * height * 6 do list.Add(index) new Int32Collection(list) // center the plane in the field of view let mapPositionsCenter (positions : Point3DCollection) = let newPositions = positions |> Seq.map (fun position -> new Point3D( (position.X - 0.5 ) * −1.0, (position.Y - 0.5 ) * −1.0, position.Z)) new Point3DCollection(newPositions)
// create a plane and add it to the given mesh let addPlaneToMesh (mesh : MeshGeometry3D) xRes yRes = mesh.Positions <- mapPositionsCenter (createPlanePositions xRes yRes) mesh.TextureCoordinates <- createPlaneTextures xRes yRes mesh.TriangleIndices <- createIndicesPlane xRes yRes let movingWaves (t : float) x y = (Math.Cos((x + t) * Math.PI * 4.0) / 3.0) * (Math.Cos(y * Math.PI * 2.0) / 3.0) // create our window let window = createWindow "Window2.xaml" let mesh = // grab the 3D view port let viewport = window.FindName("ViewPort") :?> Viewport3D // find all the meshes and get the first one let meshes = findMeshes viewport let mesh = Seq.head meshes // add plane to the mesh addPlaneToMesh mesh 20 20 mesh let changePositions () = let dispatcherTimer = new DispatcherTimer() dispatcherTimer.Tick.Add (fun e -> let t = (float DateTime.Now.Millisecond) / 2000.0 let newPositions = mesh.Positions |> Seq.map (fun position -> let z = movingWaves t position.X position.Y new Point3D(position.X, position.Y, z)) mesh.Positions <- new Point3DCollection(newPositions)) dispatcherTimer.Interval <- new TimeSpan(0,0,0,0,100) dispatcherTimer.Start() let main() = let app = new Application() changePositions()
// show the window app.Run(window) |> ignore [<STAThread>] do main()
Executing the code in Listing 8-8 produces the window shown in Figure 8-8. It doesn't show off the animated results, but you should try out the application and see the animated effects for yourself.
You should also play with this sample in fsi
. You can subtly alter the sample to run inside fsi
, and then dynamically alter the function you apply to the plane. You must alter the original script in several small ways.
Begin by setting the reference to the .dll
files in an fsi
style:
#I @"C:Program FilesReference AssembliesMicrosoftFrameworkv3.0" ;; #r @"PresentationCore.dll" ;; #r @"PresentationFramework.dll" ;; #r @"WindowsBase.dll" ;;
Next, you must alter the changePositions
function to use a mutable function:
// mutable function that is used within changePositions function let mutable f = (fun (t : float) (x : float) (y : float) -> 0.0) // function for changing the plane over time let changePositions () = let dispatcherTimer = new DispatcherTimer() dispatcherTimer.Tick.Add (fun e -> let t = (float DateTime.Now.Millisecond) / 2000.0 let newPositions = mesh.Positions |> Seq.map (fun position -> let z = f t position.X position.Y new Point3D(position.X, position.Y, z)) mesh.Positions <- new Point3DCollection(newPositions)) dispatcherTimer.Interval <- new TimeSpan(0,0,0,0,100) dispatcherTimer.Start()
Finally, you show the window using its .Show()
method rather than the Application
class's Run
method; be careful that you set its Topmost
property to true
so that it is easy to interact with the window and see the results:
// show the window, set it the top, and activate the function that will // set it moving window.Show() window.Topmost <- true changePositions ()
Finally, you need to define some other functions to map across the plane. This can be any function that takes three floating-point numbers (the first representing the time and the next two representing the X and Y coordinates, respectively) and returns a third floating-point that represents the Z coordinate. I'm particularly fond of using sine and cosine functions here because these generate interesting wave patterns. The following code includes some examples of what you might use, but please feel free to invent your own:
let cosXY _ x y = Math.Cos(x * Math.PI) * Math.Cos(y * Math.PI) let movingCosXY (t : float) x y = Math.Cos((x + t) * Math.PI) * Math.Cos((y - t) * Math.PI)
You can then easily apply these functions to the plane by updating the mutable function:
f <- movingCosXY
Using this technique produces the image you see in Figure 8-9.
The WPF framework contains lots of types and controls that will take any programmer some time to learn. Fortunately, you can find many resources available on the Internet to help you do this. One good resource is the NetFx3 WPF site (http://wpf.netfx3.com
); another is the WPF section of MSDN (http://msdn2.microsoft.com/en-us/netframework/aa663326.aspx
).
GTK# is a .NET wrapper around the popular cross-platform GUI library, GTK+. If you want to build a local application and want it to run on platforms other than Windows, GTK is probably the logical choice. GTK# works in a similar way to both WinForms and WFP; in GTK#, you base your windows on the Gtk.Window
and use widgets (the equivalent of controls) based on the Gtk.Widget
class.
GTK# is distributed with Mono Project distribution, so the easiest way to get access to it is to install Mono (http://www.go-mono.com/mono-downloads/download.html
). The classes that make up GTK# are spread across four .dll
s: atk-sharp.dll, gdk-sharp.dll, glib-sharp.dll
, and gtk-sharp.dll
. You need to add references to these .dll
s to make the example in this section work.
Before creating any GTK# widgets, you must call the Application.Init()
to initialize the GTK environment. After the controls are visible, you need to call the Application.Run()
method to start the event loop; if you do not call this method, the window and widgets will not react user clicks and other inputs. When the user closes all the windows, you need to close the event loop by calling Application.Quit()
. In the GTK# example (see Listing 8-9), you we have only one window, so you quit the GTK environment when this window closes:
// close the event loop when the window closes win.Destroyed.Add(fun _ -> Application.Quit())
You use a widget called an HBox
or a VBox to lay out a GTK# application.
Unlike the Window
class, these widgets can contain more than one widget, stacking the widgets contained in them either horizontally or vertically. In Listing 8-9, you can see that you create a VBox, which means
that the widgets contained within it are laid out horizontally:
// create a new vbox and add the sub controls let vbox = new VBox() vbox.Add(label) vbox.Add(button)
You can see the complete example in Listing 8-9; executing the code in these listings produces the image shown in Figure 8-10.
Example 8.9. A Simple Example of a GTK# Application
open Gtk let main() = // initalize the GTK environment Application.Init()
// create the window let win = new Window("GTK# and F# Application") // set the windows size win.Resize(400, 400) // create a label let label = new Label() // create a button and subscribe to // its clicked event let button = new Button(Label = "Press Me!") button.Clicked.Add(fun _ -> label.Text <- "Hello World.") // create a new vbox and add the sub controls let vbox = new VBox() vbox.Add(label) vbox.Add(button) // add the vbox to the window win.Add(vbox) // show the window win.ShowAll() // close the event loop when the window closes win.Destroyed.Add(fun _ -> Application.Quit()) // start the event loop Application.Run() do main()
ASP.NET is a technology intended to simplify creating dynamic web pages. The simplest way to do this is to implement an interface called IHttpHandler
. This interface allows the implementer to describe how an HTTP request should be responded to; the next section of the chapter will explain how this works.
Merely implementing the IHttpHandler
interface doesn't allow you to take full advantage of the ASP.NET feature set. ASP.NET allows users to create web forms, which are composed of controls that know how to render themselves into HTML. The advantage of this is that the programmer has a nice object model to manipulate, rather than having to code HTML tags. It also allows a programmer to separate the layout of controls into an .aspx
file. An .aspx
file basically contains all the static HTML you don't want to worry about in your F# code, plus a few placeholders for the dynamic controls. This approach is great for programming in F# because it allows you to separate the code that represents the layout of a form, which can look a little long in F#, from the code that controls its behavior. ASP.NET also lets you store configuration values in an XML-based web.config
file.
Working with ASP.NET presents an additional challenge; you must configure the web server that will host the ASP.NET application. Your configuration will vary, depending on your development environment.
Visual Studio has come with a built-in web server since Visual Studio 2005. Creating a new web site is a simple matter of selecting File~TRANew~TRAWeb Site and then choosing the location for the web site. This site will run only pages written in C# or Visual Basic .NET, so you need to add an F# project to the solution and then manually alter the solution file so that it lives inside the web site directory. This is easier than it sounds. All you need to do is copy the .fsproj
file to the web site directory, open the .sln
file in Notepad, and alter the path to the .fsproj
file. After this, you need to configure the project file to output a library and write this to a bin
subdirectory. This might seem like a lot of effort, but once you do this, you can press F5 to make your project compile and run.
If you do not have Visual Studio, then your next best choice is host the site in IIS. In some ways, this is easier than hosting your site in Visual Studio; however, IIS doesn't let you just execute your code once you finish writing it. To host your code in IIS, you need to create an IIS virtual directory with a subdirectory called bin
. You then need to copy your .aspx
pages and your web.config
file to the virtual directory.
ASP.NET has always been part of the .NET framework, so you don't need to install any additional features to make these examples work; however, you do need to add a reference to the System.Web.dll
to make all of the examples in this section work.
Creating an IHttpHandler
is the simplest way to take advantage of ASP.NET 2.0. This simple interface has only two members. The first member is a read-only Boolean
property called IsReusable
that you use to indicate whether the runtime can reuse the instance of the object. It is generally best to set this to false
.
The second member of the interface is the ProcessRequest
method, which is called when a web request is received. It takes one parameter of HttpContent
type; you can use this type to retrieve information about the request being made through its Request
property, as well as to respond to the request via its Response
property. The following, simple example of an IHttpHandler
responds to a request with the string "<h1>Hello World</h1>"
:
namespace Strangelights.HttpHandlers open System.Web // a http handler class type SimpleHandler() = interface IHttpHandler with // tell the ASP.NET runtime if the handler can be reused member x.IsReusable = false // The method that will be called when processing a // HTTP request member x.ProcessRequest(c : HttpContext) = c.Response.Write("<h1>Hello World</h1>")
Next, you must configure the URL where the IHttpHandler
is available. You do this by adding an entry to the web.config
file. If don't already have a web.config
file in the project, you can add one by right-clicking the web project and choosing Add New Item. The handlers are added to the httpHandlers
section, and you need to configure four properties for each handler: path
, which is the URL of the page; verb
, which configures which HTTP verbs the handler will respond to; type
, which is the name of the type that you will use to handle the request; and validate
, which tells the runtime whether it should check the availability of the type when the application loads:
<configuration> <system.web> <httpHandlers> <add path="hello.aspx" verb="*" type="Strangelights.HttpHandlers.SimpleHandler" validate="true"/> </httpHandlers> </system.web> </configuration>
Executing SimpleHandler
produces the web page shown in Figure 8-11.
This technique is unsatisfactory for creating web pages because it requires that you mix HTML tags t into your F# code. It does have some advantages, though. You can use this technique to put together documents other than HTML documents; for example, you can use it to dynamically create images on the server. The following example shows an IHttpHandler
that generates a JPEG image of a pie shape. The amount of pie shown is determined by the angle value that that is passed in on the query string. Making this example work requires that you add a reference to System.Drawing.dll
:
namespace Strangelights.HttpHandlers open System.Drawing open System.Drawing.Imaging open System.Web
// a class that will render a picture for a http request type PictureHandler() = interface IHttpHandler with // tell the ASP.NET runtime if the handler can be reused member x.IsReusable = false // The method that will be called when processing a // HTTP request and render a picture member x.ProcessRequest(c : HttpContext) = // create a new bitmap let bitmap = new Bitmap(200, 200) // create a graphics object for the bitmap let graphics = Graphics.FromImage(bitmap) // a brush to provide the color let brush = new SolidBrush(Color.Red) // get the angle to draw let x = int(c.Request.QueryString.Get("angle")) // draw the pie to bitmap graphics.FillPie(brush, 10, 10, 180, 180, 0, x) // save the bitmap to the output stream bitmap.Save(c.Response.OutputStream, ImageFormat.Gif)
Again, you still need to register this type in the web.config
file; the required configuration looks like this:
<configuration> <system.web> <httpHandlers> <add path="pic.aspx" verb="*" type="Strangelights.HttpHandlers.PictureHandler" validate="true"/> </httpHandlers> </system.web> </configuration>
Executing this code produces the image in shown in Figure 8-12. In this case, I passed in an angle of 200.
Although this is a great technique for spicing up web sites, but you should be careful when using it. Generating images can be processor intensive, especially if the images are large or complicated. This can lead to web sites that do not scale up to the required number of concurrent users; therefore, if you do use this technique, ensure you profile your code correctly.
If you want to create dynamic web pages, then you will probably have an easier time using ASP.NET forms than implementing your own IHttpHandler
. The main advantage of web forms is that you do not need to deal with HTML tags in F# code; most of this is abstracted away for you. This approach confers other, smaller advantages too. For example, it means you do not have to register the page in web.config
.
To create an ASP.NET web form, you generally start by creating the user interface, defined in an .aspx
file. The .aspx
file contains all your static HTML, plus some placeholders for the dynamic controls. An .aspx
file always starts with a Page
directive; you can see this at the top of the next example. The Page
directive allows you to specify a class that the page will inherit from; you do this by using the Inherits
attribute and giving the full name of the class. You will use an F# class to provide the dynamic functionality.
The following example includes some tags prefixed with asp:
among the regular HTML tags. These are ASP.NET web controls, and they provide the dynamic functionality. A web control is a class in the .NET Framework that knows how to render itself into HTML; for example, the <asp:TextBox />
tag will become an HTML <input />
tag. You can take control of these controls in your F# class and use them to respond to user input:
<%@ Page Inherits="Strangelights.HttpHandlers.HelloUser" %> <html> <head> <title>F# - Hello User</title> </head> <body> <p>Hello User</p> <form id="theForm" runat="server"> <asp:Label ID="OutputControl" Text="Enter you're name ..." runat="server" /> <br /> <asp:TextBox ID="InputControl" runat="server" /> <br /> <asp:LinkButton ID="SayHelloButton" Text="Say Hello ..." runat="server" OnClick="SayHelloButton_Click" /> </form> </body> </html>
When designing your class, you need to provide mutable fields with the same name as the controls you want to manipulate. The HTML page you created had three controls in it, but you provide only two mutable fields, because you don't want to manipulate the third control, a link button. You just want that button to call the SayHelloButton_Click
function when a user clicks it. You do this by adding the function name to the OnClick
attribute of the asp:LinkButton
control.
When the other two controls are created, a label and a textbox, they will be stored in the mutable fields OutputControl
and InputControl
, respectively. It is the code contained in the .aspx
page, not your class, that is responsible for creating these controls. This is why you explicitly initialize these controls to null
in the constructor. All that remains in SayHelloButton_Click
is to take the input from InputControl
and place it into OutputControl
:
namespace Strangelights.HttpHandlers open System open System.Web.UI open System.Web.UI.WebControls
// class to handle to provide the code behind for the .aspx page type HelloUser = inherit Page // fields that will hold the controls reference val mutable OutputControl: Label val mutable InputControl: TextBox // the class must have a parameterless constructor new() = { OutputControl = null InputControl = null } // method to handle the on click event member x.SayHelloButton_Click((sender : obj), (e : EventArgs)) = x.OutputControl.Text <- ("Hello ... " + x.InputControl.Text)
Executing the preceding example produces the web page shown in Figure 8-13.
This form doesn't look great, but the nice thing about defining your application in HTML is that you can quickly use images and Cascading Style Sheets (CSS) to spice up the application. Figure 8-14 shows the results of adding a little CSS magic.
You have taken only a brief look at all the functionality offered by ASP.NET. Table 8-5 summarizes all the namespaces available in System.Web.dll
that contain ASP.NET functionality.
Table 8.5. A Summary of the Namespaces Available in System.Web.dll
This chapter provided an overview of various options for creating user interfaces with F#. The scope of this topic is frankly enormous, and it would be impossible to cover all the options for user-interface programming in F#. For example, you can find hundreds of third-party components built on ASP.NET, WinForms, or WPF. These help raise the level of abstraction when creating user interfaces. You can also find libraries that offer complete alternative programming models, such as DirectX, which is designed for high-performance 3D graphics.
The next chapter will take a look at another important programming task—how to access data.