Friday, January 04, 2008

Learning WPF with F# - Single Child Elements

Examples from Chapter 11 of Petzold's book Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation. I'm still working through the various chapters in Dr. Don Syme's book and struggling with writing code functionally instead of imperatively. My struggles seems similar to my transition from programming in Fortran and C to object oriented programming in C++. For Chapter 11, I decided that I want to try writing some of the code examples in the functional programming way instead of falling back to old imperative way of programming. Those attempts are reflected in the hex calculator example and the select color examples. It seems to me that I should be able to use concepts in the chapters on "Language Oriented Programming" and "Working with Symbolic Representations" to build the core of the hex calculator, but currently I don't have a firm grasp of the subjects discuss in those two chapters. I realized my past blog titles has been "Learning WPF with F#"...I find myself learning more about F# then WPF in these exercises.

There are also some issues that I ran into while implementing ColorCell. For some reason, Rect.Inflate doesn't seem to work. I stepped it through the debugger and ran it in F# interactive to verify it's not working. Here's the output from F# interactive :

> let rect = new Rect(new Point(0.0,0.0),new Point(20.0,20.0));;

val rect : Rect

> rect.Inflate(-4.0,-4.0);;
val it : unit = ()
> rect;;
val it : Rect = 0,0,20,20 {Bottom = 20.0;
                           BottomLeft = 0,20;
                           BottomRight = 20,20;
                           Height = 20.0;
                           IsEmpty = false;
                           Left = 0.0;
                           Location = 0,0;
                           Right = 20.0;
                           Size = 20,20;
                           Top = 0.0;
                           TopLeft = 0,0;
                           TopRight = 20,0;
                           Width = 20.0;
                           X = 0.0;
                           Y = 0.0;}
>

Below are the examples from Chapter 11 in F#:


EllipseWithChild & EncloseElementInEllipse

#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 11 - EllipseWithChild & EncloseElementInEllipse
//
let mutable private initFillProperty : DependencyProperty = null
let mutable private initStrokeProperty : DependencyProperty = null

// Replacement for ternary operator ?:
let condassign a b c =
match a with
| true -> b
| false -> c
//
// From Chapter 10 - BetterEllipse (repeated)
type BetterEllipse() = class
inherit FrameworkElement() as base

static member FillProperty =
if initFillProperty = null then
initFillProperty <- DependencyProperty.Register
("Fill", typeof<Brush>,typeof<BetterEllipse>,
new FrameworkPropertyMetadata
(null, FrameworkPropertyMetadataOptions.AffectsRender))
initFillProperty
else
initFillProperty

static member StrokeProperty =
if initStrokeProperty = null then
initStrokeProperty <- DependencyProperty.Register
("Stroke", typeof<Pen>,typeof<BetterEllipse>,
new FrameworkPropertyMetadata
(null, FrameworkPropertyMetadataOptions.AffectsMeasure))
initStrokeProperty
else
initStrokeProperty

member this.Fill
with get() = (this.GetValue(BetterEllipse.FillProperty) :?> Brush)
and set (value :Brush) =
this.SetValue(BetterEllipse.FillProperty,value)

member this.Stroke
with get() = (this.GetValue(BetterEllipse.StrokeProperty) :?> Pen )
and set (value :Pen) =
this.SetValue(BetterEllipse.StrokeProperty,value)

// Override of MeasureOverride
override this.MeasureOverride (sizeAvailable:Size) =
if this.Stroke <> null then
new Size(this.Stroke.Thickness,this.Stroke.Thickness)
else
base.MeasureOverride(sizeAvailable)

// Override of OnRender
override this.OnRender (dc:DrawingContext) =
let drawEllipse width height =
dc.DrawEllipse
(this.Fill, this.Stroke,
new Point(this.RenderSize.Width /2.0, this.RenderSize.Height /2.0),
width/2.0,height/2.0)

if this.Stroke <> null then
let width = Math.Max(0.0,this.RenderSize.Width - this.Stroke.Thickness)
let height = Math.Max(0.0,this.RenderSize.Height - this.Stroke.Thickness)
drawEllipse width height
else
drawEllipse this.RenderSize.Width this.RenderSize.Height

end

// From Chapter 11 - EllipseWithChild
type EllipseWithChild () = class
inherit BetterEllipse() as base

let mutable child = null

member this.Child
with get() = child
and set (value:UIElement) =
if child <> null then
this.RemoveVisualChild(child)
this.RemoveLogicalChild(child)
child <- value
if child <> null then
this.AddVisualChild(child)
this.AddLogicalChild(child)

// Override of VisualChildrenCount returns 1 if Child is non-null.
override this.VisualChildrenCount
with get() = condassign (this.Child<>null) 1 0

override this.GetVisualChild (index:int) =
if index > 0 || this.Child = null then
raise(new ArgumentOutOfRangeException("index"))
this.Child :> Visual

// Override of MeasureOverride calls child's Measure method.
override this.MeasureOverride (sizeAvailable:Size) =
let (sizeDesired,sizeAvailable) =
match this.Stroke with
| null -> (new Size(0.0,0.0),sizeAvailable)
| _ ->
let x = 2.0*this.Stroke.Thickness
let width = Math.Max(0.0,sizeAvailable.Width -x)
let height = Math.Max(0.0,sizeAvailable.Height -x)
(new Size(x,x),new Size(width,height))

match this.Child with
| null -> new Size(0.0,0.0)
| _ ->
this.Child.Measure(sizeAvailable)
new Size(sizeDesired.Width + this.Child.DesiredSize.Width,
sizeDesired.Height + this.Child.DesiredSize.Height)

// Override of ArrangeOverride calls child's Arrange method.
override this.ArrangeOverride (sizeFinal:Size) =
if this.Child <> null then
let rect =
let diffWidth = sizeFinal.Width - this.Child.DesiredSize.Width
let diffHeight = sizeFinal.Height - this.Child.DesiredSize.Height
new Rect(new Point(diffWidth/2.0,diffHeight/2.0), this.Child.DesiredSize)
this.Child.Arrange(rect)
sizeFinal

end

// From Chapter 11 - EncloseElementInEllipse
type EncloseElementInEllipse () as this =
inherit Window() as base

do this.Title <- "Enclose Element in Ellipse"
let elips = new EllipseWithChild()
elips.Fill <- Brushes.ForestGreen
elips.Stroke <- new Pen(Brushes.Magenta, 48.0)
this.Content <- elips

let text =
new TextBlock(FontSize=48.0,
FontFamily= new FontFamily("Times New Roman"),
Text= "Text inside ellipse")
elips.Child <- text

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

RoundedButtonDecorator, RoundedButton, & CalculateInHex

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

open System
open System.Collections.Generic
open System.Globalization
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
open System.Windows.Threading
//
// From Chapter 11 - RoundedButtonDecorator, RoundedButton, & CalculateInHex
//
let mutable private initIsPressedProperty : DependencyProperty = null
let mutable private initClickEvent : RoutedEvent = null

// Replacement for ternary operator ?:
let condassign a b c =
match a with
| true -> b
| false -> c

type RoundedButtonDecorator() = class
inherit Decorator() as base

static member IsPressedProperty =
if initIsPressedProperty = null then
initIsPressedProperty <- DependencyProperty.Register
("Fill", typeof<bool>,typeof<RoundedButtonDecorator>,
new FrameworkPropertyMetadata
(false, FrameworkPropertyMetadataOptions.AffectsRender))
initIsPressedProperty
else
initIsPressedProperty

member this.IsPressed
with get() = (this.GetValue(RoundedButtonDecorator.IsPressedProperty) :?> bool)
and set (value :bool) =
this.SetValue(RoundedButtonDecorator.IsPressedProperty,value)

// Override of MeasureOverride
override this.MeasureOverride (sizeAvailable:Size) =
match this.Child with
| null -> new Size(2.0,2.0)
| _ ->
let w = sizeAvailable.Width - 2.0
let h = sizeAvailable.Height - 2.0
this.Child.Measure(new Size(w,h))
new Size(2.0 + this.Child.DesiredSize.Width,
2.0 + this.Child.DesiredSize.Height)

// Override of ArrangeOverride calls child's Arrange method.
override this.ArrangeOverride (sizeArrange:Size) =
if this.Child <> null then

let rect =
let diffWidth = sizeArrange.Width - this.Child.DesiredSize.Width
let diffHeight = sizeArrange.Height - this.Child.DesiredSize.Height
let x = Math.Max(1.0 , diffWidth/2.0)
let y = Math.Max(1.0 , diffHeight/2.0)
new Rect(new Point(x,y), this.Child.DesiredSize)
this.Child.Arrange(rect)
sizeArrange

// Override of OnRender
override this.OnRender (dc:DrawingContext) =
let brush =
new RadialGradientBrush((condassign (this.IsPressed)
SystemColors.ControlDarkColor
SystemColors.ControlLightLightColor),
SystemColors.ControlColor)
brush.GradientOrigin <-
condassign (this.IsPressed) (new Point(0.75, 0.75)) (new Point(0.25, 0.25))

dc.DrawRoundedRectangle
(brush,
new Pen(SystemColors.ControlDarkDarkBrush, 1.0),
new Rect(new Point(0.0, 0.0), this.RenderSize),
this.RenderSize.Height / 2.0, this.RenderSize.Height / 2.0)

end

// From Chapter 11 - EncloseElementInEllipse
type RoundedButton () as this = class
inherit Control() as base

let decorator = new RoundedButtonDecorator()
let triggerClick, onclickEvent = IEvent.create()

do
this.AddVisualChild(decorator)
this.AddLogicalChild(decorator)

static member ClickEvent =
if initClickEvent = null then
initClickEvent <-
EventManager.RegisterRoutedEvent("Click",
RoutingStrategy.Bubble,
typeof<RoutedEventHandler>,
typeof<RoundedButton>)
initClickEvent
else
initClickEvent

member this.Click = onclickEvent

member this.Child
with get() = decorator.Child
and set (value :UIElement ) = decorator.Child <- value

member this.IsPressed
with get() = decorator.IsPressed
and set (value :bool ) = decorator.IsPressed <- value

override this.VisualChildrenCount
with get() = 1

member this.IsMouseReallyOver
with get() =
let pt = Mouse.GetPosition(this)
pt.X >= 0.0 && pt.X < this.ActualWidth && pt.Y >= 0.0 && pt.Y < this.ActualHeight

override this.GetVisualChild (index:int) =
if index > 0 then
raise(new ArgumentOutOfRangeException("index"))
decorator :> Visual

override this.MeasureOverride (sizeAvailable:Size) =
decorator.Measure(sizeAvailable)
decorator.DesiredSize

override this.ArrangeOverride (sizeArrange:Size) =
decorator.Arrange(new Rect(new Point(0.0, 0.0), sizeArrange))
sizeArrange

override this.OnMouseMove (args:MouseEventArgs ) =
base.OnMouseMove(args)

if this.IsMouseCaptured then
this.IsPressed <- this.IsMouseReallyOver

override this.OnMouseLeftButtonDown (args:MouseButtonEventArgs) =
base.OnMouseLeftButtonDown(args)
this.CaptureMouse() |>ignore
this.IsPressed <- true
args.Handled <- true

override this.OnMouseLeftButtonUp (args:MouseButtonEventArgs) =
base.OnMouseLeftButtonUp(args)
if this.IsMouseCaptured then
if this.IsMouseReallyOver then
this.OnClick()
Mouse.Capture(null) |> ignore
this.IsPressed <- false
args.Handled <- true

member this.OnClick () =
let argsEvent = new RoutedEventArgs()
argsEvent.RoutedEvent <- RoundedButton.ClickEvent
argsEvent.Source <- this
triggerClick(argsEvent)
end

// From Chapter 11 - CalculateInHex
// The paradigm in F# is not to create a new type when you're just using
// the controls - implement calculator from Chapter 11 of Petzold's book

// Define valid digits
let digits = ['0'..'9'] @ ['A'..'F']

// Define valid operators
let operators = ['+';'-';'*';'/';'|';'&';'^';'%']

// Check if the string is a valid digit
let isDigit term = List.exists ((=) (String.get term 0)) digits

// Check if the string is a valid operator
let isOperator term = List.exists ((=) (String.get term 0)) operators

let eval (x:int64) op (y:int64) =
match op with
| "+" -> x+y
| "-" -> x-y
| "*" -> x*y
| "/" -> x/y
| "|" -> x ||| y
| "&" -> x &&& y
| "^" -> x ^^^ y
| "%" -> x%y
| "<<" -> x <<< Int64.to_int y
| ">>" -> x >>> Int64.to_int y
| _ -> failwith "Unknown operator"

type HexVal =
static member Parse v = Int64.Parse(v,NumberStyles.AllowHexSpecifier)
static member ToString v = sprintf "%X" v

// The following couple functions roughly correlate with ButtonOnClick
let equalClick (stack:Stack<string>) text =
match stack.Count with
| 3 ->
let y = HexVal.Parse (stack.Pop())
let op = stack.Pop()
let x = HexVal.Parse (stack.Pop())
let result = eval x op y
stack.Clear()
HexVal.ToString result
| 0 -> text
| _ ->
stack.Clear()
text

let backClick text =
let value = HexVal.Parse text
let term = HexVal.ToString (value/16L)
term

let digitClick digit (stack:Stack<string>) text =
if text = "0" then
stack.Push(digit)
digit
else
let result = match stack.Count with
| 3 | 1 -> stack.Pop() |> ignore; text + digit;
| 2 | 0 -> digit
| _ -> failwith "digitClick should never get here!"
stack.Push(result)
result

let opsClick op (stack:Stack<string>) text =
match stack.Count with
| 3 ->
let y = HexVal.Parse (stack.Pop())
let op = stack.Pop()
let x = HexVal.Parse (stack.Pop())
let result = eval x op y
let term = HexVal.ToString result
stack.Push(term)
term
| 2 -> stack.Pop() |>ignore; stack.Push(op);text
| 1 -> stack.Push(op);text // ignore
| 0 -> stack.Push(text);stack.Push(op);text
| _ -> failwith "Should never get here in opsClick!"



// Create the main hex calculator window
let CalculateInHex = new Window(Title="Calculate in Hex",
SizeToContent = SizeToContent.WidthAndHeight,
ResizeMode = ResizeMode.CanMinimize)


// The display button
let btnDisplay = new RoundedButton(Margin = new Thickness(4.0, 4.0, 4.0, 6.0),
Focusable = false,
Height = 32.0)
let displayText = new TextBlock(Text = "0")
btnDisplay.Child <- displayText

// Core calculator operations
let termStack = new Stack<string>()

// Build rest of the hex calculator UI
let grid = new Grid(Margin=new Thickness(4.0))
CalculateInHex.Content <- grid

// Allow for keyboard inputs for hex calculator
CalculateInHex.TextInput.Add(fun args ->
let x = args.Text.ToUpper()
for ui in grid.Children do
let btn = ui :?> RoundedButton
match btn.Child with
| :? TextBlock as txtbox ->
let text = txtbox.Text
if text = x then
let argsClick = new RoutedEventArgs(RoundedButton.ClickEvent,btn)
btn.RaiseEvent(argsClick)
btn.IsPressed <- true
// Set timer to unpress button
let timer = new DispatcherTimer()
timer.Tick.Add(fun e ->
btn.IsPressed <- false
)
timer.Interval <- TimeSpan.FromMilliseconds(100.0)
timer.Start()

| _ ->()

//List.find
let result = match x with
| "=" | "\r" ->
equalClick termStack displayText.Text
| "\b" ->
backClick displayText.Text
| _ when isDigit x -> digitClick x termStack displayText.Text
| _ when isOperator x -> opsClick x termStack displayText.Text
| _ -> displayText.Text
displayText.Text <- result
)

let colDef (count:int) (gltype:GridLength) =
seq { for i in 0..count-1 -> new ColumnDefinition(Width=gltype) }

let rowDef (count:int) (gltype:GridLength) =
seq { for i in 0..count-1 -> new RowDefinition(Height=gltype) }

Seq.iter grid.ColumnDefinitions.Add (colDef 5 GridLength.Auto)
Seq.iter grid.RowDefinitions.Add (rowDef 7 GridLength.Auto)

// Text to appear in buttons
let buttonLabels = [ "Display";"X";"X";"X";"X";
"D"; "E"; "F"; "+"; "&";
"A"; "B"; "C"; "-"; "|";
"7"; "8"; "9"; "*"; "^";
"4"; "5"; "6"; "/"; "<<";
"1"; "2"; "3"; "%"; ">>";
"0"; "Back";"X";"Equals";"X" ]



// function to create Rounded Button
let createButton (index,label) =

let createRoundButton label =
let btn = new RoundedButton(Focusable = false,
Height = 32.0,
Margin = new Thickness(4.0))
let txt = new TextBlock(Text = label)
btn.Child <- txt
btn

let addToGrid (grid:Grid) item row col span =
grid.Children.Add(item) |>ignore
Grid.SetRow(item,row)
Grid.SetColumn(item,col)
Grid.SetColumnSpan(item,span)

let col = (index-1) % 5
let row = (index-1) / 5
match label with
| "Display" ->
addToGrid grid btnDisplay 0 0 5
| "X" -> ()
| "Back" ->
let btn = createRoundButton label
addToGrid grid btn 6 col 2
btn.Click.Add(fun _ ->
let result = backClick displayText.Text
displayText.Text <- result)
| "Equals" ->
let btn = createRoundButton label
addToGrid grid btn 6 col 2
btn.Click.Add(fun _ ->
let text = equalClick termStack displayText.Text
displayText.Text <- text
)
| _ when (label.Length = 1) && isDigit label ->
let btn = createRoundButton label
btn.Width <- 32.0
addToGrid grid btn row col 1
btn.Click.Add(fun _ ->
let result = digitClick label termStack displayText.Text
displayText.Text <- result)
| _ ->
let btn = createRoundButton label
btn.Width <- 32.0
addToGrid grid btn row col 1
btn.Click.Add(fun _ ->
let text = opsClick label termStack displayText.Text
displayText.Text <- text)

// Add all the buttons to the grid
List.zip [1..35] buttonLabels |> List.map createButton

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

ButtonFactory

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

open System
open System.Collections.Generic
open System.Globalization
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
open System.Windows.Threading
//
// From Chapter 11 - Button Factory
//
let window = new Window(Title="Build Button Factory")

// Create a ControlTemplate intended for a Button object
let template = new ControlTemplate(typeof<Button>)
let factoryBorder = new FrameworkElementFactory(typeof<Border>)
factoryBorder.Name <- "border"
factoryBorder.SetValue(Border.BorderBrushProperty,Brushes.Red)
factoryBorder.SetValue(Border.BorderThicknessProperty,new Thickness(3.0))
factoryBorder.SetValue(Border.BackgroundProperty,SystemColors.ControlLightBrush)

let factoryContent = new FrameworkElementFactory(typeof<ContentPresenter>)
factoryContent.Name <- "content"

factoryContent.SetValue(ContentPresenter.ContentProperty,
new TemplateBindingExtension(Button.ContentProperty))
factoryContent.SetValue(ContentPresenter.MarginProperty,
new TemplateBindingExtension(Button.PaddingProperty))
factoryBorder.AppendChild(factoryContent)
template.VisualTree <- factoryBorder

// Define a new Trigger when IsMouseOver is true
// Associate a Setter with the Trigger to change the
// CornerRadius property of the "border" element
// along with a Setter to change the FontStyle.
// And add the trigger to the template.
template.Triggers.Add
(
let trig = new Trigger(Property=UIElement.IsMouseOverProperty,Value=true)
trig.Setters.Add
(new Setter(Property=Border.CornerRadiusProperty,
Value=new CornerRadius(24.0),
TargetName="border"))
trig.Setters.Add
(new Setter(Property=Control.FontStyleProperty,
Value=FontStyles.Italic))
trig
)

// Define trigger for IsPressed and add triger to template
template.Triggers.Add
(
let trig = new Trigger(Property=Button.IsPressedProperty,Value=true)
trig.Setters.Add
(new Setter(Property=Border.BackgroundProperty,
Value=SystemColors.ControlDarkBrush,
TargetName="border"))
trig
)

let btn = new Button(Template=template,
Content="Button with Custom Template",
Padding=new Thickness(20.0),
FontSize=48.0,
HorizontalAlignment=HorizontalAlignment.Center,
VerticalAlignment=VerticalAlignment.Center)

btn.Click.Add(fun _ -> MessageBox.Show("You clicked the button",window.Title) |> ignore)
window.Content <- btn


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

ColorCell, ColorGrid, and SelectColor

#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.Controls.Primitives
open System.Windows.Input
open System.Windows.Media
open System.Windows.Shapes

let mutable initIsSelectedProperty : DependencyProperty = null
let mutable initIsHighlightedProperty : DependencyProperty = null
let sizeCell = new Size(20.0,20.0)
//
// From Chapter 11 - Color Cell
//
type ColorCell (clr:Color) = class
inherit FrameworkElement() as base

let visColor = new DrawingVisual()
let brush = (new SolidColorBrush(clr) :> Brush)

// create a drawing visual that contains a rectangle
do
// Retrieve the DrawingContext in order to create new drawing content
let dc = visColor.RenderOpen()

//let rect = new Rect(new Point(4.0,4.0),new Size(12.0,12.0))

// This following lines of code doesn't work for some reason, I tried in F# interactive also
// let rect = new Rect(new Point(0.0,0.0),sizeCell)
// rect.Inflate(4.0,4.0)
// Below is the manually computed one
let rect = new Rect(new Point (4.0,4.0),new Point(16.0,16.0))
let pen = new Pen(SystemColors.ControlTextBrush,1.0)
dc.DrawRectangle(brush,pen,rect)
// Persist the drawing content
dc.Close()

// Add VisualChild is necessary for event routing!
base.AddVisualChild(visColor)
base.AddLogicalChild(visColor)

static member IsSelectedProperty =
if initIsSelectedProperty = null then
initIsSelectedProperty <- DependencyProperty.Register
("IsSelected", typeof<bool>,typeof<ColorCell>,
new FrameworkPropertyMetadata
(false, FrameworkPropertyMetadataOptions.AffectsRender))
initIsSelectedProperty
else
initIsSelectedProperty

static member IsHighlightedProperty =
if initIsHighlightedProperty = null then
initIsHighlightedProperty <- DependencyProperty.Register
("IsHighlighted", typeof<bool>,typeof<ColorCell>,
new FrameworkPropertyMetadata
(false, FrameworkPropertyMetadataOptions.AffectsRender))
initIsHighlightedProperty
else
initIsHighlightedProperty

member this.IsSelected
with get() = (this.GetValue(ColorCell.IsSelectedProperty) :?> bool)
and set (value :bool) =
this.SetValue(ColorCell.IsSelectedProperty,value)

member this.IsHighlighted
with get() = (this.GetValue(ColorCell.IsHighlightedProperty) :?> bool)
and set (value :bool) =
this.SetValue(ColorCell.IsHighlightedProperty,value)

member this.Brush
with get() = brush

override this.VisualChildrenCount
with get() = 1

override this.GetVisualChild(index:int) =
if index > 0 then
raise (new ArgumentOutOfRangeException("index"))
visColor :> Visual

override this.MeasureOverride(sizeAvailable:Size) =
sizeCell

override this.OnRender (dc:DrawingContext) =
let rect = new Rect(new Point(0.0,0.0),this.RenderSize)
rect.Inflate(-1.0,-1.0)
let pen = new Pen(SystemColors.HighlightBrush,1.0)
if this.IsHighlighted then
dc.DrawRectangle(SystemColors.ControlDarkBrush,pen,rect)|>ignore
elif this.IsSelected then
dc.DrawRectangle(SystemColors.ControlLightBrush,pen,rect)|>ignore
else
dc.DrawRectangle(Brushes.Transparent,null,rect)|>ignore

override this.OnMouseLeave (args:MouseEventArgs) =
base.OnMouseLeave(args)
this.IsHighlighted <- false

end
//
// From Chapter 11 - Color Grid
//
type ColorGrid() = class
inherit Control() as base
let yNum = 5
let xNum = 8

let unigrid = new UniformGrid(Background=SystemColors.WindowBrush,
Columns=xNum)
let bord = new Border(BorderBrush=SystemColors.ControlDarkBrush,
BorderThickness=new Thickness(1.0),
Child=unigrid)
let getColor strColor = (typeof<Colors>).GetProperty(strColor).GetValue(null,null) :?> Color
let strColors =
["Black";"Brown";"DarkGreen";"MidnightBlue";"Navy";"DarkBlue";"Indigo";"DimGray";
"DarkRed";"OrangeRed";"Olive";"Green";"Teal";"Blue";"SlateGray";"Gray";
"Red";"Orange";"YellowGreen";"SeaGreen";"Aqua";"LightBlue";"Violet";"DarkGray"
"Pink";"Gold";"Yellow";"Lime";"Turquoise";"SkyBlue";"Plum";"LightGray";
"LightPink";"Tan";"LightYellow";"LightGreen";"LightCyan";
"LightSkyBlue";"Lavender";"White"]

let cells = List.map (fun strColor -> new ColorCell(getColor strColor)) strColors

let clearHighlights () = List.iter (fun (c:ColorCell) -> (c.IsHighlighted <- false)) cells

let clearSelection () = List.iter (fun (c:ColorCell) -> (c.IsSelected <- false)) cells

// Selected color changed event
let triggerColorChanged, colorChanged = IEvent.create<EventArgs>()

do base.AddVisualChild(bord)
base.AddLogicalChild(bord)
cells |> List.iteri (fun i cell ->
unigrid.Children.Add(cell)|>ignore
let tip = new ToolTip()
tip.Content <- List.nth strColors i
cell.ToolTip <- tip)

member this.SelectedColorChanged = colorChanged

member this.SelectedColor
with get() =
try
let cell = List.find (fun (cell:ColorCell) -> cell.IsSelected = true) cells
(cell.Brush :?> SolidColorBrush).Color
with _ -> Colors.Black

override this.VisualChildrenCount
with get() = 1

override this.GetVisualChild(index:int) =
if index > 0 then
raise (new ArgumentOutOfRangeException("index"))
bord :> Visual

override this.MeasureOverride(sizeAvailable:Size) =
bord.Measure(sizeAvailable)
bord.DesiredSize

override this.ArrangeOverride(sizeFinal:Size) =
bord.Arrange(new Rect(new Point(0.0,0.0),sizeFinal))
sizeFinal

override this.OnMouseMove(args:MouseEventArgs) =
base.OnMouseMove(args)
match args.Source with
| :? ColorCell ->
clearHighlights()
let cell = args.Source :?> ColorCell
cell.IsHighlighted <- true
| _ -> ()


// Changed the behavior to change color on mouse down instead of up.
// Seemed more intuitive to me.
override this.OnMouseDown (args:MouseButtonEventArgs) =
base.OnMouseDown(args)
match args.Source with
| :? ColorCell ->
clearSelection ()
let cell = args.Source :?> ColorCell
cell.IsSelected <- true
triggerColorChanged(EventArgs.Empty)
| _ -> ()
this.Focus() |>ignore

override this.OnKeyDown (args:KeyEventArgs) =
base.OnKeyDown(args)

// Some more behavioral changes...instead of jumping out of
// the color palette with the left/right keys...I would stay
// inside the color palette

// Move to cell based on provided algorithm
let movecell algo =
let index = List.find_index (fun (cell:ColorCell) -> cell.IsHighlighted = true) cells
let cell = List.nth cells (algo index)
clearHighlights ()
cell.IsHighlighted <- true

match args.Key with
| Key.Home -> let cell = List.hd cells
clearHighlights ()
let mycells = cells
cell.IsHighlighted <- true
cell.Focus()|>ignore
| Key.End -> let cell = List.rev cells |> List.hd
clearHighlights ()
let mycells = cells
cell.IsHighlighted <- true
cell.Focus()|>ignore
| Key.Down ->
try
let moveAlgorithm index = (index+8) % cells.Length
movecell moveAlgorithm
args.Handled <- true
with _ ->()
| Key.Up ->
try
let moveAlgorithm index = if index<8 then cells.Length+index-8 else (index-8)
movecell moveAlgorithm
args.Handled <- true
with _ ->()
| Key.Right ->
try
let moveAlgorithm index = (index+1) % cells.Length
movecell moveAlgorithm
args.Handled <- true
with _ ->()
| Key.Left ->
try
let moveAlgorithm index = if index=0 then cells.Length-1 else (index-1)
movecell moveAlgorithm
args.Handled <- true
with _ ->()
| Key.Enter | Key.Space ->
try
let cell = List.find (fun (cell:ColorCell) -> cell.IsHighlighted = true) cells
clearSelection()
cell.IsSelected <- true
triggerColorChanged(EventArgs.Empty)
cell.Focus() |> ignore
with _ ->()

| _ -> ()
end

//
// From Chapter 11 - Color Grid
//
let createButton () =
new Button(Content="Do-nothing button\nto test tabbing",
Margin=new Thickness(24.0),
HorizontalAlignment=HorizontalAlignment.Center,
VerticalAlignment=VerticalAlignment.Center)

let clrgrid = new ColorGrid(Margin=new Thickness(24.0),
HorizontalAlignment=HorizontalAlignment.Center,
VerticalAlignment=VerticalAlignment.Center)

let stack = new StackPanel(Orientation=Orientation.Horizontal)
stack.Children.Add(createButton ())
stack.Children.Add(clrgrid)
stack.Children.Add(createButton ())
let window = new Window(Title="Select Color",
Content=stack,
SizeToContent = SizeToContent.WidthAndHeight)

clrgrid.SelectedColorChanged.Add
(fun _ ->
window.Background <- new SolidColorBrush(clrgrid.SelectedColor)
);

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

1 comment:

Anonymous said...

Hi John,

You will probably need to use "let mutable rect = ..." to get Inflate to work.

This method mutates a .NET value type inplace, hence the value must be marked mutable for this to function correctly. Operations that do this are extremely rare, and in most cases (e.g. mutating fields or properties directly) the F# compiler will give you a warning that this is necessary, however not for arbitrary method calls.

Kind regards
Don Syme