Wednesday, January 30, 2008

Learning WPF with F# - The Menu Hierarchy

In working through Chapter 14 of Petzold's book,Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation, the example that stood out the most for me was the PopupContextMenu. Petzold's implementation required that you implement a subclass of Windows class and override the OnMouseRightButtonUp. In addition, the definition of the menu items is separated from the behavioral implementation of the menu items...requiring the developer to scroll back and forth to understand the behavior to the implemented menu items. In addition, the member functions that implements the menu item behaviors does so for all the menu items.

In contrast, F# allows you to isolate the menu item creation and implement the behavior together without the need to subclass Window, override OnMouseRightButtonUp or have an "overlord" method MenuOnClick that implements the behavior of all the menu items. The creation of the menu items along with the behavioral implementation are all colocated and then the parent Menu object is attached to a standard Window object. I think this illustrates the power of functional programming language to build reusable component and then compose the components together to build functionality. Given what I've seen so far, I believe that F# and other functional programming languages will help move closer to the holy grail of reusable software components.

Here are the code samples:


PeruseTheMenu

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
//
// From Chapter 14 - PeruseTheMenu
//
let dock = new DockPanel()

let window = new Window(Title="Peruse the Menu",
SizeToContent = SizeToContent.WidthAndHeight,
Content=dock)

// Define commands
let unimplemented args (item:MenuItem) =
let miLabel = item.Header.ToString().Replace("_"," ")
MessageBox.Show("The " + miLabel + " option has not yet been implemented",
window.Title) |> ignore

let sizeOnCheck (item:MenuItem) =
window.SizeToContent <-
match item.IsChecked with
| true -> SizeToContent.WidthAndHeight
| _ -> SizeToContent.Manual

let exitCmd () = window.Close()

// Add a menu to dock
let menu = new Menu()
dock.Children.Add
(// Add "File" Menu
menu.Items.Add
(let miFile = new MenuItem(Header = "_File")
// Add "New" MenuItem
miFile.Items.Add
(let item = new MenuItem(Header="_New")
item.Click.Add (fun args -> unimplemented args item)
item)|> ignore
// Add "Open" MenuItem
miFile.Items.Add
(let item = new MenuItem(Header="_Open")
item.Click.Add (fun args -> unimplemented args item)
item)|> ignore
// Add "Save" MenuItem
miFile.Items.Add
(let item = new MenuItem(Header="_Save")
item.Click.Add (fun args -> unimplemented args item)
item)|> ignore
// Add separator
miFile.Items.Add(new Separator()) |> ignore
// Add "Exit MenuItem
miFile.Items.Add
(let item = new MenuItem(Header="_Exit")
item.Click.Add (fun _ -> exitCmd ())
item)|> ignore
miFile)|> ignore
// Add "Window" Menu
menu.Items.Add
(let miWindow = new MenuItem(Header = "_Window")
// Add "Show in Taskbar" menuitem
miWindow.Items.Add
(let item = new MenuItem(Header="_Show in Taskbar",
IsCheckable=true,
IsChecked=window.ShowInTaskbar)
item.Click.Add(fun _ -> window.ShowInTaskbar <- item.IsChecked)
item)|>ignore
// Add "Size to Content" menuitem
miWindow.Items.Add
(let item =
new MenuItem(Header="Size to _Content",
IsCheckable=true,
IsChecked=(window.SizeToContent=SizeToContent.WidthAndHeight))
item.Checked.Add (fun _ -> sizeOnCheck item)
item.Unchecked.Add(fun _ -> sizeOnCheck item)
item)|>ignore
// Add "Resizable" menuitem
miWindow.Items.Add
(let item = new MenuItem(Header="_Resizable",
IsCheckable=true,
IsChecked=(window.ResizeMode=ResizeMode.CanResize))
item.Click.Add (fun _ ->
window.ResizeMode <-
if item.IsChecked
then ResizeMode.CanResize
else ResizeMode.NoResize)
item)|>ignore
// Add "Topmost" menuitem
miWindow.Items.Add
(let item = new MenuItem(Header="_Topmost",
IsCheckable=true,
IsChecked=window.Topmost)
item.Checked.Add (fun _ -> window.Topmost <- item.IsChecked)
item.Unchecked.Add (fun _ -> window.Topmost <- item.IsChecked)
item)|>ignore
miWindow) |> ignore
menu) |> ignore
DockPanel.SetDock(menu, Dock.Top)

// Add TextBlock to DockPanel
dock.Children.Add
(new TextBlock(Text=window.Title,
FontSize=32.0,
TextAlignment = TextAlignment.Center)
) |> ignore


#if COMPILED
[<STAThread()>]
do
let app = Application() in
app.Run(window) |> ignore
#endif

CheckTheWindowStyle

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
//
// From Chapter 14 - CheckTheWindowStyle
//

let mutable itemChecked:MenuItem = null

let dock = new DockPanel()

let window = new Window(Title="Check the Window Style",
SizeToContent = SizeToContent.WidthAndHeight,
Content=dock)

// Create MenuItem objects to change WindowStyle
let itemStyle = new MenuItem(Header="_Style");

// Add window styles menu items
[("_No border or caption", WindowStyle.None);
("_Single-border window",WindowStyle.SingleBorderWindow);
("3_D-border window",WindowStyle.ThreeDBorderWindow);
("_Tool window",WindowStyle.ToolWindow)]
|> Seq.iter (fun (str,style) ->
let item = new MenuItem(Header=str,
Tag=style,
IsChecked= (style=window.WindowStyle))
if item.IsChecked then itemChecked <- item
item.Click.Add( fun _ ->
itemChecked.IsChecked <- false
item.IsChecked <- true
window.WindowStyle <-(item.Tag :?> WindowStyle)
itemChecked <- item)

itemStyle.Items.Add(item) |> ignore)

// Add Style menu to dock panel
let menu = new Menu()
menu.Items.Add(itemStyle)|>ignore
dock.Children.Add(menu))|>ignore
DockPanel.SetDock(menu, Dock.Top)


// Add TextBlock to DockPanel
dock.Children.Add
(new TextBlock(Text=window.Title,
FontSize=32.0,
TextAlignment = TextAlignment.Center)
) |> ignore


#if COMPILED
[<STAThread()>]
do
let app = Application() in
app.Run(window) |> ignore
#endif

CheckTheColor

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
//
// From Chapter 14 - CheckTheColor
//
let dock = new DockPanel()

let window = new Window(Title="Check the Color",
Content=dock)

let text = new TextBlock(Text=window.Title,
TextAlignment=TextAlignment.Center,
FontSize=32.0,
Background=SystemColors.WindowBrush,
Foreground = SystemColors.WindowTextBrush)

let selectedColor (clr:Color) =
let check = clr.A = 255uy
let r = if clr.R = 255uy then 0uy else clr.R
let g = if clr.G = 255uy then 0uy else clr.G
let b = if clr.B = 255uy then 0uy else clr.B

match (r,g,b) with
| (0uy,0uy,_) -> check
| (0uy,_,0uy) -> check
| (_,0uy,0uy) -> check
| _ -> false

let colorProps = seq { for prop in (typeof<Colors>.GetProperties()) do
let clr = prop.GetValue(null,null) :?> Color
if selectedColor(clr) then yield (prop)}

let foregroundMenuItems = new ResizeArray<MenuItem>()
let backgroundMenuItems = new ResizeArray<MenuItem>()

// Create background menu items
colorProps |> Seq.iter (fun prop ->
let clr = prop.GetValue(null,null) :?> Color
let item = new MenuItem(Header = "_" + prop.Name,Tag=clr)
// Implement BackgroundOnClick
item.Click.Add( fun args ->
text.Background <- new SolidColorBrush(item.Tag :?> Color)
)
backgroundMenuItems.Add(item))

// Create foreground menu items
colorProps |> Seq.iter (fun prop ->
let clr = prop.GetValue(null,null) :?> Color
let item = new MenuItem(Header = "_" + prop.Name,Tag=clr)
// Implement ForegroundOnClick
item.Click.Add( fun args ->
text.Foreground <- new SolidColorBrush(item.Tag :?> Color)
)
foregroundMenuItems.Add(item))


let menu = new Menu()
let itemColor = new MenuItem(Header="_Color")
let itemForeground = new MenuItem(Header="_Foreground")
// FillWithColors for foreground
foregroundMenuItems |> ResizeArray.iter (fun x -> itemForeground.Items.Add(x)|>ignore)
// Implement ForegroundOnOpened
itemForeground.SubmenuOpened .Add(fun args ->
foregroundMenuItems |> ResizeArray.iter (fun item ->
item.IsChecked <- ((text.Foreground :?> SolidColorBrush).Color = (item.Tag :?> Color))))

let itemBackground = new MenuItem(Header="_Background")
backgroundMenuItems |> ResizeArray.iter (fun x -> itemBackground.Items.Add(x)|>ignore)

// Implement BackgroundOnOpened
itemBackground.SubmenuOpened .Add(fun args ->
backgroundMenuItems |> ResizeArray.iter (fun item ->
item.IsChecked <- ((text.Background :?> SolidColorBrush).Color = (item.Tag :?> Color))))

itemColor.Items.Add(itemForeground)
itemColor.Items.Add(itemBackground)
menu.Items.Add(itemColor)

// Add menu to doc panel
dock.Children.Add(menu)|>ignore
DockPanel.SetDock(menu, Dock.Top)

// Add TextBlock to DockPanel
dock.Children.Add(text) |>ignore


#if COMPILED
[<STAThread()>]
do
let app = Application() in
app.Run(window) |> ignore
#endif

SelectColorFromMenuGrid

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"
#r @"selectcolorfromgrid.dll"

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
open Chapter13
//
// From Chapter 14 - SelectColorFromMenuGrid
//
let dock = new DockPanel()

let window = new Window(Title="Select Color from Menu Grid",
Content=dock)

let menu = new Menu()
dock.Children.Add(menu) |>ignore
DockPanel.SetDock(menu, Dock.Top)
dock.Children.Add
(new TextBlock(Text=window.Title,
FontSize=32.0,
TextAlignment = TextAlignment.Center)) |> ignore

let itemColor = new MenuItem(Header="_Color")
menu.Items.Add(itemColor)

let itemForeground = new MenuItem(Header="_Foreground")
itemColor.Items.Add(itemForeground)

itemForeground.Items.Add
(let clrbox = new ColorGridBox()
clrbox.SetBinding(ColorGridBox.SelectedValueProperty,"Foreground") |> ignore
clrbox.DataContext <- window
clrbox)


let itemBackground = new MenuItem(Header="_Background")
itemColor.Items.Add(itemBackground)

itemBackground.Items.Add
(let clrbox = new ColorGridBox()
clrbox.SetBinding(ColorGridBox.SelectedValueProperty,"Background") |> ignore
clrbox.DataContext <- window
clrbox)

#if COMPILED
[<STAThread()>]
do
let app = Application() in
app.Run(window) |> ignore
#endif

CutCopyAndPaste

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
open System.Windows.Media.Imaging
//
// From Chapter 14 - CutCopyAndPaste
//
let dock = new DockPanel()

let window = new Window(Title="Cut, Copy, and Paste",
Content=dock)

let mainMenu = new Menu()
dock.Children.Add(mainMenu) |>ignore
DockPanel.SetDock(mainMenu, Dock.Top)
let text = new TextBlock(Text = "Sample clipboard text",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FontSize = 32.0,
TextWrapping = TextWrapping.Wrap)
dock.Children.Add(text) |>ignore

// Experimenting with record types as containers for
// all actionable application menu items
let buildMenuItem (header,iconFile) =
let item = new MenuItem(Header=header)
if header <> "_Edit" then
let fullFilePath = @"file:///icons/" + iconFile
let bmp = new BitmapImage(new Uri(fullFilePath))
let image = new Image()
image.Source <- bmp
item.Icon <- image
item

type AppMenus =
{ Edit : MenuItem;
Cut : MenuItem;
Copy : MenuItem;
Paste : MenuItem;
Delete : MenuItem }

let menus =
{Edit=buildMenuItem ("_Edit","");
Cut=buildMenuItem ("Cu_t","Cut24.gif");
Copy=buildMenuItem ("_Copy","Copy24.gif");
Paste=buildMenuItem ("_Paste","Paste24.gif");
Delete=buildMenuItem ("_Delete","Delete24.gif")}

mainMenu.Items.Add(menus.Edit) |>ignore
menus.Edit.Items.Add(menus.Cut) |>ignore
menus.Edit.Items.Add(menus.Copy) |>ignore
menus.Edit.Items.Add(menus.Paste) |>ignore
menus.Edit.Items.Add(menus.Delete) |>ignore

// Generic functions
let CopyOnClick () =
if text.Text <> null && text.Text.Length > 0 then
Clipboard.SetText(text.Text)

let PasteOnClick () =
if Clipboard.ContainsText() then
text.Text <- Clipboard.GetText()

let DeleteOnClick () = text.Text <- null

let CutOnClick () =
CopyOnClick()
DeleteOnClick()


// Implement EditOnOpened
menus.Edit.SubmenuOpened.Add(fun _ ->
let enabledFlag = (text.Text <> null && text.Text.Length > 0)
menus.Cut.IsEnabled <- enabledFlag
menus.Copy.IsEnabled <- enabledFlag
menus.Delete.IsEnabled <- enabledFlag
menus.Paste.IsEnabled <- Clipboard.ContainsText())

menus.Cut.Click.Add( fun _ -> CutOnClick())
menus.Copy.Click.Add (fun _ -> CopyOnClick())
menus.Delete.Click.Add (fun _ -> DeleteOnClick())
menus.Paste.Click.Add (fun _ -> PasteOnClick())




#if COMPILED
[<STAThread()>]
do
let app = Application() in
app.Run(window) |> ignore
#endif

ControlXCV

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
open System.Windows.Media.Imaging
//
// From Chapter 14 - ControlXCV
//
// Create TextBlock first, to be used in action commands
let text = new TextBlock(Text = "Sample clipboard text",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FontSize = 32.0,
TextWrapping = TextWrapping.Wrap)

// Action commands
let CopyOnClick () =
if text.Text <> null && text.Text.Length > 0 then
Clipboard.SetText(text.Text)

let PasteOnClick () =
if Clipboard.ContainsText() then
text.Text <- Clipboard.GetText()

let DeleteOnClick () = text.Text <- null

let CutOnClick () =
CopyOnClick()
DeleteOnClick()


// Need to create new class to override OnPreviewKeyDown
type ControlXCV() = class
inherit Window() as base

let gestCut = new KeyGesture(Key.X, ModifierKeys.Control)
let gestCopy = new KeyGesture(Key.C, ModifierKeys.Control)
let gestPaste = new KeyGesture(Key.V, ModifierKeys.Control)
let gestDelete = new KeyGesture(Key.Delete)

override this.OnPreviewKeyDown (args:KeyEventArgs) =
base.OnKeyDown(args)
args.Handled <- true

if gestCut.Matches(null,args) then CutOnClick()
else if gestCopy.Matches(null,args) then CopyOnClick()
else if gestPaste.Matches(null,args) then PasteOnClick()
else if gestDelete.Matches(null,args) then DeleteOnClick()
else args.Handled <- false

end

let dock = new DockPanel()

let window = new ControlXCV(Title="Control X, C, and V",
Content=dock)

let menu = new Menu()
dock.Children.Add(menu)
DockPanel.SetDock(menu, Dock.Top)

let itemEdit = new MenuItem(Header="_Edit")
menu.Items.Add(itemEdit) |>ignore

dock.Children.Add(text) |>ignore

// Experimenting with record types as containers for
// all actionable application menu items
let buildMenuItem (header,iconFile,keyGestureText) =
let item = new MenuItem(Header=header)
let fullFilePath = @"file:///icons/" + iconFile
let bmp = new BitmapImage(new Uri(fullFilePath))
let image = new Image()
image.Source <- bmp
item.Icon <- image
item.InputGestureText <- keyGestureText
item

type ActionMenus =
{ Cut : MenuItem;
Copy : MenuItem;
Paste : MenuItem;
Delete : MenuItem }

let actionMenus =
{Cut=buildMenuItem ("Cu_t","Cut24.gif","Control+X");
Copy=buildMenuItem ("_Copy","Copy24.gif","Control+C");
Paste=buildMenuItem ("_Paste","Paste24.gif","Control+V");
Delete=buildMenuItem ("_Delete","Delete24.gif","Delete")}

itemEdit.Items.Add(actionMenus.Cut) |>ignore
itemEdit.Items.Add(actionMenus.Copy) |>ignore
itemEdit.Items.Add(actionMenus.Paste) |>ignore
itemEdit.Items.Add(actionMenus.Delete) |>ignore



// Implement EditOnOpened
itemEdit.SubmenuOpened.Add(fun _ ->
let enabledFlag = (text.Text <> null && text.Text.Length > 0)
actionMenus.Cut.IsEnabled <- enabledFlag
actionMenus.Copy.IsEnabled <- enabledFlag
actionMenus.Delete.IsEnabled <- enabledFlag
actionMenus.Paste.IsEnabled <- Clipboard.ContainsText())

actionMenus.Cut.Click.Add( fun _ -> CutOnClick())
actionMenus.Copy.Click.Add (fun _ -> CopyOnClick())
actionMenus.Delete.Click.Add (fun _ -> DeleteOnClick())
actionMenus.Paste.Click.Add (fun _ -> PasteOnClick())


#if COMPILED
[<STAThread()>]
do
let app = Application() in
app.Run(window) |> ignore
#endif

CommandTheMenu

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
//
// From Chapter 14 - CommandTheMenu
//
// Use record type to track all application menu itesm
type AppMenu =
{ : Menu;
Edit : MenuItem;
Cut : MenuItem;
Copy : MenuItem;
Paste : MenuItem;
Delete : MenuItem }

member v.buildMenuTree (dock:DockPanel) =
dock.Children.Add(v.Main) |>ignore
DockPanel.SetDock(v.Main,Dock.Top)
v.Main.Items.Add(v.Edit) |>ignore
v.Edit.Items.Add(v.Cut) |>ignore
v.Edit.Items.Add(v.Copy) |>ignore
v.Edit.Items.Add(v.Paste) |>ignore
v.Edit.Items.Add(v.Delete)|>ignore

static member buildMenu header command =
new MenuItem(Header=header,Command=command)


// Create TextBlock first, to be used in action commands
let text = new TextBlock(Text = "Sample clipboard text",
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
FontSize = 32.0,
TextWrapping = TextWrapping.Wrap)

let dock = new DockPanel()

let window = new Window(Title="Command the Menu",
Content=dock)
let menus =
{Main = new Menu();
Edit = new MenuItem(Header="_Edit");
Cut = AppMenu.buildMenu "Cu_t" ApplicationCommands.Cut;
Copy = AppMenu.buildMenu "_Copy" ApplicationCommands.Copy;
Paste = AppMenu.buildMenu "_Paste" ApplicationCommands.Paste;
Delete = AppMenu.buildMenu "_Delete" ApplicationCommands.Delete; }

menus.buildMenuTree (dock)
dock.Children.Add(text) |>ignore

// Define applications commands
let CutCanExecute (args:CanExecuteRoutedEventArgs) =
args.CanExecute <- text.Text <> null && text.Text.Length > 0

let PasteCanExecute (args:CanExecuteRoutedEventArgs) =
args.CanExecute <- Clipboard.ContainsText()

// Bind commands

window.CommandBindings.Add
(new CommandBinding(ApplicationCommands.Cut,
(fun _ _->
ApplicationCommands.Copy.Execute(null, window)
ApplicationCommands.Delete.Execute(null, window)),
(fun _ args -> CutCanExecute(args))))

window.CommandBindings.Add
(new CommandBinding(ApplicationCommands.Copy,
(fun _ _->
Clipboard.SetText(text.Text)),
(fun _ args -> CutCanExecute(args))))


window.CommandBindings.Add
(new CommandBinding(ApplicationCommands.Paste,
(fun _ _->
text.Text <- Clipboard.GetText()),
(fun _ args -> PasteCanExecute(args))))

window.CommandBindings.Add
(new CommandBinding(ApplicationCommands.Delete,
(fun _ _->
text.Text <- null),
(fun _ args -> CutCanExecute(args))))


#if COMPILED
[<STAThread()>]
do
let app = Application() in
app.Run(window) |> ignore
#endif

PopupContextMenu

#light
#I @"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0"
#r @"WindowsBase.dll"
#r @"PresentationCore.dll"
#r @"PresentationFramework.dll"

open System
open System.Windows
open System.Windows.Controls
open System.Windows.Documents
open System.Windows.Input
open System.Windows.Media
open Microsoft.FSharp.Compatibility
//
// From Chapter 14 - PopupContextMenu
//
let window = new Window(Title="Popup Context Menu")

let text= new TextBlock(FontSize=32.0,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center)
window.Content <- text

let quote = "To be, or not to be, that is the question"
quote |> String.split [' '] |> Seq.iter (fun word ->
let menu = new ContextMenu()
let run = new Run(word)
run.TextDecorations <- new TextDecorationCollection()
run.ContextMenu <- menu
text.Inlines.Add(run)
text.Inlines.Add(" ")

menu.Items.Add
(let item = new MenuItem(Header="Bold")
item.Click.Add (fun args ->
if run.FontWeight = FontWeights.Bold then
item.IsChecked <- false
run.FontWeight <- FontWeights.Normal
else
item.IsChecked <- true
run.FontWeight <- FontWeights.Bold
)
item) |>ignore

menu.Items.Add
(let item = new MenuItem(Header="Italic")
item.Click.Add (fun args ->
if run.FontStyle = FontStyles.Italic then
item.IsChecked <- false
run.FontStyle <- FontStyles.Normal
else
item.IsChecked <- true
run.FontStyle <- FontStyles.Italic)
item) |>ignore

let decorateText (marker:Run) loc =
let decor = new TextDecoration()
decor.Location <- loc
let item = new MenuItem(Header=loc.ToString(),Tag=decor)
item.Click.Add(fun args ->
if item.IsChecked then
marker.TextDecorations.Remove(item.Tag :?> TextDecoration) |> ignore
item.IsChecked <- false
else
marker.TextDecorations.Add(item.Tag :?> TextDecoration)
item.IsChecked <- true)
menu.Items.Add(item) |>ignore

// Add TextDecorationLocation members to Context Menu
let decorations = Enum.GetValues(typeof<TextDecorationLocation>)
:?> (TextDecorationLocation [])
let decorate = decorateText run
Seq.iter decorate decorations)



#if COMPILED
[<STAThread()>]
do
let app = Application() in
app.Run(window) |> ignore
#endif

No comments: