Friday, January 11, 2008

Learning WPF with F# - Custom Panels

In my last blog posting, I decided to label my blog with the title "Learning F# with WPF" and later decided that title doesn't make sense semantically even if most people would understand what I mean. So I later revert the title back to "Learning WPF with F#". Unbeknownst to me, Dr. Don Syme kindly featured my blogs in his blog. All of a sudden my blog jumped from page hits of less then a hundred for the entire existence of this blog to several hundred page hits in just a couple of days. It made me realize that my blog posting is no longer my personal journal to be changed on a whim and apologize if my title changed may have caused any confusion with what Dr. Don Syme have posted.

I'm continuing to experiment with the different expressions of F# language. I suppose when this language becomes popular enough, someone will be able to put together a book on programming idioms in F#. Here are more code examples from working on Chapter 12 of Petzold's book Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation.


UniformGridAlmost & DuplicateUniformGrid


#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

// Again, plagarizing some functions from Haskell's Prelude
let max x y = if x <= y then y else x
// Find the max value of a sequence
let seqmax s = Seq.fold1 max s

let mutable initColumnsProperty : DependencyProperty = null

//
// From Chapter 12 - UniformGridAlmost
//
type UniformGridAlmost () = class
inherit Panel() as base

static member ColumnsProperty =
if initColumnsProperty = null then
initColumnsProperty <- DependencyProperty.Register
("Columns", typeof<int>,typeof<UniformGridAlmost>,
new FrameworkPropertyMetadata
(1, FrameworkPropertyMetadataOptions.AffectsMeasure))
initColumnsProperty
else
initColumnsProperty

member this.Columns
with get() = (this.GetValue(UniformGridAlmost.ColumnsProperty) :?> int)
and set (value :int) =
this.SetValue(UniformGridAlmost.ColumnsProperty,value)


member this.Rows
with get() = (this.InternalChildren.Count + this.Columns -1)/ this.Columns


override this.MeasureOverride(sizeAvailable:Size) =

// aliases for column/row count in float type
let colf = Float.of_int this.Columns
let rowf = Float.of_int this.Rows

let sizeChild = new Size(sizeAvailable.Width/ colf,
sizeAvailable.Height/ rowf)

let internalChildren = this.InternalChildren |> Seq.untyped_to_typed
// Alternatively, I could have written the above expression as
// let internalChildren = Seq.untyped_to_typed this.InternalChildren
// However, I'm leaning toward this first form because it makes it's easier
// to pick out that I'm working with this.InternalChildren

// Call Measure for each child....
internalChildren |> Seq.iter (fun (child:UIElement) -> child.Measure(sizeChild))

// Get max width/height
let maxwidth = internalChildren
|> Seq.map (fun (child:UIElement) -> child.DesiredSize.Width)
|> seqmax

let maxheight = internalChildren
|> Seq.map (fun (child:UIElement) -> child.DesiredSize.Height)
|> seqmax

new Size(colf * maxwidth, rowf * maxheight)


override this.ArrangeOverride(sizeFinal:Size) =
// aliases for column/row count in float type
let colf = Float.of_int this.Columns
let rowf = Float.of_int this.Rows

let sizeChild = new Size(sizeFinal.Width/ colf,
sizeFinal.Height/ rowf)

let internalChildren = this.InternalChildren |> Seq.untyped_to_typed

internalChildren
|> Seq.iteri (fun i (child:UIElement) ->
let row = Int32.to_float (i / this.Columns)
let col = Int32.to_float (i % this.Columns)
let rectChild = new Rect(new Point(col*sizeChild.Width,
row*sizeChild.Height),
sizeChild)
child.Arrange(rectChild))
sizeFinal


end
//
// From Chapter 12 - DuplicateUniformGrid
//
let window = new Window(Title="Duplicate Uniform Grid",
SizeToContent = SizeToContent.WidthAndHeight)

let unigrid = new UniformGridAlmost(Columns=5)
let rand = new Random()

// Fill grid with randomly-sized buttons
seq {0..47} |> Seq.iteri (fun i _ ->
let name = "Button" + Int32.to_string i
let sizemodifier = Float.of_int (rand.Next(10) )
let btn = new Button(Name=name,
Content=name)
btn.FontSize <- btn.FontSize + sizemodifier
btn.Click.Add(fun _ -> MessageBox.Show(btn.Name + " has been clicked",window.Title)|>ignore)
unigrid.Children.Add(btn)|>ignore)

window.Content <- unigrid


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

CanvasClone & PaintOnCanvasClone

#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.Shapes

let mutable initLeftProperty : DependencyProperty = null
let mutable initTopProperty : DependencyProperty = null

//
// From Chapter 12 - CanvasClone
//
type CanvasClone () = class
inherit Panel() as base

static member LeftProperty =
if initLeftProperty = null then
initLeftProperty <- DependencyProperty.RegisterAttached
("Left", typeof<double>,typeof<CanvasClone>,
new FrameworkPropertyMetadata
(0.0, FrameworkPropertyMetadataOptions.AffectsParentArrange))
initLeftProperty
else
initLeftProperty

static member TopProperty =
if initTopProperty = null then
initTopProperty <- DependencyProperty.RegisterAttached
("Top", typeof<double>,typeof<CanvasClone>,
new FrameworkPropertyMetadata
(0.0, FrameworkPropertyMetadataOptions.AffectsParentArrange))
initTopProperty
else
initTopProperty

static member SetLeft (depobj:DependencyObject, value:double) =
depobj.SetValue(CanvasClone.LeftProperty,value)

static member GetLeft (depobj:DependencyObject) =
depobj.GetValue(CanvasClone.LeftProperty) :?> double

static member SetTop (depobj:DependencyObject, value:double) =
depobj.SetValue(CanvasClone.TopProperty,value)

static member GetTop (depobj:DependencyObject) =
depobj.GetValue(CanvasClone.TopProperty) :?> double

override this.MeasureOverride(sizeAvailable:Size) =
let posInf = new Size(Double.PositiveInfinity,Double.PositiveInfinity)
let internalChildren = this.InternalChildren |> Seq.untyped_to_typed
internalChildren
|> Seq.iter (fun (child:UIElement) -> child.Measure(posInf))
base.MeasureOverride(sizeAvailable)

override this.ArrangeOverride(sizeFinal:Size) =
let internalChildren = this.InternalChildren |> Seq.untyped_to_typed
internalChildren |> Seq.iter (fun (child:UIElement) ->
child.Arrange(new Rect(new Point(CanvasClone.GetLeft(child),
CanvasClone.GetTop(child)),
child.DesiredSize)))
sizeFinal
end
//
// From Chapter 12 - PaintOnCanvasClone
//
let canv = new CanvasClone()
let brushes = [Brushes.Red; Brushes.Green; Brushes.Blue; ]
brushes |> Seq.iteri (fun i brush ->
let location = Float.of_int (100*(i+1))
let rect = new Rectangle(Fill=brush,
Width = 200.0,
Height = 200.0)
canv.Children.Add(rect)|>ignore
CanvasClone.SetLeft(rect, location)
CanvasClone.SetTop(rect, location))

let window = new Window(Title="Paint on Canvas Clone",
Content=canv)

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

DiagonalPanel & DiagonalizeTheButtons

#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

let mutable initBackgroundProperty : DependencyProperty = null

//
// From Chapter 12 - DiagonalPanel
//
type DiagonalPanel () = class
inherit FrameworkElement() as base

let children = new ResizeArray<UIElement>()

// Sum the widths of all the child elements
let sumwidth elements =
Seq.map (fun (c:UIElement) -> c.DesiredSize.Width) (ResizeArray.to_list elements)
|> Seq.fold1 (+)

// Sum the heights of all the child elements
let sumheight elements =
Seq.map (fun (c:UIElement) -> c.DesiredSize.Height) (ResizeArray.to_list elements)
|> Seq.fold1 (+)

static member BackgroundProperty =
if initBackgroundProperty = null then
initBackgroundProperty <- DependencyProperty.Register
("Background", typeof<Brush>,typeof<DiagonalPanel>,
new FrameworkPropertyMetadata
(null, FrameworkPropertyMetadataOptions.AffectsRender))
initBackgroundProperty
else
initBackgroundProperty

member this.Background
with get() = (this.GetValue(DiagonalPanel.BackgroundProperty) :?> Brush)
and set (value :Brush) =
this.SetValue(DiagonalPanel.BackgroundProperty,value)

member this.Add (el:UIElement) =
children.Add(el)
this.AddVisualChild(el)
this.AddLogicalChild(el)
this.InvalidateMeasure()

member this.Remove (el:UIElement) =
children.Remove(el) |> ignore
this.RemoveVisualChild(el)
this.RemoveLogicalChild(el)
this.InvalidateMeasure()

member this.IndexOf (el:UIElement) =
children.IndexOf(el)

override this.VisualChildrenCount =
children.Count

override this.GetVisualChild (index:int) =
if index >= children.Count then
raise (new ArgumentOutOfRangeException("index"))
let child = ResizeArray.get children index
child :> Visual

override this.MeasureOverride(sizeAvailable:Size) =
children |> ResizeArray.iter (fun child ->
child.Measure(new Size(Double.PositiveInfinity,Double.PositiveInfinity)))
new Size(sumwidth children ,sumheight children)


// Whew! This seems a lot more complicated then the C# version.
// In theory, writing functionally should scale better on multicore/multi cpu
// machines and allow the compiler to better perform parallel processing optimization.
override this.ArrangeOverride(sizeFinal:Size) =
// Precalculate all starting points and sizes with sequence expressions
let rec arrangelist (pt:Point) (elements:UIElement list) =
match elements with
| child :: remainder ->
let w = child.DesiredSize.Width * (sizeFinal.Width/(sumwidth children))
let h = child.DesiredSize.Height *(sizeFinal.Height/(sumheight children))
(pt,new Size(w,h),child) :: arrangelist (new Point(pt.X+w,pt.Y+h)) remainder
| [] -> []

arrangelist (new Point(0.0,0.0)) (ResizeArray.to_list children)
|> Seq.iter (fun item ->
match item with
| (pt,size,child) -> child.Arrange(new Rect(pt,size)))
sizeFinal

override this.OnRender (dc:DrawingContext) =
dc.DrawRectangle(this.Background, null, new Rect(new Point(0.0,0.0),this.RenderSize))

end
//
// From Chapter 12 - DiagonalizeTheButtons
//
let pnl = new DiagonalPanel()
let rand = new Random()
[1..5] |> List.iter (fun i ->
let btn = new Button(Content="Button Number " + Int32.to_string i)
btn.FontSize <- btn.FontSize + Int32.to_float (rand.Next(20))
pnl.Add(btn))

let window = new Window(Title="Diagonalize the Buttons", Content=pnl)

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

RadialPanelOrientation, RadialPanel & CircleTheButtons

#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

// Again, plagarizing some functions from Haskell's Prelude
let max x y = if x <= y then y else x
// Find the max value of a sequence
let seqmax s = Seq.fold1 max s
//
// From Chapter 12 - RadialPanelOrientation enum
//
type RadialPanelOrientation =
| ByWidth
| ByHeight

let mutable initOrientationProperty : DependencyProperty = null

//
// From Chapter 12 - RadialPanel
//
type RadialPanel () = class
inherit Panel() as base

let mutable showPieLines = false

// I'm caching all the following calculations. Not sure what is
// the rule of thumb in terms of what should be cached and what
// should be calculated on the fly.
let mutable radius = 0.0
let mutable din = 0.0 // innerEdgeFromCenter
let mutable dout = 0.0 // outerEdgeFromCenter
let mutable sizeLargest = new Size(0.0,0.0)

static member OrientationProperty =
if initOrientationProperty = null then
initOrientationProperty <- DependencyProperty.Register
("Orientation", typeof<RadialPanelOrientation>,typeof<RadialPanel>,
new FrameworkPropertyMetadata
(RadialPanelOrientation.ByWidth, FrameworkPropertyMetadataOptions.AffectsMeasure))
initOrientationProperty
else
initOrientationProperty

member this.ShowPieLines
with get() = showPieLines
and set (value) =
if value <> showPieLines then this.InvalidateVisual()
showPieLines <- value

member this.Orientation
with get() = (this.GetValue(RadialPanel.OrientationProperty) :?> RadialPanelOrientation)
and set (value :Brush) =
this.SetValue(RadialPanel.OrientationProperty,value)

override this.MeasureOverride(sizeAvailable:Size) =
let measure (children:seq<UIElement>) =
let maxSize = new Size(Double.PositiveInfinity,Double.PositiveInfinity)

// Call measure for each child...
children |> Seq.iter (fun (x:UIElement) -> x.Measure(maxSize))

let maxwidth = children
|> Seq.map (fun (x:UIElement) -> x.DesiredSize.Width)
|> seqmax
let maxheight = children
|> Seq.map (fun (child:UIElement) -> child.DesiredSize.Height)
|> seqmax

// AngleEach in radians
let angle = Math.PI / Int32.to_float (Seq.length children)

// Cache this calculation
sizeLargest <- new Size(maxwidth,maxheight)

match this.Orientation with
| ByWidth ->
din <- maxwidth / (2.0 * Math.Tan(angle))
dout <- din + maxheight
radius <- Math.Sqrt((maxwidth/2.0)**2.0 + dout**2.0)
new Size(2.0*radius,2.0*radius)
| ByHeight ->
din <- maxheight / (2.0 * Math.Tan(angle))
dout <- din + maxwidth
radius <- Math.Sqrt((maxheight/2.0)**2.0 + dout**2.0)
new Size(2.0*radius,2.0*radius)

match this.InternalChildren.Count with
| 0 -> new Size(0.0,0.0)
| _ -> measure (Seq.untyped_to_typed this.InternalChildren)

override this.ArrangeOverride(sizeFinal:Size) =

// Generate a list of angles
let genAngles n = seq { for i in 0 .. (n-1) do
yield Int32.to_float(i)*360.0/Int32.to_float(n) }

// Generate a list of tuples (angle,child). There's probably a better
// term for the variable "elements" to make it more clear.
let elements = Seq.untyped_to_typed this.InternalChildren
|> Seq.zip (genAngles this.InternalChildren.Count)

let ptCenter = new Point(sizeFinal.Width / 2.0, sizeFinal.Height/2.0)
let multiplier = Math.Min(sizeFinal.Width / (2.0*radius),
sizeFinal.Height / (2.0*radius))

let arrangeRect =
match this.Orientation with
| ByWidth ->
new Rect(ptCenter.X - multiplier * sizeLargest.Width / 2.0,
ptCenter.Y - multiplier * dout,
multiplier * sizeLargest.Width,
multiplier * sizeLargest.Height)
| ByHeight ->
new Rect(ptCenter.X + multiplier * din,
ptCenter.Y - multiplier * sizeLargest.Height/2.0,
multiplier * sizeLargest.Width,
multiplier * sizeLargest.Height)

// Use pattern matching to extract the angle and child from each tuple in elements
elements |> Seq.iter (fun element ->
match element with
| (angle,child:UIElement) ->
child.RenderTransform <- Transform.Identity
child.Arrange(arrangeRect)
let pt = this.TranslatePoint(ptCenter,child)
child.RenderTransform <- new RotateTransform(angle,pt.X,pt.Y) )
sizeFinal

override this.OnRender (dc:DrawingContext) =
base.OnRender(dc)

// Display option pie lines
if showPieLines then
let ptCenter = new Point(this.RenderSize.Width / 2.0,
this.RenderSize.Height / 2.0)
let multiplier = Math.Min(this.RenderSize.Width / (2.0*radius),
this.RenderSize.Height / (2.0*radius))
let pen = new Pen(SystemColors.WindowTextBrush,1.0)
pen.DashStyle <- DashStyles.Dash

// Display circle
dc.DrawEllipse(null,pen,ptCenter, multiplier* radius, multiplier * radius)

let genAngles n = seq { for i in 0 .. (n-1) do
let floati = Int32.to_float(i)
let floatn = Int32.to_float(n)
let offset = Math.PI/floatn
let angle = offset + 2.0 * Math.PI * floati/floatn
yield angle }
let angles =
match this.Orientation with
// Add 90 degrees (in radians) if orientation is ByWidth
| ByWidth -> Seq.map (fun x -> x+ Math.PI/2.0)
(genAngles this.InternalChildren.Count)
| ByHeight -> genAngles this.InternalChildren.Count

let elements = Seq.untyped_to_typed this.InternalChildren
|> Seq.zip angles

// Draw each line. Again I prefer the using the forward pipe operator
// to make it clear that we're working with elements.
elements |> Seq.iter (fun element ->
match element with
| (angle,child:UIElement) ->
let pt2 = new Point(ptCenter.X + multiplier * radius * Math.Cos(angle),
ptCenter.Y + multiplier * radius * Math.Sin(angle))
dc.DrawLine(pen, ptCenter,pt2))
end
//
// From Chapter 12 - CircleTheButtons
//
let pnl = new RadialPanel(Orientation=ByHeight,
ShowPieLines=true)
let rand = new Random()
[1..10] |> List.iter (fun i ->
let btn = new Button(Content="Button Number + " + Int32.to_string i)
btn.FontSize <- btn.FontSize + Int32.to_float (rand.Next(10))
pnl.Children.Add(btn) |>ignore)

let window = new Window(Title="Circle the Buttons",Content=pnl)

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


No comments: